mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
Merge branch 'p5e/pg' into feat/disaggregated (P5e pg)
# Conflicts: # MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp # MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp
This commit is contained in:
@@ -4717,6 +4717,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// never be consumed (the stale pair is overwritten before any consumer runs).
|
||||
static const MG_State::GLState::ProgramObject* g_currentDrawFrontendProgram = nullptr;
|
||||
static BackendProgramObjectImpl* g_currentDrawBackendProgram = nullptr;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5e (pg), CONTRACT-P5E.md §5.5: THE SAME STASH, KEYED ON THE HANDLE. The raw frontend
|
||||
// pointer above is one of the five frontend-keyed reads this family retires: every
|
||||
// consumer compared it against GetProgramForDraw().get(), which under run-ahead is a
|
||||
// pointer to an object the client may already have relinked or freed. The handle arm
|
||||
// compares against MGPipeApplier().DrawProgram instead - a {slot, gen} the record
|
||||
// carried, which cannot be recycled behind the server's back because the gen moves with
|
||||
// the slot.
|
||||
//
|
||||
// BOTH HALVES SURVIVE because the two arms do (ruling 1): the pointer is the monolith
|
||||
// glue's key and the handle is the transport's, and SyncCurrentProgram* clears the one
|
||||
// it does not use so a stale key can never be consumed.
|
||||
static MG_Pipe::MGPipeHandle g_currentDrawProgramHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
#endif
|
||||
|
||||
// Memo of the per-draw enabled-draw-buffers walk feeding g_fragColorBroadcastCount:
|
||||
// the answer is a pure function of WHICH FBO is bound and its draw-buffer edits, so
|
||||
@@ -4848,19 +4862,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// the upload copies GetUBOSize() bytes and a short record would read past it.
|
||||
//
|
||||
// Anything else and the frontend's own MapUBO answers, exactly as it does today.
|
||||
static const MG_Pipe::MGPipeShaderCsoRecord* ResolveGlobalConstantsRecord(
|
||||
const MG_State::GLState::ProgramObject* program) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5c (G6, CONTRACT-P5C §5.4): the program's handle is resolved by frontend
|
||||
// identity below - frontend-keyed twin resolution, named debt inside the scope -
|
||||
// P3b/P4b rekeys the registry onto handles.
|
||||
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
|
||||
#endif
|
||||
// P4a decline-site P1: M - the mask says this family is not switched on, or there
|
||||
// is no current program at all; it IS D's ProgramSubsystemEnabled() now. Silent,
|
||||
// confirmed at the verification round.
|
||||
if (!ProgramSubsystemEnabled() || program == nullptr) return nullptr;
|
||||
const MG_Pipe::MGPipeHandle handle = g_backendProgramObjects.HandleOf(program);
|
||||
// P5e (pg): `uboSize` is what the block image is checked against, and it is a PARAMETER
|
||||
// now rather than a `program->GetUBOSize()` read inside the body - on the handle arm the
|
||||
// size is the record's own Desc.GlobalUboSize and there is no ProgramObject to ask.
|
||||
// `program` is null on that arm and is used for nothing but the monolith identity
|
||||
// resolution below.
|
||||
static const MG_Pipe::MGPipeShaderCsoRecord* ResolveGlobalConstantsRecordForHandle(
|
||||
MG_Pipe::MGPipeHandle handle, Uint uboSize) {
|
||||
const MG_Pipe::MGPipeShaderCsoRecord* const record = FindShaderCsoRecord(handle);
|
||||
// P4a decline-site P4: S - FOLDED INTO P2/P3 AT THE VERIFICATION ROUND, which is
|
||||
// what the review asked for and is why this one stays silent: both reasons
|
||||
@@ -4898,7 +4906,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"of a program with a %u-byte default uniform block; running the "
|
||||
"frontend's own uniform block.",
|
||||
static_cast<unsigned>(handle.Slot), static_cast<unsigned>(handle.Gen),
|
||||
static_cast<unsigned>(program->GetUBOSize()));
|
||||
static_cast<unsigned>(uboSize));
|
||||
return nullptr;
|
||||
}
|
||||
// P4a decline-site P7: S, and LOUD (ID-19) - this one is not a missing record,
|
||||
@@ -4915,7 +4923,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// defect has to be caught - never reach MapUBO() at all, and a shipped build does
|
||||
// not abort a running game over a client-side bug. If the integrator wants the
|
||||
// stop on every arm, it is one #if away and this comment is where to say so.
|
||||
if (record->GlobalConstants.size() < static_cast<SizeT>(program->GetUBOSize())) {
|
||||
if (record->GlobalConstants.size() < static_cast<SizeT>(uboSize)) {
|
||||
#if MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY
|
||||
MGLOG_F("MGPipe: Fatal{ProtocolCorruption} "
|
||||
"A program record does not describe the binding it names: the ShaderCso "
|
||||
@@ -4923,7 +4931,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"%llu-byte default uniform block.",
|
||||
static_cast<unsigned>(handle.Slot), static_cast<unsigned>(handle.Gen),
|
||||
static_cast<unsigned long long>(record->GlobalConstants.size()),
|
||||
static_cast<unsigned long long>(program->GetUBOSize()));
|
||||
static_cast<unsigned long long>(uboSize));
|
||||
std::abort();
|
||||
#else
|
||||
MGLOG_E_ONCE("A program record does not describe the binding it names: the ShaderCso "
|
||||
@@ -4931,13 +4939,133 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"%llu-byte default uniform block; running the frontend's own uniform block.",
|
||||
static_cast<unsigned>(handle.Slot), static_cast<unsigned>(handle.Gen),
|
||||
static_cast<unsigned long long>(record->GlobalConstants.size()),
|
||||
static_cast<unsigned long long>(program->GetUBOSize()));
|
||||
static_cast<unsigned long long>(uboSize));
|
||||
#endif
|
||||
return nullptr;
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
// THE MONOLITH-GLUE HALF (ruling 1 / ID-81), unchanged in what it does: resolve the
|
||||
// program's handle by frontend identity inside the named scope, then ask the body above.
|
||||
// Reached only when `Transport == Monolith`, which is what keeps the push-monolith build
|
||||
// token for token.
|
||||
static const MG_Pipe::MGPipeShaderCsoRecord* ResolveGlobalConstantsRecord(
|
||||
const MG_State::GLState::ProgramObject* program) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside the
|
||||
// scope. P5e does not delete it - it stops REACHING it under a transport, which is
|
||||
// the thing the allocator guard measures.
|
||||
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
|
||||
#endif
|
||||
// P4a decline-site P1: M - the mask says this family is not switched on, or there
|
||||
// is no current program at all; it IS D's ProgramSubsystemEnabled() now. Silent,
|
||||
// confirmed at the verification round.
|
||||
if (!ProgramSubsystemEnabled() || program == nullptr) return nullptr;
|
||||
return ResolveGlobalConstantsRecordForHandle(g_backendProgramObjects.HandleOf(program),
|
||||
program->GetUBOSize());
|
||||
}
|
||||
#endif
|
||||
|
||||
// Read from the frontend rather than from the backend framebuffer sync, which
|
||||
// only runs later in PrepareForDraw: a program compiled against a stale count
|
||||
// would not be relinked until the draw after the one that needed it.
|
||||
//
|
||||
// P5e (pg): ITS OWN FUNCTION AND ITS OWN POLICY, because the three callers want three
|
||||
// different answers when the record arm has nothing to say:
|
||||
//
|
||||
// FrontendFallback - the monolith arm, unchanged: read the draw-FBO binding slot.
|
||||
// RefuseFallback - the handle arm AT A DRAW. With a live wire that binding slot is a
|
||||
// BARRIER_PULLED row the client may already have moved, so a count
|
||||
// read from it would compile the program against a framebuffer that
|
||||
// is not the one being drawn to - silently, and only sometimes.
|
||||
// The record is the answer or there is no answer (rule F).
|
||||
// RecordOnly - the handle arm AT A DISPATCH. A compute program has no fragment
|
||||
// stage and a dispatch has no draw framebuffer, so this count is not
|
||||
// an input to what is being built; PrepareForCompute also does not
|
||||
// run SyncCurrentFBO, so the framebuffer records are legitimately
|
||||
// untrusted here and a refusal would abort a correct program. The
|
||||
// previous resolution stands, which is what the monolith arm
|
||||
// effectively does too when nothing rebound between the two calls.
|
||||
enum class BroadcastCountPolicy { FrontendFallback, RefuseFallback, RecordOnly };
|
||||
|
||||
static void ResolveFragColorBroadcastCount(BroadcastCountPolicy policy) {
|
||||
Bool broadcastCountResolved = false;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The trust latch is fresh here and costs nothing: PrepareForDraw runs
|
||||
// SyncCurrentFBO immediately before this, so the records have just been
|
||||
// checked against the two bindings.
|
||||
if (FramebufferSubsystemEnabled() && FramebufferImpl::g_fboRecordsTrusted) {
|
||||
const MG_Pipe::MGPFramebufferState* const recordPtr =
|
||||
BoundFramebufferRecord(FramebufferTarget::Draw);
|
||||
// P4a decline-site F7: FLIPPED AT THE VERIFICATION ROUND from a silent
|
||||
// decline to an assertion, because it is unreachable and a silent decline
|
||||
// here is indistinguishable from a legitimate one. g_fboRecordsTrusted
|
||||
// implies a record with a non-null handle on BOTH targets:
|
||||
// SyncCurrentFBOByRecord sets the latch only after F2 has rejected a null
|
||||
// pointer or handle on either, and it runs immediately before this in
|
||||
// PrepareForDraw. Kept as a checked decline rather than deleted so a
|
||||
// release build cannot dereference null if that ordering ever changes.
|
||||
MOBILEGL_ASSERT(recordPtr != nullptr && !MG_Pipe::MGPipeHandleIsNull(recordPtr->Fbo));
|
||||
if (recordPtr != nullptr && !MG_Pipe::MGPipeHandleIsNull(recordPtr->Fbo)) {
|
||||
const MG_Pipe::MGPFramebufferState& record = *recordPtr;
|
||||
if (!g_broadcastMemoHandleValid || g_broadcastMemoContentHash != record.ContentHash) {
|
||||
Uint enabledDrawBuffers = 0;
|
||||
for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
|
||||
// -1 is FramebufferAttachmentType::None on the wire; every
|
||||
// other value is an attachment index.
|
||||
if (record.DrawBuffers[i] >= 0) {
|
||||
enabledDrawBuffers = i + 1;
|
||||
}
|
||||
}
|
||||
g_broadcastMemoContentHash = record.ContentHash;
|
||||
g_broadcastMemoHandleCount = std::max<Uint>(enabledDrawBuffers, 1);
|
||||
g_broadcastMemoHandleValid = true;
|
||||
}
|
||||
g_fragColorBroadcastCount = g_broadcastMemoHandleCount;
|
||||
broadcastCountResolved = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (policy == BroadcastCountPolicy::RecordOnly) return;
|
||||
if (policy == BroadcastCountPolicy::RefuseFallback && !broadcastCountResolved) {
|
||||
// The record arm declined under a transport. g_fboRecordsTrusted is set by
|
||||
// SyncCurrentFBOByRecord, which PrepareForDraw runs immediately before this,
|
||||
// so reaching here means the framebuffer family's own records are missing or
|
||||
// untrusted - and the frontend fallback below is exactly the read this phase
|
||||
// exists to retire.
|
||||
MGLOG_F("MGPipe: Fatal{UnmigratedPipeInput, "
|
||||
"\"GetFramebufferBindingSlot@SyncCurrentProgram\"} - the draw "
|
||||
"framebuffer's record did not answer the fragColor broadcast count on "
|
||||
"the handle arm, and the frontend binding slot is a row the client owns");
|
||||
std::abort();
|
||||
}
|
||||
if (!broadcastCountResolved) {
|
||||
const auto& drawSlot = GetFramebufferBindingSlotChecked(FramebufferTarget::Draw);
|
||||
const auto& drawFBO = drawSlot.GetBoundObject();
|
||||
const Uint16 slotVersion = drawSlot.GetVersion();
|
||||
const Uint16 objectVersion = drawFBO ? drawFBO->GetObjectVersion() : 0;
|
||||
if (!g_broadcastMemoValid || g_broadcastMemoFbo != drawFBO.get() ||
|
||||
g_broadcastMemoSlotVersion != slotVersion ||
|
||||
g_broadcastMemoObjectVersion != objectVersion) {
|
||||
Uint enabledDrawBuffers = 0;
|
||||
if (drawFBO) {
|
||||
const auto& drawBuffers = drawFBO->GetDrawBuffers();
|
||||
for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
|
||||
if (drawBuffers[i] != FramebufferAttachmentType::None) {
|
||||
enabledDrawBuffers = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
g_broadcastMemoFbo = drawFBO.get();
|
||||
g_broadcastMemoSlotVersion = slotVersion;
|
||||
g_broadcastMemoObjectVersion = objectVersion;
|
||||
g_broadcastMemoCount = std::max<Uint>(enabledDrawBuffers, 1);
|
||||
g_broadcastMemoValid = true;
|
||||
}
|
||||
g_fragColorBroadcastCount = g_broadcastMemoCount;
|
||||
}
|
||||
}
|
||||
|
||||
void SyncCurrentProgram(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram) {
|
||||
#ifdef TRACY_ENABLE
|
||||
@@ -4962,73 +5090,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_lastUsedBackendProgramId = 0;
|
||||
return;
|
||||
}
|
||||
// Read from the frontend rather than from the backend framebuffer sync, which
|
||||
// only runs later in PrepareForDraw: a program compiled against a stale count
|
||||
// would not be relinked until the draw after the one that needed it.
|
||||
{
|
||||
Bool broadcastCountResolved = false;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The trust latch is fresh here and costs nothing: PrepareForDraw runs
|
||||
// SyncCurrentFBO immediately before this, so the records have just been
|
||||
// checked against the two bindings.
|
||||
if (FramebufferSubsystemEnabled() && FramebufferImpl::g_fboRecordsTrusted) {
|
||||
const MG_Pipe::MGPFramebufferState* const recordPtr =
|
||||
BoundFramebufferRecord(FramebufferTarget::Draw);
|
||||
// P4a decline-site F7: FLIPPED AT THE VERIFICATION ROUND from a silent
|
||||
// decline to an assertion, because it is unreachable and a silent decline
|
||||
// here is indistinguishable from a legitimate one. g_fboRecordsTrusted
|
||||
// implies a record with a non-null handle on BOTH targets:
|
||||
// SyncCurrentFBOByRecord sets the latch only after F2 has rejected a null
|
||||
// pointer or handle on either, and it runs immediately before this in
|
||||
// PrepareForDraw. Kept as a checked decline rather than deleted so a
|
||||
// release build cannot dereference null if that ordering ever changes.
|
||||
MOBILEGL_ASSERT(recordPtr != nullptr && !MG_Pipe::MGPipeHandleIsNull(recordPtr->Fbo));
|
||||
if (recordPtr != nullptr && !MG_Pipe::MGPipeHandleIsNull(recordPtr->Fbo)) {
|
||||
const MG_Pipe::MGPFramebufferState& record = *recordPtr;
|
||||
if (!g_broadcastMemoHandleValid || g_broadcastMemoContentHash != record.ContentHash) {
|
||||
Uint enabledDrawBuffers = 0;
|
||||
for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
|
||||
// -1 is FramebufferAttachmentType::None on the wire; every
|
||||
// other value is an attachment index.
|
||||
if (record.DrawBuffers[i] >= 0) {
|
||||
enabledDrawBuffers = i + 1;
|
||||
}
|
||||
}
|
||||
g_broadcastMemoContentHash = record.ContentHash;
|
||||
g_broadcastMemoHandleCount = std::max<Uint>(enabledDrawBuffers, 1);
|
||||
g_broadcastMemoHandleValid = true;
|
||||
}
|
||||
g_fragColorBroadcastCount = g_broadcastMemoHandleCount;
|
||||
broadcastCountResolved = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (!broadcastCountResolved) {
|
||||
const auto& drawSlot = GetFramebufferBindingSlotChecked(FramebufferTarget::Draw);
|
||||
const auto& drawFBO = drawSlot.GetBoundObject();
|
||||
const Uint16 slotVersion = drawSlot.GetVersion();
|
||||
const Uint16 objectVersion = drawFBO ? drawFBO->GetObjectVersion() : 0;
|
||||
if (!g_broadcastMemoValid || g_broadcastMemoFbo != drawFBO.get() ||
|
||||
g_broadcastMemoSlotVersion != slotVersion ||
|
||||
g_broadcastMemoObjectVersion != objectVersion) {
|
||||
Uint enabledDrawBuffers = 0;
|
||||
if (drawFBO) {
|
||||
const auto& drawBuffers = drawFBO->GetDrawBuffers();
|
||||
for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
|
||||
if (drawBuffers[i] != FramebufferAttachmentType::None) {
|
||||
enabledDrawBuffers = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
g_broadcastMemoFbo = drawFBO.get();
|
||||
g_broadcastMemoSlotVersion = slotVersion;
|
||||
g_broadcastMemoObjectVersion = objectVersion;
|
||||
g_broadcastMemoCount = std::max<Uint>(enabledDrawBuffers, 1);
|
||||
g_broadcastMemoValid = true;
|
||||
}
|
||||
g_fragColorBroadcastCount = g_broadcastMemoCount;
|
||||
}
|
||||
}
|
||||
ResolveFragColorBroadcastCount(BroadcastCountPolicy::FrontendFallback);
|
||||
|
||||
BackendProgramObjectImpl* twin = nullptr;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
@@ -5082,7 +5144,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
twin->GetUnormFallbackClampOutputMask() != g_unormFallbackClampOutputMask ||
|
||||
twin->GetFragColorBroadcastCount() != g_fragColorBroadcastCount ||
|
||||
twin->GetShaderStorageBlockBindingSignature() !=
|
||||
ComputeShaderStorageBlockBindingSignature(*currentProgram) ||
|
||||
ComputeShaderStorageBlockBindingSignatureOf(*currentProgram) ||
|
||||
// A fourth of the same shape, and the reason glBindImageTexture itself does
|
||||
// nothing: GLSL ES demands a format layout qualifier on an image where desktop
|
||||
// GLSL lets a writeonly declaration omit one, so a format-less declaration is
|
||||
@@ -5129,7 +5191,82 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
g_currentDrawFrontendProgram = currentProgram.get();
|
||||
g_currentDrawBackendProgram = twin;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
g_currentDrawProgramHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5e (pg), CONTRACT-P5E.md §5.5: THE SAME SYNC, FROM THE HANDLE THE RECORD CARRIED.
|
||||
// Two overloads and not an `#if` inside one body (ruling 1 / ID-81): the frontend one
|
||||
// above is visibly the monolith-glue half, this one names no frontend object at all.
|
||||
//
|
||||
// WHAT THE NINE-CLAUSE CONDITION BECOMES, and the clause COUNT does not shrink - its
|
||||
// inputs move (D-H5, §5.5):
|
||||
// GetLinkVersion() -> record.Serial vs GetSyncedShaderCsoSerial()
|
||||
// GetImageUnitVersion() -> record.BindingsSerial vs GetSyncedBindingsSerial()
|
||||
// ComputeShaderStorage...() -> record.Signature
|
||||
// GetLinkStatus/SpirvStatus -> Desc.LinkStatus / Desc.SpirvStatus, above
|
||||
// and the clamp masks, the broadcast count, ImageUnitFormatsStillMatch and the three
|
||||
// patch clauses are unchanged - they are backend globals, fb's row and VALUE rows of the
|
||||
// residual block, none of which this family owns.
|
||||
void SyncCurrentProgramByHandle(MG_Pipe::MGPipeHandle cso, Bool forDraw) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
g_backendProgramObjects.CollectGarbageIfNeeded();
|
||||
SamplerImpl::g_backendSamplerObjects.CollectGarbageIfNeeded();
|
||||
|
||||
g_currentDrawFrontendProgram = nullptr;
|
||||
g_currentDrawBackendProgram = nullptr;
|
||||
g_currentDrawProgramHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
|
||||
const MG_Pipe::MGPipeShaderCsoRecord* const record = FindShaderCsoRecord(cso);
|
||||
// THE SAME THREE-PART TEST THE FRONTEND ARM MAKES, from the descriptor. LinkStatus
|
||||
// is a field rather than an implication (ID-88): create_shader_state is re-issued at
|
||||
// every link that moves the link version and a FAILED relink of a bound program
|
||||
// moves it too, so "a record exists" and "the program linked" are different
|
||||
// statements. A program the frontend reports unlinked draws nothing, which is
|
||||
// exactly what the monolith arm does with GetLinkStatus() == false.
|
||||
if (record == nullptr || record->Desc.LinkStatus == 0 || record->Desc.SpirvStatus == 0) {
|
||||
g_GLESFuncs.glUseProgram(0);
|
||||
g_lastUsedBackendProgramId = 0;
|
||||
return;
|
||||
}
|
||||
// REFUSED at a draw, record-only at a dispatch: see the policy's own note.
|
||||
ResolveFragColorBroadcastCount(forDraw ? BroadcastCountPolicy::RefuseFallback
|
||||
: BroadcastCountPolicy::RecordOnly);
|
||||
|
||||
BackendProgramObjectImpl* const twin = ResolveProgramTwin(cso);
|
||||
if (twin == nullptr) {
|
||||
// ResolveProgramTwin has already named the handle. A draw with no twin binds
|
||||
// nothing, which is the visible no-op Use() makes of an unusable program rather
|
||||
// than a draw with somebody else's shader.
|
||||
g_GLESFuncs.glUseProgram(0);
|
||||
g_lastUsedBackendProgramId = 0;
|
||||
return;
|
||||
}
|
||||
if (!twin->GetBackendProgramId() ||
|
||||
twin->GetSyncedShaderCsoSerial() != record->Serial ||
|
||||
twin->GetSyncedBindingsSerial() != record->BindingsSerial ||
|
||||
twin->GetSnormFallbackClampOutputMask() != g_snormFallbackClampOutputMask ||
|
||||
twin->GetUnormFallbackClampOutputMask() != g_unormFallbackClampOutputMask ||
|
||||
twin->GetFragColorBroadcastCount() != g_fragColorBroadcastCount ||
|
||||
twin->GetShaderStorageBlockBindingSignature() != record->Signature ||
|
||||
!twin->ImageUnitFormatsStillMatch() ||
|
||||
(twin->GetPassthroughTessControlPatchVertices() >= 0 &&
|
||||
(twin->GetPassthroughTessControlPatchVertices() !=
|
||||
static_cast<Int>(MGB_CTX->GetPatchVertices()) ||
|
||||
!BitwiseEqual(twin->GetPassthroughTessControlOuterLevel(),
|
||||
MGB_CTX->GetPatchDefaultOuterLevel()) ||
|
||||
!BitwiseEqual(twin->GetPassthroughTessControlInnerLevel(),
|
||||
MGB_CTX->GetPatchDefaultInnerLevel())))) {
|
||||
twin->SyncToBackendByHandle(cso);
|
||||
}
|
||||
g_currentDrawProgramHandle = cso;
|
||||
g_currentDrawBackendProgram = twin;
|
||||
}
|
||||
#endif
|
||||
} // namespace PrgramImpl
|
||||
|
||||
void BindCurrentFBO(FramebufferTarget target) {
|
||||
@@ -5376,7 +5513,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
static void BindCurrentProgramWithResources(
|
||||
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
|
||||
const TextureImpl::DrawTextureSyncKeys& keys);
|
||||
const TextureImpl::DrawTextureSyncKeys& keys, MG_Pipe::MGPipeHandle programCso);
|
||||
static void BindCurrentTextures(const TextureImpl::DrawTextureSyncKeys& keys,
|
||||
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram);
|
||||
|
||||
@@ -5454,7 +5591,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// stores into a buffer texture from the FRAGMENT stage, not from a dispatch.
|
||||
TextureImpl::MarkWritableImageBufferTexturesGpuWritten();
|
||||
FramebufferImpl::SyncCurrentFBO();
|
||||
PrgramImpl::SyncCurrentProgram(currentProgram);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5e (pg), kimi row 91 / S4 1.1: the SharedPtr pull above is the row this family
|
||||
// retires. On the handle arm the program is MGPipeApplier().DrawProgram - a handle the
|
||||
// set_draw_program record carried - and `currentProgram` is not consulted for the sync
|
||||
// at all; the remaining uses of it below are the two callees' own, and both have a
|
||||
// handle arm of their own.
|
||||
if (ProgramHandleArm()) {
|
||||
PrgramImpl::SyncCurrentProgramByHandle(MG_Pipe::MGPipeApplier().DrawProgram, true);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
PrgramImpl::SyncCurrentProgram(currentProgram);
|
||||
}
|
||||
RenderStateImpl::SyncRenderState();
|
||||
|
||||
BindCurrentFBO(FramebufferTarget::Draw);
|
||||
@@ -5473,7 +5622,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
VertexArrayImpl::SyncCurrentVertexAttributeValues(vaoTwin, currentProgram);
|
||||
|
||||
BindCurrentTextures(textureKeys, currentProgram);
|
||||
BindCurrentProgramWithResources(currentProgram, textureKeys);
|
||||
BindCurrentProgramWithResources(currentProgram, textureKeys,
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
MG_Pipe::MGPipeApplier().DrawProgram
|
||||
#else
|
||||
MG_Pipe::kMGPipeNullHandle
|
||||
#endif
|
||||
);
|
||||
|
||||
// Last: opening the capture span needs the program current and the capture
|
||||
// buffers bound, and ES rejects most binding changes once it is open.
|
||||
@@ -5873,6 +6028,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint32 programBackendStateVersion = 0;
|
||||
Bool programLinked = false;
|
||||
Uint contextGeneration = 0;
|
||||
// P5e (pg) -> tx2, ruling 6 / ID-86: THE PROGRAM HALF OF THIS KEY HAS TO GO BY HANDLE,
|
||||
// and this struct is tx2's line, so pg states the shape rather than writing it. The four
|
||||
// program fields above are read off the frontend ProgramObject on EVERY DRAW, memo hit
|
||||
// included (kimi row 100, the worst row of the program family). What replaces them on
|
||||
// the handle arm is {DrawProgram.Slot, DrawProgram.Gen, ShaderCso record Serial,
|
||||
// BindingsSerial}: Serial moves on every applied create_shader_state (how a relink
|
||||
// travels) and BindingsSerial on every applied set_program_bindings (how a glUniform1i
|
||||
// on a sampler travels). Both are needed - ruling 6's "both" - because a relink with
|
||||
// UNCHANGED texture resolution still hands the twin new unit assignments.
|
||||
decltype(TextureImpl::g_boundTexturesCache) boundTextures{};
|
||||
};
|
||||
// Small per-PROGRAM memo set, not one global: the program is part of the key
|
||||
@@ -5967,27 +6131,63 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// _FlwFrameUniforms block and the _flw_depthPyramid sampler.
|
||||
static void BindCurrentProgramWithResources(
|
||||
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
|
||||
const TextureImpl::DrawTextureSyncKeys& keys) {
|
||||
if (currentProgram && currentProgram->GetLinkStatus() && currentProgram->GetSpirvStatus()) {
|
||||
const TextureImpl::DrawTextureSyncKeys& keys, MG_Pipe::MGPipeHandle programCso) {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5e (pg): THE HANDLE ARM'S GATE, from the record rather than from the object. Hoisted
|
||||
// out of the `if` below because the whole body's entry condition is "the program linked
|
||||
// and has SPIR-V", and on this arm both answers are fields of the descriptor
|
||||
// (Desc.LinkStatus is ID-88's, Desc.SpirvStatus P4a's).
|
||||
const Bool handleArm = ProgramHandleArm();
|
||||
const MG_Pipe::MGPipeShaderCsoRecord* programRecord = nullptr;
|
||||
if (handleArm) {
|
||||
programRecord = PrgramImpl::FindShaderCsoRecord(programCso);
|
||||
if (programRecord != nullptr &&
|
||||
(programRecord->Desc.LinkStatus == 0 || programRecord->Desc.SpirvStatus == 0)) {
|
||||
programRecord = nullptr;
|
||||
}
|
||||
}
|
||||
const Bool programUsable =
|
||||
handleArm ? programRecord != nullptr
|
||||
: (currentProgram && currentProgram->GetLinkStatus() && currentProgram->GetSpirvStatus());
|
||||
#else
|
||||
const Bool programUsable =
|
||||
currentProgram && currentProgram->GetLinkStatus() && currentProgram->GetSpirvStatus();
|
||||
#endif
|
||||
if (programUsable) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5c (G6, CONTRACT-P5C §5.4): the program/sampler twin resolutions below still key
|
||||
// on frontend objects - named debt inside the scope - P3b/P4b rekeys the registry
|
||||
// onto handles.
|
||||
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
|
||||
PrgramImpl::BackendProgramObjectImpl* twin = nullptr;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (handleArm) {
|
||||
// THE STASH, KEYED ON THE HANDLE (CONTRACT-P5E §5.5). SyncCurrentProgramByHandle
|
||||
// wrote it a few lines ago in the same Prepare; the fallback is the by-handle
|
||||
// resolver, which is record-first and touches no frontend identity - so unlike
|
||||
// the frontend fallback below it is not a registry probe at all.
|
||||
twin = PrgramImpl::g_currentDrawProgramHandle == programCso
|
||||
? PrgramImpl::g_currentDrawBackendProgram
|
||||
: nullptr;
|
||||
if (!twin) twin = PrgramImpl::ResolveProgramTwin(programCso);
|
||||
} else
|
||||
#endif
|
||||
// The twin SyncCurrentProgram just resolved for this draw; the registry
|
||||
// Find only runs if the stash somehow does not match (defensive fallback).
|
||||
PrgramImpl::BackendProgramObjectImpl* twin =
|
||||
PrgramImpl::g_currentDrawFrontendProgram == currentProgram.get()
|
||||
? PrgramImpl::g_currentDrawBackendProgram
|
||||
: nullptr;
|
||||
if (!twin) {
|
||||
auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(currentProgram.get());
|
||||
if (backendProgramSlot) {
|
||||
twin = backendProgramSlot->get();
|
||||
{
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5c (G6, CONTRACT-P5C §5.4): MONOLITH GLUE ONLY now. The frontend-keyed twin
|
||||
// resolution below is the fourth of this family's five named scope sites, and
|
||||
// P5e does not delete it - it stops reaching it under a transport, which is what
|
||||
// the allocator guard measures.
|
||||
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
|
||||
#endif
|
||||
// The twin SyncCurrentProgram just resolved for this draw; the registry
|
||||
// Find only runs if the stash somehow does not match (defensive fallback).
|
||||
twin = PrgramImpl::g_currentDrawFrontendProgram == currentProgram.get()
|
||||
? PrgramImpl::g_currentDrawBackendProgram
|
||||
: nullptr;
|
||||
if (!twin) {
|
||||
auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(currentProgram.get());
|
||||
if (backendProgramSlot) {
|
||||
twin = backendProgramSlot->get();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (twin) {
|
||||
@@ -5997,7 +6197,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Global UBO: block index and binding-point assignment are cached at
|
||||
// link time (CacheResourceLocations); re-upload only when the CPU shadow
|
||||
// actually changed since the last upload for this program.
|
||||
if (currentProgram->GetUBOSize() > 0 && backendProgram.HasGlobalUboBlock()) {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5e (pg): the block's SIZE is Desc.GlobalUboSize on the handle arm. It is the
|
||||
// same number GetUBOSize() answers - the emitter takes it from there - but it is
|
||||
// the record's copy, which is the point.
|
||||
const Uint globalUboSize =
|
||||
handleArm ? programRecord->Desc.GlobalUboSize : currentProgram->GetUBOSize();
|
||||
#else
|
||||
const Uint globalUboSize = currentProgram->GetUBOSize();
|
||||
#endif
|
||||
if (globalUboSize > 0 && backendProgram.HasGlobalUboBlock()) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedNC("UpdateGlobalUBO", TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
@@ -6014,14 +6223,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// block right after this one and they are P4b's (dirty bits 15-17), which
|
||||
// is why nothing here touches them.
|
||||
const MG_Pipe::MGPipeShaderCsoRecord* const globalConstants =
|
||||
PrgramImpl::ResolveGlobalConstantsRecord(currentProgram.get());
|
||||
handleArm ? PrgramImpl::ResolveGlobalConstantsRecordForHandle(programCso,
|
||||
globalUboSize)
|
||||
: PrgramImpl::ResolveGlobalConstantsRecord(currentProgram.get());
|
||||
// P5e (pg), gap G-C: `MapUBO()` HAS NO RUN-AHEAD ANSWER. It returns
|
||||
// ProgramObject::globalUboScratch - the live CPU array every glUniform*
|
||||
// writes into - so under a transport it is a torn read of client memory by
|
||||
// construction, not a read that happens to race. Every reason
|
||||
// ResolveGlobalConstantsRecord* can decline is a SEAM DEFECT here (bit 12
|
||||
// off, the mis-keyed descriptor, the never-uploaded sentinel, a short block
|
||||
// image), and each of them has already said so by name one level down; what
|
||||
// this adds is that on the handle arm there is nothing legal to fall back
|
||||
// to. CONTRACT-P5E §5.5: Fatal{UnmigratedVerb, "set_global_constants"}, not
|
||||
// a torn upload.
|
||||
if (handleArm && globalConstants == nullptr) {
|
||||
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"set_global_constants\"} - the "
|
||||
"ShaderCso record {%u, %u} does not answer the default uniform "
|
||||
"block for a draw of a program that declares %u bytes of it, and "
|
||||
"MapUBO() is the client's live scratch array",
|
||||
programCso.Slot, programCso.Gen, globalUboSize);
|
||||
std::abort();
|
||||
}
|
||||
#endif
|
||||
const Uint32 uboContentVersion =
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
globalConstants ? globalConstants->GlobalConstantsVersion :
|
||||
#endif
|
||||
currentProgram->GetUBOContentVersion();
|
||||
const SizeT uboSize = static_cast<SizeT>(currentProgram->GetUBOSize());
|
||||
const SizeT uboSize = static_cast<SizeT>(globalUboSize);
|
||||
// Substituted at the two upload sites rather than hoisted into a local:
|
||||
// MapUBO() is called there today and only there, and hoisting it would
|
||||
// call it on paths that skip the upload entirely.
|
||||
@@ -6073,12 +6302,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// failed): the original in-place upload.
|
||||
if (backendProgram.GetLastUploadedGlobalUboVersion() != uboContentVersion) {
|
||||
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, backendProgram.GetBackendGlobalUBOId());
|
||||
g_GLESFuncs.glBufferSubData(GL_UNIFORM_BUFFER, 0, currentProgram->GetUBOSize(),
|
||||
g_GLESFuncs.glBufferSubData(GL_UNIFORM_BUFFER, 0, globalUboSize,
|
||||
MGB_UBO_BYTES);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(
|
||||
MG_Util::PipeStats::ByteClass::StageUboGlobal,
|
||||
static_cast<Uint64>(currentProgram->GetUBOSize()));
|
||||
static_cast<Uint64>(globalUboSize));
|
||||
}
|
||||
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||
backendProgram.SetLastUploadedGlobalUboVersion(uboContentVersion);
|
||||
@@ -6110,8 +6339,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Connect buffer to backend binding point
|
||||
auto binding = currentProgram->GetUniformBlockBinding(i);
|
||||
// Connect buffer to backend binding point.
|
||||
//
|
||||
// P5e (pg): glUniformBlockBinding MOVES THIS AFTER THE LINK, so it is
|
||||
// one of the three post-link mutable reflection fields the archive alone
|
||||
// cannot answer - set_program_bindings' first tail carries it, dense in
|
||||
// this very index space. The BINDING POINT it names is sb's row (the
|
||||
// GetBufferBindingPoint below); this line is the boundary S4 R5 named.
|
||||
auto binding =
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
handleArm ? PrgramImpl::ProgramBlockBindingFromRecord(*programRecord, i)
|
||||
:
|
||||
#endif
|
||||
currentProgram->GetUniformBlockBinding(i);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5e (sb, §5.6): THE POINT HALF, by record. `binding` above is the
|
||||
// PROGRAM's block binding and stays exactly where it is (it is package
|
||||
@@ -6232,7 +6472,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// and every previously touched unit's sampler-shadow row is exactly
|
||||
// what this pass last left there, re-running it is a provable no-op.
|
||||
auto& samplerPassMemo = backendProgram.GetSamplerPassMemo();
|
||||
const Uint32 programBackendStateVersion = currentProgram->GetBackendStateVersion();
|
||||
// P5e (pg), CONTRACT-P5E §5.5: THE SAMPLER PASS MEMO KEYS ON BindingsSerial
|
||||
// on the handle arm. m_backendStateVersion is a frontend counter read on
|
||||
// every draw (kimi row 105); what it is standing in for here is "has a
|
||||
// sampler uniform's unit moved", and that is exactly what an applied
|
||||
// set_program_bindings means. Truncated to the memo's Uint32 field, which is
|
||||
// safe for a memo key - it is a change detector, not an ordering - and the
|
||||
// record's serial would need four billion binding records to wrap.
|
||||
const Uint32 programBackendStateVersion =
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
handleArm ? static_cast<Uint32>(programRecord->BindingsSerial) :
|
||||
#endif
|
||||
currentProgram->GetBackendStateVersion();
|
||||
Bool samplerPassClean =
|
||||
samplerPassMemo.valid && samplerPassMemo.contextId == keys.contextId &&
|
||||
samplerPassMemo.unitBindingsEpoch == keys.unitBindingsEpoch &&
|
||||
@@ -6255,8 +6506,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
samplerPassMemo.count = 0;
|
||||
Bool memoisable = true;
|
||||
for (auto& samplerBinding : backendProgram.GetSamplerUniformBindings()) {
|
||||
// P5e (pg): the unit glUniform1i put on this sampler - the second
|
||||
// of the three post-link mutable fields, carried sparse and ascending
|
||||
// by location in set_program_bindings' second tail.
|
||||
const auto unit =
|
||||
currentProgram->GetUniformSamplerOrImageUnitIndex(samplerBinding.frontendLocation);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
handleArm
|
||||
? PrgramImpl::ProgramSamplerUnitFromRecord(
|
||||
*programRecord, samplerBinding.frontendLocation)
|
||||
:
|
||||
#endif
|
||||
currentProgram->GetUniformSamplerOrImageUnitIndex(
|
||||
samplerBinding.frontendLocation);
|
||||
if (unit == -1) continue;
|
||||
// Record the touched unit for the memo; a pass touching more
|
||||
// units than the memo can carry (or an out-of-range unit)
|
||||
@@ -6389,6 +6650,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// PrepareForCompute, where the current program (and therefore its registry twin)
|
||||
// is pinned for the duration. Prefers the per-draw stash those preparations wrote.
|
||||
static PrgramImpl::BackendProgramObjectImpl* GetCurrentBackendProgram() {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5e (pg), kimi row 107: THE PER-SUB-DRAW ROW. This is reached once per indirect
|
||||
// sub-draw for the gl_DrawID / gl_BaseVertex / gl_BaseInstance uniforms, and before P5e
|
||||
// every one of those pulled GetProgramForDraw() and, on a stash miss, probed the client
|
||||
// allocator. On the handle arm the stash key is the applier's own DrawProgram and the
|
||||
// miss path is the by-handle resolver, which is record-first and names no frontend
|
||||
// identity - so the whole entry point leaves the frontend.
|
||||
if (ProgramHandleArm()) {
|
||||
const MG_Pipe::MGPipeHandle cso = MG_Pipe::MGPipeApplier().DrawProgram;
|
||||
if (MG_Pipe::MGPipeHandleIsNull(cso)) return nullptr;
|
||||
const auto* const record = PrgramImpl::FindShaderCsoRecord(cso);
|
||||
if (record == nullptr || record->Desc.LinkStatus == 0 || record->Desc.SpirvStatus == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
if (PrgramImpl::g_currentDrawProgramHandle == cso) {
|
||||
return PrgramImpl::g_currentDrawBackendProgram;
|
||||
}
|
||||
return PrgramImpl::ResolveProgramTwin(cso);
|
||||
}
|
||||
#endif
|
||||
const auto& currentProgram = MGB_CTX->GetProgramForDraw();
|
||||
if (!currentProgram || !currentProgram->GetLinkStatus() || !currentProgram->GetSpirvStatus()) {
|
||||
return nullptr;
|
||||
@@ -6397,8 +6678,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return PrgramImpl::g_currentDrawBackendProgram;
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside the
|
||||
// scope - P3b/P4b rekeys the registry onto handles.
|
||||
// P5c (G6, CONTRACT-P5C §5.4): MONOLITH GLUE ONLY now - the fifth of this family's five
|
||||
// named scope sites, unreachable under a transport by the arm above.
|
||||
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
|
||||
#endif
|
||||
if (auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(currentProgram.get())) {
|
||||
@@ -6444,10 +6725,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// flattening a batch that turns out to need per-sub-draw values is unrecoverable -
|
||||
// so an unanswerable program counts as needing them.
|
||||
Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices) {
|
||||
const auto& currentProgram = MGB_CTX->GetProgramForDraw();
|
||||
const auto program = GetCurrentBackendProgram();
|
||||
if (!currentProgram || program == nullptr ||
|
||||
program->GetSyncedLinkVersion() != currentProgram->GetLinkVersion()) {
|
||||
if (program == nullptr) return true;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5e (pg), kimi row 108: the same "is this twin current" question, from the record's
|
||||
// serial instead of the frontend's link version. The ANSWER TO AN UNANSWERABLE PROGRAM
|
||||
// IS STILL `true` - a flattened batch that turns out to need per-sub-draw values is
|
||||
// unrecoverable, so "not known yet" has to count as "may need them", and a missing
|
||||
// record is the most not-known-yet a program can be.
|
||||
if (ProgramHandleArm()) {
|
||||
const auto* const record =
|
||||
PrgramImpl::FindShaderCsoRecord(MG_Pipe::MGPipeApplier().DrawProgram);
|
||||
if (record == nullptr || program->GetSyncedShaderCsoSerial() != record->Serial) {
|
||||
return true;
|
||||
}
|
||||
return program->ReadsDrawID() || (batchCarriesBaseVertices && program->ReadsBaseVertex());
|
||||
}
|
||||
#endif
|
||||
const auto& currentProgram = MGB_CTX->GetProgramForDraw();
|
||||
if (!currentProgram || program->GetSyncedLinkVersion() != currentProgram->GetLinkVersion()) {
|
||||
return true;
|
||||
}
|
||||
return program->ReadsDrawID() || (batchCarriesBaseVertices && program->ReadsBaseVertex());
|
||||
@@ -6843,8 +7139,33 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
TextureImpl::SyncNeccessaryTextures(textureKeys);
|
||||
TextureImpl::SyncImageTextureBindings();
|
||||
TextureImpl::MarkWritableImageBufferTexturesGpuWritten();
|
||||
PrgramImpl::SyncCurrentProgram(currentProgram);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5e (pg), kimi row 92: the DISPATCH half of the same retirement -
|
||||
// MGPipeApplier().DispatchProgram, which set_dispatch_program carried. A compute-only
|
||||
// pipeline has no draw program at all, which is why the two applier fields are separate.
|
||||
const Bool programHandleArm = ProgramHandleArm();
|
||||
const MG_Pipe::MGPipeHandle dispatchCso =
|
||||
programHandleArm ? MG_Pipe::MGPipeApplier().DispatchProgram : MG_Pipe::kMGPipeNullHandle;
|
||||
if (programHandleArm) {
|
||||
PrgramImpl::SyncCurrentProgramByHandle(dispatchCso, false);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
PrgramImpl::SyncCurrentProgram(currentProgram);
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (programHandleArm) {
|
||||
// The same gate as the frontend one below, from the record: a dispatch of a program
|
||||
// the frontend reports unlinked, or one whose SPIR-V never arrived, binds nothing.
|
||||
const auto* const record = PrgramImpl::FindShaderCsoRecord(dispatchCso);
|
||||
if (record == nullptr || record->Desc.LinkStatus == 0 || record->Desc.SpirvStatus == 0) {
|
||||
g_GLESFuncs.glUseProgram(0);
|
||||
PrgramImpl::g_lastUsedBackendProgramId = 0;
|
||||
return;
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
if (!currentProgram || !currentProgram->GetLinkStatus() || !currentProgram->GetSpirvStatus()) {
|
||||
g_GLESFuncs.glUseProgram(0);
|
||||
PrgramImpl::g_lastUsedBackendProgramId = 0;
|
||||
@@ -6858,7 +7179,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Compute programs need the same per-program resource sync as draws:
|
||||
// uniform-block bindings and sampler units only exist through the API
|
||||
// because layout(binding) is stripped from the transpiled ESSL.
|
||||
BindCurrentProgramWithResources(currentProgram, textureKeys);
|
||||
BindCurrentProgramWithResources(currentProgram, textureKeys,
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
dispatchCso
|
||||
#else
|
||||
MG_Pipe::kMGPipeNullHandle
|
||||
#endif
|
||||
);
|
||||
}
|
||||
|
||||
GLuint GetBackendProgramId(GLuint program) {
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
#include <MG_Pipe/MGPipeCallbacks.h>
|
||||
#include <MG_Pipe/MGPipeHostSpan.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
// P5e (pg): the DEFINITION of the archive MGPipeShaderCsoRecord holds by SharedPtr. PipeApply.h
|
||||
// needs only the name - the deleter is captured where the archive is constructed - but the
|
||||
// program build reads through it, so the one translation unit that does needs the whole type.
|
||||
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// R-11's server-owned staging copy. Header-only and package v1's; see its own header block for
|
||||
@@ -742,9 +746,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// layout(binding) reflected at link time, or whatever glUniform1i stored afterwards.
|
||||
// Rewrite every image uniform declaration to that unit so imageLoad/Store hits the
|
||||
// unit the app bound with glBindImageTexture.
|
||||
String RebindImageUniformsToFrontendUnits(
|
||||
String source, const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {
|
||||
if (!stateProgramObject || source.find("image") == String::npos) {
|
||||
String RebindImageUniformsToFrontendUnits(String source, const PrgramImpl::ProgramBuildSource& src) {
|
||||
// P5e (pg): the SOURCE, not the frontend object. The two reads below - the uniform's
|
||||
// location and the unit glUniform1i put there - are exactly the pair the record's
|
||||
// set_program_bindings tail carries, so on the handle arm this bakes the unit the client
|
||||
// observed rather than one it would have had to dereference a live ProgramObject for.
|
||||
// The null test the SharedPtr form carried moved to the caller, which returned early on
|
||||
// a null program long before it reached here.
|
||||
if (source.find("image") == String::npos) {
|
||||
return source;
|
||||
}
|
||||
static const std::regex imageDeclRegex(
|
||||
@@ -762,12 +771,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
std::smatch match;
|
||||
if (std::regex_search(line, match, imageDeclRegex)) {
|
||||
const String name = match[3].str();
|
||||
Int location = stateProgramObject->GetUniformLocation(name);
|
||||
Int location = src.GetUniformLocation(name);
|
||||
if (location < 0) {
|
||||
location = stateProgramObject->GetUniformLocation(name + "[0]");
|
||||
location = src.GetUniformLocation(name + "[0]");
|
||||
}
|
||||
if (location >= 0) {
|
||||
const Int unit = stateProgramObject->GetUniformSamplerOrImageUnitIndex(location);
|
||||
const Int unit = src.GetUniformSamplerOrImageUnitIndex(location);
|
||||
if (unit >= 0) {
|
||||
const String bindingText = "binding = " + std::to_string(unit);
|
||||
if (std::regex_search(line, bindingValueRegex)) {
|
||||
@@ -10907,6 +10916,222 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
TwinRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl, MG_Pipe::MGPipeKind::ShaderCso> g_backendProgramObjects;
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// ---- P5e (pg): the source, both ways round ---------------------------------------
|
||||
|
||||
Int ProgramArchiveSource::GetUniformLocation(const String& name) const {
|
||||
// ProgramObject::GetUniformLocation over the archive. Reproduced rather than called
|
||||
// because the frontend's form is a non-static member over Artifacts() and this arm
|
||||
// has no ProgramObject to call it on; the ARRAY RULES are the whole body and they
|
||||
// are the part a paraphrase would get wrong, so they are transcribed exactly.
|
||||
const auto it = Link->uniformLocations.find(name);
|
||||
if (it != Link->uniformLocations.end()) return static_cast<Int>(it->second);
|
||||
if (name.empty()) return -1;
|
||||
// Reflection stores GL-style names: an array uniform is keyed "arr[0]" at its base
|
||||
// location, so a bare "arr" resolves to that entry.
|
||||
if (name.back() != ']') {
|
||||
const auto suffixed = Link->uniformLocations.find(name + "[0]");
|
||||
if (suffixed != Link->uniformLocations.end()) return static_cast<Int>(suffixed->second);
|
||||
return -1;
|
||||
}
|
||||
if (name.length() < 4) return -1;
|
||||
// An ARRAY OF ARRAYS is keyed by its full "[0]"-terminated spelling ("a[2][1][0]"),
|
||||
// so a query that already ends in a subscript may still be the NAME of an array
|
||||
// rather than an element of one. That reading is tried FIRST; only then is the
|
||||
// trailing subscript read as an element index.
|
||||
{
|
||||
const auto arrayOfArrays = Link->uniformLocations.find(name + "[0]");
|
||||
if (arrayOfArrays != Link->uniformLocations.end()) {
|
||||
return static_cast<Int>(arrayOfArrays->second);
|
||||
}
|
||||
}
|
||||
const SizeT bracket = name.rfind('[');
|
||||
if (bracket == String::npos || bracket + 1 >= name.length() - 1) return -1;
|
||||
Uint element = 0;
|
||||
for (SizeT i = bracket + 1; i < name.length() - 1; ++i) {
|
||||
if (name[i] < '0' || name[i] > '9') return -1;
|
||||
element = element * 10 + static_cast<Uint>(name[i] - '0');
|
||||
if (element > 0x0FFFFFFFu) return -1;
|
||||
}
|
||||
auto base = Link->uniformLocations.find(name.substr(0, bracket) + "[0]");
|
||||
if (base == Link->uniformLocations.end()) {
|
||||
base = Link->uniformLocations.find(name.substr(0, bracket));
|
||||
if (base == Link->uniformLocations.end()) return -1;
|
||||
}
|
||||
const Int baseLocation = static_cast<Int>(base->second);
|
||||
if (!IsValidUniformLocation(baseLocation)) return -1;
|
||||
const Int index = Link->uniformIndexInTProgram[baseLocation];
|
||||
if (!UniformAt(index).type.isArray) return -1;
|
||||
if (static_cast<GLint>(element) >=
|
||||
MG_State::GLState::ProgramObject::GetUniformArraySizeByTIndex(*Link, index)) {
|
||||
return -1;
|
||||
}
|
||||
const Int location = baseLocation + static_cast<Int>(element);
|
||||
if (!UniformLocationsAliasSameUniform(baseLocation, location)) return -1;
|
||||
return location;
|
||||
}
|
||||
|
||||
const String& ProgramArchiveSource::GetUniformBlockName(Uint index) const {
|
||||
static const String kEmpty;
|
||||
if (index >= Link->glBlockIndexToTProgram.size()) return kEmpty;
|
||||
const Int tBlockIndex = Link->glBlockIndexToTProgram[index];
|
||||
if (tBlockIndex < 0 || static_cast<SizeT>(tBlockIndex) >= Link->blockReflection.size()) {
|
||||
return kEmpty;
|
||||
}
|
||||
return Link->blockReflection[tBlockIndex].name;
|
||||
}
|
||||
|
||||
Uint ProgramArchiveSource::GetUniformBlockBinding(Uint index) const {
|
||||
// THE OVERLAY WINS WHEN THERE IS ONE, and the archive's link-time value stands when
|
||||
// there is not. glUniformBlockBinding moves this AFTER the link that produced the
|
||||
// archive, so a twin answering from the archive alone would bind the program's
|
||||
// blocks to the points it was LINKED with rather than the ones it is BOUND with.
|
||||
if (OverlayGoverns && BlockBindingOverlay != nullptr) {
|
||||
if (index < BlockBindingOverlay->size()) {
|
||||
return static_cast<Uint>((*BlockBindingOverlay)[index]);
|
||||
}
|
||||
// A block past the tail the record declared. The tail is dense over
|
||||
// GetActiveUniformBlocksCount(), so this is a record and an archive that
|
||||
// disagree about how many blocks the program has - which can only happen if a
|
||||
// create and a bindings record crossed. 0 is GL's own default and is the safe
|
||||
// answer; the mismatch is the emitter's to fix, not this read's to hide.
|
||||
return 0;
|
||||
}
|
||||
if (index >= Link->uniformBlockBinding.size()) return 0;
|
||||
return static_cast<Uint>(Link->uniformBlockBinding[index]);
|
||||
}
|
||||
|
||||
Int ProgramArchiveSource::GetUniformSamplerOrImageUnitIndex(Uint location) const {
|
||||
if (OverlayGoverns) {
|
||||
// THE TAIL IS THE WHOLE SET, so a location it does not name has no unit - NOT
|
||||
// whatever the archive's link-time snapshot said. The client emits every
|
||||
// assigned unit it can see, including the ones the link seeded from
|
||||
// layout(binding=N), so "absent" really does mean -1.
|
||||
//
|
||||
// Sparse and ASCENDING BY LOCATION on the wire (CONTRACT-P5E §1), which is what
|
||||
// lets this be a binary search: the build walks every location from 0 to
|
||||
// maxUniformLocation, and a linear scan per location would be quadratic on a
|
||||
// program with a thousand locations and a handful of samplers.
|
||||
if (SamplerUnitOverlay == nullptr) return -1;
|
||||
const auto it = std::lower_bound(
|
||||
SamplerUnitOverlay->begin(), SamplerUnitOverlay->end(), location,
|
||||
[](const MG_Pipe::MGPProgramSamplerUnit& entry, Uint value) {
|
||||
return entry.Location < value;
|
||||
});
|
||||
if (it == SamplerUnitOverlay->end() || it->Location != location) return -1;
|
||||
return static_cast<Int>(it->Unit);
|
||||
}
|
||||
if (location >= Link->uniformSamplerOrImageUnitIndex.size()) return -1;
|
||||
return Link->uniformSamplerOrImageUnitIndex[location];
|
||||
}
|
||||
|
||||
ProgramArchiveSource ProgramArchiveSource::FromFrontend(
|
||||
const MG_State::GLState::ProgramObject& program) {
|
||||
// THE MONOLITH-GLUE HALF (ruling 1 / ID-81). The "archive" is the frontend's own
|
||||
// live tables, which is what makes this arm byte-for-byte what it was: there is no
|
||||
// overlay because the three mutable fields ARE those tables' members, and the
|
||||
// program object is pinned by the draw that is reading it.
|
||||
ProgramArchiveSource source;
|
||||
source.Link = &program.GetLinkReflection();
|
||||
source.Spirv = &program.GetSpirvReflection();
|
||||
source.Identity = program.GetExternalIndex();
|
||||
source.IdentityIsHandleSlot = false;
|
||||
source.Linked = program.GetLinkStatus();
|
||||
source.SpirvUsable = program.GetSpirvStatus();
|
||||
source.SpirvValidationEnabled = program.GetSpirvValidationEnabled();
|
||||
source.PointSizeWasDemoted = program.PointSizeDemoted();
|
||||
source.GlobalUboSize = program.GetUBOSize();
|
||||
source.LinkedStages = program.GetLinkedShaderStages();
|
||||
source.OverlayGoverns = false;
|
||||
source.StorageOverrides = program.GetShaderStorageBlockBindingOverrides();
|
||||
// The function this source's ComputeShaderStorageBlockBindingSignature overload
|
||||
// replaces, so the monolith answer is unchanged and there is still exactly one
|
||||
// place the number comes from on this arm.
|
||||
source.StorageOverrideSignature = ComputeShaderStorageBlockBindingSignatureOf(program);
|
||||
return source;
|
||||
}
|
||||
|
||||
ProgramArchiveSource ProgramArchiveSource::FromRecord(
|
||||
MG_Pipe::MGPipeHandle cso, const MG_Pipe::MGPipeShaderCsoRecord& record) {
|
||||
// THE HANDLE ARM. Every field below is server-owned: the archive the create adopted,
|
||||
// the descriptor the create stored, and the three tails set_program_bindings
|
||||
// carried. Nothing here touches a ProgramObject, which is the property the strict
|
||||
// lane measures.
|
||||
ProgramArchiveSource source;
|
||||
source.Link = &record.Archive->Link;
|
||||
source.Spirv = &record.Archive->Spirv;
|
||||
source.Identity = cso.Slot;
|
||||
source.IdentityIsHandleSlot = true;
|
||||
// LinkStatus IS A FIELD NOW, and that is the whole point of c0e adding it: before
|
||||
// P5e "linked" was only implied by the create existing, which is a different
|
||||
// statement - create_shader_state is re-issued at every link that moves the link
|
||||
// version, and a FAILED relink of a bound program moves it too (ID-88).
|
||||
source.Linked = record.Desc.LinkStatus != 0;
|
||||
source.SpirvUsable = record.Desc.SpirvStatus != 0;
|
||||
source.SpirvValidationEnabled = record.Desc.EnableSpirvValidation != 0;
|
||||
source.PointSizeWasDemoted = record.Desc.PointSizeDemoted != 0;
|
||||
source.GlobalUboSize = record.Desc.GlobalUboSize;
|
||||
source.LinkedStages.reserve(record.Archive->LinkedStages.size());
|
||||
for (const Uint32 stage : record.Archive->LinkedStages) {
|
||||
source.LinkedStages.push_back(static_cast<ShaderStage>(stage));
|
||||
}
|
||||
// "HAS A BINDINGS RECORD EVER BEEN APPLIED TO THIS RECORD" - not "is the tail
|
||||
// non-empty". A program with no uniform blocks, no sampler and no override emits a
|
||||
// record with three empty tails, and that record is the statement that the archive's
|
||||
// link-time values are no longer the answer.
|
||||
source.OverlayGoverns = record.BindingsSerial != 0;
|
||||
source.BlockBindingOverlay = &record.BlockBindings;
|
||||
source.SamplerUnitOverlay = &record.SamplerUnits;
|
||||
if (source.OverlayGoverns) {
|
||||
source.StorageOverrides.reserve(record.StorageOverrides.size());
|
||||
for (const auto& entry : record.StorageOverrides) {
|
||||
source.StorageOverrides[entry.Name] = entry.Binding;
|
||||
}
|
||||
} else {
|
||||
source.StorageOverrides = record.Archive->Link.shaderStorageBlockBinding;
|
||||
}
|
||||
// THE RECORD'S NUMBER EITHER WAY, and deliberately not a recomputation when the
|
||||
// overlay is absent. This is the value the twin STAMPS and the draw path's clean
|
||||
// clause COMPARES (CONTRACT-P5E §5.5 names record.Signature as the input), so the
|
||||
// two have to be the same field or every draw of a program with an override
|
||||
// rebuilds. A record with no bindings applied carries 0 while the archive may hold
|
||||
// a link-time override map - the next set_program_bindings then produces a non-zero
|
||||
// signature and forces exactly one rebuild, which is correct and, since the emitter
|
||||
// sends the bindings in the same breath as the create, not a state a draw reaches.
|
||||
source.StorageOverrideSignature = record.Signature;
|
||||
return source;
|
||||
}
|
||||
|
||||
Uint ProgramBlockBindingFromRecord(const MG_Pipe::MGPipeShaderCsoRecord& record, Int blockIndex) {
|
||||
if (blockIndex < 0) return 0;
|
||||
const SizeT index = static_cast<SizeT>(blockIndex);
|
||||
if (record.BindingsSerial != 0) {
|
||||
// The tail is the whole set; a block past it is a record and an archive that
|
||||
// disagree about the block count, and GL's own default of 0 is the safe answer.
|
||||
return index < record.BlockBindings.size()
|
||||
? static_cast<Uint>(record.BlockBindings[index])
|
||||
: 0;
|
||||
}
|
||||
if (!record.Archive || index >= record.Archive->Link.uniformBlockBinding.size()) return 0;
|
||||
return static_cast<Uint>(record.Archive->Link.uniformBlockBinding[index]);
|
||||
}
|
||||
|
||||
Int ProgramSamplerUnitFromRecord(const MG_Pipe::MGPipeShaderCsoRecord& record, Uint location) {
|
||||
if (record.BindingsSerial != 0) {
|
||||
const auto it = std::lower_bound(
|
||||
record.SamplerUnits.begin(), record.SamplerUnits.end(), location,
|
||||
[](const MG_Pipe::MGPProgramSamplerUnit& entry, Uint value) {
|
||||
return entry.Location < value;
|
||||
});
|
||||
if (it == record.SamplerUnits.end() || it->Location != location) return -1;
|
||||
return static_cast<Int>(it->Unit);
|
||||
}
|
||||
if (!record.Archive ||
|
||||
location >= record.Archive->Link.uniformSamplerOrImageUnitIndex.size()) {
|
||||
return -1;
|
||||
}
|
||||
return record.Archive->Link.uniformSamplerOrImageUnitIndex[location];
|
||||
}
|
||||
|
||||
// P5e (id), CONTRACT-P5E §4.1. See Managers.h for the contract - in particular why the
|
||||
// composite band had to land in the same package as this function.
|
||||
BackendProgramObjectImpl* ResolveProgramTwin(MG_Pipe::MGPipeHandle cso) {
|
||||
@@ -10978,8 +11203,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
void ReseedShaderStorageBlockBindings(Uint backendProgramId,
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject) {
|
||||
const auto& overrides = stateProgramObject.GetShaderStorageBlockBindingOverrides();
|
||||
const ProgramBuildSource& src) {
|
||||
const auto& overrides = src.GetShaderStorageBlockBindingOverrides();
|
||||
if (overrides.empty()) return; // the overwhelming majority of programs
|
||||
for (const auto& [blockName, binding] : overrides) {
|
||||
if (binding < 0) continue;
|
||||
@@ -10987,9 +11212,51 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
Uint64 ComputeShaderStorageBlockBindingSignature(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject) {
|
||||
const auto& overrides = stateProgramObject.GetShaderStorageBlockBindingOverrides();
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5e (pg). ON A PUSH BUILD THE SIGNATURE IS THE SOURCE'S, TAKEN VERBATIM, and it is not
|
||||
// recomputed here for a reason worth stating: on the handle arm the number the DRAW PATH
|
||||
// compares against is MGPipeShaderCsoRecord::Signature - the CLIENT's commutative hash,
|
||||
// computed in MG_Impl/Pipe/ProgramEmit.h. If the server recomputed its own from the
|
||||
// record's override tail, the two formulas would have to stay bit-identical for ever or
|
||||
// every draw of every program with an override would rebuild. Taking the client's number
|
||||
// verbatim removes that coupling: there is one authority, and the twin's stamp and the
|
||||
// clean clause read the same field. The monolith arm's source carries the answer of the
|
||||
// function this replaces, computed from the frontend map, so its behaviour is unchanged.
|
||||
Uint64 ComputeShaderStorageBlockBindingSignature(const ProgramBuildSource& src) {
|
||||
return src.StorageOverrideSignature;
|
||||
}
|
||||
|
||||
// The COMPUTATION, still here and still the only one on this side: FromFrontend seeds a
|
||||
// monolith source with it, and the monolith arm of SyncCurrentProgram's clean condition
|
||||
// asks it per draw exactly as it always did. In a pull build this function does not
|
||||
// exist and the overload above IS this body (see the #else), so the pull build gains no
|
||||
// symbol - G1's rule for every new name in this phase.
|
||||
Uint64 ComputeShaderStorageBlockBindingSignatureOf(
|
||||
const MG_State::GLState::ProgramObject& program) {
|
||||
const auto& overrides = program.GetShaderStorageBlockBindingOverrides();
|
||||
if (overrides.empty()) return 0; // the overwhelming majority of programs
|
||||
// Order-independent on purpose: the source is an UnorderedMap, so any signature that
|
||||
// depended on iteration order would differ between two identical override sets and
|
||||
// rebuild the program for nothing. Built from the VALUES, not from a change counter,
|
||||
// so re-setting a block to the binding it already carries forces no rebuild - which
|
||||
// is what the pipeline composite's per-draw uniform mirror depends on.
|
||||
//
|
||||
// MG_Impl/Pipe/ProgramEmit.h's MGPipeStorageOverrideSignatureEntry is this entry
|
||||
// mix, verbatim, on the client side; the two are twins with a named pointer at each
|
||||
// other because MG_Backend includes nothing from MG_Impl.
|
||||
Uint64 signature = 0;
|
||||
for (const auto& [blockName, binding] : overrides) {
|
||||
if (binding < 0) continue; // never rebound; the declared qualifier still stands
|
||||
Uint64 entry = std::hash<String>{}(blockName);
|
||||
entry ^= (static_cast<Uint64>(static_cast<Uint32>(binding)) + 0x9e3779b97f4a7c15ull +
|
||||
(entry << 6) + (entry >> 2));
|
||||
signature += entry; // commutative combine
|
||||
}
|
||||
return signature;
|
||||
}
|
||||
#else
|
||||
Uint64 ComputeShaderStorageBlockBindingSignature(const ProgramBuildSource& src) {
|
||||
const auto& overrides = src.GetShaderStorageBlockBindingOverrides();
|
||||
if (overrides.empty()) return 0; // the overwhelming majority of programs
|
||||
// Order-independent on purpose: the source is an UnorderedMap, so any signature that
|
||||
// depended on iteration order would differ between two identical override sets and
|
||||
@@ -11011,6 +11278,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
return signature;
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
// The GL internal format bound to an image unit right now. GL_NONE for a unit
|
||||
@@ -11187,8 +11455,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// reflection: which image uniforms declared NO format (the only ones a bake may touch -
|
||||
// a declared format is authoritative and stays), what the units they address currently
|
||||
// hold, and whether any format in play - declared or baked - is outside the ES core set.
|
||||
ImageFormatBakeInputs CollectImageFormatBakeInputs(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject) {
|
||||
ImageFormatBakeInputs CollectImageFormatBakeInputs(const ProgramBuildSource& src) {
|
||||
ImageFormatBakeInputs inputs;
|
||||
// A format GLSL ES cannot spell on a driver with no GL_NV_image_formats to spell it
|
||||
// with. There is no legal ESSL for such a shader at all, so the stage will not
|
||||
@@ -11206,12 +11473,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
++unspellableCount;
|
||||
};
|
||||
|
||||
const Uint maxUniformLoc = stateProgramObject.GetMaxUniformLocation();
|
||||
const Uint maxUniformLoc = src.GetMaxUniformLocation();
|
||||
for (Uint loc = 0; loc <= maxUniformLoc; ++loc) {
|
||||
const auto& name = stateProgramObject.GetUniformName(loc);
|
||||
const auto& name = src.GetUniformName(loc);
|
||||
if (name.empty()) continue;
|
||||
if (!IsImageUniformType(stateProgramObject.GetUniformType(loc))) continue;
|
||||
const auto& type = stateProgramObject.GetUniformTypeFacts(loc);
|
||||
if (!IsImageUniformType(src.GetUniformType(loc))) continue;
|
||||
const auto& type = src.GetUniformTypeFacts(loc);
|
||||
if (type.hasFormat) {
|
||||
// Declared, and therefore never overridden by the BAKE - but a non-core
|
||||
// spelling still has to become legal ESSL somehow.
|
||||
@@ -11237,7 +11504,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const Int unit = stateProgramObject.GetUniformSamplerOrImageUnitIndex(loc);
|
||||
const Int unit = src.GetUniformSamplerOrImageUnitIndex(loc);
|
||||
if (unit < 0 || unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) continue;
|
||||
const Uint boundFormat = BoundImageUnitFormat(unit);
|
||||
|
||||
@@ -11356,27 +11623,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// so there is nothing to fix at the API end and the emitted text has to carry it
|
||||
// (RemapImageArrayElementUnits). Empty for every program that does not do this, which is
|
||||
// very nearly all of them - one walk of the reflection and no allocation in that case.
|
||||
Vector<ImageArrayUnitPlan> CollectNonConsecutiveImageArrayPlans(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject) {
|
||||
Vector<ImageArrayUnitPlan> CollectNonConsecutiveImageArrayPlans(const ProgramBuildSource& src) {
|
||||
Vector<ImageArrayUnitPlan> plans;
|
||||
const Uint maxUniformLoc = stateProgramObject.GetMaxUniformLocation();
|
||||
const Uint maxUniformLoc = src.GetMaxUniformLocation();
|
||||
for (Uint loc = 0; loc <= maxUniformLoc; ++loc) {
|
||||
const auto& name = stateProgramObject.GetUniformName(loc);
|
||||
const auto& name = src.GetUniformName(loc);
|
||||
if (name.empty()) continue;
|
||||
if (!IsImageUniformType(stateProgramObject.GetUniformType(loc))) continue;
|
||||
if (!IsImageUniformType(src.GetUniformType(loc))) continue;
|
||||
// Reflection repeats the array's "g_image[0]" spelling at EVERY location the array
|
||||
// spans, so only the location that name resolves back to is the array itself.
|
||||
if (stateProgramObject.GetUniformLocation(name) != static_cast<Int>(loc)) continue;
|
||||
if (src.GetUniformLocation(name) != static_cast<Int>(loc)) continue;
|
||||
const String baseName = ImageUniformBaseName(name);
|
||||
if (baseName == name) continue; // a scalar image: one binding says it all
|
||||
|
||||
ImageArrayUnitPlan plan;
|
||||
plan.name = baseName;
|
||||
for (Uint element = loc; element <= maxUniformLoc &&
|
||||
stateProgramObject.UniformLocationsAliasSameUniform(
|
||||
src.UniformLocationsAliasSameUniform(
|
||||
static_cast<Int>(loc), static_cast<Int>(element));
|
||||
++element) {
|
||||
plan.units.push_back(stateProgramObject.GetUniformSamplerOrImageUnitIndex(element));
|
||||
plan.units.push_back(src.GetUniformSamplerOrImageUnitIndex(element));
|
||||
}
|
||||
if (plan.units.size() < 2) continue;
|
||||
|
||||
@@ -11972,7 +12238,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// the program exactly as dead as it already was - but with a driver log that says why,
|
||||
// where today there is an empty one.
|
||||
void BackendProgramObjectImpl::AttachPassthroughTessControlStage(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject, const Int tessEvalShaderIndex,
|
||||
const ProgramBuildSource& src, const Int tessEvalShaderIndex,
|
||||
const Vector<Vector<unsigned int>>& shaderSpirvs, const String& vertexStageEssl,
|
||||
const String& tessEvalStageEssl) {
|
||||
// PATCH_VERTICES is dynamic state, and it decides the synthesized stage's output
|
||||
@@ -11999,7 +12265,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_E("Program %u has a tessellation evaluation stage with no control stage, but no "
|
||||
"SPIR-V for it; the pass-through control stage GL describes cannot be checked, so "
|
||||
"the program is left to fail its ES link.",
|
||||
stateProgramObject.GetExternalIndex());
|
||||
src.GetExternalIndex());
|
||||
m_backendProgramUsable = false;
|
||||
return;
|
||||
}
|
||||
@@ -12015,7 +12281,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_E("Program %u has a tessellation evaluation stage with no control stage AND reads a "
|
||||
"user-defined input through it; a synthesized pass-through control stage cannot "
|
||||
"forward that, so the program is declined rather than fed an undefined varying.",
|
||||
stateProgramObject.GetExternalIndex());
|
||||
src.GetExternalIndex());
|
||||
m_backendProgramUsable = false;
|
||||
return;
|
||||
}
|
||||
@@ -12053,7 +12319,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (backendShaderId == 0) {
|
||||
MGLOG_E("Failed to create the synthesized pass-through tessellation control shader for "
|
||||
"program %u.",
|
||||
stateProgramObject.GetExternalIndex());
|
||||
src.GetExternalIndex());
|
||||
m_backendProgramUsable = false;
|
||||
return;
|
||||
}
|
||||
@@ -12061,7 +12327,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const char* sourceCStr = source.c_str();
|
||||
MGLOG_D("Synthesized pass-through tessellation control stage for program %u (patch vertices "
|
||||
"%u):\n%s",
|
||||
stateProgramObject.GetExternalIndex(), patchVertices, sourceCStr);
|
||||
src.GetExternalIndex(), patchVertices, sourceCStr);
|
||||
g_GLESFuncs.glShaderSource(backendShaderId, 1, &sourceCStr, nullptr);
|
||||
g_GLESFuncs.glCompileShader(backendShaderId);
|
||||
|
||||
@@ -12078,7 +12344,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
log.back() = '\0';
|
||||
MGLOG_E("The synthesized pass-through tessellation control stage failed to compile for "
|
||||
"program %u. Driver log: %s\nSource:\n%s",
|
||||
stateProgramObject.GetExternalIndex(), log.data(), sourceCStr);
|
||||
src.GetExternalIndex(), log.data(), sourceCStr);
|
||||
m_backendProgramUsable = false;
|
||||
g_GLESFuncs.glDeleteShader(backendShaderId);
|
||||
return;
|
||||
@@ -12090,6 +12356,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glDeleteShader(backendShaderId);
|
||||
}
|
||||
|
||||
// ---- THE PROGRAM BUILD, AND THE ONE SOURCE IT READS (P5e pg) ----------------------
|
||||
//
|
||||
// ONE BODY, TWO HEADS, AND THE PULL BUILD'S HEAD IS THE ORIGINAL ONE. In the pull build
|
||||
// `ProgramBuildSource` IS `MG_State::GLState::ProgramObject`, this function keeps its
|
||||
// name, its signature and its null guard, and `src` is a reference to the object it was
|
||||
// already dereferencing on every line - so the pull build compiles the same reads
|
||||
// through the same accessors and G1's byte identity survives a rename and nothing else.
|
||||
//
|
||||
// In a PUSH build the body is a private worker that the two public overloads feed:
|
||||
// SyncToBackend(program) - the monolith-glue half, ruling 1 - wraps the frontend's own
|
||||
// archive, and SyncToBackendByHandle(cso) wraps the RECORD's. That is the whole of this
|
||||
// family's rule F: after this, nothing in a program build dereferences a frontend
|
||||
// object, and the forty accessor calls below read memory the server owns.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
void BackendProgramObjectImpl::SyncToBackendFromSource(const ProgramBuildSource& src) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
#else
|
||||
void BackendProgramObjectImpl::SyncToBackend(
|
||||
const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {
|
||||
#ifdef TRACY_ENABLE
|
||||
@@ -12099,24 +12384,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_E_ONCE("State program object is null, skipping backend sync.");
|
||||
return;
|
||||
}
|
||||
const ProgramBuildSource& src = *stateProgramObject;
|
||||
#endif
|
||||
// Recorded before either early return below, so Use() can always name the GL
|
||||
// program a no-op draw belongs to - including the "linked but not drawable" exit.
|
||||
m_frontendProgramId = stateProgramObject->GetExternalIndex();
|
||||
m_frontendProgramId = src.GetExternalIndex();
|
||||
|
||||
// GetSpirvStatus() as well as GetLinkStatus(): a program whose phase-B job was
|
||||
// cancelled (teardown) or whose optimizer run failed is fully linked and fully
|
||||
// queryable, but has no SPIR-V to build a driver program out of. GL cannot retract
|
||||
// a LINK_STATUS it already reported true, so "linked but not drawable" is the
|
||||
// answer, and this is where the ES backend expresses it.
|
||||
if (!stateProgramObject->GetLinkStatus() || !stateProgramObject->GetSpirvStatus()) {
|
||||
if (!src.GetLinkStatus() || !src.GetSpirvStatus()) {
|
||||
MGLOG_E_ONCE("Program object is not linked or has no generated SPIR-V, skipping backend sync. State "
|
||||
"program ID: %u",
|
||||
stateProgramObject->GetExternalIndex());
|
||||
src.GetExternalIndex());
|
||||
return;
|
||||
}
|
||||
|
||||
MGLOG_D("Syncing program to backend. State program ID: %u, Backend ID: %u",
|
||||
stateProgramObject->GetExternalIndex(), m_backendProgramId);
|
||||
src.GetExternalIndex(), m_backendProgramId);
|
||||
// Every link-derived cache below (incl. m_samplerUniformBindings and its
|
||||
// lastAssignedUnit/lastAssignedLodBias program-state mirrors) is rebuilt;
|
||||
// the sampler-pass memo keyed on them must not survive.
|
||||
@@ -12128,8 +12415,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// The generated ESSL bakes these in (see the SetShaderStorageBlockBinding call in the
|
||||
// transpile loop below), so the set they were generated against is part of what makes
|
||||
// this build current - the draw path compares the signature and rebuilds on a change.
|
||||
const auto& storageBlockBindingOverrides = stateProgramObject->GetShaderStorageBlockBindingOverrides();
|
||||
m_shaderStorageBlockBindingSignature = ComputeShaderStorageBlockBindingSignature(*stateProgramObject);
|
||||
const auto& storageBlockBindingOverrides = src.GetShaderStorageBlockBindingOverrides();
|
||||
m_shaderStorageBlockBindingSignature = ComputeShaderStorageBlockBindingSignature(src);
|
||||
// Rebuilt by the transpile loop below, one entry per atomic-counter block it finds.
|
||||
// The top is snapshotted here so every stage of this program - and the draw path
|
||||
// reading it afterwards - resolves the same slot for the same GL binding.
|
||||
@@ -12146,19 +12433,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// compiles to depends on live glBindImageTexture state, so the pairs it was built
|
||||
// against are recorded here and compared per draw (ImageUnitFormatsStillMatch).
|
||||
// Taken BEFORE the transpile loop so both the bake and the key see one snapshot.
|
||||
const ImageFormatBakeInputs imageFormatBake = CollectImageFormatBakeInputs(*stateProgramObject);
|
||||
const ImageFormatBakeInputs imageFormatBake = CollectImageFormatBakeInputs(src);
|
||||
m_formatlessImageUnits = imageFormatBake.units;
|
||||
m_imageUnitFormatSignature = imageFormatBake.signature;
|
||||
for (const auto& conflicted : imageFormatBake.conflictedNames) {
|
||||
MGLOG_D("Image uniform '%s' of program %u declares no format and its elements address units with "
|
||||
"different bound formats; left format-less.",
|
||||
conflicted.c_str(), stateProgramObject->GetExternalIndex());
|
||||
conflicted.c_str(), src.GetExternalIndex());
|
||||
}
|
||||
// ...and once more for image ARRAYS whose per-element units are not consecutive, which
|
||||
// ESSL has no way to express in one declaration. Program-wide, like the bake, and read
|
||||
// from the same snapshot of the reflection; the per-stage rewrite happens below.
|
||||
const Vector<ImageArrayUnitPlan> nonConsecutiveImageArrays =
|
||||
CollectNonConsecutiveImageArrayPlans(*stateProgramObject);
|
||||
CollectNonConsecutiveImageArrayPlans(src);
|
||||
|
||||
// Detach all existing shaders
|
||||
GLint attachedCount = 0;
|
||||
@@ -12195,19 +12482,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// straight off the end of shaderSpirvs (a std::vector copy from garbage, which is
|
||||
// how this crashed). GetLinkedShaderStages() is the list the modules were generated
|
||||
// from, one entry per module, in module order.
|
||||
const Vector<ShaderStage> linkedStages = stateProgramObject->GetLinkedShaderStages();
|
||||
auto& shaderSpirvs = stateProgramObject->GetGeneratedSpirv();
|
||||
const Vector<ShaderStage> linkedStages = src.GetLinkedShaderStages();
|
||||
auto& shaderSpirvs = src.GetGeneratedSpirv();
|
||||
// Both come from the same Link(), so they agree by construction. If they ever did
|
||||
// not there would be no index this function could safely use for EITHER array, so
|
||||
// this refuses the build instead of picking one and hoping.
|
||||
if (linkedStages.size() != shaderSpirvs.size()) {
|
||||
MGLOG_E_ONCE("Program %u: %zu linked stage(s) but %zu generated SPIR-V module(s); refusing to "
|
||||
"build a backend program from mismatched link artifacts.",
|
||||
stateProgramObject->GetExternalIndex(), linkedStages.size(), shaderSpirvs.size());
|
||||
src.GetExternalIndex(), linkedStages.size(), shaderSpirvs.size());
|
||||
m_backendProgramUsable = false;
|
||||
return;
|
||||
}
|
||||
if (stateProgramObject->PointSizeDemoted()) {
|
||||
if (src.PointSizeDemoted()) {
|
||||
// THE ARMING SIGNAL, INFO on purpose and latched: the integration lane that
|
||||
// pins MOBILEGL_POINT_SIZE_DEMOTION=1 asserts on exactly this line, because
|
||||
// every rendering assertion stays green on a healthy driver whether the
|
||||
@@ -12217,7 +12504,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"in those stages.");
|
||||
}
|
||||
MGLOG_D("Attaching %zu shaders to program %u", linkedStages.size(), m_backendProgramId);
|
||||
for (const auto& ref : stateProgramObject->GetLinkedShaderSnapshot()) {
|
||||
for (const auto& ref : src.GetLinkedShaderSnapshot()) {
|
||||
if (!ref.shader) continue;
|
||||
const auto& stage =
|
||||
MG_Util::ConvertGLEnumToString(MG_Util::ConvertShaderStageToGLEnum(ref.shader->GetShaderStage()));
|
||||
@@ -12226,7 +12513,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_D("Original src @ %s: \n", stage.c_str());
|
||||
MGLOG_D("%s:", src.empty() ? "" : src.c_str());
|
||||
}
|
||||
const Bool enableSpirvValidation = stateProgramObject->GetSpirvValidationEnabled();
|
||||
const Bool enableSpirvValidation = src.GetSpirvValidationEnabled();
|
||||
|
||||
// Blocks a transform-feedback capture request names a member of ("StageData" of
|
||||
// "StageData.attrib[0]"). The Adreno ES driver accepts such a request, links, and
|
||||
@@ -12235,7 +12522,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// consumer keep matching. gl_PerVertex members ("gl_Position") carry no block
|
||||
// prefix and so never enter this set.
|
||||
std::set<String> xfbCaptureBlockNames;
|
||||
for (const auto& xfbVarying : stateProgramObject->GetTransformFeedbackVaryings()) {
|
||||
for (const auto& xfbVarying : src.GetTransformFeedbackVaryings()) {
|
||||
const SizeT dot = xfbVarying.name.find('.');
|
||||
if (dot != String::npos && dot > 0) {
|
||||
xfbCaptureBlockNames.insert(xfbVarying.name.substr(0, dot));
|
||||
@@ -12562,7 +12849,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// KHR-GL43.vertex_attrib_binding family behind "the draw captured zeros".
|
||||
MGLOG_E("Shader transpilation to ESSL failed. State program ID: %u, stage: %s, "
|
||||
"SPIRV-Cross error: %s",
|
||||
stateProgramObject->GetExternalIndex(),
|
||||
src.GetExternalIndex(),
|
||||
MG_Util::ConvertGLEnumToString(glShaderType).c_str(),
|
||||
transpileError.c_str());
|
||||
m_backendProgramUsable = false;
|
||||
@@ -12654,13 +12941,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"available on this device.",
|
||||
tessellationStage ? "tessellation" : "geometry",
|
||||
tessellationStage ? "tessellation" : "geometry",
|
||||
stateProgramObject->GetExternalIndex());
|
||||
src.GetExternalIndex());
|
||||
}
|
||||
source = RequestPointSizeExtension(std::move(source), pointSizeExtension);
|
||||
}
|
||||
}
|
||||
|
||||
source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject);
|
||||
source = RebindImageUniformsToFrontendUnits(std::move(source), src);
|
||||
// The completion half of the format bake, for the formats SPIRV-Cross throws on
|
||||
// rather than prints (r8ui and the rest of its desktop-only set). Empty for every
|
||||
// program whose format-less images bound a format the module could carry, which
|
||||
@@ -12690,7 +12977,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_E("Image array %s. Its elements address image units GLSL ES cannot be made to reach "
|
||||
"from one declaration, so this stage will read and write the WRONG units. State "
|
||||
"program ID: %u, stage: %s.",
|
||||
declined.c_str(), stateProgramObject->GetExternalIndex(),
|
||||
declined.c_str(), src.GetExternalIndex(),
|
||||
MG_Util::ConvertGLEnumToString(glShaderType).c_str());
|
||||
}
|
||||
}
|
||||
@@ -12735,7 +13022,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_E("Program %u routes gl_ViewportIndex but its fragment stage has no "
|
||||
"entry point to gate, so the routing cannot be emulated: every "
|
||||
"index will rasterize against viewport 0. State program ID: %u.",
|
||||
m_backendProgramId, stateProgramObject->GetExternalIndex());
|
||||
m_backendProgramId, src.GetExternalIndex());
|
||||
}
|
||||
} else if (PromoteViewportIndexGlobalToVarying(source)) {
|
||||
programRoutesViewportIndex = true;
|
||||
@@ -12816,7 +13103,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
MGLOG_E("Shader compilation failed. State program ID: %u, stage: %s, backend shader ID: "
|
||||
"%u, driver log: %s\nSource:\n%s",
|
||||
stateProgramObject->GetExternalIndex(),
|
||||
src.GetExternalIndex(),
|
||||
MG_Util::ConvertGLEnumToString(glShaderType).c_str(), backendShaderId,
|
||||
log.data(), sourceForLog);
|
||||
m_backendProgramUsable = false;
|
||||
@@ -12857,7 +13144,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
if (needsPassthroughTessControl) {
|
||||
AttachPassthroughTessControlStage(*stateProgramObject, tessEvalShaderIndex, shaderSpirvs,
|
||||
AttachPassthroughTessControlStage(src, tessEvalShaderIndex, shaderSpirvs,
|
||||
vertexStageEssl, tessEvalStageEssl);
|
||||
}
|
||||
|
||||
@@ -12877,9 +13164,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// the transpiled ESSL (`out vec4 result_0;` stays `result_0`), so the
|
||||
// frontend's requested names carry over unchanged.
|
||||
SizeT declaredXfbVaryingCount = 0;
|
||||
if (stateProgramObject->GetTransformFeedbackVaryingCount() > 0 &&
|
||||
if (src.GetTransformFeedbackVaryingCount() > 0 &&
|
||||
g_GLESFuncs.glTransformFeedbackVaryings != nullptr) {
|
||||
const auto& xfbVaryings = stateProgramObject->GetTransformFeedbackVaryings();
|
||||
const auto& xfbVaryings = src.GetTransformFeedbackVaryings();
|
||||
Vector<const GLchar*> xfbNames;
|
||||
xfbNames.reserve(xfbVaryings.size());
|
||||
// A block this build flattened no longer HAS the member the application asked
|
||||
@@ -12894,7 +13181,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// capture stage is the vertex shader keeps the built-in and its spelling,
|
||||
// whatever happened to a control stage behind it.
|
||||
Bool captureStageDemoted = false;
|
||||
if (stateProgramObject->PointSizeDemoted()) {
|
||||
if (src.PointSizeDemoted()) {
|
||||
for (const ShaderStage linkedStage : linkedStages) {
|
||||
if (linkedStage == ShaderStage::TessEval || linkedStage == ShaderStage::Geometry) {
|
||||
captureStageDemoted = true;
|
||||
@@ -12927,7 +13214,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
g_GLESFuncs.glTransformFeedbackVaryings(m_backendProgramId, static_cast<GLsizei>(xfbNames.size()),
|
||||
xfbNames.data(),
|
||||
stateProgramObject->GetTransformFeedbackBufferMode());
|
||||
src.GetTransformFeedbackBufferMode());
|
||||
// Unchecked before. A rejected capture set leaves the program linking happily
|
||||
// with NO capture set at all, and then every draw of every span records
|
||||
// nothing while the application reads its buffer's pre-draw bytes and
|
||||
@@ -12942,8 +13229,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"%s (mode %s): [%s]. Every capture made with GL program %u will record nothing.",
|
||||
m_backendProgramId, MG_Util::ConvertGLEnumToString(xfbError).c_str(),
|
||||
MG_Util::ConvertGLEnumToString(
|
||||
stateProgramObject->GetTransformFeedbackBufferMode()).c_str(),
|
||||
declared.c_str(), stateProgramObject->GetExternalIndex());
|
||||
src.GetTransformFeedbackBufferMode()).c_str(),
|
||||
declared.c_str(), src.GetExternalIndex());
|
||||
}
|
||||
declaredXfbVaryingCount = xfbNames.size();
|
||||
}
|
||||
@@ -12966,7 +13253,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// links nothing no-ops every draw that uses it, and that has to be readable
|
||||
// in an INFO-level artifact.
|
||||
MGLOG_E("Program linking failed. State program ID: %u, backend program ID: %u, driver log: %s",
|
||||
stateProgramObject->GetExternalIndex(), m_backendProgramId, log.data());
|
||||
src.GetExternalIndex(), m_backendProgramId, log.data());
|
||||
// The one link failure MobileGL can name a cause for that the driver's log never
|
||||
// will: ESSL has no legal single declaration for a read+write image outside
|
||||
// r32f/r32i/r32ui, so those are split into a coherent pair and the stage ends up
|
||||
@@ -13006,13 +13293,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
&linkedXfbBufferMode);
|
||||
for (Int i = 0; i < kMaxDrainedProgramErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
|
||||
}
|
||||
const GLenum requestedMode = stateProgramObject->GetTransformFeedbackBufferMode();
|
||||
const GLenum requestedMode = src.GetTransformFeedbackBufferMode();
|
||||
if (static_cast<SizeT>(std::max(linkedXfbVaryings, 0)) != declaredXfbVaryingCount ||
|
||||
static_cast<GLenum>(linkedXfbBufferMode) != requestedMode) {
|
||||
MGLOG_E("Backend program %u (GL program %u) linked with a capture set the driver does not "
|
||||
"agree with: asked for %zu varying(s) in mode %s, the driver reports %d varying(s) "
|
||||
"in mode %s. Captures made with it will be empty or wrongly laid out.",
|
||||
m_backendProgramId, stateProgramObject->GetExternalIndex(), declaredXfbVaryingCount,
|
||||
m_backendProgramId, src.GetExternalIndex(), declaredXfbVaryingCount,
|
||||
MG_Util::ConvertGLEnumToString(requestedMode).c_str(), linkedXfbVaryings,
|
||||
MG_Util::ConvertGLEnumToString(
|
||||
static_cast<GLenum>(linkedXfbBufferMode)).c_str());
|
||||
@@ -13077,55 +13364,75 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
// Create global UBO
|
||||
if (stateProgramObject->GetUBOSize() > 0) {
|
||||
if (src.GetUBOSize() > 0) {
|
||||
g_GLESFuncs.glGenBuffers(1, &m_backendGlobalUBOId);
|
||||
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, m_backendGlobalUBOId);
|
||||
g_GLESFuncs.glBufferData(GL_UNIFORM_BUFFER, stateProgramObject->GetUBOSize(), nullptr, GL_STREAM_DRAW);
|
||||
g_GLESFuncs.glBufferData(GL_UNIFORM_BUFFER, src.GetUBOSize(), nullptr, GL_STREAM_DRAW);
|
||||
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||
} else {
|
||||
m_backendGlobalUBOId = 0;
|
||||
}
|
||||
|
||||
CacheResourceLocations(stateProgramObject);
|
||||
CacheResourceLocations(src);
|
||||
// NOT the mechanism that makes a rebinding work - the transpiled qualifier above is.
|
||||
// glShaderStorageBlockBinding is a GL 4.3 entry point that no real ES driver exposes,
|
||||
// so this replay is a no-op almost everywhere; it stays because it is still correct
|
||||
// (and cheaper than a rebuild) on a driver that does expose it, e.g. a desktop GL
|
||||
// driver used as the ES backend. AFTER the link either way, because it needs the
|
||||
// driver's linked interface.
|
||||
ReseedShaderStorageBlockBindings(m_backendProgramId, *stateProgramObject);
|
||||
ReseedShaderStorageBlockBindings(m_backendProgramId, src);
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
m_syncedLinkVersion = src.GetLinkVersion();
|
||||
m_syncedImageUnitVersion = src.GetImageUnitVersion();
|
||||
#endif
|
||||
|
||||
m_isInitialized = true;
|
||||
MGLOG_D("Program sync completed. backend ID %u", m_backendProgramId);
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// ---- the two public heads (P5e pg) -----------------------------------------------
|
||||
//
|
||||
// THE CLEAN KEYS ARE STAMPED HERE AND NOT IN THE WORKER, because which key is
|
||||
// authoritative is exactly what distinguishes the two arms - and a worker that stamped
|
||||
// both would be claiming to know something it was not told. The monolith half keeps the
|
||||
// frontend's two versions (and the record serial P4a added beside them); the handle half
|
||||
// keeps the record's two serials and leaves the frontend versions at their
|
||||
// never-matched sentinel, which is honest: on that arm nothing may ask a ProgramObject
|
||||
// what its link version is.
|
||||
void BackendProgramObjectImpl::SyncToBackend(
|
||||
const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (!stateProgramObject) {
|
||||
MGLOG_E_ONCE("State program object is null, skipping backend sync.");
|
||||
return;
|
||||
}
|
||||
SyncToBackendFromSource(ProgramArchiveSource::FromFrontend(*stateProgramObject));
|
||||
m_syncedLinkVersion = stateProgramObject->GetLinkVersion();
|
||||
m_syncedImageUnitVersion = stateProgramObject->GetImageUnitVersion();
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P4a (D-B3): the ShaderCso record's Serial, stamped in the same breath as the two
|
||||
// frontend versions it replaces. GetSyncedShaderCsoSerial() beside
|
||||
// GetSyncedLinkVersion()/GetSyncedImageUnitVersion() is what the draw path's
|
||||
// nine-clause rebuild condition reads on the handle arm; the clause COUNT does not
|
||||
// shrink, its inputs move (D-H5).
|
||||
//
|
||||
// WHAT DOES *NOT* MOVE, and it is a design statement rather than an omission: the
|
||||
// ARTEFACTS. D-H3 rules that in monolith all seven MGPBlobRefs are declared with
|
||||
// Size 0 - "this record does not declare its blob" - and the LinkArtifacts /
|
||||
// SpirvArtifacts ride beside the record through the entry point's companion
|
||||
// pointers, so the applier stores the DESCRIPTOR and the identity and the server
|
||||
// reads the frontend's own archive. That is what keeps the codec off the monolith
|
||||
// hot path entirely, and it is why every artefact read above is still a read of
|
||||
// stateProgramObject. The verify build is where the codec is exercised, by
|
||||
// serialising, deserialising and field-comparing before storing.
|
||||
if (ProgramSubsystemEnabled()) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5c (G6, CONTRACT-P5C §5.4): the program's ShaderCso handle is resolved by
|
||||
// frontend identity below - frontend-keyed twin resolution, named debt inside
|
||||
// the scope - P3b/P4b rekeys the registry onto handles.
|
||||
// P5c (G6, CONTRACT-P5C §5.4): MONOLITH GLUE ONLY. This is the frontend-keyed
|
||||
// lookup the phase exists to retire, and it survives here for the reason
|
||||
// ruling 1 (ID-81) gives: under Transport=monolith the push build keeps its
|
||||
// frontend arms token for token, and this arm is only ever reached from them.
|
||||
// The handle half below names no frontend identity at all.
|
||||
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
|
||||
#endif
|
||||
// MONOLITH GLUE: the ShaderCso handle of a program this backend still arrives
|
||||
// holding. The reader hides the composite band, so a program-pipeline composite
|
||||
// - which the server must never learn is one - resolves through the same call.
|
||||
// The reader hides the composite band, so a program-pipeline composite - which
|
||||
// the server must never learn is one - resolves through the same call.
|
||||
const MG_Pipe::MGPipeHandle cso = g_backendProgramObjects.HandleOf(stateProgramObject.get());
|
||||
const auto* record = PipeShaderCsoRecordForHandle(cso);
|
||||
if (record != nullptr) {
|
||||
m_syncedShaderCsoSerial = record->Serial;
|
||||
m_syncedBindingsSerial = record->BindingsSerial;
|
||||
} else {
|
||||
// NOT a fall-back and not a silent zero: a stamped 0 would make every later
|
||||
// serial compare fire for ever, which is the safe direction but hides the
|
||||
@@ -13135,12 +13442,50 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
stateProgramObject->GetExternalIndex(), cso.Slot, cso.Gen);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
m_isInitialized = true;
|
||||
MGLOG_D("Program sync completed. backend ID %u", m_backendProgramId);
|
||||
}
|
||||
|
||||
// P5e (pg), CONTRACT-P5E §5.5: THE BUILD THAT NAMES NO CLIENT MEMORY. Everything it
|
||||
// reads is the record: the archive create_shader_state carried, the three binding tails
|
||||
// set_program_bindings carried, and the descriptor. Nothing here resolves a frontend
|
||||
// identity, probes the client allocator or holds a SharedPtr to a frontend object - the
|
||||
// three absences rule F is made of.
|
||||
void BackendProgramObjectImpl::SyncToBackendByHandle(MG_Pipe::MGPipeHandle cso) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
const auto* record = PipeShaderCsoRecordForHandle(cso);
|
||||
if (record == nullptr) {
|
||||
MGLOG_E_ONCE("MGPipe: shader CSO {%u, %u} has no applier record, so no driver "
|
||||
"program can be built for it",
|
||||
cso.Slot, cso.Gen);
|
||||
return;
|
||||
}
|
||||
// THE ARCHIVE IS THE RECORD'S OR THE BUILD DOES NOT HAPPEN. A record with no archive
|
||||
// is one the transport arm never filled - a monolith create that reached a handle
|
||||
// arm - and building from an empty reflection would produce a program with no
|
||||
// uniforms and no modules, i.e. a black screen with no marker anywhere. The
|
||||
// create's own trip wire already refuses a record that declares no blobs AND
|
||||
// carries no artefacts (PipeApply.cpp); this is the same statement one level later,
|
||||
// where the twin can name the handle.
|
||||
if (!record->Archive) {
|
||||
MGLOG_E_ONCE("MGPipe: shader CSO {%u, %u} carries no server-owned archive; its "
|
||||
"create_shader_state was emitted by the monolith arm and this is "
|
||||
"the handle arm, so there is nothing to build from",
|
||||
cso.Slot, cso.Gen);
|
||||
m_backendProgramUsable = false;
|
||||
return;
|
||||
}
|
||||
SyncToBackendFromSource(ProgramArchiveSource::FromRecord(cso, *record));
|
||||
// The two server-owned keys the draw path's clean condition reads instead of
|
||||
// GetLinkVersion()/GetImageUnitVersion(). BindingsSerial is the second because a
|
||||
// glUniform1i on an IMAGE uniform is baked into the generated ESSL rather than
|
||||
// re-issued per draw - the record that carries it must therefore force a rebuild,
|
||||
// exactly as m_imageUnitVersion used to.
|
||||
m_syncedShaderCsoSerial = record->Serial;
|
||||
m_syncedBindingsSerial = record->BindingsSerial;
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
// The GL name of the array element that lives at `location`, given the reflection
|
||||
// name reported for it. Reflection reports one name per UNIFORM ("goku[0]") but
|
||||
@@ -13148,7 +13493,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// repeatedly; this turns it back into "goku[k]". Anything that is not an array
|
||||
// (or whose base location cannot be resolved) comes back unchanged, so the only
|
||||
// behaviour that moves is the array case.
|
||||
String SubscriptUniformNameForElement(const MG_State::GLState::ProgramObject& program, const String& name,
|
||||
String SubscriptUniformNameForElement(const ProgramBuildSource& program, const String& name,
|
||||
Uint location) {
|
||||
if (name.size() < 3 || name.compare(name.size() - 3, 3, "[0]") != 0) return name;
|
||||
const Int base = program.GetUniformLocation(name);
|
||||
@@ -13163,13 +13508,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// (BindCurrentProgramWithResources) never issues glGetUniformBlockIndex /
|
||||
// glGetUniformLocation string queries; block-to-binding-point assignments are
|
||||
// program state and only need to be established here.
|
||||
void BackendProgramObjectImpl::CacheResourceLocations(
|
||||
const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {
|
||||
void BackendProgramObjectImpl::CacheResourceLocations(const ProgramBuildSource& src) {
|
||||
m_globalUboBackendBlockIndex = -1;
|
||||
m_globalUboBackendBlockSize = 0;
|
||||
m_lastUploadedGlobalUboVersion = ~0u;
|
||||
m_globalUboRingAllocation = {};
|
||||
if (stateProgramObject->GetUBOSize() > 0) {
|
||||
if (src.GetUBOSize() > 0) {
|
||||
const Uint blockIndex =
|
||||
g_GLESFuncs.glGetUniformBlockIndex(m_backendProgramId, MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME);
|
||||
if (blockIndex != GL_INVALID_INDEX) {
|
||||
@@ -13186,16 +13530,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
} else {
|
||||
MGLOG_W_ONCE("Program %u has frontend global UBO storage, but backend has no %s block.",
|
||||
stateProgramObject->GetExternalIndex(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME);
|
||||
src.GetExternalIndex(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
const Int uboCount = stateProgramObject->GetActiveUniformBlocksCount();
|
||||
const Int uboCount = src.GetActiveUniformBlocksCount();
|
||||
m_uniformBlockBackendIndices.assign(static_cast<SizeT>(std::max(uboCount, 0)), -1);
|
||||
Uint lastUBOBinding = 0; // binding 0 is reserved for the global UBO
|
||||
for (Int i = 0; i < uboCount; ++i) {
|
||||
++lastUBOBinding;
|
||||
const auto& name = stateProgramObject->GetUniformBlockName(static_cast<Uint>(i));
|
||||
const auto& name = src.GetUniformBlockName(static_cast<Uint>(i));
|
||||
const GLuint backendBlkIdx = g_GLESFuncs.glGetUniformBlockIndex(m_backendProgramId, name.c_str());
|
||||
if (backendBlkIdx == GL_INVALID_INDEX) {
|
||||
// Either eliminated as unused, or an SSBO block (frontend reflection
|
||||
@@ -13205,16 +13549,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
m_uniformBlockBackendIndices[static_cast<SizeT>(i)] = static_cast<Int>(backendBlkIdx);
|
||||
g_GLESFuncs.glUniformBlockBinding(m_backendProgramId, backendBlkIdx, lastUBOBinding);
|
||||
MGLOG_D("CACHE prog=%u beProg=%u blk[%d]='%s' beIdx=%u -> bePoint=%u",
|
||||
stateProgramObject->GetExternalIndex(), m_backendProgramId, i, name.c_str(), backendBlkIdx,
|
||||
src.GetExternalIndex(), m_backendProgramId, i, name.c_str(), backendBlkIdx,
|
||||
lastUBOBinding);
|
||||
}
|
||||
|
||||
m_samplerUniformBindings.clear();
|
||||
const Uint maxUniformLoc = stateProgramObject->GetMaxUniformLocation();
|
||||
const Uint maxUniformLoc = src.GetMaxUniformLocation();
|
||||
for (Uint loc = 0; loc <= maxUniformLoc; ++loc) {
|
||||
const auto& name = stateProgramObject->GetUniformName(loc);
|
||||
const auto& name = src.GetUniformName(loc);
|
||||
if (name.empty()) continue;
|
||||
const GLenum uniformType = stateProgramObject->GetUniformType(loc);
|
||||
const GLenum uniformType = src.GetUniformType(loc);
|
||||
if (IsImageUniformType(uniformType)) {
|
||||
// ES image units come exclusively from the layout(binding=N) qualifier
|
||||
// (preserved in the transpiled ESSL); glUniform1i on an image uniform
|
||||
@@ -13230,7 +13574,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Address each element by its own name instead; the frontend already
|
||||
// reserves one location per element, so the element index is the distance
|
||||
// from the array's base location.
|
||||
const String elementName = SubscriptUniformNameForElement(*stateProgramObject, name, loc);
|
||||
const String elementName = SubscriptUniformNameForElement(src, name, loc);
|
||||
const Int backendLoc = g_GLESFuncs.glGetUniformLocation(m_backendProgramId, elementName.c_str());
|
||||
if (backendLoc < 0) continue;
|
||||
SamplerUniformBinding binding;
|
||||
@@ -13925,11 +14269,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
} // namespace FramebufferImpl
|
||||
|
||||
namespace PrgramImpl {
|
||||
void BackendProgramObjectImpl::SyncToBackendByHandle(MG_Pipe::MGPipeHandle cso) {
|
||||
MGPipeP5eSeamNotLanded("BackendProgramObjectImpl::SyncToBackendByHandle", "pg", cso);
|
||||
}
|
||||
} // namespace PrgramImpl
|
||||
// PrgramImpl's seam is GONE, not stubbed: package pg wrote
|
||||
// BackendProgramObjectImpl::SyncToBackendByHandle's real body beside the frontend overload
|
||||
// it is the twin of (above, with the two ProgramArchiveSource heads). A second definition
|
||||
// here would make the linker's answer depend on link order, which is the one failure this
|
||||
// block's own comment was written to prevent.
|
||||
|
||||
namespace RenderbufferImpl {
|
||||
void BackendRenderbufferObject::SyncToBackendByHandle(MG_Pipe::MGPipeHandle renderbuffer) {
|
||||
|
||||
@@ -696,6 +696,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5e (pg), CONTRACT-P5E.md §5.8 / ruling 1 (ID-81): THE HANDLE ARM'S SELECTOR for the
|
||||
// program family. Transport AND the family bit, in that order and both required:
|
||||
//
|
||||
// * `Transport != Monolith` because the push-MONOLITH build keeps its frontend arms token
|
||||
// for token - the verify comparator needs them, and it is what makes
|
||||
// MOBILEGL_IPC_RUN_AHEAD=0 a pure wait-rule A/B on identical server code rather than a
|
||||
// comparison of two different backends;
|
||||
// * the family bit because the A/B that switches this family off has to switch off the
|
||||
// arm too, not only the emission - a server reading records nobody sends would draw
|
||||
// with no program at all.
|
||||
//
|
||||
// NOT LATCHED, unlike the four above: Transport is configuration read once at bring-up and
|
||||
// ProgramSubsystemEnabled() already latches, so this is one load and one test, and a latch
|
||||
// here would only hide which half answered.
|
||||
inline Bool ProgramHandleArm() {
|
||||
return MG_Config::Transport != MG_Config::TransportMode::Monolith && ProgramSubsystemEnabled();
|
||||
}
|
||||
#endif
|
||||
|
||||
// ---- P4a: what the twins read INSTEAD of the frontend object ----
|
||||
//
|
||||
// One reader per record kind, all const, all null-on-miss, and all bounds-checked against
|
||||
@@ -2436,6 +2456,173 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Defined further down, next to CollectImageFormatBakeInputs; only referenced here.
|
||||
struct ImageFormatBakeInputs;
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// ---- P5e (pg), CONTRACT-P5E.md §5.5: THE ONE SOURCE A PROGRAM BUILD READS ----------
|
||||
//
|
||||
// WHY THIS TYPE EXISTS AT ALL. Building a driver program asks the frontend
|
||||
// ProgramObject sixteen distinct questions and asks its reflection tables thousands of
|
||||
// times, and EVERY ONE OF THEM used to be a read of client-owned memory on the apply
|
||||
// thread. Under run-ahead that is not a race to be careful about, it is wrong by
|
||||
// construction: ProgramObject::Link() REPLACES m_artifacts and m_spirv in place, and by
|
||||
// the time the server builds, the client may be several links past the one the record
|
||||
// describes. So the build reads THIS instead, and the two arms differ only in where it
|
||||
// points.
|
||||
//
|
||||
// AND IT IS DELIBERATELY NOT A SECOND ProgramObject. Nothing here constructs a frontend
|
||||
// object on the apply thread - ID-102's lesson, learned by id: a ProgramObject's own
|
||||
// constructor mints a lifetime id, so an "object" built server-side would trip the very
|
||||
// guards this phase installs and would make a red-once falsely green.
|
||||
//
|
||||
// THE OVERLAY IS THE OTHER HALF. Three reflection fields - the block-to-point map, the
|
||||
// sampler unit per uniform LOCATION and the name-keyed storage-block override set - are
|
||||
// members of LinkArtifacts that glUniformBlockBinding, glUniform1i and
|
||||
// glShaderStorageBlockBinding move AFTER the link that produced the archive, without
|
||||
// relinking. On the handle arm the record's set_program_bindings tails are the whole
|
||||
// truth for all three (whole-set replacement, not a merge) and the archive's link-time
|
||||
// values are shadowed; on the monolith arm there is no overlay because the "archive"
|
||||
// IS the frontend's live table.
|
||||
//
|
||||
// THE ACCESSORS BELOW MIRROR ProgramObject's, one for one, and that duplication is
|
||||
// deliberate rather than lazy: MG_Backend includes nothing from MG_State's program
|
||||
// internals beyond the archive structs themselves, and ProgramObject's own forms are
|
||||
// non-static members over Artifacts(). The three that ARE already static over
|
||||
// LinkArtifacts (IsValidUniformLocation, UniformAtIn, GetUniformArraySizeByTIndex) are
|
||||
// called rather than copied, which is where the boundary sits.
|
||||
struct ProgramArchiveSource {
|
||||
using LinkArtifacts = MG_State::GLState::LinkArtifacts;
|
||||
using SpirvArtifacts = MG_State::GLState::SpirvArtifacts;
|
||||
using UniformReflection = MG_State::GLState::ResourceReflection;
|
||||
using TypeFacts = MG_State::GLState::TypeFacts;
|
||||
using XfbVarying = MG_State::GLState::XfbVarying;
|
||||
|
||||
// Both are non-null for the whole lifetime of a source; a source is a local of the
|
||||
// build that made it and never outlives the record or the object it points into.
|
||||
const LinkArtifacts* Link = nullptr;
|
||||
const SpirvArtifacts* Spirv = nullptr;
|
||||
|
||||
// WHAT A LOG LINE NAMES. The frontend's GL program name on the monolith arm; the
|
||||
// ShaderCso slot on the handle arm, because the server does not know GL names and
|
||||
// must not learn them (the handle is the identity the whole phase speaks in).
|
||||
Uint Identity = 0;
|
||||
// True when Identity is a handle slot rather than a GL name, so a message can say
|
||||
// which it printed instead of leaving a reader to guess.
|
||||
Bool IdentityIsHandleSlot = false;
|
||||
|
||||
Bool Linked = false;
|
||||
Bool SpirvUsable = false;
|
||||
Bool SpirvValidationEnabled = false;
|
||||
Bool PointSizeWasDemoted = false;
|
||||
Uint GlobalUboSize = 0;
|
||||
|
||||
// One entry per Spirv->generatedSpirv module, at the same index. Owned rather than
|
||||
// referenced because the monolith arm builds it (GetLinkedShaderStages() returns by
|
||||
// value) and the handle arm converts the frame's Uint32 words.
|
||||
Vector<ShaderStage> LinkedStages;
|
||||
|
||||
// The post-link overlay. Governs iff OverlayGoverns; see the type comment.
|
||||
Bool OverlayGoverns = false;
|
||||
const Vector<Int32>* BlockBindingOverlay = nullptr;
|
||||
const Vector<MG_Pipe::MGPProgramSamplerUnit>* SamplerUnitOverlay = nullptr;
|
||||
// Built once per build from whichever side owns it, because TranspileSpirvToEssl
|
||||
// takes the map by const reference and the record carries a vector.
|
||||
UnorderedMap<String, Int> StorageOverrides;
|
||||
// The client's commutative hash on the handle arm, ComputeShaderStorageBlockBinding-
|
||||
// Signature's on the monolith one. Same function, computed on whichever side owns
|
||||
// the map (MG_Impl/Pipe/ProgramEmit.h names its twin).
|
||||
Uint64 StorageOverrideSignature = 0;
|
||||
|
||||
// ---- the archive's own answers ----
|
||||
//
|
||||
// EVERY NAME HERE IS ProgramObject's NAME, and that is load-bearing rather than
|
||||
// tidy: the build body below is written once against `src`, whose TYPE is the
|
||||
// build's (ProgramBuildSource), so the pull build compiles the very same text
|
||||
// against a ProgramObject and G1's byte identity survives.
|
||||
Uint GetExternalIndex() const { return Identity; }
|
||||
Bool GetLinkStatus() const { return Linked; }
|
||||
Bool GetSpirvStatus() const { return SpirvUsable; }
|
||||
Bool GetSpirvValidationEnabled() const { return SpirvValidationEnabled; }
|
||||
Bool PointSizeDemoted() const { return PointSizeWasDemoted; }
|
||||
Uint GetUBOSize() const { return GlobalUboSize; }
|
||||
const Vector<ShaderStage>& GetLinkedShaderStages() const { return LinkedStages; }
|
||||
const UnorderedMap<String, Int>& GetShaderStorageBlockBindingOverrides() const {
|
||||
return StorageOverrides;
|
||||
}
|
||||
// THE DEBUG LOG IS DELETED ON THIS ARM (CONTRACT-P5E §5.5). The snapshot is
|
||||
// GL-thread-owned ShaderObject SharedPtrs - the exact shape rule C forbids an
|
||||
// applier entry point to reach - and its one consumer in the build is an MGLOG_D
|
||||
// dump of each stage's original GLSL, which the server does not have and does not
|
||||
// need. An empty list makes that loop a no-op rather than a special case.
|
||||
const Vector<MG_State::GLState::ProgramObject::LinkedShaderRef>& GetLinkedShaderSnapshot() const {
|
||||
static const Vector<MG_State::GLState::ProgramObject::LinkedShaderRef> kNone;
|
||||
return kNone;
|
||||
}
|
||||
|
||||
Uint GetMaxUniformLocation() const { return Link->maxUniformLocation; }
|
||||
Bool IsValidUniformLocation(Int location) const {
|
||||
return MG_State::GLState::ProgramObject::IsValidUniformLocation(*Link, location);
|
||||
}
|
||||
const UniformReflection& UniformAt(Int tIndex) const {
|
||||
return MG_State::GLState::ProgramObject::UniformAtIn(*Link, tIndex);
|
||||
}
|
||||
// Bounds-checked, exactly as ProgramObject::GetUniformName is not: the frontend's
|
||||
// form indexes uniformIndexInTProgram raw because every caller there has already
|
||||
// walked a legal location, and this one is reached from a handle arm where the
|
||||
// location space comes off a record.
|
||||
const String& GetUniformName(Uint location) const {
|
||||
static const String kEmpty;
|
||||
if (location >= Link->uniformIndexInTProgram.size()) return kEmpty;
|
||||
return UniformAt(Link->uniformIndexInTProgram[location]).name;
|
||||
}
|
||||
GLenum GetUniformType(Uint location) const {
|
||||
if (location >= Link->uniformIndexInTProgram.size()) return 0;
|
||||
return UniformAt(Link->uniformIndexInTProgram[location]).glDefineType;
|
||||
}
|
||||
const TypeFacts& GetUniformTypeFacts(Uint location) const {
|
||||
static const TypeFacts kEmpty{};
|
||||
if (location >= Link->uniformIndexInTProgram.size()) return kEmpty;
|
||||
return UniformAt(Link->uniformIndexInTProgram[location]).type;
|
||||
}
|
||||
Bool UniformLocationsAliasSameUniform(Int a, Int b) const {
|
||||
if (!IsValidUniformLocation(a) || !IsValidUniformLocation(b)) return false;
|
||||
return Link->uniformIndexInTProgram[a] == Link->uniformIndexInTProgram[b];
|
||||
}
|
||||
// ProgramObject::GetUniformLocation, reproduced over the archive. The array rules
|
||||
// are the whole body: reflection keys an array under "arr[0]" at its base location,
|
||||
// a bare "arr" resolves to that, an "arr[k]" resolves to base + k, and an array of
|
||||
// arrays is keyed by its full "[0]"-terminated spelling - which is why the
|
||||
// suffixed lookup is tried before the trailing subscript is read as an index.
|
||||
Int GetUniformLocation(const String& name) const;
|
||||
Int GetActiveUniformBlocksCount() const {
|
||||
return static_cast<Int>(Link->glBlockIndexToTProgram.size());
|
||||
}
|
||||
const String& GetUniformBlockName(Uint index) const;
|
||||
|
||||
GLenum GetTransformFeedbackBufferMode() const { return Link->xfbBufferMode; }
|
||||
SizeT GetTransformFeedbackVaryingCount() const { return Link->xfbVaryings.size(); }
|
||||
const Vector<XfbVarying>& GetTransformFeedbackVaryings() const { return Link->xfbVaryings; }
|
||||
const Vector<Vector<unsigned>>& GetGeneratedSpirv() const { return Spirv->generatedSpirv; }
|
||||
|
||||
// ---- the three the overlay governs ----
|
||||
Uint GetUniformBlockBinding(Uint index) const;
|
||||
Int GetUniformSamplerOrImageUnitIndex(Uint location) const;
|
||||
|
||||
// The monolith-glue constructor: the archive IS the frontend's live tables, so
|
||||
// there is no overlay and the three mutable fields answer from them directly.
|
||||
static ProgramArchiveSource FromFrontend(const MG_State::GLState::ProgramObject& program);
|
||||
// The handle arm: the record's own archive, with its three tails overlaid.
|
||||
static ProgramArchiveSource FromRecord(MG_Pipe::MGPipeHandle cso,
|
||||
const MG_Pipe::MGPipeShaderCsoRecord& record);
|
||||
};
|
||||
|
||||
// THE BUILD'S SOURCE TYPE, per build. This alias is what lets the program build have one
|
||||
// body: in a push build it is the record-or-frontend view above, in a pull build it is
|
||||
// the frontend object the body always read, spelled through the same name so the text
|
||||
// does not move (G1).
|
||||
using ProgramBuildSource = ProgramArchiveSource;
|
||||
#else
|
||||
using ProgramBuildSource = MG_State::GLState::ProgramObject;
|
||||
#endif
|
||||
|
||||
class BackendProgramObjectImpl {
|
||||
public:
|
||||
// Per-link cache of a sampler-style uniform's backend location: built once in
|
||||
@@ -2496,11 +2683,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
~BackendProgramObjectImpl();
|
||||
void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5e SEAM (MG_Remote/CONTRACT-P5E.md §5.5; declared by c0e, bodied by pg): the same
|
||||
// sync keyed on the ShaderCso HANDLE and answered from the record - the archive the
|
||||
// create carries and the three binding tails set_program_bindings carries. It is an
|
||||
// OVERLOAD beside the frontend one, which stays as the monolith-glue half, so the
|
||||
// pull build's mangled names do not move.
|
||||
// P5e (pg), CONTRACT-P5E.md §5.5: the same sync keyed on the ShaderCso HANDLE and
|
||||
// answered from the record - the archive the create carries and the three binding
|
||||
// tails set_program_bindings carries. It is an OVERLOAD beside the frontend one,
|
||||
// which stays as the monolith-glue half, so the pull build's mangled names do not
|
||||
// move (ruling 1 / ID-81: two overloads, not an #if inside one body).
|
||||
void SyncToBackendByHandle(MG_Pipe::MGPipeHandle cso);
|
||||
#endif
|
||||
void Use();
|
||||
@@ -2601,6 +2788,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// parameters - are all still specialised at the verb, from state this backend holds.
|
||||
// 0 means "never stamped", which is a guaranteed miss (applier serials start at 1).
|
||||
Uint64 GetSyncedShaderCsoSerial() const { return m_syncedShaderCsoSerial; }
|
||||
// P5e (pg): the SECOND server-owned key, and it replaces GetImageUnitVersion() on
|
||||
// the handle arm rather than duplicating the one above. A glUniform1i on an IMAGE
|
||||
// uniform is BAKED into the generated ESSL (ES forbids the call outright), so the
|
||||
// record that carries the new unit has to force a rebuild; BindingsSerial moves on
|
||||
// every applied set_program_bindings, which is exactly when one can have changed.
|
||||
// 0 means "never stamped", a guaranteed miss, because applier serials start at 1.
|
||||
Uint64 GetSyncedBindingsSerial() const { return m_syncedBindingsSerial; }
|
||||
#endif
|
||||
// Whether the (unit, bound format) pairs this program's FORMAT-LESS image uniforms
|
||||
// resolve to are still the ones its ESSL was generated against.
|
||||
@@ -2626,7 +2820,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint64 ComputeImageUnitFormatSignature() const;
|
||||
|
||||
private:
|
||||
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The one body both public heads feed; see its definition for why it is a worker in
|
||||
// a push build and IS SyncToBackend in a pull build.
|
||||
void SyncToBackendFromSource(const ProgramBuildSource& src);
|
||||
#endif
|
||||
void CacheResourceLocations(const ProgramBuildSource& src);
|
||||
|
||||
// Builds, compiles and attaches the pass-through tessellation control stage GL 4.6
|
||||
// core 11.2.2 describes, for a program that has an evaluation stage and none of its
|
||||
@@ -2634,7 +2833,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// stage has been attached and before the link; see the definition for why it cannot
|
||||
// regress a program that works today.
|
||||
void AttachPassthroughTessControlStage(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject, Int tessEvalShaderIndex,
|
||||
const ProgramBuildSource& src, Int tessEvalShaderIndex,
|
||||
const Vector<Vector<unsigned int>>& shaderSpirvs, const String& vertexStageEssl,
|
||||
const String& tessEvalStageEssl);
|
||||
|
||||
@@ -2705,6 +2904,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// P4a's replacement for the two above on the handle arm; see GetSyncedShaderCsoSerial.
|
||||
// Push-only, so the pull build's object is byte-for-byte the pre-P4a one (D-P).
|
||||
Uint64 m_syncedShaderCsoSerial = 0;
|
||||
Uint64 m_syncedBindingsSerial = 0;
|
||||
#endif
|
||||
// Image units addressed by the program's FORMAT-LESS image uniforms, and the digest
|
||||
// of the (unit, format) pairs the generated ESSL baked. Empty/0 for every program
|
||||
@@ -2763,15 +2963,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// program that was just built - best effort, on the same "only where the driver has
|
||||
// the entry point" terms as ApplyShaderStorageBlockBinding above. Mirrors
|
||||
// DirectVulkan's reseed-on-rebuild in BuildProgramResourceCache.
|
||||
void ReseedShaderStorageBlockBindings(Uint backendProgramId,
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||
void ReseedShaderStorageBlockBindings(Uint backendProgramId, const ProgramBuildSource& src);
|
||||
// Order-independent digest of the program's glShaderStorageBlockBinding overrides.
|
||||
// The generated ESSL carries them (ES has no way to move a storage block's binding
|
||||
// after link), so a program built against a different set is stale and the draw path
|
||||
// has to rebuild it. Computed from the values, so re-setting a block to the binding it
|
||||
// already has costs nothing. 0 when nothing was ever rebound.
|
||||
Uint64 ComputeShaderStorageBlockBindingSignature(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||
Uint64 ComputeShaderStorageBlockBindingSignature(const ProgramBuildSource& src);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5e (pg): the computation itself, for the monolith arm - the source constructor seeds
|
||||
// itself with it and the monolith draw path asks it per draw. Push-only: in a pull build
|
||||
// the overload above IS this body, so no name is added there.
|
||||
Uint64 ComputeShaderStorageBlockBindingSignatureOf(
|
||||
const MG_State::GLState::ProgramObject& program);
|
||||
#endif
|
||||
|
||||
// Everything the image-format bake needs from one walk of a program's uniform
|
||||
// reflection. GLSL ES requires a format layout qualifier on every image uniform;
|
||||
@@ -2812,8 +3017,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// keeps the module it already had.
|
||||
Bool declaresWidenableImageFormat = false;
|
||||
};
|
||||
ImageFormatBakeInputs CollectImageFormatBakeInputs(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||
ImageFormatBakeInputs CollectImageFormatBakeInputs(const ProgramBuildSource& src);
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// ---- P5e SEAM (MG_Remote/CONTRACT-P5E.md §4.2, §5.5; declared by c0e, bodied by
|
||||
@@ -2830,6 +3034,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// ResolveSamplerCsoTwin set - and null silently for the null handle, which is the legal
|
||||
// "nothing bound".
|
||||
BackendProgramObjectImpl* ResolveProgramTwin(MG_Pipe::MGPipeHandle cso);
|
||||
|
||||
// P5e (pg): the two PER-DRAW reads of set_program_bindings' tails, as free functions
|
||||
// rather than through a ProgramArchiveSource - building a source per draw would copy the
|
||||
// override map for a question that is one indexed read. Both answer exactly what their
|
||||
// frontend twins do when the record carries no bindings yet: the archive's own link-time
|
||||
// value, which for an untouched program IS the right answer.
|
||||
//
|
||||
// The block binding is dense in the BLOCK index space; the sampler unit is sparse and
|
||||
// ascending by LOCATION, so it is a binary search (a program has thousands of locations
|
||||
// and a handful of samplers, and a linear scan per sampler would be quadratic).
|
||||
Uint ProgramBlockBindingFromRecord(const MG_Pipe::MGPipeShaderCsoRecord& record, Int blockIndex);
|
||||
Int ProgramSamplerUnitFromRecord(const MG_Pipe::MGPipeShaderCsoRecord& record, Uint location);
|
||||
#endif
|
||||
} // namespace PrgramImpl
|
||||
|
||||
|
||||
@@ -79,6 +79,25 @@ namespace MobileGL::MG_Pipe {
|
||||
// would describe a program that does not exist yet. GetLinkedShaderStages() is also what
|
||||
// indexes GetGeneratedSpirv(), so the two halves of this descriptor are guaranteed to agree
|
||||
// by construction rather than by care.
|
||||
// P5e (pg). ONE ENTRY OF THE STORAGE-OVERRIDE SIGNATURE, and it is a VERBATIM copy of the
|
||||
// mix in `MG_Backend/DirectGLES/Managers.cpp`'s ComputeShaderStorageBlockBindingSignature -
|
||||
// the function whose answer this record replaces on the handle arm. The two halves cannot
|
||||
// share one definition today because MG_Backend includes nothing from MG_Impl (checked at
|
||||
// this head), so they are twins with a named pointer at each other and a unit case in
|
||||
// ProgramEmitTest that pins the number. Order-independent by construction: the source is an
|
||||
// UnorderedMap, the combine is commutative, and the entry mixes name and binding so two
|
||||
// entries cannot trade halves and cancel out.
|
||||
//
|
||||
// OVER THE VALUES, NOT OVER A CHANGE COUNTER, which is the property the pipeline composite's
|
||||
// uniform mirror depends on: it re-sets every override every draw, and re-setting a block to
|
||||
// the binding it already carries must produce the same signature and force no rebuild.
|
||||
inline Uint64 MGPipeStorageOverrideSignatureEntry(const String& blockName, Int binding) {
|
||||
Uint64 entry = std::hash<String>{}(blockName);
|
||||
entry ^= (static_cast<Uint64>(static_cast<Uint32>(binding)) + 0x9e3779b97f4a7c15ull +
|
||||
(entry << 6) + (entry >> 2));
|
||||
return entry;
|
||||
}
|
||||
|
||||
inline Uint32 MGPipeStageMaskOf(const MG_State::GLState::ProgramObject& program) {
|
||||
Uint32 mask = 0;
|
||||
for (const ShaderStage stage : program.GetLinkedShaderStages()) {
|
||||
@@ -109,6 +128,17 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
const MGPipeHandle drawCso =
|
||||
drawProgram ? AcquireShaderCso(*drawProgram, bytes) : kMGPipeNullHandle;
|
||||
// P5e (pg): AFTER the create and never before it. The draft note on
|
||||
// MGPProgramBindings said "emitted BEFORE create_shader_state at the same validate
|
||||
// point", and that ordering is wrong against the applier the same contract
|
||||
// specifies: a re-issued create CLEARS all three tails, for the reason it clears
|
||||
// GlobalConstants (a relink replaces the archive the tails index into), so bindings
|
||||
// published ahead of it are wiped by the create that follows. What the draft was
|
||||
// protecting - "a rebuild inside the verb must already see the bindings" - is
|
||||
// satisfied by both records going out at the SAME validate point, ahead of the verb,
|
||||
// which is what this ordering does. See MG_Remote/CONTRACT-P5E.md §5.5 and the
|
||||
// record's own comment in PipeApply.h.
|
||||
if (drawProgram) bytes += EmitProgramBindings(*drawProgram, drawCso);
|
||||
// THE COMPOSITE'S SECOND RELEASE PATH is spoken here, not in a destructor: when the
|
||||
// bound pipeline's draw-program signature moves, the resolver releases the slot the
|
||||
// previous composite held. Whichever of the two paths runs second - this one or the
|
||||
@@ -130,6 +160,9 @@ namespace MobileGL::MG_Pipe {
|
||||
dispatchProgram ? (dispatchProgram == drawProgram ? drawCso
|
||||
: AcquireShaderCso(*dispatchProgram, bytes))
|
||||
: kMGPipeNullHandle;
|
||||
if (dispatchProgram && dispatchProgram != drawProgram) {
|
||||
bytes += EmitProgramBindings(*dispatchProgram, dispatchCso);
|
||||
}
|
||||
|
||||
// THE BOUND CSO IS THE DRAW ONE WHEN THERE IS ONE. bind_shader_state names what
|
||||
// glUseProgram selected, and when a program pipeline is bound instead that is the
|
||||
@@ -208,6 +241,136 @@ namespace MobileGL::MG_Pipe {
|
||||
return bytes + sizeof(MGPGlobalConstants) + size;
|
||||
}
|
||||
|
||||
// ---- P5e (pg): set_program_bindings, opcode 80 -----------------------------------
|
||||
//
|
||||
// THE THREE POST-LINK MUTABLE REFLECTION FIELDS, and why an archive alone is not an
|
||||
// answer. `uniformBlockBinding[i]`, `uniformSamplerOrImageUnitIndex[loc]` and the
|
||||
// name-keyed `shaderStorageBlockBinding` map are all MEMBERS OF LinkArtifacts - so the
|
||||
// archive create_shader_state carries does hold them - but glUniformBlockBinding,
|
||||
// glUniform1i on a sampler uniform and glShaderStorageBlockBinding all move them AFTER
|
||||
// the link that produced that archive, without relinking. A server answering a draw
|
||||
// from the archive alone would bind the uniform blocks the program was LINKED with
|
||||
// rather than the ones it is BOUND with. This record is the delta carrier; the applier
|
||||
// overlays it on the archive, and a re-issued create drops it because the indices no
|
||||
// longer mean anything.
|
||||
//
|
||||
// LATCHED ON THE TWO COUNTERS THAT MOVE WHEN ANY OF THE THREE DOES, plus the handle:
|
||||
// m_backendStateVersion (glUniformBlockBinding and glUniform1i both bump it) and
|
||||
// m_blockBindingVersion (glUniformBlockBinding and glShaderStorageBlockBinding both
|
||||
// bump it, and the storage setter deliberately bumps ONLY it). Neither setter moves a
|
||||
// counter on an unchanged value - both have an equality bail-out - so an application
|
||||
// that re-sets the same bindings every frame emits nothing, which is what the pipeline
|
||||
// composite's uniform mirror needs (it replays every override per draw).
|
||||
Uint64 EmitProgramBindings(const ProgramObject& program, MGPipeHandle cso) {
|
||||
if (MGPipeHandleIsNull(cso)) return 0;
|
||||
const Uint32 backendStateVersion = program.GetBackendStateVersion();
|
||||
const Uint32 blockBindingVersion = program.GetBlockBindingVersion();
|
||||
Latch& latch = LatchFor(cso);
|
||||
if (latch.BindingsLive && latch.BindingsGen == cso.Gen &&
|
||||
latch.BindingsBackendStateVersion == backendStateVersion &&
|
||||
latch.BindingsBlockBindingVersion == blockBindingVersion) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
m_blockBindings.clear();
|
||||
m_samplerUnits.clear();
|
||||
m_storageOverrides.clear();
|
||||
m_storageOverrideNames.clear();
|
||||
|
||||
// TAIL 1: dense, in BLOCK index order - the space GetActiveUniformBlocksCount(),
|
||||
// GetUniformBlockName(i) and GetUniformBlockBinding(i) all speak, which is exactly
|
||||
// the space the server's CacheResourceLocations walks. Dense because the server
|
||||
// indexes it by i and a sparse form would need a second index space on the wire.
|
||||
const Int blockCount = program.GetActiveUniformBlocksCount();
|
||||
if (static_cast<Uint32>(std::max(blockCount, 0)) > kMGPipeMaxProgramBlockBindings) {
|
||||
// A COUNTED REFUSAL AND NOT A TRUNCATION (D-J3): a program with more uniform
|
||||
// blocks than the record can name would have the tail of its block set bound
|
||||
// to whatever the archive's link-time snapshot said, silently. The counter is
|
||||
// what makes that visible; the record is not emitted at all, so the server
|
||||
// keeps the archive's values for every block rather than for some of them.
|
||||
++m_bindingRefusals;
|
||||
return 0;
|
||||
}
|
||||
m_blockBindings.reserve(static_cast<SizeT>(std::max(blockCount, 0)));
|
||||
for (Int i = 0; i < blockCount; ++i) {
|
||||
m_blockBindings.push_back(static_cast<Int32>(program.GetUniformBlockBinding(static_cast<Uint>(i))));
|
||||
}
|
||||
|
||||
// TAIL 2: sparse, ascending by LOCATION, because the frontend's array is indexed by
|
||||
// uniform location and maxUniformLocation runs into the thousands while the
|
||||
// sampler count is a handful. -1 is "never assigned" and is what the server's own
|
||||
// default already is, so it is not worth a wire entry.
|
||||
const Uint maxUniformLoc = program.GetMaxUniformLocation();
|
||||
for (Uint loc = 0; loc <= maxUniformLoc; ++loc) {
|
||||
// The same guard CacheResourceLocations uses one level down: a location with no
|
||||
// uniform behind it has an empty name and must not index the type tables.
|
||||
if (program.GetUniformName(loc).empty()) continue;
|
||||
const Int unit = program.GetUniformSamplerOrImageUnitIndex(loc);
|
||||
if (unit < 0) continue;
|
||||
if (m_samplerUnits.size() >= kMGPipeMaxProgramSamplerUnits) {
|
||||
++m_bindingRefusals;
|
||||
return 0;
|
||||
}
|
||||
MGPProgramSamplerUnit entry{};
|
||||
entry.Location = loc;
|
||||
entry.Unit = static_cast<Int32>(unit);
|
||||
m_samplerUnits.push_back(entry);
|
||||
}
|
||||
|
||||
// TAIL 3: name-keyed BY DESIGN. A shader storage block has three index spaces - the
|
||||
// frontend interface query, DirectVulkan's descriptor order and the real driver's -
|
||||
// and the name is the only coordinate all three agree on (ProgramObject.h says so
|
||||
// over the setter). Empty for the overwhelming majority of programs, which is why
|
||||
// the signature below exists: the server compares one Uint64 and touches the names
|
||||
// only on a rebuild.
|
||||
const auto& overrides = program.GetShaderStorageBlockBindingOverrides();
|
||||
if (overrides.size() > kMGPipeMaxProgramStorageOverrides) {
|
||||
++m_bindingRefusals;
|
||||
return 0;
|
||||
}
|
||||
Uint64 signature = 0;
|
||||
m_storageOverrides.reserve(overrides.size());
|
||||
m_storageOverrideNames.reserve(overrides.size());
|
||||
for (const auto& [blockName, binding] : overrides) {
|
||||
if (binding < 0) continue; // never rebound; the declared qualifier still stands
|
||||
MGPProgramStorageOverride entry{};
|
||||
entry.Binding = binding;
|
||||
m_storageOverrides.push_back(entry);
|
||||
m_storageOverrideNames.push_back(blockName);
|
||||
signature += MGPipeStorageOverrideSignatureEntry(blockName, binding);
|
||||
}
|
||||
m_storageOverrides.resize(m_storageOverrideNames.size());
|
||||
|
||||
m_lastBindings = MGPProgramBindings{};
|
||||
m_lastBindings.Cso = cso;
|
||||
m_lastBindings.Signature = signature;
|
||||
m_lastBindings.BlockBindingCount = static_cast<Uint32>(m_blockBindings.size());
|
||||
m_lastBindings.SamplerUnitCount = static_cast<Uint32>(m_samplerUnits.size());
|
||||
m_lastBindings.StorageOverrideCount = static_cast<Uint32>(m_storageOverrides.size());
|
||||
|
||||
// The name POINTERS, index-aligned with the override tail. Built here rather than
|
||||
// inside the loop because push_back on m_storageOverrideNames may reallocate and
|
||||
// every c_str() taken before that would dangle.
|
||||
m_storageOverrideNamePtrs.clear();
|
||||
m_storageOverrideNamePtrs.reserve(m_storageOverrideNames.size());
|
||||
for (const String& name : m_storageOverrideNames) m_storageOverrideNamePtrs.push_back(name.c_str());
|
||||
|
||||
MGPipeRouteSetProgramBindings(
|
||||
m_lastBindings, m_blockBindings.empty() ? nullptr : m_blockBindings.data(),
|
||||
m_samplerUnits.empty() ? nullptr : m_samplerUnits.data(),
|
||||
m_storageOverrides.empty() ? nullptr : m_storageOverrides.data(),
|
||||
m_storageOverrideNamePtrs.empty() ? nullptr : m_storageOverrideNamePtrs.data());
|
||||
++m_bindingSets;
|
||||
|
||||
latch.BindingsLive = true;
|
||||
latch.BindingsGen = cso.Gen;
|
||||
latch.BindingsBackendStateVersion = backendStateVersion;
|
||||
latch.BindingsBlockBindingVersion = blockBindingVersion;
|
||||
return sizeof(MGPProgramBindings) + m_blockBindings.size() * sizeof(Int32) +
|
||||
m_samplerUnits.size() * sizeof(MGPProgramSamplerUnit) +
|
||||
m_storageOverrides.size() * sizeof(MGPProgramStorageOverride);
|
||||
}
|
||||
|
||||
// D-H4's re-issue rule, and it is the CreateVertexElements shape one for one: the
|
||||
// record goes out again on the SAME handle whenever the link version moves, which is
|
||||
// legal because MGPipeHandle::Gen increments only on slot reuse and never on a
|
||||
@@ -232,6 +395,21 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastDesc.GlobalUboSize = static_cast<Uint32>(program.GetUBOSize());
|
||||
m_lastDesc.ReservedNumSamplesOffset = static_cast<Uint32>(spirv.reservedNumSamplesOffset);
|
||||
m_lastDesc.SpirvStatus = spirv.spirvStatus ? 1 : 0;
|
||||
// P5e (pg), ID-88 / ruling 9: THE FAILED RELINK OF A BOUND PROGRAM, and the ruling
|
||||
// asked this package to verify the frontend's actual behaviour rather than assume
|
||||
// it. VERIFIED, at ProgramObject.cpp:510-531: `Link()`'s PROLOGUE bumps
|
||||
// m_linkVersion and then assigns `m_artifacts = {}` - "the complete not-linked
|
||||
// state" in its own words - before the link body runs, and every failure arm of
|
||||
// ProgramLinkTask leaves `linkStatus = false`. So a failed relink reports UNLINKED
|
||||
// through GetLinkStatus(), the re-issue this latch triggers is the one the ruling
|
||||
// names, and it carries LinkStatus = 0.
|
||||
//
|
||||
// AND NEVER AN object_death: the program object is alive, its handle is alive, and
|
||||
// its twin must stay alive to be rebuilt by the next successful link. What the
|
||||
// server does with LinkStatus = 0 is exactly what the monolith arm already does
|
||||
// with GetLinkStatus() == false - glUseProgram(0), a visible no-op draw - which is
|
||||
// also what GL requires, since MobileGL's frontend withdraws LINK_STATUS here.
|
||||
m_lastDesc.LinkStatus = program.GetLinkStatus() ? 1 : 0;
|
||||
m_lastDesc.NativeFloat64 = spirv.nativeFloat64 ? 1 : 0;
|
||||
m_lastDesc.PointSizeDemoted = spirv.pointSizeDemoted ? 1 : 0;
|
||||
m_lastDesc.EnableSpirvValidation = spirv.enableSpirvValidation ? 1 : 0;
|
||||
@@ -260,7 +438,20 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastDesc.Reflection.Offset = reinterpret_cast<Uint64>(&link);
|
||||
m_lastDesc.Reflection.Size = 0;
|
||||
|
||||
MGPipeRouteCreateShaderState(m_lastDesc, &link, &spirv);
|
||||
// P5e (pg): THE STAGE OF EACH MODULE, beside the modules and not folded into
|
||||
// StageMask. StageMask is a bit SET and GL lets two shader objects of one stage be
|
||||
// attached to a program, so a list rebuilt from it can be SHORTER than
|
||||
// generatedSpirv - and the server pairs the two by one running index. The client
|
||||
// arm frames this in front of the archive; the monolith arm discards it and reads
|
||||
// the snapshot off the object it is handed. Taken from the same accessor
|
||||
// MGPipeStageMaskOf walks, so the mask and the list cannot disagree.
|
||||
m_linkedStageWords.clear();
|
||||
for (const ShaderStage stage : program.GetLinkedShaderStages()) {
|
||||
m_linkedStageWords.push_back(static_cast<Uint32>(stage));
|
||||
}
|
||||
|
||||
MGPipeRouteCreateShaderState(m_lastDesc, &link, &spirv, m_linkedStageWords.data(),
|
||||
static_cast<Uint32>(m_linkedStageWords.size()));
|
||||
// THE CREATE WENT OUT, so the publication latch is taken here and nowhere else
|
||||
// (contract-v2 §3.1). MGPipeEmitShaderCsoDestroyAndFree reads it, and without it
|
||||
// delete_shader_state can never go out - for an ordinary program or for a
|
||||
@@ -283,6 +474,14 @@ namespace MobileGL::MG_Pipe {
|
||||
m_constantsVersion = kMGPipeGlobalConstantsNeverUploaded;
|
||||
}
|
||||
|
||||
// P5e (pg): THE BINDINGS LATCH GOES WITH THE GLOBAL-CONSTANTS ONE, and for the same
|
||||
// reason stated one branch up. The applier clears all three binding tails on a
|
||||
// re-issued create (the indices point into an archive that has just been replaced),
|
||||
// so a latch that survived it would suppress the very record that has to re-establish
|
||||
// them and the server would draw the whole program off the archive's link-time
|
||||
// snapshot. Invalidated rather than emitted here: EmitShaderState sends the bindings
|
||||
// immediately after this returns, which is also the ONLY order that works.
|
||||
latch.BindingsLive = false;
|
||||
latch.RecordLive = true;
|
||||
latch.RecordGen = handle.Gen;
|
||||
latch.LinkVersion = linkVersion;
|
||||
@@ -304,7 +503,13 @@ namespace MobileGL::MG_Pipe {
|
||||
// validate-point emission and has no payload budget to report into.
|
||||
void EmitShaderCso(ProgramObject& program) {
|
||||
Uint64 bytes = 0;
|
||||
AcquireShaderCso(program, bytes);
|
||||
const MGPipeHandle cso = AcquireShaderCso(program, bytes);
|
||||
// P5e (pg): the birth hook publishes the bindings too, for the same reason the
|
||||
// validate point does. A program born here and drawn before the next validate point
|
||||
// would otherwise reach the server with its archive's LINK-TIME snapshot and no
|
||||
// delta, which is right only for a program nothing has rebound since - and this
|
||||
// hook is exactly where a program that WAS rebound at creation time arrives.
|
||||
EmitProgramBindings(program, cso);
|
||||
}
|
||||
|
||||
// The emitter's OWN record memo - "have I already published a create_shader_state at
|
||||
@@ -378,6 +583,7 @@ namespace MobileGL::MG_Pipe {
|
||||
void ResetCounters() {
|
||||
m_creates = m_binds = m_drawSets = m_dispatchSets = m_constantSets = 0;
|
||||
m_moduleTruncations = 0;
|
||||
m_bindingSets = m_bindingRefusals = 0;
|
||||
}
|
||||
|
||||
// ---- what a unit case reads ----
|
||||
@@ -399,6 +605,15 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint64 DrawProgramSetCount() const { return m_drawSets; }
|
||||
Uint64 DispatchProgramSetCount() const { return m_dispatchSets; }
|
||||
Uint64 GlobalConstantsSetCount() const { return m_constantSets; }
|
||||
// P5e (pg). The record a case reads back, and the two counters D-J3's rule asks for:
|
||||
// how many set_program_bindings went out, and how many programs were REFUSED because
|
||||
// one of their three sets is larger than the record can name. A refusal leaves the
|
||||
// server on the archive's link-time snapshot for that program - correct for anything
|
||||
// that was never rebound after the link, wrong for anything that was, and countable
|
||||
// either way, which is the whole point of not truncating.
|
||||
const MGPProgramBindings& LastProgramBindings() const { return m_lastBindings; }
|
||||
Uint64 ProgramBindingsSetCount() const { return m_bindingSets; }
|
||||
Uint64 ProgramBindingsRefusalCount() const { return m_bindingRefusals; }
|
||||
|
||||
private:
|
||||
// THE ONE PLACE THE BAND CAN ENTER. An ordinary program's slot comes from the ordinary
|
||||
@@ -426,6 +641,15 @@ namespace MobileGL::MG_Pipe {
|
||||
Bool RecordLive = false;
|
||||
Uint32 RecordGen = 0;
|
||||
Uint32 LinkVersion = 0;
|
||||
// P5e (pg): set_program_bindings' own key, in the SAME latch because it is keyed on
|
||||
// the same handle and a slot recycle has to invalidate both at once. Kept separate
|
||||
// from RecordLive because the two records have different triggers - a create fires
|
||||
// on a link-version move, the bindings fire on a binding move without a relink, and
|
||||
// most frames have neither.
|
||||
Bool BindingsLive = false;
|
||||
Uint32 BindingsGen = 0;
|
||||
Uint32 BindingsBackendStateVersion = 0;
|
||||
Uint32 BindingsBlockBindingVersion = 0;
|
||||
};
|
||||
|
||||
// TWO TABLES, NOT A WIDER ONE, and it is the allocator's own reason repeated where it
|
||||
@@ -461,6 +685,19 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
MGPProgramDesc m_lastDesc{};
|
||||
MGPGlobalConstants m_lastConstants{};
|
||||
// P5e (pg): the three tails and their names, MEMBERS rather than locals so the steady
|
||||
// frame allocates nothing - the same reason every other emitter in this directory keeps
|
||||
// its scratch. Cleared and refilled per emission; none of them outlives the route call
|
||||
// except as the record's own copy on the far side (rule C).
|
||||
MGPProgramBindings m_lastBindings{};
|
||||
Vector<Int32> m_blockBindings;
|
||||
Vector<MGPProgramSamplerUnit> m_samplerUnits;
|
||||
Vector<MGPProgramStorageOverride> m_storageOverrides;
|
||||
Vector<String> m_storageOverrideNames;
|
||||
Vector<const char*> m_storageOverrideNamePtrs;
|
||||
// The stage of each module of the descriptor being built, index-aligned with
|
||||
// GetGeneratedSpirv(); the client arm frames it in front of the archive.
|
||||
Vector<Uint32> m_linkedStageWords;
|
||||
|
||||
Vector<Latch> m_latch;
|
||||
Vector<Latch> m_compositeLatch;
|
||||
@@ -476,6 +713,8 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint64 m_dispatchSets = 0;
|
||||
Uint64 m_constantSets = 0;
|
||||
Uint64 m_moduleTruncations = 0;
|
||||
Uint64 m_bindingSets = 0;
|
||||
Uint64 m_bindingRefusals = 0;
|
||||
};
|
||||
|
||||
inline MGPipeProgramEmitter& MGPipeProgramEmitterInstance() {
|
||||
|
||||
@@ -2929,21 +2929,69 @@ namespace MobileGL::MG_Pipe {
|
||||
++g_applier.ShaderBuffersSerial;
|
||||
}
|
||||
|
||||
// P5e (pg) WRITES THE SECOND BODY. The three post-link mutable reflection fields -
|
||||
// glUniformBlockBinding's block-to-point map, glUniform1i's sampler unit per uniform
|
||||
// LOCATION, and glShaderStorageBlockBinding's name-keyed override set - all live INSIDE
|
||||
// the frontend's LinkArtifacts and all move after the link that produced it. An archive
|
||||
// snapshot alone therefore answers the wrong question: a twin built from it would bind the
|
||||
// uniform blocks the program was LINKED with rather than the ones it is BOUND with.
|
||||
void MGPipeApplySetProgramBindings(const MGPProgramBindings& hdr, const Int32* blockBindings,
|
||||
const MGPProgramSamplerUnit* samplerUnits,
|
||||
const MGPProgramStorageOverride* storageOverrides,
|
||||
const char* const* storageOverrideNames) {
|
||||
(void)blockBindings;
|
||||
(void)samplerUnits;
|
||||
(void)storageOverrides;
|
||||
(void)storageOverrideNames;
|
||||
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"set_program_bindings\"} - the applier entry point "
|
||||
"is declared by P5e package c0e and bodied by package pg; a record for shader CSO "
|
||||
"{%u, %u} with %u/%u/%u tail entries reached it, which means a route was installed "
|
||||
"ahead of its consumer",
|
||||
hdr.Cso.Slot, hdr.Cso.Gen, hdr.BlockBindingCount, hdr.SamplerUnitCount,
|
||||
hdr.StorageOverrideCount);
|
||||
std::abort();
|
||||
// THE BOUNDS ARE THE DECODER'S AND ARE RE-ASSERTED HERE, because the monolith adapter
|
||||
// reaches this entry point without passing the decoder at all: under monolith the only
|
||||
// validation a record ever gets is the one the applier does itself, which is the rule
|
||||
// every sibling in this file already keeps.
|
||||
const char* fault = nullptr;
|
||||
if (hdr.BlockBindingCount > kMGPipeMaxProgramBlockBindings) fault = "BlockBindingCount";
|
||||
else if (hdr.SamplerUnitCount > kMGPipeMaxProgramSamplerUnits) fault = "SamplerUnitCount";
|
||||
else if (hdr.StorageOverrideCount > kMGPipeMaxProgramStorageOverrides) fault = "StorageOverrideCount";
|
||||
else if (hdr.BlockBindingCount != 0 && blockBindings == nullptr) fault = "BlockBindings";
|
||||
else if (hdr.SamplerUnitCount != 0 && samplerUnits == nullptr) fault = "SamplerUnits";
|
||||
else if (hdr.StorageOverrideCount != 0 &&
|
||||
(storageOverrides == nullptr || storageOverrideNames == nullptr)) {
|
||||
fault = "StorageOverrides";
|
||||
}
|
||||
if (fault != nullptr) {
|
||||
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption")
|
||||
" set_program_bindings {slot=%u, gen=%u}: %s is out of range or "
|
||||
"its tail is absent (%u/%u/%u declared)",
|
||||
hdr.Cso.Slot, hdr.Cso.Gen, fault, hdr.BlockBindingCount,
|
||||
hdr.SamplerUnitCount, hdr.StorageOverrideCount);
|
||||
return;
|
||||
}
|
||||
// P4a's belt, for MGPipeApplyBindShaderState's reason: with no consumer this applier
|
||||
// holds no shader CSO record at all, so the resolution below would report the designed
|
||||
// state as RefusedObjectCalls.
|
||||
if (NoP4aConsumer()) return;
|
||||
|
||||
MGPipeShaderCsoRecord* record = ResolveShaderCso("set_program_bindings", hdr.Cso);
|
||||
if (record == nullptr) return;
|
||||
|
||||
// WHOLE-SET REPLACEMENT, NOT A MERGE, in all three tails. Each one describes the
|
||||
// program's complete state for its index space as the client observed it at emit; a
|
||||
// merge would leave a block binding the application has since reset to its declared
|
||||
// default sitting on top of the archive's value for ever, and there is no per-entry
|
||||
// "cleared" token on the wire that could say otherwise.
|
||||
record->BlockBindings.assign(blockBindings, blockBindings + hdr.BlockBindingCount);
|
||||
record->SamplerUnits.assign(samplerUnits, samplerUnits + hdr.SamplerUnitCount);
|
||||
record->StorageOverrides.clear();
|
||||
record->StorageOverrides.reserve(hdr.StorageOverrideCount);
|
||||
for (Uint32 i = 0; i < hdr.StorageOverrideCount; ++i) {
|
||||
MGPipeProgramStorageOverride entry;
|
||||
// COPIED, NOT POINTED AT (rule C). The name rides SEG_STAGE and the staged run
|
||||
// retires with the record that named it; the rebuild that reads this may be a
|
||||
// frame later, and by then those bytes are somebody else's.
|
||||
entry.Name = storageOverrideNames[i] != nullptr ? String(storageOverrideNames[i]) : String();
|
||||
entry.Binding = storageOverrides[i].Binding;
|
||||
record->StorageOverrides.push_back(Move(entry));
|
||||
}
|
||||
record->Signature = hdr.Signature;
|
||||
// ADVANCED, NEVER RETURNED TO A VALUE IT HAS HANDED OUT - VertexBuffersSerial's rule.
|
||||
// Both texture-unit memos and the program twin's clean condition key on it (ruling 6),
|
||||
// so a serial that went backwards would make a memo match state it has never seen.
|
||||
++record->BindingsSerial;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
@@ -3010,7 +3058,8 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
void MGPipeApplyCreateShaderState(const MGPProgramDesc& desc,
|
||||
const MG_State::GLState::LinkArtifacts* link,
|
||||
const MG_State::GLState::SpirvArtifacts* spirv) {
|
||||
const MG_State::GLState::SpirvArtifacts* spirv,
|
||||
SharedPtr<const MG_State::GLState::ProgramArchive> archive) {
|
||||
MOBILEGL_ASSERT(desc.Cso.Slot >= kMGPipeFirstAllocatableSlot,
|
||||
"create_shader_state named the reserved slot 0");
|
||||
if (desc.Cso.Slot < kMGPipeFirstAllocatableSlot) return;
|
||||
@@ -3083,6 +3132,31 @@ namespace MobileGL::MG_Pipe {
|
||||
// deserialised archive is attached to. Either way the applier owns identity, extent and
|
||||
// order, and never content.
|
||||
record.Desc = desc;
|
||||
// P5e (pg), gap G-A: "under split the descriptor is what the deserialised archive is
|
||||
// attached to" is now literally true, and this is the attachment. ADOPTED
|
||||
// UNCONDITIONALLY, INCLUDING A NULL: a monolith create must CLEAR an archive a
|
||||
// previous split-arm create left behind, because the record outlives a transport
|
||||
// change (MGPipeApplierReset keeps the CSO table - a program lives in a share group)
|
||||
// and a stale archive is worse than none.
|
||||
//
|
||||
// AFTER Desc, so a reader that sees the new descriptor sees the artefacts it describes;
|
||||
// the apply thread is the only writer and the only reader, so ordering here is about
|
||||
// reading this function rather than about memory visibility.
|
||||
record.Archive = Move(archive);
|
||||
// THE RE-ISSUE CLEARS THE THREE BINDING TAILS for the reason it clears GlobalConstants
|
||||
// one branch up: they are indices INTO the archive that has just been replaced, so a
|
||||
// surviving tail would name block indices and uniform locations of a program that no
|
||||
// longer exists. Unconditional rather than inside the else-branch above, because a
|
||||
// recycled slot must not inherit its predecessor's bindings either - and a fresh record
|
||||
// has empty tails, so the clear costs a live program nothing. The client re-emits
|
||||
// set_program_bindings immediately after this create (ProgramEmit.h), which is the
|
||||
// ordering the applier relies on: emitting them BEFORE would put them where this line
|
||||
// wipes them.
|
||||
record.BlockBindings.clear();
|
||||
record.SamplerUnits.clear();
|
||||
record.StorageOverrides.clear();
|
||||
record.Signature = 0;
|
||||
++record.BindingsSerial;
|
||||
}
|
||||
|
||||
void MGPipeApplyBindShaderState(const MGPHandleOnly& handle) {
|
||||
|
||||
@@ -38,9 +38,16 @@
|
||||
// this header needs their NAMES and never their definitions; the forward declaration is the
|
||||
// whole coupling and the closure gate is what keeps it one. The verify build is the only
|
||||
// place the codec runs, and it runs from PipeApply.cpp.
|
||||
// P5e (pg) ADDS A THIRD NAME AND KEEPS THE RULE: ProgramArchive is what a SPLIT create carries
|
||||
// - the record's OWN copy of both structs plus the stage of each module - and the record holds
|
||||
// it through a SharedPtr, which needs no definition either (the deleter is captured where the
|
||||
// archive is constructed, in the decoder's TU). So the coupling is still three names and no
|
||||
// closure change, and MG_Backend, which does need the definition, includes
|
||||
// ProgramArtifactsCodec.h for it.
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
struct LinkArtifacts;
|
||||
struct SpirvArtifacts;
|
||||
struct ProgramArchive;
|
||||
} // namespace MobileGL::MG_State::GLState
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
@@ -392,6 +399,28 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint64 GlobalConstantsSerial = 0;
|
||||
Uint64 Serial = 0;
|
||||
|
||||
// ---- P5e's server-owned archive (CONTRACT-P5E.md §5.5, gap G-A) --------------------
|
||||
//
|
||||
// FILLED BY PACKAGE pg, AND ONLY UNDER A TRANSPORT. This is the whole of rule F for
|
||||
// this family: before P5e the record stored the DESCRIPTOR and the artefacts rode
|
||||
// beside it as two companion pointers into the frontend's own ProgramObject, so every
|
||||
// reflection question the program twin asked - forty accessors in SyncToBackend alone -
|
||||
// was a read of client memory that `ProgramObject::Link()` REPLACES IN PLACE. Under
|
||||
// run-ahead the client is already several records past the create, so those reads are
|
||||
// torn or stale by construction and no amount of care at the read site can fix it.
|
||||
//
|
||||
// The client encodes the archive once per link (ProgramEmit.h -> SEG_STAGE), the
|
||||
// decoder frames and deserialises it, and the record ADOPTS it here - a SharedPtr and
|
||||
// not a Vector, because a program-pipeline composite and its stage programs are three
|
||||
// records over one link's artefacts, and because the twin holds the pointer across the
|
||||
// build it is running. It retires with the record, which is rule C: the staged run the
|
||||
// bytes arrived in belongs to somebody else by the next frame.
|
||||
//
|
||||
// NULL UNDER MONOLITH, and that is the arm selection and not an omission (ruling 1):
|
||||
// the push-monolith build keeps its frontend arms token for token, so its twin reads
|
||||
// the frontend's archive as it always has and this pointer is never consulted.
|
||||
SharedPtr<const MG_State::GLState::ProgramArchive> Archive;
|
||||
|
||||
// ---- P5e's set_program_bindings tails (CONTRACT-P5E.md §1, §5.5) -------------------
|
||||
//
|
||||
// DECLARED BY c0e, FILLED BY PACKAGE pg. THE THREE POST-LINK MUTABLE REFLECTION FIELDS
|
||||
@@ -1331,9 +1360,20 @@ namespace MobileGL::MG_Pipe {
|
||||
// field-compares before storing, and a mismatch is Fatal{PipeVerifyDiffer, "program-archive"}.
|
||||
// Splitting this record for a transport whose ring caps one record at half its capacity is
|
||||
// P5's problem, not this entry point's.
|
||||
//
|
||||
// P5e (pg) ADDS THE FOURTH ARGUMENT AND DID NOT DEFAULT IT, for the reason PipeRoute.h
|
||||
// gives about the three byte counts it also refused to default: a caller that HAS the
|
||||
// archive and forgets to pass it would compile, and the failure would be a program twin
|
||||
// reading the frontend's artefacts on the apply thread - silent, and exactly what this
|
||||
// package exists to end. `archive` is null under monolith (the two companion pointers are
|
||||
// the frontend's own and the handle arm is not taken there) and non-null under a
|
||||
// transport, where `link` and `spirv` point INTO it: the record adopts it, so every later
|
||||
// reflection read is of memory the server owns. A non-null archive whose Link/Spirv are
|
||||
// not the two pointers passed beside it is a caller bug this entry point asserts on.
|
||||
void MGPipeApplyCreateShaderState(const MGPProgramDesc& desc,
|
||||
const MG_State::GLState::LinkArtifacts* link,
|
||||
const MG_State::GLState::SpirvArtifacts* spirv);
|
||||
const MG_State::GLState::SpirvArtifacts* spirv,
|
||||
SharedPtr<const MG_State::GLState::ProgramArchive> archive);
|
||||
void MGPipeApplyBindShaderState(const MGPHandleOnly& handle);
|
||||
void MGPipeApplyDeleteShaderState(const MGPHandleOnly& handle);
|
||||
void MGPipeApplySetDrawProgram(const MGPHandleOnly& handle);
|
||||
|
||||
@@ -315,8 +315,33 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
void Mono_Escape_CreateShaderState(const MGPProgramDesc* desc,
|
||||
const MG_State::GLState::LinkArtifacts* link,
|
||||
const MG_State::GLState::SpirvArtifacts* spirv) {
|
||||
MGPipeApplyCreateShaderState(*desc, link, spirv);
|
||||
const MG_State::GLState::SpirvArtifacts* spirv,
|
||||
const Uint32* linkedStages, Uint32 linkedStageCount) {
|
||||
// The stage list is the CLIENT ARM's framing input and this arm has no use for it:
|
||||
// the monolith twin reads GetLinkedShaderStages() off the frontend object it is
|
||||
// handed. Named and discarded rather than left out of the signature, so the two
|
||||
// arms stay one row.
|
||||
(void)linkedStages;
|
||||
(void)linkedStageCount;
|
||||
// P5e (pg): NO ARCHIVE ON THIS ARM, and the null is the arm selection rather than a
|
||||
// hole. Under monolith the two companion pointers ARE the frontend's own archive,
|
||||
// the backend twin reads it through its frontend overload (ruling 1 keeps that arm
|
||||
// token for token), and serialising here to deserialise into the record would put
|
||||
// EncodeProgramArtifacts on the monolith path - which PipeRoute.h's escape note
|
||||
// and PipeApply.h both promise it is not.
|
||||
MGPipeApplyCreateShaderState(*desc, link, spirv, nullptr);
|
||||
}
|
||||
|
||||
// P5e (pg), the fifth escape. A pass-through like the four above: the caller already
|
||||
// holds the three tails and the parallel name array, and under monolith there is no
|
||||
// segment to resolve - the names ARE the frontend's own String::c_str()s, alive for
|
||||
// the duration of the call, which is exactly the lifetime the applier's copy-in needs.
|
||||
void Mono_Escape_SetProgramBindings(const MGPProgramBindings* hdr, const Int32* blockBindings,
|
||||
const MGPProgramSamplerUnit* samplerUnits,
|
||||
const MGPProgramStorageOverride* storageOverrides,
|
||||
const char* const* storageOverrideNames) {
|
||||
MGPipeApplySetProgramBindings(*hdr, blockBindings, samplerUnits, storageOverrides,
|
||||
storageOverrideNames);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -363,6 +388,7 @@ namespace MobileGL::MG_Pipe {
|
||||
gMGPipeRouteEscapes.ResourceFlushRange = &Mono_Escape_ResourceFlushRange;
|
||||
gMGPipeRouteEscapes.MapPersistent = &Mono_Escape_MapPersistent;
|
||||
gMGPipeRouteEscapes.CreateShaderState = &Mono_Escape_CreateShaderState;
|
||||
gMGPipeRouteEscapes.SetProgramBindings = &Mono_Escape_SetProgramBindings;
|
||||
|
||||
// KEPT, not merely installed. The client arm overwrites the three tables above; these
|
||||
// three copies are what it forwards to when it finds itself on the server's own thread.
|
||||
|
||||
@@ -144,6 +144,16 @@ namespace MobileGL::MG_Pipe {
|
||||
// `EncodeProgramArtifacts` on the monolith path, which `PipeApply.h`
|
||||
// explicitly promises it is not ("zero serialisation cost on the
|
||||
// monolith path").
|
||||
// SetProgramBindings THREE tails in three index spaces plus a PARALLEL NAME ARRAY
|
||||
// (P5e, MG_Remote/CONTRACT-P5E.md §1/§5.5). One `varTail` +
|
||||
// `varTailCount` pair cannot express three runs at all, and the
|
||||
// fourth argument is not a run: the storage-override key is a
|
||||
// `MGHostSpan` on the wire and a plain `const char*` under monolith,
|
||||
// so the applier takes the resolved names beside the tail rather
|
||||
// than resolving segments itself (rule C - a span retires with the
|
||||
// record that named it, and the applier copies the bytes it keeps).
|
||||
// The generated `gMGPipeContext.SetProgramBindings` row therefore
|
||||
// STAYS NULL, which is what PipeCatalogueTest already pins.
|
||||
//
|
||||
// OVERTURN CONDITIONS, one per row: give `MGPResourceDesc` and `MGPFlushRange` a blobref
|
||||
// (overturns R-13.2/R-13.3, and c0 owns `MGPipeTypes.h`); give `MGPHandleOnly` a size
|
||||
@@ -154,9 +164,19 @@ namespace MobileGL::MG_Pipe {
|
||||
const MGPRespecifiedLevel* level);
|
||||
void (*ResourceFlushRange)(const MGPFlushRange* record, const void* bytes);
|
||||
void* (*MapPersistent)(const MGPHandleOnly* handle, Uint64 size, const void* seedBytes);
|
||||
// P5e (pg) grew this row by the STAGE LIST, and it belongs on the route rather than in
|
||||
// the two structs: `SpirvArtifacts::generatedSpirv` is one module per shader object and
|
||||
// the stage of each lives in `ProgramObject::m_linkedShaderSnapshot`, which is GL-thread
|
||||
// state and not an artifact. The client arm frames it in front of the archive; the
|
||||
// monolith arm ignores it, because its twin reads the snapshot directly.
|
||||
void (*CreateShaderState)(const MGPProgramDesc* desc,
|
||||
const MG_State::GLState::LinkArtifacts* link,
|
||||
const MG_State::GLState::SpirvArtifacts* spirv);
|
||||
const MG_State::GLState::SpirvArtifacts* spirv,
|
||||
const Uint32* linkedStages, Uint32 linkedStageCount);
|
||||
void (*SetProgramBindings)(const MGPProgramBindings* hdr, const Int32* blockBindings,
|
||||
const MGPProgramSamplerUnit* samplerUnits,
|
||||
const MGPProgramStorageOverride* storageOverrides,
|
||||
const char* const* storageOverrideNames);
|
||||
};
|
||||
inline MGPipeRouteEscapes gMGPipeRouteEscapes{};
|
||||
|
||||
@@ -396,8 +416,9 @@ namespace MobileGL::MG_Pipe {
|
||||
// ---- programs ----------------------------------------------------------------------
|
||||
inline void MGPipeRouteCreateShaderState(const MGPProgramDesc& desc,
|
||||
const MG_State::GLState::LinkArtifacts* link,
|
||||
const MG_State::GLState::SpirvArtifacts* spirv) {
|
||||
gMGPipeRouteEscapes.CreateShaderState(&desc, link, spirv);
|
||||
const MG_State::GLState::SpirvArtifacts* spirv,
|
||||
const Uint32* linkedStages, Uint32 linkedStageCount) {
|
||||
gMGPipeRouteEscapes.CreateShaderState(&desc, link, spirv, linkedStages, linkedStageCount);
|
||||
}
|
||||
inline void MGPipeRouteBindShaderState(const MGPHandleOnly& handle) {
|
||||
MGP_BindShaderState(&handle);
|
||||
@@ -415,6 +436,18 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint64 byteCount) {
|
||||
MGP_SetGlobalConstants(&record, bytes, byteCount);
|
||||
}
|
||||
// P5e (pg), opcode 80. The fifth escape; see MGPipeRouteEscapes above for why this row has
|
||||
// no generated shape. `storageOverrideNames` is index-aligned with `storageOverrides` and
|
||||
// is what the applier copies into the record - the MGHostSpan inside each override element
|
||||
// describes where the name lives on the WIRE and is resolved by the decoder, never here.
|
||||
inline void MGPipeRouteSetProgramBindings(const MGPProgramBindings& hdr,
|
||||
const Int32* blockBindings,
|
||||
const MGPProgramSamplerUnit* samplerUnits,
|
||||
const MGPProgramStorageOverride* storageOverrides,
|
||||
const char* const* storageOverrideNames) {
|
||||
gMGPipeRouteEscapes.SetProgramBindings(&hdr, blockBindings, samplerUnits, storageOverrides,
|
||||
storageOverrideNames);
|
||||
}
|
||||
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
// P5e (pg): ID-87 asks this package for bytes-per-link measured rather than argued, and the
|
||||
// archive's byte count exists exactly once - here, where it is serialised.
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
@@ -492,9 +495,11 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// Spirv[i] is Fatal on the far side rather than ignored.
|
||||
void Wire_Escape_CreateShaderState(const MG_Pipe::MGPProgramDesc* desc,
|
||||
const MG_State::GLState::LinkArtifacts* link,
|
||||
const MG_State::GLState::SpirvArtifacts* spirv) {
|
||||
const MG_State::GLState::SpirvArtifacts* spirv,
|
||||
const Uint32* linkedStages, Uint32 linkedStageCount) {
|
||||
if (RunsAsTheServerRole()) {
|
||||
MG_Pipe::MGPipeMonolithEscapes().CreateShaderState(desc, link, spirv);
|
||||
MG_Pipe::MGPipeMonolithEscapes().CreateShaderState(desc, link, spirv, linkedStages,
|
||||
linkedStageCount);
|
||||
return;
|
||||
}
|
||||
ClientSession& session = RequireSession("CreateShaderState");
|
||||
@@ -506,11 +511,23 @@ namespace MobileGL::MG_Remote::Client {
|
||||
"code");
|
||||
std::abort();
|
||||
}
|
||||
// EncodeProgramArtifacts APPENDS and never fails - everything it walks is owned
|
||||
// EncodeProgramArchive APPENDS and never fails - everything it walks is owned
|
||||
// plain data - so an empty archive means the two structs themselves were empty,
|
||||
// which is a linked program with no artefacts and is not a codec question.
|
||||
//
|
||||
// P5e (pg): THE FRAMED form, so the record's own copy on the far side can pair each
|
||||
// module with its stage. The bare EncodeProgramArtifacts is still what the verify
|
||||
// build's round-trip pin uses; this is the one that crosses.
|
||||
Vector<Uint8> archive;
|
||||
MG_State::GLState::EncodeProgramArtifacts(*link, *spirv, archive);
|
||||
Vector<Uint32> stages(linkedStages, linkedStages + linkedStageCount);
|
||||
MG_State::GLState::EncodeProgramArchive(*link, *spirv, stages, archive);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// ID-87's measurement, taken where the bytes actually exist rather than
|
||||
// estimated: one sample per link, so the steady-state cost is (links this
|
||||
// frame) x (bytes per link) and both halves are countable from the stats line.
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::CsoBlobBytes,
|
||||
static_cast<Uint64>(archive.size()));
|
||||
}
|
||||
if (archive.empty()) {
|
||||
MGLOG_F("MGPipe: Fatal{ArchiveEmpty, \"create_shader_state\"} - the program's "
|
||||
"artefacts serialised to nothing");
|
||||
@@ -524,6 +541,71 @@ namespace MobileGL::MG_Remote::Client {
|
||||
++g_emitted;
|
||||
}
|
||||
|
||||
// set_program_bindings (P5e, pg). THE FIFTH ESCAPE, for PipeRoute.h's reason: three
|
||||
// tails in three index spaces plus a parallel name array, which no generated
|
||||
// (payload, varTail, varTailCount) row can express.
|
||||
//
|
||||
// THE NAMES ARE STAGED ONE BY ONE and each element's own MGHostSpan names its run, so
|
||||
// the third tail is self-describing the way set_storage_block_binding's single name
|
||||
// already is. Staged rather than packed into one run with offsets, because the honesty
|
||||
// pass the decoder runs is per span and a packed run would have to be re-split there
|
||||
// against arithmetic nothing on the wire declares.
|
||||
void Wire_Escape_SetProgramBindings(const MG_Pipe::MGPProgramBindings* hdr,
|
||||
const Int32* blockBindings,
|
||||
const MG_Pipe::MGPProgramSamplerUnit* samplerUnits,
|
||||
const MG_Pipe::MGPProgramStorageOverride* storageOverrides,
|
||||
const char* const* storageOverrideNames) {
|
||||
if (RunsAsTheServerRole()) {
|
||||
MG_Pipe::MGPipeMonolithEscapes().SetProgramBindings(hdr, blockBindings, samplerUnits,
|
||||
storageOverrides,
|
||||
storageOverrideNames);
|
||||
return;
|
||||
}
|
||||
ClientSession& session = RequireSession("SetProgramBindings");
|
||||
MG_Pipe::MGPProgramBindings record = *hdr;
|
||||
|
||||
// The override tail is COPIED before it is emitted, because the span is the one
|
||||
// field the client must write after the caller is done with it - the same reason
|
||||
// MGP_WIRE_BLOB copies its payload.
|
||||
Vector<MG_Pipe::MGPProgramStorageOverride> overrides;
|
||||
overrides.reserve(record.StorageOverrideCount);
|
||||
for (Uint32 i = 0; i < record.StorageOverrideCount; ++i) {
|
||||
MG_Pipe::MGPProgramStorageOverride entry = storageOverrides[i];
|
||||
const char* const name = storageOverrideNames[i];
|
||||
// Size = strlen + 1: THE NUL TRAVELS, exactly as set_storage_block_binding's
|
||||
// name does (contract table 0's block-name row), and the decoder refuses a run
|
||||
// whose last byte is not NUL.
|
||||
const Uint64 nameBytes = static_cast<Uint64>(std::strlen(name)) + 1ull;
|
||||
const MG_Pipe::MGPBlobRef staged = session.Encoder().StageBytes(name, nameBytes);
|
||||
// A HOST SPAN AND NOT A BLOBREF, which is what the row's kHostSpan flag means:
|
||||
// the same run, described in the shape the decoder's honesty pass reads. Ptr
|
||||
// stays null - rule B, "the encoder writes nullptr and names SEG_STAGE" - so
|
||||
// the name is unreachable on a second process except through the segment table,
|
||||
// which is the whole point of the flag.
|
||||
entry.Name = MG_Pipe::MGHostSpan{};
|
||||
entry.Name.Ptr = nullptr;
|
||||
entry.Name.Seg = staged.Seg;
|
||||
entry.Name.Offset = staged.Offset;
|
||||
entry.Name.Size = staged.Size;
|
||||
overrides.push_back(entry);
|
||||
}
|
||||
|
||||
const Wire::WireTail tails[3] = {
|
||||
{blockBindings, static_cast<Uint64>(record.BlockBindingCount) * sizeof(Int32)},
|
||||
{samplerUnits, static_cast<Uint64>(record.SamplerUnitCount) *
|
||||
sizeof(MG_Pipe::MGPProgramSamplerUnit)},
|
||||
{overrides.empty() ? nullptr : overrides.data(),
|
||||
static_cast<Uint64>(record.StorageOverrideCount) *
|
||||
sizeof(MG_Pipe::MGPProgramStorageOverride)},
|
||||
};
|
||||
// ALWAYS THREE, even when a count is 0 - the decoder derives the third tail's
|
||||
// offset from the first two, so declaring fewer would put the same bytes somewhere
|
||||
// else (PipeWireCodec.cpp's layout arm says this from the other side).
|
||||
session.EmitAndWaitTails(MGPWireOp::SetProgramBindings, &record, sizeof(record), tails, 3,
|
||||
nullptr, 0, nullptr);
|
||||
++g_emitted;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// =====================================================================================
|
||||
@@ -670,6 +752,7 @@ namespace MobileGL::MG_Remote::Client {
|
||||
gMGPipeRouteEscapes.ResourceFlushRange = &Wire_Escape_ResourceFlushRange;
|
||||
gMGPipeRouteEscapes.MapPersistent = &Wire_Escape_MapPersistent;
|
||||
gMGPipeRouteEscapes.CreateShaderState = &Wire_Escape_CreateShaderState;
|
||||
gMGPipeRouteEscapes.SetProgramBindings = &Wire_Escape_SetProgramBindings;
|
||||
|
||||
MGPipeNoteInstalledArm(MGPipeRouteArm::kClientWire);
|
||||
}
|
||||
|
||||
@@ -1767,17 +1767,28 @@ namespace MobileGL::MG_Remote::Wire {
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
const void* archive = ResolveOrFatal(op, desc.Reflection);
|
||||
MG_State::GLState::LinkArtifacts link;
|
||||
MG_State::GLState::SpirvArtifacts spirv;
|
||||
if (!MG_State::GLState::DecodeProgramArtifacts(static_cast<const Uint8*>(archive),
|
||||
static_cast<SizeT>(desc.Reflection.Size),
|
||||
link, spirv)) {
|
||||
const void* archiveBytes = ResolveOrFatal(op, desc.Reflection);
|
||||
// P5e (pg), gap G-A: DECODED ONTO THE HEAP AND HANDED TO THE RECORD, not onto this
|
||||
// stack. Before P5e these were two locals that died at the closing brace and the
|
||||
// applier kept only the descriptor, so every reflection question the program twin
|
||||
// later asked went back to the frontend's own ProgramObject - which Link() replaces
|
||||
// in place, and which a run-ahead client is several records past by then. The
|
||||
// record adopts the archive here and answers from it for the rest of its life.
|
||||
//
|
||||
// AND THE FRAME, not the bare codec stream: ProgramArchive carries the stage of
|
||||
// each module beside the modules, because SpirvArtifacts does not and StageMask
|
||||
// cannot stand in for it (two shader objects may share a stage). See
|
||||
// ProgramArtifactsCodec.h.
|
||||
auto archive = MakeShared<MG_State::GLState::ProgramArchive>();
|
||||
if (!MG_State::GLState::DecodeProgramArchive(static_cast<const Uint8*>(archiveBytes),
|
||||
static_cast<SizeT>(desc.Reflection.Size),
|
||||
*archive)) {
|
||||
WireProtocolFatal("CreateShaderState.Reflection",
|
||||
"DecodeProgramArtifacts refused the archive - a truncated "
|
||||
"stream, a codec version mismatch or a struct-size mismatch");
|
||||
"DecodeProgramArchive refused the archive - a truncated "
|
||||
"stream, a codec version mismatch, a struct-size mismatch, or "
|
||||
"a stage list that does not match the modules it frames");
|
||||
}
|
||||
MGPipeApplyCreateShaderState(desc, &link, &spirv);
|
||||
MGPipeApplyCreateShaderState(desc, &archive->Link, &archive->Spirv, archive);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1878,12 +1889,11 @@ namespace MobileGL::MG_Remote::Wire {
|
||||
return false;
|
||||
|
||||
case MGPWireOp::SetProgramBindings: {
|
||||
// P5e, opcode 80 (MG_Remote/CONTRACT-P5E.md §1). NO APPLIER ENTRY POINT EXISTS YET
|
||||
// and c0e does not invent one: the record's consumer is the program twin's
|
||||
// rebuild, which package pg writes. What the record needs NOW and cannot get later
|
||||
// is its shape - the three declared counts against their bounds and the three-tail
|
||||
// arithmetic, both of which MGPipeWireRecordLayout above has already run - plus the
|
||||
// honesty of every name span it carries.
|
||||
// P5e, opcode 80 (MG_Remote/CONTRACT-P5E.md §1). c0e wrote the shape check and left
|
||||
// the arm declining; PACKAGE pg COMPLETES IT. What the record needs before its
|
||||
// consumer sees it is its shape - the three declared counts against their bounds
|
||||
// and the three-tail arithmetic, both of which MGPipeWireRecordLayout above has
|
||||
// already run - plus the honesty of every name span it carries.
|
||||
//
|
||||
// THE SPANS ARE WALKED HERE RATHER THAN BY THE ENCODER'S BLANKET PASS because they
|
||||
// are MEMBERS of the third tail's 40-byte elements, not a bare MGHostSpan array:
|
||||
@@ -1891,17 +1901,48 @@ namespace MobileGL::MG_Remote::Wire {
|
||||
// pad} would read one span and a quarter of garbage. All four arms of
|
||||
// CheckHostSpanIsHonest run, the segment-range one included, so an override name
|
||||
// pointing outside SEG_STAGE is caught on the way in rather than at the rebuild.
|
||||
const auto& bindings = *static_cast<const MGPProgramBindings*>(payload);
|
||||
// RESOLVED HERE AND VALID ONLY FOR THIS CALL (rule C): the names live in the staged
|
||||
// run this record named, and the applier copies them into the record before it
|
||||
// returns. A Vector of resolved pointers rather than a second pass inside the
|
||||
// applier, because segment resolution is the decoder's job and the applier has no
|
||||
// segment table - it is reached by the monolith adapter too, where the names are
|
||||
// the frontend's own c_str()s and there is nothing to resolve.
|
||||
Vector<const char*> overrideNames;
|
||||
const MGPProgramStorageOverride* overrides = nullptr;
|
||||
if (layout.TailCount == 3 && layout.TailBytes[2] != 0 && m_segments != nullptr) {
|
||||
const auto* overrides = tailAt(2);
|
||||
overrides = reinterpret_cast<const MGPProgramStorageOverride*>(tailAt(2));
|
||||
const Uint64 count = layout.TailBytes[2] / sizeof(MGPProgramStorageOverride);
|
||||
overrideNames.reserve(static_cast<SizeT>(count));
|
||||
for (Uint64 i = 0; i < count; ++i) {
|
||||
MGPProgramStorageOverride entry{};
|
||||
std::memcpy(&entry, overrides + i * sizeof(MGPProgramStorageOverride),
|
||||
std::memcpy(&entry, reinterpret_cast<const Uint8*>(overrides) +
|
||||
i * sizeof(MGPProgramStorageOverride),
|
||||
sizeof(MGPProgramStorageOverride));
|
||||
CheckHostSpanIsHonest(entry.Name, *m_segments);
|
||||
// THE NUL IS THE CLIENT'S AND IS CHECKED, not assumed: the emitter stages
|
||||
// the name with its terminator (ProgramEmit.h), and a span whose last byte
|
||||
// is not 0 would make the applier's String(const char*) walk off the end of
|
||||
// SEG_STAGE. Honest-but-unterminated is the one shape CheckHostSpanIsHonest
|
||||
// cannot see.
|
||||
const auto* name = static_cast<const char*>(
|
||||
m_segments->Resolve(entry.Name.Seg, entry.Name.Offset, entry.Name.Size));
|
||||
if (entry.Name.Size == 0 || name == nullptr ||
|
||||
name[entry.Name.Size - 1] != '\0') {
|
||||
WireProtocolFatal("SetProgramBindings.StorageOverrides.Name",
|
||||
"an override name span is empty or is not "
|
||||
"NUL-terminated; the applier copies it as a C string");
|
||||
}
|
||||
overrideNames.push_back(name);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
MGPipeApplySetProgramBindings(
|
||||
bindings,
|
||||
layout.TailBytes[0] != 0 ? reinterpret_cast<const Int32*>(tailAt(0)) : nullptr,
|
||||
layout.TailBytes[1] != 0 ? reinterpret_cast<const MGPProgramSamplerUnit*>(tailAt(1))
|
||||
: nullptr,
|
||||
overrides, overrideNames.empty() ? nullptr : overrideNames.data());
|
||||
return true;
|
||||
}
|
||||
|
||||
case MGPWireOp::SetGlobalConstants: {
|
||||
|
||||
@@ -242,6 +242,14 @@ namespace MobileGL::MG_State::GLState {
|
||||
// (ProgramArtifacts.h's libc++ branch until the integrator fills it in) this is 0,
|
||||
// which still round-trips within one build - the echo compares what THIS build wrote
|
||||
// against what THIS build expects - and stops mattering the moment the pin lands.
|
||||
//
|
||||
// P5e (pg) READ THIS LINE AS THE CONTRACT ASKED (CONTRACT-P5E §5.5) and its verdict is
|
||||
// in ProgramArtifactsCodec.h's header comment: the caveat is about the ECHO WORD, not
|
||||
// about a member the codec skips - the one member VisitFields omits is
|
||||
// `LinkArtifacts::program`, and no backend twin reads it. A 0 echo is exact for an
|
||||
// in-process split, where the writer and the reader are one build, and becomes a real
|
||||
// cross-toolchain check when P6 puts a client and a server in different processes
|
||||
// built by different compilers.
|
||||
#ifdef MGL_LINKARTIFACTS_SIZE
|
||||
inline constexpr Uint64 kLinkArtifactsSizeEcho = MGL_LINKARTIFACTS_SIZE;
|
||||
#else
|
||||
@@ -300,4 +308,55 @@ namespace MobileGL::MG_State::GLState {
|
||||
link.program = nullptr;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- P5e (pg): the frame -------------------------------------------------------------
|
||||
//
|
||||
// ONE MORE LENGTH-PREFIXED RUN IN FRONT, and deliberately in front rather than behind:
|
||||
// DecodeProgramArtifacts refuses trailing bytes ("the format accounts for every byte it
|
||||
// writes"), which is a rule worth keeping, so the stage list is consumed BEFORE the
|
||||
// codec's own stream is handed the exact remainder. The version word inside that stream
|
||||
// still governs the archive proper; the frame has no version of its own because it is one
|
||||
// count and one run of Uint32 and there is nothing about it a future reader could
|
||||
// misinterpret without the count already disagreeing.
|
||||
void EncodeProgramArchive(const LinkArtifacts& link, const SpirvArtifacts& spirv,
|
||||
const Vector<Uint32>& linkedStages, Vector<Uint8>& out) {
|
||||
PutRaw(out, static_cast<Uint32>(linkedStages.size()));
|
||||
for (const Uint32 stage : linkedStages) PutRaw(out, stage);
|
||||
EncodeProgramArtifacts(link, spirv, out);
|
||||
}
|
||||
|
||||
Bool DecodeProgramArchive(const Uint8* bytes, SizeT size, ProgramArchive& out) {
|
||||
out = ProgramArchive{};
|
||||
if (bytes == nullptr) return false;
|
||||
if (size < sizeof(Uint32)) return false;
|
||||
|
||||
Uint32 stageCount = 0;
|
||||
std::memcpy(&stageCount, bytes, sizeof(stageCount));
|
||||
// BOUNDED BEFORE IT IS MULTIPLIED, the same rule the codec's own TakeCount keeps: a
|
||||
// corrupt count must not become a four-billion-element resize, and a count past the
|
||||
// declared maximum is a program this frame cannot describe rather than one to truncate.
|
||||
if (static_cast<SizeT>(stageCount) > kProgramArchiveMaxStages) return false;
|
||||
const SizeT framed = sizeof(Uint32) + static_cast<SizeT>(stageCount) * sizeof(Uint32);
|
||||
if (size < framed) return false;
|
||||
|
||||
out.LinkedStages.resize(stageCount);
|
||||
for (Uint32 i = 0; i < stageCount; ++i) {
|
||||
std::memcpy(&out.LinkedStages[i], bytes + sizeof(Uint32) + i * sizeof(Uint32),
|
||||
sizeof(Uint32));
|
||||
}
|
||||
if (!DecodeProgramArtifacts(bytes + framed, size - framed, out.Link, out.Spirv)) {
|
||||
out = ProgramArchive{};
|
||||
return false;
|
||||
}
|
||||
// THE TWO HALVES MUST AGREE, and this is the only place that can say so: the backend
|
||||
// pairs linkedStages[i] with generatedSpirv[i] and indexes both by one running index,
|
||||
// so a frame whose count disagrees with the decoded module count would read off the
|
||||
// end of one of them. The frontend builds both from one snapshot loop, so a mismatch
|
||||
// here is a wire fault and not a program shape.
|
||||
if (out.LinkedStages.size() != out.Spirv.generatedSpirv.size()) {
|
||||
out = ProgramArchive{};
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace MobileGL::MG_State::GLState
|
||||
|
||||
@@ -42,6 +42,26 @@
|
||||
// * and `LinkArtifacts::program` is NEVER visited. It is the live glslang TProgram, it is
|
||||
// null for every archived instance by construction, and VisitFields deliberately omits it
|
||||
// (57 of the 58 members). Decode leaves it null.
|
||||
//
|
||||
// P5e (pg), CONTRACT-P5E §5.5 "codec completeness": THE SKIPPED MEMBER IS NAMED, AND IT IS
|
||||
// `LinkArtifacts::program`. The contract asks this package either to carry it or to prove the
|
||||
// twin never reads it, and the proof is a closed grep rather than an argument: every read of
|
||||
// that member in the tree is inside `ProgramLinkTask.cpp` (`DoReflection`, which is the only
|
||||
// code that may dereference it at all) and the three sites that `reset()` it -
|
||||
// `ProgramLinkTask.cpp:678,764,877` and `ProgramObject::ResetLinkArtifacts`. `MG_Backend/`
|
||||
// contains NO read of it, in either backend; `ProgramTranslationCache` asserts it is null at
|
||||
// insert. So the archive answers every reflection question the program twin asks, and the one
|
||||
// member it does not carry is one no twin has ever asked for. It could not be carried in any
|
||||
// case: it points into a glslang arena that no archived instance owns.
|
||||
//
|
||||
// AND THE OTHER HALF OF THAT CONTRACT LINE - `ProgramArtifactsCodec.cpp`'s libc++ caveat - is
|
||||
// about the STRUCT-SIZE ECHO, not about a member: under a toolchain whose `MGL_LINKARTIFACTS_SIZE`
|
||||
// is not pinned yet (the NDK's libc++ branch of `ProgramArtifacts.h`, inert until the integrator
|
||||
// fills it in) the echo word is 0. That is SAFE FOR P5e AND NOT A GAP: the echo compares what
|
||||
// this build wrote against what this build expects, so a round trip within one build is exact,
|
||||
// and P5e is an in-process split where the writer and the reader ARE one build. It becomes a
|
||||
// real check the day a client and a server built by different toolchains share a wire, which is
|
||||
// P6's boundary and P6's problem - stated here so it cannot be discovered instead.
|
||||
namespace MobileGL::MG_State::GLState {
|
||||
|
||||
// Bumped whenever the byte format changes in a way a previous reader would misread. A
|
||||
@@ -60,6 +80,45 @@ namespace MobileGL::MG_State::GLState {
|
||||
Bool DecodeProgramArtifacts(const Uint8* bytes, SizeT size, LinkArtifacts& link,
|
||||
SpirvArtifacts& spirv);
|
||||
|
||||
// ---- P5e (pg): the archive as the SERVER OWNS it -------------------------------------
|
||||
//
|
||||
// WHAT THE TWO STRUCTS ABOVE DO NOT CARRY, and why a package that makes the server answer
|
||||
// every reflection question out of them had to notice: THE STAGE OF EACH MODULE.
|
||||
// `SpirvArtifacts::generatedSpirv` is one module per SHADER OBJECT the link consumed, in
|
||||
// snapshot order, and the stage of each lives in `ProgramObject::m_linkedShaderSnapshot` -
|
||||
// GL-thread state, not an artifact. `MGPProgramDesc::StageMask` cannot stand in for it: it
|
||||
// is a bit SET, and GL lets two shader objects of the same stage be attached to one
|
||||
// program, so a list rebuilt from the mask would be shorter than `generatedSpirv` and the
|
||||
// backend's "linked stages and modules must agree" refusal would fire on a perfectly good
|
||||
// program.
|
||||
//
|
||||
// So the frame is [stage count][stage words][the codec's own bytes], written and read by
|
||||
// this package on both sides, and `ProgramArchive` is what a decode produces: the record's
|
||||
// OWN copy, with the lifetime of the record rather than of the staged run that carried it
|
||||
// (rule C). Stages travel as Uint32 rather than as `ShaderStage` so this header keeps its
|
||||
// include closure - `ProgramArtifacts.h` beside it may not reach `ShaderStage.h`.
|
||||
struct ProgramArchive {
|
||||
LinkArtifacts Link;
|
||||
SpirvArtifacts Spirv;
|
||||
// ShaderStage, one per Spirv.generatedSpirv entry, at the same index.
|
||||
Vector<Uint32> LinkedStages;
|
||||
};
|
||||
|
||||
// The bound a decode refuses past: six is what MGPProgramDesc::Spirv[] can name and what
|
||||
// ProgramEmit.h's own truncation counter already watches, doubled so that a program this
|
||||
// stack can build is never refused here for a reason the emitter did not already count.
|
||||
inline constexpr SizeT kProgramArchiveMaxStages = 12;
|
||||
|
||||
// Appends the framed archive to `out` (not cleared, so a caller may frame it further).
|
||||
// `linkedStages` must be index-aligned with `spirv.generatedSpirv`.
|
||||
void EncodeProgramArchive(const LinkArtifacts& link, const SpirvArtifacts& spirv,
|
||||
const Vector<Uint32>& linkedStages, Vector<Uint8>& out);
|
||||
|
||||
// Replaces `out` with what `bytes` describes. Same refusal terms as
|
||||
// DecodeProgramArtifacts, plus: a stage count past kProgramArchiveMaxStages, and a stage
|
||||
// count that does not match the decoded module count.
|
||||
Bool DecodeProgramArchive(const Uint8* bytes, SizeT size, ProgramArchive& out);
|
||||
|
||||
// How many fields a type's VisitFields table actually visits. The codec walks exactly that
|
||||
// table, so this is what pins "the codec did not quietly grow an arm of its own" - most of
|
||||
// all for LinkArtifacts, whose 58th member is the live TProgram the table omits. It is a
|
||||
|
||||
@@ -279,7 +279,7 @@ TEST(CompositeResolver, ACompositeRecordLandsInTheBandsOwnTableAndNeverGrowsTheO
|
||||
const MGPipeHandle composite{kMGPipeShaderCsoCompositeSlotBase + 2, 1};
|
||||
ASSERT_TRUE(MGPipeIsCompositeShaderSlot(composite.Slot));
|
||||
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(composite, 0x3u), &link, &spirv);
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(composite, 0x3u), &link, &spirv, nullptr);
|
||||
EXPECT_TRUE(MGPipeApplier().ShaderCsos.empty())
|
||||
<< "one composite grew the ordinary table to the band's base - that is the 236 MB spike";
|
||||
ASSERT_EQ(MGPipeApplier().CompositeShaderCsos.size(), 3u)
|
||||
@@ -298,7 +298,7 @@ TEST(CompositeResolver, ACompositeRecordLandsInTheBandsOwnTableAndNeverGrowsTheO
|
||||
|
||||
// An ordinary program lands in the other table, and the two do not see each other even
|
||||
// though the composite's record is at index 2 of its own.
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(MGPipeHandle{2, 1}, 0x7u), &link, &spirv);
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(MGPipeHandle{2, 1}, 0x7u), &link, &spirv, nullptr);
|
||||
ASSERT_GT(MGPipeApplier().ShaderCsos.size(), 2u);
|
||||
EXPECT_EQ(MGPipeApplier().ShaderCsos[2].Desc.StageMask, 0x7u);
|
||||
EXPECT_EQ(MGPipeApplier().CompositeShaderCsos[2].Desc.StageMask, 0x3u)
|
||||
@@ -319,7 +319,7 @@ TEST(CompositeResolver, ASecondDeleteOfACompositeIsARefusedNoOpRatherThanASecond
|
||||
const SpirvArtifacts spirv;
|
||||
const MGPipeHandle composite{kMGPipeShaderCsoCompositeSlotBase, 3};
|
||||
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(composite, 0x3u), &link, &spirv);
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(composite, 0x3u), &link, &spirv, nullptr);
|
||||
MGPipeApplySetDrawProgram(ProgramHandle(composite));
|
||||
ASSERT_EQ(MGPipeApplier().DrawProgram, composite);
|
||||
|
||||
@@ -337,7 +337,7 @@ TEST(CompositeResolver, ASecondDeleteOfACompositeIsARefusedNoOpRatherThanASecond
|
||||
|
||||
// And the band's slot is re-usable afterwards: a recycled composite is a new identity and
|
||||
// starts its record over.
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(MGPipeHandle{composite.Slot, 4}, 0x1u), &link, &spirv);
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(MGPipeHandle{composite.Slot, 4}, 0x1u), &link, &spirv, nullptr);
|
||||
EXPECT_TRUE(MGPipeApplier().CompositeShaderCsos[0].Live);
|
||||
EXPECT_EQ(MGPipeApplier().CompositeShaderCsos[0].Gen, 4u);
|
||||
EXPECT_EQ(MGPipeApplier().CompositeShaderCsos[0].Serial, 0u);
|
||||
@@ -358,7 +358,7 @@ TEST(CompositeResolver, ASlotAtTheShaderCsoLimitIsRefusedWhileTheLastBandSlotIsN
|
||||
// The positive control: the LAST slot of the band is a legal composite handle.
|
||||
const MGPipeHandle last{kMGPipeShaderCsoSlotLimit - 1, 1};
|
||||
ASSERT_TRUE(MGPipeIsCompositeShaderSlot(last.Slot));
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(last, 0x3u), &link, &spirv);
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(last, 0x3u), &link, &spirv, nullptr);
|
||||
ASSERT_EQ(MGPipeApplier().CompositeShaderCsos.size(),
|
||||
static_cast<SizeT>(kMGPipeShaderCsoSlotLimit - kMGPipeShaderCsoCompositeSlotBase));
|
||||
EXPECT_TRUE(MGPipeApplier().CompositeShaderCsos.back().Live);
|
||||
@@ -367,7 +367,7 @@ TEST(CompositeResolver, ASlotAtTheShaderCsoLimitIsRefusedWhileTheLastBandSlotIsN
|
||||
const MGPProgramDesc past = CompositeDesc(MGPipeHandle{kMGPipeShaderCsoSlotLimit, 1}, 0x3u);
|
||||
ExpectRefusedNaming("create_shader_state {slot=1048576, gen=1}: the slot is outside the record table's "
|
||||
"bound",
|
||||
[&past, &link, &spirv]() { MGPipeApplyCreateShaderState(past, &link, &spirv); });
|
||||
[&past, &link, &spirv]() { MGPipeApplyCreateShaderState(past, &link, &spirv, nullptr); });
|
||||
|
||||
// And an ORDINARY slot at or above the band's base is out of range by definition: the
|
||||
// allocator refuses the band for an ordinary program, so nothing legal can name one.
|
||||
@@ -684,7 +684,7 @@ void main() { o_color = vec4(0.5); }
|
||||
const MGPipeHandle handleB = MGPipeSlots().AllocateComposite(compositeB->GetLifetimeId());
|
||||
ASSERT_NE(handleA, handleB);
|
||||
for (const MGPipeHandle handle : {handleA, handleB}) {
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(handle, 0x3u), &link, &spirv);
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(handle, 0x3u), &link, &spirv, nullptr);
|
||||
MGPipeNoteHandlePublished(MGPipeKind::ShaderCso, handle);
|
||||
}
|
||||
const Uint64 releasesBefore = resolver.GetCounters().Releases;
|
||||
@@ -736,7 +736,7 @@ void main() { o_color = vec4(0.5); }
|
||||
const ProgramPipelineObject pipeline{kPipelineName};
|
||||
SharedPtr<ProgramObject> composite = MakeShared<ProgramObject>(0u);
|
||||
const MGPipeHandle handle = MGPipeSlots().AllocateComposite(composite->GetLifetimeId());
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(handle, 0x3u), &link, &spirv);
|
||||
MGPipeApplyCreateShaderState(CompositeDesc(handle, 0x3u), &link, &spirv, nullptr);
|
||||
MGPipeNoteHandlePublished(MGPipeKind::ShaderCso, handle);
|
||||
ASSERT_EQ(resolver.Observe(kDoomedContext, pipeline, *composite, handle), handle);
|
||||
ASSERT_EQ(resolver.Size(), sizeBefore + 1u);
|
||||
|
||||
@@ -266,7 +266,12 @@ TEST(PipeCatalogue, ExactlyTheRoutedRowsAreInstalledAndTheRestAreStillNull) {
|
||||
for (SizeT i = 0; i < sizeof(MGPipeRouteEscapes) / sizeof(void*); ++i) {
|
||||
if (escapes[i] != nullptr) ++escapesInstalled;
|
||||
}
|
||||
EXPECT_EQ(escapesInstalled, 4u) << "an escape row is null; its call site would take a null "
|
||||
// P5e (pg) MADE IT FIVE: set_program_bindings carries three tails in three index spaces
|
||||
// plus a parallel name array, which no generated (payload, varTail, varTailCount) row can
|
||||
// express - so opcode 80's `gMGPipeContext` slot stays null (pinned below, unchanged) and
|
||||
// its adapter lives in the escape table beside create_shader_state's. The applier's entry
|
||||
// points are 39 with it.
|
||||
EXPECT_EQ(escapesInstalled, 5u) << "an escape row is null; its call site would take a null "
|
||||
"pointer rather than fall back to anything";
|
||||
EXPECT_EQ(installed + escapesInstalled, 39u)
|
||||
<< "the two tables plus the escapes must be exactly PipeApply.h's bodied entry points";
|
||||
|
||||
@@ -66,6 +66,9 @@
|
||||
// drives create_shader_state needs their definitions - the applier's own header deliberately
|
||||
// only forward-declares them.
|
||||
#include <MG_State/GLState/ProgramState/ProgramArtifacts.h>
|
||||
// P5e (pg): the FRAMED archive - ProgramArchive and the two frame entry points - is what
|
||||
// crosses under split, and the record holds one; the round-trip case below drives it directly.
|
||||
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#endif
|
||||
|
||||
@@ -258,7 +261,7 @@ TEST(ProgramEmit, ACreateStoresTheDescriptorAndARelinkCountsUpAndDropsTheBlockKe
|
||||
const MGPipeHandle cso{5, 2};
|
||||
const Uint8 block[64] = {};
|
||||
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x3u, 64), &link, &spirv);
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x3u, 64), &link, &spirv, nullptr);
|
||||
EXPECT_TRUE(ProgramRecordOf(5).Live);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Gen, 2u);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Serial, 0u) << "a create is not a mutation";
|
||||
@@ -274,7 +277,7 @@ TEST(ProgramEmit, ACreateStoresTheDescriptorAndARelinkCountsUpAndDropsTheBlockKe
|
||||
const Uint64 blockSerial = ProgramRecordOf(5).GlobalConstantsSerial;
|
||||
|
||||
// The relink.
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x7u, 32), &link, &spirv);
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x7u, 32), &link, &spirv, nullptr);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Serial, 1u);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Desc.StageMask, 0x7u);
|
||||
EXPECT_TRUE(ProgramRecordOf(5).GlobalConstants.empty())
|
||||
@@ -285,7 +288,7 @@ TEST(ProgramEmit, ACreateStoresTheDescriptorAndARelinkCountsUpAndDropsTheBlockKe
|
||||
|
||||
// A RECYCLED SLOT STARTS OVER: inheriting one field of the previous occupant is how a
|
||||
// program at a recycled slot inherits its predecessor's reflection.
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(MGPipeHandle{5, 3}, 0x1u, 16), &link, &spirv);
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(MGPipeHandle{5, 3}, 0x1u, 16), &link, &spirv, nullptr);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Gen, 3u);
|
||||
EXPECT_EQ(ProgramRecordOf(5).Serial, 0u) << "a recycled slot kept its predecessor's serial";
|
||||
EXPECT_EQ(ProgramRecordOf(5).Desc.StageMask, 0x1u);
|
||||
@@ -305,23 +308,23 @@ TEST(ProgramEmit, ACreateWithNoArtefactsAnOversizedBlockOrACorruptSlotIsRefusedN
|
||||
const MGPProgramDesc desc = ProgramDesc(MGPipeHandle{4, 1}, 0x3u, 0);
|
||||
ExpectRefusedNaming("create_shader_state {slot=4, gen=1}: the record declares no blobs and carries no "
|
||||
"artefacts",
|
||||
[&desc, &spirv]() { MGPipeApplyCreateShaderState(desc, nullptr, &spirv); });
|
||||
[&desc, &spirv]() { MGPipeApplyCreateShaderState(desc, nullptr, &spirv, nullptr); });
|
||||
ExpectRefusedNaming("create_shader_state {slot=4, gen=1}: the record declares no blobs and carries no "
|
||||
"artefacts",
|
||||
[&desc, &link]() { MGPipeApplyCreateShaderState(desc, &link, nullptr); });
|
||||
[&desc, &link]() { MGPipeApplyCreateShaderState(desc, &link, nullptr, nullptr); });
|
||||
EXPECT_TRUE(MGPipeApplier().ShaderCsos.empty());
|
||||
|
||||
const MGPProgramDesc huge = ProgramDesc(MGPipeHandle{4, 1}, 0x3u, kMGPipeMaxGlobalConstantsBytes + 1);
|
||||
ExpectRefusedNaming("create_shader_state {slot=4, gen=1}: the default uniform block is larger than any "
|
||||
"program may declare",
|
||||
[&huge, &link, &spirv]() { MGPipeApplyCreateShaderState(huge, &link, &spirv); });
|
||||
[&huge, &link, &spirv]() { MGPipeApplyCreateShaderState(huge, &link, &spirv, nullptr); });
|
||||
EXPECT_TRUE(MGPipeApplier().ShaderCsos.empty()) << "the table was grown by a refused record";
|
||||
|
||||
const MGPProgramDesc pastTheBound = ProgramDesc(MGPipeHandle{kMGPipeMaxShaderCsoSlots, 1}, 0x3u, 0);
|
||||
ExpectRefusedNaming("create_shader_state {slot=1048576, gen=1}: the slot is outside the record table's "
|
||||
"bound",
|
||||
[&pastTheBound, &link, &spirv]() {
|
||||
MGPipeApplyCreateShaderState(pastTheBound, &link, &spirv);
|
||||
MGPipeApplyCreateShaderState(pastTheBound, &link, &spirv, nullptr);
|
||||
});
|
||||
EXPECT_TRUE(MGPipeApplier().ShaderCsos.empty());
|
||||
EXPECT_TRUE(MGPipeApplier().CompositeShaderCsos.empty());
|
||||
@@ -340,8 +343,8 @@ TEST(ProgramEmit, TheThreeBindingsFollowTheirOwnHandleAndADeadOneLeavesThePrevio
|
||||
const SpirvArtifacts spirv;
|
||||
const MGPipeHandle draw{2, 1};
|
||||
const MGPipeHandle dispatch{3, 1};
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(draw, 0x3u, 0), &link, &spirv);
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(dispatch, 0x20u, 0), &link, &spirv);
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(draw, 0x3u, 0), &link, &spirv, nullptr);
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(dispatch, 0x20u, 0), &link, &spirv, nullptr);
|
||||
|
||||
const Uint64 serialBefore = MGPipeApplier().ProgramBindingSerial;
|
||||
MGPipeApplyBindShaderState(ProgramHandle(draw));
|
||||
@@ -383,7 +386,7 @@ TEST(ProgramEmit, ADeleteDropsTheRecordAndClearsEveryBindingThatNamedIt) {
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
const MGPipeHandle cso{6, 4};
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x3u, 0), &link, &spirv);
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x3u, 0), &link, &spirv, nullptr);
|
||||
MGPipeApplyBindShaderState(ProgramHandle(cso));
|
||||
MGPipeApplySetDrawProgram(ProgramHandle(cso));
|
||||
MGPipeApplySetDispatchProgram(ProgramHandle(cso));
|
||||
@@ -420,7 +423,7 @@ TEST(ProgramEmit, TheDefaultUniformBlockLandsOnTheProgramsRecordAndTheSentinelIs
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
const MGPipeHandle cso{7, 1};
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x3u, 8), &link, &spirv);
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x3u, 8), &link, &spirv, nullptr);
|
||||
|
||||
Uint8 block[8] = {1, 2, 3, 4, 5, 6, 7, 8};
|
||||
MGPipeApplySetGlobalConstants(GlobalConstants(cso, 11), block);
|
||||
@@ -473,7 +476,7 @@ TEST(ProgramEmit, TheProgramRecordSurvivesAMakeCurrentWhileTheThreeBindingsDoNot
|
||||
const SpirvArtifacts spirv;
|
||||
const MGPipeHandle cso{8, 1};
|
||||
const Uint8 block[4] = {9, 9, 9, 9};
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x3u, 4), &link, &spirv);
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x3u, 4), &link, &spirv, nullptr);
|
||||
MGPipeApplySetGlobalConstants(GlobalConstants(cso, 21), block);
|
||||
MGPipeApplyBindShaderState(ProgramHandle(cso));
|
||||
MGPipeApplySetDrawProgram(ProgramHandle(cso));
|
||||
@@ -509,7 +512,11 @@ TEST(ProgramEmit, TheProgramRecordSurvivesAMakeCurrentWhileTheThreeBindingsDoNot
|
||||
X(ProgramEmit, TheDrawAndDispatchProgramsAreTwoIndependentSlots) \
|
||||
X(ProgramEmit, AnUnchangedProgramEmitsNothingAtAll) \
|
||||
X(ProgramEmit, AReIssuedCreateReSendsTheDefaultUniformBlock) \
|
||||
X(ProgramEmit, ADeadProgramsRecordLatchIsRetiredAtItsDeath)
|
||||
X(ProgramEmit, ADeadProgramsRecordLatchIsRetiredAtItsDeath) \
|
||||
X(ProgramEmit, AUniformBlockBindingAfterTheLinkTravelsOnItsOwnRecord) \
|
||||
X(ProgramEmit, TheThreeBindingTailsAreWholeSetsAndAReIssuedCreateDropsThem) \
|
||||
X(ProgramEmit, AFailedRelinkReIssuesWithLinkStatusZeroAndNeverAnObjectDeath) \
|
||||
X(ProgramEmit, TheFramedArchiveRoundTripsWithTheStageOfEveryModule)
|
||||
|
||||
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
||||
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
||||
@@ -802,6 +809,199 @@ void main() { gl_Position = vec4(0.0); EmitVertex(); }
|
||||
EXPECT_FALSE(Emitter().RecordIsPublished(handle))
|
||||
<< "a dead program still reads as published in the program emitter's memo";
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// P5e (pg): set_program_bindings, and the archive as the server owns it
|
||||
// =====================================================================================
|
||||
|
||||
// THE CASE THE PACKAGE'S FIRST RED-ONCE IS BUILT ON. glUniformBlockBinding moves a block's
|
||||
// binding INSIDE LinkArtifacts after the link that produced the archive, so a server that
|
||||
// answered a draw from the archive alone would bind the uniform blocks the program was
|
||||
// LINKED with rather than the ones it is BOUND with. The record is what closes that, and
|
||||
// this is the case that says the client sends it.
|
||||
TEST(ProgramEmit, AUniformBlockBindingAfterTheLinkTravelsOnItsOwnRecord) {
|
||||
EmitterScope scope;
|
||||
const GLuint name = MakeVsFsProgram();
|
||||
const SharedPtr<ProgramObject>& program = Ctx().GetProgramObject(name);
|
||||
ASSERT_TRUE(program);
|
||||
Uint64 bytes = 0;
|
||||
const MGPipeHandle cso = Emitter().AcquireShaderCso(*program, bytes);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(cso));
|
||||
|
||||
// The create clears the latch, so the first bindings emission always goes out - which
|
||||
// is the ordering the applier depends on (a re-issued create wipes all three tails).
|
||||
const Uint64 before = Emitter().ProgramBindingsSetCount();
|
||||
Emitter().EmitProgramBindings(*program, cso);
|
||||
EXPECT_EQ(Emitter().ProgramBindingsSetCount(), before + 1)
|
||||
<< "the bindings that follow a create were suppressed, so the server would draw "
|
||||
"this program off its archive's link-time snapshot for ever";
|
||||
EXPECT_EQ(Emitter().LastProgramBindings().Cso, cso);
|
||||
EXPECT_EQ(Emitter().ProgramBindingsRefusalCount(), 0u);
|
||||
|
||||
// UNCHANGED STATE EMITS NOTHING. The latch is (Cso, backendStateVersion,
|
||||
// blockBindingVersion) and neither setter moves a counter on an unchanged value, which
|
||||
// is what keeps a program-pipeline composite's per-draw uniform mirror free.
|
||||
const Uint64 latched = Emitter().ProgramBindingsSetCount();
|
||||
Emitter().EmitProgramBindings(*program, cso);
|
||||
EXPECT_EQ(Emitter().ProgramBindingsSetCount(), latched)
|
||||
<< "an unchanged binding set was re-sent";
|
||||
|
||||
// A REAL glUniformBlockBinding, after the link, on a program that declares no block:
|
||||
// the frontend's setter bails out on an out-of-range index, so this drives the other
|
||||
// half - glUniform1i on the sampler-shaped uniform the test shader declares - which
|
||||
// moves m_backendStateVersion and must unlatch the record.
|
||||
const Int location = program->GetUniformLocation("u_value");
|
||||
if (location >= 0) {
|
||||
program->SetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location), 3);
|
||||
Emitter().EmitProgramBindings(*program, cso);
|
||||
EXPECT_EQ(Emitter().ProgramBindingsSetCount(), latched + 1)
|
||||
<< "a post-link unit assignment did not move the latch, so it never travels";
|
||||
const auto& hdr = Emitter().LastProgramBindings();
|
||||
EXPECT_EQ(hdr.Cso, cso);
|
||||
EXPECT_GE(hdr.SamplerUnitCount, 1u)
|
||||
<< "the assigned unit is not in the record's second tail";
|
||||
}
|
||||
}
|
||||
|
||||
// The applier's half: whole-set replacement in all three tails, and a re-issued create
|
||||
// drops them because they are INDICES INTO the archive it has just replaced.
|
||||
TEST(ProgramEmit, TheThreeBindingTailsAreWholeSetsAndAReIssuedCreateDropsThem) {
|
||||
ApplierGuard guard;
|
||||
const LinkArtifacts link;
|
||||
const SpirvArtifacts spirv;
|
||||
const MGPipeHandle cso{9, 1};
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x3u, 0), &link, &spirv, nullptr);
|
||||
ASSERT_TRUE(ProgramRecordOf(9).Live) << "the create did not land, so nothing below is about "
|
||||
"set_program_bindings at all";
|
||||
const Uint64 bindingsSerialAfterCreate = ProgramRecordOf(9).BindingsSerial;
|
||||
const Uint64 refusalsBefore = MGPipeApplier().RefusedObjectCalls;
|
||||
|
||||
const Int32 blocks[3] = {0, 5, 2};
|
||||
const MGPProgramSamplerUnit units[2] = {{4u, 1}, {9u, 7}};
|
||||
MGPProgramStorageOverride overrides[1]{};
|
||||
overrides[0].Binding = 6;
|
||||
const char* const names[1] = {"Blocky"};
|
||||
MGPProgramBindings hdr{};
|
||||
hdr.Cso = cso;
|
||||
hdr.Signature = 0xABCDEFull;
|
||||
hdr.BlockBindingCount = 3;
|
||||
hdr.SamplerUnitCount = 2;
|
||||
hdr.StorageOverrideCount = 1;
|
||||
MGPipeApplySetProgramBindings(hdr, blocks, units, overrides, names);
|
||||
ASSERT_EQ(MGPipeApplier().RefusedObjectCalls, refusalsBefore)
|
||||
<< "the record the bindings name was not resolved";
|
||||
|
||||
ASSERT_EQ(ProgramRecordOf(9).BlockBindings.size(), 3u);
|
||||
EXPECT_EQ(ProgramRecordOf(9).BlockBindings[1], 5);
|
||||
ASSERT_EQ(ProgramRecordOf(9).SamplerUnits.size(), 2u);
|
||||
EXPECT_EQ(ProgramRecordOf(9).SamplerUnits[1].Location, 9u);
|
||||
EXPECT_EQ(ProgramRecordOf(9).SamplerUnits[1].Unit, 7);
|
||||
ASSERT_EQ(ProgramRecordOf(9).StorageOverrides.size(), 1u);
|
||||
// COPIED, NOT POINTED AT (rule C): the wire's name is a host span into SEG_STAGE, which
|
||||
// retires with the record that named it, and the rebuild that reads this may be a frame
|
||||
// later.
|
||||
EXPECT_EQ(ProgramRecordOf(9).StorageOverrides[0].Name, String("Blocky"));
|
||||
EXPECT_EQ(ProgramRecordOf(9).StorageOverrides[0].Binding, 6);
|
||||
EXPECT_EQ(ProgramRecordOf(9).Signature, 0xABCDEFull);
|
||||
EXPECT_GT(ProgramRecordOf(9).BindingsSerial, bindingsSerialAfterCreate)
|
||||
<< "BindingsSerial did not advance, so both texture memos and the twin's clean "
|
||||
"condition would match state they have never seen";
|
||||
|
||||
// A SHORTER SET REPLACES, never merges: a merge would leave a binding the application
|
||||
// has since reset to its declared default standing for ever.
|
||||
const Int32 oneBlock[1] = {4};
|
||||
MGPProgramBindings second = hdr;
|
||||
second.BlockBindingCount = 1;
|
||||
second.SamplerUnitCount = 0;
|
||||
second.StorageOverrideCount = 0;
|
||||
MGPipeApplySetProgramBindings(second, oneBlock, nullptr, nullptr, nullptr);
|
||||
ASSERT_EQ(ProgramRecordOf(9).BlockBindings.size(), 1u);
|
||||
EXPECT_TRUE(ProgramRecordOf(9).SamplerUnits.empty());
|
||||
EXPECT_TRUE(ProgramRecordOf(9).StorageOverrides.empty());
|
||||
|
||||
// THE RE-ISSUE DROPS ALL THREE, for the reason it drops the default uniform block.
|
||||
MGPipeApplyCreateShaderState(ProgramDesc(cso, 0x7u, 0), &link, &spirv, nullptr);
|
||||
EXPECT_TRUE(ProgramRecordOf(9).BlockBindings.empty())
|
||||
<< "a tail indexing an archive the relink replaced survived it";
|
||||
EXPECT_EQ(ProgramRecordOf(9).Signature, 0u);
|
||||
}
|
||||
|
||||
// ID-88 / ruling 9, VERIFIED IN THE FRONTEND rather than assumed: Link()'s prologue bumps
|
||||
// the link version and assigns `m_artifacts = {}` - "the complete not-linked state" in its
|
||||
// own words - BEFORE the link body runs, so a failed relink reports UNLINKED. The emitter
|
||||
// therefore re-issues on the same handle with LinkStatus = 0, and nothing needs an
|
||||
// object_death: the program object is alive and its twin must survive to be rebuilt.
|
||||
TEST(ProgramEmit, AFailedRelinkReIssuesWithLinkStatusZeroAndNeverAnObjectDeath) {
|
||||
EmitterScope scope;
|
||||
const GLuint name = MakeVsFsProgram();
|
||||
const SharedPtr<ProgramObject>& program = Ctx().GetProgramObject(name);
|
||||
ASSERT_TRUE(program);
|
||||
Uint64 bytes = 0;
|
||||
const MGPipeHandle cso = Emitter().AcquireShaderCso(*program, bytes);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(cso));
|
||||
EXPECT_TRUE(program->GetLinkStatus());
|
||||
EXPECT_EQ(Emitter().LastProgramDesc().LinkStatus, 1u);
|
||||
const Uint64 creates = Emitter().CreateCount();
|
||||
|
||||
// A relink with a shader that cannot compile. Detaching everything is the cleanest
|
||||
// failure this suite can produce without a compiler diagnostic of its own: Link() with
|
||||
// no shaders attached is the "No shader objects are attached" arm, which leaves
|
||||
// linkStatus false.
|
||||
for (const auto& attached : program->GetAttachedShaders()) {
|
||||
GL::DetachShader(name, attached->GetExternalIndex());
|
||||
}
|
||||
GL::LinkProgram(name);
|
||||
ASSERT_FALSE(program->GetLinkStatus()) << "the frontend did NOT withdraw LINK_STATUS on a "
|
||||
"failed relink; ID-88's 'iff' is then false and "
|
||||
"the emitter's rule has to be re-decided";
|
||||
|
||||
const MGPipeHandle again = Emitter().AcquireShaderCso(*program, bytes);
|
||||
EXPECT_EQ(again, cso) << "a relink minted a second handle; the twin table would be asked "
|
||||
"for two twins of one GL object";
|
||||
EXPECT_EQ(Emitter().CreateCount(), creates + 1)
|
||||
<< "the failed relink did not re-issue, so the server keeps drawing an executable "
|
||||
"the frontend reports unlinked";
|
||||
EXPECT_EQ(Emitter().LastProgramDesc().LinkStatus, 0u)
|
||||
<< "the re-issue claims the program linked";
|
||||
EXPECT_TRUE(MGPipeSlots().IsLive(MGPipeKind::ShaderCso, cso))
|
||||
<< "a failed relink must never be modelled as an object_death";
|
||||
}
|
||||
|
||||
// The frame, both ways round. SpirvArtifacts carries no stage list and MGPProgramDesc::
|
||||
// StageMask cannot stand in for one - it is a bit SET, and GL lets two shader objects of
|
||||
// one stage be attached to a program, so a list rebuilt from the mask can be SHORTER than
|
||||
// generatedSpirv while the backend pairs the two by one running index.
|
||||
TEST(ProgramEmit, TheFramedArchiveRoundTripsWithTheStageOfEveryModule) {
|
||||
MG_State::GLState::LinkArtifacts link;
|
||||
MG_State::GLState::SpirvArtifacts spirv;
|
||||
spirv.spirvStatus = true;
|
||||
// TWO MODULES OF THE SAME STAGE, which is the shape a mask cannot describe.
|
||||
spirv.generatedSpirv.resize(2);
|
||||
spirv.generatedSpirv[0].assign(3, 0x07230203u);
|
||||
spirv.generatedSpirv[1].assign(5, 0x07230204u);
|
||||
const Vector<Uint32> stages{static_cast<Uint32>(ShaderStage::Vertex),
|
||||
static_cast<Uint32>(ShaderStage::Vertex)};
|
||||
|
||||
Vector<Uint8> framed;
|
||||
MG_State::GLState::EncodeProgramArchive(link, spirv, stages, framed);
|
||||
ASSERT_FALSE(framed.empty());
|
||||
|
||||
MG_State::GLState::ProgramArchive back;
|
||||
ASSERT_TRUE(MG_State::GLState::DecodeProgramArchive(framed.data(), framed.size(), back));
|
||||
EXPECT_EQ(back.LinkedStages, stages);
|
||||
ASSERT_EQ(back.Spirv.generatedSpirv.size(), 2u);
|
||||
EXPECT_EQ(back.Spirv.generatedSpirv[1].size(), 5u);
|
||||
EXPECT_EQ(back.Link.program, nullptr)
|
||||
<< "the one member the codec skips is the live glslang TProgram, and decode must "
|
||||
"leave it null";
|
||||
|
||||
// A TRUNCATED FRAME IS REFUSED, not guessed, and both outputs come back defined.
|
||||
MG_State::GLState::ProgramArchive refused;
|
||||
EXPECT_FALSE(
|
||||
MG_State::GLState::DecodeProgramArchive(framed.data(), framed.size() - 1, refused));
|
||||
EXPECT_TRUE(refused.LinkedStages.empty());
|
||||
EXPECT_TRUE(refused.Spirv.generatedSpirv.empty());
|
||||
}
|
||||
} // namespace
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
|
||||
@@ -1522,7 +1522,7 @@ namespace {
|
||||
MGPipeApplySetSamplerViews(viewSet, viewTail);
|
||||
MGPipeApplyBindSamplerStates(stateSet, stateTail);
|
||||
MGPipeApplySetShaderImages(imageSet, imageTail);
|
||||
MGPipeApplyCreateShaderState(program, &link, &spirv);
|
||||
MGPipeApplyCreateShaderState(program, &link, &spirv, nullptr);
|
||||
MGPipeApplyBindShaderState(csoHandle);
|
||||
MGPipeApplySetDrawProgram(csoHandle);
|
||||
MGPipeApplySetDispatchProgram(csoHandle);
|
||||
@@ -1583,7 +1583,7 @@ namespace {
|
||||
MGPipeApplySetSamplerViews(viewSet, viewTail);
|
||||
MGPipeApplyBindSamplerStates(stateSet, stateTail);
|
||||
MGPipeApplySetShaderImages(imageSet, imageTail);
|
||||
MGPipeApplyCreateShaderState(program, &link, &spirv);
|
||||
MGPipeApplyCreateShaderState(program, &link, &spirv, nullptr);
|
||||
MGPipeApplyBindShaderState(csoHandle);
|
||||
MGPipeApplySetDrawProgram(csoHandle);
|
||||
MGPipeApplySetDispatchProgram(csoHandle);
|
||||
|
||||
@@ -1561,8 +1561,14 @@ TEST_F(PipeWireCodecTest, CreateShaderStateCrossesAsOneArchiveAndSixUndeclaredRu
|
||||
}
|
||||
spirv.globalUboScratch.assign(32, 0xAB);
|
||||
|
||||
// P5e (pg): THE FRAMED archive is what crosses now - the codec's own stream with the stage
|
||||
// of each module in front of it. SpirvArtifacts does not carry the stages and StageMask
|
||||
// cannot stand in for them (two shader objects may share a stage, so a list rebuilt from
|
||||
// the mask can be shorter than generatedSpirv), and the server pairs the two by one running
|
||||
// index. See ProgramArtifactsCodec.h.
|
||||
const Vector<Uint32> stages{0, 1, 2, 3, 4, 5};
|
||||
Vector<Uint8> archive;
|
||||
MG_State::GLState::EncodeProgramArtifacts(link, spirv, archive);
|
||||
MG_State::GLState::EncodeProgramArchive(link, spirv, stages, archive);
|
||||
ASSERT_FALSE(archive.empty());
|
||||
|
||||
MGPProgramDesc desc{};
|
||||
@@ -1586,16 +1592,16 @@ TEST_F(PipeWireCodecTest, CreateShaderStateCrossesAsOneArchiveAndSixUndeclaredRu
|
||||
EXPECT_TRUE(applied);
|
||||
|
||||
// And the archive really did carry the six modules: decode it the way the arm does.
|
||||
MG_State::GLState::LinkArtifacts back;
|
||||
MG_State::GLState::SpirvArtifacts backSpirv;
|
||||
ASSERT_TRUE(MG_State::GLState::DecodeProgramArtifacts(archive.data(), archive.size(), back,
|
||||
backSpirv));
|
||||
ASSERT_EQ(backSpirv.generatedSpirv.size(), 6u);
|
||||
MG_State::GLState::ProgramArchive back;
|
||||
ASSERT_TRUE(MG_State::GLState::DecodeProgramArchive(archive.data(), archive.size(), back));
|
||||
ASSERT_EQ(back.Spirv.generatedSpirv.size(), 6u);
|
||||
for (std::size_t stage = 0; stage < 6; ++stage) {
|
||||
EXPECT_EQ(backSpirv.generatedSpirv[stage].size(), 4 + stage);
|
||||
EXPECT_EQ(backSpirv.generatedSpirv[stage][0], 0x07230203u + stage);
|
||||
EXPECT_EQ(back.Spirv.generatedSpirv[stage].size(), 4 + stage);
|
||||
EXPECT_EQ(back.Spirv.generatedSpirv[stage][0], 0x07230203u + stage);
|
||||
}
|
||||
EXPECT_TRUE(backSpirv.spirvStatus);
|
||||
EXPECT_TRUE(back.Spirv.spirvStatus);
|
||||
// The frame's own half: one stage word per module, at the same index.
|
||||
EXPECT_EQ(back.LinkedStages, stages);
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
|
||||
@@ -1148,6 +1148,9 @@ TEST(PipeRouting, TheInstalledClientArmIsWireAndNotMonolithAndEveryRoutedRowMove
|
||||
C1F_ESCAPE(ResourceFlushRange);
|
||||
C1F_ESCAPE(MapPersistent);
|
||||
C1F_ESCAPE(CreateShaderState);
|
||||
// P5e (pg): set_program_bindings is the fifth escape - three tails in three index spaces
|
||||
// plus a parallel name array, which no generated row can express (MG_Pipe/PipeRoute.h).
|
||||
C1F_ESCAPE(SetProgramBindings);
|
||||
#undef C1F_ESCAPE
|
||||
const SizeT movedContext = CountDifferingCells(gMGPipeContext, MGPipeMonolithContext());
|
||||
EXPECT_EQ(movedScreen + movedContext, 35u)
|
||||
@@ -1158,8 +1161,8 @@ TEST(PipeRouting, TheInstalledClientArmIsWireAndNotMonolithAndEveryRoutedRowMove
|
||||
"thread under split) or an unrouted row was overwritten";
|
||||
|
||||
const SizeT movedEscapes = CountDifferingCells(gMGPipeRouteEscapes, MGPipeMonolithEscapes());
|
||||
EXPECT_EQ(movedEscapes, 4u)
|
||||
<< "the four escape routes must move off the monolith escapes too";
|
||||
EXPECT_EQ(movedEscapes, 5u)
|
||||
<< "the five escape routes must move off the monolith escapes too";
|
||||
|
||||
// Restore the monolith arm for the sibling cases that assert it (and for a clean binary).
|
||||
MGPipeInstallMonolithTables();
|
||||
|
||||
Reference in New Issue
Block a user