mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
349 lines
17 KiB
C++
349 lines
17 KiB
C++
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DrawParametersScenario.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
|
|
//
|
|
// gl_BaseVertex / gl_BaseInstance / gl_DrawID (GL_ARB_shader_draw_parameters),
|
|
// read straight out of the shader that a draw command produced.
|
|
//
|
|
// Neither backend has these builtins for free, and each is wrong in its own way
|
|
// when nobody watches:
|
|
//
|
|
// * DirectVulkan HAS a BaseVertex builtin, but Vulkan's carries the draw's
|
|
// firstVertex on a NON-INDEXED draw where GL's is defined to be zero ("the
|
|
// value passed to the baseVertex parameter, or zero for a command with no
|
|
// such parameter"). Only the indexed meaning of the two agrees. Every
|
|
// DrawArrays form therefore takes the ZeroBaseVertex program variant.
|
|
// * DirectGLES has no such builtins at all: ESSL knows none of them, so the
|
|
// transpiler demotes each one to a uniform the draw paths feed. A uniform
|
|
// nobody writes keeps whatever the previous draw left in it - which is what
|
|
// made gl_BaseVertex report a stale base vertex, and what made
|
|
// gl_BaseInstance read an unbound storage buffer on a plain glDrawArrays.
|
|
//
|
|
// The shader paints the three values, so a draw that carries the wrong ones
|
|
// paints the wrong colour rather than merely disagreeing with an expectation
|
|
// somewhere. The framebuffer is cleared to WHITE and no case expects 255 in any
|
|
// channel, so "the draw did not happen" can never be mistaken for a pass.
|
|
|
|
#include <cstdint>
|
|
#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/glext.h>
|
|
|
|
namespace MGITest {
|
|
namespace {
|
|
|
|
// #version 450: glslang only declares the ARB builtins from 440 up.
|
|
//
|
|
// Each value is painted at 8 units per count, not 1: the errors these builtins
|
|
// actually have are OFF BY ONE (a sub-draw that never got its own gl_DrawID reads
|
|
// the previous one's, a base vertex that arrives one command late), and at one unit
|
|
// per count no readback tolerance can tell those from rounding.
|
|
//
|
|
// And biased by two counts, so that ZERO is not the clamp floor. Five of these cases
|
|
// expect zero, and an unbiased encoding would let every negative value - the shape a
|
|
// sign or rebase mistake produces - clamp to the same black and pass.
|
|
constexpr const char* kVertexSource = R"(#version 450 core
|
|
#extension GL_ARB_shader_draw_parameters : require
|
|
layout(location = 0) in vec2 aPos;
|
|
flat out vec3 vParams;
|
|
void main() {
|
|
vParams = (vec3(gl_BaseVertexARB, gl_BaseInstanceARB, gl_DrawIDARB) * 8.0 + 16.0) / 255.0;
|
|
gl_Position = vec4(aPos, 0.0, 1.0);
|
|
}
|
|
)";
|
|
|
|
constexpr const char* kFragmentSource = R"(#version 450 core
|
|
flat in vec3 vParams;
|
|
out vec4 oColor;
|
|
void main() {
|
|
oColor = vec4(vParams, 1.0);
|
|
}
|
|
)";
|
|
|
|
struct Vertex {
|
|
float x, y;
|
|
};
|
|
|
|
// 3 dummy vertices, then the left half of the viewport as two triangles,
|
|
// then the right half. Nothing here is symmetric by accident:
|
|
//
|
|
// * the padding makes a draw that ignores `first` / baseVertex paint a
|
|
// degenerate triangle (i.e. nothing) instead of the right picture;
|
|
// * the two halves let one multi-draw show TWO different gl_DrawID
|
|
// values in one readback.
|
|
//
|
|
// Indices 3..14 together cover the whole viewport, which is what the
|
|
// single-draw cases use.
|
|
constexpr int kPad = 3;
|
|
constexpr int kLeftFirst = kPad; // 3
|
|
constexpr int kRightFirst = kPad + 6; // 9
|
|
constexpr int kHalfCount = 6;
|
|
|
|
std::vector<Vertex> SceneVertices() {
|
|
std::vector<Vertex> vertices(static_cast<std::size_t>(kPad), Vertex{0.0f, 0.0f});
|
|
const float bounds[2][2] = {{-1.0f, 0.0f}, {0.0f, 1.0f}};
|
|
for (const auto& half : bounds) {
|
|
const float x0 = half[0];
|
|
const float x1 = half[1];
|
|
vertices.push_back({x0, -1.0f});
|
|
vertices.push_back({x1, -1.0f});
|
|
vertices.push_back({x1, 1.0f});
|
|
vertices.push_back({x0, -1.0f});
|
|
vertices.push_back({x1, 1.0f});
|
|
vertices.push_back({x0, 1.0f});
|
|
}
|
|
return vertices;
|
|
}
|
|
|
|
// GL's DrawArraysIndirectCommand / DrawElementsIndirectCommand, spelled out
|
|
// so a test can write one without depending on a GL header's struct.
|
|
struct ArraysCommand {
|
|
std::uint32_t count, instanceCount, first, baseInstance;
|
|
};
|
|
struct ElementsCommand {
|
|
std::uint32_t count, instanceCount, firstIndex;
|
|
std::int32_t baseVertex;
|
|
std::uint32_t baseInstance;
|
|
};
|
|
|
|
class DrawParametersScenario : public ScenarioTest {
|
|
protected:
|
|
void SetUp() override {
|
|
ScenarioTest::SetUp();
|
|
if (!Ready()) return;
|
|
std::string error;
|
|
m_program = CompileProgram(kVertexSource, kFragmentSource, &error);
|
|
ASSERT_NE(m_program, 0u) << error;
|
|
|
|
const std::vector<Vertex> vertices = SceneVertices();
|
|
glGenVertexArrays(1, &m_vao);
|
|
glBindVertexArray(m_vao);
|
|
glGenBuffers(1, &m_vbo);
|
|
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
|
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(vertices.size() * sizeof(Vertex)),
|
|
vertices.data(), GL_STATIC_DRAW);
|
|
glEnableVertexAttribArray(0);
|
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast<const void*>(0));
|
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "scene setup left a GL error behind";
|
|
}
|
|
|
|
void TearDown() override {
|
|
if (!Ready()) return;
|
|
for (GLuint* buffer : {&m_ebo, &m_indirect, &m_parameter, &m_vbo}) {
|
|
if (*buffer != 0) glDeleteBuffers(1, buffer);
|
|
*buffer = 0;
|
|
}
|
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
|
if (m_program != 0) glDeleteProgram(m_program);
|
|
}
|
|
|
|
template <typename T>
|
|
void FillBuffer(GLuint& name, GLenum target, const std::vector<T>& data) {
|
|
if (name == 0) glGenBuffers(1, &name);
|
|
glBindBuffer(target, name);
|
|
glBufferData(target, static_cast<GLsizeiptr>(data.size() * sizeof(T)), data.data(), GL_STATIC_DRAW);
|
|
}
|
|
|
|
// Clears to white, runs `draw` and reads the frame back.
|
|
template <typename DrawFn>
|
|
Image Render(DrawFn&& draw) {
|
|
BindDefaultFramebuffer();
|
|
glViewport(0, 0, HeadlessGL::Get().Width(), HeadlessGL::Get().Height());
|
|
ClearTo(1.0f, 1.0f, 1.0f, 1.0f);
|
|
glUseProgram(m_program);
|
|
glBindVertexArray(m_vao);
|
|
draw();
|
|
return ReadPixels(HeadlessGL::Get().Width(), HeadlessGL::Get().Height());
|
|
}
|
|
|
|
// The three builtins as the shader saw them, at a point in one half of
|
|
// the viewport. `half` is 0 for the left half and 1 for the right.
|
|
struct DrawParams {
|
|
int baseVertex = -1, baseInstance = -1, drawId = -1;
|
|
};
|
|
// Decodes the biased 8-units-per-count encoding back to the integer the
|
|
// shader saw. Rounding to the nearest step absorbs any UNORM slop; adjacent
|
|
// values stay eight units apart, so an off-by-one still reads as one, and a
|
|
// negative value lands below the bias and decodes negative rather than
|
|
// clamping into a legitimate zero.
|
|
static DrawParams ParamsAt(const Image& image, int half) {
|
|
const int x = image.Width() * (1 + 2 * half) / 4;
|
|
const Rgba8 pixel = image.At(x, image.Height() / 2);
|
|
const auto decode = [](std::uint8_t channel) {
|
|
return (static_cast<int>(channel) - 16 + 4) / 8;
|
|
};
|
|
return {decode(pixel.r), decode(pixel.g), decode(pixel.b)};
|
|
}
|
|
|
|
static void ExpectParams(const Image& image, int half, const DrawParams& expected,
|
|
const std::string& what) {
|
|
const DrawParams actual = ParamsAt(image, half);
|
|
EXPECT_EQ(actual.baseVertex, expected.baseVertex)
|
|
<< what << ": gl_BaseVertex (half " << half << ")";
|
|
EXPECT_EQ(actual.baseInstance, expected.baseInstance)
|
|
<< what << ": gl_BaseInstance (half " << half << ")";
|
|
EXPECT_EQ(actual.drawId, expected.drawId) << what << ": gl_DrawID (half " << half << ")";
|
|
}
|
|
|
|
GLuint m_program = 0;
|
|
GLuint m_vao = 0;
|
|
GLuint m_vbo = 0;
|
|
GLuint m_ebo = 0;
|
|
GLuint m_indirect = 0;
|
|
GLuint m_parameter = 0;
|
|
};
|
|
|
|
// ---- the non-indexed forms: gl_BaseVertex is zero, `first` or not ----
|
|
|
|
// Vulkan's BaseVertex would answer 3 here (the draw's firstVertex); GL's
|
|
// must answer 0, because glDrawArrays has no baseVertex parameter at all.
|
|
TEST_F(DrawParametersScenario, DrawArraysReportsAZeroBaseVertexDespiteItsFirst) {
|
|
if (!Ready()) return;
|
|
const Image image = Render([&] { glDrawArrays(GL_TRIANGLES, kLeftFirst, 2 * kHalfCount); });
|
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
|
ExpectParams(image, 0, {0, 0, 0}, "glDrawArrays(first=3)");
|
|
ExpectParams(image, 1, {0, 0, 0}, "glDrawArrays(first=3)");
|
|
}
|
|
|
|
TEST_F(DrawParametersScenario, DrawArraysInstancedBaseInstanceReportsItsBaseInstance) {
|
|
if (!Ready()) return;
|
|
const Image image = Render([&] {
|
|
glDrawArraysInstancedBaseInstance(GL_TRIANGLES, kLeftFirst, 2 * kHalfCount, 1, 5);
|
|
});
|
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
|
ExpectParams(image, 0, {0, 5, 0}, "glDrawArraysInstancedBaseInstance(baseInstance=5)");
|
|
}
|
|
|
|
// The base instance of one draw must not survive into the next one. This is
|
|
// the shape that broke on DirectGLES: the emulation uniform is per-program
|
|
// state, so a draw that never writes it inherits the last writer's value.
|
|
TEST_F(DrawParametersScenario, APlainDrawAfterABaseInstancedOneSeesZeroAgain) {
|
|
if (!Ready()) return;
|
|
const Image image = Render([&] {
|
|
glDrawArraysInstancedBaseInstance(GL_TRIANGLES, kLeftFirst, 2 * kHalfCount, 1, 7);
|
|
glDrawArrays(GL_TRIANGLES, kLeftFirst, 2 * kHalfCount);
|
|
});
|
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
|
ExpectParams(image, 0, {0, 0, 0}, "plain glDrawArrays after a base-instanced draw");
|
|
}
|
|
|
|
// ---- the indexed forms: gl_BaseVertex IS the base vertex ----
|
|
|
|
TEST_F(DrawParametersScenario, DrawElementsBaseVertexReportsItsBaseVertex) {
|
|
if (!Ready()) return;
|
|
std::vector<std::uint32_t> indices;
|
|
for (std::uint32_t i = 0; i < 2 * kHalfCount; ++i) indices.push_back(i);
|
|
FillBuffer(m_ebo, GL_ELEMENT_ARRAY_BUFFER, indices);
|
|
|
|
const Image image = Render([&] {
|
|
glDrawElementsBaseVertex(GL_TRIANGLES, 2 * kHalfCount, GL_UNSIGNED_INT,
|
|
reinterpret_cast<const void*>(0), kLeftFirst);
|
|
});
|
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
|
ExpectParams(image, 0, {kLeftFirst, 0, 0}, "glDrawElementsBaseVertex(basevertex=3)");
|
|
ExpectParams(image, 1, {kLeftFirst, 0, 0}, "glDrawElementsBaseVertex(basevertex=3)");
|
|
}
|
|
|
|
// ... and is zero again for the command that has none, including after one
|
|
// that did: the same leak the base instance has, on the other builtin. The
|
|
// preceding draw MUST carry a non-zero base vertex or this case proves nothing -
|
|
// one index run reaches the geometry through the base vertex, the second through
|
|
// its own indices, so the two draws paint the same picture with different
|
|
// gl_BaseVertex and only the second one's value survives in the framebuffer.
|
|
TEST_F(DrawParametersScenario, DrawElementsAfterABaseVertexDrawReportsZeroAgain) {
|
|
if (!Ready()) return;
|
|
std::vector<std::uint32_t> indices;
|
|
for (std::uint32_t i = 0; i < 2 * kHalfCount; ++i) indices.push_back(i);
|
|
for (std::uint32_t i = 0; i < 2 * kHalfCount; ++i) indices.push_back(i + kLeftFirst);
|
|
FillBuffer(m_ebo, GL_ELEMENT_ARRAY_BUFFER, indices);
|
|
const auto rebasedRun = reinterpret_cast<const void*>(2 * kHalfCount * sizeof(std::uint32_t));
|
|
|
|
const Image image = Render([&] {
|
|
glDrawElementsBaseVertex(GL_TRIANGLES, 2 * kHalfCount, GL_UNSIGNED_INT,
|
|
reinterpret_cast<const void*>(0), kLeftFirst);
|
|
glDrawElements(GL_TRIANGLES, 2 * kHalfCount, GL_UNSIGNED_INT, rebasedRun);
|
|
});
|
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
|
ExpectParams(image, 0, {0, 0, 0}, "glDrawElements after a base-vertex draw");
|
|
ExpectParams(image, 1, {0, 0, 0}, "glDrawElements after a base-vertex draw");
|
|
}
|
|
|
|
// ---- the multi-draw forms: one gl_DrawID per sub-draw ----
|
|
|
|
TEST_F(DrawParametersScenario, MultiDrawArraysNumbersItsSubDraws) {
|
|
if (!Ready()) return;
|
|
const GLint firsts[2] = {kLeftFirst, kRightFirst};
|
|
const GLsizei counts[2] = {kHalfCount, kHalfCount};
|
|
|
|
const Image image = Render([&] { glMultiDrawArrays(GL_TRIANGLES, firsts, counts, 2); });
|
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
|
ExpectParams(image, 0, {0, 0, 0}, "glMultiDrawArrays sub-draw 0");
|
|
ExpectParams(image, 1, {0, 0, 1}, "glMultiDrawArrays sub-draw 1");
|
|
}
|
|
|
|
// Every field of an indexed indirect command at once: its own gl_DrawID, the
|
|
// baseVertex word (which the CPU reads out of the command) and the
|
|
// baseInstance word (which DirectGLES reads through a storage-buffer view of
|
|
// the very same buffer).
|
|
TEST_F(DrawParametersScenario, MultiDrawElementsIndirectCarriesEveryCommandsParameters) {
|
|
if (!Ready()) return;
|
|
std::vector<std::uint32_t> indices;
|
|
for (std::uint32_t i = 0; i < kHalfCount; ++i) indices.push_back(i);
|
|
FillBuffer(m_ebo, GL_ELEMENT_ARRAY_BUFFER, indices);
|
|
|
|
const std::vector<ElementsCommand> commands = {
|
|
{kHalfCount, 1, 0, kLeftFirst, 0},
|
|
{kHalfCount, 1, 0, kRightFirst, 4},
|
|
};
|
|
FillBuffer(m_indirect, GL_DRAW_INDIRECT_BUFFER, commands);
|
|
|
|
const Image image = Render([&] {
|
|
glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, reinterpret_cast<const void*>(0), 2,
|
|
sizeof(ElementsCommand));
|
|
});
|
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
|
ExpectParams(image, 0, {kLeftFirst, 0, 0}, "indirect command 0");
|
|
ExpectParams(image, 1, {kRightFirst, 4, 1}, "indirect command 1");
|
|
}
|
|
|
|
// glMultiDrawArraysIndirectCount was missing from the DirectGLES backend
|
|
// table entirely, so the frontend answered INVALID_OPERATION for every call
|
|
// while GL_ARB_indirect_parameters was advertised. The parameter buffer here
|
|
// holds a count SMALLER than maxdrawcount, so a path that ignores it draws a
|
|
// third command over the top of the second and changes the right half.
|
|
TEST_F(DrawParametersScenario, MultiDrawArraysIndirectCountObeysItsParameterBuffer) {
|
|
if (!Ready()) return;
|
|
const std::vector<ArraysCommand> commands = {
|
|
{kHalfCount, 1, kLeftFirst, 0},
|
|
{kHalfCount, 1, kRightFirst, 6},
|
|
{kHalfCount, 1, kRightFirst, 9},
|
|
};
|
|
FillBuffer(m_indirect, GL_DRAW_INDIRECT_BUFFER, commands);
|
|
const std::vector<std::uint32_t> parameters = {2};
|
|
FillBuffer(m_parameter, GL_PARAMETER_BUFFER, parameters);
|
|
|
|
const Image image = Render([&] {
|
|
glMultiDrawArraysIndirectCount(GL_TRIANGLES, reinterpret_cast<const void*>(0), 0, 3,
|
|
sizeof(ArraysCommand));
|
|
});
|
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
|
ExpectParams(image, 0, {0, 0, 0}, "counted indirect command 0");
|
|
ExpectParams(image, 1, {0, 6, 1}, "counted indirect command 1");
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace MGITest
|