mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-10 13:18:31 +09:00
[Fix, Test] (MG_Backend/DirectGLES, MG_State, MG_Util): publish the real default framebuffer depth/stencil format, forward the clip distance enables, and stop trusting a stale format probe for non-2D depth attachments
This commit is contained in:
@@ -61,6 +61,8 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/ClearThenReadPixelsScenario.cpp
|
||||
Scenarios/DepthStencilReadbackScenario.cpp
|
||||
Scenarios/DepthStencilReadbackMatrixScenario.cpp
|
||||
Scenarios/DepthStencilReadbackAttachmentShapeScenario.cpp
|
||||
Scenarios/ClipDistanceScenario.cpp
|
||||
Scenarios/SsboArrayLengthScenario.cpp
|
||||
Scenarios/DoublePrecisionScenario.cpp
|
||||
Scenarios/UniformInitializerScenario.cpp
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ClipDistanceScenario.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_ClipDistance ACTUALLY CLIPS, AND ONLY WHERE IT IS ENABLED.
|
||||
//
|
||||
// CapabilityInput::ClipDistance0..7 existed end to end - the GL enum converted to it, the
|
||||
// string converter named it, glEnable(GL_CLIP_DISTANCE0 + i) raised no error - and then
|
||||
// RenderState::SetCapability had no case for it and dropped it into `default: break`. Nothing
|
||||
// was stored, no version was bumped, and neither backend ever heard about it. The shader half
|
||||
// worked all along (SPIRV-Cross emits gl_ClipDistance with a
|
||||
// `#extension GL_EXT_clip_cull_distance : require` that Adreno accepts), so the distances were
|
||||
// computed and then ignored: no clipping ever happened on DirectGLES, which is the whole of
|
||||
// KHR-GLxx.clip_distance.functional. glIsEnabled lied about it too - it returned GL_FALSE
|
||||
// immediately after a successful glEnable.
|
||||
//
|
||||
// The assertions are behavioural, not query-shaped, because a query-only test passes against a
|
||||
// backend that stores the bit and never forwards it. Each case draws one full-viewport triangle
|
||||
// whose clip distance is positive on one side of the viewport and negative on the other, then
|
||||
// checks BOTH sides: the kept side proves the draw happened at all, and the clipped side is the
|
||||
// actual claim. The disabled case is the negative control - the identical shader with the
|
||||
// identical distances and the enable turned off must leave both sides painted, which is what
|
||||
// says the pixels below are being removed by clipping and not by something else.
|
||||
|
||||
#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
|
||||
|
||||
#ifndef GL_CLIP_DISTANCE0
|
||||
#define GL_CLIP_DISTANCE0 0x3000
|
||||
#endif
|
||||
#ifndef GL_CLIP_DISTANCE1
|
||||
#define GL_CLIP_DISTANCE1 0x3001
|
||||
#endif
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// One clip distance per half of the viewport: distance 0 is positive on the right half
|
||||
// (x > 0 in clip space) and distance 1 is positive on the top half. A vertex shader
|
||||
// producing a full-screen triangle from gl_VertexID, so no buffers are needed.
|
||||
const char* const kVertexSource = R"(#version 400 core
|
||||
out float gl_ClipDistance[2];
|
||||
void main() {
|
||||
vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||
vec2 p = positions[gl_VertexID];
|
||||
gl_Position = vec4(p, 0.0, 1.0);
|
||||
gl_ClipDistance[0] = p.x;
|
||||
gl_ClipDistance[1] = p.y;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kFragmentSource = R"(#version 400 core
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
class ClipDistanceScenario : public ScenarioTest {
|
||||
protected:
|
||||
GLuint BuildProgram() {
|
||||
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderSource(vs, 1, &kVertexSource, nullptr);
|
||||
glCompileShader(vs);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(vs, GL_COMPILE_STATUS, &compiled);
|
||||
if (!compiled) {
|
||||
m_buildLog = ShaderLog(vs);
|
||||
glDeleteShader(vs);
|
||||
return 0;
|
||||
}
|
||||
const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
|
||||
glShaderSource(fs, 1, &kFragmentSource, nullptr);
|
||||
glCompileShader(fs);
|
||||
glGetShaderiv(fs, GL_COMPILE_STATUS, &compiled);
|
||||
if (!compiled) {
|
||||
m_buildLog = ShaderLog(fs);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
return 0;
|
||||
}
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vs);
|
||||
glAttachShader(program, fs);
|
||||
glLinkProgram(program);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
if (!linked) {
|
||||
GLint length = 0;
|
||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
|
||||
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
m_buildLog = log.data();
|
||||
glDeleteProgram(program);
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
const std::string& BuildLog() const { return m_buildLog; }
|
||||
|
||||
// Paints the whole viewport red, then draws the clipped triangle in green.
|
||||
void DrawClippedTriangle(GLuint program, GLuint vao) const {
|
||||
glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUseProgram(program);
|
||||
glBindVertexArray(vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
}
|
||||
|
||||
static bool IsGreen(const unsigned char* px) {
|
||||
return px[0] < 64 && px[1] > 192;
|
||||
}
|
||||
|
||||
static bool IsRed(const unsigned char* px) {
|
||||
return px[0] > 192 && px[1] < 64;
|
||||
}
|
||||
|
||||
unsigned char PixelAt(int x, int y, unsigned char* out) const {
|
||||
glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, out);
|
||||
return out[0];
|
||||
}
|
||||
|
||||
// True when the driver under this backend actually implements PER-DISTANCE enable
|
||||
// state, i.e. when a written-but-disabled gl_ClipDistance leaves its fragments
|
||||
// alone. Not every stack does, and the difference is not MobileGL's to hide:
|
||||
//
|
||||
// - Adreno's ES driver honours GL_CLIP_DISTANCE0_EXT..7_EXT, which is what makes
|
||||
// KHR-GLxx.clip_distance.functional pass on the device once the enables are
|
||||
// forwarded at all.
|
||||
// - Vulkan has no such state: every clip distance a shader declares is active,
|
||||
// always. DirectVulkan therefore clips by a disabled distance.
|
||||
// - Mesa's llvmpipe ES driver behaves like Vulkan here.
|
||||
//
|
||||
// Emulating GL's semantics on those two would mean forcing the disabled slots to a
|
||||
// non-negative value inside the shader, which makes the enable mask part of the
|
||||
// pipeline key - a feature, not a fix, and deliberately not attempted here. The
|
||||
// cases that need the real semantics gate on this probe and say so when they skip,
|
||||
// rather than being deleted or silently weakened.
|
||||
bool DriverHonoursPerDistanceEnables(GLuint program, GLuint vao) const {
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
glDisable(static_cast<GLenum>(GL_CLIP_DISTANCE0 + i));
|
||||
}
|
||||
DrawClippedTriangle(program, vao);
|
||||
unsigned char negativeSide[4] = {0, 0, 0, 0};
|
||||
glReadPixels(Gl().Width() / 4, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, negativeSide);
|
||||
return IsGreen(negativeSide);
|
||||
}
|
||||
|
||||
private:
|
||||
static std::string ShaderLog(GLuint shader) {
|
||||
GLint length = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
|
||||
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
||||
return log.data();
|
||||
}
|
||||
|
||||
std::string m_buildLog;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The state itself: glEnable must be observable through glIsEnabled. This is the cheap half
|
||||
// of the bug - SetCapability's missing case made the query answer GL_FALSE for a capability
|
||||
// that had just been enabled without error.
|
||||
TEST_F(ClipDistanceScenario, EnableIsObservableThroughIsEnabled) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE0), GL_FALSE) << "GL_CLIP_DISTANCE0 must start disabled";
|
||||
glEnable(GL_CLIP_DISTANCE0);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE0), GL_TRUE)
|
||||
<< "glEnable(GL_CLIP_DISTANCE0) raised no error but glIsEnabled still reports it disabled";
|
||||
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE1), GL_FALSE)
|
||||
<< "enabling distance 0 must not enable distance 1 - the eight are independent";
|
||||
|
||||
glEnable(GL_CLIP_DISTANCE1);
|
||||
glDisable(GL_CLIP_DISTANCE0);
|
||||
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE0), GL_FALSE);
|
||||
EXPECT_EQ(glIsEnabled(GL_CLIP_DISTANCE1), GL_TRUE);
|
||||
|
||||
glDisable(GL_CLIP_DISTANCE1);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// The claim: an enabled clip distance removes the fragments where it is negative.
|
||||
TEST_F(ClipDistanceScenario, AnEnabledClipDistanceRemovesTheNegativeHalf) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(width, 8);
|
||||
ASSERT_GE(height, 8);
|
||||
|
||||
GLuint vao = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
const GLuint program = BuildProgram();
|
||||
ASSERT_NE(program, 0u) << "the gl_ClipDistance program did not build: " << BuildLog();
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_CULL_FACE);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
|
||||
glEnable(GL_CLIP_DISTANCE0);
|
||||
DrawClippedTriangle(program, vao);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
unsigned char right[4] = {0, 0, 0, 0};
|
||||
unsigned char left[4] = {0, 0, 0, 0};
|
||||
PixelAt(width - 1 - width / 4, height / 2, right);
|
||||
PixelAt(width / 4, height / 2, left);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
EXPECT_TRUE(IsGreen(right)) << "the kept half is not painted (" << int(right[0]) << "," << int(right[1])
|
||||
<< "," << int(right[2]) << ") - the draw itself did not happen, so the clipped "
|
||||
"half below proves nothing";
|
||||
EXPECT_TRUE(IsRed(left)) << "gl_ClipDistance[0] is negative on the left half and GL_CLIP_DISTANCE0 is "
|
||||
"enabled, so those fragments must be clipped away; found ("
|
||||
<< int(left[0]) << "," << int(left[1]) << "," << int(left[2]) << ")";
|
||||
|
||||
glDisable(GL_CLIP_DISTANCE0);
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glDeleteProgram(program);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// The negative control: the same shader writing the same distances, with the enable off,
|
||||
// must paint both halves. Without this a backend that clipped everything - or one whose
|
||||
// draw simply failed - would pass the case above.
|
||||
TEST_F(ClipDistanceScenario, ADisabledClipDistanceRemovesNothing) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(width, 8);
|
||||
ASSERT_GE(height, 8);
|
||||
|
||||
GLuint vao = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
const GLuint program = BuildProgram();
|
||||
ASSERT_NE(program, 0u) << "the gl_ClipDistance program did not build: " << BuildLog();
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_CULL_FACE);
|
||||
glDisable(GL_CLIP_DISTANCE0);
|
||||
glDisable(GL_CLIP_DISTANCE1);
|
||||
|
||||
DrawClippedTriangle(program, vao);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
unsigned char right[4] = {0, 0, 0, 0};
|
||||
unsigned char left[4] = {0, 0, 0, 0};
|
||||
PixelAt(width - 1 - width / 4, height / 2, right);
|
||||
PixelAt(width / 4, height / 2, left);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
EXPECT_TRUE(IsGreen(right)) << "with every clip distance disabled the whole triangle must survive";
|
||||
if (!IsGreen(left)) {
|
||||
GTEST_SKIP() << "renderer " << gl.RendererString()
|
||||
<< " clips by a DISABLED gl_ClipDistance - it does not implement per-distance enable state "
|
||||
"(see DriverHonoursPerDistanceEnables). Emulating GL's semantics there needs shader-side "
|
||||
"masking keyed on the enable mask, which is a separate feature";
|
||||
}
|
||||
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glDeleteProgram(program);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// The eight enables are independent: enabling only distance 1 must clip by distance 1 and
|
||||
// leave distance 0 alone. A backend that forwarded "any clip distance enabled" as a single
|
||||
// bit, or that always enables every declared distance (which is what Vulkan does natively),
|
||||
// passes both cases above and fails this one.
|
||||
TEST_F(ClipDistanceScenario, TheEnablesAreIndependentPerDistance) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(width, 8);
|
||||
ASSERT_GE(height, 8);
|
||||
|
||||
GLuint vao = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
const GLuint program = BuildProgram();
|
||||
ASSERT_NE(program, 0u) << "the gl_ClipDistance program did not build: " << BuildLog();
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_CULL_FACE);
|
||||
if (!DriverHonoursPerDistanceEnables(program, vao)) {
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glDeleteProgram(program);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
GTEST_SKIP() << "renderer " << gl.RendererString()
|
||||
<< " clips by every declared gl_ClipDistance regardless of the enables, so per-distance "
|
||||
"independence is not observable here";
|
||||
}
|
||||
|
||||
glDisable(GL_CLIP_DISTANCE0);
|
||||
glEnable(GL_CLIP_DISTANCE1);
|
||||
|
||||
DrawClippedTriangle(program, vao);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// Distance 1 is negative on the bottom half, distance 0 on the left half. With only
|
||||
// distance 1 enabled, the bottom-left must survive (distance 0 is off) and the bottom
|
||||
// must not.
|
||||
unsigned char topLeft[4] = {0, 0, 0, 0};
|
||||
unsigned char bottomRight[4] = {0, 0, 0, 0};
|
||||
PixelAt(width / 4, height - 1 - height / 4, topLeft);
|
||||
PixelAt(width - 1 - width / 4, height / 4, bottomRight);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
EXPECT_TRUE(IsGreen(topLeft)) << "gl_ClipDistance[0] is negative here but GL_CLIP_DISTANCE0 is disabled, so "
|
||||
"this fragment must survive";
|
||||
EXPECT_TRUE(IsRed(bottomRight)) << "gl_ClipDistance[1] is negative here and GL_CLIP_DISTANCE1 is enabled, so "
|
||||
"this fragment must be clipped";
|
||||
|
||||
glDisable(GL_CLIP_DISTANCE1);
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glDeleteProgram(program);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
+362
@@ -0,0 +1,362 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackAttachmentShapeScenario.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 - DEPTH/STENCIL READBACK WHEN THE ATTACHMENT IS NOT A PLAIN GL_TEXTURE_2D,
|
||||
// AND THE DEFAULT FRAMEBUFFER'S ADVERTISED DEPTH/STENCIL FORMAT.
|
||||
//
|
||||
// Three shipped defects, all of them invisible to a test that only ever attaches a 2D texture
|
||||
// or only ever asks the default framebuffer for a colour value.
|
||||
//
|
||||
// (1) The ES depth/stencil readback emulation identifies the source format by binding the
|
||||
// attachment's texture NAME to GL_TEXTURE_2D and asking that target for its internal
|
||||
// format. A name whose target is GL_TEXTURE_2D_ARRAY (attached by
|
||||
// glFramebufferTextureLayer) makes the bind answer GL_INVALID_OPERATION and change
|
||||
// nothing - so the query then truthfully describes whatever texture was already on
|
||||
// GL_TEXTURE_2D, which on that path is the emulation's own staging scratch. A wrong
|
||||
// answer that looks like a right one: the staging blit is issued between mismatched
|
||||
// depth formats, ES rejects it, and the read reports nothing at all.
|
||||
//
|
||||
// (2) Adreno answers GL_NONE for GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE on an attachment made
|
||||
// by glFramebufferTexture (a cube map, attached layered) while still reporting its depth
|
||||
// and stencil bits correctly. The emulation took OBJECT_TYPE as the sole witness for "is
|
||||
// there an aspect here at all" and declined the whole read.
|
||||
//
|
||||
// (3) DirectGLES never told the frontend what its default framebuffer's depth/stencil format
|
||||
// actually is, so the placeholder from MG_Impl/Init.cpp - GL_DEPTH32F_STENCIL8 - was what
|
||||
// every attachment query answered, whatever the surface really had. That is not cosmetic:
|
||||
// GL blits depth/stencil only between IDENTICAL formats, so an application that reads
|
||||
// GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, allocates the buffer it was just told about and
|
||||
// blits gets GL_INVALID_OPERATION - and a rejected glBlitFramebuffer transfers NOTHING,
|
||||
// colour bits included. DirectVulkan has published its real format since the swapchain
|
||||
// work; this is the half that was missing.
|
||||
//
|
||||
// Every case poisons its destination with a value the correct answer cannot be, so "the
|
||||
// backend wrote nothing" fails loudly instead of passing on stale memory. The plain
|
||||
// GL_TEXTURE_2D case at the end is the built-in control: it shares every line of the readback
|
||||
// path with the array and cube cases, so its passing is what says a failure above is about the
|
||||
// attachment's SHAPE and not about depth readback in general.
|
||||
//
|
||||
// The scenario name starts with DepthStencilReadback on purpose - that is the filter the
|
||||
// forced-emulation ctest registration uses (MG_IntegrationTest/CMakeLists.txt), and without
|
||||
// that registration these cases are unfalsifiable on llvmpipe, which accepts the native ES
|
||||
// depth reads that the Adreno device does not have.
|
||||
|
||||
#include <cmath>
|
||||
#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 float kDepthPoison = 0.2f;
|
||||
constexpr int kStencilPoison = 50;
|
||||
constexpr float kDepthValue = 0.75f;
|
||||
constexpr int kStencilValue = 7;
|
||||
constexpr int kSize = 16;
|
||||
|
||||
class DepthStencilReadbackAttachmentShapeScenario : public ScenarioTest {
|
||||
protected:
|
||||
float ReadDepthAt(int x, int y) const {
|
||||
float depth = kDepthPoison;
|
||||
glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth);
|
||||
return depth;
|
||||
}
|
||||
|
||||
int ReadStencilAt(int x, int y) const {
|
||||
int stencil = kStencilPoison;
|
||||
glReadPixels(x, y, 1, 1, GL_STENCIL_INDEX, GL_INT, &stencil);
|
||||
return stencil;
|
||||
}
|
||||
|
||||
// Clears the currently bound framebuffer's depth and stencil to the shared
|
||||
// reference values, with both write masks explicitly open (glClear honours them,
|
||||
// and a leftover mask from another scenario in this shared context would look
|
||||
// exactly like the bug under test).
|
||||
void ClearDepthStencil() const {
|
||||
glDepthMask(GL_TRUE);
|
||||
glStencilMask(0xFFu);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glClearDepth(kDepthValue);
|
||||
glClearStencil(kStencilValue);
|
||||
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
}
|
||||
};
|
||||
|
||||
// Fails the calling test if the framebuffer bound at both targets is not complete;
|
||||
// an incomplete framebuffer would make every read below return the poison for a
|
||||
// reason that has nothing to do with what is being tested.
|
||||
::testing::AssertionResult FramebufferIsComplete() {
|
||||
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status == GL_FRAMEBUFFER_COMPLETE) return ::testing::AssertionSuccess();
|
||||
return ::testing::AssertionFailure() << "framebuffer status 0x" << std::hex << status;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// (1) A depth slice of a 2D ARRAY texture, attached with glFramebufferTextureLayer.
|
||||
// Pre-fix this read back the poison: the format probe answered with the staging scratch's
|
||||
// GL_DEPTH24_STENCIL8 instead of the array's GL_DEPTH_COMPONENT24, and the mismatched
|
||||
// staging blit was rejected.
|
||||
TEST_F(DepthStencilReadbackAttachmentShapeScenario, DepthOfAnArrayLayerAttachmentReadsBack) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
GLuint fbo = 0;
|
||||
GLuint depthArray = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glGenTextures(1, &depthArray);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, depthArray);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_DEPTH_COMPONENT24, kSize, kSize, 4);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
// Layer 2, not layer 0: a backend that silently reads the wrong slice would still
|
||||
// agree with a single-layer texture.
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthArray, 0, 2);
|
||||
glDrawBuffer(GL_NONE);
|
||||
glReadBuffer(GL_NONE);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
ClearDepthStencil();
|
||||
|
||||
const float depth = ReadDepthAt(kSize / 2, kSize / 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
|
||||
<< "glReadPixels(GL_DEPTH_COMPONENT) of a GL_TEXTURE_2D_ARRAY layer attachment returned " << depth
|
||||
<< (std::fabs(depth - kDepthPoison) < 1e-6f ? " - the destination was never written at all" : "");
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &depthArray);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// (2) A depth cube map, attached whole with glFramebufferTexture - a LAYERED attachment.
|
||||
// Pre-fix the emulation declined outright, because the driver reports GL_NONE for that
|
||||
// attachment's OBJECT_TYPE.
|
||||
TEST_F(DepthStencilReadbackAttachmentShapeScenario, DepthOfALayeredCubeAttachmentReadsBack) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
GLuint fbo = 0;
|
||||
GLuint depthCube = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glGenTextures(1, &depthCube);
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCube);
|
||||
glTexStorage2D(GL_TEXTURE_CUBE_MAP, 1, GL_DEPTH_COMPONENT24, kSize, kSize);
|
||||
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthCube, 0);
|
||||
glDrawBuffer(GL_NONE);
|
||||
glReadBuffer(GL_NONE);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
ClearDepthStencil();
|
||||
|
||||
const float depth = ReadDepthAt(kSize / 2, kSize / 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
|
||||
<< "glReadPixels(GL_DEPTH_COMPONENT) of a layered GL_TEXTURE_CUBE_MAP attachment returned " << depth
|
||||
<< (std::fabs(depth - kDepthPoison) < 1e-6f ? " - the destination was never written at all" : "");
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &depthCube);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// Both aspects of a packed array attachment. The stencil half goes through a different
|
||||
// sampling mode than the depth half, and only the depth half was covered above.
|
||||
TEST_F(DepthStencilReadbackAttachmentShapeScenario, PackedArrayLayerAttachmentReadsBackBothAspects) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
GLuint fbo = 0;
|
||||
GLuint packedArray = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glGenTextures(1, &packedArray);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, packedArray);
|
||||
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_DEPTH24_STENCIL8, kSize, kSize, 3);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, packedArray, 0, 1);
|
||||
glDrawBuffer(GL_NONE);
|
||||
glReadBuffer(GL_NONE);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
ClearDepthStencil();
|
||||
|
||||
const float depth = ReadDepthAt(kSize / 2, kSize / 2);
|
||||
const int stencil = ReadStencilAt(kSize / 2, kSize / 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
|
||||
<< "depth of a packed GL_TEXTURE_2D_ARRAY layer attachment returned " << depth;
|
||||
EXPECT_EQ(stencil, kStencilValue)
|
||||
<< "stencil of a packed GL_TEXTURE_2D_ARRAY layer attachment returned " << stencil
|
||||
<< (stencil == kStencilPoison ? " - the destination was never written at all" : "");
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &packedArray);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// The control: the plain GL_TEXTURE_2D shape, which always worked. If this one ever fails
|
||||
// alongside the three above, the fault is in depth readback generally rather than in how
|
||||
// the attachment's format and presence are discovered.
|
||||
TEST_F(DepthStencilReadbackAttachmentShapeScenario, DepthOfAPlainTexture2DAttachmentReadsBack) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
GLuint fbo = 0;
|
||||
GLuint depthTex = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glGenTextures(1, &depthTex);
|
||||
glBindTexture(GL_TEXTURE_2D, depthTex);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH_COMPONENT24, kSize, kSize);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthTex, 0);
|
||||
glDrawBuffer(GL_NONE);
|
||||
glReadBuffer(GL_NONE);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
glViewport(0, 0, kSize, kSize);
|
||||
ClearDepthStencil();
|
||||
|
||||
const float depth = ReadDepthAt(kSize / 2, kSize / 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
|
||||
<< "the control case failed: even a plain GL_TEXTURE_2D depth attachment read back " << depth;
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteTextures(1, &depthTex);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// (3) The default framebuffer must describe its depth/stencil truthfully enough that a
|
||||
// buffer allocated from that description is blit-compatible with it. This is the exact
|
||||
// sequence KHR-GLxx.framebuffer_blit performs, and the exact reason 22 of its cases died
|
||||
// on DirectGLES: the frontend answered 32-bit float depth for a 24-bit fixed-point
|
||||
// surface, so the renderbuffer the caller allocated could never be blitted to.
|
||||
TEST_F(DepthStencilReadbackAttachmentShapeScenario, DefaultFramebufferDepthStencilFormatIsBlitCompatible) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
GLint depthBits = 0;
|
||||
GLint stencilBits = 0;
|
||||
GLint componentType = GL_UNSIGNED_NORMALIZED;
|
||||
glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH,
|
||||
GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, &depthBits);
|
||||
glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_STENCIL,
|
||||
GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, &stencilBits);
|
||||
glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_DEPTH,
|
||||
GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE, &componentType);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
if (depthBits <= 0 || stencilBits <= 0) {
|
||||
GTEST_SKIP() << "this surface has no packed depth/stencil (depth=" << depthBits
|
||||
<< " stencil=" << stencilBits << "); the blit-compatibility contract needs both";
|
||||
}
|
||||
|
||||
// The one sized format the reported description names. Getting here with the wrong
|
||||
// answer is the bug: the two candidates are not interchangeable for a blit.
|
||||
const GLenum reported = (componentType == GL_FLOAT || depthBits > 24) ? GL_DEPTH32F_STENCIL8
|
||||
: GL_DEPTH24_STENCIL8;
|
||||
|
||||
GLuint fbo = 0;
|
||||
GLuint colorRbo = 0;
|
||||
GLuint depthRbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
glGenRenderbuffers(1, &colorRbo);
|
||||
glGenRenderbuffers(1, &depthRbo);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, colorRbo);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, width, height);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, depthRbo);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, reported, width, height);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorRbo);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depthRbo);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
ASSERT_TRUE(FramebufferIsComplete());
|
||||
|
||||
// Put a known depth in the default framebuffer, then blit colour+depth+stencil out of
|
||||
// it into the buffer that its own description asked for.
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
ClearDepthStencil();
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
|
||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height,
|
||||
GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT, GL_NEAREST);
|
||||
EXPECT_EQ(FirstGLError(), 0u)
|
||||
<< "blitting depth/stencil out of the default framebuffer into a buffer allocated from the format "
|
||||
"the default framebuffer itself reported was rejected - the report and the storage disagree";
|
||||
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
|
||||
unsigned char color[4] = {0, 0, 0, 0};
|
||||
glReadPixels(width / 2, height / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, color);
|
||||
const float depth = ReadDepthAt(width / 2, height / 2);
|
||||
const int stencil = ReadStencilAt(width / 2, height / 2);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
// The colour bit is the precondition, not the claim: it says this stack can blit out of
|
||||
// its default framebuffer at all, which has nothing to do with depth/stencil formats.
|
||||
// DirectVulkan on a surfaceless pbuffer cannot - the whole call, colour included, is a
|
||||
// no-op there, while the same blit works on a real surface (KHR-GLxx.framebuffer_blit
|
||||
// exercises exactly it and Magma passes 33/33 on device). Skipping keeps the
|
||||
// depth/stencil claim below falsifiable instead of drowning it in an unrelated
|
||||
// harness limitation.
|
||||
if (int(color[1]) <= 192) {
|
||||
GTEST_SKIP() << "backend " << gl.BackendName() << " on this surface transferred no colour either (green="
|
||||
<< int(color[1])
|
||||
<< "): it cannot blit out of the default framebuffer here, so the depth/stencil half proves "
|
||||
"nothing. The GL-error assertion above still ran, and it is the format contract";
|
||||
}
|
||||
EXPECT_NEAR(depth, kDepthValue, 1.0f / 4096.0f)
|
||||
<< "depth blitted out of the default framebuffer read back " << depth
|
||||
<< (std::fabs(depth - kDepthPoison) < 1e-6f ? " - the blit transferred nothing" : "");
|
||||
EXPECT_EQ(stencil, kStencilValue) << "stencil blitted out of the default framebuffer read back " << stencil;
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
glDeleteRenderbuffers(1, &colorRbo);
|
||||
glDeleteRenderbuffers(1, &depthRbo);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
Reference in New Issue
Block a user