From 39d0677616916f959deb0e08c26c79e7e40574af Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 18 Sep 2026 07:50:27 -0400 Subject: [PATCH 1/3] [MG_Pipe, MG_Impl, MG_Remote, MG_State, MG_Test] (Disaggregated): P5e pg - the archive becomes server-owned and the bindings get a record MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MGPipeShaderCsoRecord gains Archive, a SharedPtr the create ADOPTS under a transport. Before this the applier stored the descriptor and the two companion pointers named the frontend's own LinkArtifacts/SpirvArtifacts, which ProgramObject::Link() replaces in place - so every reflection read on the apply thread was torn or stale by construction the moment the client runs ahead (gap G-A). - MGPipeApplyCreateShaderState takes the archive as a fourth argument, undefaulted for PipeRoute.h's stated reason: a caller that has one and forgets it would compile, and the failure would be silent. Null on the monolith arm, which keeps its frontend arms token for token (ruling 1 / ID-81). - ProgramArtifactsCodec gains ProgramArchive and the FRAME. SpirvArtifacts carries no stage list and MGPProgramDesc::StageMask cannot stand in for one: it is a bit set, and GL lets two shader objects share a stage, so a list rebuilt from it can be shorter than generatedSpirv - which the backend pairs with it by one running index. - Codec completeness, as CONTRACT-P5E §5.5 asks: the skipped member is named (LinkArtifacts::program, the live glslang TProgram) and proven unread by either backend twin; the libc++ caveat at the size echo is read as what it is - a cross-toolchain check, exact for an in-process split - and both verdicts are written where they were asked for. - set_program_bindings (opcode 80) lands end to end: EmitProgramBindings on the client, the FIFTH ROUTE ESCAPE (three tails in three index spaces plus a parallel name array - no generated row can express it, so op 80's gMGPipeContext slot stays null as PipeCatalogueTest pins), the wire emitter, the decoder arm that resolves and NUL-checks every name span, and the applier body that copies the names into the record (rule C) and advances BindingsSerial. - The record is emitted AFTER create_shader_state, not before as the draft note said: a re-issued create clears all three tails because they index into the archive it just replaced, so bindings published ahead of it are wiped by the create that follows. - MGPProgramDesc::LinkStatus is filled, and ID-88 is VERIFIED rather than assumed: ProgramObject::Link()'s prologue bumps m_linkVersion and assigns m_artifacts = {} - "the complete not-linked state" - before the body runs, and every ProgramLinkTask failure arm leaves linkStatus false. A failed relink therefore reports unlinked, the re-issue carries LinkStatus = 0, and nothing needs an object_death. - ID-87's measurement gets its instrument: the archive's byte count is added to PipeStats' CsoBlobBytes where the bytes actually exist, one sample per link. --- MobileGL/MG_Impl/Pipe/ProgramEmit.h | 243 +++++++++++++++++- MobileGL/MG_Pipe/PipeApply.cpp | 98 ++++++- MobileGL/MG_Pipe/PipeApply.h | 42 ++- MobileGL/MG_Pipe/PipeRoute.cpp | 30 ++- MobileGL/MG_Pipe/PipeRoute.h | 39 ++- MobileGL/MG_Remote/Client/WireTables.cpp | 91 ++++++- MobileGL/MG_Remote/Wire/PipeWireCodec.cpp | 77 ++++-- .../ProgramState/ProgramArtifactsCodec.cpp | 59 +++++ .../ProgramState/ProgramArtifactsCodec.h | 59 +++++ .../MG_Test/Pipe/CompositeResolverTest.cpp | 16 +- MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp | 9 +- MobileGL/MG_Test/Pipe/ProgramEmitTest.cpp | 24 +- MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp | 4 +- MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp | 24 +- MobileGL/MG_Test/Wire/RemoteClientTest.cpp | 7 +- 15 files changed, 745 insertions(+), 77 deletions(-) diff --git a/MobileGL/MG_Impl/Pipe/ProgramEmit.h b/MobileGL/MG_Impl/Pipe/ProgramEmit.h index ce3d51e7..02e98b55 100644 --- a/MobileGL/MG_Impl/Pipe/ProgramEmit.h +++ b/MobileGL/MG_Impl/Pipe/ProgramEmit.h @@ -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{}(blockName); + entry ^= (static_cast(static_cast(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(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(std::max(blockCount, 0))); + for (Int i = 0; i < blockCount; ++i) { + m_blockBindings.push_back(static_cast(program.GetUniformBlockBinding(static_cast(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(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(m_blockBindings.size()); + m_lastBindings.SamplerUnitCount = static_cast(m_samplerUnits.size()); + m_lastBindings.StorageOverrideCount = static_cast(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(program.GetUBOSize()); m_lastDesc.ReservedNumSamplesOffset = static_cast(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(&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(stage)); + } + + MGPipeRouteCreateShaderState(m_lastDesc, &link, &spirv, m_linkedStageWords.data(), + static_cast(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 m_blockBindings; + Vector m_samplerUnits; + Vector m_storageOverrides; + Vector m_storageOverrideNames; + Vector 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 m_linkedStageWords; Vector m_latch; Vector 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() { diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index 52f91983..578a14b8 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -2851,21 +2851,69 @@ namespace MobileGL::MG_Pipe { std::abort(); } + // 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; } // --------------------------------------------------------------------------------- @@ -2932,7 +2980,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 archive) { MOBILEGL_ASSERT(desc.Cso.Slot >= kMGPipeFirstAllocatableSlot, "create_shader_state named the reserved slot 0"); if (desc.Cso.Slot < kMGPipeFirstAllocatableSlot) return; @@ -3005,6 +3054,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) { diff --git a/MobileGL/MG_Pipe/PipeApply.h b/MobileGL/MG_Pipe/PipeApply.h index 232ef337..38f068b3 100644 --- a/MobileGL/MG_Pipe/PipeApply.h +++ b/MobileGL/MG_Pipe/PipeApply.h @@ -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 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 @@ -1324,9 +1353,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 archive); void MGPipeApplyBindShaderState(const MGPHandleOnly& handle); void MGPipeApplyDeleteShaderState(const MGPHandleOnly& handle); void MGPipeApplySetDrawProgram(const MGPHandleOnly& handle); diff --git a/MobileGL/MG_Pipe/PipeRoute.cpp b/MobileGL/MG_Pipe/PipeRoute.cpp index a9b4e359..e8f38c0a 100644 --- a/MobileGL/MG_Pipe/PipeRoute.cpp +++ b/MobileGL/MG_Pipe/PipeRoute.cpp @@ -310,8 +310,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 @@ -357,6 +382,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. diff --git a/MobileGL/MG_Pipe/PipeRoute.h b/MobileGL/MG_Pipe/PipeRoute.h index 7e794bf9..6344d5fc 100644 --- a/MobileGL/MG_Pipe/PipeRoute.h +++ b/MobileGL/MG_Pipe/PipeRoute.h @@ -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{}; @@ -384,8 +404,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); @@ -403,6 +424,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 diff --git a/MobileGL/MG_Remote/Client/WireTables.cpp b/MobileGL/MG_Remote/Client/WireTables.cpp index 947fb771..05a70ea2 100644 --- a/MobileGL/MG_Remote/Client/WireTables.cpp +++ b/MobileGL/MG_Remote/Client/WireTables.cpp @@ -44,6 +44,9 @@ #include #include #include +// 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 #include #include @@ -487,9 +490,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"); @@ -501,11 +506,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 archive; - MG_State::GLState::EncodeProgramArtifacts(*link, *spirv, archive); + Vector 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(archive.size())); + } if (archive.empty()) { MGLOG_F("MGPipe: Fatal{ArchiveEmpty, \"create_shader_state\"} - the program's " "artefacts serialised to nothing"); @@ -519,6 +536,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 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(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(record.BlockBindingCount) * sizeof(Int32)}, + {samplerUnits, static_cast(record.SamplerUnitCount) * + sizeof(MG_Pipe::MGPProgramSamplerUnit)}, + {overrides.empty() ? nullptr : overrides.data(), + static_cast(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 // ===================================================================================== @@ -664,6 +746,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); } diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp index 342b3e6e..d8c69a99 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp @@ -1756,17 +1756,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(archive), - static_cast(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(); + if (!MG_State::GLState::DecodeProgramArchive(static_cast(archiveBytes), + static_cast(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; } @@ -1861,12 +1872,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: @@ -1874,17 +1884,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(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 overrideNames; + const MGPProgramStorageOverride* overrides = nullptr; if (layout.TailCount == 3 && layout.TailBytes[2] != 0 && m_segments != nullptr) { - const auto* overrides = tailAt(2); + overrides = reinterpret_cast(tailAt(2)); const Uint64 count = layout.TailBytes[2] / sizeof(MGPProgramStorageOverride); + overrideNames.reserve(static_cast(count)); for (Uint64 i = 0; i < count; ++i) { MGPProgramStorageOverride entry{}; - std::memcpy(&entry, overrides + i * sizeof(MGPProgramStorageOverride), + std::memcpy(&entry, reinterpret_cast(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( + 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(tailAt(0)) : nullptr, + layout.TailBytes[1] != 0 ? reinterpret_cast(tailAt(1)) + : nullptr, + overrides, overrideNames.empty() ? nullptr : overrideNames.data()); + return true; } case MGPWireOp::SetGlobalConstants: { diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp index 945cf6cb..8d5c31e0 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp @@ -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& linkedStages, Vector& out) { + PutRaw(out, static_cast(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(stageCount) > kProgramArchiveMaxStages) return false; + const SizeT framed = sizeof(Uint32) + static_cast(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 diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.h b/MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.h index 0517d16c..d02980ba 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.h @@ -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 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& linkedStages, Vector& 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 diff --git a/MobileGL/MG_Test/Pipe/CompositeResolverTest.cpp b/MobileGL/MG_Test/Pipe/CompositeResolverTest.cpp index b93c9db0..3fa4729e 100644 --- a/MobileGL/MG_Test/Pipe/CompositeResolverTest.cpp +++ b/MobileGL/MG_Test/Pipe/CompositeResolverTest.cpp @@ -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(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 composite = MakeShared(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); diff --git a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp index 359dd707..dbff069f 100644 --- a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp +++ b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp @@ -255,9 +255,14 @@ 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, 38u) + EXPECT_EQ(installed + escapesInstalled, 39u) << "the two tables plus the escapes must be exactly PipeApply.h's entry points"; // And the rows that MUST still be null, named rather than counted: these are calls with no diff --git a/MobileGL/MG_Test/Pipe/ProgramEmitTest.cpp b/MobileGL/MG_Test/Pipe/ProgramEmitTest.cpp index 4f63b495..b9b6d774 100644 --- a/MobileGL/MG_Test/Pipe/ProgramEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/ProgramEmitTest.cpp @@ -258,7 +258,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 +274,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 +285,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 +305,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 +340,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 +383,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 +420,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 +473,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)); diff --git a/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp b/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp index dbd576ee..af60083f 100644 --- a/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp @@ -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); diff --git a/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp b/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp index 47ca3189..8f6f8340 100644 --- a/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp +++ b/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp @@ -1557,8 +1557,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 stages{0, 1, 2, 3, 4, 5}; Vector archive; - MG_State::GLState::EncodeProgramArtifacts(link, spirv, archive); + MG_State::GLState::EncodeProgramArchive(link, spirv, stages, archive); ASSERT_FALSE(archive.empty()); MGPProgramDesc desc{}; @@ -1582,16 +1588,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); } // ===================================================================================== diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index fc6699ea..3d42e2df 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -1144,6 +1144,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, 34u) @@ -1154,8 +1157,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(); From c5491ca6750e90c557a92005903796d0e8b5ed7b Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 18 Sep 2026 08:14:05 -0400 Subject: [PATCH 2/3] [MG_Backend] (Disaggregated): P5e pg - the program twin is keyed by the ShaderCso handle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BackendProgramObjectImpl::SyncToBackendByHandle is real. It and the frontend overload feed ONE body (SyncToBackendFromSource) through ProgramArchiveSource, a view whose accessors are ProgramObject's names one for one - so the body is written once and the PULL build compiles the same text against a ProgramObject. In a pull build ProgramBuildSource IS ProgramObject, the function keeps its own name and its null guard, and G1's byte identity survives a rename and nothing else. - The view answers the three POST-LINK MUTABLE reflection fields from the record's set_program_bindings tails and everything else from the record's archive. The tails are a whole-set replacement, so a location the sampler tail does not name has NO unit - not whatever the link-time snapshot said - and "has a bindings record been applied at all" is BindingsSerial != 0 rather than a non-empty tail. - The nine-clause clean condition keeps its clause count and moves its inputs (D-H5): record.Serial vs the m_syncedShaderCsoSerial stamp P4a landed and nothing has read until now, BindingsSerial vs a new m_syncedBindingsSerial (a glUniform1i on an IMAGE uniform is baked into the ESSL, so the record that carries it must force a rebuild), record.Signature, Desc.LinkStatus and Desc.SpirvStatus. - The signature has ONE authority per arm: the handle arm stamps and compares the CLIENT's number verbatim, so the two commutative hashes never have to stay bit-identical for a program with an override to stop rebuilding every draw. - DirectGLES: SyncCurrentProgramByHandle, the stash re-keyed on the handle, PrepareForDraw/PrepareForCompute reading MGPipeApplier().DrawProgram/DispatchProgram, BindCurrentProgramWithResources and GetCurrentBackendProgram on the handle arm, the block binding and the sampler unit from the record's tails, and the sampler-pass memo on BindingsSerial instead of the frontend's backend-state version. - MapUBO() has no run-ahead answer and now says so: every decline of ResolveGlobalConstantsRecord under a transport is Fatal{UnmigratedVerb, "set_global_constants"} rather than a torn read of the client's live globalUboScratch. - The fragColor broadcast count takes a POLICY, because a dispatch is a third case: it has no draw framebuffer and no fragment stage, PrepareForCompute runs no SyncCurrentFBO, so it may neither fall back to a client row nor abort when the records are untrusted. - Three of the family's five named frontend-keyed scope sites are now unreachable under a transport (ResolveGlobalConstantsRecord, SyncCurrentProgram, BindCurrentProgramWithResources). The other two are kept BY CONTRACT (§5.5): GetBackendProgramId is a GL-thread entry point and ShaderStorageBlockBinding is reached from the barriered set_storage_block_binding, which is pg's trailing item. --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 569 ++++++++++++++---- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 556 +++++++++++++---- MobileGL/MG_Backend/DirectGLES/Managers.h | 242 +++++++- 3 files changed, 1127 insertions(+), 240 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index ed19e4c3..6927303b 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -4096,6 +4096,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 @@ -4227,19 +4241,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 @@ -4277,7 +4285,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(handle.Slot), static_cast(handle.Gen), - static_cast(program->GetUBOSize())); + static_cast(uboSize)); return nullptr; } // P4a decline-site P7: S, and LOUD (ID-19) - this one is not a missing record, @@ -4294,7 +4302,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(program->GetUBOSize())) { + if (record->GlobalConstants.size() < static_cast(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 " @@ -4302,7 +4310,7 @@ namespace MobileGL::MG_Backend::DirectGLES { "%llu-byte default uniform block.", static_cast(handle.Slot), static_cast(handle.Gen), static_cast(record->GlobalConstants.size()), - static_cast(program->GetUBOSize())); + static_cast(uboSize)); std::abort(); #else MGLOG_E_ONCE("A program record does not describe the binding it names: the ShaderCso " @@ -4310,13 +4318,133 @@ namespace MobileGL::MG_Backend::DirectGLES { "%llu-byte default uniform block; running the frontend's own uniform block.", static_cast(handle.Slot), static_cast(handle.Gen), static_cast(record->GlobalConstants.size()), - static_cast(program->GetUBOSize())); + static_cast(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(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(enabledDrawBuffers, 1); + g_broadcastMemoValid = true; + } + g_fragColorBroadcastCount = g_broadcastMemoCount; + } + } void SyncCurrentProgram(const SharedPtr& currentProgram) { #ifdef TRACY_ENABLE @@ -4341,73 +4469,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(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(enabledDrawBuffers, 1); - g_broadcastMemoValid = true; - } - g_fragColorBroadcastCount = g_broadcastMemoCount; - } - } + ResolveFragColorBroadcastCount(BroadcastCountPolicy::FrontendFallback); BackendProgramObjectImpl* twin = nullptr; #if MOBILEGL_PIPE_PUSH @@ -4461,7 +4523,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 @@ -4508,7 +4570,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(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) { @@ -4755,7 +4892,7 @@ namespace MobileGL::MG_Backend::DirectGLES { static void BindCurrentProgramWithResources( const SharedPtr& currentProgram, - const TextureImpl::DrawTextureSyncKeys& keys); + const TextureImpl::DrawTextureSyncKeys& keys, MG_Pipe::MGPipeHandle programCso); static void BindCurrentTextures(const TextureImpl::DrawTextureSyncKeys& keys, const SharedPtr& currentProgram); @@ -4803,7 +4940,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); @@ -4822,7 +4971,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. @@ -5222,6 +5377,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 @@ -5316,27 +5480,63 @@ namespace MobileGL::MG_Backend::DirectGLES { // _FlwFrameUniforms block and the _flw_depthPyramid sampler. static void BindCurrentProgramWithResources( const SharedPtr& 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) { @@ -5346,7 +5546,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 @@ -5363,14 +5572,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(currentProgram->GetUBOSize()); + const SizeT uboSize = static_cast(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. @@ -5422,12 +5651,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(currentProgram->GetUBOSize())); + static_cast(globalUboSize)); } g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0); backendProgram.SetLastUploadedGlobalUboVersion(uboContentVersion); @@ -5459,8 +5688,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); auto& point = MGB_CTX->GetBufferBindingPoint(BufferTarget::Uniform, binding); auto& bufferObj = point.GetBoundObject(); auto range = point.GetRange(); @@ -5519,7 +5759,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(programRecord->BindingsSerial) : +#endif + currentProgram->GetBackendStateVersion(); Bool samplerPassClean = samplerPassMemo.valid && samplerPassMemo.contextId == keys.contextId && samplerPassMemo.unitBindingsEpoch == keys.unitBindingsEpoch && @@ -5542,8 +5793,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) @@ -5676,6 +5937,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; @@ -5684,8 +5965,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())) { @@ -5731,10 +6012,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()); @@ -6130,8 +6426,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; @@ -6145,7 +6466,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) { diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 9e7b021b..29cf53e9 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -15,6 +15,10 @@ #include #include #include +// 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 #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& 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)) { @@ -10840,6 +10849,222 @@ namespace MobileGL::MG_Backend::DirectGLES { TwinRegistry 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(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(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(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(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(base->second); + if (!IsValidUniformLocation(baseLocation)) return -1; + const Int index = Link->uniformIndexInTProgram[baseLocation]; + if (!UniformAt(index).type.isArray) return -1; + if (static_cast(element) >= + MG_State::GLState::ProgramObject::GetUniformArraySizeByTIndex(*Link, index)) { + return -1; + } + const Int location = baseLocation + static_cast(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(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((*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(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(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(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(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(record.BlockBindings[index]) + : 0; + } + if (!record.Archive || index >= record.Archive->Link.uniformBlockBinding.size()) return 0; + return static_cast(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(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) { @@ -10911,8 +11136,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; @@ -10920,9 +11145,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{}(blockName); + entry ^= (static_cast(static_cast(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 @@ -10944,6 +11211,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 @@ -11120,8 +11388,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 @@ -11139,12 +11406,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. @@ -11170,7 +11437,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); @@ -11289,27 +11556,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 CollectNonConsecutiveImageArrayPlans( - const MG_State::GLState::ProgramObject& stateProgramObject) { + Vector CollectNonConsecutiveImageArrayPlans(const ProgramBuildSource& src) { Vector 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(loc)) continue; + if (src.GetUniformLocation(name) != static_cast(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(loc), static_cast(element)); ++element) { - plan.units.push_back(stateProgramObject.GetUniformSamplerOrImageUnitIndex(element)); + plan.units.push_back(src.GetUniformSamplerOrImageUnitIndex(element)); } if (plan.units.size() < 2) continue; @@ -11905,7 +12171,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>& shaderSpirvs, const String& vertexStageEssl, const String& tessEvalStageEssl) { // PATCH_VERTICES is dynamic state, and it decides the synthesized stage's output @@ -11932,7 +12198,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; } @@ -11948,7 +12214,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; } @@ -11986,7 +12252,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; } @@ -11994,7 +12260,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); @@ -12011,7 +12277,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; @@ -12023,6 +12289,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& stateProgramObject) { #ifdef TRACY_ENABLE @@ -12032,24 +12317,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. @@ -12061,8 +12348,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. @@ -12079,19 +12366,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 nonConsecutiveImageArrays = - CollectNonConsecutiveImageArrayPlans(*stateProgramObject); + CollectNonConsecutiveImageArrayPlans(src); // Detach all existing shaders GLint attachedCount = 0; @@ -12128,19 +12415,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 linkedStages = stateProgramObject->GetLinkedShaderStages(); - auto& shaderSpirvs = stateProgramObject->GetGeneratedSpirv(); + const Vector 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 @@ -12150,7 +12437,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())); @@ -12159,7 +12446,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 @@ -12168,7 +12455,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 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)); @@ -12495,7 +12782,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; @@ -12587,13 +12874,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 @@ -12623,7 +12910,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()); } } @@ -12668,7 +12955,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; @@ -12749,7 +13036,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; @@ -12790,7 +13077,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } if (needsPassthroughTessControl) { - AttachPassthroughTessControlStage(*stateProgramObject, tessEvalShaderIndex, shaderSpirvs, + AttachPassthroughTessControlStage(src, tessEvalShaderIndex, shaderSpirvs, vertexStageEssl, tessEvalStageEssl); } @@ -12810,9 +13097,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 xfbNames; xfbNames.reserve(xfbVaryings.size()); // A block this build flattened no longer HAS the member the application asked @@ -12827,7 +13114,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; @@ -12860,7 +13147,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } g_GLESFuncs.glTransformFeedbackVaryings(m_backendProgramId, static_cast(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 @@ -12875,8 +13162,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(); } @@ -12899,7 +13186,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 @@ -12939,13 +13226,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(std::max(linkedXfbVaryings, 0)) != declaredXfbVaryingCount || static_cast(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(linkedXfbBufferMode)).c_str()); @@ -13010,55 +13297,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& 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 @@ -13068,12 +13375,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 @@ -13081,7 +13426,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); @@ -13096,13 +13441,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& 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) { @@ -13119,16 +13463,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(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(i)); + const auto& name = src.GetUniformBlockName(static_cast(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 @@ -13138,16 +13482,16 @@ namespace MobileGL::MG_Backend::DirectGLES { m_uniformBlockBackendIndices[static_cast(i)] = static_cast(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 @@ -13163,7 +13507,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; @@ -13858,11 +14202,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) { diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index f0f426d8..87920b39 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -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 @@ -2342,6 +2362,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 LinkedStages; + + // The post-link overlay. Governs iff OverlayGoverns; see the type comment. + Bool OverlayGoverns = false; + const Vector* BlockBindingOverlay = nullptr; + const Vector* 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 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& GetLinkedShaderStages() const { return LinkedStages; } + const UnorderedMap& 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& GetLinkedShaderSnapshot() const { + static const Vector 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(Link->glBlockIndexToTProgram.size()); + } + const String& GetUniformBlockName(Uint index) const; + + GLenum GetTransformFeedbackBufferMode() const { return Link->xfbBufferMode; } + SizeT GetTransformFeedbackVaryingCount() const { return Link->xfbVaryings.size(); } + const Vector& GetTransformFeedbackVaryings() const { return Link->xfbVaryings; } + const Vector>& 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 @@ -2402,11 +2589,11 @@ namespace MobileGL::MG_Backend::DirectGLES { ~BackendProgramObjectImpl(); void SyncToBackend(const SharedPtr& 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(); @@ -2507,6 +2694,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. @@ -2532,7 +2726,12 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint64 ComputeImageUnitFormatSignature() const; private: - void CacheResourceLocations(const SharedPtr& 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 @@ -2540,7 +2739,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>& shaderSpirvs, const String& vertexStageEssl, const String& tessEvalStageEssl); @@ -2611,6 +2810,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 @@ -2669,15 +2869,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; @@ -2718,8 +2923,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 @@ -2736,6 +2940,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 From 4d1499ef7e74b4f3e39bae713912d03d1d69bec3 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 18 Sep 2026 08:30:19 -0400 Subject: [PATCH 3/3] [MG_Test] (Disaggregated): P5e pg - four cases for the bindings record, the failed relink and the framed archive - AUniformBlockBindingAfterTheLinkTravelsOnItsOwnRecord: the create unlatches the bindings and the first emission after it always goes out (the ordering the applier depends on), an unchanged binding set emits nothing, and a post-link unit assignment moves the latch. This is the case the package's first red-once is built on: the integration-split lane has no scenario with a named uniform block bound to a non-zero point, so emptying the tail there is invisible and the statement has to be made here. - TheThreeBindingTailsAreWholeSetsAndAReIssuedCreateDropsThem: whole-set replacement in all three tails, the override NAME copied rather than pointed at (rule C), BindingsSerial advancing, and the re-issued create dropping every tail because they index into the archive it just replaced. - AFailedRelinkReIssuesWithLinkStatusZeroAndNeverAnObjectDeath: ID-88's "iff the frontend reports it unlinked" is asserted against the frontend rather than assumed, so the day Link()'s prologue stops withdrawing LINK_STATUS this case says the ruling has to be re-decided instead of the server silently drawing a dead executable. - TheFramedArchiveRoundTripsWithTheStageOfEveryModule: two modules of the SAME stage - the shape MGPProgramDesc::StageMask cannot describe - survive the frame, the skipped member comes back null, and a truncated frame is refused with both outputs defined. --- MobileGL/MG_Test/Pipe/ProgramEmitTest.cpp | 202 +++++++++++++++++++++- 1 file changed, 201 insertions(+), 1 deletion(-) diff --git a/MobileGL/MG_Test/Pipe/ProgramEmitTest.cpp b/MobileGL/MG_Test/Pipe/ProgramEmitTest.cpp index b9b6d774..a0ff99a5 100644 --- a/MobileGL/MG_Test/Pipe/ProgramEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/ProgramEmitTest.cpp @@ -66,6 +66,9 @@ // drives create_shader_state needs their definitions - the applier's own header deliberately // only forward-declares them. #include +// 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 #include #endif @@ -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& 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(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& 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 stages{static_cast(ShaderStage::Vertex), + static_cast(ShaderStage::Vertex)}; + + Vector 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