mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
[Fix, Test] (TextureFormatProcessor, DirectGLES, MG_IntegrationTest): give every unrenderable signed-normalized colour attachment an exact float substitute
This commit is contained in:
@@ -213,7 +213,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
||||
reasons.push_back("no colour-renderable three-channel format on OpenGL ES");
|
||||
}
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
||||
// A format is either 8- or 16-bit signed normalized, so at most one of the two ever
|
||||
// survives GetApplicablePixelFormatNormalizeOptions and the reason is not duplicated.
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
||||
reasons.push_back("EXT_render_snorm not supported");
|
||||
}
|
||||
|
||||
|
||||
@@ -171,6 +171,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (!capabilities.SupportsRenderSnorm || !capabilities.SupportsNorm16Texture) {
|
||||
options |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||
}
|
||||
// 8-bit signed-normalized storage is core ES, so only the rendering half is in
|
||||
// question here; the 16-bit bit above additionally needs EXT_texture_norm16 for the
|
||||
// encoding to exist at all.
|
||||
if (!capabilities.SupportsRenderSnorm) {
|
||||
options |= PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/AsyncCompileScenario.cpp
|
||||
Scenarios/XfbAfterClipDistanceScenario.cpp
|
||||
Scenarios/ThreeChannelAttachmentScenario.cpp
|
||||
Scenarios/SnormAttachmentScenario.cpp
|
||||
Scenarios/PipelineFailureScenario.cpp
|
||||
Scenarios/AdvertisedLimitsScenario.cpp
|
||||
Scenarios/PixelStoreSweepScenario.cpp
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SnormAttachmentScenario.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
|
||||
//
|
||||
// Scenario - SIGNED-NORMALIZED COLOUR ATTACHMENTS, on a live driver.
|
||||
//
|
||||
// The bug: a GLES driver without GL_EXT_render_snorm treats every signed-normalized format as
|
||||
// texture-only. DirectGLES had a colour-renderable substitute for exactly one of the eight
|
||||
// (GL_RGB16_SNORM, through the three-channel widening), so an R8_SNORM or R16_SNORM attachment got
|
||||
// no storage the driver would render into: the ES framebuffer was incomplete, the draw landed
|
||||
// nowhere, and glGetTexImage fell through to the CPU shadow - all zeroes for a texture created with
|
||||
// no data. KHR-GL4x.texture_swizzle renders into a SINGLE-CHANNEL SNORM output for every one of its
|
||||
// SNORM source formats, which is why all 46 of its GL43 SNORM cases failed on Mali.
|
||||
//
|
||||
// THE OTHER HALF, and the reason this scenario asserts VALUES rather than only completeness: the
|
||||
// substitute has to be exact. A half float's 11-bit mantissa cannot represent a 16-bit SNORM
|
||||
// channel - 23451/32767 quantizes about six SNORM steps away, against a conformance window of one -
|
||||
// so the 16-bit formats must land on a 32-bit float even though the 8-bit ones are fine in a half.
|
||||
// Trading 46 visible failures for silent precision loss in Iris' SNORM normal buffers would be the
|
||||
// worse outcome, so the round trip below is pinned tightly enough to fail on a half-float substitute
|
||||
// (tolerance two SNORM steps, half-float error six).
|
||||
//
|
||||
// WHAT THIS GATE CAN AND CANNOT SEE. Both CI drivers (Mesa llvmpipe) and Adreno expose
|
||||
// GL_EXT_render_snorm, so they take the NATIVE path here and the substitution stays dead. That is
|
||||
// precisely why the assertions are written as invariants of the format rather than of the fallback:
|
||||
// "a signed-normalized colour attachment is complete and round-trips its channel values" has to
|
||||
// hold whichever path answers it, so the scenario fails if anyone ever routes these formats to a
|
||||
// lossy storage on a driver where it IS live. The substitution itself can only be observed on a
|
||||
// device without EXT_render_snorm (Mali Immortalis-G925).
|
||||
//
|
||||
// DirectGLES only, like the three-channel scenario next door: DirectVulkan resolves SNORM formats
|
||||
// on its own terms and asserting Espryt's answers there would pin a coincidence.
|
||||
|
||||
#include <string>
|
||||
#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 const char* kVS = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
void main() {
|
||||
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// A uniform rather than a literal so nothing can constant-fold the value into a different
|
||||
// precision than the one the attachment stores.
|
||||
constexpr const char* kFS = R"(#version 330 core
|
||||
out vec4 oColor;
|
||||
uniform float uValue;
|
||||
void main() { oColor = vec4(uValue, 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
constexpr int kSize = 8;
|
||||
|
||||
// The two channel values the round trip is pinned on. Both are positive on purpose:
|
||||
// glReadPixels applies GL_CLAMP_READ_COLOR (GL_FIXED_ONLY by default) to a fixed-point
|
||||
// colour buffer, so the negative half of a SNORM attachment reads back as 0 and would
|
||||
// measure the clamp instead of the storage.
|
||||
constexpr int kSnorm8Value = 99;
|
||||
constexpr int kSnorm16Value = 23451;
|
||||
|
||||
class SnormAttachmentScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
if (Gl().BackendName() != "DirectGLES") {
|
||||
GTEST_SKIP() << "the signed-normalized substitution is a DirectGLES fallback; backend is "
|
||||
<< Gl().BackendName();
|
||||
}
|
||||
}
|
||||
|
||||
// A single-level 2D texture in `internalFormat`, or 0 when the driver rejects the
|
||||
// storage outright (which is a different failure from rejecting the ATTACHMENT).
|
||||
static GLuint MakeTexture(GLenum internalFormat) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kSize, kSize);
|
||||
if (glGetError() != GL_NO_ERROR) {
|
||||
glDeleteTextures(1, &texture);
|
||||
return 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;
|
||||
}
|
||||
|
||||
static GLenum SingleAttachmentStatus(GLenum internalFormat) {
|
||||
const GLuint texture = MakeTexture(internalFormat);
|
||||
if (texture == 0) return GL_NONE;
|
||||
GLuint fbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||
const GLenum status = glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &texture);
|
||||
return status;
|
||||
}
|
||||
|
||||
// Renders `value` into the red channel of a fresh `internalFormat` attachment and hands
|
||||
// back what glReadPixels sees. Returns false when the framebuffer never came up, which
|
||||
// is the failure mode this scenario exists for - a draw into an incomplete framebuffer
|
||||
// is dropped by the driver and leaves the caller reading the cleared texture.
|
||||
bool RenderAndReadRed(GLenum internalFormat, float value, float* outRed) {
|
||||
std::string error;
|
||||
const GLuint program = CompileProgram(kVS, kFS, &error);
|
||||
EXPECT_NE(program, 0u) << error;
|
||||
if (program == 0) return false;
|
||||
const GLint valueLocation = glGetUniformLocation(program, "uValue");
|
||||
EXPECT_GE(valueLocation, 0);
|
||||
|
||||
const GLuint texture = MakeTexture(internalFormat);
|
||||
EXPECT_NE(texture, 0u) << "the driver refused the texture storage itself";
|
||||
if (texture == 0) {
|
||||
glDeleteProgram(program);
|
||||
return false;
|
||||
}
|
||||
|
||||
GLuint fbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||
const bool complete = glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
|
||||
|
||||
if (complete) {
|
||||
const float quad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||
GLuint vao = 0;
|
||||
GLuint vbo = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
glUseProgram(program);
|
||||
glUniform1f(valueLocation, value);
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
// Cleared to zero so a dropped draw cannot be mistaken for a correct one.
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
std::vector<float> pixels(static_cast<std::size_t>(kSize) * kSize * 4, -1.0f);
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glReadPixels(0, 0, kSize, kSize, GL_RGBA, GL_FLOAT, pixels.data());
|
||||
if (outRed) *outRed = pixels[0];
|
||||
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &texture);
|
||||
glDeleteProgram(program);
|
||||
return complete;
|
||||
}
|
||||
};
|
||||
|
||||
// THE regression gate for the frontend's answer. Every one of these used to be
|
||||
// GL_FRAMEBUFFER_UNSUPPORTED on a driver without EXT_render_snorm, and nothing in the CTS
|
||||
// (or in Iris) checks the status before drawing, so the failure was silent all the way to a
|
||||
// readback of zeroes.
|
||||
TEST_F(SnormAttachmentScenario, SignedNormalizedColorAttachmentsReportComplete) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
// GL_R8 is the control: colour-renderable in ES core, so it must pass with or without
|
||||
// any substitution. If it ever fails, nothing below means anything.
|
||||
EXPECT_EQ(SingleAttachmentStatus(GL_R8), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||
<< "GL_R8 is ES-core colour-renderable";
|
||||
|
||||
// The single-channel pair KHR-GL4x.texture_swizzle renders into for every SNORM source
|
||||
// format - the whole 46-case failure.
|
||||
EXPECT_EQ(SingleAttachmentStatus(GL_R8_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
EXPECT_EQ(SingleAttachmentStatus(GL_R16_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
// ...and the two- and four-channel siblings, which are what a shaderpack actually
|
||||
// declares (Iris colortex buffers in RGBA16_SNORM).
|
||||
EXPECT_EQ(SingleAttachmentStatus(GL_RG8_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
EXPECT_EQ(SingleAttachmentStatus(GL_RG16_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
EXPECT_EQ(SingleAttachmentStatus(GL_RGBA8_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
EXPECT_EQ(SingleAttachmentStatus(GL_RGBA16_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||
|
||||
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||
}
|
||||
|
||||
// The other half: whatever storage answers for the attachment has to hold the channel value
|
||||
// to the format's own precision. This is the assertion that fails if the 16-bit formats are
|
||||
// ever routed to a half float - the substitute an implementer naturally reaches for, because
|
||||
// it is what the 8-bit ones correctly use.
|
||||
TEST_F(SnormAttachmentScenario, SignedNormalizedAttachmentsRoundTripTheirChannelValues) {
|
||||
if (!Ready() || IsSkipped()) return;
|
||||
|
||||
const float snorm8Expected = static_cast<float>(kSnorm8Value) / 127.0f;
|
||||
float red8 = -1.0f;
|
||||
ASSERT_TRUE(RenderAndReadRed(GL_R8_SNORM, snorm8Expected, &red8))
|
||||
<< "an R8_SNORM colour attachment must be complete before any value can be asserted";
|
||||
// Two 8-bit SNORM steps. A half float is exact here (worst case 0.03 of a step), so this
|
||||
// only has to catch a storage that quantizes harder than the format itself.
|
||||
EXPECT_NEAR(red8, snorm8Expected, 2.0f / 127.0f)
|
||||
<< "R8_SNORM attachment lost its channel value";
|
||||
EXPECT_GT(red8, 0.5f) << "the draw never landed - this is the cleared texture, not the rendered one";
|
||||
|
||||
const float snorm16Expected = static_cast<float>(kSnorm16Value) / 32767.0f;
|
||||
float red16 = -1.0f;
|
||||
ASSERT_TRUE(RenderAndReadRed(GL_R16_SNORM, snorm16Expected, &red16))
|
||||
<< "an R16_SNORM colour attachment must be complete before any value can be asserted";
|
||||
// Two 16-bit SNORM steps (6.1e-5). A half float would land 1.9e-4 away - three times
|
||||
// this window - which is exactly the failure this bound exists to catch.
|
||||
EXPECT_NEAR(red16, snorm16Expected, 2.0f / 32767.0f)
|
||||
<< "R16_SNORM attachment was stored in something that cannot hold 16 signed bits";
|
||||
EXPECT_GT(red16, 0.5f) << "the draw never landed - this is the cleared texture, not the rendered one";
|
||||
|
||||
float red16x4 = -1.0f;
|
||||
ASSERT_TRUE(RenderAndReadRed(GL_RGBA16_SNORM, snorm16Expected, &red16x4))
|
||||
<< "an RGBA16_SNORM colour attachment must be complete before any value can be asserted";
|
||||
EXPECT_NEAR(red16x4, snorm16Expected, 2.0f / 32767.0f)
|
||||
<< "RGBA16_SNORM attachment was stored in something that cannot hold 16 signed bits";
|
||||
|
||||
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -3868,6 +3868,131 @@ TEST_F(TextureTest, ColorAttachableTargetsRequestTheThreeChannelWidening) {
|
||||
PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget);
|
||||
EXPECT_FALSE(GetRenderTargetNormalizeOptions(capabilities, texture2DIndex) &
|
||||
PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget);
|
||||
|
||||
// ...and neither can an 8-bit one. That half of the answer used to be missing entirely, which
|
||||
// is why an R8_SNORM / RG8_SNORM colour attachment got no substitute at all on a driver
|
||||
// without EXT_render_snorm.
|
||||
EXPECT_TRUE(GetRenderTargetNormalizeOptions(noSnormCapabilities, texture2DIndex) &
|
||||
PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget);
|
||||
EXPECT_FALSE(GetRenderTargetNormalizeOptions(capabilities, texture2DIndex) &
|
||||
PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget);
|
||||
EXPECT_FALSE(GetRenderTargetNormalizeOptions(noSnormCapabilities, bufferIndex) &
|
||||
PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget);
|
||||
|
||||
// 8-bit signed-normalized storage is core ES, so only EXT_render_snorm gates the 8-bit bit;
|
||||
// the 16-bit one also needs EXT_texture_norm16 for the encoding to exist at all.
|
||||
MG_External::GLESCapabilities noNorm16Capabilities{};
|
||||
noNorm16Capabilities.SupportsRenderSnorm = true;
|
||||
noNorm16Capabilities.SupportsNorm16Texture = false;
|
||||
EXPECT_TRUE(GetRenderTargetNormalizeOptions(noNorm16Capabilities, texture2DIndex) &
|
||||
PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget);
|
||||
EXPECT_FALSE(GetRenderTargetNormalizeOptions(noNorm16Capabilities, texture2DIndex) &
|
||||
PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget);
|
||||
}
|
||||
|
||||
// ---- Signed-normalized colour-renderable substitution (KHR-GL4x.texture_swizzle on Mali) -------
|
||||
//
|
||||
// A driver without GL_EXT_render_snorm treats every signed-normalized format as texture-only, so a
|
||||
// colour attachment in one of them leaves the ES framebuffer incomplete: the draw lands nowhere and
|
||||
// the readback falls through to the CPU shadow, which for a glTexImage2D(..., nullptr) output
|
||||
// texture is all zeroes. The render-target bits used to reach GL_RGB16_SNORM alone, so five of the
|
||||
// eight SNORM formats - and in particular the single-channel GL_R8_SNORM / GL_R16_SNORM that
|
||||
// KHR-GL4x.texture_swizzle renders into for EVERY SNORM source format - had no fallback at all.
|
||||
|
||||
TEST_F(TextureTest, SnormRenderTargetOptionsApplyToEverySignedNormalizedFormat) {
|
||||
using MG_Util::TextureFormatProcessor::GetApplicablePixelFormatNormalizeOptions;
|
||||
const Flags<PixelFormatNormalizeOptionBit> requested =
|
||||
PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget | PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||
|
||||
for (const GLenum internalFormat : {GL_R8_SNORM, GL_RG8_SNORM, GL_RGB8_SNORM, GL_RGBA8_SNORM}) {
|
||||
const auto applicable = GetApplicablePixelFormatNormalizeOptions(internalFormat, requested);
|
||||
EXPECT_TRUE(applicable & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)
|
||||
<< "internalformat 0x" << std::hex << internalFormat;
|
||||
// The two bits are per precision class, so the 16-bit one never reaches an 8-bit format -
|
||||
// that is what keeps the fallback reason from naming both.
|
||||
EXPECT_FALSE(applicable & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget)
|
||||
<< "internalformat 0x" << std::hex << internalFormat;
|
||||
}
|
||||
for (const GLenum internalFormat : {GL_R16_SNORM, GL_RG16_SNORM, GL_RGB16_SNORM, GL_RGBA16_SNORM}) {
|
||||
const auto applicable = GetApplicablePixelFormatNormalizeOptions(internalFormat, requested);
|
||||
EXPECT_TRUE(applicable & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget)
|
||||
<< "internalformat 0x" << std::hex << internalFormat;
|
||||
EXPECT_FALSE(applicable & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)
|
||||
<< "internalformat 0x" << std::hex << internalFormat;
|
||||
}
|
||||
// GL_RGB16_SNORM used to be granted the 16-bit bit only when the three-channel widening was
|
||||
// requested alongside it, which made the answer depend on the order the caller assembled its
|
||||
// option set in. The capability probe and the runtime storage choice assemble different sets.
|
||||
EXPECT_TRUE(GetApplicablePixelFormatNormalizeOptions(GL_RGB16_SNORM,
|
||||
PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) &
|
||||
PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget);
|
||||
|
||||
// Nothing else responds to either bit; an unsigned-normalized or float format keeps its storage.
|
||||
for (const GLenum internalFormat : {GL_R8, GL_R16, GL_RGBA8, GL_RGBA16, GL_RGB16F, GL_RGBA32F, GL_RGB9_E5}) {
|
||||
EXPECT_FALSE(GetApplicablePixelFormatNormalizeOptions(internalFormat, requested))
|
||||
<< "internalformat 0x" << std::hex << internalFormat;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TextureTest, SnormRenderTargetSubstitutesKeepEveryChannelValueExactly) {
|
||||
using MG_Util::TextureFormatProcessor::NormalizePixelFormat;
|
||||
struct Case {
|
||||
GLenum requested;
|
||||
Flags<PixelFormatNormalizeOptionBit> options;
|
||||
GLenum internalFormat;
|
||||
GLenum format;
|
||||
GLenum type;
|
||||
};
|
||||
const Flags<PixelFormatNormalizeOptionBit> snorm8RT = PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget;
|
||||
const Flags<PixelFormatNormalizeOptionBit> snorm16RT = PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||
|
||||
const Case cases[] = {
|
||||
// 8-bit: a half float represents every v/127 exactly (the worst case, -123/127, quantizes
|
||||
// 0.03 of a SNORM step away), so it is the same storage GL_RGBA8_SNORM already always got.
|
||||
{GL_R8_SNORM, snorm8RT, GL_R16F, GL_RED, GL_FLOAT},
|
||||
{GL_RG8_SNORM, snorm8RT, GL_RG16F, GL_RG, GL_FLOAT},
|
||||
{GL_RGBA8_SNORM, snorm8RT, GL_RGBA16F, GL_RGBA, GL_FLOAT},
|
||||
// 16-bit: NOT a half float. Its spacing just below 1.0 is some 16 SNORM steps, so it hands
|
||||
// -23451/32767 back as -23457 against a conformance window of one step; a 32-bit float
|
||||
// round-trips all 65535 channel values.
|
||||
{GL_R16_SNORM, snorm16RT, GL_R32F, GL_RED, GL_FLOAT},
|
||||
{GL_RG16_SNORM, snorm16RT, GL_RG32F, GL_RG, GL_FLOAT},
|
||||
{GL_RGBA16_SNORM, snorm16RT, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||
// The render-target bit outranks the narrower fallbacks, whichever way the caller's option
|
||||
// set was assembled: the capability probe folds the driver options in, the runtime storage
|
||||
// choice can see the render-target bit alone, and the two have to pick the same storage.
|
||||
{GL_R16_SNORM, snorm16RT | PixelFormatNormalizeOptionBit::NoNorm16, GL_R32F, GL_RED, GL_FLOAT},
|
||||
{GL_RG16_SNORM, snorm16RT | PixelFormatNormalizeOptionBit::NoSnorm16, GL_RG32F, GL_RG, GL_FLOAT},
|
||||
{GL_RGBA16_SNORM,
|
||||
snorm16RT | PixelFormatNormalizeOptionBit::NoNorm16 | PixelFormatNormalizeOptionBit::NoSnorm16,
|
||||
GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||
// The three-channel formats go on through the widening, which outranks everything.
|
||||
{GL_RGB8_SNORM, snorm8RT | PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget, GL_RGBA16F, GL_RGBA,
|
||||
GL_FLOAT},
|
||||
{GL_RGB16_SNORM, snorm16RT | PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget, GL_RGBA32F, GL_RGBA,
|
||||
GL_FLOAT},
|
||||
// Control: with EXT_render_snorm neither bit is ever set, so the driver that renders to the
|
||||
// signed-normalized encoding keeps storing it byte for byte. This is the shape Adreno and
|
||||
// llvmpipe take, which is why the substitution is invisible on every gate the project runs.
|
||||
{GL_R8_SNORM, PixelFormatNormalizeOptionBit::None, GL_R8_SNORM, GL_RED, GL_BYTE},
|
||||
{GL_RG8_SNORM, PixelFormatNormalizeOptionBit::None, GL_RG8_SNORM, GL_RG, GL_BYTE},
|
||||
{GL_R16_SNORM, PixelFormatNormalizeOptionBit::None, GL_R16_SNORM, GL_RED, GL_SHORT},
|
||||
{GL_RG16_SNORM, PixelFormatNormalizeOptionBit::None, GL_RG16_SNORM, GL_RG, GL_SHORT},
|
||||
{GL_RGBA16_SNORM, PixelFormatNormalizeOptionBit::None, GL_RGBA16_SNORM, GL_RGBA, GL_SHORT},
|
||||
// ...and the bit for the other precision class does nothing on its own.
|
||||
{GL_R8_SNORM, snorm16RT, GL_R8_SNORM, GL_RED, GL_BYTE},
|
||||
{GL_R16_SNORM, snorm8RT, GL_R16_SNORM, GL_RED, GL_SHORT},
|
||||
};
|
||||
|
||||
for (const auto& testCase : cases) {
|
||||
GLenum internalFormat = 0;
|
||||
GLenum format = 0;
|
||||
GLenum type = 0;
|
||||
NormalizePixelFormat(testCase.requested, testCase.options, &internalFormat, &format, &type);
|
||||
EXPECT_EQ(internalFormat, testCase.internalFormat) << "requested 0x" << std::hex << testCase.requested;
|
||||
EXPECT_EQ(format, testCase.format) << "requested 0x" << std::hex << testCase.requested;
|
||||
EXPECT_EQ(type, testCase.type) << "requested 0x" << std::hex << testCase.requested;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TextureTest, ThreeChannelRenderTargetOptionAppliesToEveryDeniedThreeChannelFormat) {
|
||||
@@ -3918,9 +4043,10 @@ TEST_F(TextureTest, ThreeChannelWideningRetargetsInternalFormatAndTransferPairTo
|
||||
{GL_RGB16F, widen, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT},
|
||||
{GL_RGB32F, widen, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||
// 16-bit SNORM keeps its encoding where EXT_render_snorm can render to it; a half float's
|
||||
// 11-bit mantissa cannot represent a 16-bit SNORM channel exactly.
|
||||
// 11-bit mantissa cannot represent a 16-bit SNORM channel exactly, so the driver that
|
||||
// cannot render to the encoding gets the 32-bit float rather than the half.
|
||||
{GL_RGB16_SNORM, widen, GL_RGBA16_SNORM, GL_RGBA, GL_SHORT},
|
||||
{GL_RGB16_SNORM, widenNoSnorm16, GL_RGBA16F, GL_RGBA, GL_FLOAT},
|
||||
{GL_RGB16_SNORM, widenNoSnorm16, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||
// 16-bit UNORM and the legacy 10/12-bit formats stored as RGB16.
|
||||
{GL_RGB16, widen, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||
{GL_RGB10, widen, GL_RGBA32F, GL_RGBA, GL_FLOAT},
|
||||
|
||||
@@ -558,8 +558,9 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
} else {
|
||||
builder.Warn("GL_EXT_render_snorm",
|
||||
"not supported; signed-normalized formats are texture-only, so every SNORM "
|
||||
"render target is stored as a float (GL_RGBA8_SNORM/GL_RGB8_SNORM -> "
|
||||
"GL_RGBA16F) and its fragment outputs are clamped to [-1,1] in software");
|
||||
"render target is stored as a float (8-bit -> *16F, 16-bit -> *32F, which "
|
||||
"is the narrowest float that still holds a 16-bit SNORM channel exactly) "
|
||||
"and its fragment outputs are clamped to [-1,1] in software");
|
||||
}
|
||||
// FAIL, not WARN: ES 3.x core makes every float format texture-only, and every Iris
|
||||
// shaderpack renders into at least GL_R11F_G11F_B10F (Complementary's colortex0, BSL's
|
||||
|
||||
@@ -31,32 +31,41 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRgb16;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||
break;
|
||||
// The two render-target bits reach EVERY signed-normalized format, one-, two- and
|
||||
// four-channel included. They used to be granted to GL_RGB16_SNORM alone, which left the
|
||||
// other seven with no colour-renderable fallback at all on a driver without
|
||||
// EXT_render_snorm: an R8_SNORM or R16_SNORM attachment (what KHR-GL4x.texture_swizzle
|
||||
// renders into for every SNORM source format) got no substitute, so the ES framebuffer was
|
||||
// incomplete, the draw landed nowhere and the readback fell through to the never-written
|
||||
// CPU shadow.
|
||||
case GL_RGB16_SNORM:
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRGB16Snorm;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||
}
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||
break;
|
||||
case GL_RGBA16_SNORM:
|
||||
case GL_RG16_SNORM:
|
||||
case GL_R16_SNORM:
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||
break;
|
||||
case GL_RGBA8_SNORM:
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget;
|
||||
break;
|
||||
case GL_RGB8_SNORM:
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget;
|
||||
break;
|
||||
case GL_RG8_SNORM:
|
||||
case GL_R8_SNORM:
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8;
|
||||
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget;
|
||||
break;
|
||||
// The rest of the three-channel formats no real ES driver renders to. They have no
|
||||
// other fallback: none of the driver/forced option bits names them, so before the
|
||||
@@ -113,9 +122,12 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
return {GL_RGBA16F, GL_RGBA, GL_FLOAT};
|
||||
case GL_RGB16_SNORM:
|
||||
// A half float loses the low bits of a 16-bit SNORM channel, so keep the
|
||||
// signed-normalized encoding whenever the driver can render to it.
|
||||
// signed-normalized encoding whenever the driver can render to it - and when it
|
||||
// cannot, widen to the 32-bit float, which is the only renderable storage that
|
||||
// still holds all 65535 channel values exactly. GL_RGBA16F here handed -23451/32767
|
||||
// back as -23457, six times the +/-1-step window KHR-GL4x.texture_swizzle allows.
|
||||
return (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget)
|
||||
? ThreeChannelWidening{GL_RGBA16F, GL_RGBA, GL_FLOAT}
|
||||
? ThreeChannelWidening{GL_RGBA32F, GL_RGBA, GL_FLOAT}
|
||||
: ThreeChannelWidening{GL_RGBA16_SNORM, GL_RGBA, GL_SHORT};
|
||||
// Unsigned-normalized 16-bit (and the legacy 10/12-bit formats stored as RGB16):
|
||||
// GL_RGB32F is a legal ES texture format but is not colour-renderable either.
|
||||
@@ -203,7 +215,17 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
}
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
// NoSnorm16RenderTarget outranks the other two 16-bit fallbacks on purpose: it is the
|
||||
// only one whose substitute has to be EXACT, so it picks the 32-bit float rather than
|
||||
// the half the driver/ANGLE fallbacks settle for. The capability probe folds the
|
||||
// driver options and the render-target options into one set while the runtime storage
|
||||
// choice can see the render-target bit alone (GetRuntimeFallbackNormalizeOptions), so
|
||||
// the two would disagree on the storage format without a fixed precedence.
|
||||
case GL_RGBA16_SNORM:
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
||||
*outInternalFormat = GL_RGBA32F;
|
||||
break;
|
||||
}
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
||||
*outInternalFormat = GL_RGBA16F;
|
||||
@@ -212,6 +234,12 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
case GL_RGB16_SNORM:
|
||||
// The three-channel widening below replaces this whenever the target has to stay
|
||||
// renderable; GL_RGB32F keeps the precision for the targets that do not.
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
||||
*outInternalFormat = GL_RGB32F;
|
||||
break;
|
||||
}
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoRGB16Snorm) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
||||
@@ -221,6 +249,10 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
case GL_RG16_SNORM:
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
||||
*outInternalFormat = GL_RG32F;
|
||||
break;
|
||||
}
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
||||
*outInternalFormat = GL_RG16F;
|
||||
@@ -229,6 +261,10 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
case GL_R16_SNORM:
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
||||
*outInternalFormat = GL_R32F;
|
||||
break;
|
||||
}
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
||||
*outInternalFormat = GL_R16F;
|
||||
@@ -236,30 +272,36 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
}
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
// 8-bit SNORM: the half float already IS exact here, so the render-target bit lands on
|
||||
// the same storage the other two 8-bit fallbacks pick.
|
||||
case GL_RGBA8_SNORM:
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoSnorm8) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm)) {
|
||||
(options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
||||
*outInternalFormat = GL_RGBA16F;
|
||||
break;
|
||||
}
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
case GL_RGB8_SNORM:
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm8) {
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoSnorm8) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
||||
*outInternalFormat = GL_RGB16F;
|
||||
break;
|
||||
}
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
case GL_RG8_SNORM:
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm8) {
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoSnorm8) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
||||
*outInternalFormat = GL_RG16F;
|
||||
break;
|
||||
}
|
||||
*outInternalFormat = internalFormat;
|
||||
break;
|
||||
case GL_R8_SNORM:
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm8) {
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoSnorm8) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
||||
*outInternalFormat = GL_R16F;
|
||||
break;
|
||||
}
|
||||
@@ -533,7 +575,8 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
||||
(internalFormat == GL_RGB16_SNORM &&
|
||||
(options & PixelFormatNormalizeOptionBit::NoRGB16Snorm)) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget)) {
|
||||
*outType = GL_FLOAT;
|
||||
break;
|
||||
} else {
|
||||
@@ -543,7 +586,8 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
case GL_RGB8_SNORM:
|
||||
case GL_RG8_SNORM:
|
||||
case GL_R8_SNORM:
|
||||
if (options & PixelFormatNormalizeOptionBit::NoSnorm8) {
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoSnorm8) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
||||
*outType = GL_FLOAT;
|
||||
break;
|
||||
}
|
||||
@@ -551,7 +595,8 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
break;
|
||||
case GL_RGBA8_SNORM:
|
||||
if ((options & PixelFormatNormalizeOptionBit::NoSnorm8) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm)) {
|
||||
(options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm) ||
|
||||
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
||||
*outType = GL_FLOAT;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -29,11 +29,21 @@ namespace MobileGL {
|
||||
// three-channel client data with an alpha of 1.0, and sampling/readback has to hide
|
||||
// the added alpha again (BackendTextureFormatAddsAlpha).
|
||||
NoThreeChannelRenderTarget = 1 << 7,
|
||||
// Pairs with the bit above: the widened four-channel format has to stay renderable AND
|
||||
// keep 16-bit signed-normalized precision, which needs both EXT_texture_norm16 and
|
||||
// EXT_render_snorm. Without them the only renderable widening left is a half float, whose
|
||||
// 11-bit mantissa cannot represent a 16-bit SNORM channel exactly.
|
||||
// A 16-bit signed-normalized image has to back a colour attachment, and the driver cannot
|
||||
// render to the signed-normalized encoding itself: that needs both EXT_texture_norm16 and
|
||||
// EXT_render_snorm, and without either one an R16_SNORM / RG16_SNORM / RGB16_SNORM /
|
||||
// RGBA16_SNORM attachment is texture-only, so the framebuffer is never complete and the
|
||||
// draw silently lands nowhere. The substitute is a 32-bit float, NOT the half float the
|
||||
// other SNORM fallbacks use: a half's 11-bit mantissa cannot represent a 16-bit SNORM
|
||||
// channel exactly - its spacing just below 1.0 is 2^-11, some 16 SNORM steps, so
|
||||
// -23451/32767 comes back as -23457 - while a 32-bit float round-trips every one of the
|
||||
// 65535 channel values bit for bit.
|
||||
NoSnorm16RenderTarget = 1 << 8,
|
||||
// The 8-bit twin of the bit above: without EXT_render_snorm an R8_SNORM / RG8_SNORM /
|
||||
// RGB8_SNORM / RGBA8_SNORM colour attachment is not renderable either. Here a half float
|
||||
// IS exact - every value in [-127, 127] divided by 127 round-trips through a half - so the
|
||||
// substitute matches what the always-on GL_RGBA8_SNORM fallback already picks.
|
||||
NoSnorm8RenderTarget = 1 << 9,
|
||||
None = 0,
|
||||
};
|
||||
namespace MG_Util::TextureFormatProcessor {
|
||||
|
||||
Reference in New Issue
Block a user