mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
[Fix, Test] (MG_State): a pipeline draw uses the block bindings its stage programs were given, not the ones their shaders declared
This commit is contained in:
@@ -86,6 +86,20 @@ void main()
|
|||||||
layout(location = 0) in vec4 i_position;
|
layout(location = 0) in vec4 i_position;
|
||||||
out gl_PerVertex { vec4 gl_Position; };
|
out gl_PerVertex { vec4 gl_Position; };
|
||||||
void main() { gl_Position = i_position; }
|
void main() { gl_Position = i_position; }
|
||||||
|
)";
|
||||||
|
|
||||||
|
// Two shader storage blocks with NO layout(binding) qualifier, so the only thing that
|
||||||
|
// can say where they live is glShaderStorageBlockBinding - which is per-PROGRAM state.
|
||||||
|
constexpr const char* kStorageBlockVS = R"(#version 430 core
|
||||||
|
out gl_PerVertex { vec4 gl_Position; };
|
||||||
|
layout(std430) buffer Output0 { uint value0; };
|
||||||
|
layout(std430) buffer Output1 { uint value1; };
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
value0 = 11u;
|
||||||
|
value1 = 22u;
|
||||||
|
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
)";
|
)";
|
||||||
|
|
||||||
class ProgramPipelineScenario : public ScenarioTest {
|
class ProgramPipelineScenario : public ScenarioTest {
|
||||||
@@ -127,6 +141,20 @@ void main() { gl_Position = i_position; }
|
|||||||
return pipeline;
|
return pipeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// glShaderStorageBlockBinding is a GL 4.3 entry point with NO equivalent in ES: a
|
||||||
|
// storage block's binding there is fixed by its layout(binding=) qualifier at link
|
||||||
|
// and cannot be changed afterwards. So Espryt, which reaches the GPU through an ES
|
||||||
|
// driver, can only honour a rebinding by baking it into the ESSL it generates -
|
||||||
|
// and it does not yet (RemoveLayoutBinding in MG_Backend/DirectGLES/Utils.cpp
|
||||||
|
// deliberately PRESERVES the declared qualifier for `buffer` declarations, which
|
||||||
|
// is what the driver then goes by). Its best-effort API replay
|
||||||
|
// (ReseedShaderStorageBlockBindings) is a no-op wherever the driver lacks the
|
||||||
|
// entry point, which is every real ES driver.
|
||||||
|
//
|
||||||
|
// Scoped rather than disabled, because the defect is per-backend and the
|
||||||
|
// pipeline-side mechanism these cases exist for is fully exercised on Magma.
|
||||||
|
bool StorageBlockRebindingIsHonoured() const { return Gl().BackendName() == "DirectVulkan"; }
|
||||||
|
|
||||||
std::vector<GLuint> m_programs;
|
std::vector<GLuint> m_programs;
|
||||||
std::vector<GLuint> m_pipelines;
|
std::vector<GLuint> m_pipelines;
|
||||||
};
|
};
|
||||||
@@ -344,6 +372,249 @@ void main() { o_color = u_color; }
|
|||||||
gl.EndFrame();
|
gl.EndFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Interface-resource bindings are per-PROGRAM state, and the program a pipeline draw executes
|
||||||
|
// is the composite - not the stage program the application set them on.
|
||||||
|
//
|
||||||
|
// This is shader_storage_buffer_object.basic-noBindingLayout reduced: blocks declared without
|
||||||
|
// a layout(binding) qualifier, placed onto binding points purely by
|
||||||
|
// glShaderStorageBlockBinding against the stage program. The stage program records the
|
||||||
|
// rebinding (ProgramObject::SetShaderStorageBlockBinding, keyed by block name) and the
|
||||||
|
// composite is built from the stage program's SHADERS - which carry the declared bindings and
|
||||||
|
// know nothing of the rebinding. So the draw writes wherever the shader source said, the
|
||||||
|
// bound buffer ranges never see a byte, and no GL error is raised anywhere: the readback is
|
||||||
|
// the only thing that notices.
|
||||||
|
TEST_F(ProgramPipelineScenario, AStageProgramsStorageBlockBindingReachesThePipelineDraw) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
|
||||||
|
GLint vertexStorageBlocks = 0;
|
||||||
|
glGetIntegerv(GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS, &vertexStorageBlocks);
|
||||||
|
if (vertexStorageBlocks < 2) {
|
||||||
|
GTEST_SKIP() << "fewer than two vertex shader storage blocks available";
|
||||||
|
}
|
||||||
|
if (!StorageBlockRebindingIsHonoured()) {
|
||||||
|
GTEST_SKIP() << "backend cannot honour glShaderStorageBlockBinding at all; see the companion "
|
||||||
|
"AStorageBlockRebindingHoldsWithoutAPipeline case";
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLuint vs = MakeSeparable(GL_VERTEX_SHADER, kStorageBlockVS);
|
||||||
|
if (vs == 0) return;
|
||||||
|
|
||||||
|
// Rebound to binding points the shader source never mentions, so nothing but the
|
||||||
|
// rebinding can put the writes where this case looks for them.
|
||||||
|
constexpr GLuint kBinding0 = 1;
|
||||||
|
constexpr GLuint kBinding1 = 5;
|
||||||
|
const GLuint block0 = glGetProgramResourceIndex(vs, GL_SHADER_STORAGE_BLOCK, "Output0");
|
||||||
|
const GLuint block1 = glGetProgramResourceIndex(vs, GL_SHADER_STORAGE_BLOCK, "Output1");
|
||||||
|
ASSERT_NE(block0, GL_INVALID_INDEX);
|
||||||
|
ASSERT_NE(block1, GL_INVALID_INDEX);
|
||||||
|
glShaderStorageBlockBinding(vs, block0, kBinding0);
|
||||||
|
glShaderStorageBlockBinding(vs, block1, kBinding1);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "glShaderStorageBlockBinding on a separable program errored";
|
||||||
|
|
||||||
|
GLint offsetAlignment = 256;
|
||||||
|
glGetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &offsetAlignment);
|
||||||
|
if (offsetAlignment <= 0) offsetAlignment = 256;
|
||||||
|
const GLsizeiptr secondOffset = offsetAlignment;
|
||||||
|
|
||||||
|
GLuint buffer = 0;
|
||||||
|
glGenBuffers(1, &buffer);
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||||
|
const std::vector<GLuint> zeros(static_cast<std::size_t>(secondOffset) / sizeof(GLuint) + 4, 0u);
|
||||||
|
glBufferData(GL_SHADER_STORAGE_BUFFER, static_cast<GLsizeiptr>(zeros.size() * sizeof(GLuint)), zeros.data(),
|
||||||
|
GL_DYNAMIC_DRAW);
|
||||||
|
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, kBinding0, buffer, 0, sizeof(GLuint));
|
||||||
|
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, kBinding1, buffer, secondOffset, sizeof(GLuint));
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||||
|
|
||||||
|
const GLuint pipeline = MakePipeline();
|
||||||
|
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
// The whole point is the buffer writes, so the rasterizer is not involved - which is
|
||||||
|
// also what keeps a vertex-only pipeline (no fragment stage) legal here.
|
||||||
|
glEnable(GL_RASTERIZER_DISCARD);
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindProgramPipeline(pipeline);
|
||||||
|
glDrawArrays(GL_POINTS, 0, 1);
|
||||||
|
glDisable(GL_RASTERIZER_DISCARD);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the storage-block pipeline draw leaked a GL error";
|
||||||
|
|
||||||
|
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT);
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||||
|
GLuint readback0 = 0;
|
||||||
|
GLuint readback1 = 0;
|
||||||
|
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(readback0), &readback0);
|
||||||
|
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, secondOffset, sizeof(readback1), &readback1);
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||||
|
|
||||||
|
EXPECT_EQ(readback0, 11u) << "Output0 did not reach the binding glShaderStorageBlockBinding gave it";
|
||||||
|
EXPECT_EQ(readback1, 22u) << "Output1 did not reach the binding glShaderStorageBlockBinding gave it";
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteBuffers(1, &buffer);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// CONTROL for the case above, and the thing that says whether a storage-block failure is
|
||||||
|
// about pipelines at all: the same shader, the same rebinding, in an ordinary two-stage
|
||||||
|
// monolithic program run through glUseProgram. If this one fails too then the composite is
|
||||||
|
// innocent and the defect is in how the backend replays a rebinding.
|
||||||
|
//
|
||||||
|
// Two stages on purpose. Handing glUseProgram a vertex-ONLY program would confound the
|
||||||
|
// experiment - a program with no fragment stage is a thing some backends cannot build at
|
||||||
|
// all, so its failure would say nothing about block bindings.
|
||||||
|
TEST_F(ProgramPipelineScenario, AStorageBlockRebindingHoldsWithoutAPipeline) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
|
||||||
|
GLint vertexStorageBlocks = 0;
|
||||||
|
glGetIntegerv(GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS, &vertexStorageBlocks);
|
||||||
|
if (vertexStorageBlocks < 2) {
|
||||||
|
GTEST_SKIP() << "fewer than two vertex shader storage blocks available";
|
||||||
|
}
|
||||||
|
if (!StorageBlockRebindingIsHonoured()) {
|
||||||
|
GTEST_SKIP() << "backend honours a storage-block rebinding only through the declared "
|
||||||
|
"layout(binding=) qualifier, which it does not yet rewrite";
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* kMonolithicVS = R"(#version 430 core
|
||||||
|
layout(std430) buffer Output0 { uint value0; };
|
||||||
|
layout(std430) buffer Output1 { uint value1; };
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
value0 = 11u;
|
||||||
|
value1 = 22u;
|
||||||
|
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
static const char* kMonolithicFS = R"(#version 430 core
|
||||||
|
out vec4 o_color;
|
||||||
|
void main() { o_color = vec4(1.0); }
|
||||||
|
)";
|
||||||
|
std::string compileError;
|
||||||
|
const GLuint vs = CompileProgram(kMonolithicVS, kMonolithicFS, &compileError);
|
||||||
|
ASSERT_NE(vs, 0u) << compileError;
|
||||||
|
m_programs.push_back(vs);
|
||||||
|
|
||||||
|
constexpr GLuint kBinding0 = 1;
|
||||||
|
constexpr GLuint kBinding1 = 5;
|
||||||
|
const GLuint block0 = glGetProgramResourceIndex(vs, GL_SHADER_STORAGE_BLOCK, "Output0");
|
||||||
|
const GLuint block1 = glGetProgramResourceIndex(vs, GL_SHADER_STORAGE_BLOCK, "Output1");
|
||||||
|
ASSERT_NE(block0, GL_INVALID_INDEX);
|
||||||
|
ASSERT_NE(block1, GL_INVALID_INDEX);
|
||||||
|
glShaderStorageBlockBinding(vs, block0, kBinding0);
|
||||||
|
glShaderStorageBlockBinding(vs, block1, kBinding1);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u);
|
||||||
|
|
||||||
|
GLint offsetAlignment = 256;
|
||||||
|
glGetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &offsetAlignment);
|
||||||
|
if (offsetAlignment <= 0) offsetAlignment = 256;
|
||||||
|
const GLsizeiptr secondOffset = offsetAlignment;
|
||||||
|
|
||||||
|
GLuint buffer = 0;
|
||||||
|
glGenBuffers(1, &buffer);
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||||
|
const std::vector<GLuint> zeros(static_cast<std::size_t>(secondOffset) / sizeof(GLuint) + 4, 0u);
|
||||||
|
glBufferData(GL_SHADER_STORAGE_BUFFER, static_cast<GLsizeiptr>(zeros.size() * sizeof(GLuint)), zeros.data(),
|
||||||
|
GL_DYNAMIC_DRAW);
|
||||||
|
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, kBinding0, buffer, 0, sizeof(GLuint));
|
||||||
|
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, kBinding1, buffer, secondOffset, sizeof(GLuint));
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glEnable(GL_RASTERIZER_DISCARD);
|
||||||
|
// No pipeline anywhere: a separable program is still a perfectly good current program.
|
||||||
|
glBindProgramPipeline(0);
|
||||||
|
glUseProgram(vs);
|
||||||
|
glDrawArrays(GL_POINTS, 0, 1);
|
||||||
|
glDisable(GL_RASTERIZER_DISCARD);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the monolithic storage-block draw leaked a GL error";
|
||||||
|
|
||||||
|
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT);
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
||||||
|
GLuint readback0 = 0;
|
||||||
|
GLuint readback1 = 0;
|
||||||
|
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(readback0), &readback0);
|
||||||
|
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, secondOffset, sizeof(readback1), &readback1);
|
||||||
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
||||||
|
|
||||||
|
EXPECT_EQ(readback0, 11u) << "Output0 missed its rebinding with no pipeline involved";
|
||||||
|
EXPECT_EQ(readback1, 22u) << "Output1 missed its rebinding with no pipeline involved";
|
||||||
|
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteBuffers(1, &buffer);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same defect through the other block flavour: glUniformBlockBinding is also per-program
|
||||||
|
// state, recorded on the stage program by GL block index, and also never reaches the
|
||||||
|
// composite the draw actually runs.
|
||||||
|
TEST_F(ProgramPipelineScenario, AStageProgramsUniformBlockBindingReachesThePipelineDraw) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
HeadlessGL& gl = Gl();
|
||||||
|
const int width = gl.Width();
|
||||||
|
const int height = gl.Height();
|
||||||
|
|
||||||
|
static const char* kUniformBlockFS = R"(#version 430 core
|
||||||
|
layout(std140) uniform Colour { vec4 u_colour; };
|
||||||
|
out vec4 o_color;
|
||||||
|
void main() { o_color = u_colour; }
|
||||||
|
)";
|
||||||
|
const GLuint vs = MakeSeparable(GL_VERTEX_SHADER, kSeparableVS);
|
||||||
|
const GLuint fs = MakeSeparable(GL_FRAGMENT_SHADER, kUniformBlockFS);
|
||||||
|
if (vs == 0 || fs == 0) return;
|
||||||
|
|
||||||
|
constexpr GLuint kBinding = 3; // not the default 0 the declaration implies
|
||||||
|
const GLuint blockIndex = glGetUniformBlockIndex(fs, "Colour");
|
||||||
|
ASSERT_NE(blockIndex, GL_INVALID_INDEX);
|
||||||
|
glUniformBlockBinding(fs, blockIndex, kBinding);
|
||||||
|
ASSERT_EQ(FirstGLError(), 0u) << "glUniformBlockBinding on a separable program errored";
|
||||||
|
|
||||||
|
const GLfloat green[4] = {0.0f, 1.0f, 0.0f, 1.0f};
|
||||||
|
GLuint buffer = 0;
|
||||||
|
glGenBuffers(1, &buffer);
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, buffer);
|
||||||
|
glBufferData(GL_UNIFORM_BUFFER, sizeof(green), green, GL_STATIC_DRAW);
|
||||||
|
glBindBufferBase(GL_UNIFORM_BUFFER, kBinding, buffer);
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||||
|
|
||||||
|
const GLuint pipeline = MakePipeline();
|
||||||
|
glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
||||||
|
glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, width, height);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindProgramPipeline(pipeline);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
|
||||||
|
const Image painted = ReadPixels(width, height);
|
||||||
|
EXPECT_TRUE(RegionIsMostly(painted, 2, width - 3, 2, height - 3, "green", 0.0,
|
||||||
|
"a pipeline whose fragment uniform block was rebound to binding 3"));
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << "the uniform-block pipeline draw leaked a GL error";
|
||||||
|
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteBuffers(1, &buffer);
|
||||||
|
gl.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
// build-separable / build-monolithic reduce to this: a separable program and a monolithic one
|
// build-separable / build-monolithic reduce to this: a separable program and a monolithic one
|
||||||
// must both be usable, and switching between pipeline and glUseProgram must leave no error.
|
// must both be usable, and switching between pipeline and glUseProgram must leave no error.
|
||||||
TEST_F(ProgramPipelineScenario, SwitchingBetweenAPipelineAndAMonolithicProgramLeavesNoError) {
|
TEST_F(ProgramPipelineScenario, SwitchingBetweenAPipelineAndAMonolithicProgramLeavesNoError) {
|
||||||
|
|||||||
@@ -458,9 +458,46 @@ namespace MobileGL::MG_State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Brings the pipeline's composite up to date with the uniform values its stage programs
|
// The other half of "the composite is a different program object": interface BLOCK
|
||||||
// now hold. Runs on every draw through a pipeline, so the common case is the version
|
// bindings. glUniformBlockBinding and glShaderStorageBlockBinding place a block on a
|
||||||
// compare below and nothing else.
|
// binding point, and they do it per program - so a pipeline whose blocks were placed
|
||||||
|
// that way drew against the composite's own bindings, which come from the shader
|
||||||
|
// declarations alone. A block declared without any layout(binding) therefore sat on
|
||||||
|
// whatever the declaration implied while the application's buffers sat somewhere else,
|
||||||
|
// and nothing anywhere raised an error: the draw simply read or wrote the wrong place.
|
||||||
|
//
|
||||||
|
// Both sides seed these from the same shader declarations at link, so mirroring a block
|
||||||
|
// the application never rebound writes back the value the destination already holds and
|
||||||
|
// the setters' equality checks make it free.
|
||||||
|
static void MirrorBlockBindings(const ProgramObject& source, ProgramObject& destination) {
|
||||||
|
// Storage blocks are keyed by GL name on both sides - the one coordinate the
|
||||||
|
// frontend, SPIR-V and driver index spaces all agree on - so this is a direct
|
||||||
|
// replay. Empty for the overwhelming majority of programs.
|
||||||
|
for (const auto& [blockName, binding] : source.GetShaderStorageBlockBindingOverrides()) {
|
||||||
|
if (binding < 0) continue;
|
||||||
|
destination.SetShaderStorageBlockBinding(blockName, static_cast<Uint>(binding));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uniform blocks are keyed by index, and the two programs number them
|
||||||
|
// independently, so they are matched by name.
|
||||||
|
const Int sourceBlockCount = source.GetActiveUniformBlocksCount();
|
||||||
|
for (Int sourceIndex = 0; sourceIndex < sourceBlockCount; ++sourceIndex) {
|
||||||
|
const Int binding = static_cast<Int>(source.GetUniformBlockBinding(static_cast<Uint>(sourceIndex)));
|
||||||
|
// -1 is "no declared binding and never rebound" - there is nothing to carry,
|
||||||
|
// and forwarding it would land as binding 0xFFFFFFFF.
|
||||||
|
if (binding < 0) continue;
|
||||||
|
const String& blockName = source.GetUniformBlockName(static_cast<Uint>(sourceIndex));
|
||||||
|
if (blockName.empty()) continue;
|
||||||
|
const Uint destinationIndex = destination.GetUniformBlockIndex(blockName.c_str());
|
||||||
|
if (destinationIndex == 0xFFFFFFFFu) continue; // GL_INVALID_INDEX
|
||||||
|
destination.SetUniformBlockBinding(destinationIndex, static_cast<Uint>(binding));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Brings the pipeline's composite up to date with the per-program state its stage
|
||||||
|
// programs hold and it does not: uniform values, and interface block bindings. Runs on
|
||||||
|
// every draw through a pipeline, so the common case is the version compare below and
|
||||||
|
// nothing else.
|
||||||
static void RefreshCompositeUniforms(ProgramPipelineObject& pipeline, const SharedPtr<ProgramObject>& composite) {
|
static void RefreshCompositeUniforms(ProgramPipelineObject& pipeline, const SharedPtr<ProgramObject>& composite) {
|
||||||
if (!composite) return;
|
if (!composite) return;
|
||||||
const auto versions = pipeline.ComputeUniformMirrorVersions();
|
const auto versions = pipeline.ComputeUniformMirrorVersions();
|
||||||
@@ -483,6 +520,7 @@ namespace MobileGL::MG_State {
|
|||||||
if (alreadyMirrored) continue;
|
if (alreadyMirrored) continue;
|
||||||
mirrored[mirroredCount++] = stageProgram.get();
|
mirrored[mirroredCount++] = stageProgram.get();
|
||||||
MirrorUniformValues(*stageProgram, *composite);
|
MirrorUniformValues(*stageProgram, *composite);
|
||||||
|
MirrorBlockBindings(*stageProgram, *composite);
|
||||||
}
|
}
|
||||||
pipeline.SetMirroredUniformVersions(versions);
|
pipeline.SetMirroredUniformVersions(versions);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -613,13 +613,23 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return (ubo.stages & stageMask) != 0;
|
return (ubo.stages & stageMask) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set by glUniformBlockBinding
|
// Bumped by both block-binding setters below. A program pipeline's flattened composite
|
||||||
|
// is a different program object from the stage programs the application rebinds blocks
|
||||||
|
// on, so it has to be told - and this is what tells it something is worth re-reading.
|
||||||
|
// Separate from m_backendStateVersion because the storage-block setter deliberately
|
||||||
|
// does not disturb that one (see SetShaderStorageBlockBinding).
|
||||||
|
Uint32 GetBlockBindingVersion() const { return m_blockBindingVersion; }
|
||||||
|
|
||||||
|
// Set by glUniformBlockBinding. The vector is seeded at link with each block's DECLARED
|
||||||
|
// binding (layout(binding=N), else -1), so an untouched program already reports what its
|
||||||
|
// shaders asked for.
|
||||||
void SetUniformBlockBinding(Uint index, Uint binding) {
|
void SetUniformBlockBinding(Uint index, Uint binding) {
|
||||||
if (index >= Artifacts().uniformBlockBinding.size() || Artifacts().uniformBlockBinding[index] == static_cast<Int>(binding)) {
|
if (index >= Artifacts().uniformBlockBinding.size() || Artifacts().uniformBlockBinding[index] == static_cast<Int>(binding)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Artifacts().uniformBlockBinding[index] = static_cast<Int>(binding);
|
Artifacts().uniformBlockBinding[index] = static_cast<Int>(binding);
|
||||||
++m_backendStateVersion;
|
++m_backendStateVersion;
|
||||||
|
++m_blockBindingVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint GetUniformBlockBinding(Uint index) const { return Artifacts().uniformBlockBinding[index]; }
|
Uint GetUniformBlockBinding(Uint index) const { return Artifacts().uniformBlockBinding[index]; }
|
||||||
@@ -631,6 +641,10 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// means "never rebound", and the shader's declared binding still stands.
|
// means "never rebound", and the shader's declared binding still stands.
|
||||||
void SetShaderStorageBlockBinding(const String& blockName, Uint binding) {
|
void SetShaderStorageBlockBinding(const String& blockName, Uint binding) {
|
||||||
Artifacts().shaderStorageBlockBinding[blockName] = static_cast<Int>(binding);
|
Artifacts().shaderStorageBlockBinding[blockName] = static_cast<Int>(binding);
|
||||||
|
// Deliberately NOT m_backendStateVersion: Espryt's entry point never forces a
|
||||||
|
// program build off this, and bumping that version would start doing so. The
|
||||||
|
// dedicated counter carries the news to the pipeline composite instead.
|
||||||
|
++m_blockBindingVersion;
|
||||||
}
|
}
|
||||||
// -1 when the block has never been rebound. `blockName` is the interface-query
|
// -1 when the block has never been rebound. `blockName` is the interface-query
|
||||||
// spelling; an arrayed block's elements ("B[0]", "B[1]") are separate GL resources
|
// spelling; an arrayed block's elements ("B[0]", "B[1]") are separate GL resources
|
||||||
@@ -1059,6 +1073,8 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// READ-side operation (the first gated getter is what pulls the result in), and the
|
// READ-side operation (the first gated getter is what pulls the result in), and the
|
||||||
// publish has to bump these. Still GL-thread-only - a worker never touches them.
|
// publish has to bump these. Still GL-thread-only - a worker never touches them.
|
||||||
mutable Uint32 m_backendStateVersion = 0;
|
mutable Uint32 m_backendStateVersion = 0;
|
||||||
|
// Interface-block binding generation; see GetBlockBindingVersion.
|
||||||
|
Uint32 m_blockBindingVersion = 0;
|
||||||
|
|
||||||
// Backend-owned content-hash memo (see GetBackendHashMemo): valid only while
|
// Backend-owned content-hash memo (see GetBackendHashMemo): valid only while
|
||||||
// m_backendStateVersion matches. Several slots, not one: a backend may resolve the same
|
// m_backendStateVersion matches. Several slots, not one: a backend may resolve the same
|
||||||
|
|||||||
@@ -94,24 +94,27 @@ namespace MobileGL {
|
|||||||
return signature;
|
return signature;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uniform values are written to the STAGE programs - glUniform* addresses the
|
// Per-program state is written to the STAGE programs - glUniform* addresses the
|
||||||
// pipeline's active program (GL 4.6 core 7.6.1) and glProgramUniform* addresses
|
// pipeline's active program (GL 4.6 core 7.6.1), glProgramUniform* addresses a
|
||||||
// a named one - while the draw reads the composite. Two different objects'
|
// named one, and the two block-binding calls address a named one - while the
|
||||||
// storage, so the composite is refreshed from its stage programs before each
|
// draw reads the composite. Two different objects' state, so the composite is
|
||||||
// draw that needs it. These are the per-stage versions "needs it" is measured
|
// refreshed from its stage programs before each draw that needs it. These are
|
||||||
// against: the stage program's uniform-shadow content version in the low half
|
// the per-stage versions "needs it" is measured against. All zero after a
|
||||||
// and its backend state version (which the opaque/sampler-unit writes bump) in
|
// rebuild, because a fresh composite holds only what its shaders declared and
|
||||||
// the high half. All zero after a rebuild, because a fresh composite starts at
|
// so needs a full refresh.
|
||||||
// GL's zero defaults and so needs a full refresh.
|
using UniformMirrorVersions = Array<Uint64, kGraphicsStageCount * 2>;
|
||||||
using UniformMirrorVersions = Array<Uint64, kGraphicsStageCount>;
|
|
||||||
|
|
||||||
UniformMirrorVersions ComputeUniformMirrorVersions() const {
|
UniformMirrorVersions ComputeUniformMirrorVersions() const {
|
||||||
UniformMirrorVersions versions{};
|
UniformMirrorVersions versions{};
|
||||||
for (SizeT stage = 0; stage < kGraphicsStageCount; ++stage) {
|
for (SizeT stage = 0; stage < kGraphicsStageCount; ++stage) {
|
||||||
const auto& program = m_stagePrograms[stage];
|
const auto& program = m_stagePrograms[stage];
|
||||||
if (!program) continue;
|
if (!program) continue;
|
||||||
versions[stage] = (static_cast<Uint64>(program->GetBackendStateVersion()) << 32) |
|
versions[stage * 2] = (static_cast<Uint64>(program->GetBackendStateVersion()) << 32) |
|
||||||
static_cast<Uint64>(program->GetUBOContentVersion());
|
static_cast<Uint64>(program->GetUBOContentVersion());
|
||||||
|
// Its own slot rather than folded into the pair above: the storage-block
|
||||||
|
// setter moves this and NOTHING else, so a rebinding would otherwise be
|
||||||
|
// invisible to the refresh gate.
|
||||||
|
versions[stage * 2 + 1] = program->GetBlockBindingVersion();
|
||||||
}
|
}
|
||||||
return versions;
|
return versions;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user