[Feat] (MG_State, MG_Impl, MG_Backend): let a bound program pipeline actually draw

The pipeline object bookkeeping landed already - names, stage slots, queries -
but nothing consumed it. Every draw asked the context for the current program,
got null because a pipeline is used with program zero, and drew nothing;
glCreateShaderProgramv was still a stub returning zero, so
direct_state_access.program_pipelines_functional could not even build its stage
programs and reported InternalError on both backends.

glCreateShaderProgramv is written as the exact call sequence the spec defines it
to be, with one deviation that matters: the link goes straight to
ProgramObject::Link(false) rather than through LinkProgram, because LinkProgram
injects a default fragment shader into a program that has none - correct for a
whole program, wrong for a separable vertex-stage one whose fragment stage comes
from the pipeline. glDetachShader defers removal to the next link, so the program
keeps the shader object it was built from while correctly no longer reporting it
attached. GL_PROGRAM_SEPARABLE joins glProgramParameteri and glGetProgramiv.

Everything downstream of a draw - both backends, the uniform plumbing, the draw
validation - is written against one linked program, so rather than teach all of
it about stages, the pipeline is flattened: GetProgramForDraw() composites the
stage programs' shaders into a single hidden program object and caches it against
a signature of each stage program's lifetime id and link generation, so it is
rebuilt exactly when a stage or a stage's link changes. The composite carries no
GL name - it must not answer glIsProgram, and it must not consume a name the
application could be handed.

Uniform entry points get their own resolver rather than sharing that one:
glUniform* addresses the pipeline's active program, not the composited draw
program. GL_CURRENT_PROGRAM still reads the program in use, which is zero here.

