From e80a23eae6033a78fed1b5d213869484ff3dfde9 Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Wed, 5 Aug 2026 03:55:30 -0400 Subject: [PATCH] [Fix] (MG_Backend): read a multi-slice glGetTexImage off the GPU instead of the CPU shadow DirectGLES served every multi-slice glGetTexImage from the CPU shadow copy, on the grounds that its scratch FBO can only expose one layer at a time. But the shadow only holds what was uploaded, so any slice that was rendered to rather than written by glTexSubImage came back stale - and a layered framebuffer produces exactly that. The scratch FBO can expose one layer at a time repeatedly. The read now attaches each layer in turn and takes the slice off the GPU, walking the destination over GL_PACK_SKIP_IMAGES / GL_PACK_IMAGE_HEIGHT itself so each per-slice call packs a plain 2D image with the same layout StoreWideRowsToClient computes for the whole stack. The shadow stays as the fallback for the formats a colour attachment cannot represent at all, and for any slice whose attachment comes back incomplete. Takes all 27 remaining direct_state_access.textures_storage_multisample_3d_* cases from failing to passing on Espryt - they render into a TEXTURE_2D_MULTISAMPLE_ARRAY one layer per colour attachment and then read the whole array back. DirectVulkan is untouched. --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 17382b48..ba805d69 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -5345,10 +5345,58 @@ namespace MobileGL::MG_Backend::DirectGLES { const Bool applyPackImageParams = backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY || backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY; - // 3D/array images read back every slice, but the FBO path can only read one layer: - // multi-slice reads are served from the CPU shadow (slice-major, tight layout). const GLsizei sliceCount = std::max(size.z(), 1); const Bool multiSlice = size.z() > 1; + // A multi-slice read used to go to the CPU shadow outright, on the grounds that the + // scratch FBO can only expose one layer at a time. But the shadow only holds what was + // uploaded, so every slice that was rendered to came back stale - which is exactly what + // a layered framebuffer produces, and what textures_storage_multisample_3d_* checks. + // Attach the layers one at a time instead and read each off the GPU, keeping the shadow + // for the formats the FBO cannot represent at all. + if (multiSlice && tempFBOComplete && + (backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY)) { + // Each slice is packed as its own 2D image, so the per-slice call must not apply + // GL_PACK_SKIP_IMAGES / GL_PACK_IMAGE_HEIGHT itself - this walks the destination + // over them, using the same layout StoreWideRowsToClient computes. + const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false); + const SizeT dstPixelBytes = GetReadbackDstPixelSize(conversionMapping, type); + const SizeT rowPixels = + static_cast(packParams.RowLength > 0 ? packParams.RowLength : size.x()); + const SizeT alignment = + packParams.Alignment > 0 ? static_cast(packParams.Alignment) : SizeT{1}; + const SizeT dstRowStride = (rowPixels * dstPixelBytes + alignment - 1) / alignment * alignment; + const SizeT imageRows = applyPackImageParams && packParams.ImageHeight > 0 + ? static_cast(packParams.ImageHeight) + : static_cast(size.y()); + const SizeT dstImageStride = imageRows * dstRowStride; + const SizeT skipImages = + applyPackImageParams ? static_cast(std::max(packParams.SkipImages, 0)) : SizeT{0}; + + Bool allSlicesRead = true; + for (GLsizei slice = 0; slice < sliceCount; ++slice) { + ScratchFBOImpl::EnsureColorAttachmentLayer(tempFB, GL_READ_FRAMEBUFFER, backendTexId, level, + slice); + if (g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + allSlicesRead = false; + break; + } + const SizeT sliceOffset = (skipImages + static_cast(slice)) * dstImageStride; + void* sliceDst = static_cast(pixels) + sliceOffset; + if (!ReadPixelsViaFormatConversion(0, 0, size.x(), size.y(), format, type, sliceDst, + /*honorPackImageParams=*/false, + /*applyFixedPointReadClamp=*/false)) { + allSlicesRead = false; + break; + } + } + // Leave the scratch FBO on layer 0 so the single-slice paths below see what they + // set up. + ScratchFBOImpl::EnsureColorAttachmentLayer(tempFB, GL_READ_FRAMEBUFFER, backendTexId, level, 0); + if (allSlicesRead) { + MGLOG_D("GetTexImage: finished %d slices via per-layer readback", sliceCount); + return; + } + } if (multiSlice && GetTexImageViaShadowConversion(textureMipmapObject, MG_Util::ConvertGLEnumToTextureUploadTarget(target), level, size.x(),