[Merge] (DirectGLES, ShaderTranspiler): take the viewport routing and image repairs under the fp64 and qualifier fixes

This commit is contained in:
2026-08-21 22:21:17 -04:00
18 changed files with 1338 additions and 122 deletions
@@ -262,6 +262,95 @@ void main()
}
}
// GL_R11F_G11F_B10F, the format the allFormats and allTargets walkers stop at once the
// channel widening has carried everything before it - and the one carrier that is NOT a
// channel widening. It has no core format of its own per-channel width, so it is carried
// in GL_RGBA16F, whose 5-bit exponent and longer mantissa hold every 11f (e5m6) and 10f
// (e5m5) value exactly.
//
// What makes this case different from every other one here, and why it is worth its own
// test: the frontend's shadow for this format is ONE PACKED 32-BIT WORD per texel, not
// three components of the carrier's type. The upload therefore has to DECODE it, where
// every other widening only pads channels onto data already in the right component type.
// A widening that reused the channel repack reads three floats out of a four-byte texel
// and shears the whole level - which a STORE test cannot see, because the dispatch
// overwrites every texel the upload got wrong. So the seed here is per-texel distinct and
// is checked through an imageLoad BEFORE anything is stored.
//
// Every constant is chosen to be exact in both encodings, so the comparisons can be
// equality rather than tolerance: the 1/8 steps need three mantissa bits of the 11f
// channels' six, and the 1/16 steps at exponent 1 need one of the 10f channel's five.
TEST_F(NonCoreImageFormatScenario, PackedFloatImageDecodesItsUploadAndDropsSurplusStores) {
if (!Ready()) GTEST_SKIP() << "no GL context";
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
constexpr int kTexels = kExtent * kExtent;
std::vector<float> seed(static_cast<std::size_t>(kTexels) * 3u, 0.0f);
for (int texel = 0; texel < kTexels; ++texel) {
seed[texel * 3 + 0] = 1.0f + static_cast<float>(texel) / 8.0f;
seed[texel * 3 + 1] = 2.0f + static_cast<float>(texel) / 8.0f;
seed[texel * 3 + 2] = 3.0f + static_cast<float>(texel) / 16.0f;
}
const std::vector<float> wideSeed(static_cast<std::size_t>(kTexels) * 4u, -1.0f);
const GLuint narrow = MakeTexture(GL_R11F_G11F_B10F, GL_RGB, GL_FLOAT, seed.data());
const GLuint wide = MakeTexture(GL_RGBA32F, GL_RGBA, GL_FLOAT, wideSeed.data());
if (narrow == 0 || wide == 0) return;
const GLuint loadProgram = MakeComputeProgram(R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (r11f_g11f_b10f, binding = 0) readonly uniform image2D narrow;
layout (rgba32f, binding = 1) writeonly uniform image2D wide;
void main()
{
ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
imageStore(wide, coord, imageLoad(narrow, coord));
}
)");
const GLuint storeProgram = MakeComputeProgram(R"(#version 430 core
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (r11f_g11f_b10f, binding = 0) writeonly uniform image2D narrow;
void main()
{
imageStore(narrow, ivec2(gl_GlobalInvocationID.xy), vec4(5.0, 6.0, 7.0, 8.0));
}
)");
if (loadProgram == 0 || storeProgram == 0) return;
// THE UPLOAD, read back through the image. A sheared decode still produces plausible
// floats, so the check is per texel and the seed never repeats a value.
BindImage(kNarrowUnit, narrow, GL_R11F_G11F_B10F, GL_READ_ONLY);
BindImage(kWideUnit, wide, GL_RGBA32F, GL_WRITE_ONLY);
Dispatch(loadProgram);
const std::vector<float> loaded = ReadFloats(wide, GL_RGBA, 4);
for (int texel = 0; texel < kTexels; ++texel) {
EXPECT_FLOAT_EQ(loaded[texel * 4 + 0], seed[texel * 3 + 0]) << "texel " << texel << " red";
EXPECT_FLOAT_EQ(loaded[texel * 4 + 1], seed[texel * 3 + 1]) << "texel " << texel << " green";
EXPECT_FLOAT_EQ(loaded[texel * 4 + 2], seed[texel * 3 + 2]) << "texel " << texel << " blue";
EXPECT_FLOAT_EQ(loaded[texel * 4 + 3], 1.0f)
<< "texel " << texel << ": imageLoad on a format without alpha must report 1";
}
// THE STORE. Three channels survive and the fourth is dropped, which is the mask this
// format needs and no other widened format does - every other carrier here pins two
// or three of the carrier's channels, this one pins only alpha.
BindImage(kNarrowUnit, narrow, GL_R11F_G11F_B10F, GL_WRITE_ONLY);
Dispatch(storeProgram);
const std::vector<float> stored = ReadFloats(narrow, GL_RGB, 3);
for (int texel = 0; texel < kTexels; ++texel) {
EXPECT_FLOAT_EQ(stored[texel * 3 + 0], 5.0f) << "texel " << texel << " red";
EXPECT_FLOAT_EQ(stored[texel * 3 + 1], 6.0f) << "texel " << texel << " green";
EXPECT_FLOAT_EQ(stored[texel * 3 + 2], 7.0f) << "texel " << texel << " blue";
}
}
// GL_R8UI: the only format KHR-GL43.shader_image_load_store.single-byte_data_alignment
// declares, and one SPIRV-Cross refuses to print for ESSL at all, so before the emulation
// no text was produced for the stage and the dispatch could not run.
@@ -30,15 +30,23 @@
// applies the flip to viewport 0 and forgets the other fifteen renders a correct-looking FBO and
// an upside-down window - the classic multi-viewport bug, and invisible to every FBO-only case.
//
// HONEST LIMIT OF THIS FILE. DirectGLES SKIPS every case: GLES has one viewport, one scissor
// rectangle and no gl_ViewportIndex, so routing to index > 0 is an emulation feature that has
// not been built (the Espryt half of KHR-GL43.viewport_array's rendering group is deliberately
// still red). The skip is explicit rather than silent so a future emulation lands here as a
// failing test and not as a test that was quietly never running. DirectVulkan additionally
// skips when the device lacks the multiViewport feature - Vulkan then forbids a pipeline from
// declaring more than one viewport at all, which is a device limit and not a MobileGL bug;
// lavapipe (every CI lane) and both Mali/Adreno devices support it, so the cases do run where
// it matters.
// BOTH BACKENDS RUN EVERY CASE, by two completely different routes, which is the point of
// keeping them in one file. DirectVulkan declares sixteen viewports on the pipeline and lets the
// hardware route. DirectGLES has one viewport, one scissor rectangle and one depth range and no
// gl_ViewportIndex at all, so it EMULATES: the builtin becomes a flat varying, the fragment stage
// gets a gate, and the draw is replayed once per distinct viewport state (Managers.h,
// ForEachViewportRoutingPass). Every assertion below is about pixels, so it cannot tell the two
// apart - which is exactly what has to be true.
//
// DirectVulkan skips when the device lacks the multiViewport feature - Vulkan then forbids a
// pipeline from declaring more than one viewport at all, which is a device limit and not a
// MobileGL bug; lavapipe (every CI lane) and both Mali/Adreno devices support it, so the cases do
// run where it matters.
//
// The last case is the negative control for the emulation and runs on DirectGLES only: it builds
// the SAME program with the emulation switched off and requires the routing to collapse onto
// viewport 0. Without it every assertion above could be satisfied by a backend that happened to
// be right for some other reason, and the emulation's own switch would be untested.
#include <cmath>
#include <string>
@@ -47,6 +55,10 @@
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
// For the emulation switch the negative-control case below flips. Nothing else in this file needs
// to know which backend it is running on.
#include <Config.h>
#ifdef GLAPI
#undef GLAPI
#endif
@@ -142,13 +154,6 @@ void main() { fragColor = gl_FragCoord.z; }
ScenarioTest::SetUp();
if (!Ready()) return;
if (Gl().BackendName() == "DirectGLES") {
GTEST_SKIP() << "gl_ViewportIndex routing is not emulated on DirectGLES: GLES has one viewport "
"and one scissor rectangle, so every index rasterizes as index 0. The indexed "
"STATE is still asserted (MG_Test RenderStateTest); this is the deferred "
"rendering half of KHR-GL43.viewport_array.";
}
GLint maxViewports = 0;
glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
ASSERT_GE(maxViewports, kViewportCount) << "GL 4.3 core requires GL_MAX_VIEWPORTS >= 16";
@@ -520,11 +525,78 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
DestroyIntTarget(target);
}
// --- 4. an explicitly EMPTY scissor box clips, it does not mean "never written" --------
// --- 4. the negative control for the DirectGLES emulation -----------------------------
//
// Deliberately NOT a ViewportArrayScenario case, because it must run on DirectGLES - the
// backend that got it wrong - and that fixture skips there. It needs none of the routing:
// one viewport, one scissor rectangle, no geometry stage.
// Everything above is a claim about pixels, and a claim about pixels cannot tell an
// emulation that works from a backend that was going to be right anyway. This case builds
// the SAME program with MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION off and requires case 1's
// result to COLLAPSE: with no routing, every geometry invocation rasterizes against
// viewport 0's rectangle, so the last invocation paints the whole surface and every cell
// reads 15 instead of its own index. That is the pre-emulation behaviour this backend had
// (and the failure signature KHR-GL43.viewport_array reported on it), pinned here so that
// (a) the three cases above are known to be testing the emulation and not the weather,
// and (b) the switch itself has a test.
//
// DirectGLES only: the flag steers nothing on DirectVulkan, which routes natively.
TEST_F(ViewportArrayScenario, WithoutTheEmulationEveryIndexCollapsesOntoViewportZero) {
if (Gl().BackendName() != "DirectGLES") {
GTEST_SKIP() << "the emulation switch is a DirectGLES concern; DirectVulkan routes "
"gl_ViewportIndex natively and ignores it";
}
// The feature table is a process-global and this fixture shares its context with every
// other scenario in the process, so the restore is not optional.
struct ScopedEmulationOff {
ScopedEmulationOff(): saved(MobileGL::MG_Config::Features.ViewportArrayEmulation) {
MobileGL::MG_Config::Features.ViewportArrayEmulation =
MobileGL::MG_Config::QuirkOverride::ForceOff;
}
~ScopedEmulationOff() { MobileGL::MG_Config::Features.ViewportArrayEmulation = saved; }
MobileGL::MG_Config::QuirkOverride saved;
};
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
SetupGridViewports(kCellSize, kCellSize);
GLuint unroutedProgram = 0;
{
const ScopedEmulationOff scopedEmulationOff;
// A FRESH program: the emitted ESSL is decided at link time and memoized on a key
// that carries this flag, so reusing m_program would just replay the routed build.
unroutedProgram = BuildProgram(kGridGeometrySource, kIntFragmentSource);
ASSERT_NE(unroutedProgram, 0u) << "unrouted program failed to build: " << m_buildLog;
glUseProgram(unroutedProgram);
glBindVertexArray(m_vao);
glDrawArrays(GL_POINTS, 0, 1);
ASSERT_EQ(glGetError(), GL_NO_ERROR);
}
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
// Cell (0, 0) IS viewport 0's rectangle, so it is the one cell an unrouted draw paints
// with something. Everything it holds comes from the last geometry invocation.
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, 0, 0), kViewportCount - 1)
<< "with the emulation off, viewport 0's rectangle must hold the LAST invocation's "
"index - if it holds 0 the routing is still happening and this control proves "
"nothing";
for (int y = 0; y < kGridSide; ++y) {
for (int x = 0; x < kGridSide; ++x) {
if (x == 0 && y == 0) continue;
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), kUnwritten)
<< "cell (" << x << ", " << y << ") is outside viewport 0's rectangle and an "
<< "unrouted draw cannot reach it";
}
}
glUseProgram(0);
glDeleteProgram(unroutedProgram);
DestroyIntTarget(target);
}
// --- 5. an explicitly EMPTY scissor box clips, it does not mean "never written" --------
//
// Deliberately NOT a ViewportArrayScenario case, because that fixture's geometry stage
// routes and this claim needs none of it: one viewport, one scissor rectangle, no
// geometry stage - and it has to hold identically whether or not anything routes.
//
// glScissor(0, 0, 0, 0) is legal GL meaning "the scissor test rejects every fragment",
// but it is byte-identical to the all-zero rectangle a context starts with, whose meaning