mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-10 05:08:31 +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:
@@ -3010,8 +3010,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// Single per-dispatch program resolve and texture-key capture, as in
|
||||
// PrepareForDraw (nothing below can move either).
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
// PrepareForDraw (nothing below can move either). The DISPATCH accessor: with a
|
||||
// pipeline bound this is its compute stage program, which is a whole program on its
|
||||
// own - the graphics composite a draw builds carries no compute stage.
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDispatch();
|
||||
const TextureImpl::DrawTextureSyncKeys textureKeys = TextureImpl::CaptureDrawTextureSyncKeys();
|
||||
|
||||
BufferImpl::SyncComputeBuffers(includeDispatchIndirectBuffer);
|
||||
|
||||
@@ -4582,7 +4582,6 @@ void main() {
|
||||
MGLOG_D("GetOrCreatePipeline skipped: program has no shader stages");
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
// Fast path: skip the full pipeline resolution when the pipeline state is unchanged from the
|
||||
// previous draw (the common intra-batch case). The key provably covers every
|
||||
// PipelineCreatePayload field: draw mode (topology + polygon-fill depth-bias gate), program
|
||||
@@ -6144,7 +6143,9 @@ void main() {
|
||||
void VulkanRenderer::DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) {
|
||||
m_textureManager->CollectGarbage();
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
const auto& program = *MG_State::pGLContext->GetProgramForDraw();
|
||||
// The DISPATCH accessor: with a pipeline bound this is its compute stage program
|
||||
// itself, never the graphics composite (which carries no compute stage at all).
|
||||
const auto& program = *MG_State::pGLContext->GetProgramForDispatch();
|
||||
if (!program.GetLinkStatus() || !program.GetSpirvStatus()) {
|
||||
MGLOG_E("DispatchCompute skipped: program=%u has no optimized SPIR-V",
|
||||
program.GetExternalIndex());
|
||||
@@ -6189,7 +6190,8 @@ void main() {
|
||||
void VulkanRenderer::DispatchComputeIndirect(GLintptr indirect) {
|
||||
m_textureManager->CollectGarbage();
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
const auto& program = *MG_State::pGLContext->GetProgramForDraw();
|
||||
// See DispatchCompute: the dispatch accessor, not the draw one.
|
||||
const auto& program = *MG_State::pGLContext->GetProgramForDispatch();
|
||||
if (!program.GetLinkStatus() || !program.GetSpirvStatus()) {
|
||||
MGLOG_E("DispatchComputeIndirect skipped: program=%u has no optimized SPIR-V",
|
||||
program.GetExternalIndex());
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
#include "../Getter/GL_Getter.h"
|
||||
|
||||
namespace MobileGL::MG_Impl::GLImpl {
|
||||
static Bool ValidateCurrentProgramForExecution(const char* functionName) {
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
static Bool ValidateProgramForExecution(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
|
||||
const char* functionName) {
|
||||
if (!currentProgram) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
@@ -34,10 +34,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
static Bool ValidateCurrentProgramForExecution(const char* functionName) {
|
||||
return ValidateProgramForExecution(MG_State::pGLContext->GetProgramForDraw(), functionName);
|
||||
}
|
||||
|
||||
// A dispatch resolves its program through the DISPATCH accessor: with a pipeline bound
|
||||
// that is the pipeline's compute stage program, not the graphics composite a draw would
|
||||
// build - which no longer contains a compute stage to find at all.
|
||||
static Bool ValidateCurrentProgramForCompute(const char* functionName) {
|
||||
if (!ValidateCurrentProgramForExecution(functionName)) return false;
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDispatch();
|
||||
if (!ValidateProgramForExecution(currentProgram, functionName)) return false;
|
||||
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
if (currentProgram->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
|
||||
@@ -863,12 +863,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
// Bytes a uniform actually occupies in the global UBO. It is the tight GL type size for
|
||||
// everything except a float matrix, whose padded columns make it wider.
|
||||
// everything except a float matrix, whose padded columns make it wider. The rule itself
|
||||
// lives on ProgramObject, because the pipeline composite's uniform refresh needs the same
|
||||
// one and two copies of a layout rule is one too many.
|
||||
SizeT UniformStorageSpanInBytes(const glslang::TType* ttype, SizeT tightSize) {
|
||||
if (ttype != nullptr && ttype->isMatrix() && ttype->getBasicType() != glslang::EbtDouble) {
|
||||
return static_cast<SizeT>(ttype->getMatrixCols()) * 4 * sizeof(GLfloat);
|
||||
}
|
||||
return tightSize;
|
||||
return MG_State::GLState::ProgramObject::UniformStorageSpanInBytes(ttype, tightSize);
|
||||
}
|
||||
|
||||
void GetUniform_State(GLuint program, GLint location, void* params) {
|
||||
|
||||
@@ -176,17 +176,15 @@ void main() { gl_Position = i_position; }
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// glActiveShaderProgram picks which stage program glUniform* addresses.
|
||||
// glActiveShaderProgram picks which stage program glUniform* addresses - and the draw has to
|
||||
// see what was written there.
|
||||
//
|
||||
// DISABLED: a second, independent defect, left failing on purpose rather than deleted. The
|
||||
// materialization fix above got the stages recorded and the pipeline drawing, but a uniform
|
||||
// set through the active shader program does not reach the flattened composite: the draw
|
||||
// paints u_color's default rather than the value written. GetProgramForUniform() returns the
|
||||
// pipeline's active program, while GetProgramForDraw() builds a SEPARATE composite object out
|
||||
// of the stage programs' shaders - so uniform values live on one object and the draw reads
|
||||
// another. Enable this the moment the composite inherits (or aliases) its stage programs'
|
||||
// uniform storage.
|
||||
TEST_F(ProgramPipelineScenario, DISABLED_UniformsGoToTheActiveShaderProgram) {
|
||||
// The second defect of the cluster, and the one the pixels expose most directly: uniform
|
||||
// values live on the stage program (GetProgramForUniform returns the pipeline's active
|
||||
// program) while the draw reads the composite GetProgramForDraw builds out of the stage
|
||||
// programs' shaders. Two objects, two sets of uniform storage; before the composite was
|
||||
// refreshed from its stage programs this painted u_color's zero default instead of green.
|
||||
TEST_F(ProgramPipelineScenario, UniformsGoToTheActiveShaderProgram) {
|
||||
if (!Ready()) return;
|
||||
|
||||
static const char* kUniformFS = R"(#version 430 core
|
||||
@@ -236,14 +234,15 @@ void main() { o_color = u_color; }
|
||||
// The sso-compute-pipeline shape: compute and non-compute stages on ONE pipeline object, the
|
||||
// compute stage writing the buffer the vertex stage then reads.
|
||||
//
|
||||
// DISABLED: the third defect in this cluster. Attaching a compute stage alongside graphics
|
||||
// stages is now accepted, but the dispatch/draw pair still paints nothing and leaves an error
|
||||
// behind - GetProgramForDraw flattens EVERY stage of the pipeline into one composite, so the
|
||||
// compute stage and the graphics stages end up in a single program that can serve neither
|
||||
// glDispatchCompute nor glDrawArrays correctly. GL keeps them separate: a pipeline's compute
|
||||
// stage is dispatched on its own and never participates in a draw. Enable this when the
|
||||
// flattening splits the compute stage out from the graphics ones.
|
||||
TEST_F(ProgramPipelineScenario, DISABLED_ComputeAndGraphicsStagesShareOnePipeline) {
|
||||
// The third defect of the cluster: the flattening used to pull EVERY stage into one
|
||||
// composite, so a single program was asked to serve both glDispatchCompute and glDrawArrays.
|
||||
// GL keeps them apart - a pipeline's compute stage is a whole program dispatched on its own
|
||||
// and never participates in a draw - which is why the accessors are split (GetProgramForDraw
|
||||
// composites the graphics stages, GetProgramForDispatch hands back the compute stage
|
||||
// program). It is also the shape that killed the process on Adreno: the composite carried a
|
||||
// compute module into vkCreateGraphicsPipelines, and that driver SIGSEGVs rather than
|
||||
// returning an error.
|
||||
TEST_F(ProgramPipelineScenario, ComputeAndGraphicsStagesShareOnePipeline) {
|
||||
if (!Ready()) return;
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
|
||||
@@ -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