[Fix, Test] (DirectGLES, TextureUtil): return RGB9_E5 glGetTexImage from the stored words

This commit is contained in:
2026-08-20 10:14:48 -04:00
parent 52718ecf84
commit 1c0be3e715
4 changed files with 160 additions and 0 deletions
@@ -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<MG_State::GLState::ITextureObject>& 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<Uint>(srcLevel));
const IntVec3 dstSize = dstMipmap->GetMipmapTexelSize(dstUploadTarget, static_cast<Uint>(dstLevel));
const SizeT srcSlices = static_cast<SizeT>(std::max(srcSize.z(), 1));
const SizeT dstSlices = static_cast<SizeT>(std::max(dstSize.z(), 1));
if (srcSize.x() <= 0 || srcSize.y() <= 0 || dstSize.x() <= 0 || dstSize.y() <= 0) return;
const SizeT srcTexels = static_cast<SizeT>(srcSize.x()) * static_cast<SizeT>(srcSize.y()) * srcSlices;
const SizeT dstTexels = static_cast<SizeT>(dstSize.x()) * static_cast<SizeT>(dstSize.y()) * dstSlices;
const SizeT srcBytes = srcMipmap->GetMipmapByteSize(srcUploadTarget, static_cast<Uint>(srcLevel));
const SizeT dstBytes = dstMipmap->GetMipmapByteSize(dstUploadTarget, static_cast<Uint>(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<SizeT>(srcX) + width > static_cast<SizeT>(srcSize.x()) ||
static_cast<SizeT>(srcY) + height > static_cast<SizeT>(srcSize.y()) ||
static_cast<SizeT>(srcZ) + depth > srcSlices ||
static_cast<SizeT>(dstX) + width > static_cast<SizeT>(dstSize.x()) ||
static_cast<SizeT>(dstY) + height > static_cast<SizeT>(dstSize.y()) ||
static_cast<SizeT>(dstZ) + depth > dstSlices) {
return;
}
const auto* srcBase = static_cast<const Uint8*>(
srcMipmap->MapMipmapData(srcUploadTarget, static_cast<Uint>(srcLevel)));
auto* dstBase = static_cast<Uint8*>(dstMipmap->MapMipmapData(dstUploadTarget, static_cast<Uint>(dstLevel)));
if (!srcBase || !dstBase) return;
const SizeT rowBytes = static_cast<SizeT>(width) * texelBytes;
for (GLsizei slice = 0; slice < depth; ++slice) {
for (GLsizei row = 0; row < height; ++row) {
const SizeT srcOffset = ((static_cast<SizeT>(srcZ + slice) * static_cast<SizeT>(srcSize.y()) +
static_cast<SizeT>(srcY + row)) *
static_cast<SizeT>(srcSize.x()) +
static_cast<SizeT>(srcX)) *
texelBytes;
const SizeT dstOffset = ((static_cast<SizeT>(dstZ + slice) * static_cast<SizeT>(dstSize.y()) +
static_cast<SizeT>(dstY + row)) *
static_cast<SizeT>(dstSize.x()) +
static_cast<SizeT>(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
+23
View File
@@ -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.
@@ -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<TextureSwizzleParam>& swizzle) {
@@ -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