diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index f8ba1dc6..715ddd90 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -5805,6 +5805,92 @@ namespace MobileGL::MG_Backend::DirectGLES { return endpoint.Texture ? endpoint.Texture->GetFormat() : TextureInternalFormat::Unknown; } + // Whether this endpoint's CPU shadow can be addressed texel-exactly by the mirror below: one + // upload target (so not a cube map, whose six chains the z axis selects between) and layers on + // the z axis (GL_TEXTURE_1D_ARRAY carries them on y). + static Bool CanMirrorCopyImageShadow(const SharedPtr& texture) { + if (!texture) return false; + if (texture->GetTarget() == TextureTarget::Texture1DArray) return false; + return texture->GetUploadTargets().size() == 1; + } + + // glCopyImageSubData is defined as a raw texel-block move, so for a destination whose CPU + // shadow has to stay authoritative - a packed format with redundant encodings, where a GPU + // readback can only answer with RE-ENCODED words (see the verbatim branch in GetTexImage) - + // the same move is replayed on the shadow. Nothing is marked dirty: the driver copy already + // put these texels on the GPU, and flagging the level would only schedule a redundant upload + // back over them. + // + // Declined, leaving the shadow exactly as it was, for every shape whose bytes this cannot + // address exactly - a renderbuffer (no shadow at all), a cube or 1D-array endpoint, a level + // whose shadow is missing or not a plain texel grid, a region outside either level, or a + // self-copy within one level, where the row copies could overlap. + static void MirrorCopyImageIntoDestinationShadow(const CopyImageEndpoint& srcEndpoint, GLint srcLevel, GLint srcX, + GLint srcY, GLint srcZ, const CopyImageEndpoint& dstEndpoint, + GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, + GLsizei width, GLsizei height, GLsizei depth) { + if (!CanMirrorCopyImageShadow(srcEndpoint.Texture) || !CanMirrorCopyImageShadow(dstEndpoint.Texture)) return; + if (srcEndpoint.Texture == dstEndpoint.Texture && srcLevel == dstLevel) return; + if (width <= 0 || height <= 0 || depth <= 0) return; + if (srcLevel < 0 || dstLevel < 0 || srcX < 0 || srcY < 0 || srcZ < 0 || dstX < 0 || dstY < 0 || dstZ < 0) { + return; + } + auto* srcMipmap = MG_State::GLState::AsMipmapTexture(srcEndpoint.Texture.get()); + auto* dstMipmap = MG_State::GLState::AsMipmapTexture(dstEndpoint.Texture.get()); + if (!srcMipmap || !dstMipmap) return; + + const auto srcUploadTarget = srcEndpoint.Texture->GetUploadTargets()[0]; + const auto dstUploadTarget = dstEndpoint.Texture->GetUploadTargets()[0]; + const IntVec3 srcSize = srcMipmap->GetMipmapTexelSize(srcUploadTarget, static_cast(srcLevel)); + const IntVec3 dstSize = dstMipmap->GetMipmapTexelSize(dstUploadTarget, static_cast(dstLevel)); + const SizeT srcSlices = static_cast(std::max(srcSize.z(), 1)); + const SizeT dstSlices = static_cast(std::max(dstSize.z(), 1)); + if (srcSize.x() <= 0 || srcSize.y() <= 0 || dstSize.x() <= 0 || dstSize.y() <= 0) return; + const SizeT srcTexels = static_cast(srcSize.x()) * static_cast(srcSize.y()) * srcSlices; + const SizeT dstTexels = static_cast(dstSize.x()) * static_cast(dstSize.y()) * dstSlices; + const SizeT srcBytes = srcMipmap->GetMipmapByteSize(srcUploadTarget, static_cast(srcLevel)); + const SizeT dstBytes = dstMipmap->GetMipmapByteSize(dstUploadTarget, static_cast(dstLevel)); + // A shadow that is not exactly texels x texelSize bytes is one this cannot index (a + // compressed blob, or a level whose allocation disagrees with its recorded extent). + const SizeT texelBytes = srcTexels == 0 ? 0 : srcBytes / srcTexels; + if (texelBytes == 0 || srcBytes != srcTexels * texelBytes || dstTexels == 0 || + dstBytes != dstTexels * texelBytes) { + return; + } + if (static_cast(srcX) + width > static_cast(srcSize.x()) || + static_cast(srcY) + height > static_cast(srcSize.y()) || + static_cast(srcZ) + depth > srcSlices || + static_cast(dstX) + width > static_cast(dstSize.x()) || + static_cast(dstY) + height > static_cast(dstSize.y()) || + static_cast(dstZ) + depth > dstSlices) { + return; + } + + const auto* srcBase = static_cast( + srcMipmap->MapMipmapData(srcUploadTarget, static_cast(srcLevel))); + auto* dstBase = static_cast(dstMipmap->MapMipmapData(dstUploadTarget, static_cast(dstLevel))); + if (!srcBase || !dstBase) return; + + const SizeT rowBytes = static_cast(width) * texelBytes; + for (GLsizei slice = 0; slice < depth; ++slice) { + for (GLsizei row = 0; row < height; ++row) { + const SizeT srcOffset = ((static_cast(srcZ + slice) * static_cast(srcSize.y()) + + static_cast(srcY + row)) * + static_cast(srcSize.x()) + + static_cast(srcX)) * + texelBytes; + const SizeT dstOffset = ((static_cast(dstZ + slice) * static_cast(dstSize.y()) + + static_cast(dstY + row)) * + static_cast(dstSize.x()) + + static_cast(dstX)) * + texelBytes; + Memcpy(dstBase + dstOffset, srcBase + srcOffset, rowBytes); + } + } + MGLOG_D("CopyImageSubData: mirrored %dx%dx%d texels into the destination's CPU shadow", width, height, + depth); + } + void CopyImageSubData(const CopyImageEndpoint& srcEndpoint, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, const CopyImageEndpoint& dstEndpoint, @@ -5895,6 +5981,14 @@ namespace MobileGL::MG_Backend::DirectGLES { MG_Util::ConvertGLEnumToString(dst.target).c_str(), MG_Util::ConvertGLEnumToString(dstTarget).c_str()); MOBILEGL_ASSERT(false, "glCopyImageSubData failed after frontend validation accepted the request."); + return; + } + // The copy landed on the GPU. For a destination whose readback cannot be bit-exact the + // CPU shadow is what glGetTexImage answers from, so it has to follow the same move - + // otherwise it hands back whatever the level held before this copy. + if (MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding(dstFormat)) { + MirrorCopyImageIntoDestinationShadow(srcEndpoint, srcLevel, srcX, srcY, srcZ, dstEndpoint, dstLevel, + dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth); } } @@ -7818,6 +7912,30 @@ namespace MobileGL::MG_Backend::DirectGLES { backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY; const GLsizei sliceCount = std::max(size.z(), 1); const Bool multiSlice = size.z() > 1; + // glGetTexImage answers with the STORED texels, and for a packed format whose encoding + // is not unique the GPU route below cannot: it reads GL_RGBA/GL_FLOAT and re-encodes, + // which canonicalizes an RGB9_E5 shared exponent (0xf8fc0000 -> 0xe7e00000 - the same + // value 8064, different words), and the conformance suite compares the words + // ("CopyImageSubData modified contents of source image"). The scratch FBO does NOT + // decide this for us: Adreno reports an RGB9_E5 colour attachment complete, so the + // shadow branch further down was unreachable. Serve the verbatim-word pairs from the + // shadow first and keep the GPU attempts as the fallback for a level the shadow never + // received. Every other format still prefers the GPU, so a rendered-into texture is + // unaffected; RGB9_E5 is not colour-renderable, so its shadow stays authoritative - + // and the one path that GPU-writes it, CopyImageSubData, mirrors itself into the + // shadow for exactly this reason. + const Bool verbatimPackedShadowRead = + MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding(textureObject->GetFormat()) && + MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer( + textureObject->GetFormat(), MG_Util::ConvertGLEnumToTextureInputFormat(format), + MG_Util::ConvertGLEnumToTexturePixelDataType(type)); + if (verbatimPackedShadowRead && + GetTexImageViaShadowConversion(textureMipmapObject, + MG_Util::ConvertGLEnumToTextureUploadTarget(target), level, size.x(), + size.y(), sliceCount, format, type, pixels, applyPackImageParams)) { + MGLOG_D("GetTexImage: finished via shadow conversion (verbatim packed words)"); + return; + } // 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 diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 4658a7b3..8922040e 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -3320,6 +3320,29 @@ TEST(SharedExponentRGB9E5Test, RawPackedPixelTransferCoversOnlyIdenticalLayouts) TexturePixelDataType::UnsignedInt5999Rev)); } +TEST(SharedExponentRGB9E5Test, RedundantPackedEncodingIsRGB9E5Only) { + using MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding; + + // This is the predicate that decides whether the CPU shadow has to answer glGetTexImage + // instead of a GPU readback, so it must be as narrow as the defect: only the shared exponent + // has several legal encodings of one value. + EXPECT_TRUE(HasRedundantPackedEncoding(TextureInternalFormat::RGB9E5)); + + // The other three packed 32-bit layouts round-trip through float32 bit-exactly (each field is + // either an integer or a unique float encoding), so a GPU readback still serves them - which + // matters because RGB10_A2 and R11F_G11F_B10F ARE colour-renderable and their shadow can + // legitimately be stale. + EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGB10A2)); + EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGB10A2UI)); + EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::R11FG11FB10F)); + + // Nothing unpacked qualifies, and neither does an unknown format. + EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGBA8)); + EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGBA32F)); + EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGB8)); + EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::Unknown)); +} + TEST_F(TextureTest, TexImage2DRGB9E5KeepsNonCanonicalClientWords) { // Upload direction: GL_RGB / GL_UNSIGNED_INT_5_9_9_9_REV into GL_RGB9_E5 stores the client // words untouched, including the redundant encodings the CTS generates. diff --git a/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp b/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp index c52449fa..099f49b3 100644 --- a/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp +++ b/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp @@ -806,6 +806,14 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { return IsRawPackedPixelPair(packedInternal.kind, clientFormat, clientType); } + Bool HasRedundantPackedEncoding(TextureInternalFormat internalFormat) { + InternalPackedLayout packedInternal{}; + if (!GetInternalPackedLayout(internalFormat, packedInternal)) { + return false; + } + return packedInternal.kind == PackedInternalKind::FloatRGB9E5; + } + // assume 8 bit per channel // swizzle.size() == channel count void ProcessColorSwizzle(void* data, SizeT pixelCount, const Vector& swizzle) { diff --git a/MobileGL/MG_Util/Texture/PixelStoreProcessor.h b/MobileGL/MG_Util/Texture/PixelStoreProcessor.h index 91169739..c589f1ff 100644 --- a/MobileGL/MG_Util/Texture/PixelStoreProcessor.h +++ b/MobileGL/MG_Util/Texture/PixelStoreProcessor.h @@ -44,6 +44,17 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { Bool IsRawPackedPixelTransfer(TextureInternalFormat internalFormat, TextureInputFormat clientFormat, TexturePixelDataType clientType); + // True when a packed internal format has REDUNDANT encodings, so decoding a texel and + // re-encoding it keeps the VALUE but not the BITS. Only RGB9_E5 does: its shared exponent can + // be lowered with the mantissas shifted up to match, and the spec's encoder always emits the + // canonical form. RGB10_A2, RGB10_A2UI and R11F_G11F_B10F round-trip through float32 + // bit-exactly, so a GPU readback can answer for them. + // + // This is what decides whether the CPU shadow has to stay authoritative for a format: a + // readback of an RGB9_E5 level through a colour attachment cannot return the stored words, no + // matter how well behaved the driver is. + Bool HasRedundantPackedEncoding(TextureInternalFormat internalFormat); + // Decodes the canonical shadow-mip storage of `internalFormat` into wide RGBA texels for CPU // readback (GetTexImage of non-renderable formats). Non-integer formats fill outWide with // 4 Floats per texel; integer formats fill it with 4 Uint32/Int32 per texel and set