Fixes program_pipelines_functional on both backends.
This commit is contained in:
BZLZHH
2026-08-05 07:20:42 -04:00
parent 5545d31c37
commit 4ce808b9f2
10 changed files with 172 additions and 30 deletions
+49
View File
@@ -343,6 +343,55 @@ namespace MobileGL::MG_State {
return m_programState.GetCurrentProgram();
}
const SharedPtr<ProgramObject>& GLContext::GetProgramForDraw() {
static const SharedPtr<ProgramObject> nullProgram = nullptr;
const auto& currentProgram = m_programState.GetCurrentProgram();
if (currentProgram) return currentProgram;
if (m_boundProgramPipeline == 0) return nullProgram;
const auto& pipeline = GetBoundProgramPipeline();
if (!pipeline) return nullProgram;
const auto signature = pipeline->ComputeDrawProgramSignature();
if (const auto& cached = pipeline->GetCachedDrawProgram(signature)) return cached;
// Everything downstream of here - the backends, the uniform plumbing, the draw
// validation - is written against a single linked program, so the pipeline is
// flattened into one. Each stage contributes only the shaders that serve it, so a
// program bound to two stages is not pulled in twice and a program bound to a
// stage it does not implement contributes nothing.
// Deliberately not a named program: it is reachable only through the pipeline, it
// must not answer glIsProgram, and it must not consume a name the application
// could otherwise be handed. Backend registries key on the object, not the name.
auto composite = MakeShared<ProgramObject>(0u);
Bool anyStage = false;
for (SizeT stage = 0; stage < static_cast<SizeT>(ShaderStage::ShaderStageCount); ++stage) {
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
if (!stageProgram) continue;
for (const auto& shader : stageProgram->GetAttachedShaders()) {
if (!shader || static_cast<SizeT>(shader->GetShaderStage()) != stage) continue;
composite->AttachShader(shader);
anyStage = true;
}
}
if (!anyStage) return nullProgram;
// A pipeline with no fragment stage still rasterises, so the default fragment
// shader is wanted here even though the separable stage programs never get one.
composite->Link(true);
pipeline->SetCachedDrawProgram(signature, Move(composite));
return pipeline->GetCachedDrawProgram(signature);
}
const SharedPtr<ProgramObject>& GLContext::GetProgramForUniform() {
const auto& currentProgram = m_programState.GetCurrentProgram();
if (currentProgram) return currentProgram;
static const SharedPtr<ProgramObject> nullProgram = nullptr;
if (m_boundProgramPipeline == 0) return nullProgram;
const auto& pipeline = GetBoundProgramPipeline();
if (!pipeline) return nullProgram;
return pipeline->GetActiveProgram();
}
// RenderState
Uint GLContext::GetRenderStateParametersVersion() const {
return m_renderState.GetVersion();
+6
View File
@@ -137,6 +137,12 @@ namespace MobileGL {
const SharedPtr<ShaderObject>& GetShaderObject(Uint index);
void UseProgram(Uint program);
const SharedPtr<ProgramObject>& GetCurrentProgram();
// What a draw or dispatch actually executes: the program in use, or - when
// there is none - the bound pipeline's stages composited into one program.
const SharedPtr<ProgramObject>& GetProgramForDraw();
// What glUniform* addresses: the program in use, or the bound pipeline's
// active program (GL 4.6 core 7.6.1).
const SharedPtr<ProgramObject>& GetProgramForUniform();
// Program pipeline (GL_ARB_separate_shader_objects, GL 4.6 core 7.4). Like queries
// and transform feedbacks, glGenProgramPipelines only RESERVES a name - the object
@@ -45,6 +45,13 @@ namespace MobileGL::MG_State::GLState {
Vector<SharedPtr<ShaderObject>>& GetAttachedShaders();
const Vector<SharedPtr<ShaderObject>>& GetAttachedShaders() const;
const String& GetInfoLog() const { return m_infoLog; }
// glCreateShaderProgramv folds the shader's compile log into the program's log, which
// is the only place a caller can read it from once the shader name is gone.
void AppendInfoLog(const String& text) {
if (text.empty()) return;
if (!m_infoLog.empty() && m_infoLog.back() != '\n') m_infoLog += '\n';
m_infoLog += text;
}
Int GetUniformMaxLength() const { return m_uniformNameMaxLength; }
Uint GetUniformCount() const { return m_activeUniformCount; }
Uint GetMaxUniformLocation() const { return m_maxUniformLocation; }
@@ -382,6 +389,11 @@ namespace MobileGL::MG_State::GLState {
// ARB_get_program_binary requires of it.
Bool GetBinaryRetrievableHint() const { return m_binaryRetrievableHint; }
void SetBinaryRetrievableHint(Bool hint) { m_binaryRetrievableHint = hint; }
// GL_PROGRAM_SEPARABLE (GL_ARB_separate_shader_objects): the program may supply a
// subset of the stages of a program pipeline. Only takes effect on the next link,
// which is why it is plain state here rather than something Link() consults.
Bool GetSeparable() const { return m_separable; }
void SetSeparable(Bool separable) { m_separable = separable; }
// glProgramBinary always fails here (there is no format it could accept) and the
// spec then requires the program's LINK_STATUS to read FALSE.
void MarkLinkFailedByProgramBinary() {
@@ -605,6 +617,7 @@ namespace MobileGL::MG_State::GLState {
Bool m_deleteStatus = false;
Bool m_linkStatus = false;
Bool m_binaryRetrievableHint = false;
Bool m_separable = false;
Bool m_validateStatus = true;
Uint32 m_backendStateVersion = 0;
@@ -40,9 +40,40 @@ namespace MobileGL {
Uint GetExternalIndex() const { return m_externalIndex; }
// A draw sees one program, but a pipeline holds one program per stage. The
// stages are composited into a single hidden program object, rebuilt whenever
// the stage set - or any stage program's own link - changes. The signature is
// what that "changes" means: a stage program's lifetime id pins the object and
// its backend state version pins the link generation.
using DrawProgramSignature =
Array<Uint64, static_cast<SizeT>(ShaderStage::ShaderStageCount) * 2>;
DrawProgramSignature ComputeDrawProgramSignature() const {
DrawProgramSignature signature{};
for (SizeT stage = 0; stage < static_cast<SizeT>(ShaderStage::ShaderStageCount); ++stage) {
const auto& program = m_stagePrograms[stage];
if (!program) continue;
signature[stage * 2] = program->GetLifetimeId();
signature[stage * 2 + 1] = program->GetBackendStateVersion();
}
return signature;
}
const SharedPtr<ProgramObject>& GetCachedDrawProgram(const DrawProgramSignature& signature) const {
static const SharedPtr<ProgramObject> nullProgram = nullptr;
if (!m_drawProgram || m_drawProgramSignature != signature) return nullProgram;
return m_drawProgram;
}
void SetCachedDrawProgram(const DrawProgramSignature& signature, SharedPtr<ProgramObject> program) {
m_drawProgramSignature = signature;
m_drawProgram = Move(program);
}
private:
Array<SharedPtr<ProgramObject>, static_cast<SizeT>(ShaderStage::ShaderStageCount)> m_stagePrograms{};
SharedPtr<ProgramObject> m_activeProgram;
SharedPtr<ProgramObject> m_drawProgram;
DrawProgramSignature m_drawProgramSignature{};
String m_infoLog;
const Uint m_externalIndex = 0;
Bool m_validateStatus = false;