// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp // Copyright (c) 2025-2026 MobileGL-Dev // Licensed under the GNU Lesser General Public License v3.0: // https://www.gnu.org/licenses/gpl-3.0.txt // https://www.gnu.org/licenses/lgpl-3.0.txt // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header #include "VulkanRenderer.h" #include "VertexInputStateFactory.h" #include "VertexInputStateBuilder.h" #include "MG_State/GLState/Core.h" #include "MG_State/GLState/ProgramState/ProgramObject.h" #include "MG_State/GLState/ProgramState/ShaderObject.h" #include "MG_State/GLState/SamplerState/SamplerObject.h" #include "MG_State/GLState/TextureState/TextureObject.h" #include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h" #include "MG_Util/Converters/GLToMG/TextureEnumConverter.h" #include "MG_Util/Converters/MGToVk/RenderStateEnumConverter.h" #include "MG_Util/Metrics/TextureMetrics.h" #include namespace MobileGL::MG_Backend::DirectVulkan { static VkPipelineColorBlendAttachmentState MakeColorBlendAttachmentState( Bool blendEnable, VkBlendFactor srcColorBlendFactor, VkBlendFactor dstColorBlendFactor, VkBlendFactor srcAlphaBlendFactor, VkBlendFactor dstAlphaBlendFactor, VkColorComponentFlags colorWriteMask) { VkPipelineColorBlendAttachmentState attachment{}; attachment.blendEnable = blendEnable ? VK_TRUE : VK_FALSE; attachment.srcColorBlendFactor = srcColorBlendFactor; attachment.dstColorBlendFactor = dstColorBlendFactor; attachment.colorBlendOp = VK_BLEND_OP_ADD; attachment.srcAlphaBlendFactor = srcAlphaBlendFactor; attachment.dstAlphaBlendFactor = dstAlphaBlendFactor; attachment.alphaBlendOp = VK_BLEND_OP_ADD; attachment.colorWriteMask = colorWriteMask; return attachment; } static Bool ShouldUseTransientVertexIndexBuffer(const MG_State::GLState::BufferObject& bufferObject) { switch (bufferObject.GetUsage()) { case BufferUsage::StreamDraw: case BufferUsage::StreamRead: case BufferUsage::StreamCopy: case BufferUsage::DynamicDraw: case BufferUsage::DynamicRead: case BufferUsage::DynamicCopy: return true; case BufferUsage::StaticDraw: case BufferUsage::StaticRead: case BufferUsage::StaticCopy: default: return false; } } static VkColorComponentFlags GetSupportedColorWriteMaskForComponentCount(SizeT componentCount) { switch (componentCount) { case 1: return VK_COLOR_COMPONENT_R_BIT; case 2: return VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT; case 3: return VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT; case 4: return VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; default: MOBILEGL_ASSERT(false, "GetSupportedColorWriteMaskForComponentCount: unsupported componentCount=%zu", componentCount); return 0; } } enum class NumericDomain { Unknown, FloatLike, Sint, Uint, }; static NumericDomain GetNumericDomainForShaderValueType(GLenum glType) { switch (glType) { case GL_FLOAT: case GL_FLOAT_VEC2: case GL_FLOAT_VEC3: case GL_FLOAT_VEC4: return NumericDomain::FloatLike; case GL_INT: case GL_INT_VEC2: case GL_INT_VEC3: case GL_INT_VEC4: return NumericDomain::Sint; case GL_UNSIGNED_INT: case GL_UNSIGNED_INT_VEC2: case GL_UNSIGNED_INT_VEC3: case GL_UNSIGNED_INT_VEC4: return NumericDomain::Uint; default: return NumericDomain::Unknown; } } static SizeT GetComponentCountForShaderValueType(GLenum glType) { switch (glType) { case GL_FLOAT: case GL_INT: case GL_UNSIGNED_INT: return 1; case GL_FLOAT_VEC2: case GL_INT_VEC2: case GL_UNSIGNED_INT_VEC2: return 2; case GL_FLOAT_VEC3: case GL_INT_VEC3: case GL_UNSIGNED_INT_VEC3: return 3; case GL_FLOAT_VEC4: case GL_INT_VEC4: case GL_UNSIGNED_INT_VEC4: return 4; default: return 0; } } static NumericDomain GetNumericDomainForVertexFormat(VkFormat format) { switch (format) { case VK_FORMAT_R32_SFLOAT: case VK_FORMAT_R32G32_SFLOAT: case VK_FORMAT_R32G32B32_SFLOAT: case VK_FORMAT_R32G32B32A32_SFLOAT: case VK_FORMAT_R16_SNORM: case VK_FORMAT_R16G16_SNORM: case VK_FORMAT_R16G16B16_SNORM: case VK_FORMAT_R16G16B16A16_SNORM: case VK_FORMAT_R16_UNORM: case VK_FORMAT_R16G16_UNORM: case VK_FORMAT_R16G16B16_UNORM: case VK_FORMAT_R16G16B16A16_UNORM: case VK_FORMAT_R16_SSCALED: case VK_FORMAT_R16G16_SSCALED: case VK_FORMAT_R16G16B16_SSCALED: case VK_FORMAT_R16G16B16A16_SSCALED: case VK_FORMAT_R16_USCALED: case VK_FORMAT_R16G16_USCALED: case VK_FORMAT_R16G16B16_USCALED: case VK_FORMAT_R16G16B16A16_USCALED: case VK_FORMAT_R8_SNORM: case VK_FORMAT_R8G8_SNORM: case VK_FORMAT_R8G8B8_SNORM: case VK_FORMAT_R8G8B8A8_SNORM: case VK_FORMAT_R8_UNORM: case VK_FORMAT_R8G8_UNORM: case VK_FORMAT_R8G8B8_UNORM: case VK_FORMAT_R8G8B8A8_UNORM: case VK_FORMAT_R8_SSCALED: case VK_FORMAT_R8G8_SSCALED: case VK_FORMAT_R8G8B8_SSCALED: case VK_FORMAT_R8G8B8A8_SSCALED: case VK_FORMAT_R8_USCALED: case VK_FORMAT_R8G8_USCALED: case VK_FORMAT_R8G8B8_USCALED: case VK_FORMAT_R8G8B8A8_USCALED: return NumericDomain::FloatLike; case VK_FORMAT_R32_SINT: case VK_FORMAT_R32G32_SINT: case VK_FORMAT_R32G32B32_SINT: case VK_FORMAT_R32G32B32A32_SINT: case VK_FORMAT_R16_SINT: case VK_FORMAT_R16G16_SINT: case VK_FORMAT_R16G16B16_SINT: case VK_FORMAT_R16G16B16A16_SINT: case VK_FORMAT_R8_SINT: case VK_FORMAT_R8G8_SINT: case VK_FORMAT_R8G8B8_SINT: case VK_FORMAT_R8G8B8A8_SINT: return NumericDomain::Sint; case VK_FORMAT_R32_UINT: case VK_FORMAT_R32G32_UINT: case VK_FORMAT_R32G32B32_UINT: case VK_FORMAT_R32G32B32A32_UINT: case VK_FORMAT_R16_UINT: case VK_FORMAT_R16G16_UINT: case VK_FORMAT_R16G16B16_UINT: case VK_FORMAT_R16G16B16A16_UINT: case VK_FORMAT_R8_UINT: case VK_FORMAT_R8G8_UINT: case VK_FORMAT_R8G8B8_UINT: case VK_FORMAT_R8G8B8A8_UINT: return NumericDomain::Uint; default: return NumericDomain::Unknown; } } static Bool TryCoerceVertexFormatNumericDomain(VkFormat sourceFormat, NumericDomain targetDomain, VkFormat& outFormat) { const NumericDomain sourceDomain = GetNumericDomainForVertexFormat(sourceFormat); if (sourceDomain == targetDomain || targetDomain == NumericDomain::Unknown) { outFormat = sourceFormat; return true; } if (sourceDomain == NumericDomain::FloatLike) { return false; } switch (sourceFormat) { case VK_FORMAT_R32_SINT: if (targetDomain == NumericDomain::Uint) { outFormat = VK_FORMAT_R32_UINT; return true; } return false; case VK_FORMAT_R32G32_SINT: if (targetDomain == NumericDomain::Uint) { outFormat = VK_FORMAT_R32G32_UINT; return true; } return false; case VK_FORMAT_R32G32B32_SINT: if (targetDomain == NumericDomain::Uint) { outFormat = VK_FORMAT_R32G32B32_UINT; return true; } return false; case VK_FORMAT_R32G32B32A32_SINT: if (targetDomain == NumericDomain::Uint) { outFormat = VK_FORMAT_R32G32B32A32_UINT; return true; } return false; case VK_FORMAT_R32_UINT: if (targetDomain == NumericDomain::Sint) { outFormat = VK_FORMAT_R32_SINT; return true; } return false; case VK_FORMAT_R32G32_UINT: if (targetDomain == NumericDomain::Sint) { outFormat = VK_FORMAT_R32G32_SINT; return true; } return false; case VK_FORMAT_R32G32B32_UINT: if (targetDomain == NumericDomain::Sint) { outFormat = VK_FORMAT_R32G32B32_SINT; return true; } return false; case VK_FORMAT_R32G32B32A32_UINT: if (targetDomain == NumericDomain::Sint) { outFormat = VK_FORMAT_R32G32B32A32_SINT; return true; } return false; case VK_FORMAT_R16_SINT: outFormat = targetDomain == NumericDomain::Uint ? VK_FORMAT_R16_UINT : VK_FORMAT_R16_SSCALED; return true; case VK_FORMAT_R16G16_SINT: outFormat = targetDomain == NumericDomain::Uint ? VK_FORMAT_R16G16_UINT : VK_FORMAT_R16G16_SSCALED; return true; case VK_FORMAT_R16G16B16_SINT: outFormat = targetDomain == NumericDomain::Uint ? VK_FORMAT_R16G16B16_UINT : VK_FORMAT_R16G16B16_SSCALED; return true; case VK_FORMAT_R16G16B16A16_SINT: outFormat = targetDomain == NumericDomain::Uint ? VK_FORMAT_R16G16B16A16_UINT : VK_FORMAT_R16G16B16A16_SSCALED; return true; case VK_FORMAT_R16_UINT: outFormat = targetDomain == NumericDomain::Sint ? VK_FORMAT_R16_SINT : VK_FORMAT_R16_USCALED; return true; case VK_FORMAT_R16G16_UINT: outFormat = targetDomain == NumericDomain::Sint ? VK_FORMAT_R16G16_SINT : VK_FORMAT_R16G16_USCALED; return true; case VK_FORMAT_R16G16B16_UINT: outFormat = targetDomain == NumericDomain::Sint ? VK_FORMAT_R16G16B16_SINT : VK_FORMAT_R16G16B16_USCALED; return true; case VK_FORMAT_R16G16B16A16_UINT: outFormat = targetDomain == NumericDomain::Sint ? VK_FORMAT_R16G16B16A16_SINT : VK_FORMAT_R16G16B16A16_USCALED; return true; case VK_FORMAT_R8_SINT: outFormat = targetDomain == NumericDomain::Uint ? VK_FORMAT_R8_UINT : VK_FORMAT_R8_SSCALED; return true; case VK_FORMAT_R8G8_SINT: outFormat = targetDomain == NumericDomain::Uint ? VK_FORMAT_R8G8_UINT : VK_FORMAT_R8G8_SSCALED; return true; case VK_FORMAT_R8G8B8_SINT: outFormat = targetDomain == NumericDomain::Uint ? VK_FORMAT_R8G8B8_UINT : VK_FORMAT_R8G8B8_SSCALED; return true; case VK_FORMAT_R8G8B8A8_SINT: outFormat = targetDomain == NumericDomain::Uint ? VK_FORMAT_R8G8B8A8_UINT : VK_FORMAT_R8G8B8A8_SSCALED; return true; case VK_FORMAT_R8_UINT: outFormat = targetDomain == NumericDomain::Sint ? VK_FORMAT_R8_SINT : VK_FORMAT_R8_USCALED; return true; case VK_FORMAT_R8G8_UINT: outFormat = targetDomain == NumericDomain::Sint ? VK_FORMAT_R8G8_SINT : VK_FORMAT_R8G8_USCALED; return true; case VK_FORMAT_R8G8B8_UINT: outFormat = targetDomain == NumericDomain::Sint ? VK_FORMAT_R8G8B8_SINT : VK_FORMAT_R8G8B8_USCALED; return true; case VK_FORMAT_R8G8B8A8_UINT: outFormat = targetDomain == NumericDomain::Sint ? VK_FORMAT_R8G8B8A8_SINT : VK_FORMAT_R8G8B8A8_USCALED; return true; default: return false; } } static NumericDomain GetNumericDomainForTextureInternalFormat(TextureInternalFormat format) { switch (format) { case TextureInternalFormat::R8I: case TextureInternalFormat::R16I: case TextureInternalFormat::R32I: case TextureInternalFormat::RG8I: case TextureInternalFormat::RG16I: case TextureInternalFormat::RG32I: case TextureInternalFormat::RGB8I: case TextureInternalFormat::RGB16I: case TextureInternalFormat::RGB32I: case TextureInternalFormat::RGBA8I: case TextureInternalFormat::RGBA16I: case TextureInternalFormat::RGBA32I: return NumericDomain::Sint; case TextureInternalFormat::R8UI: case TextureInternalFormat::R16UI: case TextureInternalFormat::R32UI: case TextureInternalFormat::RG8UI: case TextureInternalFormat::RG16UI: case TextureInternalFormat::RG32UI: case TextureInternalFormat::RGB8UI: case TextureInternalFormat::RGB16UI: case TextureInternalFormat::RGB32UI: case TextureInternalFormat::RGBA8UI: case TextureInternalFormat::RGBA16UI: case TextureInternalFormat::RGBA32UI: case TextureInternalFormat::RGB10A2UI: return NumericDomain::Uint; case TextureInternalFormat::DepthComponent: case TextureInternalFormat::DepthComponent16: case TextureInternalFormat::DepthComponent24: case TextureInternalFormat::DepthComponent32: case TextureInternalFormat::DepthComponent32F: case TextureInternalFormat::Depth24Stencil8: case TextureInternalFormat::Depth32FStencil8: case TextureInternalFormat::DepthStencil: return NumericDomain::Unknown; default: return NumericDomain::FloatLike; } } static Uint32 BuildVertexInputAttributeMask(const Vector& attributes) { Uint32 attributeMask = 0; for (const auto& attribute : attributes) { if (attribute.location < 32) { attributeMask |= (1u << attribute.location); } } return attributeMask; } static Bool TryGetCurrentVertexAttributeFormat(GLenum glType, VkFormat& outFormat) { switch (glType) { case GL_FLOAT: outFormat = VK_FORMAT_R32_SFLOAT; return true; case GL_FLOAT_VEC2: outFormat = VK_FORMAT_R32G32_SFLOAT; return true; case GL_FLOAT_VEC3: outFormat = VK_FORMAT_R32G32B32_SFLOAT; return true; case GL_FLOAT_VEC4: outFormat = VK_FORMAT_R32G32B32A32_SFLOAT; return true; case GL_INT: outFormat = VK_FORMAT_R32_SINT; return true; case GL_INT_VEC2: outFormat = VK_FORMAT_R32G32_SINT; return true; case GL_INT_VEC3: outFormat = VK_FORMAT_R32G32B32_SINT; return true; case GL_INT_VEC4: outFormat = VK_FORMAT_R32G32B32A32_SINT; return true; case GL_UNSIGNED_INT: outFormat = VK_FORMAT_R32_UINT; return true; case GL_UNSIGNED_INT_VEC2: outFormat = VK_FORMAT_R32G32_UINT; return true; case GL_UNSIGNED_INT_VEC3: outFormat = VK_FORMAT_R32G32B32_UINT; return true; case GL_UNSIGNED_INT_VEC4: outFormat = VK_FORMAT_R32G32B32A32_UINT; return true; default: return false; } } static Bool TryGetCurrentVertexAttributeUploadPayload( const MG_State::GLState::CurrentVertexAttributeValue& currentValue, GLenum glType, VkFormat& outFormat, const void*& outData, VkDeviceSize& outSize) { switch (glType) { case GL_FLOAT: outFormat = VK_FORMAT_R32_SFLOAT; outData = currentValue.floatValue.data(); outSize = sizeof(Float); return true; case GL_FLOAT_VEC2: outFormat = VK_FORMAT_R32G32_SFLOAT; outData = currentValue.floatValue.data(); outSize = sizeof(Float) * 2; return true; case GL_FLOAT_VEC3: outFormat = VK_FORMAT_R32G32B32_SFLOAT; outData = currentValue.floatValue.data(); outSize = sizeof(Float) * 3; return true; case GL_FLOAT_VEC4: outFormat = VK_FORMAT_R32G32B32A32_SFLOAT; outData = currentValue.floatValue.data(); outSize = sizeof(Float) * 4; return true; case GL_INT: outFormat = VK_FORMAT_R32_SINT; outData = currentValue.intValue.data(); outSize = sizeof(Int32); return true; case GL_INT_VEC2: outFormat = VK_FORMAT_R32G32_SINT; outData = currentValue.intValue.data(); outSize = sizeof(Int32) * 2; return true; case GL_INT_VEC3: outFormat = VK_FORMAT_R32G32B32_SINT; outData = currentValue.intValue.data(); outSize = sizeof(Int32) * 3; return true; case GL_INT_VEC4: outFormat = VK_FORMAT_R32G32B32A32_SINT; outData = currentValue.intValue.data(); outSize = sizeof(Int32) * 4; return true; case GL_UNSIGNED_INT: outFormat = VK_FORMAT_R32_UINT; outData = currentValue.uintValue.data(); outSize = sizeof(Uint32); return true; case GL_UNSIGNED_INT_VEC2: outFormat = VK_FORMAT_R32G32_UINT; outData = currentValue.uintValue.data(); outSize = sizeof(Uint32) * 2; return true; case GL_UNSIGNED_INT_VEC3: outFormat = VK_FORMAT_R32G32B32_UINT; outData = currentValue.uintValue.data(); outSize = sizeof(Uint32) * 3; return true; case GL_UNSIGNED_INT_VEC4: outFormat = VK_FORMAT_R32G32B32A32_UINT; outData = currentValue.uintValue.data(); outSize = sizeof(Uint32) * 4; return true; default: return false; } } static const char* VkImageLayoutToString(VkImageLayout layout) { switch (layout) { case VK_IMAGE_LAYOUT_UNDEFINED: return "VK_IMAGE_LAYOUT_UNDEFINED"; case VK_IMAGE_LAYOUT_GENERAL: return "VK_IMAGE_LAYOUT_GENERAL"; case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: return "VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL"; case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: return "VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL"; case VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL: return "VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL"; case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: return "VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL"; case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: return "VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL"; case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: return "VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL"; case VK_IMAGE_LAYOUT_PRESENT_SRC_KHR: return "VK_IMAGE_LAYOUT_PRESENT_SRC_KHR"; case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL: return "VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL"; case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL: return "VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL"; default: return "VK_IMAGE_LAYOUT_OTHER"; } } static Bool ActiveRenderPassUsesTexture(const ActiveRenderPassInfo& activeRenderPass, const MG_State::GLState::ITextureObject& texture) { for (const auto& trackedAttachment : activeRenderPass.trackedAttachmentLayouts) { if (trackedAttachment.target != TrackedAttachmentTarget::Texture) { continue; } if (trackedAttachment.texture == &texture) { return true; } } return false; } static void RecordClearBufferError(const char* func, ErrorCode code, const char* message) { MG_State::pGLContext->RecordError(code, MakeUnique("DirectVulkan", func, message)); } static void RecordTextureCopyError(const char* func, ErrorCode code, const char* message) { MG_State::pGLContext->RecordError(code, MakeUnique("DirectVulkan", func, message)); } static Bool IsValidSampledImageLayout(VkImageLayout layout) { switch (layout) { case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: case VK_IMAGE_LAYOUT_GENERAL: case VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL: case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL: case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL: return true; default: return false; } } namespace { static constexpr Uint32 kDescriptorSetsPerFrame = 64; static constexpr Uint kHiddenBlitProgramId = 0xFFFFFFF0u; static constexpr Uint kHiddenBlitVertexShaderId = 0xFFFFFFF1u; static constexpr Uint kHiddenBlitFragmentShaderId = 0xFFFFFFF2u; static constexpr Uint kHiddenBlitNearestSamplerId = 0xFFFFFFF3u; static constexpr Uint kHiddenBlitLinearSamplerId = 0xFFFFFFF4u; static constexpr Uint kHiddenDepthMipmapProgramId = 0xFFFFFFF5u; static constexpr Uint kHiddenDepthMipmapVertexShaderId = 0xFFFFFFF6u; static constexpr Uint kHiddenDepthMipmapFragmentShaderId = 0xFFFFFFF7u; static constexpr const char* kFullscreenTriangleVertexShaderSource = R"(#version 460 core uniform vec4 uSrcRect; uniform vec4 uDstRect; uniform int uSurfaceTransform; layout(location = 0) out vec2 vTexCoord; vec2 ApplySurfaceTransform(vec2 position, int transform) { vec2 p = position; p.y = -p.y; if (transform == 1) { p = vec2(-p.y, p.x); } else if (transform == 2) { p = -p; } else if (transform == 3) { p = vec2(p.y, -p.x); } return p; } void main() { const vec2 uvTri[3] = vec2[]( vec2(0.0, 0.0), vec2(2.0, 0.0), vec2(0.0, 2.0) ); vec2 uv = uvTri[gl_VertexID]; vec2 dst = uDstRect.xy + uv * uDstRect.zw; vec2 clip = dst * 2.0 - 1.0; clip = ApplySurfaceTransform(clip, uSurfaceTransform); gl_Position = vec4(clip, 0.0, 1.0); vTexCoord = uSrcRect.xy + uv * uSrcRect.zw; } )"; static constexpr const char* kBlitFragmentShaderSource = R"(#version 460 core layout(binding = 0) uniform sampler2D uSource; layout(location = 0) in vec2 vTexCoord; layout(location = 0) out vec4 outColor; void main() { outColor = texture(uSource, vTexCoord); } )"; static constexpr const char* kDepthMipmapFragmentShaderSource = R"(#version 460 core layout(binding = 0) uniform sampler2D uSource; layout(location = 0) in vec2 vTexCoord; uniform ivec2 uSrcTexelSize; void main() { ivec2 srcBase = ivec2(gl_FragCoord.xy) * 2; ivec2 srcMax = max(uSrcTexelSize - ivec2(1), ivec2(0)); float depth0 = texelFetch(uSource, clamp(srcBase + ivec2(0, 0), ivec2(0), srcMax), 0).r; float depth1 = texelFetch(uSource, clamp(srcBase + ivec2(1, 0), ivec2(0), srcMax), 0).r; float depth2 = texelFetch(uSource, clamp(srcBase + ivec2(0, 1), ivec2(0), srcMax), 0).r; float depth3 = texelFetch(uSource, clamp(srcBase + ivec2(1, 1), ivec2(0), srcMax), 0).r; gl_FragDepth = 0.25 * (depth0 + depth1 + depth2 + depth3); } )"; static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) { Int maxDimension = std::max( baseTexelSize.x(), std::max(baseTexelSize.y(), std::max(baseTexelSize.z(), 1))); Uint32 mipLevelCount = 1; while (maxDimension > 1) { maxDimension = std::max(maxDimension / 2, 1); ++mipLevelCount; } return mipLevelCount; } static IntVec3 ComputeMipTexelSize(const IntVec3& baseTexelSize, Uint32 relativeMipLevel) { const Int width = std::max(baseTexelSize.x() >> static_cast(relativeMipLevel), 1); const Int height = std::max(baseTexelSize.y() >> static_cast(relativeMipLevel), 1); const Int depth = std::max(baseTexelSize.z() >> static_cast(relativeMipLevel), 1); return {width, height, depth}; } static Bool EnsureGenerateMipmapStorageAllocated(::MobileGL::MG_State::GLState::TextureObjectMipmap& texture, ::MobileGL::TextureUploadTarget uploadTarget, Uint32 baseMipLevel) { const Uint32 existingMipLevelCount = static_cast(texture.GetMipmapLevelCount()); if (existingMipLevelCount <= baseMipLevel) { return false; } const IntVec3 baseTexelSize = texture.GetMipmapTexelSize(uploadTarget, baseMipLevel); const SizeT baseByteSize = texture.GetMipmapByteSize(uploadTarget, baseMipLevel); if (baseTexelSize.x() <= 0 || baseTexelSize.y() <= 0 || baseTexelSize.z() <= 0 || baseByteSize == 0) { return false; } const SizeT baseTexelCount = static_cast(baseTexelSize.x()) * static_cast(baseTexelSize.y()) * static_cast(baseTexelSize.z()); if (baseTexelCount == 0 || (baseByteSize % baseTexelCount) != 0) { return false; } const SizeT bytesPerTexel = baseByteSize / baseTexelCount; const Uint32 requiredMipLevelCount = baseMipLevel + ComputeFullMipLevelCount(baseTexelSize); if (existingMipLevelCount >= requiredMipLevelCount) { return true; } for (Uint32 level = existingMipLevelCount; level < requiredMipLevelCount; ++level) { const IntVec3 levelTexelSize = ComputeMipTexelSize(baseTexelSize, level - baseMipLevel); const SizeT levelByteSize = bytesPerTexel * static_cast(levelTexelSize.x()) * static_cast(levelTexelSize.y()) * static_cast(levelTexelSize.z()); texture.AllocateStorage(uploadTarget, level, {levelTexelSize, levelByteSize}); texture.MarkStorageDirty(uploadTarget, level, false); } return true; } static VkImageLayout ResolveGenerateMipmapFinalLayout(VkImageAspectFlags aspectMask) { return (aspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0 ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; } enum class BlitSurfaceTransform : Uint32 { Identity = 0, Rotate90 = 1, Rotate180 = 2, Rotate270 = 3, }; struct BlitImageBinding { VkImage image = VK_NULL_HANDLE; VkImageLayout* trackedLayout = nullptr; VkImageAspectFlags aspectMask = VK_IMAGE_ASPECT_NONE; IntVec2 extent = {0, 0}; Uint32 mipLevel = 0; Uint32 mipLevelCount = 1; const char* label = nullptr; }; static Uint32 ComputeMaxProgramBindings(const VkPhysicalDeviceProperties& properties) { const auto& limits = properties.limits; static constexpr Uint32 kMinProgramBindings = 16; static constexpr Uint32 kMaxProgramBindingsCap = 256; const Uint32 maxCombinedImageSamplers = std::min(limits.maxPerStageDescriptorSamplers, limits.maxDescriptorSetSamplers); const Uint32 maxSampledImages = std::min(limits.maxPerStageDescriptorSampledImages, limits.maxDescriptorSetSampledImages); const Uint32 maxDynamicUniformBuffers = std::min(limits.maxPerStageDescriptorUniformBuffers, limits.maxDescriptorSetUniformBuffersDynamic); Uint32 maxBindings = limits.maxPerStageResources; maxBindings = std::min(maxBindings, maxCombinedImageSamplers); maxBindings = std::min(maxBindings, maxSampledImages + maxDynamicUniformBuffers); maxBindings = std::max(kMinProgramBindings, maxBindings); maxBindings = std::min(kMaxProgramBindingsCap, maxBindings); return maxBindings; } static void GetImageTransitionSourceState(VkImageLayout oldLayout, VkPipelineStageFlags& outSrcStageMask, VkAccessFlags& outSrcAccessMask) { switch (oldLayout) { case VK_IMAGE_LAYOUT_UNDEFINED: case VK_IMAGE_LAYOUT_PRESENT_SRC_KHR: outSrcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; outSrcAccessMask = 0; break; case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: outSrcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; outSrcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; break; case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: outSrcStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; outSrcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; break; case VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL: case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL: case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL: outSrcStageMask = VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT; outSrcAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT; break; case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: outSrcStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT; outSrcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; break; case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: outSrcStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT; outSrcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; break; case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: outSrcStageMask = VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT; outSrcAccessMask = VK_ACCESS_SHADER_READ_BIT; break; default: outSrcStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT; outSrcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; break; } } static void GetImageTransitionDestinationState(VkImageLayout newLayout, VkPipelineStageFlags& outDstStageMask, VkAccessFlags& outDstAccessMask) { switch (newLayout) { case VK_IMAGE_LAYOUT_UNDEFINED: outDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; outDstAccessMask = 0; break; case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: outDstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; outDstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; break; case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: outDstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; outDstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; break; case VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL: case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL: case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL: case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: outDstStageMask = VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT; outDstAccessMask = VK_ACCESS_SHADER_READ_BIT; break; case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: outDstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT; outDstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; break; case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: outDstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT; outDstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; break; case VK_IMAGE_LAYOUT_PRESENT_SRC_KHR: outDstStageMask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT; outDstAccessMask = VK_ACCESS_MEMORY_READ_BIT; break; default: outDstStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT; outDstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; break; } } static VkImageAspectFlags GetSwapchainDepthStencilAspectMask(const SwapchainObject& swapchainObject) { VkImageAspectFlags aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; switch (swapchainObject.GetDepthStencilFormat()) { case VK_FORMAT_D24_UNORM_S8_UINT: case VK_FORMAT_D32_SFLOAT_S8_UINT: aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT; break; default: break; } return aspectMask; } static FramebufferAttachmentType ResolveFramebufferCopyAttachmentType( const MG_State::GLState::FramebufferObject& fbo, Bool isReadFramebuffer, VkImageAspectFlags aspectMask) { if ((aspectMask & VK_IMAGE_ASPECT_COLOR_BIT) != 0) { return isReadFramebuffer ? fbo.GetReadBuffer() : fbo.GetDrawBuffers()[0]; } if ((aspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) { return FramebufferAttachmentType::Depth; } if ((aspectMask & VK_IMAGE_ASPECT_STENCIL_BIT) != 0) { return FramebufferAttachmentType::Stencil; } return FramebufferAttachmentType::None; } static Bool ResolveColorBlitBinding(MG_State::GLState::FramebufferObject& fbo, Bool isReadFramebuffer, Uint32 swapchainImageIndex, SwapchainObject& swapchainObject, VkTextureManager& textureManager, BlitImageBinding& outBinding) { const Bool isDefaultFbo = (&fbo == MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO.get()); const FramebufferAttachmentType attachmentType = isReadFramebuffer ? fbo.GetReadBuffer() : fbo.GetDrawBuffers()[0]; if (attachmentType < FramebufferAttachmentType::Color0 || attachmentType > FramebufferAttachmentType::Color31) { MGLOG_E("BlitFramebuffer only supports color attachments right now (attachment=%d)", static_cast(attachmentType)); return false; } const auto& attachment = fbo.GetAttachment(attachmentType); if (!attachment.IsComplete()) { MGLOG_E("BlitFramebuffer skipped: %s framebuffer color attachment is incomplete", isReadFramebuffer ? "read" : "draw"); return false; } if (attachment.IsRenderbuffer()) { MGLOG_E("BlitFramebuffer skipped: renderbuffer attachments are not supported yet"); return false; } if (!attachment.IsTexture()) { MGLOG_E("BlitFramebuffer skipped: unsupported framebuffer attachment type"); return false; } auto* texture = attachment.GetTexture().get(); MOBILEGL_ASSERT(texture != nullptr, "ResolveColorBlitBinding: texture attachment is null"); outBinding.label = isReadFramebuffer ? "read" : "draw"; if (isDefaultFbo) { outBinding.image = swapchainObject.GetImage(swapchainImageIndex); outBinding.trackedLayout = nullptr; outBinding.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; const auto extent = swapchainObject.GetExtent(); outBinding.extent = {static_cast(extent.width), static_cast(extent.height)}; outBinding.mipLevel = 0; outBinding.mipLevelCount = 1; return true; } auto* resource = textureManager.SyncTextureAndGetDescriptor(*texture); if (resource == nullptr) { MGLOG_E("BlitFramebuffer skipped: failed to sync %s framebuffer textureId=%d", outBinding.label, texture->GetExternalIndex()); return false; } if ((resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) { MGLOG_E("BlitFramebuffer skipped: %s framebuffer attachment textureId=%d is not a color image", outBinding.label, texture->GetExternalIndex()); return false; } outBinding.image = resource->image; outBinding.trackedLayout = &resource->layout; outBinding.aspectMask = resource->aspect; const auto attachmentExtent = attachment.GetSize(); outBinding.extent = {attachmentExtent.x(), attachmentExtent.y()}; outBinding.mipLevel = static_cast(std::max(attachment.GetTextureLevel(), 0)); outBinding.mipLevelCount = resource->mipLevels; return true; } static Bool ResolveFramebufferBlitBinding(MG_State::GLState::FramebufferObject& fbo, Bool isReadFramebuffer, Uint32 swapchainImageIndex, SwapchainObject& swapchainObject, VkTextureManager& textureManager, VkImageAspectFlags requiredAspectMask, BlitImageBinding& outBinding) { const Bool isDefaultFbo = (&fbo == MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO.get()); const auto attachmentType = ResolveFramebufferCopyAttachmentType(fbo, isReadFramebuffer, requiredAspectMask); if (attachmentType == FramebufferAttachmentType::None) { MGLOG_E("BlitFramebuffer skipped: unsupported aspect mask=0x%x", static_cast(requiredAspectMask)); return false; } outBinding.label = isReadFramebuffer ? "read" : "draw"; if (isDefaultFbo) { const auto extent = swapchainObject.GetExtent(); outBinding.extent = {static_cast(extent.width), static_cast(extent.height)}; outBinding.mipLevel = 0; outBinding.mipLevelCount = 1; outBinding.trackedLayout = nullptr; if ((requiredAspectMask & VK_IMAGE_ASPECT_COLOR_BIT) != 0) { outBinding.image = swapchainObject.GetImage(swapchainImageIndex); outBinding.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; return true; } const VkImageAspectFlags swapchainAspectMask = GetSwapchainDepthStencilAspectMask(swapchainObject); if ((swapchainAspectMask & requiredAspectMask) != requiredAspectMask) { MGLOG_E("BlitFramebuffer skipped: swapchain depth image missing required aspect mask=0x%x", static_cast(requiredAspectMask)); return false; } outBinding.image = swapchainObject.GetDepthStencilImage(swapchainImageIndex); outBinding.aspectMask = requiredAspectMask; return true; } const auto& attachment = fbo.GetAttachment(attachmentType); if (!attachment.IsComplete()) { MGLOG_E("BlitFramebuffer skipped: %s framebuffer attachment is incomplete", outBinding.label); return false; } if (attachment.IsRenderbuffer()) { MGLOG_E("BlitFramebuffer skipped: renderbuffer attachments are not supported yet"); return false; } if (!attachment.IsTexture()) { MGLOG_E("BlitFramebuffer skipped: unsupported framebuffer attachment type"); return false; } auto* texture = attachment.GetTexture().get(); MOBILEGL_ASSERT(texture != nullptr, "ResolveFramebufferBlitBinding: texture attachment is null"); auto* resource = textureManager.SyncTextureAndGetDescriptor(*texture); if (resource == nullptr) { MGLOG_E("BlitFramebuffer skipped: failed to sync %s framebuffer textureId=%d", outBinding.label, texture->GetExternalIndex()); return false; } if ((resource->aspect & requiredAspectMask) != requiredAspectMask) { MGLOG_E("BlitFramebuffer skipped: %s framebuffer attachment textureId=%d is missing aspect mask=0x%x", outBinding.label, texture->GetExternalIndex(), static_cast(requiredAspectMask)); return false; } outBinding.image = resource->image; outBinding.trackedLayout = &resource->layout; outBinding.aspectMask = requiredAspectMask; const auto attachmentExtent = attachment.GetSize(); outBinding.extent = {attachmentExtent.x(), attachmentExtent.y()}; outBinding.mipLevel = static_cast(std::max(attachment.GetTextureLevel(), 0)); outBinding.mipLevelCount = resource->mipLevels; return true; } static Bool ResolveTextureCopyDestinationBinding(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel, VkTextureManager& textureManager, BlitImageBinding& outBinding) { auto* resource = textureManager.SyncTextureAndGetDescriptor(texture); if (resource == nullptr) { MGLOG_E("CopyTexSubImage2D skipped: failed to sync destination textureId=%d", texture.GetExternalIndex()); return false; } const VkImageAspectFlags copyAspectMask = resource->aspect & (VK_IMAGE_ASPECT_COLOR_BIT | VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT); if (copyAspectMask == 0) { MGLOG_E("CopyTexSubImage2D skipped: destination textureId=%d uses unsupported aspect mask=0x%x", texture.GetExternalIndex()); return false; } if (mipLevel >= resource->mipLevels) { MGLOG_E("CopyTexSubImage2D skipped: destination textureId=%d mip=%u out of range (mips=%u)", texture.GetExternalIndex(), mipLevel, resource->mipLevels); return false; } outBinding.image = resource->image; outBinding.trackedLayout = &resource->layout; outBinding.aspectMask = copyAspectMask; outBinding.extent = { static_cast(std::max(1u, resource->extent.width >> mipLevel)), static_cast(std::max(1u, resource->extent.height >> mipLevel))}; outBinding.mipLevel = mipLevel; outBinding.mipLevelCount = 1; outBinding.label = "destination texture"; return true; } static Bool ResolveTextureCopySourceBinding(MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, SwapchainObject& swapchainObject, VkTextureManager& textureManager, VkImageAspectFlags requiredAspectMask, BlitImageBinding& outBinding) { const Bool isDefaultFbo = (&fbo == MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO.get()); const auto attachmentType = ResolveFramebufferCopyAttachmentType(fbo, true, requiredAspectMask); if (attachmentType == FramebufferAttachmentType::None) { MGLOG_E("CopyTexSubImage2D skipped: unsupported source aspect mask=0x%x", static_cast(requiredAspectMask)); return false; } outBinding.label = "read"; if (isDefaultFbo) { const auto extent = swapchainObject.GetExtent(); outBinding.extent = {static_cast(extent.width), static_cast(extent.height)}; outBinding.mipLevel = 0; outBinding.mipLevelCount = 1; outBinding.trackedLayout = nullptr; if ((requiredAspectMask & VK_IMAGE_ASPECT_COLOR_BIT) != 0) { outBinding.image = swapchainObject.GetImage(swapchainImageIndex); outBinding.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; return true; } const VkImageAspectFlags swapchainAspectMask = GetSwapchainDepthStencilAspectMask(swapchainObject); if ((swapchainAspectMask & requiredAspectMask) != requiredAspectMask) { MGLOG_E("CopyTexSubImage2D skipped: swapchain depth image missing required aspect mask=0x%x", static_cast(requiredAspectMask)); return false; } outBinding.image = swapchainObject.GetDepthStencilImage(swapchainImageIndex); outBinding.aspectMask = requiredAspectMask; return true; } const auto& attachment = fbo.GetAttachment(attachmentType); if (!attachment.IsComplete()) { MGLOG_E("CopyTexSubImage2D skipped: read framebuffer attachment %d is incomplete", static_cast(attachmentType)); return false; } if (attachment.IsRenderbuffer()) { MGLOG_E("CopyTexSubImage2D skipped: renderbuffer read attachments are not supported yet"); return false; } if (!attachment.IsTexture()) { MGLOG_E("CopyTexSubImage2D skipped: unsupported read framebuffer attachment type"); return false; } auto* texture = attachment.GetTexture().get(); MOBILEGL_ASSERT(texture != nullptr, "ResolveTextureCopySourceBinding: source texture attachment is null"); auto* resource = textureManager.SyncTextureAndGetDescriptor(*texture); if (resource == nullptr) { MGLOG_E("CopyTexSubImage2D skipped: failed to sync read framebuffer textureId=%d", texture->GetExternalIndex()); return false; } if ((resource->aspect & requiredAspectMask) != requiredAspectMask) { MGLOG_E("CopyTexSubImage2D skipped: read framebuffer textureId=%d aspect mask=0x%x does not satisfy requested mask=0x%x", texture->GetExternalIndex(), static_cast(resource->aspect), static_cast(requiredAspectMask)); return false; } outBinding.image = resource->image; outBinding.trackedLayout = &resource->layout; outBinding.aspectMask = requiredAspectMask; const auto attachmentExtent = attachment.GetSize(); outBinding.extent = {attachmentExtent.x(), attachmentExtent.y()}; outBinding.mipLevel = static_cast(std::max(attachment.GetTextureLevel(), 0)); outBinding.mipLevelCount = 1; return true; } static BlitSurfaceTransform ToBlitSurfaceTransform(VkSurfaceTransformFlagBitsKHR preTransform) { switch (preTransform) { case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR: return BlitSurfaceTransform::Rotate90; case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR: return BlitSurfaceTransform::Rotate180; case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR: return BlitSurfaceTransform::Rotate270; default: return BlitSurfaceTransform::Identity; } } } // namespace VkBool32 VulkanRenderer::DebugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, VkDebugUtilsMessageTypeFlagsEXT messageType, const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) { auto typeToString = [](VkDebugUtilsMessageTypeFlagsEXT messageType) { switch (messageType) { case VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT: return "General"; case VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT: return "Validation"; case VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT: return "Performance"; case VK_DEBUG_UTILS_MESSAGE_TYPE_DEVICE_ADDRESS_BINDING_BIT_EXT: return "DeviceAddressBinding"; default: return "Other"; } }; switch (messageSeverity) { case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT: MGLOG_E("Vulkan Debug: [%s] %s", typeToString(messageType), pCallbackData->pMessage); break; case VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT: MGLOG_W("Vulkan Debug: [%s] %s", typeToString(messageType), pCallbackData->pMessage); break; case VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT: MGLOG_I("Vulkan Debug: [%s] %s", typeToString(messageType), pCallbackData->pMessage); break; case VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT: MGLOG_D("Vulkan Debug: [%s] %s", typeToString(messageType), pCallbackData->pMessage); break; default: break; } return VK_FALSE; } VulkanRenderer::VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg) : m_window(window), m_config(cfg) { // Initialize(); } VulkanRenderer::~VulkanRenderer() { Shutdown(); } inline ProgramFactory::CompileOptionFlags GetShaderTransformFlags(VkSurfaceTransformFlagBitsKHR preTransform) { ProgramFactory::CompileOptionFlags flags = ProgramFactory::CompileOptionBit::PositionZRemap; const auto& currentDrawFBO = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); if (currentDrawFBO == MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO) { flags |= ProgramFactory::CompileOptionBit::PositionYFlip; switch (preTransform) { case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR: flags |= ProgramFactory::CompileOptionBit::SurfaceRotate90; break; case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR: flags |= ProgramFactory::CompileOptionBit::SurfaceRotate180; break; case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR: flags |= ProgramFactory::CompileOptionBit::SurfaceRotate270; break; default: break; } } return flags; } void VulkanRenderer::Initialize() { CreateInstance(); CreateSurface(); PickPhysicalDevice(); CreateLogicalDeviceAndQueues(); CreateAllocator(); CreateCommandPool(); VK_VERIFY(m_frameContext.Initialize(m_device, m_commandPool, m_config.MaxFramesInFlight), "CreateFrameContexts"); MGLOG_I("CreateFrameContexts completed"); m_deferredDepthMipmapCleanup.assign(m_frameContext.GetFrameCount(), {}); auto succeeded = false; succeeded = m_bufferManager.Initialize({ .allocator = m_allocator, .frameCount = m_frameContext.GetFrameCount(), .minUploadBytes = 4 * 1024 * 1024, .transientMemoryUsage = VMA_MEMORY_USAGE_AUTO, .transientAllocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT, .transientPersistentMapping = false, }); MOBILEGL_ASSERT(succeeded, "VkBufferManager initialization failed."); m_textureManager = MakeUnique(); MOBILEGL_ASSERT(m_textureManager != nullptr, "VkTextureManager creation failed."); succeeded = m_textureManager->Initialize( {m_device, m_physicalDevice.handle, m_allocator, m_commandPool, m_graphicsQueue, m_frameContext.GetFrameCount()}); MOBILEGL_ASSERT(succeeded, "VkTextureManager initialization failed."); m_clearManager = MakeUnique(); MOBILEGL_ASSERT(m_clearManager != nullptr, "VkClearManager creation failed."); succeeded = m_clearManager->Initialize(); MOBILEGL_ASSERT(succeeded, "VkClearManager initialization failed."); m_renderPassManager = MakeUnique(m_device, m_config, *m_clearManager, *m_textureManager, m_swapchainObject); MOBILEGL_ASSERT(m_renderPassManager != nullptr, "VkRenderPassManager creation failed."); succeeded = m_renderPassManager->Initialize(); MOBILEGL_ASSERT(succeeded, "VkRenderPassManager initialization failed."); const Uint32 maxProgramBindings = ComputeMaxProgramBindings(m_physicalDevice.properties); MGLOG_I("DirectVulkan: using %u program descriptor bindings", maxProgramBindings); RecreateSwapchain(); m_pipelineFactory = MakeUnique(m_device, m_config); MOBILEGL_ASSERT(m_pipelineFactory != nullptr, "PipelineFactory creation failed."); m_programFactory = MakeUnique(m_device, m_config, maxProgramBindings); MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed."); m_samplerManager = MakeUnique(); MOBILEGL_ASSERT(m_samplerManager != nullptr, "VkSamplerManager creation failed."); succeeded = m_samplerManager->Initialize({m_device, &m_config}); MOBILEGL_ASSERT(succeeded, "VkSamplerManager initialization failed."); succeeded = InitializeBlitResources(); MOBILEGL_ASSERT(succeeded, "Blit pipeline resource initialization failed."); succeeded = InitializeDepthMipmapResources(); MOBILEGL_ASSERT(succeeded, "Depth mipmap pipeline resource initialization failed."); m_uniformManager = MakeUnique(); MOBILEGL_ASSERT(m_uniformManager != nullptr, "UniformDescriptorBinder creation failed."); succeeded = m_uniformManager->Initialize( m_device, &m_bufferManager, m_programFactory.get(), m_physicalDevice.properties.limits.minUniformBufferOffsetAlignment, m_config.MaxFramesInFlight, maxProgramBindings, kDescriptorSetsPerFrame, m_textureManager.get(), m_samplerManager.get()); MOBILEGL_ASSERT(succeeded, "UniformDescriptorBinder initialization failed."); m_vertexInputStateFactory = MakeUnique(m_config); MOBILEGL_ASSERT(m_vertexInputStateFactory != nullptr, "VertexInputStateFactory creation failed."); // Prime the first frame so Render() always targets an acquired swapchain image. VK_VERIFY(m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired), "Initialize, WaitAndAcquireNextImage"); m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_transientVertexIndexBufferSlicesThisFrame.clear(); MGLOG_D("VulkanRenderer initialized"); } void VulkanRenderer::Shutdown() { VK_VERIFY(vkDeviceWaitIdle(m_device)); DestroyDeferredDepthMipmapCleanup(); m_pipelineFactory.reset(); ShutdownBlitResources(); ShutdownDepthMipmapResources(); if (m_samplerManager) { m_samplerManager->Shutdown(); m_samplerManager.reset(); } if (m_textureManager) { m_textureManager->Shutdown(); m_textureManager.reset(); } m_vertexInputStateFactory.reset(); m_bufferManager.Shutdown(); m_transientVertexIndexBufferSlicesThisFrame.clear(); m_frameContext.Destroy(m_device, m_commandPool); if (m_uniformManager) { m_uniformManager->Shutdown(); m_uniformManager.reset(); } m_programFactory.reset(); ShutdownSwapchain(); m_renderPassManager.reset(); if (m_clearManager) { m_clearManager->Shutdown(); m_clearManager.reset(); } if (m_commandPool != VK_NULL_HANDLE) { vkDestroyCommandPool(m_device, m_commandPool, nullptr); m_commandPool = VK_NULL_HANDLE; } DestroyAllocator(); if (m_device != VK_NULL_HANDLE) { vkDestroyDevice(m_device, nullptr); m_device = VK_NULL_HANDLE; } s_vkCmdDrawIndexedIndirectCount = nullptr; if (m_surface != VK_NULL_HANDLE) { vkDestroySurfaceKHR(m_instance, m_surface, nullptr); m_surface = VK_NULL_HANDLE; } if (m_debugMessenger != VK_NULL_HANDLE) { DestroyDebugMessenger(); m_debugMessenger = VK_NULL_HANDLE; } if (m_instance != VK_NULL_HANDLE) { vkDestroyInstance(m_instance, nullptr); m_instance = VK_NULL_HANDLE; } MGLOG_I("VulkanRenderer shut down completed"); } Bool VulkanRenderer::UploadAndBindVertexBuffers( VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao) { auto& vertexInputState = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao); const auto& program = *MG_State::pGLContext->GetCurrentProgram(); const auto transformFlags = GetShaderTransformFlags(m_swapchainObject.GetPreTransform()); const auto& programObj = m_programFactory->GetOrCreateProgram(program, transformFlags); const Uint32 activeAttribMask = programObj.activeVertexInputLocationMask; const Uint32 vertexInputAttribMask = BuildVertexInputAttributeMask(vertexInputState.attributes); const Uint32 missingAttribMask = activeAttribMask & ~vertexInputAttribMask; const auto bindingCount = vertexInputState.bindings.size() + static_cast(std::popcount(missingAttribMask)); Vector vkBuffers(bindingCount, VK_NULL_HANDLE); Vector vkOffsets(bindingCount, 0); auto findBufferByKey = [&](SizeT bufferKey) -> const MG_State::GLState::BufferObject* { const auto& attrs = vao.GetAllAttributes(); for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) { const auto& attr = attrs[location]; if (!attr.Buffer) { continue; } const auto* buffer = attr.Buffer.get(); if (reinterpret_cast(buffer) == bufferKey) { return buffer; } } return nullptr; }; for (SizeT binding = 0; binding < bindingCount; ++binding) { if (binding >= vertexInputState.bindings.size()) { break; } const SizeT bufferKey = vertexInputState.bindingBufferKeys[binding]; const MG_State::GLState::BufferObject* sourceBuffer = findBufferByKey(bufferKey); MOBILEGL_ASSERT(sourceBuffer != nullptr, "UploadAndBindVertexStreams failed to resolve source buffer"); auto sourceBufferShared = MG_State::pGLContext->GetBufferObject(sourceBuffer->GetExternalIndex()); MOBILEGL_ASSERT(sourceBufferShared != nullptr, "UploadAndBindVertexStreams failed to resolve shared source buffer"); BufferSlice slice{}; const Bool isDirty = (sourceBufferShared->GetChangeBits() & BufferChangeBits::DirtyBit); const Uint64 changeSerial = sourceBufferShared->GetChangeSerial(); const SizeT sourceSize = sourceBufferShared->GetSize(); auto cachedTransient = m_transientVertexIndexBufferSlicesThisFrame.find(sourceBufferShared.get()); if (cachedTransient != m_transientVertexIndexBufferSlicesThisFrame.end() && !isDirty && cachedTransient->second.changeSerial == changeSerial && cachedTransient->second.size == sourceSize) { slice = cachedTransient->second.slice; } else if (ShouldUseTransientVertexIndexBuffer(*sourceBufferShared) || isDirty) { const auto sourceData = sourceBufferShared->GetDataReadOnly(); if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), sourceData->data(), static_cast(sourceSize), 16, slice)) { MOBILEGL_ASSERT(false, "UploadAndBindVertexStreams skipped: failed to upload transient binding %zu", binding); return false; } m_transientVertexIndexBufferSlicesThisFrame[sourceBufferShared.get()] = { .slice = slice, .changeSerial = changeSerial, .size = sourceSize, }; m_bufferManager.DowngradeResidentBufferToTransient(sourceBufferShared); sourceBufferShared->ClearDirty(); } else { if (!m_bufferManager.SyncResidentBuffer(BufferKind::Vertex, sourceBufferShared, slice)) { MGLOG_E("UploadAndBindVertexStreams skipped: failed to sync resident binding %zu", binding); return false; } } vkBuffers[binding] = slice.buffer; vkOffsets[binding] = slice.offset; } SizeT syntheticBinding = vertexInputState.bindings.size(); for (Uint32 location = 0; location < 32; ++location) { if ((missingAttribMask & (1u << location)) == 0) { continue; } const auto glType = programObj.vertexInputTypes[location]; const auto& currentValue = MG_State::pGLContext->GetCurrentVertexAttribute(location); VkFormat format = VK_FORMAT_UNDEFINED; const void* sourceData = nullptr; VkDeviceSize sourceSize = 0; const Bool supported = TryGetCurrentVertexAttributeUploadPayload(currentValue, glType, format, sourceData, sourceSize); MOBILEGL_ASSERT(supported, "DirectVulkan does not support current generic vertex attribute type yet: program=%u location=%u type=0x%x", program.GetExternalIndex(), location, glType); BufferSlice slice{}; if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), sourceData, sourceSize, 16, slice)) { MOBILEGL_ASSERT(false, "UploadAndBindVertexStreams skipped: failed to upload current attribute binding for location %u", location); return false; } vkBuffers[syntheticBinding] = slice.buffer; vkOffsets[syntheticBinding] = slice.offset; ++syntheticBinding; } vkCmdBindVertexBuffers(commandBuffer, 0, static_cast(bindingCount), vkBuffers.data(), vkOffsets.data()); return true; } Bool VulkanRenderer::UploadAndBindIndexBuffer(FrameContext::FrameData& frame, const MG_State::GLState::VertexArrayObject& vao, const IndexBufferView* pIndexBufferView) { VkIndexType vkIndexType = VK_INDEX_TYPE_MAX_ENUM; switch (pIndexBufferView->indexType) { case GL_UNSIGNED_BYTE: MOBILEGL_ASSERT(m_indexTypeUint8ExtensionEnabled, "DrawElements with GL_UNSIGNED_BYTE requires VK_KHR_index_type_uint8 or VK_EXT_index_type_uint8"); vkIndexType = VK_INDEX_TYPE_UINT8; break; case GL_UNSIGNED_SHORT: vkIndexType = VK_INDEX_TYPE_UINT16; break; case GL_UNSIGNED_INT: vkIndexType = VK_INDEX_TYPE_UINT32; break; default: MGLOG_D("DrawElements skipped: index type %u is not supported yet", pIndexBufferView->indexType); return false; } const auto* indexBuffer = vao.GetIndexBufferBindingSlot().GetBoundObject().get(); MOBILEGL_ASSERT(indexBuffer != nullptr, "UploadAndBindIndexBuffer requires bound EBO"); const SizeT indexSize = MG_Util::GetGLTypeSize(pIndexBufferView->indexType); const SizeT indexDataSizeBytes = pIndexBufferView->indexByteSize; MOBILEGL_ASSERT(pIndexBufferView->indexByteOffset + indexDataSizeBytes <= indexBuffer->GetSize(), "DrawElements index range out of bounds"); BufferSlice slice{}; auto indexBufferShared = MG_State::pGLContext->GetBufferObject(indexBuffer->GetExternalIndex()); MOBILEGL_ASSERT(indexBufferShared != nullptr, "UploadAndBindIndexBuffer failed to resolve shared EBO"); const Bool isDirty = (indexBufferShared->GetChangeBits() & BufferChangeBits::DirtyBit); const Uint64 changeSerial = indexBufferShared->GetChangeSerial(); const SizeT indexBufferSize = indexBufferShared->GetSize(); auto cachedTransient = m_transientVertexIndexBufferSlicesThisFrame.find(indexBufferShared.get()); if (cachedTransient != m_transientVertexIndexBufferSlicesThisFrame.end() && !isDirty && cachedTransient->second.changeSerial == changeSerial && cachedTransient->second.size == indexBufferSize) { slice = cachedTransient->second.slice; vkCmdBindIndexBuffer(frame.commandBuffer, slice.buffer, slice.offset + static_cast(pIndexBufferView->indexByteOffset), vkIndexType); return true; } if (ShouldUseTransientVertexIndexBuffer(*indexBufferShared) || isDirty) { const auto indexData = indexBufferShared->GetDataReadOnly(); MOBILEGL_ASSERT(indexData != nullptr && !indexData->empty(), "DrawElements requires non-empty EBO data"); if (!m_bufferManager.UploadTransient(BufferKind::Index, m_frameContext.GetCurrentFrameIndex(), indexData->data(), static_cast(indexBufferSize), indexSize, slice)) { MOBILEGL_ASSERT(false, "DrawElements skipped: failed to prepare transient index buffer"); return false; } m_transientVertexIndexBufferSlicesThisFrame[indexBufferShared.get()] = { .slice = slice, .changeSerial = changeSerial, .size = indexBufferSize, }; m_bufferManager.DowngradeResidentBufferToTransient(indexBufferShared); indexBufferShared->ClearDirty(); vkCmdBindIndexBuffer(frame.commandBuffer, slice.buffer, slice.offset + static_cast(pIndexBufferView->indexByteOffset), vkIndexType); return true; } if (!m_bufferManager.SyncResidentBuffer(BufferKind::Index, indexBufferShared, slice)) { MGLOG_E("DrawElements skipped: failed to sync resident index buffer"); return false; } vkCmdBindIndexBuffer(frame.commandBuffer, slice.buffer, slice.offset + static_cast(pIndexBufferView->indexByteOffset), vkIndexType); return true; } Bool VulkanRenderer::InitializeBlitResources() { ShutdownBlitResources(); auto vertexShader = MakeShared(ShaderStage::Vertex, kHiddenBlitVertexShaderId); vertexShader->SetShaderSource(kFullscreenTriangleVertexShaderSource); vertexShader->Compile(); if (!vertexShader->GetCompileStatus()) { MGLOG_E("InitializeBlitResources failed: vertex shader compile error: %s", vertexShader->GetInfoLog().c_str()); return false; } auto fragmentShader = MakeShared(ShaderStage::Fragment, kHiddenBlitFragmentShaderId); fragmentShader->SetShaderSource(kBlitFragmentShaderSource); fragmentShader->Compile(); if (!fragmentShader->GetCompileStatus()) { MGLOG_E("InitializeBlitResources failed: fragment shader compile error: %s", fragmentShader->GetInfoLog().c_str()); return false; } m_blitResources.program = MakeShared(kHiddenBlitProgramId); m_blitResources.program->AttachShader(vertexShader); m_blitResources.program->AttachShader(fragmentShader); m_blitResources.program->Link(false); if (!m_blitResources.program->GetLinkStatus()) { MGLOG_E("InitializeBlitResources failed: program link error: %s", m_blitResources.program->GetInfoLog().c_str()); return false; } m_blitResources.srcRectLocation = m_blitResources.program->GetUniformLocation("uSrcRect"); m_blitResources.dstRectLocation = m_blitResources.program->GetUniformLocation("uDstRect"); m_blitResources.surfaceTransformLocation = m_blitResources.program->GetUniformLocation("uSurfaceTransform"); MOBILEGL_ASSERT(m_blitResources.srcRectLocation >= 0, "InitializeBlitResources: missing uSrcRect"); MOBILEGL_ASSERT(m_blitResources.dstRectLocation >= 0, "InitializeBlitResources: missing uDstRect"); MOBILEGL_ASSERT(m_blitResources.surfaceTransformLocation >= 0, "InitializeBlitResources: missing uSurfaceTransform"); MOBILEGL_ASSERT(m_blitResources.program->GetUBOSize() > 0, "InitializeBlitResources: blit program global UBO is empty"); MOBILEGL_ASSERT(m_programFactory != nullptr, "InitializeBlitResources: program factory is null"); ProgramFactory::CompileOptionFlags blitTransformFlags = 0; const auto& blitProgramObj = m_programFactory->GetOrCreateProgram(*m_blitResources.program, blitTransformFlags); Bool foundBlitSamplerBinding = false; for (Uint32 binding = 0; binding < blitProgramObj.samplerNameByBinding.size(); ++binding) { if (blitProgramObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) { continue; } if (blitProgramObj.samplerNameByBinding[binding] == "uSource") { m_blitResources.samplerBinding = binding; foundBlitSamplerBinding = true; break; } } MOBILEGL_ASSERT(foundBlitSamplerBinding, "InitializeBlitResources: failed to resolve reflected binding for uSource"); auto createSampler = [](Uint externalIndex, SamplerFilterMode filter) { auto sampler = MakeShared(externalIndex); sampler->SetWrapS(SamplerWrapMode::ClampToEdge); sampler->SetWrapT(SamplerWrapMode::ClampToEdge); sampler->SetWrapR(SamplerWrapMode::ClampToEdge); sampler->SetMinFilter(filter); sampler->SetMagFilter(filter); sampler->SetMipmapMode(SamplerMipmapMode::None); sampler->SetLodRange(0.0f, 0.0f); return sampler; }; m_blitResources.nearestSampler = createSampler(kHiddenBlitNearestSamplerId, SamplerFilterMode::Nearest); m_blitResources.linearSampler = createSampler(kHiddenBlitLinearSamplerId, SamplerFilterMode::Linear); return true; } void VulkanRenderer::ShutdownBlitResources() { m_blitResources = {}; } Bool VulkanRenderer::InitializeDepthMipmapResources() { ShutdownDepthMipmapResources(); auto vertexShader = MakeShared(ShaderStage::Vertex, kHiddenDepthMipmapVertexShaderId); vertexShader->SetShaderSource(kFullscreenTriangleVertexShaderSource); vertexShader->Compile(); if (!vertexShader->GetCompileStatus()) { MGLOG_E("InitializeDepthMipmapResources failed: vertex shader compile error: %s", vertexShader->GetInfoLog().c_str()); return false; } auto fragmentShader = MakeShared(ShaderStage::Fragment, kHiddenDepthMipmapFragmentShaderId); fragmentShader->SetShaderSource(kDepthMipmapFragmentShaderSource); fragmentShader->Compile(); if (!fragmentShader->GetCompileStatus()) { MGLOG_E("InitializeDepthMipmapResources failed: fragment shader compile error: %s", fragmentShader->GetInfoLog().c_str()); return false; } m_depthMipmapResources.program = MakeShared(kHiddenDepthMipmapProgramId); m_depthMipmapResources.program->AttachShader(vertexShader); m_depthMipmapResources.program->AttachShader(fragmentShader); m_depthMipmapResources.program->Link(false); if (!m_depthMipmapResources.program->GetLinkStatus()) { MGLOG_E("InitializeDepthMipmapResources failed: program link error: %s", m_depthMipmapResources.program->GetInfoLog().c_str()); return false; } m_depthMipmapResources.srcRectLocation = m_depthMipmapResources.program->GetUniformLocation("uSrcRect"); m_depthMipmapResources.dstRectLocation = m_depthMipmapResources.program->GetUniformLocation("uDstRect"); m_depthMipmapResources.surfaceTransformLocation = m_depthMipmapResources.program->GetUniformLocation("uSurfaceTransform"); m_depthMipmapResources.srcTexelSizeLocation = m_depthMipmapResources.program->GetUniformLocation("uSrcTexelSize"); MOBILEGL_ASSERT(m_depthMipmapResources.srcRectLocation >= 0, "InitializeDepthMipmapResources: missing uSrcRect"); MOBILEGL_ASSERT(m_depthMipmapResources.dstRectLocation >= 0, "InitializeDepthMipmapResources: missing uDstRect"); MOBILEGL_ASSERT(m_depthMipmapResources.surfaceTransformLocation >= 0, "InitializeDepthMipmapResources: missing uSurfaceTransform"); MOBILEGL_ASSERT(m_depthMipmapResources.srcTexelSizeLocation >= 0, "InitializeDepthMipmapResources: missing uSrcTexelSize"); MOBILEGL_ASSERT(m_depthMipmapResources.program->GetUBOSize() > 0, "InitializeDepthMipmapResources: depth mipmap program global UBO is empty"); MOBILEGL_ASSERT(m_programFactory != nullptr, "InitializeDepthMipmapResources: program factory is null"); ProgramFactory::CompileOptionFlags depthMipmapTransformFlags = 0; const auto& depthMipmapProgramObj = m_programFactory->GetOrCreateProgram(*m_depthMipmapResources.program, depthMipmapTransformFlags); Bool foundDepthMipmapSamplerBinding = false; for (Uint32 binding = 0; binding < depthMipmapProgramObj.samplerNameByBinding.size(); ++binding) { if (depthMipmapProgramObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) { continue; } if (depthMipmapProgramObj.samplerNameByBinding[binding] == "uSource") { m_depthMipmapResources.samplerBinding = binding; foundDepthMipmapSamplerBinding = true; break; } } MOBILEGL_ASSERT(foundDepthMipmapSamplerBinding, "InitializeDepthMipmapResources: failed to resolve reflected binding for uSource"); return true; } void VulkanRenderer::ShutdownDepthMipmapResources() { m_depthMipmapResources = {}; } void VulkanRenderer::CollectDeferredDepthMipmapCleanup(Uint32 frameIndex) { MOBILEGL_ASSERT(frameIndex < m_deferredDepthMipmapCleanup.size(), "CollectDeferredDepthMipmapCleanup: frame index %u out of range (size=%zu)", frameIndex, m_deferredDepthMipmapCleanup.size()); if (m_device == VK_NULL_HANDLE) { return; } auto& cleanup = m_deferredDepthMipmapCleanup[frameIndex]; for (auto framebuffer : cleanup.framebuffers) { if (framebuffer != VK_NULL_HANDLE) { vkDestroyFramebuffer(m_device, framebuffer, nullptr); } } for (auto pipeline : cleanup.pipelines) { if (pipeline != VK_NULL_HANDLE) { vkDestroyPipeline(m_device, pipeline, nullptr); } } for (auto renderPass : cleanup.renderPasses) { if (renderPass != VK_NULL_HANDLE) { vkDestroyRenderPass(m_device, renderPass, nullptr); } } for (auto imageView : cleanup.imageViews) { if (imageView != VK_NULL_HANDLE) { vkDestroyImageView(m_device, imageView, nullptr); } } cleanup.framebuffers.clear(); cleanup.pipelines.clear(); cleanup.renderPasses.clear(); cleanup.imageViews.clear(); } void VulkanRenderer::DestroyDeferredDepthMipmapCleanup() { for (Uint32 frameIndex = 0; frameIndex < m_deferredDepthMipmapCleanup.size(); ++frameIndex) { CollectDeferredDepthMipmapCleanup(frameIndex); } m_deferredDepthMipmapCleanup.clear(); } VkPipeline VulkanRenderer::GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry) { MOBILEGL_ASSERT(m_blitResources.program != nullptr, "GetOrCreateBlitPipeline: blit program is null"); MOBILEGL_ASSERT(m_programFactory != nullptr, "GetOrCreateBlitPipeline: program factory is null"); MOBILEGL_ASSERT(m_uniformManager != nullptr, "GetOrCreateBlitPipeline: descriptor binder is null"); static const VkPipelineVertexInputStateCreateInfo kEmptyVertexInputState { VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; ProgramFactory::CompileOptionFlags transformFlags = 0; const auto& programObj = m_programFactory->GetOrCreateProgram(*m_blitResources.program, transformFlags); PipelineFactory::PipelineCreatePayload payload{ .programHash = programObj.hash, .vertexInputHash = 0, .pipelineLayout = programObj.pipelineLayout, .renderPass = renderPassEntry.renderPass, .colorAttachmentCount = renderPassEntry.colorAttachmentCount, .subpass = 0, .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, .cullMode = VK_CULL_MODE_NONE, .frontFace = VK_FRONT_FACE_CLOCKWISE, .depthTestEnable = false, .depthWriteEnable = false, .depthCompareOp = VK_COMPARE_OP_ALWAYS, .stages = &programObj.stages, .vertexInputState = &kEmptyVertexInputState }; static constexpr VkColorComponentFlags kColorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; MOBILEGL_ASSERT(payload.colorAttachmentCount <= PipelineFactory::PipelineCreatePayload::kMaxColorAttachments, "GetOrCreateBlitPipeline: colorAttachmentCount=%u exceeds payload capacity", payload.colorAttachmentCount); for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) { payload.colorBlendAttachments[i] = MakeColorBlendAttachmentState( false, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, kColorWriteMask); } return m_pipelineFactory->GetOrCreatePipeline(payload); } Bool VulkanRenderer::GenerateDepthMipmapWithShader(FrameContext::FrameData& frame, MG_State::GLState::ITextureObject& texture, VkTextureManager::TextureResource& resource, Uint32 baseMipLevel, Uint32 generateMipLevelCount, const IntVec3& storageBaseTexelSize, VkImageLayout originalLayout, VkImageLayout finalLayout) { MOBILEGL_ASSERT(m_depthMipmapResources.program != nullptr, "GenerateDepthMipmapWithShader: depth mipmap program is null"); MOBILEGL_ASSERT(m_blitResources.nearestSampler != nullptr, "GenerateDepthMipmapWithShader: helper sampler is null"); MOBILEGL_ASSERT(m_programFactory != nullptr, "GenerateDepthMipmapWithShader: program factory is null"); MOBILEGL_ASSERT(m_uniformManager != nullptr, "GenerateDepthMipmapWithShader: uniform manager is null"); MOBILEGL_ASSERT(texture.GetTarget() == TextureTarget::Texture2D, "GenerateDepthMipmapWithShader only supports GL_TEXTURE_2D depth textures"); MOBILEGL_ASSERT((resource.aspect & VK_IMAGE_ASPECT_DEPTH_BIT) != 0, "GenerateDepthMipmapWithShader requires a depth aspect"); MOBILEGL_ASSERT(resource.depth == 1 && resource.arrayLayers == 1, "GenerateDepthMipmapWithShader only supports single-layer depth textures"); MOBILEGL_ASSERT(m_frameContext.GetCurrentFrameIndex() < m_deferredDepthMipmapCleanup.size(), "GenerateDepthMipmapWithShader: frame index %u out of range (cleanup slots=%zu)", m_frameContext.GetCurrentFrameIndex(), m_deferredDepthMipmapCleanup.size()); auto& deferredCleanup = m_deferredDepthMipmapCleanup[m_frameContext.GetCurrentFrameIndex()]; static const VkPipelineVertexInputStateCreateInfo kEmptyVertexInputState { VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; ProgramFactory::CompileOptionFlags depthMipmapTransformFlags = 0; const auto& programObj = m_programFactory->GetOrCreateProgram(*m_depthMipmapResources.program, depthMipmapTransformFlags); VkAttachmentDescription depthAttachment{}; depthAttachment.format = resource.format; depthAttachment.samples = VK_SAMPLE_COUNT_1_BIT; depthAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; depthAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; depthAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; depthAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; depthAttachment.initialLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; depthAttachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; VkAttachmentReference depthAttachmentRef{}; depthAttachmentRef.attachment = 0; depthAttachmentRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; VkSubpassDescription subpassDesc{}; subpassDesc.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpassDesc.pDepthStencilAttachment = &depthAttachmentRef; VkRenderPassCreateInfo renderPassCreateInfo{}; renderPassCreateInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; renderPassCreateInfo.attachmentCount = 1; renderPassCreateInfo.pAttachments = &depthAttachment; renderPassCreateInfo.subpassCount = 1; renderPassCreateInfo.pSubpasses = &subpassDesc; VkRenderPass renderPass = VK_NULL_HANDLE; VK_VERIFY(vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass), "GenerateDepthMipmapWithShader: vkCreateRenderPass"); VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; VkPipelineViewportStateCreateInfo viewportState{}; viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; viewportState.viewportCount = 1; viewportState.scissorCount = 1; VkPipelineRasterizationStateCreateInfo rasterizationState{}; rasterizationState.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; rasterizationState.polygonMode = VK_POLYGON_MODE_FILL; rasterizationState.cullMode = VK_CULL_MODE_NONE; rasterizationState.frontFace = VK_FRONT_FACE_CLOCKWISE; rasterizationState.lineWidth = 1.0f; VkPipelineMultisampleStateCreateInfo multisampleState{}; multisampleState.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; multisampleState.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; VkPipelineDepthStencilStateCreateInfo depthStencilState{}; depthStencilState.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; depthStencilState.depthTestEnable = VK_TRUE; depthStencilState.depthWriteEnable = VK_TRUE; depthStencilState.depthCompareOp = VK_COMPARE_OP_ALWAYS; depthStencilState.minDepthBounds = 0.0f; depthStencilState.maxDepthBounds = 1.0f; VkPipelineColorBlendStateCreateInfo colorBlendState{}; colorBlendState.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; const VkDynamicState dynamicStates[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; VkPipelineDynamicStateCreateInfo dynamicState{}; dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; dynamicState.dynamicStateCount = static_cast(std::size(dynamicStates)); dynamicState.pDynamicStates = dynamicStates; VkGraphicsPipelineCreateInfo pipelineCreateInfo{}; pipelineCreateInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; pipelineCreateInfo.stageCount = static_cast(programObj.stages.size()); pipelineCreateInfo.pStages = programObj.stages.data(); pipelineCreateInfo.pVertexInputState = &kEmptyVertexInputState; pipelineCreateInfo.pInputAssemblyState = &inputAssembly; pipelineCreateInfo.pViewportState = &viewportState; pipelineCreateInfo.pRasterizationState = &rasterizationState; pipelineCreateInfo.pMultisampleState = &multisampleState; pipelineCreateInfo.pDepthStencilState = &depthStencilState; pipelineCreateInfo.pColorBlendState = &colorBlendState; pipelineCreateInfo.pDynamicState = &dynamicState; pipelineCreateInfo.layout = programObj.pipelineLayout; pipelineCreateInfo.renderPass = renderPass; pipelineCreateInfo.subpass = 0; VkPipeline pipeline = VK_NULL_HANDLE; VK_VERIFY(vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &pipelineCreateInfo, nullptr, &pipeline), "GenerateDepthMipmapWithShader: vkCreateGraphicsPipelines"); deferredCleanup.renderPasses.push_back(renderPass); deferredCleanup.pipelines.push_back(pipeline); auto createMipView = [&](Uint32 mipLevel, VkImageAspectFlags aspectMask) { VkImageViewCreateInfo viewCreateInfo{}; viewCreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; viewCreateInfo.image = resource.image; viewCreateInfo.viewType = resource.viewType; viewCreateInfo.format = resource.format; viewCreateInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; viewCreateInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; viewCreateInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; viewCreateInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; viewCreateInfo.subresourceRange.aspectMask = aspectMask; viewCreateInfo.subresourceRange.baseMipLevel = mipLevel; viewCreateInfo.subresourceRange.levelCount = 1; viewCreateInfo.subresourceRange.baseArrayLayer = 0; viewCreateInfo.subresourceRange.layerCount = resource.arrayLayers; VkImageView view = VK_NULL_HANDLE; VK_VERIFY(vkCreateImageView(m_device, &viewCreateInfo, nullptr, &view), "GenerateDepthMipmapWithShader: vkCreateImageView"); return view; }; VkPipelineStageFlags originalSrcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags originalSrcAccessMask = 0; GetImageTransitionSourceState(originalLayout, originalSrcStageMask, originalSrcAccessMask); VkPipelineStageFlags finalDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags finalDstAccessMask = 0; GetImageTransitionDestinationState(finalLayout, finalDstStageMask, finalDstAccessMask); VkPipelineStageFlags depthAttachmentStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags depthAttachmentAccessMask = 0; GetImageTransitionDestinationState(VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, depthAttachmentStageMask, depthAttachmentAccessMask); if (originalLayout != finalLayout) { if (baseMipLevel > 0) { VkImageLayout lowerMipLayout = originalLayout; const Bool lowerReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource.image, lowerMipLayout, finalLayout, originalSrcStageMask, finalDstStageMask, originalSrcAccessMask, finalDstAccessMask, resource.aspect, 0, baseMipLevel); MOBILEGL_ASSERT(lowerReady, "%s: failed to transition lower untouched mip levels", __func__); } if (generateMipLevelCount < resource.mipLevels) { VkImageLayout upperMipLayout = originalLayout; const Bool upperReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource.image, upperMipLayout, finalLayout, originalSrcStageMask, finalDstStageMask, originalSrcAccessMask, finalDstAccessMask, resource.aspect, generateMipLevelCount, resource.mipLevels - generateMipLevelCount); MOBILEGL_ASSERT(upperReady, "%s: failed to transition upper untouched mip levels", __func__); } VkImageLayout srcMipLayout = originalLayout; const Bool srcReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource.image, srcMipLayout, finalLayout, originalSrcStageMask, finalDstStageMask, originalSrcAccessMask, finalDstAccessMask, resource.aspect, baseMipLevel, 1); MOBILEGL_ASSERT(srcReady, "%s: failed to transition base mip level to sampled layout", __func__); } resource.layout = finalLayout; auto* depthProgramData = static_cast(m_depthMipmapResources.program->MapUBO()); MOBILEGL_ASSERT(depthProgramData != nullptr, "GenerateDepthMipmapWithShader: depth mipmap UBO is null"); auto writeUniform = [&](Int location, const void* data, SizeT size) { MOBILEGL_ASSERT(location >= 0, "GenerateDepthMipmapWithShader: invalid uniform location"); const Uint offset = m_depthMipmapResources.program->GetUniformOffset(static_cast(location)); MOBILEGL_ASSERT(offset + size <= m_depthMipmapResources.program->GetUBOSize(), "GenerateDepthMipmapWithShader: uniform write out of bounds"); memcpy(depthProgramData + offset, data, size); }; for (Uint32 level = baseMipLevel + 1; level < generateMipLevelCount; ++level) { VkImageLayout dstMipLayout = originalLayout; const Bool dstReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource.image, dstMipLayout, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, originalSrcStageMask, depthAttachmentStageMask, originalSrcAccessMask, depthAttachmentAccessMask, resource.aspect, level, 1); MOBILEGL_ASSERT(dstReady, "%s: failed to transition mip level %u to depth attachment layout", __func__, level); const IntVec3 srcTexelSize = ComputeMipTexelSize(storageBaseTexelSize, level - 1); const IntVec3 dstTexelSize = ComputeMipTexelSize(storageBaseTexelSize, level); const Int srcTexelSizeUniform[2] = {srcTexelSize.x(), srcTexelSize.y()}; const VkImageView sourceImageView = createMipView(level - 1, VK_IMAGE_ASPECT_DEPTH_BIT); const VkImageView depthAttachmentView = createMipView(level, resource.aspect); deferredCleanup.imageViews.push_back(sourceImageView); deferredCleanup.imageViews.push_back(depthAttachmentView); VkFramebufferCreateInfo framebufferCreateInfo{}; framebufferCreateInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; framebufferCreateInfo.renderPass = renderPass; framebufferCreateInfo.attachmentCount = 1; framebufferCreateInfo.pAttachments = &depthAttachmentView; framebufferCreateInfo.width = static_cast(dstTexelSize.x()); framebufferCreateInfo.height = static_cast(dstTexelSize.y()); framebufferCreateInfo.layers = 1; VkFramebuffer framebuffer = VK_NULL_HANDLE; VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferCreateInfo, nullptr, &framebuffer), "GenerateDepthMipmapWithShader: vkCreateFramebuffer"); deferredCleanup.framebuffers.push_back(framebuffer); VkRenderPassBeginInfo renderPassBeginInfo{}; renderPassBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; renderPassBeginInfo.renderPass = renderPass; renderPassBeginInfo.framebuffer = framebuffer; renderPassBeginInfo.renderArea.offset = {0, 0}; renderPassBeginInfo.renderArea.extent = { static_cast(dstTexelSize.x()), static_cast(dstTexelSize.y()) }; vkCmdBeginRenderPass(frame.commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE); VkViewport viewport{}; viewport.x = 0.0f; viewport.y = 0.0f; viewport.width = static_cast(dstTexelSize.x()); viewport.height = static_cast(dstTexelSize.y()); viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; vkCmdSetViewport(frame.commandBuffer, 0, 1, &viewport); VkRect2D scissor{}; scissor.offset = {0, 0}; scissor.extent = {static_cast(dstTexelSize.x()), static_cast(dstTexelSize.y())}; vkCmdSetScissor(frame.commandBuffer, 0, 1, &scissor); vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); std::fill(depthProgramData, depthProgramData + m_depthMipmapResources.program->GetUBOSize(), Uint8{0}); BlitUniformData blitUniformData{}; writeUniform(m_depthMipmapResources.srcRectLocation, blitUniformData.srcRect, sizeof(blitUniformData.srcRect)); writeUniform(m_depthMipmapResources.dstRectLocation, blitUniformData.dstRect, sizeof(blitUniformData.dstRect)); writeUniform(m_depthMipmapResources.surfaceTransformLocation, &blitUniformData.surfaceTransform, sizeof(blitUniformData.surfaceTransform)); writeUniform(m_depthMipmapResources.srcTexelSizeLocation, srcTexelSizeUniform, sizeof(srcTexelSizeUniform)); const auto samplerBindingOverride = UniformManager::SamplerBindingOverride{ .binding = m_depthMipmapResources.samplerBinding, .texture = &texture, .sampler = m_blitResources.nearestSampler.get(), .imageView = sourceImageView, }; const Bool bound = m_uniformManager->BindProgramUniformBuffers( frame.commandBuffer, *m_depthMipmapResources.program, programObj, m_frameContext.GetCurrentFrameIndex(), &samplerBindingOverride); MOBILEGL_ASSERT(bound, "GenerateDepthMipmapWithShader: BindProgramUniformBuffers failed"); vkCmdDraw(frame.commandBuffer, 3, 1, 0, 0); vkCmdEndRenderPass(frame.commandBuffer); VkImageLayout finishedMipLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; const Bool finishedReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource.image, finishedMipLayout, finalLayout, depthAttachmentStageMask, finalDstStageMask, depthAttachmentAccessMask, finalDstAccessMask, resource.aspect, level, 1); MOBILEGL_ASSERT(finishedReady, "%s: failed to transition mip level %u to sampled layout", __func__, level); } return true; } VkPipeline VulkanRenderer::GetOrCreatePipeline( GLenum mode, const MG_State::GLState::ProgramObject& program, const MG_State::GLState::VertexArrayObject& vao, const RenderPassEntry& renderPassEntry) { ProgramFactory::CompileOptionFlags transformFlags = GetShaderTransformFlags(m_swapchainObject.GetPreTransform()); Bool invertClockwise = transformFlags & ProgramFactory::CompileOptionBit::PositionYFlip; const auto& programObj = m_programFactory->GetOrCreateProgram(program, transformFlags); if (programObj.stages.empty()) { MGLOG_D("GetOrCreatePipeline skipped: program has no shader stages"); return VK_NULL_HANDLE; } #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG const auto& limits = m_physicalDevice.properties.limits; if (programObj.fragmentInputComponentCount != 0) { MOBILEGL_ASSERT( programObj.fragmentInputComponentCount <= limits.maxFragmentInputComponents, "GetOrCreatePipeline: fragmentInputComponents=%u exceeds device limit=%u program=%u producerStage=%d", programObj.fragmentInputComponentCount, limits.maxFragmentInputComponents, program.GetExternalIndex(), static_cast(programObj.rasterizationProducerStage)); } if (programObj.producerOutputComponentCount != 0) { Uint32 producerOutputLimit = 0; switch (programObj.rasterizationProducerStage) { case ShaderStage::Vertex: producerOutputLimit = limits.maxVertexOutputComponents; break; case ShaderStage::Geometry: producerOutputLimit = limits.maxGeometryOutputComponents; break; case ShaderStage::TessEval: producerOutputLimit = limits.maxTessellationEvaluationOutputComponents; break; default: break; } if (producerOutputLimit != 0) { MOBILEGL_ASSERT( programObj.producerOutputComponentCount <= producerOutputLimit, "GetOrCreatePipeline: producerOutputComponents=%u exceeds stage limit=%u program=%u producerStage=%d", programObj.producerOutputComponentCount, producerOutputLimit, program.GetExternalIndex(), static_cast(programObj.rasterizationProducerStage)); } } #endif auto vertexInputHash = m_vertexInputStateFactory->ComputeHash(vao); auto& vis = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao); const Uint32 vertexInputAttribMask = BuildVertexInputAttributeMask(vis.attributes); const Uint32 activeAttribMask = programObj.activeVertexInputLocationMask; const Uint32 missingAttribMask = activeAttribMask & ~vertexInputAttribMask; Vector patchedAttributes = vis.attributes; Bool hasPatchedVertexAttributes = false; for (auto& attribute : patchedAttributes) { if (attribute.location >= 32 || (activeAttribMask & (1u << attribute.location)) == 0) { continue; } const GLenum shaderInputType = programObj.vertexInputTypes[attribute.location]; const NumericDomain shaderInputDomain = GetNumericDomainForShaderValueType(shaderInputType); const NumericDomain vertexInputDomain = GetNumericDomainForVertexFormat(attribute.format); if (shaderInputDomain == NumericDomain::Unknown || vertexInputDomain == NumericDomain::Unknown || shaderInputDomain == vertexInputDomain) { continue; } VkFormat patchedFormat = VK_FORMAT_UNDEFINED; const Bool canPatch = TryCoerceVertexFormatNumericDomain(attribute.format, shaderInputDomain, patchedFormat); MOBILEGL_ASSERT( canPatch, "GetOrCreatePipeline: vertex input location=%u format=%d mismatches shader input type=%u program=%u", attribute.location, static_cast(attribute.format), static_cast(shaderInputType), program.GetExternalIndex()); MGLOG_W("GetOrCreatePipeline: patching vertex input location=%u format=%d -> %d to match shader input type=%u for program=%u", attribute.location, static_cast(attribute.format), static_cast(patchedFormat), static_cast(shaderInputType), program.GetExternalIndex()); attribute.format = patchedFormat; hasPatchedVertexAttributes = true; } VertexInputStateBuilder syntheticVertexInputBuilder; const VkPipelineVertexInputStateCreateInfo* pipelineVertexInputState = &vis.state; if (missingAttribMask != 0 || hasPatchedVertexAttributes) { for (const auto& binding : vis.bindings) { syntheticVertexInputBuilder.AddBinding(binding.binding, binding.stride, binding.inputRate); } for (const auto& attribute : patchedAttributes) { syntheticVertexInputBuilder.AddAttribute(attribute.location, attribute.binding, attribute.format, attribute.offset); } Uint32 syntheticBinding = static_cast(vis.bindings.size()); for (Uint32 location = 0; location < 32; ++location) { if ((missingAttribMask & (1u << location)) == 0) { continue; } VkFormat format = VK_FORMAT_UNDEFINED; const Bool supported = TryGetCurrentVertexAttributeFormat(programObj.vertexInputTypes[location], format); MOBILEGL_ASSERT(supported, "DirectVulkan does not support current generic vertex attribute type yet: program=%u location=%u type=0x%x activeAttribMask=0x%x vertexInputAttribMask=0x%x", program.GetExternalIndex(), location, programObj.vertexInputTypes[location], activeAttribMask, vertexInputAttribMask); syntheticVertexInputBuilder.AddBinding(syntheticBinding, 0, VK_VERTEX_INPUT_RATE_VERTEX); syntheticVertexInputBuilder.AddAttribute(location, syntheticBinding, format, 0); ++syntheticBinding; } pipelineVertexInputState = &syntheticVertexInputBuilder.Build(); } auto cullFaceEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::CullFace); auto depthTestEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DepthTest); auto mask = MG_State::pGLContext->GetColorMask(); const auto colorWriteMask = static_cast( (mask.r() ? VK_COLOR_COMPONENT_R_BIT : 0u) | (mask.g() ? VK_COLOR_COMPONENT_G_BIT : 0u) | (mask.b() ? VK_COLOR_COMPONENT_B_BIT : 0u) | (mask.a() ? VK_COLOR_COMPONENT_A_BIT : 0u)); PipelineFactory::PipelineCreatePayload payload { .programHash = programObj.hash, .vertexInputHash = vertexInputHash, .pipelineLayout = programObj.pipelineLayout, .renderPass = renderPassEntry.renderPass, .colorAttachmentCount = renderPassEntry.colorAttachmentCount, .subpass = 0, .topology = MG_Util::ConvertPrimitiveModeToVkEnum(mode), .cullMode = cullFaceEnabled ? MG_Util::ConvertCullFaceModeToVkEnum(MG_State::pGLContext->GetCullFaceMode(), invertClockwise) : VK_CULL_MODE_NONE, .frontFace = VK_FRONT_FACE_CLOCKWISE, .depthTestEnable = depthTestEnabled, .depthWriteEnable = depthTestEnabled && MG_State::pGLContext->GetDepthMask(), .depthCompareOp = MG_Util::ConvertDepthTestFuncToVkEnum(MG_State::pGLContext->GetDepthFunc()), .stages = &programObj.stages, .vertexInputState = pipelineVertexInputState }; const Bool hasDepthStencilAttachment = renderPassEntry.hasDepthStencilAttachment; MOBILEGL_ASSERT( hasDepthStencilAttachment || (!payload.depthTestEnable && !payload.depthWriteEnable), "GetOrCreatePipeline: render pass has no depth attachment but depthTestEnable=%d depthWriteEnable=%d program=%u attachmentCount=%u colorAttachmentCount=%u", payload.depthTestEnable ? 1 : 0, payload.depthWriteEnable ? 1 : 0, program.GetExternalIndex(), renderPassEntry.attachmentCount, renderPassEntry.colorAttachmentCount); const Uint32 fragmentOutputMask = programObj.activeFragmentOutputLocationMask; MOBILEGL_ASSERT( (fragmentOutputMask >> payload.colorAttachmentCount) == 0, "GetOrCreatePipeline: fragmentOutputMask=0x%x exceeds colorAttachmentCount=%u for program=%u", fragmentOutputMask, payload.colorAttachmentCount, program.GetExternalIndex()); MOBILEGL_ASSERT(payload.colorAttachmentCount <= PipelineFactory::PipelineCreatePayload::kMaxColorAttachments, "GetOrCreatePipeline: colorAttachmentCount=%u exceeds payload capacity", payload.colorAttachmentCount); const auto& drawFboBinding = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); MOBILEGL_ASSERT(drawFboBinding != nullptr, "GetOrCreatePipeline: draw framebuffer is null"); const Bool isDefaultDrawFbo = drawFboBinding.get() == MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO.get(); const auto& drawBuffers = drawFboBinding->GetDrawBuffers(); for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) { BlendFactor srcRGB = BlendFactor::One; BlendFactor dstRGB = BlendFactor::Zero; BlendFactor srcAlpha = BlendFactor::One; BlendFactor dstAlpha = BlendFactor::Zero; MG_State::pGLContext->GetBlendFuncIndexed(i, srcRGB, dstRGB, srcAlpha, dstAlpha); const Bool blendEnabled = MG_State::pGLContext->IsCapabilityEnabledIndexed(CapabilityInput::Blend, i); VkColorComponentFlags attachmentColorWriteMask = colorWriteMask; Bool effectiveBlendEnabled = blendEnabled; if (!isDefaultDrawFbo && i < drawBuffers.size()) { const auto drawBuffer = drawBuffers[i]; if (drawBuffer == FramebufferAttachmentType::None) { // GL ignores writes and per-target blend state for GL_NONE draw buffer slots. attachmentColorWriteMask = 0; effectiveBlendEnabled = false; } if (drawBuffer != FramebufferAttachmentType::None) { const auto& attachment = drawFboBinding->GetAttachment(drawBuffer); if (attachment.IsTexture()) { auto* texture = attachment.GetTexture().get(); MOBILEGL_ASSERT(texture != nullptr, "GetOrCreatePipeline: color attachment %u texture is null", i); const auto* textureResource = m_textureManager->SyncTextureAndGetDescriptor(*texture); MOBILEGL_ASSERT(textureResource != nullptr, "GetOrCreatePipeline: failed to sync color attachment textureId=%d", texture->GetExternalIndex()); VkFormatProperties attachmentFormatProperties{}; vkGetPhysicalDeviceFormatProperties( m_physicalDevice.handle, textureResource->format, &attachmentFormatProperties); MOBILEGL_ASSERT( (attachmentFormatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0, "GetOrCreatePipeline: color attachment %u format=%d textureId=%d lacks VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT (program=%u)", i, static_cast(textureResource->format), texture->GetExternalIndex(), program.GetExternalIndex()); const SizeT componentCount = MG_Util::GetBaseInternalFormatComponentCount(texture->GetFormat()); const NumericDomain attachmentNumericDomain = GetNumericDomainForTextureInternalFormat(texture->GetFormat()); for (Uint32 outputLocation = 0; outputLocation < ProgramFactory::VkProgramObject::kMaxVertexInputLocations; ++outputLocation) { if ((programObj.activeFragmentOutputLocationMask & (1u << outputLocation)) == 0 || outputLocation != i) { continue; } const GLenum fragmentOutputType = programObj.fragmentOutputTypes[outputLocation]; const NumericDomain fragmentOutputDomain = GetNumericDomainForShaderValueType(fragmentOutputType); // GL allows fragment outputs with more components than the bound color attachment; // excess components are discarded during conversion to the attachment format. MOBILEGL_ASSERT( attachmentNumericDomain == NumericDomain::Unknown || fragmentOutputDomain == NumericDomain::Unknown || attachmentNumericDomain == fragmentOutputDomain, "GetOrCreatePipeline: fragment output location=%d type=%u mismatches color attachment %u internalFormat=%d textureId=%d program=%u", static_cast(outputLocation), static_cast(fragmentOutputType), i, static_cast(texture->GetFormat()), texture->GetExternalIndex(), program.GetExternalIndex()); } const VkColorComponentFlags supportedColorWriteMask = GetSupportedColorWriteMaskForComponentCount(componentCount); if ((attachmentColorWriteMask & ~supportedColorWriteMask) != 0) { MGLOG_W( "GetOrCreatePipeline: clamping colorWriteMask=0x%x to 0x%x on color attachment %u (componentCount=%zu textureId=%d internalFormat=%d program=%u blendEnabled=%d)", static_cast(attachmentColorWriteMask), static_cast(attachmentColorWriteMask & supportedColorWriteMask), i, componentCount, texture->GetExternalIndex(), static_cast(texture->GetFormat()), program.GetExternalIndex(), effectiveBlendEnabled ? 1 : 0); attachmentColorWriteMask &= supportedColorWriteMask; } } } } if (effectiveBlendEnabled) { MOBILEGL_ASSERT(i < drawBuffers.size(), "GetOrCreatePipeline: color attachment %u is out of draw buffer range %zu", i, drawBuffers.size()); VkFormat colorAttachmentFormat = VK_FORMAT_UNDEFINED; Int textureExternalIndex = -1; if (isDefaultDrawFbo) { colorAttachmentFormat = m_swapchainObject.GetSurfaceFormat().format; } else { const auto drawBuffer = drawBuffers[i]; MOBILEGL_ASSERT(drawBuffer != FramebufferAttachmentType::None, "GetOrCreatePipeline: blend is enabled on draw buffer %u but the attachment is None", i); const auto& attachment = drawFboBinding->GetAttachment(drawBuffer); MOBILEGL_ASSERT(attachment.IsTexture(), "GetOrCreatePipeline: blend validation currently expects texture color attachments only"); auto* texture = attachment.GetTexture().get(); MOBILEGL_ASSERT(texture != nullptr, "GetOrCreatePipeline: color attachment %u texture is null", i); textureExternalIndex = texture->GetExternalIndex(); const auto* textureResource = m_textureManager->SyncTextureAndGetDescriptor(*texture); MOBILEGL_ASSERT(textureResource != nullptr, "GetOrCreatePipeline: failed to sync color attachment textureId=%d", textureExternalIndex); colorAttachmentFormat = textureResource->format; } VkFormatProperties formatProperties{}; vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, colorAttachmentFormat, &formatProperties); MOBILEGL_ASSERT( (formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT) != 0, "GetOrCreatePipeline: blend is enabled on color attachment %u for format=%d textureId=%d, but the format lacks VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT (program=%u)", i, static_cast(colorAttachmentFormat), textureExternalIndex, program.GetExternalIndex()); } payload.colorBlendAttachments[i] = MakeColorBlendAttachmentState( effectiveBlendEnabled, MG_Util::ConvertBlendFactorToVkEnum(srcRGB), MG_Util::ConvertBlendFactorToVkEnum(dstRGB), MG_Util::ConvertBlendFactorToVkEnum(srcAlpha), MG_Util::ConvertBlendFactorToVkEnum(dstAlpha), attachmentColorWriteMask); } return m_pipelineFactory->GetOrCreatePipeline(payload); } Bool VulkanRenderer::SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags aspects, const IndexBufferView* pIndexBufferView) { m_textureManager->CollectGarbage(); const auto& drawFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); const auto& vao = *MG_State::pGLContext->GetBoundVertexArray(); const auto& program = *MG_State::pGLContext->GetCurrentProgram(); ProgramFactory::CompileOptionFlags transformFlags = GetShaderTransformFlags(m_swapchainObject.GetPreTransform()); const auto& programObj = m_programFactory->GetOrCreateProgram(program, transformFlags); // Begin command recording if not yet if (!frame.isCommandRecording) { m_frameContext.BeginCommandRecording(); m_uniformManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); } auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); // Check if any of the textures to sample have pending clears, // which probably indicates it's been gone through codepath like `fbo attach` -> `clear` -> `fbo detach`, and // without draws in between to give it a chance to materialize such clear. // Deal with this situation here. Vector sampledTextures; Bool hasSampledTextures = m_uniformManager->CollectSampledTextures(program, programObj, sampledTextures); MOBILEGL_ASSERT(hasSampledTextures, "%s: CollectSampledTextures failed", __func__); MGLOG_D("SetupDraw: program=%u drawFbo=%u sampledTextureCount=%zu activeRenderPass=%s", program.GetExternalIndex(), drawFbo ? drawFbo->GetExternalIndex() : 0u, sampledTextures.size(), activeRenderPass ? "true" : "false"); Bool activeRenderPassUsesSampledTexture = false; if (activeRenderPass != nullptr) { for (auto* sampledTexture : sampledTextures) { if (sampledTexture == nullptr) { continue; } if (ActiveRenderPassUsesTexture(*activeRenderPass, *sampledTexture)) { MGLOG_D("SetupDraw: active render pass is still using sampled textureId=%d; ending render pass before descriptor preparation", sampledTexture->GetExternalIndex()); activeRenderPassUsesSampledTexture = true; break; } } } if (activeRenderPassUsesSampledTexture) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); activeRenderPass = nullptr; } Bool needSampledTextureTransitions = false; for (auto* sampledTexture : sampledTextures) { if (!sampledTexture) { continue; } auto* textureResource = m_textureManager->SyncTextureAndGetDescriptor(*sampledTexture); MOBILEGL_ASSERT(textureResource != nullptr, "%s: SyncTextureAndGetDescriptor failed for textureId=%d", __func__, sampledTexture->GetExternalIndex()); MGLOG_D("SetupDraw: sampled textureId=%d layout(before)=%s(%d)", sampledTexture->GetExternalIndex(), VkImageLayoutToString(textureResource->layout), static_cast(textureResource->layout)); if (m_clearManager->HasPendingClear(sampledTexture) || !IsValidSampledImageLayout(textureResource->layout)) { needSampledTextureTransitions = true; break; } } if (activeRenderPass && needSampledTextureTransitions) { MGLOG_D("SetupDraw: ending active render pass before sampled texture transitions"); VkRenderPassManager::EndRenderPass(frame.commandBuffer); activeRenderPass = nullptr; } for (auto* sampledTexture : sampledTextures) { if (!sampledTexture) { continue; } const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *sampledTexture); MOBILEGL_ASSERT(clearReady, "%s: MaterializePendingClearForTexture failed for textureId=%d", __func__, sampledTexture->GetExternalIndex()); const Bool ready = m_textureManager->TransitionTextureForSampling(frame.commandBuffer, *sampledTexture); MOBILEGL_ASSERT(ready, "%s: TransitionTextureForSampling failed for textureId=%d", __func__, sampledTexture->GetExternalIndex()); auto* transitionedResource = m_textureManager->SyncTextureAndGetDescriptor(*sampledTexture); MOBILEGL_ASSERT(transitionedResource != nullptr, "%s: post-transition SyncTextureAndGetDescriptor failed for textureId=%d", __func__, sampledTexture->GetExternalIndex()); MGLOG_D("SetupDraw: sampled textureId=%d layout(after)=%s(%d)", sampledTexture->GetExternalIndex(), VkImageLayoutToString(transitionedResource->layout), static_cast(transitionedResource->layout)); } auto* renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(*drawFbo, m_imageIndexAcquired); if (activeRenderPass && !activeRenderPass->CompatibleWith(*renderPassEntry)) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); activeRenderPass = nullptr; renderPassEntry = &m_renderPassManager->GetOrCreateRenderPass(*drawFbo, m_imageIndexAcquired); } auto pipeline = GetOrCreatePipeline(mode, program, vao, *renderPassEntry); activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); // Begin render pass, and handle clear if (activeRenderPass && activeRenderPass->CompatibleWith(*renderPassEntry)) { ClearAttachmentsOnActiveRenderPass(frame.commandBuffer, *renderPassEntry); } else { // No active render pass or active one not compatible. // Restart a new render pass Bool ok = VkRenderPassManager::BeginRenderPass(frame.commandBuffer, *renderPassEntry); MOBILEGL_ASSERT(ok, "%s: BeginRenderPass failed", __func__); } vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers( frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex()); if (!boundUniforms) { MGLOG_E("SetupDraw skipped: BindProgramUniformBuffers failed"); return false; } auto vtxUploadOk = UploadAndBindVertexBuffers(frame.commandBuffer, vao); MOBILEGL_ASSERT(vtxUploadOk, "SetupDraw skipped: failed to upload vertex buffers"); if (aspects & DrawSetupAspect::IndexBuffer) { auto idxUploadOk = UploadAndBindIndexBuffer(frame, vao, pIndexBufferView); MOBILEGL_ASSERT(idxUploadOk, "SetupDraw skipped: failed to upload index buffer"); } VkViewport viewport{}; viewport.x = 0.0f; viewport.y = 0.0f; viewport.width = static_cast(renderPassEntry->extent.x()); viewport.height = static_cast(renderPassEntry->extent.y()); viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; vkCmdSetViewport(frame.commandBuffer, 0, 1, &viewport); Bool scissorEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest); VkRect2D scissor{}; if (scissorEnabled) { const auto& scissorBox = MG_State::pGLContext->GetScissorBox(); scissor.offset = { scissorBox[0], scissorBox[1] }; scissor.extent = { (Uint)scissorBox[2], (Uint)scissorBox[3] }; } else { scissor.offset = {0, 0}; scissor.extent = { (Uint)renderPassEntry->extent.x(), (Uint)renderPassEntry->extent.y() }; } vkCmdSetScissor(frame.commandBuffer, 0, 1, &scissor); return true; } void VulkanRenderer::Clear(GLbitfield mask) { m_clearManager->CollectGarbage(); auto* fbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get(); MOBILEGL_ASSERT(fbo, "VulkanRenderer::Clear: draw framebuffer not found (fbo == nullptr)"); ClearFramebufferPayload payload { .color = MG_State::pGLContext->GetClearColor(), .depth = MG_State::pGLContext->GetClearDepth(), .stencil = MG_State::pGLContext->GetClearStencil() }; m_clearManager->QueueClear(mask, payload, *fbo); } void VulkanRenderer::QueueClearBufferPayload(GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload) { m_clearManager->CollectGarbage(); auto* fbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject().get(); MOBILEGL_ASSERT(fbo, "VulkanRenderer::QueueClearBufferPayload: draw framebuffer not found"); auto queueAttachmentClear = [&](FramebufferAttachmentType attachmentType) { if (attachmentType == FramebufferAttachmentType::None) { return; } const auto& attachment = fbo->GetAttachment(attachmentType); if (!attachment.IsTexture() || attachment.IsRenderbuffer()) { return; } auto texture = attachment.GetTexture(); if (!texture) { return; } m_clearManager->QueueClear(clearPayload, texture); }; switch (buffer) { case GL_COLOR: { if (drawbuffer < 0 || drawbuffer >= static_cast(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS)) { RecordClearBufferError(__func__, ErrorCode::InvalidValue, "color drawbuffer index is out of range"); return; } queueAttachmentClear(fbo->GetDrawBuffers()[drawbuffer]); return; } case GL_DEPTH: if (drawbuffer != 0) { RecordClearBufferError(__func__, ErrorCode::InvalidValue, "depth clear requires drawbuffer 0"); return; } queueAttachmentClear(FramebufferAttachmentType::Depth); return; case GL_STENCIL: if (drawbuffer != 0) { RecordClearBufferError(__func__, ErrorCode::InvalidValue, "stencil clear requires drawbuffer 0"); return; } queueAttachmentClear(FramebufferAttachmentType::Stencil); return; case GL_DEPTH_STENCIL: if (drawbuffer != 0) { RecordClearBufferError(__func__, ErrorCode::InvalidValue, "depth/stencil clear requires drawbuffer 0"); return; } queueAttachmentClear(FramebufferAttachmentType::Depth); queueAttachmentClear(FramebufferAttachmentType::Stencil); return; default: RecordClearBufferError(__func__, ErrorCode::InvalidEnum, "unsupported clear buffer target"); return; } } void VulkanRenderer::ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { ClearAttachmentPayload payload{}; payload.mask = GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT; payload.depth = depth; payload.stencil = static_cast(stencil); QueueClearBufferPayload(buffer, drawbuffer, payload); } void VulkanRenderer::ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value) { if (value == nullptr) { return; } ClearAttachmentPayload payload{}; switch (buffer) { case GL_COLOR: payload.mask = GL_COLOR_BUFFER_BIT; payload.color = FloatVec4(value[0], value[1], value[2], value[3]); break; case GL_DEPTH: payload.mask = GL_DEPTH_BUFFER_BIT; payload.depth = value[0]; break; default: break; } QueueClearBufferPayload(buffer, drawbuffer, payload); } void VulkanRenderer::ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value) { if (value == nullptr) { return; } ClearAttachmentPayload payload{}; switch (buffer) { case GL_COLOR: payload.mask = GL_COLOR_BUFFER_BIT; payload.color = FloatVec4(static_cast(value[0]), static_cast(value[1]), static_cast(value[2]), static_cast(value[3])); break; case GL_STENCIL: payload.mask = GL_STENCIL_BUFFER_BIT; payload.stencil = value[0]; break; default: break; } QueueClearBufferPayload(buffer, drawbuffer, payload); } void VulkanRenderer::ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value) { if (value == nullptr) { return; } ClearAttachmentPayload payload{}; switch (buffer) { case GL_COLOR: payload.mask = GL_COLOR_BUFFER_BIT; payload.color = FloatVec4(static_cast(value[0]), static_cast(value[1]), static_cast(value[2]), static_cast(value[3])); break; case GL_STENCIL: payload.mask = GL_STENCIL_BUFFER_BIT; payload.stencil = static_cast(std::max(value[0], 0)); break; default: break; } QueueClearBufferPayload(buffer, drawbuffer, payload); } Bool VulkanRenderer::MaterializePendingClearForTexture(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture) { ClearAttachmentPayload clearPayload{}; if (!m_clearManager->GetPendingClear(&texture, clearPayload)) { return true; } MOBILEGL_ASSERT(VkRenderPassManager::GetActiveRenderPass() == nullptr, "MaterializePendingClearForTexture requires no active render pass"); auto* resource = m_textureManager->SyncTextureAndGetDescriptor(texture); MOBILEGL_ASSERT(resource != nullptr, "MaterializePendingClearForTexture: SyncTextureAndGetDescriptor failed for textureId=%d", texture.GetExternalIndex()); VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcAccessMask = 0; GetImageTransitionSourceState(resource->layout, srcStageMask, srcAccessMask); Bool ok = VkTextureManager::TransitionImageLayout( commandBuffer, resource->image, resource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, resource->aspect, 0, resource->mipLevels); MOBILEGL_ASSERT(ok, "MaterializePendingClearForTexture: failed to transition textureId=%d to TRANSFER_DST", texture.GetExternalIndex()); VkImageSubresourceRange subresourceRange{}; subresourceRange.baseMipLevel = 0; subresourceRange.levelCount = 1; subresourceRange.baseArrayLayer = 0; subresourceRange.layerCount = 1; VkImageLayout sampledLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; if ((resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) { subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; VkClearColorValue clearValue{}; clearValue.float32[0] = clearPayload.color.x(); clearValue.float32[1] = clearPayload.color.y(); clearValue.float32[2] = clearPayload.color.z(); clearValue.float32[3] = clearPayload.color.w(); vkCmdClearColorImage(commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearValue, 1, &subresourceRange); } else { VkImageAspectFlags clearAspectMask = 0; if ((resource->aspect & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 && (clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0) { clearAspectMask |= VK_IMAGE_ASPECT_DEPTH_BIT; } if ((resource->aspect & VK_IMAGE_ASPECT_STENCIL_BIT) != 0 && (clearPayload.mask & GL_STENCIL_BUFFER_BIT) != 0) { clearAspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT; } MOBILEGL_ASSERT(clearAspectMask != 0, "MaterializePendingClearForTexture: textureId=%d has no matching depth/stencil clear mask", texture.GetExternalIndex()); subresourceRange.aspectMask = clearAspectMask; VkClearDepthStencilValue clearValue{}; clearValue.depth = clearPayload.depth; clearValue.stencil = clearPayload.stencil; vkCmdClearDepthStencilImage(commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearValue, 1, &subresourceRange); sampledLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; } ok = VkTextureManager::TransitionImageLayout( commandBuffer, resource->image, resource->layout, sampledLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels); MOBILEGL_ASSERT(ok, "MaterializePendingClearForTexture: failed to transition textureId=%d to sampled layout", texture.GetExternalIndex()); m_clearManager->PopPendingClear(&texture); MGLOG_D("MaterializePendingClearForTexture: textureId=%d pending clear materialized", texture.GetExternalIndex()); return true; } Bool VulkanRenderer::TryBlitToDefaultFramebufferWithShader(FrameContext::FrameData& frame, MG_State::GLState::FramebufferObject& readFbo, MG_State::GLState::FramebufferObject& drawFbo, GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLenum filter) { const Bool drawIsDefaultFbo = (&drawFbo == MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO.get()); if (!drawIsDefaultFbo) { return false; } BlitImageBinding srcBinding{}; BlitImageBinding dstBinding{}; if (!ResolveColorBlitBinding(readFbo, true, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, srcBinding) || !ResolveColorBlitBinding(drawFbo, false, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, dstBinding)) { return false; } if (srcBinding.trackedLayout == nullptr) { MGLOG_E("BlitFramebuffer skipped: shader blit to default framebuffer requires a texture-backed source framebuffer"); return false; } auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); if (activeRenderPass != nullptr) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } const auto& attachment = readFbo.GetAttachment(readFbo.GetReadBuffer()); auto sourceTexture = attachment.GetTexture(); MOBILEGL_ASSERT(sourceTexture != nullptr, "TryBlitToDefaultFramebufferWithShader: source texture is null"); const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *sourceTexture); MOBILEGL_ASSERT(clearReady, "TryBlitToDefaultFramebufferWithShader: failed to materialize pending clear for textureId=%d", sourceTexture->GetExternalIndex()); const Bool ready = m_textureManager->TransitionTextureForSampling(frame.commandBuffer, *sourceTexture); if (!ready) { MGLOG_E("BlitFramebuffer skipped: failed to transition source textureId=%d for sampling", sourceTexture->GetExternalIndex()); return false; } if (m_textureManager->SyncTextureAndGetDescriptor(*sourceTexture) == nullptr) { MGLOG_E("BlitFramebuffer skipped: failed to resolve source textureId=%d after sampling transition", sourceTexture->GetExternalIndex()); return false; } const VkImageView sourceImageView = m_textureManager->GetOrCreateSampledViewAtMipLevel(*sourceTexture, srcBinding.mipLevel); MOBILEGL_ASSERT(sourceImageView != VK_NULL_HANDLE, "TryBlitToDefaultFramebufferWithShader: failed to create sampled view for textureId=%d mip=%u", sourceTexture->GetExternalIndex(), srcBinding.mipLevel); auto& renderPassEntry = m_renderPassManager->GetOrCreateRenderPass(drawFbo, m_imageIndexAcquired); const Bool ok = VkRenderPassManager::BeginRenderPass(frame.commandBuffer, renderPassEntry); MOBILEGL_ASSERT(ok, "%s: BeginRenderPass failed", __func__); VkViewport viewport{}; viewport.x = 0.0f; viewport.y = 0.0f; viewport.width = static_cast(renderPassEntry.extent.x()); viewport.height = static_cast(renderPassEntry.extent.y()); viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; vkCmdSetViewport(frame.commandBuffer, 0, 1, &viewport); VkRect2D scissor{}; scissor.offset = {0, 0}; scissor.extent = {static_cast(renderPassEntry.extent.x()), static_cast(renderPassEntry.extent.y())}; vkCmdSetScissor(frame.commandBuffer, 0, 1, &scissor); MOBILEGL_ASSERT(m_blitResources.program != nullptr, "TryBlitToDefaultFramebufferWithShader: blit program is null"); const VkPipeline pipeline = GetOrCreateBlitPipeline(renderPassEntry); MOBILEGL_ASSERT(pipeline != VK_NULL_HANDLE, "TryBlitToDefaultFramebufferWithShader: blit pipeline is null"); vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); auto* blitProgramData = static_cast(m_blitResources.program->MapUBO()); MOBILEGL_ASSERT(blitProgramData != nullptr, "TryBlitToDefaultFramebufferWithShader: blit UBO is null"); std::fill(blitProgramData, blitProgramData + m_blitResources.program->GetUBOSize(), Uint8{0}); BlitUniformData blitUniformData{}; const float srcWidth = static_cast(srcBinding.extent.x()); const float srcHeight = static_cast(srcBinding.extent.y()); const float dstWidth = static_cast(dstBinding.extent.x()); const float dstHeight = static_cast(dstBinding.extent.y()); float dstNormWidth = dstWidth; float dstNormHeight = dstHeight; switch (m_swapchainObject.GetPreTransform()) { case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR: case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR: dstNormWidth = dstHeight; dstNormHeight = dstWidth; break; default: break; } blitUniformData.srcRect[0] = static_cast(srcX0) / srcWidth; blitUniformData.srcRect[1] = static_cast(srcY0) / srcHeight; blitUniformData.srcRect[2] = static_cast(srcX1 - srcX0) / srcWidth; blitUniformData.srcRect[3] = static_cast(srcY1 - srcY0) / srcHeight; blitUniformData.dstRect[0] = static_cast(dstX0) / dstNormWidth; blitUniformData.dstRect[1] = static_cast(dstY0) / dstNormHeight; blitUniformData.dstRect[2] = static_cast(dstX1 - dstX0) / dstNormWidth; blitUniformData.dstRect[3] = static_cast(dstY1 - dstY0) / dstNormHeight; blitUniformData.surfaceTransform = static_cast(ToBlitSurfaceTransform(m_swapchainObject.GetPreTransform())); auto writeUniform = [&](Int location, const void* data, SizeT size) { MOBILEGL_ASSERT(location >= 0, "TryBlitToDefaultFramebufferWithShader: invalid uniform location"); const Uint offset = m_blitResources.program->GetUniformOffset(static_cast(location)); MOBILEGL_ASSERT(offset + size <= m_blitResources.program->GetUBOSize(), "TryBlitToDefaultFramebufferWithShader: uniform write out of bounds"); memcpy(blitProgramData + offset, data, size); }; writeUniform(m_blitResources.srcRectLocation, blitUniformData.srcRect, sizeof(blitUniformData.srcRect)); writeUniform(m_blitResources.dstRectLocation, blitUniformData.dstRect, sizeof(blitUniformData.dstRect)); writeUniform(m_blitResources.surfaceTransformLocation, &blitUniformData.surfaceTransform, sizeof(blitUniformData.surfaceTransform)); const auto samplerBindingOverride = UniformManager::SamplerBindingOverride{ .binding = m_blitResources.samplerBinding, .texture = sourceTexture.get(), .sampler = (filter == GL_LINEAR ? m_blitResources.linearSampler.get() : m_blitResources.nearestSampler.get()), .imageView = sourceImageView, }; ProgramFactory::CompileOptionFlags blitTransformFlags = 0; const auto& blitProgramObj = m_programFactory->GetOrCreateProgram(*m_blitResources.program, blitTransformFlags); const Bool bound = m_uniformManager->BindProgramUniformBuffers( frame.commandBuffer, *m_blitResources.program, blitProgramObj, m_frameContext.GetCurrentFrameIndex(), &samplerBindingOverride); MOBILEGL_ASSERT(bound, "TryBlitToDefaultFramebufferWithShader: BindProgramUniformBuffers failed"); vkCmdDraw(frame.commandBuffer, 3, 1, 0, 0); return true; } void VulkanRenderer::BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { static constexpr GLbitfield kSupportedBlitMask = GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT; if ((mask & ~kSupportedBlitMask) != 0) { MGLOG_E("BlitFramebuffer skipped: unsupported mask bits=0x%x", static_cast(mask)); return; } const Bool isColorBlit = (mask & GL_COLOR_BUFFER_BIT) != 0; const Bool isDepthBlit = (mask & GL_DEPTH_BUFFER_BIT) != 0; if (!isColorBlit && !isDepthBlit) { return; } if (isColorBlit && isDepthBlit) { MGLOG_E("BlitFramebuffer skipped: combined color+depth blits are not supported yet (mask=0x%x)", static_cast(mask)); return; } if (filter != GL_NEAREST && filter != GL_LINEAR) { MGLOG_E("BlitFramebuffer skipped: unsupported filter=0x%x", static_cast(filter)); return; } if (isDepthBlit && filter != GL_NEAREST) { MGLOG_E("BlitFramebuffer skipped: depth blits currently require GL_NEAREST"); return; } auto readFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(); auto drawFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); MOBILEGL_ASSERT(readFbo != nullptr, "VulkanRenderer::BlitFramebuffer: read framebuffer is null"); MOBILEGL_ASSERT(drawFbo != nullptr, "VulkanRenderer::BlitFramebuffer: draw framebuffer is null"); auto& frame = m_frameContext.GetCurrent(); if (!frame.isCommandRecording) { m_frameContext.BeginCommandRecording(); m_uniformManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); } auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); if (activeRenderPass != nullptr) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } const Bool readIsDefaultFbo = (readFbo == MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO); const Bool drawIsDefaultFbo = (drawFbo == MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO); if (drawIsDefaultFbo && m_swapchainObject.GetPreTransform() != VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) { if (TryBlitToDefaultFramebufferWithShader(frame, *readFbo, *drawFbo, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, filter)) { return; } MGLOG_E("BlitFramebuffer skipped: rotated blit to default framebuffer requires a texture-backed source framebuffer"); return; } if (isDepthBlit) { BlitImageBinding srcBinding{}; BlitImageBinding dstBinding{}; if (!ResolveFramebufferBlitBinding(*readFbo, true, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, VK_IMAGE_ASPECT_DEPTH_BIT, srcBinding) || !ResolveFramebufferBlitBinding(*drawFbo, false, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, VK_IMAGE_ASPECT_DEPTH_BIT, dstBinding)) { return; } if (srcX1 < srcX0 || srcY1 < srcY0 || dstX1 < dstX0 || dstY1 < dstY0) { MGLOG_E("BlitFramebuffer skipped: depth blits with flipped rectangles are not supported yet"); return; } const Int srcWidth = srcX1 - srcX0; const Int srcHeight = srcY1 - srcY0; const Int dstWidth = dstX1 - dstX0; const Int dstHeight = dstY1 - dstY0; if (srcWidth <= 0 || srcHeight <= 0 || srcWidth != dstWidth || srcHeight != dstHeight) { MGLOG_E("BlitFramebuffer skipped: depth blits currently require matching source and destination extents"); return; } if (!readIsDefaultFbo) { const auto sourceAttachmentType = ResolveFramebufferCopyAttachmentType(*readFbo, true, srcBinding.aspectMask); const auto& sourceAttachment = readFbo->GetAttachment(sourceAttachmentType); auto sourceTexture = sourceAttachment.GetTexture(); MOBILEGL_ASSERT(sourceTexture != nullptr, "BlitFramebuffer: depth source texture attachment is null"); const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *sourceTexture); MOBILEGL_ASSERT(clearReady, "BlitFramebuffer: failed to materialize pending clear for depth source textureId=%d", sourceTexture->GetExternalIndex()); } const VkImageLayout srcOriginalLayout = readIsDefaultFbo ? m_swapchainObject.GetDepthStencilImageLayout(m_imageIndexAcquired) : *srcBinding.trackedLayout; if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { MGLOG_E("BlitFramebuffer skipped: depth source image layout is undefined"); return; } const VkImageLayout dstOriginalLayout = drawIsDefaultFbo ? m_swapchainObject.GetDepthStencilImageLayout(m_imageIndexAcquired) : *dstBinding.trackedLayout; const VkImageLayout dstRestoreLayout = dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL : dstOriginalLayout; VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcAccessMask = 0; GetImageTransitionSourceState(srcOriginalLayout, srcStageMask, srcAccessMask); if (readIsDefaultFbo) { VkImageLayout srcTrackedLayout = srcOriginalLayout; Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, srcTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, srcBinding.aspectMask, srcBinding.mipLevel, srcBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to transition swapchain depth source image", __func__); m_swapchainObject.SetDepthStencilImageLayout(m_imageIndexAcquired, srcTrackedLayout); } else { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, *srcBinding.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, srcBinding.aspectMask, srcBinding.mipLevel, srcBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to transition depth source image", __func__); } VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags dstAccessMask = 0; GetImageTransitionSourceState(dstOriginalLayout, dstStageMask, dstAccessMask); if (drawIsDefaultFbo) { VkImageLayout dstTrackedLayout = dstOriginalLayout; Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstBinding.image, dstTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstBinding.aspectMask, dstBinding.mipLevel, dstBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to transition swapchain depth destination image", __func__); m_swapchainObject.SetDepthStencilImageLayout(m_imageIndexAcquired, dstTrackedLayout); } else { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstBinding.image, *dstBinding.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstBinding.aspectMask, dstBinding.mipLevel, dstBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to transition depth destination image", __func__); } VkImageCopy copyRegion{}; copyRegion.srcSubresource.aspectMask = srcBinding.aspectMask; copyRegion.srcSubresource.mipLevel = srcBinding.mipLevel; copyRegion.srcSubresource.baseArrayLayer = 0; copyRegion.srcSubresource.layerCount = 1; copyRegion.srcOffset = {srcX0, srcY0, 0}; copyRegion.dstSubresource.aspectMask = dstBinding.aspectMask; copyRegion.dstSubresource.mipLevel = dstBinding.mipLevel; copyRegion.dstSubresource.baseArrayLayer = 0; copyRegion.dstSubresource.layerCount = 1; copyRegion.dstOffset = {dstX0, dstY0, 0}; copyRegion.extent = {static_cast(srcWidth), static_cast(srcHeight), 1}; vkCmdCopyImage(frame.commandBuffer, srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dstBinding.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©Region); VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcRestoreAccessMask = 0; GetImageTransitionDestinationState(srcOriginalLayout, srcRestoreStageMask, srcRestoreAccessMask); if (readIsDefaultFbo) { VkImageLayout srcTrackedLayout = m_swapchainObject.GetDepthStencilImageLayout(m_imageIndexAcquired); Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, srcTrackedLayout, srcOriginalLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask, VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, srcBinding.aspectMask, srcBinding.mipLevel, srcBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to restore swapchain depth source image layout", __func__); m_swapchainObject.SetDepthStencilImageLayout(m_imageIndexAcquired, srcTrackedLayout); } else { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, *srcBinding.trackedLayout, srcOriginalLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask, VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, srcBinding.aspectMask, srcBinding.mipLevel, srcBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to restore depth source image layout", __func__); } VkPipelineStageFlags dstRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags dstRestoreAccessMask = 0; GetImageTransitionDestinationState(dstRestoreLayout, dstRestoreStageMask, dstRestoreAccessMask); if (drawIsDefaultFbo) { VkImageLayout dstTrackedLayout = m_swapchainObject.GetDepthStencilImageLayout(m_imageIndexAcquired); Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstBinding.image, dstTrackedLayout, dstRestoreLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, dstBinding.aspectMask, dstBinding.mipLevel, dstBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to restore swapchain depth destination image layout", __func__); m_swapchainObject.SetDepthStencilImageLayout(m_imageIndexAcquired, dstTrackedLayout); } else { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstBinding.image, *dstBinding.trackedLayout, dstRestoreLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, dstBinding.aspectMask, dstBinding.mipLevel, dstBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to restore depth destination image layout", __func__); } return; } BlitImageBinding srcBinding{}; BlitImageBinding dstBinding{}; if (!ResolveColorBlitBinding(*readFbo, true, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, srcBinding) || !ResolveColorBlitBinding(*drawFbo, false, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, dstBinding)) { return; } if (!readIsDefaultFbo) { const auto& sourceAttachment = readFbo->GetAttachment(readFbo->GetReadBuffer()); auto sourceTexture = sourceAttachment.GetTexture(); MOBILEGL_ASSERT(sourceTexture != nullptr, "BlitFramebuffer: source texture attachment is null"); const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *sourceTexture); MOBILEGL_ASSERT(clearReady, "BlitFramebuffer: failed to materialize pending clear for source textureId=%d", sourceTexture->GetExternalIndex()); } VkImageLayout srcLayout = readIsDefaultFbo ? m_swapchainObject.GetImageLayout(m_imageIndexAcquired) : *srcBinding.trackedLayout; VkImageLayout dstLayout = drawIsDefaultFbo ? m_swapchainObject.GetImageLayout(m_imageIndexAcquired) : *dstBinding.trackedLayout; if (readIsDefaultFbo && srcLayout == VK_IMAGE_LAYOUT_UNDEFINED) { MGLOG_E("BlitFramebuffer skipped: swapchain source image layout is undefined"); return; } if (srcLayout == VK_IMAGE_LAYOUT_UNDEFINED) { MGLOG_E("BlitFramebuffer skipped: source image layout is undefined"); return; } VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcAccessMask = 0; GetImageTransitionSourceState(srcLayout, srcStageMask, srcAccessMask); if (readIsDefaultFbo) { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, srcLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, srcBinding.aspectMask); MOBILEGL_ASSERT(ok, "%s: failed to transition swapchain source image", __func__); m_swapchainObject.SetImageLayout(m_imageIndexAcquired, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); } else { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, *srcBinding.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, srcBinding.aspectMask, 0, srcBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to transition source image", __func__); } VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags dstAccessMask = 0; GetImageTransitionSourceState(dstLayout, dstStageMask, dstAccessMask); if (drawIsDefaultFbo) { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstBinding.image, dstLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstBinding.aspectMask); MOBILEGL_ASSERT(ok, "%s: failed to transition swapchain destination image", __func__); m_swapchainObject.SetImageLayout(m_imageIndexAcquired, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); } else { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstBinding.image, *dstBinding.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstBinding.aspectMask, 0, dstBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to transition destination image", __func__); } VkImageBlit blitRegion{}; blitRegion.srcSubresource.aspectMask = srcBinding.aspectMask; blitRegion.srcSubresource.mipLevel = srcBinding.mipLevel; blitRegion.srcSubresource.baseArrayLayer = 0; blitRegion.srcSubresource.layerCount = 1; blitRegion.srcOffsets[0] = {srcX0, srcY0, 0}; blitRegion.srcOffsets[1] = {srcX1, srcY1, 1}; blitRegion.dstSubresource.aspectMask = dstBinding.aspectMask; blitRegion.dstSubresource.mipLevel = dstBinding.mipLevel; blitRegion.dstSubresource.baseArrayLayer = 0; blitRegion.dstSubresource.layerCount = 1; blitRegion.dstOffsets[0] = {dstX0, dstY0, 0}; blitRegion.dstOffsets[1] = {dstX1, dstY1, 1}; vkCmdBlitImage(frame.commandBuffer, srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dstBinding.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blitRegion, filter == GL_LINEAR ? VK_FILTER_LINEAR : VK_FILTER_NEAREST); } void VulkanRenderer::CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) { if (width <= 0 || height <= 0) { return; } const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); if (textureTarget != TextureTarget::Texture2D) { RecordTextureCopyError(__func__, ErrorCode::InvalidOperation, "CopyTexSubImage2D currently only supports GL_TEXTURE_2D destinations."); return; } if (level < 0) { RecordTextureCopyError(__func__, ErrorCode::InvalidValue, "CopyTexSubImage2D level must be non-negative."); return; } auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit()); auto destinationTexture = textureUnit.GetBindingSlot(textureTarget).GetBoundObject(); if (destinationTexture == nullptr) { RecordTextureCopyError(__func__, ErrorCode::InvalidOperation, "CopyTexSubImage2D requires a bound destination texture."); return; } auto readFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(); if (readFbo == nullptr) { RecordTextureCopyError(__func__, ErrorCode::InvalidOperation, "CopyTexSubImage2D requires a framebuffer bound to GL_READ_FRAMEBUFFER."); return; } auto& frame = m_frameContext.GetCurrent(); if (!frame.isCommandRecording) { m_frameContext.BeginCommandRecording(); m_uniformManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); } if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } const Bool readIsDefaultFbo = (readFbo == MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO); BlitImageBinding dstBinding{}; if (!ResolveTextureCopyDestinationBinding(*destinationTexture, static_cast(level), *m_textureManager, dstBinding)) { RecordTextureCopyError(__func__, ErrorCode::InvalidOperation, "CopyTexSubImage2D failed to resolve the destination texture."); return; } BlitImageBinding srcBinding{}; if (!ResolveTextureCopySourceBinding(*readFbo, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, dstBinding.aspectMask, srcBinding)) { RecordTextureCopyError(__func__, ErrorCode::InvalidOperation, "CopyTexSubImage2D requires a complete read attachment compatible with the destination texture."); return; } if (!readIsDefaultFbo) { const auto sourceAttachmentType = ResolveFramebufferCopyAttachmentType(*readFbo, true, srcBinding.aspectMask); const auto& sourceAttachment = readFbo->GetAttachment(sourceAttachmentType); auto sourceTexture = sourceAttachment.GetTexture(); MOBILEGL_ASSERT(sourceTexture != nullptr, "CopyTexSubImage2D: source texture attachment is null"); const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *sourceTexture); MOBILEGL_ASSERT(clearReady, "CopyTexSubImage2D: failed to materialize pending clear for source textureId=%d", sourceTexture->GetExternalIndex()); } const Bool srcUsesSwapchainDepth = readIsDefaultFbo && (srcBinding.aspectMask & VK_IMAGE_ASPECT_COLOR_BIT) == 0; const VkImageLayout srcOriginalLayout = readIsDefaultFbo ? (srcUsesSwapchainDepth ? m_swapchainObject.GetDepthStencilImageLayout(m_imageIndexAcquired) : m_swapchainObject.GetImageLayout(m_imageIndexAcquired)) : *srcBinding.trackedLayout; if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { RecordTextureCopyError(__func__, ErrorCode::InvalidOperation, "CopyTexSubImage2D source image has undefined layout."); return; } const VkImageLayout dstOriginalLayout = *dstBinding.trackedLayout; const VkImageLayout dstRestoreLayout = dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED ? ((dstBinding.aspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0 ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) : dstOriginalLayout; VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcAccessMask = 0; GetImageTransitionSourceState(srcOriginalLayout, srcStageMask, srcAccessMask); if (readIsDefaultFbo) { VkImageLayout srcTrackedLayout = srcOriginalLayout; Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, srcTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, srcBinding.aspectMask, srcBinding.mipLevel, srcBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to transition swapchain source image", __func__); if (srcUsesSwapchainDepth) { m_swapchainObject.SetDepthStencilImageLayout(m_imageIndexAcquired, srcTrackedLayout); } else { m_swapchainObject.SetImageLayout(m_imageIndexAcquired, srcTrackedLayout); } } else { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, *srcBinding.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, srcBinding.aspectMask, srcBinding.mipLevel, srcBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to transition source image", __func__); } VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags dstAccessMask = 0; GetImageTransitionSourceState(dstOriginalLayout, dstStageMask, dstAccessMask); Bool dstReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstBinding.image, *dstBinding.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstBinding.aspectMask, dstBinding.mipLevel, dstBinding.mipLevelCount); MOBILEGL_ASSERT(dstReady, "%s: failed to transition destination image", __func__); VkImageCopy copyRegion{}; copyRegion.srcSubresource.aspectMask = srcBinding.aspectMask; copyRegion.srcSubresource.mipLevel = srcBinding.mipLevel; copyRegion.srcSubresource.baseArrayLayer = 0; copyRegion.srcSubresource.layerCount = 1; copyRegion.srcOffset = {x, y, 0}; copyRegion.dstSubresource.aspectMask = dstBinding.aspectMask; copyRegion.dstSubresource.mipLevel = dstBinding.mipLevel; copyRegion.dstSubresource.baseArrayLayer = 0; copyRegion.dstSubresource.layerCount = 1; copyRegion.dstOffset = {xoffset, yoffset, 0}; copyRegion.extent = {static_cast(width), static_cast(height), 1}; vkCmdCopyImage(frame.commandBuffer, srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dstBinding.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©Region); VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcRestoreAccessMask = 0; GetImageTransitionDestinationState(srcOriginalLayout, srcRestoreStageMask, srcRestoreAccessMask); if (readIsDefaultFbo) { VkImageLayout srcTrackedLayout = srcUsesSwapchainDepth ? m_swapchainObject.GetDepthStencilImageLayout(m_imageIndexAcquired) : m_swapchainObject.GetImageLayout(m_imageIndexAcquired); Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, srcTrackedLayout, srcOriginalLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask, VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, srcBinding.aspectMask, srcBinding.mipLevel, srcBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to restore swapchain source image layout", __func__); if (srcUsesSwapchainDepth) { m_swapchainObject.SetDepthStencilImageLayout(m_imageIndexAcquired, srcTrackedLayout); } else { m_swapchainObject.SetImageLayout(m_imageIndexAcquired, srcTrackedLayout); } } else { Bool ok = VkTextureManager::TransitionImageLayout( frame.commandBuffer, srcBinding.image, *srcBinding.trackedLayout, srcOriginalLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask, VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, srcBinding.aspectMask, srcBinding.mipLevel, srcBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to restore source image layout", __func__); } VkPipelineStageFlags dstRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags dstRestoreAccessMask = 0; GetImageTransitionDestinationState(dstRestoreLayout, dstRestoreStageMask, dstRestoreAccessMask); Bool dstRestored = VkTextureManager::TransitionImageLayout( frame.commandBuffer, dstBinding.image, *dstBinding.trackedLayout, dstRestoreLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, dstBinding.aspectMask, dstBinding.mipLevel, dstBinding.mipLevelCount); MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__); } void VulkanRenderer::GenerateMipmap(GLenum target) { const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target); const auto uploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target); MOBILEGL_ASSERT(textureTarget == TextureTarget::Texture2D || textureTarget == TextureTarget::Texture3D, "GenerateMipmap currently only supports GL_TEXTURE_2D and GL_TEXTURE_3D."); auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit()); auto texture = textureUnit.GetBindingSlot(textureTarget).GetBoundObject(); MOBILEGL_ASSERT(texture != nullptr, "GenerateMipmap requires a bound texture."); MOBILEGL_ASSERT(texture->IsComplete(), "GenerateMipmap requires a complete texture."); auto* mipmapTexture = dynamic_cast(texture.get()); MOBILEGL_ASSERT(mipmapTexture != nullptr, "GenerateMipmap requires a mipmapped texture object."); const Uint32 currentMipLevelCount = static_cast(mipmapTexture->GetMipmapLevelCount()); MOBILEGL_ASSERT(currentMipLevelCount > 0, "GenerateMipmap requires level 0 storage."); const Uint32 baseMipLevel = std::min(static_cast(texture->GetLevelRange().x()), currentMipLevelCount - 1); auto& frame = m_frameContext.GetCurrent(); if (!frame.isCommandRecording) { m_frameContext.BeginCommandRecording(); m_uniformManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); } if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *texture); MOBILEGL_ASSERT(clearReady, "GenerateMipmap: failed to materialize pending clear for textureId=%d", texture->GetExternalIndex()); auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*texture); MOBILEGL_ASSERT(resource != nullptr && resource->image != VK_NULL_HANDLE, "GenerateMipmap failed to sync the backend texture."); VkFormatProperties formatProperties{}; vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, resource->format, &formatProperties); const VkFormatFeatureFlags optimalTilingFeatures = formatProperties.optimalTilingFeatures; const Bool isDepthOrStencilTexture = (resource->aspect & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0; if (!isDepthOrStencilTexture && ((optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT) == 0 || (optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT) == 0)) { MGLOG_W("GenerateMipmap skipped for textureId=%d because Vulkan format %d does not support blit-based mip generation", texture->GetExternalIndex(), static_cast(resource->format)); return; } MOBILEGL_ASSERT(EnsureGenerateMipmapStorageAllocated(*mipmapTexture, uploadTarget, baseMipLevel), "GenerateMipmap could not allocate a full mip chain for this texture."); resource = m_textureManager->SyncTextureAndGetDescriptor(*texture); MOBILEGL_ASSERT(resource != nullptr && resource->image != VK_NULL_HANDLE, "GenerateMipmap failed to resync the backend texture after allocating mip storage."); MOBILEGL_ASSERT(resource->layout != VK_IMAGE_LAYOUT_UNDEFINED, "GenerateMipmap requires initialized base-level image data."); const IntVec3 storageBaseTexelSize = { static_cast(resource->extent.width), static_cast(resource->extent.height), static_cast(resource->depth), }; const IntVec3 baseTexelSize = ComputeMipTexelSize(storageBaseTexelSize, baseMipLevel); const Uint32 requiredMipLevelCount = baseMipLevel + ComputeFullMipLevelCount(baseTexelSize); const Uint32 generateMipLevelCount = std::min(requiredMipLevelCount, resource->mipLevels); if (generateMipLevelCount <= baseMipLevel + 1) { resource->layout = ResolveGenerateMipmapFinalLayout(resource->aspect); return; } const VkImageLayout originalLayout = resource->layout; const VkImageLayout finalLayout = ResolveGenerateMipmapFinalLayout(resource->aspect); if (isDepthOrStencilTexture) { const Bool depthReady = GenerateDepthMipmapWithShader(frame, *texture, *resource, baseMipLevel, generateMipLevelCount, storageBaseTexelSize, originalLayout, finalLayout); MOBILEGL_ASSERT(depthReady, "GenerateMipmap: depth/stencil fallback failed for textureId=%d target=%d internalFormat=%d vkFormat=%d", texture->GetExternalIndex(), static_cast(texture->GetTarget()), static_cast(texture->GetFormat()), static_cast(resource->format)); return; } const VkFilter blitFilter = (optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0 ? VK_FILTER_LINEAR : VK_FILTER_NEAREST; VkPipelineStageFlags originalSrcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags originalSrcAccessMask = 0; GetImageTransitionSourceState(originalLayout, originalSrcStageMask, originalSrcAccessMask); VkPipelineStageFlags finalDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags finalDstAccessMask = 0; GetImageTransitionDestinationState(finalLayout, finalDstStageMask, finalDstAccessMask); if (originalLayout != finalLayout) { if (baseMipLevel > 0) { VkImageLayout lowerMipLayout = originalLayout; const Bool lowerReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource->image, lowerMipLayout, finalLayout, originalSrcStageMask, finalDstStageMask, originalSrcAccessMask, finalDstAccessMask, resource->aspect, 0, baseMipLevel); MOBILEGL_ASSERT(lowerReady, "%s: failed to transition lower untouched mip levels", __func__); } if (generateMipLevelCount < resource->mipLevels) { VkImageLayout upperMipLayout = originalLayout; const Bool upperReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource->image, upperMipLayout, finalLayout, originalSrcStageMask, finalDstStageMask, originalSrcAccessMask, finalDstAccessMask, resource->aspect, generateMipLevelCount, resource->mipLevels - generateMipLevelCount); MOBILEGL_ASSERT(upperReady, "%s: failed to transition upper untouched mip levels", __func__); } } VkImageLayout srcMipLayout = originalLayout; Bool srcReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource->image, srcMipLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, originalSrcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, originalSrcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, resource->aspect, baseMipLevel, 1); MOBILEGL_ASSERT(srcReady, "%s: failed to transition base mip level to transfer source", __func__); for (Uint32 level = baseMipLevel + 1; level < generateMipLevelCount; ++level) { VkImageLayout dstMipLayout = originalLayout; Bool dstReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource->image, dstMipLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, originalSrcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, originalSrcAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, resource->aspect, level, 1); MOBILEGL_ASSERT(dstReady, "%s: failed to transition mip level %u to transfer destination", __func__, level); const IntVec3 srcTexelSize = ComputeMipTexelSize(storageBaseTexelSize, level - 1); const IntVec3 dstTexelSize = ComputeMipTexelSize(storageBaseTexelSize, level); VkImageBlit blitRegion{}; blitRegion.srcSubresource.aspectMask = resource->aspect; blitRegion.srcSubresource.mipLevel = level - 1; blitRegion.srcSubresource.baseArrayLayer = 0; blitRegion.srcSubresource.layerCount = 1; blitRegion.srcOffsets[0] = {0, 0, 0}; blitRegion.srcOffsets[1] = {srcTexelSize.x(), srcTexelSize.y(), srcTexelSize.z()}; blitRegion.dstSubresource.aspectMask = resource->aspect; blitRegion.dstSubresource.mipLevel = level; blitRegion.dstSubresource.baseArrayLayer = 0; blitRegion.dstSubresource.layerCount = 1; blitRegion.dstOffsets[0] = {0, 0, 0}; blitRegion.dstOffsets[1] = {dstTexelSize.x(), dstTexelSize.y(), dstTexelSize.z()}; vkCmdBlitImage(frame.commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, resource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blitRegion, blitFilter); VkImageLayout finishedSrcLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; Bool srcRestored = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource->image, finishedSrcLayout, finalLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, finalDstStageMask, VK_ACCESS_TRANSFER_READ_BIT, finalDstAccessMask, resource->aspect, level - 1, 1); MOBILEGL_ASSERT(srcRestored, "%s: failed to transition mip level %u to final layout", __func__, level - 1); if (level + 1 < generateMipLevelCount) { VkImageLayout nextSrcLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; Bool nextSrcReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource->image, nextSrcLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, resource->aspect, level, 1); MOBILEGL_ASSERT(nextSrcReady, "%s: failed to prepare mip level %u as next transfer source", __func__, level); } else { VkImageLayout lastMipLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; Bool lastMipReady = VkTextureManager::TransitionImageLayout( frame.commandBuffer, resource->image, lastMipLayout, finalLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, finalDstStageMask, VK_ACCESS_TRANSFER_WRITE_BIT, finalDstAccessMask, resource->aspect, level, 1); MOBILEGL_ASSERT(lastMipReady, "%s: failed to transition last mip level to final layout", __func__); } } resource->layout = finalLayout; } void VulkanRenderer::DrawArrays(const DrawCmd& payload) { auto& frame = m_frameContext.GetCurrent(); if (!SetupDraw(frame, payload.mode, 0)) { return; } MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__); VkCommandBuffer& commandBuffer = frame.commandBuffer; vkCmdDraw(commandBuffer, payload.params.vertexCount, payload.params.instanceCount, payload.params.firstVertex, payload.params.firstInstance); } void VulkanRenderer::DrawElements(const DrawIndexedCmd& payload) { auto& frame = m_frameContext.GetCurrent(); if (!SetupDraw(frame, payload.mode, DrawSetupAspect::IndexBuffer, &payload.indexBufferView)) { return; } MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__); VkCommandBuffer& commandBuffer = frame.commandBuffer; vkCmdDrawIndexed(commandBuffer, payload.params.indexCount, payload.params.instanceCount, payload.params.firstIndex, payload.params.vertexOffset, payload.params.firstInstance); } void VulkanRenderer::MultiDrawElements(const MultiDrawIndexedCmd& payload) { auto& frame = m_frameContext.GetCurrent(); if (!SetupDraw(frame, payload.mode, DrawSetupAspect::IndexBuffer, &payload.indexBufferView)) { return; } MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__); VkCommandBuffer& commandBuffer = frame.commandBuffer; for (Uint32 idraw = 0; idraw < payload.drawCount; ++idraw) { vkCmdDrawIndexed(commandBuffer, payload.pParams[idraw].indexCount, payload.pParams[idraw].instanceCount, payload.pParams[idraw].firstIndex, payload.pParams[idraw].vertexOffset, payload.pParams[idraw].firstInstance); } } void VulkanRenderer::Present() { MOBILEGL_ASSERT(m_imageIndexAcquired < m_swapchainObject.GetImageCount(), "Present, acquired image index out of range"); auto& frame = m_frameContext.GetCurrent(); auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); if (activeRenderPass) VkRenderPassManager::EndRenderPass(frame.commandBuffer); if (frame.isCommandRecording) { m_frameContext.EndCommandRecording(); frame.hasCommandBufferRecorded = true; } const auto acquiredImageLayout = m_swapchainObject.GetImageLayout(m_imageIndexAcquired); const Bool needsLayoutTransitionForPresent = m_frameContext.TransitionToPresent(m_swapchainObject.GetImage(m_imageIndexAcquired), acquiredImageLayout); const Bool shouldSubmitCommandBuffer = frame.hasCommandBufferRecorded || needsLayoutTransitionForPresent; // 1) Submit current frame work. auto submitPacket = m_frameContext.GetSubmitInfo(shouldSubmitCommandBuffer, m_imageIndexAcquired); VK_VERIFY(vkQueueSubmit(m_graphicsQueue, 1, &submitPacket.submitInfo, frame.imageInFlightFence)); frame.isCommandRecording = false; frame.hasCommandBufferRecorded = false; m_swapchainObject.SetImageLayout(m_imageIndexAcquired, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); // 2) Present current frame. auto presentPacket = m_frameContext.GetPresentInfo(m_swapchainObject.GetHandle(), m_imageIndexAcquired); auto result = vkQueuePresentKHR(m_presentQueue, &presentPacket.presentInfo); if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { MGLOG_D("Present, vkQueuePresentKHR got %d, recreating swapchain", result); RecreateSwapchain(); result = VK_SUCCESS; } VK_VERIFY(result, "Present, vkQueuePresentKHR"); // 3) Advance frame slot. m_frameContext.AdvanceToNext(); // 4) Wait/reset/acquire for next frame. result = m_frameContext.WaitAndAcquireNextImage(m_device, m_swapchainObject.GetHandle(), m_imageIndexAcquired); if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { MGLOG_D("Present, vkAcquireNextImageKHR got %d, recreating swapchain", result); RecreateSwapchain(); result = VK_SUCCESS; } VK_VERIFY(result, "Present, vkAcquireNextImageKHR"); CollectDeferredDepthMipmapCleanup(m_frameContext.GetCurrentFrameIndex()); m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_transientVertexIndexBufferSlicesThisFrame.clear(); } void VulkanRenderer::CreateInstance() { m_extensions = EnumerateInstanceExtensions(); MGLOG_I("Got %d Vulkan instance extensions: ", m_extensions.size()); for (auto& extension : m_extensions) { MGLOG_I(" %s (r.%u)", extension.extensionName, extension.specVersion); } Bool validationLayerAvailable = CheckValidationLayerSupport(); MGLOG_I("Validation layers %s.", validationLayerAvailable ? "available" : "not available"); MGLOG_I("Validation layers %s.", m_config.EnableValidationLayers ? "requested" : "not requested"); if (m_config.EnableValidationLayers && !validationLayerAvailable) { MGLOG_I("Validation layers not available! Disabling validation layers."); } m_validationLayersEnabled = m_config.EnableValidationLayers && validationLayerAvailable; // ---------------- App info ------------------- VkApplicationInfo appInfo = {}; appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; appInfo.pApplicationName = m_config.AppName.c_str(); appInfo.applicationVersion = VK_MAKE_VERSION(m_config.CacheVersion, 0, 0); appInfo.pEngineName = "MobileGL"; appInfo.engineVersion = VK_MAKE_VERSION(m_config.Version.Major, m_config.Version.Minor, m_config.Version.Patch); #ifdef VK_USE_PLATFORM_WIN32_KHR appInfo.apiVersion = VK_API_VERSION_1_3; #else appInfo.apiVersion = VK_API_VERSION_1_1; #endif // ---------------- Instance info ------------------- VkInstanceCreateInfo instanceInfo = {}; instanceInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; instanceInfo.pApplicationInfo = &appInfo; // Extensions Vector exts = {VK_KHR_SURFACE_EXTENSION_NAME, #ifdef VK_USE_PLATFORM_ANDROID_KHR VK_KHR_ANDROID_SURFACE_EXTENSION_NAME #elif defined VK_USE_PLATFORM_WIN32_KHR VK_KHR_WIN32_SURFACE_EXTENSION_NAME #elif defined VK_USE_PLATFORM_METAL_EXT VK_EXT_METAL_SURFACE_EXTENSION_NAME #else #warning "VulkanContext::CreateInstance: VK_KHR_*_surface extension not defined on this platform" #endif }; // TODO: support more platforms #if defined(VK_USE_PLATFORM_METAL_EXT) exts.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); instanceInfo.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; #endif if (m_validationLayersEnabled) { exts.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); } instanceInfo.enabledExtensionCount = exts.size(); instanceInfo.ppEnabledExtensionNames = exts.data(); auto debugMessengerCreateInfo = PopulateDebugMessengerCreateInfo(); // Layers if (m_validationLayersEnabled) { MGLOG_I("Enabling validation layer..."); instanceInfo.enabledLayerCount = static_cast(std::size(s_validationLayerNames)); instanceInfo.ppEnabledLayerNames = s_validationLayerNames; instanceInfo.pNext = &debugMessengerCreateInfo; } else { instanceInfo.enabledLayerCount = 0; instanceInfo.pNext = nullptr; } VK_VERIFY(vkCreateInstance(&instanceInfo, nullptr, &m_instance), "vkCreateInstance failed"); if (m_validationLayersEnabled) VK_VERIFY(SetupDebugMessenger()); } VkResult VulkanRenderer::SetupDebugMessenger() { auto createInfo = PopulateDebugMessengerCreateInfo(); auto vkCreateDebugUtilsMessengerEXT = (PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(m_instance, "vkCreateDebugUtilsMessengerEXT"); if (!vkCreateDebugUtilsMessengerEXT) return VK_ERROR_EXTENSION_NOT_PRESENT; VK_VERIFY(vkCreateDebugUtilsMessengerEXT(m_instance, &createInfo, nullptr, &m_debugMessenger)); return VK_SUCCESS; } VkResult VulkanRenderer::DestroyDebugMessenger() { if (m_debugMessenger != VK_NULL_HANDLE) { auto func = (PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr(m_instance, "vkDestroyDebugUtilsMessengerEXT"); if (func != nullptr) { func(m_instance, m_debugMessenger, nullptr); } else { return VK_ERROR_EXTENSION_NOT_PRESENT; } } return VK_SUCCESS; } VkDebugUtilsMessengerCreateInfoEXT VulkanRenderer::PopulateDebugMessengerCreateInfo() { VkDebugUtilsMessengerCreateInfoEXT createInfo{}; createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; createInfo.pfnUserCallback = DebugCallback; createInfo.pUserData = this; return createInfo; } void VulkanRenderer::PickPhysicalDevice() { Uint32 deviceCount = 0; vkEnumeratePhysicalDevices(m_instance, &deviceCount, nullptr); if (deviceCount == 0) { MGLOG_E("No physical devices supporting Vulkan found."); } else { MGLOG_I("Found %d physical device(s).", deviceCount); } MOBILEGL_ASSERT(deviceCount > 0, "No physical devices found."); Vector devices(deviceCount); vkEnumeratePhysicalDevices(m_instance, &deviceCount, devices.data()); for (Int i = 0; i < deviceCount; i++) { if (GetMoreCapablePhysicalDevice(devices[i], m_surface, m_physicalDevice, m_physicalDevice)) MGLOG_I("Picked physical device %d.", i); } if (m_physicalDevice.handle == VK_NULL_HANDLE) { m_physicalDevice.handle = devices[0]; vkGetPhysicalDeviceProperties(devices[0], &m_physicalDevice.properties); MGLOG_I("No suitable physical device picked yet, defaulting to device 0."); MGLOG_W("No graphics queue found on physical device. Picking a device that doesn't do graphics?"); } } Bool VulkanRenderer::GetMoreCapablePhysicalDevice(VkPhysicalDevice newVkDevice, VkSurfaceKHR surface, const PhysicalDevice& otherDevice, PhysicalDevice& outBetterDevice) { const auto deviceTypeToStr = [](VkPhysicalDeviceType type) { switch (type) { case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return "INTEGRATED_GPU"; case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return "DISCRETE_GPU"; case VK_PHYSICAL_DEVICE_TYPE_CPU: return "CPU"; case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return "VIRTUAL_GPU"; case VK_PHYSICAL_DEVICE_TYPE_OTHER: return "OTHER"; default: return "UNKNOWN"; } }; PhysicalDevice newDevice; newDevice.handle = newVkDevice; vkGetPhysicalDeviceProperties(newVkDevice, &newDevice.properties); const auto& deviceProperties = newDevice.properties; auto apiVersion = deviceProperties.apiVersion; MGLOG_I(" %s (Vulkan %d.%d.%d, %s)", deviceProperties.deviceName, VK_VERSION_MAJOR(apiVersion), VK_VERSION_MINOR(apiVersion), VK_VERSION_PATCH(apiVersion), deviceTypeToStr(deviceProperties.deviceType)); // Check device extensions (including swapchain extension) Bool deviceExtSupported = IsNecessaryDeviceExtensionSupported(newVkDevice); if (!deviceExtSupported) { outBetterDevice = otherDevice; MGLOG_I(" Ignored physical device. (Reason: Some of the required device extension not supported on this " "device)"); return false; } // Check swapchain capabilities auto swapchainCapabilities = SwapchainObject::GetSwapchainCapabilities(newVkDevice, surface); if (!swapchainCapabilities.IsComplete()) { outBetterDevice = otherDevice; MGLOG_I(" Ignored physical device. (Reason: Swapchain capabilities not met)"); return false; } // Check queue families Vector queueFamilies = GetQueueFamilyFromPhysicalDevice(newVkDevice); newDevice.queueFamilies.graphicsFamily = GetQueueFamilyIndex(queueFamilies, VK_QUEUE_GRAPHICS_BIT); if (newDevice.queueFamilies.graphicsFamily == -1) { outBetterDevice = otherDevice; MGLOG_I(" Ignored physical device. (Reason: No graphics queue family)"); return false; } newDevice.queueFamilies.presentFamily = GetPresentQueueFamilyIndex(newDevice, surface, queueFamilies, newDevice.queueFamilies.graphicsFamily); if (newDevice.queueFamilies.presentFamily == -1) { outBetterDevice = otherDevice; MGLOG_I(" Ignored physical device. (Reason: No present queue family)"); return false; } // Pick discrete GPU if (newDevice.properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU && otherDevice.properties.deviceType != VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { outBetterDevice = newDevice; MGLOG_I(" Picked physical device. (Reason: Discrete GPU)"); return true; } // Pick integrated GPU if no discrete GPU if (newDevice.properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU && otherDevice.properties.deviceType != VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { outBetterDevice = newDevice; MGLOG_I(" Picked physical device. (Reason: Integrated GPU and no discrete one found yet)"); return true; } // Ignore other GPU when discrete GPU found if (newDevice.properties.deviceType != VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU && otherDevice.properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { outBetterDevice = newDevice; MGLOG_I(" Ignored physical device. (Reason: Already picked discrete GPU)"); return false; } return false; } Bool VulkanRenderer::IsNecessaryDeviceExtensionSupported(VkPhysicalDevice device) { const Vector availableExtensions = EnumerateDeviceExtensions(device); MGLOG_I("Got %u Vulkan device extensions: ", static_cast(availableExtensions.size())); for (auto& extension : availableExtensions) { MGLOG_I(" %s (r.%u)", extension.extensionName, extension.specVersion); } for (SizeT i = 0; i < std::size(s_deviceExtensionNames); ++i) { if (!IsExtensionSupported(availableExtensions, s_deviceExtensionNames[i])) { MGLOG_I("Required extension not found: %s", s_deviceExtensionNames[i]); return false; } MGLOG_I("Required extension found: %s", s_deviceExtensionNames[i]); } return true; } void VulkanRenderer::CreateLogicalDeviceAndQueues() { Float queuePriority = 1.0f; Vector queueCreateInfos; MOBILEGL_ASSERT(m_physicalDevice.queueFamilies.graphicsFamily != -1, "Graphics queue family not found."); VkDeviceQueueCreateInfo& gfxQueueCreateInfo = queueCreateInfos.emplace_back(); gfxQueueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; gfxQueueCreateInfo.queueFamilyIndex = m_physicalDevice.queueFamilies.graphicsFamily; gfxQueueCreateInfo.queueCount = 1; gfxQueueCreateInfo.pQueuePriorities = &queuePriority; if (m_physicalDevice.queueFamilies.graphicsFamily != m_physicalDevice.queueFamilies.presentFamily) { MOBILEGL_ASSERT(m_physicalDevice.queueFamilies.presentFamily != -1, "Present queue family not found."); VkDeviceQueueCreateInfo& presentQueueCreateInfo = queueCreateInfos.emplace_back(); presentQueueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; presentQueueCreateInfo.queueFamilyIndex = m_physicalDevice.queueFamilies.presentFamily; presentQueueCreateInfo.queueCount = 1; presentQueueCreateInfo.pQueuePriorities = &queuePriority; } VkPhysicalDeviceFeatures supportedDeviceFeatures{}; vkGetPhysicalDeviceFeatures(m_physicalDevice.handle, &supportedDeviceFeatures); VkPhysicalDeviceFeatures deviceFeatures{}; deviceFeatures.geometryShader = supportedDeviceFeatures.geometryShader; deviceFeatures.independentBlend = supportedDeviceFeatures.independentBlend; deviceFeatures.shaderClipDistance = supportedDeviceFeatures.shaderClipDistance; deviceFeatures.shaderCullDistance = supportedDeviceFeatures.shaderCullDistance; VkDeviceCreateInfo deviceCreateInfo{}; deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; deviceCreateInfo.pQueueCreateInfos = queueCreateInfos.data(); deviceCreateInfo.queueCreateInfoCount = queueCreateInfos.size(); deviceCreateInfo.pEnabledFeatures = &deviceFeatures; if (m_validationLayersEnabled) { deviceCreateInfo.enabledLayerCount = static_cast(std::size(s_validationLayerNames)); deviceCreateInfo.ppEnabledLayerNames = s_validationLayerNames; } else { deviceCreateInfo.enabledLayerCount = 0; } Vector enabledDeviceExtensions; enabledDeviceExtensions.reserve(std::size(s_deviceExtensionNames) + 2); for (const char* extensionName : s_deviceExtensionNames) { enabledDeviceExtensions.push_back(extensionName); } const Vector availableExtensions = EnumerateDeviceExtensions(m_physicalDevice.handle); ResolveOptionalDeviceExtensions(availableExtensions, enabledDeviceExtensions); MGLOG_I("VK_KHR_draw_indirect_count enabled: %s", m_drawIndirectCountExtensionEnabled ? "true" : "false"); m_indexTypeUint8ExtensionEnabled = false; const char* indexTypeUint8ExtensionName = nullptr; if (IsExtensionSupported(availableExtensions, VK_KHR_INDEX_TYPE_UINT8_EXTENSION_NAME)) { indexTypeUint8ExtensionName = VK_KHR_INDEX_TYPE_UINT8_EXTENSION_NAME; } else if (IsExtensionSupported(availableExtensions, VK_EXT_INDEX_TYPE_UINT8_EXTENSION_NAME)) { indexTypeUint8ExtensionName = VK_EXT_INDEX_TYPE_UINT8_EXTENSION_NAME; } VkPhysicalDeviceIndexTypeUint8Features indexTypeUint8Features{}; indexTypeUint8Features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_INDEX_TYPE_UINT8_FEATURES; if (indexTypeUint8ExtensionName != nullptr) { VkPhysicalDeviceFeatures2 featureQuery{}; featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; featureQuery.pNext = &indexTypeUint8Features; auto getPhysicalDeviceFeatures2 = reinterpret_cast( vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceFeatures2")); if (getPhysicalDeviceFeatures2 == nullptr) { getPhysicalDeviceFeatures2 = reinterpret_cast( vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceFeatures2KHR")); } MOBILEGL_ASSERT(getPhysicalDeviceFeatures2 != nullptr, "CreateLogicalDeviceAndQueues: vkGetPhysicalDeviceFeatures2 is unavailable"); getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery); if (indexTypeUint8Features.indexTypeUint8 == VK_TRUE) { if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions, indexTypeUint8ExtensionName)) { enabledDeviceExtensions.push_back(indexTypeUint8ExtensionName); } m_indexTypeUint8ExtensionEnabled = true; indexTypeUint8Features.pNext = const_cast(deviceCreateInfo.pNext); deviceCreateInfo.pNext = &indexTypeUint8Features; MGLOG_I("Enabled optional device extension: %s", indexTypeUint8ExtensionName); } else { MGLOG_W("%s is advertised, but indexTypeUint8 feature is unavailable; uint8 index buffers will stay disabled", indexTypeUint8ExtensionName); } } else { MGLOG_W("VK_KHR_index_type_uint8 / VK_EXT_index_type_uint8 not supported; uint8 index buffers will stay disabled"); } deviceCreateInfo.enabledExtensionCount = static_cast(enabledDeviceExtensions.size()); deviceCreateInfo.ppEnabledExtensionNames = enabledDeviceExtensions.data(); MGLOG_I("Device feature support: geometryShader=%s independentBlend=%s shaderClipDistance=%s shaderCullDistance=%s", supportedDeviceFeatures.geometryShader ? "true" : "false", supportedDeviceFeatures.independentBlend ? "true" : "false", supportedDeviceFeatures.shaderClipDistance ? "true" : "false", supportedDeviceFeatures.shaderCullDistance ? "true" : "false"); MGLOG_I("Device feature enabled: geometryShader=%s independentBlend=%s shaderClipDistance=%s shaderCullDistance=%s", deviceFeatures.geometryShader ? "true" : "false", deviceFeatures.independentBlend ? "true" : "false", deviceFeatures.shaderClipDistance ? "true" : "false", deviceFeatures.shaderCullDistance ? "true" : "false"); VK_VERIFY(vkCreateDevice(m_physicalDevice.handle, &deviceCreateInfo, nullptr, &m_device), "vkCreateDevice"); s_vkCmdDrawIndexedIndirectCount = reinterpret_cast( vkGetDeviceProcAddr(m_device, "vkCmdDrawIndexedIndirectCountKHR")); if (s_vkCmdDrawIndexedIndirectCount == nullptr) { s_vkCmdDrawIndexedIndirectCount = reinterpret_cast( vkGetDeviceProcAddr(m_device, "vkCmdDrawIndexedIndirectCount")); } if (m_drawIndirectCountExtensionEnabled && s_vkCmdDrawIndexedIndirectCount == nullptr) { MGLOG_W("VK_KHR_draw_indirect_count enabled but vkCmdDrawIndexedIndirectCount entry point is missing, will continue as if VK_KHR_draw_indirect_count is not supported!"); m_drawIndirectCountExtensionEnabled = false; } MGLOG_I("index type uint8 enabled: %s", m_indexTypeUint8ExtensionEnabled ? "true" : "false"); MGLOG_I("Logical device created."); // Queues vkGetDeviceQueue(m_device, m_physicalDevice.queueFamilies.graphicsFamily, 0, &m_graphicsQueue); vkGetDeviceQueue(m_device, m_physicalDevice.queueFamilies.presentFamily, 0, &m_presentQueue); MGLOG_I("Queues got successfully."); } void VulkanRenderer::CreateAllocator() { MOBILEGL_ASSERT(m_instance != VK_NULL_HANDLE, "CreateAllocator requires valid VkInstance"); MOBILEGL_ASSERT(m_physicalDevice.handle != VK_NULL_HANDLE, "CreateAllocator requires valid physical device"); MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "CreateAllocator requires valid VkDevice"); if (m_allocator != nullptr) { return; } VmaAllocatorCreateInfo allocatorInfo{}; VmaVulkanFunctions vulkanFunctions{}; vulkanFunctions.vkGetInstanceProcAddr = vkGetInstanceProcAddr; vulkanFunctions.vkGetDeviceProcAddr = vkGetDeviceProcAddr; allocatorInfo.instance = m_instance; allocatorInfo.physicalDevice = m_physicalDevice.handle; allocatorInfo.device = m_device; allocatorInfo.pVulkanFunctions = &vulkanFunctions; allocatorInfo.vulkanApiVersion = VK_API_VERSION_1_0; VK_VERIFY(vmaCreateAllocator(&allocatorInfo, &m_allocator), "vmaCreateAllocator"); } void VulkanRenderer::DestroyAllocator() { if (m_allocator != nullptr) { vmaDestroyAllocator(m_allocator); m_allocator = nullptr; } } void VulkanRenderer::CreateSwapchain() { m_swapchainObject.Create(m_device, m_physicalDevice.handle, m_surface, static_cast(m_physicalDevice.queueFamilies.graphicsFamily), static_cast(m_physicalDevice.queueFamilies.presentFamily), m_config.MaxFramesInFlight); } void VulkanRenderer::CreateCommandPool() { VkCommandPoolCreateInfo createInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; createInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; createInfo.queueFamilyIndex = m_physicalDevice.queueFamilies.graphicsFamily; VK_VERIFY(vkCreateCommandPool(m_device, &createInfo, nullptr, &m_commandPool)); MGLOG_I("Command pool created"); } void VulkanRenderer::CreateSurface() { #if defined VK_USE_PLATFORM_ANDROID_KHR auto* nativeWindow = static_cast(m_window); if (!nativeWindow) throw RuntimeError("ANativeWindowType is null"); VkAndroidSurfaceCreateInfoKHR sci{VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR}; sci.window = nativeWindow; VK_VERIFY(vkCreateAndroidSurfaceKHR(m_instance, &sci, nullptr, &m_surface), "vkCreateAndroidSurfaceKHR failed"); #elif defined VK_USE_PLATFORM_WIN32_KHR auto hwnd = static_cast(m_window); MOBILEGL_ASSERT(hwnd, "HWND is null"); VkWin32SurfaceCreateInfoKHR sci{VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR}; sci.hwnd = hwnd; VK_VERIFY(vkCreateWin32SurfaceKHR(m_instance, &sci, nullptr, &m_surface), "vkCreateWin32SurfaceKHR failed"); #elif defined VK_USE_PLATFORM_METAL_EXT MOBILEGL_ASSERT(m_window, "CAMetalLayer is null"); VkMetalSurfaceCreateInfoEXT sci{VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT}; sci.pLayer = reinterpret_cast(m_window); VK_VERIFY(vkCreateMetalSurfaceEXT(m_instance, &sci, nullptr, &m_surface), "vkCreateMetalSurfaceEXT failed"); #else // #warning "VulkanRenderer::Initialize called on a platform which is not supported yet" MGLOG_W("VulkanRenderer::Initialize called on a platform which is not supported yet"); // TODO: support more // platforms #endif } Vector VulkanRenderer::GetQueueFamilyFromPhysicalDevice(VkPhysicalDevice device) { Uint32 queueFamilyCount = 0; vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr); Vector queueFamilies(queueFamilyCount); vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data()); return queueFamilies; } Int VulkanRenderer::GetQueueFamilyIndex(const Vector& queueFamilies, VkQueueFlagBits flag) { for (Uint32 i = 0; i < queueFamilies.size(); i++) { if (queueFamilies[i].queueFlags & flag) { return i; } } return -1; } Int VulkanRenderer::GetPresentQueueFamilyIndex(const PhysicalDevice& physicalDevice, VkSurfaceKHR surface, const Vector& queueFamilies, Int preferredFamilyIndex) { if (preferredFamilyIndex != -1) { VkBool32 supportsPresent = false; vkGetPhysicalDeviceSurfaceSupportKHR(physicalDevice.handle, preferredFamilyIndex, surface, &supportsPresent); if (supportsPresent) return preferredFamilyIndex; } for (Uint32 i = 0; i < queueFamilies.size(); i++) { VkBool32 supportsPresent = false; vkGetPhysicalDeviceSurfaceSupportKHR(physicalDevice.handle, i, surface, &supportsPresent); if (supportsPresent) return i; } return -1; } Vector VulkanRenderer::EnumerateInstanceExtensions() { Uint32 extensionCount = 0; VK_VERIFY(vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr)); Vector extensions(extensionCount); vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data()); return extensions; } Vector VulkanRenderer::EnumerateDeviceExtensions(VkPhysicalDevice device) { Uint32 extensionCount = 0; VK_VERIFY(vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, nullptr)); Vector extensions(extensionCount); VK_VERIFY(vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, extensions.data())); return extensions; } Bool VulkanRenderer::IsExtensionSupported(const Vector& availableExtensions, const char* extensionName) { for (const auto& extension : availableExtensions) { if (strcmp(extension.extensionName, extensionName) == 0) { return true; } } return false; } Bool VulkanRenderer::IsExtensionAlreadyEnabled(const Vector& enabledExtensions, const char* extensionName) { return std::any_of(enabledExtensions.begin(), enabledExtensions.end(), [&extensionName](const String& name) { return name == extensionName; }); } Bool VulkanRenderer::EnableOptionalDeviceExtension(const Vector& availableExtensions, Vector& inOutEnabledExtensions, const char* extensionName) { if (!IsExtensionSupported(availableExtensions, extensionName)) { MGLOG_I("Optional device extension not supported: %s", extensionName); return false; } if (!IsExtensionAlreadyEnabled(inOutEnabledExtensions, extensionName)) { inOutEnabledExtensions.push_back(extensionName); } MGLOG_I("Enabled optional device extension: %s", extensionName); return true; } void VulkanRenderer::ResolveOptionalDeviceExtensions(const Vector& availableExtensions, Vector& inOutEnabledExtensions) { m_drawIndirectCountExtensionEnabled = EnableOptionalDeviceExtension(availableExtensions, inOutEnabledExtensions, VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME); #ifdef VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME EnableOptionalDeviceExtension(availableExtensions, inOutEnabledExtensions, VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME); #endif } Bool VulkanRenderer::CheckValidationLayerSupport() { Uint32 layerCount = 0; VK_VERIFY(vkEnumerateInstanceLayerProperties(&layerCount, nullptr)); Vector layers(layerCount); VK_VERIFY(vkEnumerateInstanceLayerProperties(&layerCount, layers.data())); for (const char* layerName : s_validationLayerNames) { for (const auto& layerProperties : layers) { if (strcmp(layerName, layerProperties.layerName) == 0) { return true; } } } return false; } void VulkanRenderer::ShutdownSwapchain() { MOBILEGL_ASSERT(m_renderPassManager != nullptr, "ShutdownSwapchain: render pass manager is null"); m_renderPassManager->Shutdown(); m_swapchainObject.Shutdown(m_device); } void VulkanRenderer::RecreateSwapchain() { // Handle cases like minimize on Windows, where swapchain could return a 0x0 extent const auto swapchainCapabilities = SwapchainObject::GetSwapchainCapabilities(m_physicalDevice.handle, m_surface); if (swapchainCapabilities.capabilities.currentExtent.width == 0 || swapchainCapabilities.capabilities.currentExtent.height == 0) { return; } vkDeviceWaitIdle(m_device); DestroyDeferredDepthMipmapCleanup(); m_deferredDepthMipmapCleanup.assign(m_frameContext.GetFrameCount(), {}); ShutdownSwapchain(); CreateSwapchain(); VK_VERIFY(m_frameContext.InitializeSwapchainSemaphores(m_device, static_cast(m_swapchainObject.GetImageCount())), "RecreateSwapchain, InitializeSwapchainSemaphores"); MOBILEGL_ASSERT(m_renderPassManager != nullptr, "RecreateSwapchain: render pass manager is null"); Bool ok = m_renderPassManager->Initialize(); MOBILEGL_ASSERT(ok, "RecreateSwapchain: render pass manager initialization failed"); if (m_pipelineFactory) { m_pipelineFactory->DestroyAll(); } if (m_frameContext.GetFrameCount() > 0) { m_frameContext.GetCurrent().isCommandRecording = false; m_frameContext.GetCurrent().hasCommandBufferRecorded = false; } const Bool okArena = m_bufferManager.RecreateTransientArenas(m_frameContext.GetFrameCount()); MOBILEGL_ASSERT(okArena, "RecreateSwapchain: buffer manager transient arena initialization failed"); if (m_frameContext.GetFrameCount() > 0) { if (m_textureManager) { m_textureManager->BeginFrame(m_frameContext.GetCurrentFrameIndex()); } m_bufferManager.BeginFrame(m_frameContext.GetCurrentFrameIndex()); m_transientVertexIndexBufferSlicesThisFrame.clear(); } } const PhysicalDevice& VulkanRenderer::GetPhysicalDevice() const { return m_physicalDevice; } Bool VulkanRenderer::IsDrawIndirectCountExtensionEnabled() const { return m_drawIndirectCountExtensionEnabled; } void VulkanRenderer::ClearAttachmentsOnActiveRenderPass(VkCommandBuffer commandBuffer, const RenderPassEntry &compatibleRenderPassEntry) { auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); MOBILEGL_ASSERT(activeRenderPass, "No render pass active"); VkClearRect clearRect{}; clearRect.rect.offset = {0, 0}; clearRect.rect.extent = { static_cast(activeRenderPass->extent.x()), static_cast(activeRenderPass->extent.y()) }; clearRect.baseArrayLayer = 0; clearRect.layerCount = 1; for (const auto& pending : compatibleRenderPassEntry.pendingClearAttachments) { if (!pending.texture) { continue; } ClearAttachmentPayload clearPayload{}; if (!m_clearManager->GetPendingClear(pending.texture, clearPayload)) { continue; } VkClearAttachment clearAttachment{}; clearAttachment.clearValue.depthStencil = {1.0f, 0}; if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0) { clearAttachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; clearAttachment.colorAttachment = pending.attachmentIndex; clearAttachment.clearValue.color = { clearPayload.color.x(), clearPayload.color.y(), clearPayload.color.z(), clearPayload.color.w() }; } else { if ((clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0) { clearAttachment.aspectMask |= VK_IMAGE_ASPECT_DEPTH_BIT; clearAttachment.clearValue.depthStencil.depth = clearPayload.depth; } if ((clearPayload.mask & GL_STENCIL_BUFFER_BIT) != 0) { clearAttachment.aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT; clearAttachment.clearValue.depthStencil.stencil = clearPayload.stencil; } if (clearAttachment.aspectMask == 0) { continue; } } vkCmdClearAttachments(commandBuffer, 1, &clearAttachment, 1, &clearRect); m_clearManager->PopPendingClear(pending.texture); } } } // namespace MobileGL::MG_Backend::DirectVulkan