mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
[Test] (IntegrationTest): gl_NumSamples scenario - the value must follow the draw framebuffer, not the link
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user