mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
[Fix, Test] (DirectGLES, TextureUtil): return RGB9_E5 glGetTexImage from the stored words
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user