From a2f3efe22cf88a74a1cd3dcccb7d57f96341269c Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Sat, 1 Aug 2026 02:19:07 -0400 Subject: [PATCH] [Feat] (DirectVulkan): combined and scissored blits; cross-format depth-stencil blits BlitFramebuffer now serves any GL_COLOR/DEPTH/STENCIL mask combination: the depth/stencil aspects run as per-aspect image copies before the color path, renderbuffer attachments materialize their pending clears like texture ones, and the scissor test clips blit writes (destination rect intersected, source shrunk proportionally). Depth copies between images of different depth formats (a D24S8 renderbuffer into a DEPTH_COMPONENT24 texture riding the D32_SFLOAT fallback) round-trip through the host with a per-texel re-encode; stencil aspects pass through raw since every packed format encodes S8. scissor_blit and packed_depth_stencil.blit.* now pass. --- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 301 ++++++++++++++++-- .../DirectVulkan/Renderer/VulkanRenderer.h | 8 + 2 files changed, 285 insertions(+), 24 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 97950132..8c4f0eb4 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -1529,6 +1529,7 @@ void main() { } outBinding.image = swapchainObject.GetDepthStencilImage(swapchainImageIndex); + outBinding.format = swapchainObject.GetDepthStencilFormat(); outBinding.aspectMask = requiredAspectMask; return true; } @@ -1555,6 +1556,7 @@ void main() { outBinding.image = rbResource->image; outBinding.trackedLayout = &rbResource->layout; outBinding.aspectMask = requiredAspectMask; + outBinding.format = rbResource->format; outBinding.extent = {static_cast(rbResource->extent.width), static_cast(rbResource->extent.height)}; outBinding.mipLevel = 0; @@ -1585,6 +1587,7 @@ void main() { outBinding.image = resource->image; outBinding.trackedLayout = &resource->layout; outBinding.aspectMask = requiredAspectMask; + outBinding.format = resource->format; const auto attachmentExtent = attachment.GetSize(); outBinding.extent = {attachmentExtent.x(), attachmentExtent.y()}; outBinding.mipLevel = static_cast(std::max(attachment.GetTextureLevel(), 0)); @@ -1694,6 +1697,7 @@ void main() { outBinding.image = rbResource->image; outBinding.trackedLayout = &rbResource->layout; outBinding.aspectMask = requiredAspectMask; + outBinding.format = rbResource->format; outBinding.extent = {static_cast(rbResource->extent.width), static_cast(rbResource->extent.height)}; outBinding.mipLevel = 0; @@ -1725,6 +1729,7 @@ void main() { outBinding.image = resource->image; outBinding.trackedLayout = &resource->layout; outBinding.aspectMask = requiredAspectMask; + outBinding.format = resource->format; const auto attachmentExtent = attachment.GetSize(); outBinding.extent = {attachmentExtent.x(), attachmentExtent.y()}; outBinding.mipLevel = static_cast(std::max(attachment.GetTextureLevel(), 0)); @@ -6078,30 +6083,58 @@ void main() { 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; + static constexpr GLbitfield kSupportedBlitMask = + GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_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)); + const Bool isStencilBlit = (mask & GL_STENCIL_BUFFER_BIT) != 0; + if (!isColorBlit && !isDepthBlit && !isStencilBlit) { 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"); + if ((isDepthBlit || isStencilBlit) && filter != GL_NEAREST) { + MGLOG_E("BlitFramebuffer skipped: depth/stencil blits require GL_NEAREST"); return; } + // The scissor test clips blit writes: intersect the destination rectangle with + // the scissor box and shrink the source proportionally. + if (MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest)) { + const IntVec4& scissor = MG_State::pGLContext->GetScissorBox(); + const auto clipAxis = [](GLint& d0, GLint& d1, GLint& s0, GLint& s1, GLint clipLo, GLint clipHi) -> Bool { + const Bool dstFlipped = d1 < d0; + GLint lo = dstFlipped ? d1 : d0; + GLint hi = dstFlipped ? d0 : d1; + const GLint newLo = std::max(lo, clipLo); + const GLint newHi = std::min(hi, clipHi); + if (newLo >= newHi) { + return false; + } + const double srcSpan = static_cast(s1 - s0); + const double dstSpan = static_cast(d1 - d0); + const double scale = dstSpan != 0.0 ? srcSpan / dstSpan : 0.0; + const GLint origD0 = d0; + const GLint clippedD0 = dstFlipped ? newHi : newLo; + const GLint clippedD1 = dstFlipped ? newLo : newHi; + s0 = s0 + static_cast(std::lround((clippedD0 - origD0) * scale)); + s1 = s0 + static_cast(std::lround((clippedD1 - clippedD0) * scale)); + d0 = clippedD0; + d1 = clippedD1; + return true; + }; + if (!clipAxis(dstX0, dstX1, srcX0, srcX1, scissor.x(), scissor.x() + scissor.z()) || + !clipAxis(dstY0, dstY1, srcY0, srcY1, scissor.y(), scissor.y() + scissor.w())) { + return; // fully scissored out + } + } + MOBILEGL_ASSERT(readFbo != nullptr, "VulkanRenderer::BlitFramebuffer: read framebuffer is null"); MOBILEGL_ASSERT(drawFbo != nullptr, "VulkanRenderer::BlitFramebuffer: draw framebuffer is null"); if (IsUnsupportedFramebufferForDirectVulkan(*readFbo) || @@ -6133,15 +6166,18 @@ void main() { return; } - if (isDepthBlit) { + Vector depthStencilAspects; + if (isDepthBlit) depthStencilAspects.push_back(VK_IMAGE_ASPECT_DEPTH_BIT); + if (isStencilBlit) depthStencilAspects.push_back(VK_IMAGE_ASPECT_STENCIL_BIT); + for (const VkImageAspectFlagBits depthStencilAspect : depthStencilAspects) { BlitImageBinding srcBinding{}; BlitImageBinding dstBinding{}; if (!ResolveFramebufferBlitBinding(*readFbo, true, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, *m_renderPassManager, - VK_IMAGE_ASPECT_DEPTH_BIT, srcBinding) || + depthStencilAspect, srcBinding) || !ResolveFramebufferBlitBinding(*drawFbo, false, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, *m_renderPassManager, - VK_IMAGE_ASPECT_DEPTH_BIT, dstBinding)) { + depthStencilAspect, dstBinding)) { return; } @@ -6162,12 +6198,18 @@ void main() { 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()); + if (auto sourceTexture = sourceAttachment.GetTexture(); sourceTexture != nullptr) { + const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *sourceTexture); + MOBILEGL_ASSERT(clearReady, + "BlitFramebuffer: failed to materialize pending clear for depth source textureId=%d", + sourceTexture->GetExternalIndex()); + } else if (sourceAttachment.IsRenderbuffer()) { + const Bool clearReady = MaterializePendingClearForRenderbuffer(frame.commandBuffer, + sourceAttachment.GetRenderbuffer()); + MOBILEGL_ASSERT(clearReady, + "BlitFramebuffer: failed to materialize pending clear for depth source renderbuffer %u", + sourceAttachment.GetRenderbuffer()->GetExternalIndex()); + } } if (!drawIsDefaultFbo) { @@ -6177,12 +6219,18 @@ void main() { // depth into it - the stale loadOp=CLEAR erased the copy). const auto destAttachmentType = ResolveFramebufferCopyAttachmentType(*drawFbo, false, dstBinding.aspectMask); const auto& destAttachment = drawFbo->GetAttachment(destAttachmentType); - auto destTexture = destAttachment.GetTexture(); - MOBILEGL_ASSERT(destTexture != nullptr, "BlitFramebuffer: depth destination texture attachment is null"); - const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *destTexture); - MOBILEGL_ASSERT(dstClearReady, - "BlitFramebuffer: failed to materialize pending clear for depth destination textureId=%d", - destTexture->GetExternalIndex()); + if (auto destTexture = destAttachment.GetTexture(); destTexture != nullptr) { + const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *destTexture); + MOBILEGL_ASSERT(dstClearReady, + "BlitFramebuffer: failed to materialize pending clear for depth destination textureId=%d", + destTexture->GetExternalIndex()); + } else if (destAttachment.IsRenderbuffer()) { + const Bool dstClearReady = MaterializePendingClearForRenderbuffer(frame.commandBuffer, + destAttachment.GetRenderbuffer()); + MOBILEGL_ASSERT(dstClearReady, + "BlitFramebuffer: failed to materialize pending clear for depth destination renderbuffer %u", + destAttachment.GetRenderbuffer()->GetExternalIndex()); + } } const VkImageLayout srcOriginalLayout = readIsDefaultFbo @@ -6200,6 +6248,26 @@ void main() { ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL : dstOriginalLayout; + // vkCmdCopyImage requires identical depth formats; a mismatched pair (e.g. + // a D24S8 renderbuffer into a DEPTH_COMPONENT24 texture backed by the + // D32_SFLOAT fallback) round-trips the region through the host with a + // per-texel re-encode instead. + if (srcBinding.format != dstBinding.format) { + if (readIsDefaultFbo || drawIsDefaultFbo) { + MGLOG_E("BlitFramebuffer skipped: cross-format depth/stencil blit with the default framebuffer"); + return; + } + if (!BlitDepthAcrossFormats(frame, srcBinding.image, srcBinding.format, srcBinding.trackedLayout, + srcBinding.mipLevel, srcBinding.baseArrayLayer, dstBinding.image, + dstBinding.format, dstBinding.trackedLayout, dstBinding.mipLevel, + dstBinding.baseArrayLayer, srcX0, srcY0, dstX0, dstY0, srcX1 - srcX0, + srcY1 - srcY0, srcOriginalLayout, dstRestoreLayout, + depthStencilAspect == VK_IMAGE_ASPECT_STENCIL_BIT)) { + return; + } + continue; + } + VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcAccessMask = 0; GetImageTransitionSourceState(srcOriginalLayout, srcStageMask, srcAccessMask); @@ -6301,6 +6369,8 @@ void main() { dstBinding.mipLevel, dstBinding.mipLevelCount); MOBILEGL_ASSERT(ok, "%s: failed to restore depth destination image layout", __func__); } + } + if (!isColorBlit) { return; } @@ -7010,6 +7080,189 @@ void main() { /*applyPackImageParams=*/false, /*applyReadColorClamp=*/true); } + Bool VulkanRenderer::BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat, + VkImageLayout* srcTrackedLayout, Uint32 srcMipLevel, + Uint32 srcBaseArrayLayer, VkImage dstImage, VkFormat dstFormat, + VkImageLayout* dstTrackedLayout, Uint32 dstMipLevel, + Uint32 dstBaseArrayLayer, GLint srcX, GLint srcY, GLint dstX, + GLint dstY, GLint width, GLint height, + VkImageLayout srcRestoreLayout, VkImageLayout dstRestoreLayout, + Bool stencilAspect) { + const auto aspectMaskForFormat = [](VkFormat format) -> VkImageAspectFlags { + switch (format) { + case VK_FORMAT_D16_UNORM: + case VK_FORMAT_X8_D24_UNORM_PACK32: + case VK_FORMAT_D32_SFLOAT: + return VK_IMAGE_ASPECT_DEPTH_BIT; + case VK_FORMAT_D24_UNORM_S8_UINT: + case VK_FORMAT_D32_SFLOAT_S8_UINT: + return VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + default: + return VK_IMAGE_ASPECT_COLOR_BIT; + } + }; + const auto depthTexelSize = [](VkFormat format) -> SizeT { + switch (format) { + case VK_FORMAT_D16_UNORM: + return 2; + case VK_FORMAT_X8_D24_UNORM_PACK32: + case VK_FORMAT_D24_UNORM_S8_UINT: + case VK_FORMAT_D32_SFLOAT: + case VK_FORMAT_D32_SFLOAT_S8_UINT: + return 4; + default: + return 0; + } + }; + // The stencil aspect of every supported format copies as one byte per texel, + // so a cross-format stencil "blit" is a raw pass-through. + const SizeT srcTexel = stencilAspect ? 1 : depthTexelSize(srcFormat); + const SizeT dstTexel = stencilAspect ? 1 : depthTexelSize(dstFormat); + if (srcTexel == 0 || dstTexel == 0 || width <= 0 || height <= 0) { + MGLOG_E("BlitDepthAcrossFormats skipped: unsupported formats src=%d dst=%d", + static_cast(srcFormat), static_cast(dstFormat)); + return false; + } + const SizeT pixelCount = static_cast(width) * static_cast(height); + + VkBufferObject readback; + if (!readback.Create({ + .allocator = m_allocator, + .size = pixelCount * srcTexel, + .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .memoryUsage = VMA_MEMORY_USAGE_AUTO, + .allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT, + })) { + return false; + } + + VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkAccessFlags srcAccessMask = 0; + GetImageTransitionSourceState(*srcTrackedLayout, srcStageMask, srcAccessMask); + Bool ok = VkTextureManager::TransitionImageLayout( + frame.commandBuffer, srcImage, *srcTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, + VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, + aspectMaskForFormat(srcFormat), srcMipLevel, 1); + MOBILEGL_ASSERT(ok, "BlitDepthAcrossFormats: source transition failed"); + + VkBufferImageCopy readRegion{}; + readRegion.imageSubresource.aspectMask = + stencilAspect ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT; + readRegion.imageSubresource.mipLevel = srcMipLevel; + readRegion.imageSubresource.baseArrayLayer = srcBaseArrayLayer; + readRegion.imageSubresource.layerCount = 1; + readRegion.imageOffset = {srcX, srcY, 0}; + readRegion.imageExtent = {static_cast(width), static_cast(height), 1}; + vkCmdCopyImageToBuffer(frame.commandBuffer, srcImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + readback.GetHandle(), 1, &readRegion); + + VkPipelineStageFlags srcRestoreStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkAccessFlags srcRestoreAccess = 0; + GetImageTransitionDestinationState(srcRestoreLayout, srcRestoreStage, srcRestoreAccess); + ok = VkTextureManager::TransitionImageLayout( + frame.commandBuffer, srcImage, *srcTrackedLayout, srcRestoreLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, + srcRestoreStage, VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccess, + aspectMaskForFormat(srcFormat), srcMipLevel, 1); + MOBILEGL_ASSERT(ok, "BlitDepthAcrossFormats: source restore failed"); + + if (!SubmitReadbackCommandsAndWait(frame)) { + return false; + } + const auto* mapped = static_cast(readback.Map()); + if (mapped == nullptr || !readback.Invalidate(pixelCount * srcTexel)) { + return false; + } + + // Decode source depths to float, re-encode into the destination texel layout. + Vector encoded(pixelCount * dstTexel); + if (stencilAspect) { + Memcpy(encoded.data(), mapped, pixelCount); + } + for (SizeT i = 0; !stencilAspect && i < pixelCount; ++i) { + Float depthValue = 0.0f; + switch (srcFormat) { + case VK_FORMAT_D16_UNORM: { + Uint16 raw = 0; + Memcpy(&raw, mapped + i * 2, sizeof(raw)); + depthValue = static_cast(raw) / 65535.0f; + break; + } + case VK_FORMAT_X8_D24_UNORM_PACK32: + case VK_FORMAT_D24_UNORM_S8_UINT: { + Uint32 raw = 0; + Memcpy(&raw, mapped + i * 4, sizeof(raw)); + depthValue = static_cast(raw & 0xFFFFFFu) / static_cast(0xFFFFFFu); + break; + } + default: { + Memcpy(&depthValue, mapped + i * 4, sizeof(depthValue)); + break; + } + } + Uint8* dst = encoded.data() + i * dstTexel; + switch (dstFormat) { + case VK_FORMAT_D16_UNORM: { + const Uint16 value = + static_cast(std::lround(static_cast(std::clamp(depthValue, 0.0f, 1.0f)) * 65535.0)); + Memcpy(dst, &value, sizeof(value)); + break; + } + case VK_FORMAT_X8_D24_UNORM_PACK32: + case VK_FORMAT_D24_UNORM_S8_UINT: { + const Uint32 value = static_cast( + std::lround(static_cast(std::clamp(depthValue, 0.0f, 1.0f)) * 16777215.0)); + Memcpy(dst, &value, sizeof(value)); + break; + } + default: + Memcpy(dst, &depthValue, sizeof(depthValue)); + break; + } + } + + // Upload the converted region; recording restarted after the readback flush. + if (!frame.isCommandRecording) { + m_frameContext.BeginCommandRecording(); + } + BufferSlice slice{}; + if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), encoded.data(), + encoded.size(), 4, slice)) { + MGLOG_E("BlitDepthAcrossFormats: staging upload failed"); + return false; + } + + VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkAccessFlags dstAccessMask = 0; + GetImageTransitionSourceState(*dstTrackedLayout, dstStageMask, dstAccessMask); + ok = VkTextureManager::TransitionImageLayout( + frame.commandBuffer, dstImage, *dstTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dstStageMask, + VK_PIPELINE_STAGE_TRANSFER_BIT, dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, + aspectMaskForFormat(dstFormat), dstMipLevel, 1); + MOBILEGL_ASSERT(ok, "BlitDepthAcrossFormats: destination transition failed"); + + VkBufferImageCopy writeRegion{}; + writeRegion.bufferOffset = slice.offset; + writeRegion.imageSubresource.aspectMask = + stencilAspect ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT; + writeRegion.imageSubresource.mipLevel = dstMipLevel; + writeRegion.imageSubresource.baseArrayLayer = dstBaseArrayLayer; + writeRegion.imageSubresource.layerCount = 1; + writeRegion.imageOffset = {dstX, dstY, 0}; + writeRegion.imageExtent = {static_cast(width), static_cast(height), 1}; + vkCmdCopyBufferToImage(frame.commandBuffer, slice.buffer, dstImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, + &writeRegion); + + VkPipelineStageFlags dstRestoreStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkAccessFlags dstRestoreAccess = 0; + GetImageTransitionDestinationState(dstRestoreLayout, dstRestoreStage, dstRestoreAccess); + ok = VkTextureManager::TransitionImageLayout( + frame.commandBuffer, dstImage, *dstTrackedLayout, dstRestoreLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, + dstRestoreStage, VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccess, + aspectMaskForFormat(dstFormat), dstMipLevel, 1); + MOBILEGL_ASSERT(ok, "BlitDepthAcrossFormats: destination restore failed"); + return true; + } + void VulkanRenderer::ReadDepthStencilPixels(MG_State::GLState::FramebufferObject& readFbo, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index d0377e90..0187d427 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -211,6 +211,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels); + // Same-extent depth blit between images of different depth formats: host + // round-trip with a per-texel re-encode (see BlitNamedFramebuffer). + Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat, + VkImageLayout* srcTrackedLayout, Uint32 srcMipLevel, Uint32 srcBaseArrayLayer, + VkImage dstImage, VkFormat dstFormat, VkImageLayout* dstTrackedLayout, + Uint32 dstMipLevel, Uint32 dstBaseArrayLayer, GLint srcX, GLint srcY, GLint dstX, + GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout, + VkImageLayout dstRestoreLayout, Bool stencilAspect); static SizeT GetReadbackTexelSize(VkFormat sourceFormat); static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat, GLsizei width, GLsizei height, GLenum destinationFormat,