[Test] (IntegrationTest): gl_NumSamples scenario - the value must follow the draw framebuffer, not the link

This commit is contained in:
2026-08-27 03:35:56 -04:00
parent 2ae848ca19
commit b7a694711a
2 changed files with 187 additions and 0 deletions
@@ -66,6 +66,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/PrimitiveRestartScenario.cpp
Scenarios/FragCoordOriginScenario.cpp
Scenarios/ClearThenReadPixelsScenario.cpp
Scenarios/SampleVariablesScenario.cpp
Scenarios/DepthStencilReadbackScenario.cpp
Scenarios/DepthStencilReadbackMatrixScenario.cpp
Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
@@ -0,0 +1,186 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SampleVariablesScenario.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 - gl_NumSamples REACHES THE SHADER, AND IT FOLLOWS THE DRAW FRAMEBUFFER.
//
// glslang declares gl_NumSamples only when it is NOT targeting SPIR-V - both the desktop and the
// ES branch of Initialize.cpp wrap `uniform int gl_NumSamples;` in `if (spvVersion.spv == 0)`,
// because SPIR-V has no NumSamples builtin to lower it to - and MobileGL always targets SPIR-V.
// Every fragment shader that read the built-in therefore died at COMPILE time with
// "'gl_NumSamples' : undeclared identifier", which is all 144 KHR-GL46.sample_variables.mask.*
// bodies plus their es_31_compatibility twins.
//
// The source pipeline now lowers it onto a reserved default-block uniform and the draw path writes
// the current draw framebuffer's sample count into it. Two claims, and the second is the one a
// compile-only test cannot make: the value must be the DRAW FRAMEBUFFER's, so one program drawn
// into a multisample target and then into a single-sample target has to report both counts. A
// link-time bake would pass the first assertion and fail the second, which is exactly why the
// write lives per draw.
//
// llvmpipe and lavapipe both offer 4x multisample RGBA8, so this runs for real in CI rather than
// skipping; the skips below are for a driver that offers no multisample renderbuffer at all.
#include <algorithm>
#include <string>
#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 400 core
in vec2 aPos;
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
)";
// gl_NumSamples scaled so each count lands on its own well-separated 8-bit value: 1 -> 16,
// 2 -> 32, 4 -> 64. Every sample of the fragment gets the same colour, so the resolve blit
// averages identical values and the readback is exact rather than approximate.
constexpr const char* kFS = R"(#version 400 core
out vec4 o_color;
void main() { o_color = vec4(float(gl_NumSamples) * (16.0 / 255.0), 0.0, 0.0, 1.0); }
)";
class SampleVariablesScenario : public ScenarioTest {};
void DrawFullViewportQuad(unsigned int program) {
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
GLuint vao = 0, vbo = 0;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
glUseProgram(program);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
glDeleteBuffers(1, &vbo);
glDeleteVertexArrays(1, &vao);
}
} // namespace
TEST_F(SampleVariablesScenario, GlNumSamplesFollowsTheDrawFramebuffersSampleCount) {
if (!Ready()) return;
HeadlessGL& gl = Gl();
const int width = gl.Width();
const int height = gl.Height();
ASSERT_GE(width, 8);
ASSERT_GE(height, 8);
std::string error;
const unsigned int program = CompileProgram(kVS, kFS, &error);
// The compile failure this scenario exists for lands here, with glslang's own text.
ASSERT_NE(program, 0u) << error;
GLint maxSamples = 0;
glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
const GLint requestedSamples = std::min<GLint>(maxSamples, 4);
if (requestedSamples < 2) {
glDeleteProgram(program);
GTEST_SKIP() << "GL_MAX_SAMPLES is " << maxSamples << "; this needs a multisample renderbuffer";
}
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
// ---- multisample target ----
GLuint msFbo = 0, msRbo = 0;
glGenFramebuffers(1, &msFbo);
glBindFramebuffer(GL_FRAMEBUFFER, msFbo);
glGenRenderbuffers(1, &msRbo);
glBindRenderbuffer(GL_RENDERBUFFER, msRbo);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, requestedSamples, GL_RGBA8, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, msRbo);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
glDeleteRenderbuffers(1, &msRbo);
glDeleteFramebuffers(1, &msFbo);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteProgram(program);
GTEST_SKIP() << "no complete " << requestedSamples << "x multisample RGBA8 renderbuffer on this driver";
}
// What the driver actually allocated - a request is a lower bound, and the shader has to
// agree with the query rather than with what was asked for.
GLint realizedSamples = 0;
glGetIntegerv(GL_SAMPLES, &realizedSamples);
ASSERT_GE(realizedSamples, 2) << "the multisample framebuffer reports GL_SAMPLES " << realizedSamples;
glViewport(0, 0, width, height);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
DrawFullViewportQuad(program);
EXPECT_EQ(FirstGLError(), 0u);
// Resolve into the default framebuffer to read it back.
BindDefaultFramebuffer();
glViewport(0, 0, width, height);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
glBindFramebuffer(GL_READ_FRAMEBUFFER, msFbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
EXPECT_EQ(FirstGLError(), 0u);
{
const Image resolved = ReadPixels(width, height);
const Rgba8 centre = resolved.At(width / 2, height / 2);
EXPECT_NEAR(centre.r, 16 * realizedSamples, 2)
<< "gl_NumSamples read " << (centre.r / 16.0) << " into a " << realizedSamples
<< "-sample framebuffer; 1 means the reserved uniform was never written, 0 means it was "
<< "written but never uploaded";
}
gl.EndFrame();
// ---- the SAME program into a single-sample target ----
// A link-time bake of the sample count would keep reporting the multisample value here.
GLuint ssFbo = 0, ssRbo = 0;
glGenFramebuffers(1, &ssFbo);
glBindFramebuffer(GL_FRAMEBUFFER, ssFbo);
glGenRenderbuffers(1, &ssRbo);
glBindRenderbuffer(GL_RENDERBUFFER, ssRbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, ssRbo);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
glViewport(0, 0, width, height);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
DrawFullViewportQuad(program);
EXPECT_EQ(FirstGLError(), 0u);
{
const Image single = ReadPixels(width, height);
const Rgba8 centre = single.At(width / 2, height / 2);
// GL 4.6 core 15.2.2: gl_NumSamples is ONE for a non-multisample framebuffer, where
// glGetIntegerv(GL_SAMPLES) answers zero.
EXPECT_NEAR(centre.r, 16, 2)
<< "gl_NumSamples read " << (centre.r / 16.0)
<< " into a single-sample framebuffer; the value is a property of the DRAW FRAMEBUFFER, "
<< "so re-using the program must re-write it";
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteRenderbuffers(1, &ssRbo);
glDeleteFramebuffers(1, &ssFbo);
glDeleteRenderbuffers(1, &msRbo);
glDeleteFramebuffers(1, &msFbo);
glDeleteProgram(program);
gl.EndFrame();
}
} // namespace MGITest