mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 12:18:30 +09:00
[Fix, Test] (DirectGLES, PixelStoreProcessor, MG_IntegrationTest): read a packed level's stored words instead of trusting the shadow
This commit is contained in:
@@ -7684,6 +7684,149 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- Bit-exact readback of a 32-bit packed colour level ---------------------------------------
|
||||
//
|
||||
// glGetTexImage of a packed format read with its OWN client type owes the application the words
|
||||
// the image HOLDS, and neither of the two routes above can promise that once anything other than
|
||||
// a glTexImage has written the level:
|
||||
//
|
||||
// * the colour-attachment route reads GL_RGBA/GL_FLOAT and re-encodes, which canonicalizes an
|
||||
// RGB9_E5 shared exponent (0xf8fc0000 -> 0xe7e00000, same value, different bits) and
|
||||
// collapses an R11F_G11F_B10F NaN to the canonical payload 1
|
||||
// (MG_Util::EncodeFloatToUnsignedSmallFloat) - and a copy-image from RGB9_E5 lands exactly
|
||||
// such a NaN in the 10-bit blue field every time, because the source's shared-exponent
|
||||
// field is all ones;
|
||||
// * the CPU shadow only ever holds what was UPLOADED, so for a level glCopyImageSubData wrote
|
||||
// it answers with the PRE-COPY contents. MirrorCopyImageIntoDestinationShadow patches that
|
||||
// up for the shapes it can address texel-exactly and declines for the rest - a renderbuffer
|
||||
// source (which has no shadow to mirror from at all), a cube or 1D-array endpoint, a
|
||||
// self-copy - and the decline is silent, so the stale words are served as truth.
|
||||
//
|
||||
// glCopyImageSubData is a raw texel-block move and EXT_copy_image puts every 32-bit colour
|
||||
// format in one compatibility class, so copying the level into a scratch GL_R32UI image and
|
||||
// reading THAT back as unsigned integers hands over the stored words themselves, whoever wrote
|
||||
// them. This is what lets the shadow stop being the authority for these formats: it is tried
|
||||
// first, and every step reports rather than guesses, so a driver that turns any of it down
|
||||
// simply leaves the old shadow/attachment fallbacks to run.
|
||||
static GLuint g_packedWordScratchTextureId = 0;
|
||||
static GLsizei g_packedWordScratchWidth = 0;
|
||||
static GLsizei g_packedWordScratchHeight = 0;
|
||||
|
||||
// Grow-only, so a readback sweep over a mip chain allocates once. Zero when the driver refused
|
||||
// the storage, which is a decline and not an error.
|
||||
static GLuint EnsurePackedWordScratchTexture(GLsizei width, GLsizei height) {
|
||||
if (g_packedWordScratchTextureId != 0 && g_packedWordScratchWidth >= width &&
|
||||
g_packedWordScratchHeight >= height) {
|
||||
return g_packedWordScratchTextureId;
|
||||
}
|
||||
const GLsizei newWidth = std::max(width, g_packedWordScratchWidth);
|
||||
const GLsizei newHeight = std::max(height, g_packedWordScratchHeight);
|
||||
if (g_packedWordScratchTextureId != 0) {
|
||||
// A scratch FBO may still name the old id, and the driver is free to hand the same
|
||||
// number back for the replacement - which would false-skip the re-attach.
|
||||
ScratchFBOImpl::NoteTextureIdDeleted(g_packedWordScratchTextureId);
|
||||
g_GLESFuncs.glDeleteTextures(1, &g_packedWordScratchTextureId);
|
||||
g_packedWordScratchTextureId = 0;
|
||||
g_packedWordScratchWidth = 0;
|
||||
g_packedWordScratchHeight = 0;
|
||||
}
|
||||
GLuint texture = 0;
|
||||
g_GLESFuncs.glGenTextures(1, &texture);
|
||||
if (texture == 0) return 0;
|
||||
|
||||
ClearGLErrors();
|
||||
TextureImpl::ActivateTextureUnit(TextureImpl::TempTextureUnit);
|
||||
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, texture);
|
||||
// Immutable single-level storage: glCopyImageSubData wants a complete image, and
|
||||
// glTexStorage clamps TEXTURE_MAX_LEVEL, which is what makes a one-level texture complete
|
||||
// under the default mipmapping filter.
|
||||
g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32UI, newWidth, newHeight);
|
||||
const GLenum storageError = g_GLESFuncs.glGetError();
|
||||
// Re-bind whatever the binding cache says lives on the temp unit, so the cache stays
|
||||
// truthful without a driver query (same discipline as CopyR32FTexture2D).
|
||||
auto* cachedBound = TextureImpl::g_boundTexturesCache[TextureImpl::TempTextureUnit]
|
||||
[static_cast<SizeT>(TextureTarget::Texture2D)];
|
||||
g_GLESFuncs.glBindTexture(GL_TEXTURE_2D, cachedBound ? cachedBound->GetBackendTextureId() : 0);
|
||||
if (storageError != GL_NO_ERROR) {
|
||||
g_GLESFuncs.glDeleteTextures(1, &texture);
|
||||
MGLOG_D("GetTexImage: no %dx%d GL_R32UI scratch image (%s); the verbatim word readback is unavailable",
|
||||
newWidth, newHeight, MG_Util::ConvertGLEnumToString(storageError).c_str());
|
||||
return 0;
|
||||
}
|
||||
g_packedWordScratchTextureId = texture;
|
||||
g_packedWordScratchWidth = newWidth;
|
||||
g_packedWordScratchHeight = newHeight;
|
||||
return texture;
|
||||
}
|
||||
|
||||
static void ReleasePackedWordScratchTexture() {
|
||||
// The ES context (and the name with it) is gone; deleting here would target a recycled
|
||||
// name in the successor context.
|
||||
g_packedWordScratchTextureId = 0;
|
||||
g_packedWordScratchWidth = 0;
|
||||
g_packedWordScratchHeight = 0;
|
||||
}
|
||||
|
||||
// One slice of `backendTarget`'s level, as width*height stored 32-bit words in `outWords`.
|
||||
static Bool ReadPackedLevelWordsViaScratch(GLuint texture, GLenum backendTarget, GLint level, GLint slice,
|
||||
GLsizei width, GLsizei height, Uint32* outWords) {
|
||||
if (texture == 0 || outWords == nullptr || width <= 0 || height <= 0 || level < 0 || slice < 0) return false;
|
||||
if (!g_GLESFuncs.glCopyImageSubData) return false;
|
||||
|
||||
// Horizontal bands, so neither the scratch image nor the staging buffer scales with the
|
||||
// level. The scratch is grow-only on purpose - a sweep down a mip chain must not
|
||||
// reallocate per level - which without a band cap would leave a 4096x4096 readback's
|
||||
// 64 MiB image parked for the rest of the process. The cap is 1 MiB of GL_R32UI, with
|
||||
// 4 MiB of staging behind it because the read lands four words per texel.
|
||||
constexpr SizeT kMaxScratchTexels = SizeT{1} << 18;
|
||||
const GLsizei bandRows = std::max<GLsizei>(
|
||||
1, static_cast<GLsizei>(std::min<SizeT>(kMaxScratchTexels / static_cast<SizeT>(width),
|
||||
static_cast<SizeT>(height))));
|
||||
const GLuint scratch = EnsurePackedWordScratchTexture(width, bandRows);
|
||||
if (scratch == 0) return false;
|
||||
|
||||
ScopedFramebufferBinding readBinding(/*saveRead=*/true, /*saveDraw=*/false);
|
||||
auto& scratchFB = ScratchFBOImpl::BlitReadFramebuffer();
|
||||
FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, ScratchFBOImpl::EnsureId(scratchFB));
|
||||
ScratchFBOImpl::EnsureColorAttachment2D(scratchFB, GL_READ_FRAMEBUFFER, scratch, GL_TEXTURE_2D, 0);
|
||||
ScratchFBOImpl::EnsureReadBuffer(scratchFB, GL_COLOR_ATTACHMENT0);
|
||||
if (g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
MGLOG_D("GetTexImage: the GL_R32UI scratch attachment is incomplete; falling back");
|
||||
return false;
|
||||
}
|
||||
|
||||
// GL_RGBA_INTEGER/GL_UNSIGNED_INT is the one combination ES guarantees for an integer
|
||||
// colour buffer, so the read lands four words per texel and the red one is compacted out
|
||||
// here. The PACK scope is the tight default rather than the application's, so a row comes
|
||||
// back packed at exactly `width * 4` words. One glGetError covers the whole loop: it
|
||||
// accumulates, and a failure anywhere means the caller falls back rather than trusting a
|
||||
// partial result.
|
||||
const SizeT wordsPerRow = static_cast<SizeT>(width) * 4;
|
||||
Vector<Uint32> staging(static_cast<SizeT>(bandRows) * wordsPerRow);
|
||||
ScopedPixelPackBuffer packBuffer(0);
|
||||
ScopedPackState packState(PixelStoreImpl::PackState{4, 0, 0, 0});
|
||||
ClearGLErrors();
|
||||
for (GLsizei y = 0; y < height; y += bandRows) {
|
||||
const GLsizei rows = std::min(bandRows, height - y);
|
||||
g_GLESFuncs.glCopyImageSubData(texture, backendTarget, level, 0, y, slice, scratch, GL_TEXTURE_2D, 0, 0,
|
||||
0, 0, width, rows, 1);
|
||||
g_GLESFuncs.glReadPixels(0, 0, width, rows, GL_RGBA_INTEGER, GL_UNSIGNED_INT, staging.data());
|
||||
for (GLsizei row = 0; row < rows; ++row) {
|
||||
const Uint32* srcRow = staging.data() + static_cast<SizeT>(row) * wordsPerRow;
|
||||
Uint32* dstRow = outWords + static_cast<SizeT>(y + row) * static_cast<SizeT>(width);
|
||||
for (GLsizei x = 0; x < width; ++x) dstRow[x] = srcRow[static_cast<SizeT>(x) * 4];
|
||||
}
|
||||
}
|
||||
const GLenum error = g_GLESFuncs.glGetError();
|
||||
if (error != GL_NO_ERROR) {
|
||||
MGLOG_D("GetTexImage: the GL_R32UI word readback of %s was refused (%s); falling back",
|
||||
MG_Util::ConvertGLEnumToString(backendTarget).c_str(),
|
||||
MG_Util::ConvertGLEnumToString(error).c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static Bool IsLegacyNativeReadPixelsFormat(GLenum format) {
|
||||
return format == GL_RGBA || format == GL_RGBA_INTEGER || format == GL_RED || format == GL_RED_INTEGER ||
|
||||
format == GL_DEPTH_COMPONENT || format == GL_STENCIL_INDEX || format == GL_DEPTH_STENCIL;
|
||||
@@ -8058,17 +8201,52 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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.
|
||||
// shadow branch further down was unreachable. Every other format still prefers the
|
||||
// GPU, so a rendered-into texture is unaffected.
|
||||
const Bool rawPackedWordRead = MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
|
||||
textureObject->GetFormat(), MG_Util::ConvertGLEnumToTextureInputFormat(format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
|
||||
// ...and the GPU CAN answer with the stored words after all, for any 32-bit packed
|
||||
// format and whoever wrote the level, by going through a scratch GL_R32UI image (see
|
||||
// ReadPackedLevelWordsViaScratch). Preferred over both routes below because it is the
|
||||
// only one that is right for a level glCopyImageSubData wrote: the shadow may never
|
||||
// have seen that write, and re-encoding the attachment cannot reproduce an RGB9_E5
|
||||
// shared exponent or an R11F_G11F_B10F NaN payload. A multisample image is excluded
|
||||
// because copy-image requires matching sample counts.
|
||||
if (rawPackedWordRead && textureObject->GetSamples() == 0) {
|
||||
// Copy-image addresses a cube map as ONE image with the face on z, where
|
||||
// glGetTexImage names the face in its target.
|
||||
const auto readUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
|
||||
const GLint copyBaseSlice =
|
||||
(readUploadTarget >= TextureUploadTarget::CubeMapPositiveX &&
|
||||
readUploadTarget <= TextureUploadTarget::CubeMapNegativeZ)
|
||||
? static_cast<GLint>(readUploadTarget) -
|
||||
static_cast<GLint>(TextureUploadTarget::CubeMapPositiveX)
|
||||
: 0;
|
||||
const GLenum copyTarget =
|
||||
TextureImpl::ConvertTextureTargetToBackendGLEnum(textureObject->GetTarget());
|
||||
const SizeT sliceWords = static_cast<SizeT>(size.x()) * static_cast<SizeT>(size.y());
|
||||
Vector<Uint32> words(sliceWords * static_cast<SizeT>(sliceCount));
|
||||
Bool allSlicesRead = true;
|
||||
for (GLsizei slice = 0; slice < sliceCount && allSlicesRead; ++slice) {
|
||||
allSlicesRead = ReadPackedLevelWordsViaScratch(backendTexId, copyTarget, level,
|
||||
copyBaseSlice + slice, size.x(), size.y(),
|
||||
words.data() + sliceWords * static_cast<SizeT>(slice));
|
||||
}
|
||||
if (allSlicesRead &&
|
||||
ReadbackImpl::StorePackedWordsToClient(reinterpret_cast<const Uint8*>(words.data()), size.x(),
|
||||
size.y(), sliceCount, type, pixels,
|
||||
applyPackImageParams)) {
|
||||
MGLOG_D("GetTexImage: finished %d slice(s) via the bit-exact GL_R32UI word readback", sliceCount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// The last resort for the one format the attachment route can never answer for: the
|
||||
// shadow is only right while nothing but a glTexImage has written the level, which is
|
||||
// why CopyImageSubData mirrors itself into it where it can.
|
||||
const Bool verbatimPackedShadowRead =
|
||||
MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding(textureObject->GetFormat()) &&
|
||||
MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
|
||||
textureObject->GetFormat(), MG_Util::ConvertGLEnumToTextureInputFormat(format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
|
||||
rawPackedWordRead;
|
||||
if (verbatimPackedShadowRead &&
|
||||
GetTexImageViaShadowConversion(textureMipmapObject,
|
||||
MG_Util::ConvertGLEnumToTextureUploadTarget(target), level, size.x(),
|
||||
@@ -9262,6 +9440,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
XfbImpl::OnBackendContextDestroyed();
|
||||
MultiDrawImpl::OnBackendContextDestroyed();
|
||||
ScratchFBOImpl::OnBackendContextDestroyed();
|
||||
ReleasePackedWordScratchTexture();
|
||||
FramebufferImpl::InvalidateFramebufferBindingCache();
|
||||
VertexArrayImpl::InvalidateVAOBindingCache();
|
||||
PixelStoreImpl::InvalidatePackStateCache();
|
||||
|
||||
@@ -91,6 +91,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/VertexArrayEnableDisableScenario.cpp
|
||||
Scenarios/CopyImageLevelRangeScenario.cpp
|
||||
Scenarios/CopyImageLayeredScenario.cpp
|
||||
Scenarios/PackedWordReadbackScenario.cpp
|
||||
Scenarios/LayeredAttachmentBarrierScenario.cpp
|
||||
Scenarios/LayeredTextureReadbackScenario.cpp
|
||||
Scenarios/AtomicCounterScenario.cpp
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PackedWordReadbackScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// glGetTexImage of a 32-bit packed format read with its OWN client type owes the application the
|
||||
// words the image HOLDS, and KHR-GL43.copy_image compares exactly those words. Two routes used to
|
||||
// answer, and both are wrong for a level glCopyImageSubData wrote:
|
||||
//
|
||||
// * the colour-attachment route reads GL_RGBA/GL_FLOAT and re-encodes, which canonicalizes an
|
||||
// RGB9_E5 shared exponent and collapses an R11F_G11F_B10F NaN payload to 1;
|
||||
// * the CPU shadow only holds what was UPLOADED, and the mirror that replays a copy into it
|
||||
// declines - silently - for a renderbuffer source, which has no shadow to mirror from.
|
||||
//
|
||||
// Both are pinned here with words the CTS itself uses, because both failures are invisible to a
|
||||
// value comparison: every assertion below is on BITS that decode to the very value the wrong
|
||||
// answer also decodes to.
|
||||
//
|
||||
// The fix is a raw-word route (DirectGLES::ReadPackedLevelWordsViaScratch: copy the level into a
|
||||
// scratch GL_R32UI image, read that back as unsigned integers), and DirectVulkan reaches the same
|
||||
// place through PackReadbackToClientOrPbo's raw-word branch over the staging bytes - so these
|
||||
// scenarios are backend-agnostic on purpose.
|
||||
|
||||
#include <cstddef>
|
||||
#include <ios>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr GLsizei kExtent = 4;
|
||||
|
||||
// The non-canonical RGB9_E5 word KHR-GL43.copy_image writes: R=0, G=0, B mantissa 63,
|
||||
// shared exponent 31, i.e. the value 8064, which the spec's own encoder would emit as
|
||||
// 0xe7e00000 instead. Anything that decodes and re-encodes hands back the canonical word.
|
||||
//
|
||||
// Reinterpreted in the destination of an RGB9_E5 -> R11F_G11F_B10F copy it is R=0,
|
||||
// G=1920, B=995 - and B's 5-bit exponent is all ones with a nonzero mantissa, i.e. a NaN
|
||||
// whose payload 3 does not survive a float32 round trip (it comes back as the canonical
|
||||
// payload 1, B=993, word 0xf87c0000). The two defects therefore land on the same word.
|
||||
constexpr GLuint kRgb9E5Word = 0xf8fc0000u;
|
||||
|
||||
// The R11F_G11F_B10F word the same test pairs with it: R=0, G=0, B = exponent 12,
|
||||
// mantissa 0 = 0.125. As an RGB9_E5 word it is all-zero channels with a shared exponent of
|
||||
// 12, which the canonical encoder would write as 0x00000000 - so a decode/re-encode of THIS
|
||||
// one loses every bit that distinguishes it.
|
||||
constexpr GLuint kR11fG11fB10fWord = 0x60000000u;
|
||||
|
||||
class PackedWordReadbackScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
DrainErrors();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
DeleteObjects();
|
||||
DrainErrors();
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
static void DrainErrors() {
|
||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
void DeleteObjects() {
|
||||
if (m_src != 0) glDeleteTextures(1, &m_src);
|
||||
if (m_dst != 0) glDeleteTextures(1, &m_dst);
|
||||
if (m_rbo != 0) glDeleteRenderbuffers(1, &m_rbo);
|
||||
m_src = 0;
|
||||
m_dst = 0;
|
||||
m_rbo = 0;
|
||||
}
|
||||
|
||||
// A complete single-level texture whose every texel holds `word`, uploaded through the
|
||||
// packed client type so the stored bits are the client's bits and nothing has had a
|
||||
// chance to re-encode them.
|
||||
GLuint MakePackedTexture(GLenum internalFormat, GLenum type, GLuint word) {
|
||||
const std::vector<GLuint> words(static_cast<std::size_t>(kExtent) * kExtent, word);
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(internalFormat), kExtent, kExtent, 0, GL_RGB, type,
|
||||
words.data());
|
||||
// What Utils::makeTextureComplete does in the conformance cases, and what
|
||||
// glCopyImageSubData requires of both endpoints.
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// Every texel of level 0, as raw client words.
|
||||
std::vector<GLuint> ReadPackedWords(GLuint texture, GLenum type) {
|
||||
std::vector<GLuint> words(static_cast<std::size_t>(kExtent) * kExtent, 0xDEADBEEFu);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGB, type, words.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return words;
|
||||
}
|
||||
|
||||
// The copy under test. Returns the error it raised so a driver that cannot perform the
|
||||
// move at all can skip rather than fail: the point of these cases is which BITS come
|
||||
// back, and there are none to compare if the copy never happened.
|
||||
GLenum CopyWholeImage(GLuint srcName, GLenum srcTarget, GLuint dstName, GLenum dstTarget) {
|
||||
DrainErrors();
|
||||
glCopyImageSubData(srcName, srcTarget, 0, 0, 0, 0, dstName, dstTarget, 0, 0, 0, 0, kExtent, kExtent,
|
||||
1);
|
||||
const GLenum error = glGetError();
|
||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the copy recorded more than one error";
|
||||
return error;
|
||||
}
|
||||
|
||||
static void ExpectEveryTexel(const std::vector<GLuint>& words, GLuint expected, const char* what) {
|
||||
for (std::size_t i = 0; i < words.size(); ++i) {
|
||||
ASSERT_EQ(words[i], expected)
|
||||
<< what << ": texel " << i << " read 0x" << std::hex << words[i] << ", expected 0x"
|
||||
<< expected;
|
||||
}
|
||||
}
|
||||
|
||||
GLuint m_src = 0;
|
||||
GLuint m_dst = 0;
|
||||
GLuint m_rbo = 0;
|
||||
};
|
||||
|
||||
// The control that has to hold before either regression means anything: a packed word
|
||||
// uploaded and read straight back must be the SAME word, not merely the same colour.
|
||||
TEST_F(PackedWordReadbackScenario, AnUploadedPackedWordReadsBackVerbatim) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
m_src = MakePackedTexture(GL_RGB9_E5, GL_UNSIGNED_INT_5_9_9_9_REV, kRgb9E5Word);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "RGB9_E5 upload";
|
||||
ExpectEveryTexel(ReadPackedWords(m_src, GL_UNSIGNED_INT_5_9_9_9_REV), kRgb9E5Word, "RGB9_E5 round trip");
|
||||
|
||||
m_dst = MakePackedTexture(GL_R11F_G11F_B10F, GL_UNSIGNED_INT_10F_11F_11F_REV, kR11fG11fB10fWord);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "R11F_G11F_B10F upload";
|
||||
ExpectEveryTexel(ReadPackedWords(m_dst, GL_UNSIGNED_INT_10F_11F_11F_REV), kR11fG11fB10fWord,
|
||||
"R11F_G11F_B10F round trip");
|
||||
}
|
||||
|
||||
// KHR-GL43.copy_image.functional rgb9_e5 -> r11f_g11f_b10f, all nine target combinations of
|
||||
// which failed on both GPUs. glCopyImageSubData is a raw block move, so the destination
|
||||
// physically holds the source's word - but the readback decoded it to float and re-encoded,
|
||||
// and the destination's blue field is a NaN whose payload float32 does not carry. Every
|
||||
// texel came back 0xf87c0000 (payload 1) instead of 0xf8fc0000 (payload 3): the same
|
||||
// "colour", two bits apart.
|
||||
TEST_F(PackedWordReadbackScenario, ACopiedRgb9E5WordSurvivesInAnR11fG11fB10fDestination) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
m_src = MakePackedTexture(GL_RGB9_E5, GL_UNSIGNED_INT_5_9_9_9_REV, kRgb9E5Word);
|
||||
m_dst = MakePackedTexture(GL_R11F_G11F_B10F, GL_UNSIGNED_INT_10F_11F_11F_REV, 0u);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup";
|
||||
|
||||
const GLenum copyError = CopyWholeImage(m_src, GL_TEXTURE_2D, m_dst, GL_TEXTURE_2D);
|
||||
if (copyError != static_cast<GLenum>(GL_NO_ERROR)) {
|
||||
GTEST_SKIP() << "this driver declined the RGB9_E5 -> R11F_G11F_B10F copy (" << copyError << ")";
|
||||
}
|
||||
|
||||
ExpectEveryTexel(ReadPackedWords(m_dst, GL_UNSIGNED_INT_10F_11F_11F_REV), kRgb9E5Word,
|
||||
"copied word in the R11F_G11F_B10F destination");
|
||||
// ...and the source is still the source. This is verify()'s FIRST check in the
|
||||
// conformance case, and the half that a canonicalizing readback fails on its own.
|
||||
ExpectEveryTexel(ReadPackedWords(m_src, GL_UNSIGNED_INT_5_9_9_9_REV), kRgb9E5Word,
|
||||
"the RGB9_E5 source after the copy");
|
||||
}
|
||||
|
||||
// KHR-GL43.copy_image.functional *->rgb9_e5 with a GL_RENDERBUFFER source: exactly the three
|
||||
// renderbuffer combinations of each such family failed, and no texture one did. The
|
||||
// destination's CPU shadow is what the readback answered from, the mirror that replays a
|
||||
// copy into it declines when an endpoint is a renderbuffer (there is no shadow to mirror
|
||||
// FROM), and the decline is silent - so glGetTexImage handed back the destination's
|
||||
// pre-copy contents. The word chosen here makes that unmissable: it decodes to the same
|
||||
// all-zero channels the canonical encoder would write as 0x00000000.
|
||||
TEST_F(PackedWordReadbackScenario, ACopyThroughARenderbufferReachesAnRgb9E5Destination) {
|
||||
if (!Ready()) GTEST_SKIP();
|
||||
|
||||
m_src = MakePackedTexture(GL_R11F_G11F_B10F, GL_UNSIGNED_INT_10F_11F_11F_REV, kR11fG11fB10fWord);
|
||||
m_dst = MakePackedTexture(GL_RGB9_E5, GL_UNSIGNED_INT_5_9_9_9_REV, 0xFFFFFFFFu);
|
||||
glGenRenderbuffers(1, &m_rbo);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, m_rbo);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_R11F_G11F_B10F, kExtent, kExtent);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "renderbuffer setup";
|
||||
|
||||
// The conformance case's own shape: texture -> renderbuffer -> texture.
|
||||
const GLenum toRenderbuffer = CopyWholeImage(m_src, GL_TEXTURE_2D, m_rbo, GL_RENDERBUFFER);
|
||||
if (toRenderbuffer != static_cast<GLenum>(GL_NO_ERROR)) {
|
||||
GTEST_SKIP() << "this driver declined a renderbuffer copy destination (" << toRenderbuffer << ")";
|
||||
}
|
||||
const GLenum fromRenderbuffer = CopyWholeImage(m_rbo, GL_RENDERBUFFER, m_dst, GL_TEXTURE_2D);
|
||||
if (fromRenderbuffer != static_cast<GLenum>(GL_NO_ERROR)) {
|
||||
GTEST_SKIP() << "this driver declined a renderbuffer copy source (" << fromRenderbuffer << ")";
|
||||
}
|
||||
|
||||
ExpectEveryTexel(ReadPackedWords(m_dst, GL_UNSIGNED_INT_5_9_9_9_REV), kR11fG11fB10fWord,
|
||||
"copied word in the RGB9_E5 destination");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -47,12 +47,18 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
||||
// 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.
|
||||
// canonical form, so no readback that goes through a decode cycle can return the stored words.
|
||||
//
|
||||
// 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.
|
||||
// Read this as "a FINITE value re-encodes to different bits", and nothing wider. This comment
|
||||
// used to assert that RGB10_A2, RGB10_A2UI and R11F_G11F_B10F "round-trip through float32
|
||||
// bit-exactly, so a GPU readback can answer for them", and that is false for
|
||||
// R11F_G11F_B10F: a field whose 5-bit exponent is all ones is an Inf or a NaN, and a NaN's
|
||||
// payload does not survive the trip (EncodeFloatToUnsignedSmallFloat re-encodes every NaN as
|
||||
// the canonical payload 1). glCopyImageSubData from an RGB9_E5 source produces exactly such a
|
||||
// word in the blue field on every texel, because the source's shared-exponent field is all
|
||||
// ones. The bit-exact answer for all four formats is the raw-word route,
|
||||
// DirectGLES::ReadPackedLevelWordsViaScratch; this predicate only picks which of the older
|
||||
// fallbacks to prefer when that route is unavailable.
|
||||
Bool HasRedundantPackedEncoding(TextureInternalFormat internalFormat);
|
||||
|
||||
// Decodes the canonical shadow-mip storage of `internalFormat` into wide RGBA texels for CPU
|
||||
|
||||
Reference in New Issue
Block a user