mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
175 lines
11 KiB
C++
175 lines
11 KiB
C++
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramPipelineObject.h
|
|
// 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
|
|
|
|
#pragma once
|
|
#include <Includes.h>
|
|
#include "ProgramObject.h"
|
|
|
|
namespace MobileGL {
|
|
namespace MG_State {
|
|
namespace GLState {
|
|
// A GL_ARB_separate_shader_objects program pipeline (GL 4.6 core 7.4): a set of
|
|
// per-stage program references plus the program glProgramUniform* addresses when no
|
|
// program object is in use.
|
|
class ProgramPipelineObject {
|
|
public:
|
|
explicit ProgramPipelineObject(Uint externalIndex) : m_externalIndex(externalIndex) {}
|
|
|
|
const SharedPtr<ProgramObject>& GetStageProgram(ShaderStage stage) const {
|
|
return m_stagePrograms[static_cast<SizeT>(stage)];
|
|
}
|
|
void SetStageProgram(ShaderStage stage, const SharedPtr<ProgramObject>& program) {
|
|
m_stagePrograms[static_cast<SizeT>(stage)] = program;
|
|
}
|
|
|
|
const SharedPtr<ProgramObject>& GetActiveProgram() const { return m_activeProgram; }
|
|
void SetActiveProgram(const SharedPtr<ProgramObject>& program) { m_activeProgram = program; }
|
|
|
|
// Distinct from ProgramObject's, which starts true: a pipeline that has never been
|
|
// validated must report GL_VALIDATE_STATUS as 0 (GL 4.6 core table 23.31).
|
|
Bool GetValidateStatus() const { return m_validateStatus; }
|
|
void SetValidateStatus(Bool value) { m_validateStatus = value; }
|
|
|
|
const String& GetInfoLog() const { return m_infoLog; }
|
|
void SetInfoLog(String log) { m_infoLog = Move(log); }
|
|
|
|
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
|
|
// dispatched on its own and never participates in a draw. So the compute stage
|
|
// is not merely irrelevant to the composite below, it must never enter it: a
|
|
// compute module handed to vkCreateGraphicsPipelines is a driver crash rather
|
|
// than an error return (Adreno 830 SIGSEGVs inside it).
|
|
static constexpr SizeT kGraphicsStageCount = static_cast<SizeT>(ShaderStage::Compute);
|
|
static_assert(static_cast<SizeT>(ShaderStage::Compute) + 1 ==
|
|
static_cast<SizeT>(ShaderStage::ShaderStageCount),
|
|
"ShaderStage must keep Compute last so the graphics stages are a prefix");
|
|
|
|
// A draw sees one program, but a pipeline holds one program per stage. The
|
|
// 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 LINK version pins the link generation. 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.
|
|
//
|
|
// GetLinkVersion() and NOT GetBackendStateVersion(), which is what this used to
|
|
// key on. The backend state version moves on every glUniform1i to a sampler and
|
|
// every glUniformBlockBinding, so the "set a sampler unit, draw" loop that the
|
|
// SSO conformance cases run threw the composite away and REBUILT it on every
|
|
// single draw: a fresh ProgramObject, a full Link(true) settled synchronously
|
|
// (glslang + SPIR-V + spirv-opt), a full re-mirror, and a brand-new program
|
|
// identity that invalidated both backends' per-program registries and pipeline
|
|
// memos along the way. The composite's CONTENT depends on the link generations
|
|
// and nothing else, and m_linkVersion is bumped by exactly those
|
|
// (BumpLinkObservableVersions).
|
|
//
|
|
// The prerequisite that makes the narrowing legal: because the composite no
|
|
// longer rebuilds when per-program uniform STATE changes, every such change must
|
|
// reach it through the refresh below instead. Both do - sampler/image units via
|
|
// MirrorUniformValues, interface block bindings via MirrorBlockBindings - and
|
|
// the two setters that write them still bump the counters the REFRESH gate reads
|
|
// (see ComputeUniformMirrorVersions), which is a separate question from what
|
|
// this signature reads. They are the only two writers of m_backendStateVersion
|
|
// outside the link paths, so nothing else was ever riding on the rebuild.
|
|
using DrawProgramSignature = Array<Uint64, kGraphicsStageCount * 2>;
|
|
|
|
DrawProgramSignature ComputeDrawProgramSignature() const {
|
|
DrawProgramSignature signature{};
|
|
for (SizeT stage = 0; stage < kGraphicsStageCount; ++stage) {
|
|
const auto& program = m_stagePrograms[stage];
|
|
if (!program) continue;
|
|
signature[stage * 2] = program->GetLifetimeId();
|
|
signature[stage * 2 + 1] = program->GetLinkVersion();
|
|
}
|
|
return signature;
|
|
}
|
|
|
|
// Per-program state is written to the STAGE programs - glUniform* addresses the
|
|
// pipeline's active program (GL 4.6 core 7.6.1), glProgramUniform* addresses a
|
|
// named one, and the two block-binding calls address a named one - while the
|
|
// draw reads the composite. Two different objects' state, so the composite is
|
|
// refreshed from its stage programs before each draw that needs it. These are
|
|
// the per-stage versions "needs it" is measured against. All zero after a
|
|
// rebuild, because a fresh composite holds only what its shaders declared and
|
|
// so needs a full refresh.
|
|
//
|
|
// backendStateVersion belongs HERE even though ComputeDrawProgramSignature no
|
|
// longer reads it, and that is the whole point of the split: a sampler-unit or
|
|
// uniform-block-binding write must still trip the MIRROR (it is now the only
|
|
// route those values have to the composite) while deliberately NOT tripping the
|
|
// rebuild. uboContentVersion covers ordinary uniform writes, and
|
|
// blockBindingVersion covers the storage-block setter, which moves neither of
|
|
// the other two.
|
|
using UniformMirrorVersions = Array<Uint64, kGraphicsStageCount * 2>;
|
|
|
|
UniformMirrorVersions ComputeUniformMirrorVersions() const {
|
|
UniformMirrorVersions versions{};
|
|
for (SizeT stage = 0; stage < kGraphicsStageCount; ++stage) {
|
|
const auto& program = m_stagePrograms[stage];
|
|
if (!program) continue;
|
|
versions[stage * 2] = (static_cast<Uint64>(program->GetBackendStateVersion()) << 32) |
|
|
static_cast<Uint64>(program->GetUBOContentVersion());
|
|
// Their own slot rather than folded into the pair above: the
|
|
// storage-block setter moves the block-binding version and NOTHING
|
|
// else, so a rebinding would otherwise be invisible to the refresh
|
|
// gate - and the write-set version is the only counter that moves for
|
|
// a write which ENLARGES the set without changing a byte (see
|
|
// ProgramObject::GetUniformWriteSetVersion), which is what decides
|
|
// which stage owns a shared name.
|
|
versions[stage * 2 + 1] = (static_cast<Uint64>(program->GetBlockBindingVersion()) << 32) |
|
|
static_cast<Uint64>(program->GetUniformWriteSetVersion());
|
|
}
|
|
return versions;
|
|
}
|
|
const UniformMirrorVersions& GetMirroredUniformVersions() const { return m_mirroredUniformVersions; }
|
|
void SetMirroredUniformVersions(const UniformMirrorVersions& versions) {
|
|
m_mirroredUniformVersions = versions;
|
|
}
|
|
|
|
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);
|
|
// A rebuilt composite holds none of its stage programs' uniform values yet.
|
|
m_mirroredUniformVersions = {};
|
|
}
|
|
|
|
private:
|
|
Array<SharedPtr<ProgramObject>, static_cast<SizeT>(ShaderStage::ShaderStageCount)> m_stagePrograms{};
|
|
SharedPtr<ProgramObject> m_activeProgram;
|
|
SharedPtr<ProgramObject> m_drawProgram;
|
|
DrawProgramSignature m_drawProgramSignature{};
|
|
UniformMirrorVersions m_mirroredUniformVersions{};
|
|
String m_infoLog;
|
|
const Uint m_externalIndex = 0;
|
|
Bool m_validateStatus = false;
|
|
Bool m_everBound = false;
|
|
};
|
|
} // namespace GLState
|
|
} // namespace MG_State
|
|
} // namespace MobileGL
|