mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
725 lines
34 KiB
C++
725 lines
34 KiB
C++
// MobileGL - MobileGL/MG_Test/Program/ProgramPipelineCompositeTest.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
|
|
|
|
// The hidden composite program a pipeline draw goes through (MG_State/GLState/Core.cpp,
|
|
// GetProgramForDraw), interrogated directly rather than through pixels.
|
|
//
|
|
// Two properties live here that the integration scenarios cannot see, because both are about
|
|
// the composite as an OBJECT rather than about what it paints:
|
|
//
|
|
// 1. WHICH stage's uniform value ends up in its single slot when several stages declare the
|
|
// same name. The rendering cases pin the answer for the shapes an application actually
|
|
// writes; these pin the rule itself, including the tie.
|
|
// 2. WHETHER it is the same object from one draw to the next. A composite rebuild is a full
|
|
// synchronous Link() plus a new program identity that empties both backends' per-program
|
|
// registries, and nothing about the resulting IMAGE would change if it happened on every
|
|
// draw - so an assertion on pixels can never catch that regression.
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "Config.h"
|
|
#include "Includes.h"
|
|
#include "Init.h"
|
|
#include "MG_Impl/GLImpl/Getter/GL_Getter.h"
|
|
#include "MG_Impl/GLImpl/Program/GL_Program.h"
|
|
#include "MG_Impl/GLImpl/Drawing/GL_Drawing.h"
|
|
#include "MG_Impl/GLImpl/Program/GL_ProgramPipeline.h"
|
|
#include "MG_State/GLState/Core.h"
|
|
|
|
using namespace MobileGL;
|
|
using namespace MobileGL::MG_Impl::GLImpl;
|
|
|
|
namespace {
|
|
|
|
// Both stages declare `u_shared`, which is the shared-header idiom (one header included by
|
|
// every stage) and the shape that used to render nothing: the fragment stage's untouched
|
|
// zero default overwrote the vertex stage's written value on the way into the composite.
|
|
const char* kSharedUniformVs = R"(#version 430 core
|
|
out gl_PerVertex { vec4 gl_Position; };
|
|
uniform vec4 u_shared;
|
|
uniform vec4 u_vsOnly;
|
|
void main() { gl_Position = u_shared + u_vsOnly; }
|
|
)";
|
|
|
|
const char* kSharedUniformFs = R"(#version 430 core
|
|
uniform vec4 u_shared;
|
|
out vec4 o_color;
|
|
void main() { o_color = u_shared; }
|
|
)";
|
|
|
|
const char* kArrayUniformVs = R"(#version 430 core
|
|
out gl_PerVertex { vec4 gl_Position; };
|
|
uniform vec4 u_arr[4];
|
|
void main() { gl_Position = u_arr[0] + u_arr[1] + u_arr[2] + u_arr[3]; }
|
|
)";
|
|
|
|
const char* kArrayUniformFs = R"(#version 430 core
|
|
uniform vec4 u_arr[4];
|
|
out vec4 o_color;
|
|
void main() { o_color = u_arr[0] + u_arr[1] + u_arr[2] + u_arr[3]; }
|
|
)";
|
|
|
|
const char* kSamplerVs = R"(#version 430 core
|
|
out gl_PerVertex { vec4 gl_Position; };
|
|
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
|
|
)";
|
|
|
|
const char* kSamplerFs = R"(#version 430 core
|
|
uniform sampler2D u_tex;
|
|
out vec4 o_color;
|
|
void main() { o_color = texture(u_tex, vec2(0.0)); }
|
|
)";
|
|
|
|
class ProgramPipelineCompositeTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override { MobileGL::Initialize(); }
|
|
|
|
// Built by hand rather than through glCreateShaderProgramv, for the reason AsyncLinkTest
|
|
// gives: that entry point detaches the shader right after linking, so a relink would
|
|
// leave the stage program with nothing to composite from - and one of the cases below
|
|
// relinks on purpose.
|
|
GLuint MakeSeparableProgram(const GLenum stage, const char* source) {
|
|
const GLuint shader = CreateShader(stage);
|
|
ShaderSource(shader, 1, &source, nullptr);
|
|
CompileShader(shader);
|
|
const GLuint program = CreateProgram();
|
|
ProgramParameteri(program, GL_PROGRAM_SEPARABLE, GL_TRUE);
|
|
AttachShader(program, shader);
|
|
LinkProgram(program);
|
|
GLint linked = GL_FALSE;
|
|
GetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
EXPECT_EQ(linked, GL_TRUE) << "separable stage program did not link";
|
|
return program;
|
|
}
|
|
|
|
// The composite the next draw would run, settled.
|
|
static SharedPtr<MG_State::GLState::ProgramObject> DrawProgram() {
|
|
return MG_State::pGLContext->GetProgramForDraw();
|
|
}
|
|
|
|
// A uniform's value read out of a program's own shadow, by name. This is what the draw
|
|
// would upload, which is the thing under test - glGetUniform* would answer the same for
|
|
// the STAGE programs but has no way to name the composite at all.
|
|
static std::vector<float> ReadVec4(MG_State::GLState::ProgramObject& program, const String& name) {
|
|
const Int location = program.GetUniformLocation(name);
|
|
if (location < 0) return {};
|
|
const Uint offset = program.GetUniformOffset(static_cast<Uint>(location));
|
|
const auto* ubo = static_cast<const char*>(program.GetUBOData());
|
|
if (ubo == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
|
offset + 4 * sizeof(float) > program.GetUBOSize()) {
|
|
return {};
|
|
}
|
|
std::vector<float> value(4);
|
|
std::memcpy(value.data(), ubo + offset, 4 * sizeof(float));
|
|
return value;
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Which stage wins the composite's single slot
|
|
// ---------------------------------------------------------------------------------------
|
|
|
|
// THE defect. Both stages declare `u_shared`; only the VERTEX program is ever written to.
|
|
// Walking the stages in order and copying every active uniform unconditionally meant the
|
|
// fragment stage's untouched zero default landed last and won, so the composite drew zeros - a
|
|
// whole frame of nothing, from a program that had been set up entirely correctly.
|
|
TEST_F(ProgramPipelineCompositeTest, AWrittenStageValueIsNotClobberedByAnotherStagesUntouchedDeclaration) {
|
|
const GLuint vs = MakeSeparableProgram(GL_VERTEX_SHADER, kSharedUniformVs);
|
|
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSharedUniformFs);
|
|
|
|
GLuint pipeline = 0;
|
|
GenProgramPipelines(1, &pipeline);
|
|
BindProgramPipeline(pipeline);
|
|
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
|
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
|
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
|
|
|
// Exactly what an application does: point glUniform* at the vertex stage and write there.
|
|
// The fragment program is never written to and holds nothing but GL's zero default.
|
|
ActiveShaderProgram(pipeline, vs);
|
|
const GLint location = GetUniformLocation(vs, "u_shared");
|
|
ASSERT_GE(location, 0);
|
|
const float written[4] = {0.25f, 0.5f, 0.75f, 1.0f};
|
|
Uniform4fv(location, 1, written);
|
|
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
|
|
|
const auto composite = DrawProgram();
|
|
ASSERT_NE(composite, nullptr);
|
|
const std::vector<float> value = ReadVec4(*composite, "u_shared");
|
|
ASSERT_EQ(value.size(), 4u) << "u_shared has no backing storage in the composite";
|
|
EXPECT_EQ(value, (std::vector<float>{0.25f, 0.5f, 0.75f, 1.0f}))
|
|
<< "the fragment stage's untouched declaration overwrote the vertex stage's written value";
|
|
|
|
// The uniform only one stage declares is unaffected either way; it is here so a mirror that
|
|
// copied nothing at all would not pass this case by accident.
|
|
ActiveShaderProgram(pipeline, vs);
|
|
const GLint vsOnly = GetUniformLocation(vs, "u_vsOnly");
|
|
ASSERT_GE(vsOnly, 0);
|
|
const float other[4] = {1.0f, 2.0f, 3.0f, 4.0f};
|
|
Uniform4fv(vsOnly, 1, other);
|
|
const auto refreshed = DrawProgram();
|
|
EXPECT_EQ(ReadVec4(*refreshed, "u_vsOnly"), (std::vector<float>{1.0f, 2.0f, 3.0f, 4.0f}));
|
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
|
|
BindProgramPipeline(0);
|
|
DeleteProgramPipelines(1, &pipeline);
|
|
}
|
|
|
|
// The tie the fix cannot make disappear: BOTH stages were written, and the composite still has
|
|
// one slot. The documented rule is last WRITTEN-TO graphics stage wins, in ShaderStage enum
|
|
// order - deterministic, and reachable only by a stage holding a real application value.
|
|
TEST_F(ProgramPipelineCompositeTest, WhenBothStagesWereWrittenTheLastGraphicsStageWins) {
|
|
const GLuint vs = MakeSeparableProgram(GL_VERTEX_SHADER, kSharedUniformVs);
|
|
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSharedUniformFs);
|
|
|
|
GLuint pipeline = 0;
|
|
GenProgramPipelines(1, &pipeline);
|
|
BindProgramPipeline(pipeline);
|
|
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
|
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
|
|
|
const float fromVs[4] = {1.0f, 1.0f, 1.0f, 1.0f};
|
|
const float fromFs[4] = {2.0f, 2.0f, 2.0f, 2.0f};
|
|
// Written in the order VS then FS...
|
|
ProgramUniform4fv(vs, GetUniformLocation(vs, "u_shared"), 1, fromVs);
|
|
ProgramUniform4fv(fs, GetUniformLocation(fs, "u_shared"), 1, fromFs);
|
|
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
|
EXPECT_EQ(ReadVec4(*DrawProgram(), "u_shared"), (std::vector<float>{2.0f, 2.0f, 2.0f, 2.0f}));
|
|
|
|
// ...and in the order FS then VS. The answer is the same, because the rule is stage order
|
|
// and not write order - which is the honest statement of what the dirty set can support.
|
|
ProgramUniform4fv(fs, GetUniformLocation(fs, "u_shared"), 1, fromFs);
|
|
ProgramUniform4fv(vs, GetUniformLocation(vs, "u_shared"), 1, fromVs);
|
|
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
|
EXPECT_EQ(ReadVec4(*DrawProgram(), "u_shared"), (std::vector<float>{2.0f, 2.0f, 2.0f, 2.0f}))
|
|
<< "the both-written tie must be decided by stage order, deterministically";
|
|
|
|
BindProgramPipeline(0);
|
|
DeleteProgramPipelines(1, &pipeline);
|
|
}
|
|
|
|
// The both-written tie again, through the case that has no BYTES to move: the fragment stage
|
|
// writes the value it was already holding.
|
|
//
|
|
// The refresh gate is built out of counters that move when bytes move (the UBO content
|
|
// version, the backend state version), and both write funnels drop a value-identical write
|
|
// before bumping either. So this write enlarges the write SET - it makes the fragment stage
|
|
// the last written-to stage for `u_shared`, which is what decides the slot - while moving
|
|
// nothing else. Without a generation on the set itself the gate never trips and the draw keeps
|
|
// the vertex stage's value.
|
|
TEST_F(ProgramPipelineCompositeTest, AValueIdenticalWriteStillTakesTheSlotForItsStage) {
|
|
const GLuint vs = MakeSeparableProgram(GL_VERTEX_SHADER, kSharedUniformVs);
|
|
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSharedUniformFs);
|
|
|
|
GLuint pipeline = 0;
|
|
GenProgramPipelines(1, &pipeline);
|
|
BindProgramPipeline(pipeline);
|
|
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
|
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
|
|
|
const float fromVs[4] = {5.0f, 5.0f, 5.0f, 5.0f};
|
|
ProgramUniform4fv(vs, GetUniformLocation(vs, "u_shared"), 1, fromVs);
|
|
ASSERT_EQ(ReadVec4(*DrawProgram(), "u_shared"), (std::vector<float>{5.0f, 5.0f, 5.0f, 5.0f}));
|
|
|
|
// The fragment program's u_shared already reads all-zero, so this write changes not one
|
|
// byte of its shadow - and must still hand it the composite's slot.
|
|
const float zeros[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
|
ProgramUniform4fv(fs, GetUniformLocation(fs, "u_shared"), 1, zeros);
|
|
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
|
EXPECT_EQ(ReadVec4(*DrawProgram(), "u_shared"), (std::vector<float>{0.0f, 0.0f, 0.0f, 0.0f}))
|
|
<< "a write that moved no bytes never reached the refresh gate";
|
|
|
|
BindProgramPipeline(0);
|
|
DeleteProgramPipelines(1, &pipeline);
|
|
}
|
|
|
|
// A stage program that recorded NONE of its writes, because nothing ever armed its tracking
|
|
// latch: the mirror has to fall back to carrying everything rather than carrying nothing.
|
|
// Mirroring nothing would have been a fresh regression on a shape that worked before the dirty
|
|
// set existed.
|
|
//
|
|
// The shape used to be reachable through glUseProgramStages, which accepted a program that was
|
|
// never linked as separable. It no longer is: GL 4.6 core 7.4 requires the LATCHED
|
|
// PROGRAM_SEPARABLE flag and MobileGL now enforces it, and arming that flag is the very thing
|
|
// that arms the tracking latch - so no program the entry point accepts can be in this state. The
|
|
// fallback is therefore unreachable from GL and is exercised through the state layer instead,
|
|
// which is the only way left to keep it covered rather than deleting the coverage with the hole.
|
|
TEST_F(ProgramPipelineCompositeTest, ANonSeparableStageProgramStillMirrorsItsUniforms) {
|
|
const char* vsSource = R"(#version 430 core
|
|
uniform vec4 u_vsOnly;
|
|
void main() { gl_Position = u_vsOnly; }
|
|
)";
|
|
const GLuint shader = CreateShader(GL_VERTEX_SHADER);
|
|
ShaderSource(shader, 1, &vsSource, nullptr);
|
|
CompileShader(shader);
|
|
const GLuint vs = CreateProgram();
|
|
// Deliberately NO ProgramParameteri(GL_PROGRAM_SEPARABLE): this is the shape the latch
|
|
// cannot see coming.
|
|
AttachShader(vs, shader);
|
|
LinkProgram(vs);
|
|
GLint linked = GL_FALSE;
|
|
GetProgramiv(vs, GL_LINK_STATUS, &linked);
|
|
ASSERT_EQ(linked, GL_TRUE);
|
|
|
|
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSharedUniformFs);
|
|
|
|
GLuint pipeline = 0;
|
|
GenProgramPipelines(1, &pipeline);
|
|
BindProgramPipeline(pipeline);
|
|
// The fragment stage goes through the entry point; the vertex one cannot, so it is installed
|
|
// directly on the pipeline object - the same call glUseProgramStages makes once it is done
|
|
// validating, minus the validation this shape now fails.
|
|
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
|
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
|
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
|
ASSERT_EQ(GetError(), GL_INVALID_OPERATION)
|
|
<< "a program not linked as separable is not a legal pipeline stage";
|
|
{
|
|
const auto& pipelineObject = MG_State::pGLContext->MaterializeProgramPipelineObject(pipeline);
|
|
ASSERT_NE(pipelineObject, nullptr);
|
|
pipelineObject->SetStageProgram(ShaderStage::Vertex, MG_State::pGLContext->GetProgramObject(vs));
|
|
}
|
|
|
|
const float written[4] = {3.0f, 1.0f, 4.0f, 1.0f};
|
|
ProgramUniform4fv(vs, GetUniformLocation(vs, "u_vsOnly"), 1, written);
|
|
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
|
|
|
const auto composite = DrawProgram();
|
|
ASSERT_NE(composite, nullptr);
|
|
EXPECT_FALSE(MG_State::pGLContext->GetProgramObject(vs)->TracksUniformWrites())
|
|
<< "this case is only meaningful while the stage program records nothing";
|
|
EXPECT_EQ(ReadVec4(*composite, "u_vsOnly"), (std::vector<float>{3.0f, 1.0f, 4.0f, 1.0f}))
|
|
<< "a stage program with no write record must fall back to mirroring everything";
|
|
|
|
BindProgramPipeline(0);
|
|
DeleteProgramPipelines(1, &pipeline);
|
|
}
|
|
|
|
// glProgramUniform* addresses a program by NAME and needs neither a current program nor an
|
|
// active shader program, so it is a write path that never touches the pipeline at all. It has
|
|
// to record the write exactly like glUniform* does.
|
|
TEST_F(ProgramPipelineCompositeTest, ProgramUniformOnAnUnboundStageProgramReachesTheComposite) {
|
|
const GLuint vs = MakeSeparableProgram(GL_VERTEX_SHADER, kSharedUniformVs);
|
|
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSharedUniformFs);
|
|
|
|
GLuint pipeline = 0;
|
|
GenProgramPipelines(1, &pipeline);
|
|
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
|
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
|
|
|
// Deliberately BEFORE the bind, and with no glActiveShaderProgram anywhere: the write has
|
|
// to survive from here to a draw that has not been set up yet.
|
|
const float written[4] = {9.0f, 8.0f, 7.0f, 6.0f};
|
|
ProgramUniform4fv(vs, GetUniformLocation(vs, "u_vsOnly"), 1, written);
|
|
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
|
|
|
BindProgramPipeline(pipeline);
|
|
EXPECT_EQ(ReadVec4(*DrawProgram(), "u_vsOnly"), (std::vector<float>{9.0f, 8.0f, 7.0f, 6.0f}));
|
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
|
|
BindProgramPipeline(0);
|
|
DeleteProgramPipelines(1, &pipeline);
|
|
}
|
|
|
|
// Array uniforms are written at ELEMENT locations, so the record has to be per location and not
|
|
// per name: a stage that wrote `u_arr[2]` and nothing else must carry element 2 across and
|
|
// leave the rest to whichever stage owns them.
|
|
TEST_F(ProgramPipelineCompositeTest, ArrayElementWritesMirrorPerElement) {
|
|
const GLuint vs = MakeSeparableProgram(GL_VERTEX_SHADER, kArrayUniformVs);
|
|
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kArrayUniformFs);
|
|
|
|
GLuint pipeline = 0;
|
|
GenProgramPipelines(1, &pipeline);
|
|
BindProgramPipeline(pipeline);
|
|
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
|
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
|
|
|
// Non-prefix on purpose: elements 1 and 3 from the vertex stage, element 2 from the fragment
|
|
// stage, element 0 from nobody. A per-name record would have carried whole arrays and let
|
|
// one stage's zeros take the other's elements.
|
|
const float one[4] = {11.0f, 11.0f, 11.0f, 11.0f};
|
|
const float three[4] = {33.0f, 33.0f, 33.0f, 33.0f};
|
|
const float two[4] = {22.0f, 22.0f, 22.0f, 22.0f};
|
|
ProgramUniform4fv(vs, GetUniformLocation(vs, "u_arr[1]"), 1, one);
|
|
ProgramUniform4fv(vs, GetUniformLocation(vs, "u_arr[3]"), 1, three);
|
|
ProgramUniform4fv(fs, GetUniformLocation(fs, "u_arr[2]"), 1, two);
|
|
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
|
|
|
const auto composite = DrawProgram();
|
|
ASSERT_NE(composite, nullptr);
|
|
EXPECT_EQ(ReadVec4(*composite, "u_arr[0]"), (std::vector<float>{0.0f, 0.0f, 0.0f, 0.0f}));
|
|
EXPECT_EQ(ReadVec4(*composite, "u_arr[1]"), (std::vector<float>{11.0f, 11.0f, 11.0f, 11.0f}));
|
|
EXPECT_EQ(ReadVec4(*composite, "u_arr[2]"), (std::vector<float>{22.0f, 22.0f, 22.0f, 22.0f}));
|
|
EXPECT_EQ(ReadVec4(*composite, "u_arr[3]"), (std::vector<float>{33.0f, 33.0f, 33.0f, 33.0f}));
|
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
|
|
// A multi-element glUniform*v run marks each location it actually reaches.
|
|
const float tail[8] = {44.0f, 44.0f, 44.0f, 44.0f, 55.0f, 55.0f, 55.0f, 55.0f};
|
|
ActiveShaderProgram(pipeline, fs);
|
|
Uniform4fv(GetUniformLocation(fs, "u_arr[2]"), 2, tail);
|
|
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
|
const auto refreshed = DrawProgram();
|
|
EXPECT_EQ(ReadVec4(*refreshed, "u_arr[2]"), (std::vector<float>{44.0f, 44.0f, 44.0f, 44.0f}));
|
|
EXPECT_EQ(ReadVec4(*refreshed, "u_arr[3]"), (std::vector<float>{55.0f, 55.0f, 55.0f, 55.0f}))
|
|
<< "the second element of a count=2 write was never recorded";
|
|
|
|
BindProgramPipeline(0);
|
|
DeleteProgramPipelines(1, &pipeline);
|
|
}
|
|
|
|
// Relinking resets a program's uniforms to their initial values (GL 4.6 core 7.6), so the record
|
|
// of what was written has to be reset with them. If it survived, the composite built after the
|
|
// relink would be handed values the stage program no longer holds.
|
|
TEST_F(ProgramPipelineCompositeTest, RelinkingAStageProgramClearsWhatItHadWritten) {
|
|
const GLuint vs = MakeSeparableProgram(GL_VERTEX_SHADER, kSharedUniformVs);
|
|
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSharedUniformFs);
|
|
|
|
GLuint pipeline = 0;
|
|
GenProgramPipelines(1, &pipeline);
|
|
BindProgramPipeline(pipeline);
|
|
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
|
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
|
|
|
const float written[4] = {5.0f, 6.0f, 7.0f, 8.0f};
|
|
ProgramUniform4fv(vs, GetUniformLocation(vs, "u_vsOnly"), 1, written);
|
|
ASSERT_EQ(ReadVec4(*DrawProgram(), "u_vsOnly"), (std::vector<float>{5.0f, 6.0f, 7.0f, 8.0f}));
|
|
|
|
LinkProgram(vs);
|
|
GLint linked = GL_FALSE;
|
|
GetProgramiv(vs, GL_LINK_STATUS, &linked);
|
|
ASSERT_EQ(linked, GL_TRUE);
|
|
|
|
const auto composite = DrawProgram();
|
|
ASSERT_NE(composite, nullptr);
|
|
EXPECT_EQ(ReadVec4(*composite, "u_vsOnly"), (std::vector<float>{0.0f, 0.0f, 0.0f, 0.0f}))
|
|
<< "a relinked stage program carried its pre-relink value into the new composite";
|
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
|
|
// ...and writing again after the relink is recorded afresh.
|
|
const float rewritten[4] = {1.5f, 2.5f, 3.5f, 4.5f};
|
|
ProgramUniform4fv(vs, GetUniformLocation(vs, "u_vsOnly"), 1, rewritten);
|
|
EXPECT_EQ(ReadVec4(*DrawProgram(), "u_vsOnly"), (std::vector<float>{1.5f, 2.5f, 3.5f, 4.5f}));
|
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
|
|
BindProgramPipeline(0);
|
|
DeleteProgramPipelines(1, &pipeline);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Composite cache stability
|
|
// ---------------------------------------------------------------------------------------
|
|
|
|
// The SSO-conformance shape, and the reason the composite cache stopped being keyed on the
|
|
// backend state version: pick a stage program, then per draw set a sampler unit and draw.
|
|
// glUniform1i on a sampler bumps that version, so the signature changed on every iteration and
|
|
// every single draw threw the composite away and relinked it - glslang, SPIR-V and spirv-opt,
|
|
// synchronously, inside the draw - handing the backends a brand-new program identity each time.
|
|
//
|
|
// Asserted on the composite POINTER, which is the honest observable: it is the object both
|
|
// backends key their per-program registries and pipeline memos on, so "same pointer" is exactly
|
|
// the property that was lost.
|
|
TEST_F(ProgramPipelineCompositeTest, ASamplerWritePerDrawDoesNotRebuildTheComposite) {
|
|
const GLuint vs = MakeSeparableProgram(GL_VERTEX_SHADER, kSamplerVs);
|
|
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSamplerFs);
|
|
|
|
GLuint pipeline = 0;
|
|
GenProgramPipelines(1, &pipeline);
|
|
BindProgramPipeline(pipeline);
|
|
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
|
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
|
ActiveShaderProgram(pipeline, fs);
|
|
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
|
|
|
const GLint sampler = GetUniformLocation(fs, "u_tex");
|
|
ASSERT_GE(sampler, 0);
|
|
|
|
const auto first = DrawProgram();
|
|
ASSERT_NE(first, nullptr);
|
|
const Uint64 firstLifetime = first->GetLifetimeId();
|
|
const Int compositeSampler = first->GetUniformLocation("u_tex");
|
|
ASSERT_GE(compositeSampler, 0);
|
|
|
|
for (GLint unit = 0; unit < 8; ++unit) {
|
|
Uniform1i(sampler, unit);
|
|
const auto composite = DrawProgram();
|
|
ASSERT_NE(composite, nullptr);
|
|
EXPECT_EQ(composite.get(), first.get())
|
|
<< "the composite was rebuilt by a sampler-unit write at unit " << unit;
|
|
EXPECT_EQ(composite->GetLifetimeId(), firstLifetime) << "the composite's identity changed at unit " << unit;
|
|
// The value still has to ARRIVE - the whole point is that the mirror carries it now that
|
|
// the rebuild no longer does.
|
|
EXPECT_EQ(composite->GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(compositeSampler)), unit)
|
|
<< "the sampler unit did not reach the composite at unit " << unit;
|
|
}
|
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
|
|
// A relink, by contrast, MUST replace it: that is the one thing the signature still tracks.
|
|
LinkProgram(fs);
|
|
GLint linked = GL_FALSE;
|
|
GetProgramiv(fs, GL_LINK_STATUS, &linked);
|
|
ASSERT_EQ(linked, GL_TRUE);
|
|
const auto afterRelink = DrawProgram();
|
|
ASSERT_NE(afterRelink, nullptr);
|
|
EXPECT_NE(afterRelink.get(), first.get()) << "a relinked stage program must rebuild the composite";
|
|
|
|
BindProgramPipeline(0);
|
|
DeleteProgramPipelines(1, &pipeline);
|
|
}
|
|
|
|
// The monolithic path must be untouched by any of this: a plain glUseProgram program is not
|
|
// separable, records nothing, and is its own draw program.
|
|
TEST_F(ProgramPipelineCompositeTest, AMonolithicProgramRecordsNothingAndIsItsOwnDrawProgram) {
|
|
const char* vsSource = R"(#version 430 core
|
|
uniform vec4 u_shared;
|
|
void main() { gl_Position = u_shared; }
|
|
)";
|
|
const char* fsSource = R"(#version 430 core
|
|
uniform vec4 u_shared;
|
|
out vec4 o_color;
|
|
void main() { o_color = u_shared; }
|
|
)";
|
|
const GLuint vsShader = CreateShader(GL_VERTEX_SHADER);
|
|
ShaderSource(vsShader, 1, &vsSource, nullptr);
|
|
CompileShader(vsShader);
|
|
const GLuint fsShader = CreateShader(GL_FRAGMENT_SHADER);
|
|
ShaderSource(fsShader, 1, &fsSource, nullptr);
|
|
CompileShader(fsShader);
|
|
|
|
const GLuint program = CreateProgram();
|
|
AttachShader(program, vsShader);
|
|
AttachShader(program, fsShader);
|
|
LinkProgram(program);
|
|
GLint linked = GL_FALSE;
|
|
GetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
ASSERT_EQ(linked, GL_TRUE);
|
|
|
|
UseProgram(program);
|
|
const float written[4] = {1.0f, 2.0f, 3.0f, 4.0f};
|
|
Uniform4fv(GetUniformLocation(program, "u_shared"), 1, written);
|
|
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
|
|
|
const auto drawProgram = DrawProgram();
|
|
ASSERT_NE(drawProgram, nullptr);
|
|
EXPECT_EQ(drawProgram->GetExternalIndex(), program) << "a current program IS the draw program";
|
|
// Nothing was recorded, because nothing ever asked this program to be separable - which is
|
|
// what keeps the hot uniform path free of the bookkeeping.
|
|
EXPECT_FALSE(drawProgram->TracksUniformWrites());
|
|
EXPECT_TRUE(drawProgram->GetWrittenUniformIndices().empty());
|
|
EXPECT_EQ(ReadVec4(*drawProgram, "u_shared"), (std::vector<float>{1.0f, 2.0f, 3.0f, 4.0f}));
|
|
|
|
UseProgram(0);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// The vertex stage a pre-rasterization pipeline must have
|
|
// ---------------------------------------------------------------------------------------
|
|
|
|
// GL 4.6 core 7.4.1: a pipeline whose tessellation-control, tessellation-evaluation or geometry
|
|
// stage has an executable, but which supplies no executable VERTEX shader, makes every command
|
|
// that transfers vertices an INVALID_OPERATION. MobileGL checked only "a program is current" and
|
|
// "it linked", so a geometry+fragment pipeline drew happily and rendered nothing -
|
|
// KHR-GL4x.geometry_shader.api.fs_gs_draw_call and .pipeline_program_without_active_vs.
|
|
TEST_F(ProgramPipelineCompositeTest, AGeometryPipelineWithNoVertexStageRefusesToDraw) {
|
|
const char* kGs = R"(#version 430 core
|
|
layout(points) in;
|
|
layout(points, max_vertices = 1) out;
|
|
void main() { gl_Position = vec4(0.0); EmitVertex(); EndPrimitive(); }
|
|
)";
|
|
const GLuint gs = MakeSeparableProgram(GL_GEOMETRY_SHADER, kGs);
|
|
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSharedUniformFs);
|
|
|
|
GLuint pipeline = 0;
|
|
GenProgramPipelines(1, &pipeline);
|
|
BindProgramPipeline(pipeline);
|
|
UseProgramStages(pipeline, GL_GEOMETRY_SHADER_BIT, gs);
|
|
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
|
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
|
|
|
// Not vacuous: the composite has to be a healthy linked program, so that the refusal below
|
|
// can only be the missing vertex stage and not a link that fell over on its own.
|
|
const auto composite = DrawProgram();
|
|
ASSERT_NE(composite, nullptr);
|
|
ASSERT_TRUE(composite->GetLinkStatus()) << "the composite itself must link for this test to mean anything";
|
|
ASSERT_TRUE(composite->HasLinkedShaderStage(ShaderStage::Geometry));
|
|
ASSERT_FALSE(composite->HasLinkedShaderStage(ShaderStage::Vertex));
|
|
|
|
DrawArrays(GL_POINTS, 0, 1);
|
|
EXPECT_EQ(GetError(), GL_INVALID_OPERATION)
|
|
<< "a geometry stage with no vertex stage must refuse the draw";
|
|
|
|
// A dispatch shares the same "is there a program, did it link" helper and legitimately has no
|
|
// vertex stage; the rule must not have leaked onto it. There is no compute stage here, so the
|
|
// error is the compute check's own - what matters is that the draw rule did not fire first
|
|
// with a different meaning.
|
|
for (Int drained = 0; drained < 16 && GetError() != GL_NO_ERROR; ++drained) {
|
|
}
|
|
|
|
BindProgramPipeline(0);
|
|
DeleteProgramPipelines(1, &pipeline);
|
|
for (Int drained = 0; drained < 16 && GetError() != GL_NO_ERROR; ++drained) {
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// The composite's transform-feedback capture list.
|
|
//
|
|
// Two rules, and getting either wrong turns a working pipeline into one where EVERY draw reports
|
|
// GL_INVALID_OPERATION: an unresolvable capture name fails the composite's own link, and
|
|
// ValidateProgramForExecution rejects every draw through a pipeline whose composite did not link -
|
|
// while glValidateProgramPipeline keeps reporting TRUE.
|
|
// ---------------------------------------------------------------------------------------------
|
|
|
|
namespace {
|
|
const char* kCaptureVs = R"(#version 430 core
|
|
out gl_PerVertex { vec4 gl_Position; };
|
|
out float v_captured;
|
|
out float v_other;
|
|
void main() { gl_Position = vec4(0.0); v_captured = 1.0; v_other = 2.0; }
|
|
)";
|
|
|
|
// A geometry stage that re-emits nothing the vertex stage named, so a capture list taken from
|
|
// the VERTEX program cannot resolve against it.
|
|
const char* kPassthroughGs = R"(#version 430 core
|
|
layout(points) in;
|
|
layout(points, max_vertices = 1) out;
|
|
out gl_PerVertex { vec4 gl_Position; };
|
|
out float g_only;
|
|
void main() { gl_Position = vec4(0.0); g_only = 1.0; EmitVertex(); EndPrimitive(); }
|
|
)";
|
|
|
|
Vector<String> CompositeCaptureNames(MG_State::GLState::ProgramObject& composite) {
|
|
Vector<String> names;
|
|
for (SizeT i = 0; i < composite.GetTransformFeedbackVaryingCount(); ++i) {
|
|
if (const auto* varying = composite.GetTransformFeedbackVarying(i)) {
|
|
names.push_back(varying->name);
|
|
}
|
|
}
|
|
return names;
|
|
}
|
|
} // namespace
|
|
|
|
// glTransformFeedbackVaryings does not take effect until the program's NEXT link (GL 4.6 core
|
|
// 7.3/11.1.2.1) and deliberately bumps no version, so a request written after the stage program's
|
|
// last link is invisible to the composite cache's signature - yet the next rebuild would pick it
|
|
// up. The capture list would then depend on whether some unrelated event happened to invalidate
|
|
// the cache. Reading the LINKED snapshot removes the whole class, and makes the existing cache key
|
|
// sufficient: linked state only moves at a link, which is exactly what the key tracks.
|
|
TEST_F(ProgramPipelineCompositeTest, CompositeCaptureListComesFromTheLinkedSnapshotNotThePendingRequest) {
|
|
const GLuint vs = CreateProgram();
|
|
{
|
|
const GLuint shader = CreateShader(GL_VERTEX_SHADER);
|
|
ShaderSource(shader, 1, &kCaptureVs, nullptr);
|
|
CompileShader(shader);
|
|
ProgramParameteri(vs, GL_PROGRAM_SEPARABLE, GL_TRUE);
|
|
AttachShader(vs, shader);
|
|
const char* captured = "v_captured";
|
|
TransformFeedbackVaryings(vs, 1, &captured, GL_INTERLEAVED_ATTRIBS);
|
|
LinkProgram(vs);
|
|
GLint linked = GL_FALSE;
|
|
GetProgramiv(vs, GL_LINK_STATUS, &linked);
|
|
ASSERT_EQ(linked, GL_TRUE);
|
|
}
|
|
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSharedUniformFs);
|
|
|
|
GLuint pipeline = 0;
|
|
GenProgramPipelines(1, &pipeline);
|
|
BindProgramPipeline(pipeline);
|
|
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
|
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
|
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
|
|
|
{
|
|
const auto composite = DrawProgram();
|
|
ASSERT_NE(composite, nullptr);
|
|
EXPECT_EQ(CompositeCaptureNames(*composite), (Vector<String>{"v_captured"}));
|
|
}
|
|
|
|
// A NEW request with no relink. GL says the program still captures v_captured.
|
|
const char* other = "v_other";
|
|
TransformFeedbackVaryings(vs, 1, &other, GL_INTERLEAVED_ATTRIBS);
|
|
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
|
|
|
// Force a composite rebuild through something entirely unrelated to the capture list: a new
|
|
// fragment stage program moves that slot's lifetime id, so the cache signature changes.
|
|
const GLuint fs2 = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSharedUniformFs);
|
|
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs2);
|
|
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
|
|
|
{
|
|
const auto composite = DrawProgram();
|
|
ASSERT_NE(composite, nullptr);
|
|
EXPECT_TRUE(composite->GetLinkStatus()) << "the composite must still link";
|
|
EXPECT_EQ(CompositeCaptureNames(*composite), (Vector<String>{"v_captured"}))
|
|
<< "an unlinked request must not reach the composite";
|
|
}
|
|
|
|
// Relinking the stage program IS what makes the new request take effect - and the composite
|
|
// follows, because the relink moves the link version the cache keys on.
|
|
LinkProgram(vs);
|
|
{
|
|
const auto composite = DrawProgram();
|
|
ASSERT_NE(composite, nullptr);
|
|
EXPECT_EQ(CompositeCaptureNames(*composite), (Vector<String>{"v_other"}));
|
|
}
|
|
|
|
BindProgramPipeline(0);
|
|
DeleteProgramPipelines(1, &pipeline);
|
|
}
|
|
|
|
// Transform feedback captures the output of the LAST vertex-processing stage (GL 4.6 core
|
|
// 11.1.2.1) - the last stage that EXISTS, not the last one that happens to carry a capture list.
|
|
// Falling through a geometry stage with no request and installing the vertex stage's list instead
|
|
// made the two halves disagree: this loop picks whose list, the link task resolves those names
|
|
// against the geometry intermediate. Either it captures where GL says it must not, or the
|
|
// composite fails to link and every draw through the pipeline reports GL_INVALID_OPERATION.
|
|
TEST_F(ProgramPipelineCompositeTest, CompositeCaptureStageIsTheLastVertexProcessingStageThatExists) {
|
|
const GLuint vs = CreateProgram();
|
|
{
|
|
const GLuint shader = CreateShader(GL_VERTEX_SHADER);
|
|
ShaderSource(shader, 1, &kCaptureVs, nullptr);
|
|
CompileShader(shader);
|
|
ProgramParameteri(vs, GL_PROGRAM_SEPARABLE, GL_TRUE);
|
|
AttachShader(vs, shader);
|
|
const char* captured = "v_captured";
|
|
TransformFeedbackVaryings(vs, 1, &captured, GL_INTERLEAVED_ATTRIBS);
|
|
LinkProgram(vs);
|
|
GLint linked = GL_FALSE;
|
|
GetProgramiv(vs, GL_LINK_STATUS, &linked);
|
|
ASSERT_EQ(linked, GL_TRUE);
|
|
}
|
|
// The geometry program was never given a capture list, and "v_captured" is not one of its
|
|
// outputs - so a composite seeded from the VERTEX program's list cannot resolve it.
|
|
const GLuint gs = MakeSeparableProgram(GL_GEOMETRY_SHADER, kPassthroughGs);
|
|
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kSharedUniformFs);
|
|
|
|
GLuint pipeline = 0;
|
|
GenProgramPipelines(1, &pipeline);
|
|
BindProgramPipeline(pipeline);
|
|
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
|
UseProgramStages(pipeline, GL_GEOMETRY_SHADER_BIT, gs);
|
|
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
|
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
|
|
|
const auto composite = DrawProgram();
|
|
ASSERT_NE(composite, nullptr);
|
|
EXPECT_TRUE(composite->GetLinkStatus())
|
|
<< "the geometry stage is the capture stage and has no capture list, so the composite links "
|
|
"with none - it must not inherit the vertex stage's and fail resolving it";
|
|
EXPECT_EQ(composite->GetTransformFeedbackVaryingCount(), 0u)
|
|
<< "the capture stage is the geometry program, which declared nothing to capture";
|
|
|
|
BindProgramPipeline(0);
|
|
DeleteProgramPipelines(1, &pipeline);
|
|
}
|