mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
[Fix, Test] (MG_State, MG_Impl, MG_Backend): a program pipeline's compute stage is dispatched on its own, and the graphics composite draws its stage programs' uniform values
This commit is contained in:
@@ -369,6 +369,115 @@ namespace MobileGL::MG_State {
|
||||
return m_programState.GetCurrentProgram();
|
||||
}
|
||||
|
||||
// Copies every default-block uniform value `source` holds into the same-named uniform of
|
||||
// `destination`, by name and by location.
|
||||
//
|
||||
// The composite a pipeline draws through is a DIFFERENT program object from the stage
|
||||
// programs the application writes uniforms to - glUniform* addresses the pipeline's
|
||||
// active program and glProgramUniform* addresses a named one, neither of which is the
|
||||
// composite - so without this a pipeline draw reads the composite's zero defaults and
|
||||
// paints them. Values are COPIED rather than aliased: the two programs' global UBOs are
|
||||
// laid out independently (the composite merges several stages' uniforms into one block,
|
||||
// so the same uniform sits at a different offset in each), and a copy also means the
|
||||
// composite can outlive a stage program without ever pointing into freed storage.
|
||||
//
|
||||
// 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.
|
||||
static void MirrorUniformValues(ProgramObject& source, ProgramObject& destination) {
|
||||
if (!source.GetLinkStatus() || !destination.GetLinkStatus()) return;
|
||||
const char* sourceUbo = static_cast<const char*>(source.GetUBOData());
|
||||
char* destinationUbo = static_cast<char*>(destination.MapUBO());
|
||||
const SizeT sourceUboSize = source.GetUBOSize();
|
||||
const SizeT destinationUboSize = destination.GetUBOSize();
|
||||
|
||||
const Uint uniformCount = source.GetUniformCount();
|
||||
for (Uint index = 0; index < uniformCount; ++index) {
|
||||
const String& name = source.GetActiveUniformName(index);
|
||||
if (name.empty()) continue;
|
||||
const Int sourceBase = source.GetUniformLocation(name);
|
||||
const Int destinationBase = destination.GetUniformLocation(name);
|
||||
// A uniform the composite's own link dropped (or renamed) is simply not
|
||||
// mirrored; the draw cannot read what does not exist.
|
||||
if (sourceBase < 0 || destinationBase < 0) continue;
|
||||
|
||||
const GLint arraySize = source.GetActiveUniformArraySize(index);
|
||||
const Int elements = arraySize > 0 ? static_cast<Int>(arraySize) : 1;
|
||||
for (Int element = 0; element < elements; ++element) {
|
||||
const Int sourceLocation = sourceBase + element;
|
||||
const Int destinationLocation = destinationBase + element;
|
||||
if (!source.IsValidUniformLocation(sourceLocation) ||
|
||||
!destination.IsValidUniformLocation(destinationLocation)) {
|
||||
break;
|
||||
}
|
||||
// Stop at the end of EITHER side's array rather than walking onto the
|
||||
// neighbouring uniform of whichever program has the shorter one.
|
||||
if (!source.UniformLocationsAliasSameUniform(sourceBase, sourceLocation) ||
|
||||
!destination.UniformLocationsAliasSameUniform(destinationBase, destinationLocation)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const Bool sourceOpaque = source.IsUniformOpaqueAtLocation(sourceLocation);
|
||||
if (sourceOpaque != destination.IsUniformOpaqueAtLocation(destinationLocation)) break;
|
||||
if (sourceOpaque) {
|
||||
// A sampler/image unit is phase-A state, not UBO bytes. The setter
|
||||
// itself is a no-op when the value already matches, so this does not
|
||||
// churn the composite's backend state version.
|
||||
destination.SetUniformSamplerOrImageUnitIndex(
|
||||
destinationLocation, source.GetUniformSamplerOrImageUnitIndex(sourceLocation));
|
||||
continue;
|
||||
}
|
||||
|
||||
const SizeT span = source.GetUniformStorageSpanInBytes(sourceLocation);
|
||||
if (span == 0 || span != destination.GetUniformStorageSpanInBytes(destinationLocation)) continue;
|
||||
const Uint sourceOffset = source.GetUniformOffset(sourceLocation);
|
||||
const Uint destinationOffset = destination.GetUniformOffset(destinationLocation);
|
||||
// Either side can legitimately lack backing storage: the optimizer deletes a
|
||||
// uniform nothing reads, and a program whose SPIR-V phase settled cancelled
|
||||
// has no shadow at all. Both report kInvalidUniformOffset / a null shadow.
|
||||
if (sourceUbo == nullptr || destinationUbo == nullptr ||
|
||||
sourceOffset == ProgramObject::kInvalidUniformOffset ||
|
||||
destinationOffset == ProgramObject::kInvalidUniformOffset ||
|
||||
sourceOffset + span > sourceUboSize || destinationOffset + span > destinationUboSize) {
|
||||
continue;
|
||||
}
|
||||
if (std::memcmp(destinationUbo + destinationOffset, sourceUbo + sourceOffset, span) == 0) {
|
||||
continue;
|
||||
}
|
||||
Memcpy(destinationUbo + destinationOffset, sourceUbo + sourceOffset, span);
|
||||
destination.MarkUBOContentDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Brings the pipeline's composite up to date with the uniform values its stage programs
|
||||
// now hold. 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) {
|
||||
if (!composite) return;
|
||||
const auto versions = pipeline.ComputeUniformMirrorVersions();
|
||||
if (versions == pipeline.GetMirroredUniformVersions()) return;
|
||||
|
||||
// A program bound to two stages appears twice; mirroring it twice would be
|
||||
// idempotent but is still work, and the second pass would have nothing to do.
|
||||
Array<ProgramObject*, ProgramPipelineObject::kGraphicsStageCount> mirrored{};
|
||||
SizeT mirroredCount = 0;
|
||||
for (SizeT stage = 0; stage < ProgramPipelineObject::kGraphicsStageCount; ++stage) {
|
||||
const auto& stageProgram = pipeline.GetStageProgram(static_cast<ShaderStage>(stage));
|
||||
if (!stageProgram) continue;
|
||||
Bool alreadyMirrored = false;
|
||||
for (SizeT i = 0; i < mirroredCount; ++i) {
|
||||
if (mirrored[i] == stageProgram.get()) {
|
||||
alreadyMirrored = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (alreadyMirrored) continue;
|
||||
mirrored[mirroredCount++] = stageProgram.get();
|
||||
MirrorUniformValues(*stageProgram, *composite);
|
||||
}
|
||||
pipeline.SetMirroredUniformVersions(versions);
|
||||
}
|
||||
|
||||
const SharedPtr<ProgramObject>& GLContext::GetProgramForDraw() {
|
||||
static const SharedPtr<ProgramObject> nullProgram = nullptr;
|
||||
const auto& currentProgram = m_programState.GetCurrentProgram();
|
||||
@@ -402,13 +511,16 @@ namespace MobileGL::MG_State {
|
||||
// that will never be produced again: every draw would miss the cache and rebuild
|
||||
// (and relink) the composite. Join first, so the signature describes settled
|
||||
// programs. In steady state this is a null check per stage.
|
||||
for (SizeT stage = 0; stage < static_cast<SizeT>(ShaderStage::ShaderStageCount); ++stage) {
|
||||
for (SizeT stage = 0; stage < ProgramPipelineObject::kGraphicsStageCount; ++stage) {
|
||||
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
|
||||
if (stageProgram) stageProgram->JoinLinkAndSpirv();
|
||||
}
|
||||
|
||||
const auto signature = pipeline->ComputeDrawProgramSignature();
|
||||
if (const auto& cached = pipeline->GetCachedDrawProgram(signature)) return cached;
|
||||
if (const auto& cached = pipeline->GetCachedDrawProgram(signature)) {
|
||||
RefreshCompositeUniforms(*pipeline, cached);
|
||||
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
|
||||
@@ -420,8 +532,14 @@ namespace MobileGL::MG_State {
|
||||
// could otherwise be handed. Backend registries key on the object, not the name.
|
||||
auto composite = MakeShared<ProgramObject>(0u);
|
||||
|
||||
// GRAPHICS stages only. A pipeline may carry a compute stage alongside them (GL
|
||||
// 4.6 core 7.4 forbids linking compute WITH another stage into one program, not
|
||||
// attaching a compute program to a pipeline that also has graphics ones), and that
|
||||
// stage belongs to glDispatchCompute, not to this draw. Compositing it in produced
|
||||
// a graphics program carrying a compute module, which Adreno 830 does not reject
|
||||
// from vkCreateGraphicsPipelines - it SIGSEGVs inside it.
|
||||
Bool anyStage = false;
|
||||
for (SizeT stage = 0; stage < static_cast<SizeT>(ShaderStage::ShaderStageCount); ++stage) {
|
||||
for (SizeT stage = 0; stage < ProgramPipelineObject::kGraphicsStageCount; ++stage) {
|
||||
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
|
||||
if (!stageProgram) continue;
|
||||
for (const auto& shader : stageProgram->GetAttachedShaders()) {
|
||||
@@ -440,7 +558,32 @@ namespace MobileGL::MG_State {
|
||||
// for the same reason: the backend is about to read its SPIR-V.
|
||||
composite->JoinLinkAndSpirv();
|
||||
pipeline->SetCachedDrawProgram(signature, Move(composite));
|
||||
return pipeline->GetCachedDrawProgram(signature);
|
||||
const auto& cached = pipeline->GetCachedDrawProgram(signature);
|
||||
RefreshCompositeUniforms(*pipeline, cached);
|
||||
return cached;
|
||||
}
|
||||
|
||||
const SharedPtr<ProgramObject>& GLContext::GetProgramForDispatch() {
|
||||
static const SharedPtr<ProgramObject> nullProgram = nullptr;
|
||||
const auto& currentProgram = m_programState.GetCurrentProgram();
|
||||
if (currentProgram) {
|
||||
// Same join contract as GetProgramForDraw's glUseProgram half - see the note
|
||||
// there. A dispatch reads the same non-artifact versions a draw does.
|
||||
currentProgram->JoinLinkAndSpirv();
|
||||
return currentProgram;
|
||||
}
|
||||
if (m_boundProgramPipeline == 0) return nullProgram;
|
||||
const auto& pipeline = GetBoundProgramPipeline();
|
||||
if (!pipeline) return nullProgram;
|
||||
// No compositing and no cache: GL 4.6 core 7.4 makes a compute program exclusive of
|
||||
// every other stage, so the pipeline's compute stage program IS the program to
|
||||
// dispatch, uniforms and all. That also means glUniform* through the active program
|
||||
// lands on the very object the dispatch reads - the composite's uniform refresh has
|
||||
// no counterpart to do here.
|
||||
const auto& computeProgram = pipeline->GetStageProgram(ShaderStage::Compute);
|
||||
if (!computeProgram) return nullProgram;
|
||||
computeProgram->JoinLinkAndSpirv();
|
||||
return computeProgram;
|
||||
}
|
||||
|
||||
const SharedPtr<ProgramObject>& GLContext::GetProgramForUniform() {
|
||||
|
||||
@@ -163,9 +163,15 @@ namespace MobileGL {
|
||||
}
|
||||
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.
|
||||
// What a DRAW executes: the program in use, or - when there is none - the bound
|
||||
// pipeline's GRAPHICS stages composited into one program. A pipeline's compute
|
||||
// stage is never part of that composite; ask GetProgramForDispatch for it.
|
||||
const SharedPtr<ProgramObject>& GetProgramForDraw();
|
||||
// What a DISPATCH executes: the program in use, or - when there is none - the
|
||||
// bound pipeline's compute stage program itself. GL's compute stage is a whole
|
||||
// program on its own (GL 4.6 core 7.4: it may not be linked with any other
|
||||
// stage), so there is nothing to composite and no composite to cache.
|
||||
const SharedPtr<ProgramObject>& GetProgramForDispatch();
|
||||
// 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();
|
||||
|
||||
@@ -326,6 +326,20 @@ namespace MobileGL::MG_State::GLState {
|
||||
: kInvalidUniformOffset;
|
||||
}
|
||||
Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); }
|
||||
// Bytes a uniform actually occupies in the global UBO, which is not its GL type size:
|
||||
// std140 pads each column of a float matrix out to a vec4, so a mat3 spans 48 bytes
|
||||
// even though only 36 of them carry components. Anything reading or writing a whole
|
||||
// uniform's storage - a bounds check, a copy between two programs' shadows - wants
|
||||
// this rather than GetUniformSizesInBytes.
|
||||
static SizeT UniformStorageSpanInBytes(const glslang::TType* type, SizeT tightSize) {
|
||||
if (type != nullptr && type->isMatrix() && type->getBasicType() != glslang::EbtDouble) {
|
||||
return static_cast<SizeT>(type->getMatrixCols()) * 4 * sizeof(Float);
|
||||
}
|
||||
return tightSize;
|
||||
}
|
||||
SizeT GetUniformStorageSpanInBytes(Uint location) const {
|
||||
return UniformStorageSpanInBytes(GetUniformTType(location), GetUniformSizesInBytes(location));
|
||||
}
|
||||
|
||||
Int GetAttributeLocation(const String& name) {
|
||||
const auto it = std::find(Artifacts().attribs.begin(), Artifacts().attribs.end(), name);
|
||||
|
||||
@@ -40,17 +40,31 @@ namespace MobileGL {
|
||||
|
||||
Uint GetExternalIndex() const { return m_externalIndex; }
|
||||
|
||||
// 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
|
||||
// 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>;
|
||||
// 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
|
||||
// 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.
|
||||
using DrawProgramSignature = Array<Uint64, kGraphicsStageCount * 2>;
|
||||
|
||||
DrawProgramSignature ComputeDrawProgramSignature() const {
|
||||
DrawProgramSignature signature{};
|
||||
for (SizeT stage = 0; stage < static_cast<SizeT>(ShaderStage::ShaderStageCount); ++stage) {
|
||||
for (SizeT stage = 0; stage < kGraphicsStageCount; ++stage) {
|
||||
const auto& program = m_stagePrograms[stage];
|
||||
if (!program) continue;
|
||||
signature[stage * 2] = program->GetLifetimeId();
|
||||
@@ -59,6 +73,32 @@ namespace MobileGL {
|
||||
return signature;
|
||||
}
|
||||
|
||||
// Uniform values are written to the STAGE programs - glUniform* addresses the
|
||||
// pipeline's active program (GL 4.6 core 7.6.1) and glProgramUniform* addresses
|
||||
// a named one - while the draw reads the composite. Two different objects'
|
||||
// storage, 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: the stage program's uniform-shadow content version in the low half
|
||||
// and its backend state version (which the opaque/sampler-unit writes bump) in
|
||||
// the high half. All zero after a rebuild, because a fresh composite starts at
|
||||
// GL's zero defaults and so needs a full refresh.
|
||||
using UniformMirrorVersions = Array<Uint64, kGraphicsStageCount>;
|
||||
|
||||
UniformMirrorVersions ComputeUniformMirrorVersions() const {
|
||||
UniformMirrorVersions versions{};
|
||||
for (SizeT stage = 0; stage < kGraphicsStageCount; ++stage) {
|
||||
const auto& program = m_stagePrograms[stage];
|
||||
if (!program) continue;
|
||||
versions[stage] = (static_cast<Uint64>(program->GetBackendStateVersion()) << 32) |
|
||||
static_cast<Uint64>(program->GetUBOContentVersion());
|
||||
}
|
||||
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;
|
||||
@@ -67,6 +107,8 @@ namespace MobileGL {
|
||||
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:
|
||||
@@ -74,6 +116,7 @@ namespace MobileGL {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user