mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 12:48:32 +09:00
897 lines
45 KiB
C++
897 lines
45 KiB
C++
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TessellationXfbCaptureScenario.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 - WHAT A TESSELLATION EVALUATION STAGE OWES A TRANSFORM FEEDBACK CAPTURE.
|
|
//
|
|
// XfbRepeatedCaptureScenario already pins that a capture from a GL_PATCHES draw records
|
|
// AT ALL. Everything below is the part of the same pipeline it does not reach, and every
|
|
// case here is the reduced form of a conformance body that fails on a device:
|
|
//
|
|
// * CAPTURING THE BUILT-INS BY NAME. glTransformFeedbackVaryings("gl_Position") /
|
|
// ("gl_PointSize") on a program whose last vertex-processing stage is the evaluation
|
|
// shader. Nothing in the tree captured a built-in from a tessellation stage, and the
|
|
// two backends reach it by completely different routes - DirectGLES has to name a
|
|
// real ESSL output on the driver's own glTransformFeedbackVaryings, DirectVulkan has
|
|
// to decorate a SPIR-V built-in that lives inside gl_PerVertex.
|
|
//
|
|
// * THE PER-VERTEX PAYLOAD THE CONTROL STAGE HANDS OVER. gl_PointSize and a
|
|
// user-declared per-vertex interface block, both read back out of gl_in[] by the
|
|
// evaluation stage and only then captured. This is the shape of
|
|
// KHR-GL4x.tessellation_shader.tessellation_control_to_tessellation_evaluation.
|
|
// gl_MaxPatchVertices_Position_PointSize, which is 216 of the ~240 conformance bodies
|
|
// the family still fails: gl_Position arrives, and everything travelling beside it in
|
|
// the same patch does not.
|
|
//
|
|
// The assertions are on the captured BYTES against a CPU-computed reference, never on the
|
|
// absence of a GL error: every failure this guards against is silent.
|
|
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <utility>
|
|
#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 {
|
|
|
|
// Nothing a capture can legitimately produce, so a component that still reads it
|
|
// names the failure instead of looking like an ordinary numeric mismatch.
|
|
constexpr float kPoison = -987654.0f;
|
|
|
|
const char* const kFragmentSource = R"(#version 420 core
|
|
out vec4 fragColor;
|
|
void main()
|
|
{
|
|
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
|
}
|
|
)";
|
|
|
|
class TessellationXfbCaptureScenario : public ScenarioTest {
|
|
protected:
|
|
void SetUp() override {
|
|
ScenarioTest::SetUp();
|
|
if (!Ready()) return;
|
|
glGenVertexArrays(1, &m_vao);
|
|
glBindVertexArray(m_vao);
|
|
DrainErrors();
|
|
}
|
|
|
|
void TearDown() override {
|
|
if (!Ready()) return;
|
|
glUseProgram(0);
|
|
for (const GLuint program : m_programs) {
|
|
glDeleteProgram(program);
|
|
}
|
|
m_programs.clear();
|
|
glBindVertexArray(0);
|
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
|
m_vao = 0;
|
|
ScenarioTest::TearDown();
|
|
}
|
|
|
|
static void DrainErrors() {
|
|
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
|
}
|
|
}
|
|
|
|
static bool BackendHostsTessellation() {
|
|
GLint maxTessGenLevel = 0;
|
|
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
|
DrainErrors();
|
|
return maxTessGenLevel >= 1;
|
|
}
|
|
|
|
static GLint MaxPatchVertices() {
|
|
GLint value = 0;
|
|
glGetIntegerv(GL_MAX_PATCH_VERTICES, &value);
|
|
DrainErrors();
|
|
return value;
|
|
}
|
|
|
|
static std::string InfoLog(GLuint object, bool isShader) {
|
|
GLint length = 0;
|
|
if (isShader) {
|
|
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
|
} else {
|
|
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
|
}
|
|
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
|
if (isShader) {
|
|
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
|
|
} else {
|
|
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
|
|
}
|
|
return buffer.data();
|
|
}
|
|
|
|
GLuint BuildCaptureProgram(const std::vector<std::pair<GLenum, std::string>>& stages,
|
|
const std::vector<const char*>& varyings) {
|
|
m_buildLog.clear();
|
|
std::vector<GLuint> shaders;
|
|
bool ok = true;
|
|
for (const auto& [stage, source] : stages) {
|
|
const GLuint shader = glCreateShader(stage);
|
|
const char* text = source.c_str();
|
|
glShaderSource(shader, 1, &text, nullptr);
|
|
glCompileShader(shader);
|
|
GLint compiled = 0;
|
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
|
shaders.push_back(shader);
|
|
if (compiled == GL_FALSE) {
|
|
m_buildLog = InfoLog(shader, true) + "\n--- source ---\n" + source;
|
|
ok = false;
|
|
break;
|
|
}
|
|
}
|
|
GLuint program = 0;
|
|
if (ok) {
|
|
program = glCreateProgram();
|
|
for (const GLuint shader : shaders) {
|
|
glAttachShader(program, shader);
|
|
}
|
|
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()), varyings.data(),
|
|
GL_INTERLEAVED_ATTRIBS);
|
|
glLinkProgram(program);
|
|
GLint linked = GL_FALSE;
|
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
if (linked == GL_FALSE) {
|
|
m_buildLog = InfoLog(program, false);
|
|
glDeleteProgram(program);
|
|
program = 0;
|
|
}
|
|
}
|
|
for (const GLuint shader : shaders) {
|
|
glDeleteShader(shader);
|
|
}
|
|
if (program != 0) m_programs.push_back(program);
|
|
return program;
|
|
}
|
|
|
|
// One capture span over a single patch. Returns the capture buffer read back as
|
|
// floats; `capturedFloats` is the whole buffer, poison-filled beforehand.
|
|
std::vector<float> RunPatchCaptureSpan(GLuint program, GLenum captureMode, std::size_t capturedFloats) {
|
|
const std::vector<float> poison(capturedFloats, kPoison);
|
|
GLuint xfbBuffer = 0;
|
|
glGenBuffers(1, &xfbBuffer);
|
|
glBindBuffer(GL_ARRAY_BUFFER, xfbBuffer);
|
|
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(capturedFloats * sizeof(float)), poison.data(),
|
|
GL_STATIC_COPY);
|
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
|
|
|
glBindVertexArray(m_vao);
|
|
glUseProgram(program);
|
|
glEnable(GL_RASTERIZER_DISCARD);
|
|
glBeginTransformFeedback(captureMode);
|
|
glDrawArrays(GL_PATCHES, 0, 1);
|
|
glEndTransformFeedback();
|
|
glDisable(GL_RASTERIZER_DISCARD);
|
|
|
|
std::vector<float> readback(capturedFloats, kPoison);
|
|
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
|
static_cast<GLsizeiptr>(capturedFloats * sizeof(float)), readback.data());
|
|
glUseProgram(0);
|
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
|
glDeleteBuffers(1, &xfbBuffer);
|
|
return readback;
|
|
}
|
|
|
|
static ::testing::AssertionResult ComponentIs(const std::vector<float>& data, std::size_t index,
|
|
float expected, float epsilon = 1e-4f) {
|
|
if (index >= data.size()) {
|
|
return ::testing::AssertionFailure() << "component " << index << " is past the capture buffer";
|
|
}
|
|
const float actual = data[index];
|
|
if (actual == kPoison) {
|
|
return ::testing::AssertionFailure()
|
|
<< "component " << index << " still holds the poison value - the capture never reached "
|
|
<< "these bytes (expected " << expected << ")";
|
|
}
|
|
if (std::isnan(actual) || std::abs(actual - expected) > epsilon) {
|
|
return ::testing::AssertionFailure()
|
|
<< "component " << index << " is " << actual << ", expected " << expected;
|
|
}
|
|
return ::testing::AssertionSuccess();
|
|
}
|
|
|
|
// Defined below the shader builders it uses. `withPointSize` is the conformance
|
|
// body's own should_pass_pointsize_data axis.
|
|
void RunPerVertexPayloadCase(bool withPointSize);
|
|
|
|
// Why the gl_PointSize cases cannot be run here, or empty when they can.
|
|
//
|
|
// gl_PointSize from a tessellation stage is a real DRIVER capability on both
|
|
// targets - GL_EXT/OES_tessellation_point_size on an ES driver, the
|
|
// shaderTessellationAndGeometryPointSize feature on a Vulkan device - and desktop GL
|
|
// has no query that reports either, so this probes for it by running a program.
|
|
//
|
|
// The probe is deliberately NOT a gl_PointSize capture: it captures an ordinary user
|
|
// varying out of a tessellation evaluation stage that ALSO writes gl_PointSize, and
|
|
// compares that against the identical program without the write. A backend that
|
|
// cannot express the built-in loses the whole stage (DirectGLES fails to compile it
|
|
// and binds program 0; DirectVulkan cannot build the pipeline), so the plain varying
|
|
// comes back untouched too - which is a capability answer, not a capture answer. If
|
|
// BOTH come back untouched the probe itself is meaningless and it returns empty, so
|
|
// the cases run and FAIL rather than skipping on an unrelated breakage.
|
|
//
|
|
// Returns the reason as a string instead of skipping directly: GTEST_SKIP expands to
|
|
// a `return`, so a void helper would leave only the helper and let the case run its
|
|
// assertions anyway and report Failed instead of Skipped.
|
|
std::string WhyPointSizeCasesCannotRun();
|
|
|
|
// The geometry stage's own answer, and it has to BE its own answer: the two ESSL
|
|
// extensions are independent (Loader models them as two PointSizeTier fields fed by
|
|
// four distinct strings, and neither implies the other), so a driver with
|
|
// tessellation point size and no geometry point size passes the probe above and
|
|
// still cannot run the case below. Same two-program shape, one stage over.
|
|
//
|
|
// It also replaces a guard that could never fire: GL_MAX_GEOMETRY_OUTPUT_VERTICES is
|
|
// a hardcoded frontend constant (256) with no capability behind it, so "does this
|
|
// stack have a geometry stage at all" can only be answered by trying to build one -
|
|
// which is what this does, exactly as IoBlockNameCollisionScenario does for the same
|
|
// reason.
|
|
std::string WhyGeometryPointSizeCaseCannotRun();
|
|
|
|
std::vector<GLuint> m_programs;
|
|
std::string m_buildLog;
|
|
GLuint m_vao = 0;
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// Built-ins captured BY NAME from the evaluation stage.
|
|
// ---------------------------------------------------------------------------------
|
|
|
|
const char* const kMinimalVertexSource = R"(#version 420 core
|
|
void main()
|
|
{
|
|
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
|
}
|
|
)";
|
|
|
|
const char* const kMinimalTessControlSource = R"(#version 420 core
|
|
layout(vertices = 1) out;
|
|
void main()
|
|
{
|
|
gl_out[gl_InvocationID].gl_Position = gl_in[0].gl_Position;
|
|
gl_TessLevelOuter[0] = 1.0;
|
|
gl_TessLevelOuter[1] = 1.0;
|
|
gl_TessLevelOuter[2] = 1.0;
|
|
gl_TessLevelInner[0] = 1.0;
|
|
}
|
|
)";
|
|
|
|
// Values no stale buffer would hold by accident. The two sources differ ONLY by
|
|
// gl_PointSize, so the pair isolates it: on a backend that lowers to ESSL the
|
|
// built-in is not even declared in a tessellation stage without
|
|
// GL_EXT_tessellation_point_size, and the whole shader then fails to compile.
|
|
const char* const kPositionTessEvalSource = R"(#version 420 core
|
|
layout(triangles, equal_spacing, cw, point_mode) in;
|
|
void main()
|
|
{
|
|
gl_Position = vec4(11.0, 12.0, 13.0, 14.0);
|
|
}
|
|
)";
|
|
|
|
const char* const kPositionAndPointSizeTessEvalSource = R"(#version 420 core
|
|
layout(triangles, equal_spacing, cw, point_mode) in;
|
|
void main()
|
|
{
|
|
gl_Position = vec4(11.0, 12.0, 13.0, 14.0);
|
|
gl_PointSize = 5.0;
|
|
}
|
|
)";
|
|
|
|
// The two probe programs. They differ by one statement; both capture `probe_value`,
|
|
// which has nothing to do with point size.
|
|
const char* const kPointSizeProbeTessEvalSource = R"(#version 420 core
|
|
layout(triangles, equal_spacing, cw, point_mode) in;
|
|
out float probe_value;
|
|
void main()
|
|
{
|
|
probe_value = 42.0;
|
|
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
|
gl_PointSize = 3.0;
|
|
}
|
|
)";
|
|
|
|
const char* const kPointSizeFreeProbeTessEvalSource = R"(#version 420 core
|
|
layout(triangles, equal_spacing, cw, point_mode) in;
|
|
out float probe_value;
|
|
void main()
|
|
{
|
|
probe_value = 42.0;
|
|
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
|
}
|
|
)";
|
|
|
|
std::string TessellationXfbCaptureScenario::WhyPointSizeCasesCannotRun() {
|
|
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
|
DrainErrors();
|
|
|
|
const auto probeCaptures = [&](const char* tessEvalSource) {
|
|
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
|
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
|
|
{GL_TESS_EVALUATION_SHADER, tessEvalSource},
|
|
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
|
{"probe_value"});
|
|
if (program == 0) return false;
|
|
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, 3);
|
|
DrainErrors();
|
|
return captured[0] == 42.0f;
|
|
};
|
|
|
|
const bool withPointSize = probeCaptures(kPointSizeProbeTessEvalSource);
|
|
if (withPointSize) return {};
|
|
if (!probeCaptures(kPointSizeFreeProbeTessEvalSource)) {
|
|
// The control failed too, so nothing here is about point size.
|
|
return {};
|
|
}
|
|
return "this backend cannot express gl_PointSize in a tessellation stage at all - the same "
|
|
"program captures an ordinary varying with the gl_PointSize write removed and captures "
|
|
"nothing with it present (an ES driver without GL_EXT/OES_tessellation_point_size, or a "
|
|
"Vulkan device without shaderTessellationAndGeometryPointSize)";
|
|
}
|
|
|
|
TEST_F(TessellationXfbCaptureScenario, CapturesGlPositionByNameFromTheEvaluationStage) {
|
|
if (!Ready()) GTEST_SKIP();
|
|
if (!BackendHostsTessellation()) {
|
|
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
|
<< ")";
|
|
}
|
|
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
|
DrainErrors();
|
|
|
|
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
|
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
|
|
{GL_TESS_EVALUATION_SHADER, kPositionTessEvalSource},
|
|
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
|
{"gl_Position"});
|
|
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
|
|
|
// point_mode with every level at 1 emits three points, all carrying the same
|
|
// constant; only the first record has to be right for the mechanism to be proven.
|
|
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, 4 * 3);
|
|
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f));
|
|
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f));
|
|
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f));
|
|
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f));
|
|
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
|
}
|
|
|
|
TEST_F(TessellationXfbCaptureScenario, CapturesGlPositionAndGlPointSizeByNameFromTheEvaluationStage) {
|
|
if (!Ready()) GTEST_SKIP();
|
|
if (!BackendHostsTessellation()) {
|
|
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
|
<< ")";
|
|
}
|
|
if (const std::string reason = WhyPointSizeCasesCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
|
|
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
|
DrainErrors();
|
|
|
|
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
|
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
|
|
{GL_TESS_EVALUATION_SHADER, kPositionAndPointSizeTessEvalSource},
|
|
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
|
{"gl_Position", "gl_PointSize"});
|
|
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
|
|
|
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, 5 * 3);
|
|
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f));
|
|
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f));
|
|
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f));
|
|
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f));
|
|
EXPECT_TRUE(ComponentIs(captured, 4, 5.0f));
|
|
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// The per-vertex payload the control stage hands to the evaluation stage.
|
|
// ---------------------------------------------------------------------------------
|
|
|
|
// The conformance body's own shapes, reduced to one patch and parameterised by the
|
|
// output patch size so the caller can run the real GL_MAX_PATCH_VERTICES. The
|
|
// `withPointSize` axis is the conformance body's own `should_pass_pointsize_data`,
|
|
// which it varies together with point_mode - and which decides whether the whole
|
|
// program even involves the per-vertex built-in that ESSL gates behind an extension.
|
|
std::string PayloadVertexSource(bool withPointSize) {
|
|
return R"(#version 420 core
|
|
out gl_PerVertex {
|
|
vec4 gl_Position;
|
|
)" + std::string(withPointSize ? " float gl_PointSize;\n" : "") +
|
|
R"(};
|
|
void main()
|
|
{
|
|
}
|
|
)";
|
|
}
|
|
|
|
std::string PayloadTessControlSource(int outputVertices, bool withPointSize) {
|
|
const std::string perVertexTail = withPointSize ? " float gl_PointSize;\n" : "";
|
|
return R"(#version 420 core
|
|
layout(vertices = )" + std::to_string(outputVertices) +
|
|
R"() out;
|
|
in gl_PerVertex {
|
|
vec4 gl_Position;
|
|
)" + perVertexTail +
|
|
R"(} gl_in[gl_MaxPatchVertices];
|
|
out gl_PerVertex {
|
|
vec4 gl_Position;
|
|
)" + perVertexTail +
|
|
R"(} gl_out[];
|
|
out OUT_TC
|
|
{
|
|
vec2 value1;
|
|
ivec4 value2;
|
|
} result[];
|
|
void main()
|
|
{
|
|
)" + std::string(withPointSize
|
|
? " gl_out[gl_InvocationID].gl_PointSize = 1.0 / float(gl_InvocationID + 1);\n"
|
|
: "") +
|
|
R"( gl_out[gl_InvocationID].gl_Position = vec4(float(gl_InvocationID * 4 + 0), float(gl_InvocationID * 4 + 1),
|
|
float(gl_InvocationID * 4 + 2), float(gl_InvocationID * 4 + 3));
|
|
result[gl_InvocationID].value1 = vec2(1.0 / float(gl_InvocationID + 1), 1.0 / float(gl_InvocationID + 2));
|
|
result[gl_InvocationID].value2 = ivec4(gl_InvocationID + 1, gl_InvocationID + 2,
|
|
gl_InvocationID + 3, gl_InvocationID + 4);
|
|
gl_TessLevelInner[0] = 1.0;
|
|
gl_TessLevelInner[1] = 1.0;
|
|
gl_TessLevelOuter[0] = 1.0;
|
|
gl_TessLevelOuter[1] = 1.0;
|
|
gl_TessLevelOuter[2] = 1.0;
|
|
gl_TessLevelOuter[3] = 1.0;
|
|
}
|
|
)";
|
|
}
|
|
|
|
// Deliberately NEVER writes gl_Position, exactly as the conformance shader does not:
|
|
// the redeclared block is there so the evaluation stage can READ gl_in[], and an
|
|
// output nothing stores is what UnwrittenPositionOutputScenario pins separately.
|
|
std::string PayloadTessEvalSource(int inputVertices, bool withPointSize) {
|
|
const std::string perVertexTail = withPointSize ? " float gl_PointSize;\n" : "";
|
|
return R"(#version 420 core
|
|
layout(isolines, equal_spacing, ccw, point_mode) in;
|
|
in gl_PerVertex {
|
|
vec4 gl_Position;
|
|
)" + perVertexTail +
|
|
R"(} gl_in[gl_MaxPatchVertices];
|
|
out gl_PerVertex {
|
|
vec4 gl_Position;
|
|
)" + perVertexTail +
|
|
R"(};
|
|
in OUT_TC
|
|
{
|
|
vec2 value1;
|
|
ivec4 value2;
|
|
} tc_data[];
|
|
|
|
)" + std::string(withPointSize ? "out float te_pointsize;\n" : "") +
|
|
R"(out vec4 te_position;
|
|
out vec2 te_value1;
|
|
out flat ivec4 te_value2;
|
|
|
|
void main()
|
|
{
|
|
)" + std::string(withPointSize ? " te_pointsize = 0.0;\n" : "") +
|
|
R"( te_position = vec4 (0.0);
|
|
te_value1 = vec2 (0.0);
|
|
te_value2 = ivec4(0);
|
|
|
|
for (int n = 0; n < )" + std::to_string(inputVertices) +
|
|
R"(; ++n)
|
|
{
|
|
)" + std::string(withPointSize ? " te_pointsize += gl_in [n].gl_PointSize;\n" : "") +
|
|
R"( te_position += gl_in [n].gl_Position;
|
|
te_value1 += tc_data[n].value1;
|
|
te_value2 += tc_data[n].value2;
|
|
}
|
|
}
|
|
)";
|
|
}
|
|
|
|
// The reduced conformance body. `withPointSize` selects between its two halves;
|
|
// everything else - one input vertex, an output patch of GL_MAX_PATCH_VERTICES, a
|
|
// user per-vertex block travelling beside gl_PerVertex, the capture taken off the
|
|
// evaluation stage - is the same on both.
|
|
void TessellationXfbCaptureScenario::RunPerVertexPayloadCase(bool withPointSize) {
|
|
if (!Ready()) GTEST_SKIP();
|
|
if (!BackendHostsTessellation()) {
|
|
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
|
<< ")";
|
|
}
|
|
if (withPointSize) {
|
|
if (const std::string reason = WhyPointSizeCasesCannotRun(); !reason.empty()) GTEST_SKIP() << reason;
|
|
}
|
|
const GLint patchVertices = MaxPatchVertices();
|
|
ASSERT_GE(patchVertices, 32) << "GL_MAX_PATCH_VERTICES is below the guaranteed minimum";
|
|
|
|
// One input vertex per patch, an output patch of GL_MAX_PATCH_VERTICES vertices:
|
|
// the control stage runs that many invocations and every one of them contributes.
|
|
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
|
DrainErrors();
|
|
|
|
std::vector<const char*> varyings = {"te_position", "te_value1", "te_value2"};
|
|
if (withPointSize) varyings.push_back("te_pointsize");
|
|
|
|
const GLuint program =
|
|
BuildCaptureProgram({{GL_VERTEX_SHADER, PayloadVertexSource(withPointSize)},
|
|
{GL_TESS_CONTROL_SHADER, PayloadTessControlSource(patchVertices, withPointSize)},
|
|
{GL_TESS_EVALUATION_SHADER, PayloadTessEvalSource(patchVertices, withPointSize)},
|
|
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
|
varyings);
|
|
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
|
|
|
float referencePointSize = 0.0f;
|
|
float referencePosition[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
|
float referenceValue1[2] = {0.0f, 0.0f};
|
|
int referenceValue2[4] = {0, 0, 0, 0};
|
|
for (int n = 0; n < patchVertices; ++n) {
|
|
referencePointSize += 1.0f / static_cast<float>(n + 1);
|
|
for (int c = 0; c < 4; ++c) {
|
|
referencePosition[c] += static_cast<float>(n * 4 + c);
|
|
referenceValue2[c] += n + 1 + c;
|
|
}
|
|
referenceValue1[0] += 1.0f / static_cast<float>(n + 1);
|
|
referenceValue1[1] += 1.0f / static_cast<float>(n + 2);
|
|
}
|
|
|
|
// isolines with every level at 1 emits two points; the record stride is
|
|
// vec4 + vec2 + ivec4 [+ float] components.
|
|
const std::size_t stride = withPointSize ? 11 : 10;
|
|
const std::vector<float> captured = RunPatchCaptureSpan(program, GL_POINTS, stride * 4);
|
|
for (int c = 0; c < 4; ++c) {
|
|
EXPECT_TRUE(ComponentIs(captured, static_cast<std::size_t>(c), referencePosition[c], 1e-2f))
|
|
<< "te_position." << c << " (gl_in[].gl_Position)";
|
|
}
|
|
for (int c = 0; c < 2; ++c) {
|
|
EXPECT_TRUE(ComponentIs(captured, static_cast<std::size_t>(4 + c), referenceValue1[c], 1e-3f))
|
|
<< "te_value1." << c << " (the user per-vertex block the control stage wrote)";
|
|
}
|
|
for (int c = 0; c < 4; ++c) {
|
|
const std::size_t index = static_cast<std::size_t>(6 + c);
|
|
ASSERT_LT(index, captured.size());
|
|
int actual = 0;
|
|
std::memcpy(&actual, &captured[index], sizeof(actual));
|
|
EXPECT_EQ(actual, referenceValue2[c])
|
|
<< "te_value2." << c << " (the user per-vertex block's integer member)";
|
|
}
|
|
if (withPointSize) {
|
|
EXPECT_TRUE(ComponentIs(captured, 10, referencePointSize, 1e-3f))
|
|
<< "te_pointsize (gl_in[].gl_PointSize)";
|
|
}
|
|
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
|
}
|
|
|
|
TEST_F(TessellationXfbCaptureScenario, TheEvaluationStageSeesTheUserPerVertexBlockOfItsPatch) {
|
|
RunPerVertexPayloadCase(false);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// The same built-in, one stage over.
|
|
// ---------------------------------------------------------------------------------
|
|
|
|
// ESSL gates gl_PointSize behind a per-stage extension in BOTH non-vertex
|
|
// vertex-processing stages - EXT/OES_tessellation_point_size for the two tessellation
|
|
// stages, EXT/OES_geometry_point_size for the geometry one - and they are separate
|
|
// extensions that do not imply each other, so the geometry arm is a second code path
|
|
// rather than the same one. Nothing else in the tree writes gl_PointSize from a geometry
|
|
// shader, so without this case the arm ships untested.
|
|
const char* const kPointSizeGeometrySource = R"(#version 420 core
|
|
layout(points) in;
|
|
layout(points, max_vertices = 1) out;
|
|
out float gs_value;
|
|
void main()
|
|
{
|
|
gs_value = 7.0;
|
|
gl_Position = gl_in[0].gl_Position;
|
|
gl_PointSize = 4.0;
|
|
EmitVertex();
|
|
}
|
|
)";
|
|
|
|
// The control: identical but for the gl_PointSize write, so the pair answers "can this
|
|
// stack host a geometry stage that names the built-in" without asking anything about
|
|
// capture.
|
|
const char* const kPointSizeFreeGeometrySource = R"(#version 420 core
|
|
layout(points) in;
|
|
layout(points, max_vertices = 1) out;
|
|
out float gs_value;
|
|
void main()
|
|
{
|
|
gs_value = 7.0;
|
|
gl_Position = gl_in[0].gl_Position;
|
|
EmitVertex();
|
|
}
|
|
)";
|
|
|
|
std::string TessellationXfbCaptureScenario::WhyGeometryPointSizeCaseCannotRun() {
|
|
const auto probeCaptures = [&](const char* geometrySource) {
|
|
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
|
{GL_GEOMETRY_SHADER, geometrySource},
|
|
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
|
{"gs_value"});
|
|
if (program == 0) return false;
|
|
const std::vector<float> poison(1, kPoison);
|
|
GLuint xfbBuffer = 0;
|
|
glGenBuffers(1, &xfbBuffer);
|
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
|
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(sizeof(float)), poison.data(),
|
|
GL_STATIC_DRAW);
|
|
glBindVertexArray(m_vao);
|
|
glUseProgram(program);
|
|
glEnable(GL_RASTERIZER_DISCARD);
|
|
glBeginTransformFeedback(GL_POINTS);
|
|
glDrawArrays(GL_POINTS, 0, 1);
|
|
glEndTransformFeedback();
|
|
glDisable(GL_RASTERIZER_DISCARD);
|
|
float captured = kPoison;
|
|
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, static_cast<GLsizeiptr>(sizeof(float)),
|
|
&captured);
|
|
glUseProgram(0);
|
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
|
glDeleteBuffers(1, &xfbBuffer);
|
|
DrainErrors();
|
|
return captured == 7.0f;
|
|
};
|
|
|
|
if (probeCaptures(kPointSizeGeometrySource)) return {};
|
|
if (!probeCaptures(kPointSizeFreeGeometrySource)) {
|
|
// The control failed too, so this stack cannot run a capturing geometry stage at
|
|
// all - which is not what this case is about, and is the question the dead
|
|
// GL_MAX_GEOMETRY_OUTPUT_VERTICES guard was trying to ask. Skipping rather than
|
|
// failing loses nothing: XfbRepeatedCaptureScenario pins plain geometry capture
|
|
// and goes red on its own if that is what actually broke.
|
|
return "this backend cannot capture from a geometry stage at all, with or without gl_PointSize";
|
|
}
|
|
return "this backend cannot express gl_PointSize in a geometry stage - the same program captures an "
|
|
"ordinary varying with the gl_PointSize write removed and captures nothing with it present "
|
|
"(an ES driver without GL_EXT/OES_geometry_point_size, which is a SEPARATE extension from the "
|
|
"tessellation one, or a Vulkan device without shaderTessellationAndGeometryPointSize)";
|
|
}
|
|
|
|
TEST_F(TessellationXfbCaptureScenario, CapturesGlPointSizeByNameFromTheGeometryStage) {
|
|
if (!Ready()) GTEST_SKIP();
|
|
if (const std::string reason = WhyGeometryPointSizeCaseCannotRun(); !reason.empty()) {
|
|
GTEST_SKIP() << reason << " (" << Gl().BackendName() << ", " << Gl().RendererString() << ")";
|
|
}
|
|
|
|
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
|
{GL_GEOMETRY_SHADER, kPointSizeGeometrySource},
|
|
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
|
{"gs_value", "gl_PointSize"});
|
|
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
|
|
|
GLuint xfbBuffer = 0;
|
|
glGenBuffers(1, &xfbBuffer);
|
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
|
const std::vector<float> poison(2, kPoison);
|
|
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(poison.size() * sizeof(float)),
|
|
poison.data(), GL_STATIC_DRAW);
|
|
|
|
glBindVertexArray(m_vao);
|
|
glUseProgram(program);
|
|
glEnable(GL_RASTERIZER_DISCARD);
|
|
glBeginTransformFeedback(GL_POINTS);
|
|
glDrawArrays(GL_POINTS, 0, 1);
|
|
glEndTransformFeedback();
|
|
glDisable(GL_RASTERIZER_DISCARD);
|
|
|
|
std::vector<float> captured(2, kPoison);
|
|
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
|
static_cast<GLsizeiptr>(captured.size() * sizeof(float)), captured.data());
|
|
EXPECT_TRUE(ComponentIs(captured, 0, 7.0f)) << "gs_value - an ordinary varying, which is lost too when "
|
|
"the stage carrying it fails to compile";
|
|
EXPECT_TRUE(ComponentIs(captured, 1, 4.0f)) << "gl_PointSize";
|
|
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
|
|
|
glUseProgram(0);
|
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
|
glDeleteBuffers(1, &xfbBuffer);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// The conformance body's own READBACK, which is not glGetBufferSubData.
|
|
// ---------------------------------------------------------------------------------
|
|
|
|
// Every case above reads the capture back with glGetBufferSubData because that is the
|
|
// shortest path to the bytes. The conformance bodies do something else: they respecify
|
|
// the buffer through the GENERIC GL_TRANSFORM_FEEDBACK_BUFFER binding with glBufferData
|
|
// while it is simultaneously bound to indexed capture point 0, and then read it with
|
|
// glMapBufferRange / glUnmapBuffer - twice, once per iteration of the same case, with no
|
|
// fresh buffer in between. On a device the tessellation bodies stop at exactly that map
|
|
// call, so the sequence itself is worth pinning: none of the map path's error conditions
|
|
// may fire, and the mapped bytes must be the captured ones.
|
|
TEST_F(TessellationXfbCaptureScenario, MapsTheCaptureBufferAfterEachOfTwoPatchDraws) {
|
|
if (!Ready()) GTEST_SKIP();
|
|
if (!BackendHostsTessellation()) {
|
|
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
|
<< ")";
|
|
}
|
|
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
|
DrainErrors();
|
|
|
|
const GLuint program = BuildCaptureProgram({{GL_VERTEX_SHADER, kMinimalVertexSource},
|
|
{GL_TESS_CONTROL_SHADER, kMinimalTessControlSource},
|
|
{GL_TESS_EVALUATION_SHADER, kPositionTessEvalSource},
|
|
{GL_FRAGMENT_SHADER, kFragmentSource}},
|
|
{"gl_Position"});
|
|
ASSERT_NE(program, 0u) << "program failed to build: " << m_buildLog;
|
|
|
|
GLuint xfbBuffer = 0;
|
|
glGenBuffers(1, &xfbBuffer);
|
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
|
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "binding the capture point";
|
|
|
|
constexpr std::size_t kFloats = 4 * 3;
|
|
constexpr GLsizeiptr kBytes = static_cast<GLsizeiptr>(kFloats * sizeof(float));
|
|
for (int iteration = 0; iteration < 2; ++iteration) {
|
|
// Respecified through the generic binding, exactly as the conformance body does,
|
|
// while the same buffer is still bound to capture point 0.
|
|
const std::vector<float> poison(kFloats, kPoison);
|
|
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, kBytes, poison.data(), GL_STATIC_DRAW);
|
|
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glBufferData, iteration " << iteration;
|
|
|
|
glBindVertexArray(m_vao);
|
|
glUseProgram(program);
|
|
glEnable(GL_RASTERIZER_DISCARD);
|
|
glBeginTransformFeedback(GL_POINTS);
|
|
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glBeginTransformFeedback, iteration " << iteration;
|
|
glDrawArrays(GL_PATCHES, 0, 1);
|
|
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glDrawArrays, iteration " << iteration;
|
|
glEndTransformFeedback();
|
|
glDisable(GL_RASTERIZER_DISCARD);
|
|
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glEndTransformFeedback, iteration " << iteration;
|
|
|
|
const auto* mapped =
|
|
static_cast<const float*>(glMapBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, kBytes,
|
|
GL_MAP_READ_BIT));
|
|
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glMapBufferRange, iteration " << iteration;
|
|
ASSERT_NE(mapped, nullptr) << "iteration " << iteration;
|
|
const std::vector<float> captured(mapped, mapped + kFloats);
|
|
EXPECT_EQ(glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER), GL_TRUE) << "iteration " << iteration;
|
|
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "glUnmapBuffer, iteration " << iteration;
|
|
|
|
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f)) << "iteration " << iteration;
|
|
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f)) << "iteration " << iteration;
|
|
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f)) << "iteration " << iteration;
|
|
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f)) << "iteration " << iteration;
|
|
glUseProgram(0);
|
|
}
|
|
|
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
|
glDeleteBuffers(1, &xfbBuffer);
|
|
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
|
}
|
|
|
|
// The same patch with gl_PointSize travelling in gl_PerVertex beside gl_Position.
|
|
// In ESSL gl_PointSize does not EXIST in a tessellation stage unless
|
|
// GL_EXT_tessellation_point_size is requested, so a backend that lowers to ESSL
|
|
// without asking for it does not merely lose the value - the stage fails to compile
|
|
// and the whole program is replaced by program 0.
|
|
TEST_F(TessellationXfbCaptureScenario, TheEvaluationStageSeesGlPointSizeAcrossItsPatch) {
|
|
RunPerVertexPayloadCase(true);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// The same capture through a PROGRAM PIPELINE OBJECT.
|
|
// ---------------------------------------------------------------------------------
|
|
|
|
// The conformance body runs each of its configurations twice: once with a monolithic
|
|
// program object and once with a pipeline of four separable programs, the capture
|
|
// declared on the separable EVALUATION program. That second shape goes through the
|
|
// hidden composite the pipeline object builds for the draw, and it is the only place a
|
|
// tessellation capture and the composite meet - so the capture list has to survive being
|
|
// taken from a program that is not the one bound.
|
|
TEST_F(TessellationXfbCaptureScenario, CapturesFromASeparableEvaluationProgramInAPipelineObject) {
|
|
if (!Ready()) GTEST_SKIP();
|
|
if (!BackendHostsTessellation()) {
|
|
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
|
<< ")";
|
|
}
|
|
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
|
DrainErrors();
|
|
|
|
// One separable program per stage. Only the evaluation program carries the capture
|
|
// list, because it is the one whose outputs are captured.
|
|
const auto buildSeparable = [&](GLenum stage, const char* source,
|
|
const std::vector<const char*>& varyings) -> GLuint {
|
|
const GLuint shader = glCreateShader(stage);
|
|
glShaderSource(shader, 1, &source, nullptr);
|
|
glCompileShader(shader);
|
|
GLint compiled = 0;
|
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
|
if (compiled == GL_FALSE) {
|
|
m_buildLog = InfoLog(shader, true);
|
|
glDeleteShader(shader);
|
|
return 0;
|
|
}
|
|
const GLuint program = glCreateProgram();
|
|
glProgramParameteri(program, GL_PROGRAM_SEPARABLE, GL_TRUE);
|
|
glAttachShader(program, shader);
|
|
if (!varyings.empty()) {
|
|
glTransformFeedbackVaryings(program, static_cast<GLsizei>(varyings.size()), varyings.data(),
|
|
GL_INTERLEAVED_ATTRIBS);
|
|
}
|
|
glLinkProgram(program);
|
|
GLint linked = GL_FALSE;
|
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
glDeleteShader(shader);
|
|
if (linked == GL_FALSE) {
|
|
m_buildLog = InfoLog(program, false);
|
|
glDeleteProgram(program);
|
|
return 0;
|
|
}
|
|
m_programs.push_back(program);
|
|
return program;
|
|
};
|
|
|
|
m_buildLog.clear();
|
|
const GLuint vertexProgram = buildSeparable(GL_VERTEX_SHADER, kMinimalVertexSource, {});
|
|
ASSERT_NE(vertexProgram, 0u) << "separable vertex program: " << m_buildLog;
|
|
const GLuint controlProgram = buildSeparable(GL_TESS_CONTROL_SHADER, kMinimalTessControlSource, {});
|
|
ASSERT_NE(controlProgram, 0u) << "separable control program: " << m_buildLog;
|
|
const GLuint evalProgram =
|
|
buildSeparable(GL_TESS_EVALUATION_SHADER, kPositionTessEvalSource, {"gl_Position"});
|
|
ASSERT_NE(evalProgram, 0u) << "separable evaluation program: " << m_buildLog;
|
|
const GLuint fragmentProgram = buildSeparable(GL_FRAGMENT_SHADER, kFragmentSource, {});
|
|
ASSERT_NE(fragmentProgram, 0u) << "separable fragment program: " << m_buildLog;
|
|
|
|
GLuint pipeline = 0;
|
|
glGenProgramPipelines(1, &pipeline);
|
|
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vertexProgram);
|
|
glUseProgramStages(pipeline, GL_TESS_CONTROL_SHADER_BIT, controlProgram);
|
|
glUseProgramStages(pipeline, GL_TESS_EVALUATION_SHADER_BIT, evalProgram);
|
|
glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fragmentProgram);
|
|
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "assembling the pipeline object";
|
|
|
|
constexpr std::size_t kFloats = 4 * 3;
|
|
const std::vector<float> poison(kFloats, kPoison);
|
|
GLuint xfbBuffer = 0;
|
|
glGenBuffers(1, &xfbBuffer);
|
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
|
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(kFloats * sizeof(float)),
|
|
poison.data(), GL_STATIC_DRAW);
|
|
|
|
glBindVertexArray(m_vao);
|
|
glUseProgram(0);
|
|
glBindProgramPipeline(pipeline);
|
|
glEnable(GL_RASTERIZER_DISCARD);
|
|
glBeginTransformFeedback(GL_POINTS);
|
|
EXPECT_EQ(glGetError(), GL_NO_ERROR) << "glBeginTransformFeedback on a pipeline object";
|
|
glDrawArrays(GL_PATCHES, 0, 1);
|
|
glEndTransformFeedback();
|
|
glDisable(GL_RASTERIZER_DISCARD);
|
|
|
|
std::vector<float> captured(kFloats, kPoison);
|
|
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
|
static_cast<GLsizeiptr>(kFloats * sizeof(float)), captured.data());
|
|
EXPECT_TRUE(ComponentIs(captured, 0, 11.0f));
|
|
EXPECT_TRUE(ComponentIs(captured, 1, 12.0f));
|
|
EXPECT_TRUE(ComponentIs(captured, 2, 13.0f));
|
|
EXPECT_TRUE(ComponentIs(captured, 3, 14.0f));
|
|
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
|
|
|
glBindProgramPipeline(0);
|
|
glDeleteProgramPipelines(1, &pipeline);
|
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
|
glDeleteBuffers(1, &xfbBuffer);
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace MGITest
|