[Fix, Test] (MG_State, MG_Impl): glIsProgramPipeline answers for the first bind, not for the materialization every pipeline command now does

This commit is contained in:
2026-08-11 20:10:18 -04:00
parent 43bcd03dca
commit 94e75fef79
6 changed files with 106 additions and 10 deletions
+29 -4
View File
@@ -383,6 +383,15 @@ namespace MobileGL::MG_State {
//
// Location-by-location so that arrays are carried across whole, and via the padded
// storage span so a mat3's std140 column padding travels with it.
//
// KNOWN LIMIT, inherent to flattening rather than to this copy: SSO gives each stage
// program its own storage for a uniform, so two stage programs may declare the same
// name and hold different values - but the composite is one link and has one slot for
// it. RefreshCompositeUniforms walks the stages in order, so the last graphics stage
// that declares the name wins, including when it is only holding the zero default and
// an earlier stage held a written value. Fixing it properly means mirroring only the
// uniforms a program has actually been written to, which wants a per-location dirty
// set on ProgramObject.
static void MirrorUniformValues(ProgramObject& source, ProgramObject& destination) {
if (!source.GetLinkStatus() || !destination.GetLinkStatus()) return;
const char* sourceUbo = static_cast<const char*>(source.GetUBOData());
@@ -1062,26 +1071,42 @@ namespace MobileGL::MG_State {
// Program pipeline
void GLContext::GenProgramPipelineNames(Uint number, Vector<Uint>& pipelines) {
pipelines.resize(number);
// Names only: glIsProgramPipeline must answer GL_FALSE until one is bound or created.
// Names only. The OBJECT appears as soon as a command needs somewhere to put state
// (see MaterializeProgramPipelineObject), but glIsProgramPipeline still answers
// GL_FALSE until the name is bound or created - see IsProgramPipelineObject.
m_programPipelineNames.Generate(number, pipelines.data());
}
void GLContext::CreateProgramPipelineObject(Uint index) {
m_programPipelines[index] = MakeShared<ProgramPipelineObject>(index);
const auto object = MakeShared<ProgramPipelineObject>(index);
// glCreateProgramPipelines makes the object outright, so it answers
// glIsProgramPipeline immediately - unlike a name that only got here through
// GenProgramPipelines plus a command that materialized it.
object->MarkEverBound();
m_programPipelines[index] = object;
}
Bool GLContext::ValidateProgramPipelineName(Uint index) const {
return index == 0 || m_programPipelineNames.IsValid(index);
}
// glIsProgramPipeline. Materialization is NOT the test: the object now appears as soon
// as any command takes state from a reserved name, and two of those commands are the
// pure queries glGetProgramPipelineiv / glGetProgramPipelineInfoLog - so keying this on
// map membership would let merely READING a gen'd name turn it into an object. GL 4.6
// core 7.4 gives the real rule: a GenProgramPipelines name acquires program pipeline
// state when it is first bound. Same shape as IsTransformFeedbackObject.
Bool GLContext::IsProgramPipelineObject(Uint index) const {
if (index == 0 || !m_programPipelineNames.IsValid(index)) return false;
return m_programPipelines.find(index) != m_programPipelines.end();
const auto it = m_programPipelines.find(index);
return it != m_programPipelines.end() && it->second && it->second->GetEverBound();
}
void GLContext::BindProgramPipelineObject(Uint index) {
if (index != 0) {
MaterializeProgramPipelineObject(index);
if (const auto& object = MaterializeProgramPipelineObject(index)) {
object->MarkEverBound();
}
}
m_boundProgramPipeline = index;
}
+4 -2
View File
@@ -457,8 +457,10 @@ namespace MobileGL {
UnorderedMap<Uint, TransformFeedbackObjectState> m_transformFeedbackObjects;
IndexGenerator<Uint> m_transformFeedbackNames;
Uint m_boundTransformFeedback = 0;
// Map membership IS object existence here: a pipeline has no stateful default
// object 0, so no everBound flag is needed.
// Map membership is object EXISTENCE, which is not the same as the answer
// glIsProgramPipeline gives: any command that needs somewhere to put state
// materializes a reserved name, so the object can exist well before it is
// bound. ProgramPipelineObject::everBound carries the Is* answer.
UnorderedMap<Uint, SharedPtr<ProgramPipelineObject>> m_programPipelines;
IndexGenerator<Uint> m_programPipelineNames;
Uint m_boundProgramPipeline = 0;
@@ -40,6 +40,17 @@ namespace MobileGL {
Uint GetExternalIndex() const { return m_externalIndex; }
// glIsProgramPipeline's answer, and NOT the same question as "does this object
// exist" (GL 4.6 core 7.4: a GenProgramPipelines name "acquires program pipeline
// state only when first bound"). The object is materialized by any of the
// commands that take state from a reserved name - including the pure queries
// glGetProgramPipelineiv and glGetProgramPipelineInfoLog, which have to answer
// out of default state without ever making the name report as an object. So
// existence is map membership and this is a separate latch, exactly as
// TransformFeedbackObject::everBound is.
Bool GetEverBound() const { return m_everBound; }
void MarkEverBound() { m_everBound = true; }
// The stages a DRAW is built from: every stage but compute. GL 4.6 core 7.4
// makes the compute stage exclusive - a program object containing a compute
// shader may contain no other stage, and a pipeline's compute stage is
@@ -56,7 +67,17 @@ namespace MobileGL {
// GRAPHICS 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. It covers
// object and its backend state version pins the link generation.
//
// The backend state version is BLUNTER than that description: glUniform1i on a
// sampler and glUniformBlockBinding bump it too, so either one throws the
// composite away and relinks it on the next draw. That is correct but slow, and
// it is a shape the SSO conformance cases hit in a loop. Narrowing it to
// GetLinkVersion() means the composite must instead pick those two up the way it
// picks up uniform values (below) - the sampler half already works that way,
// the block-binding half does not yet, which is why this still keys on the
// blunter version.
// It covers
// exactly the stages the composite is built from, so attaching or relinking a
// compute stage never invalidates a perfectly good graphics composite - and the
// compute stage, having no composite of its own, can never collide with it.
@@ -120,6 +141,7 @@ namespace MobileGL {
String m_infoLog;
const Uint m_externalIndex = 0;
Bool m_validateStatus = false;
Bool m_everBound = false;
};
} // namespace GLState
} // namespace MG_State