[MG_Pipe, MG_Impl, MG_Remote, MG_State, MG_Test] (Disaggregated): P5e pg - the archive becomes server-owned and the bindings get a record

- MGPipeShaderCsoRecord gains Archive, a SharedPtr<const ProgramArchive> 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.
This commit is contained in:
2026-09-18 07:50:27 -04:00
parent f6cfcbd36b
commit 39d0677616
15 changed files with 745 additions and 77 deletions
@@ -42,6 +42,26 @@
// * and `LinkArtifacts::program` is NEVER visited. It is the live glslang TProgram, it is
// null for every archived instance by construction, and VisitFields deliberately omits it
// (57 of the 58 members). Decode leaves it null.
//
// P5e (pg), CONTRACT-P5E §5.5 "codec completeness": THE SKIPPED MEMBER IS NAMED, AND IT IS
// `LinkArtifacts::program`. The contract asks this package either to carry it or to prove the
// twin never reads it, and the proof is a closed grep rather than an argument: every read of
// that member in the tree is inside `ProgramLinkTask.cpp` (`DoReflection`, which is the only
// code that may dereference it at all) and the three sites that `reset()` it -
// `ProgramLinkTask.cpp:678,764,877` and `ProgramObject::ResetLinkArtifacts`. `MG_Backend/`
// contains NO read of it, in either backend; `ProgramTranslationCache` asserts it is null at
// insert. So the archive answers every reflection question the program twin asks, and the one
// member it does not carry is one no twin has ever asked for. It could not be carried in any
// case: it points into a glslang arena that no archived instance owns.
//
// AND THE OTHER HALF OF THAT CONTRACT LINE - `ProgramArtifactsCodec.cpp`'s libc++ caveat - is
// about the STRUCT-SIZE ECHO, not about a member: under a toolchain whose `MGL_LINKARTIFACTS_SIZE`
// is not pinned yet (the NDK's libc++ branch of `ProgramArtifacts.h`, inert until the integrator
// fills it in) the echo word is 0. That is SAFE FOR P5e AND NOT A GAP: the echo compares what
// this build wrote against what this build expects, so a round trip within one build is exact,
// and P5e is an in-process split where the writer and the reader ARE one build. It becomes a
// real check the day a client and a server built by different toolchains share a wire, which is
// P6's boundary and P6's problem - stated here so it cannot be discovered instead.
namespace MobileGL::MG_State::GLState {
// Bumped whenever the byte format changes in a way a previous reader would misread. A
@@ -60,6 +80,45 @@ namespace MobileGL::MG_State::GLState {
Bool DecodeProgramArtifacts(const Uint8* bytes, SizeT size, LinkArtifacts& link,
SpirvArtifacts& spirv);
// ---- P5e (pg): the archive as the SERVER OWNS it -------------------------------------
//
// WHAT THE TWO STRUCTS ABOVE DO NOT CARRY, and why a package that makes the server answer
// every reflection question out of them had to notice: THE STAGE OF EACH MODULE.
// `SpirvArtifacts::generatedSpirv` is one module per SHADER OBJECT the link consumed, in
// snapshot order, and the stage of each lives in `ProgramObject::m_linkedShaderSnapshot` -
// GL-thread state, not an artifact. `MGPProgramDesc::StageMask` cannot stand in for it: it
// is a bit SET, and GL lets two shader objects of the same stage be attached to one
// program, so a list rebuilt from the mask would be shorter than `generatedSpirv` and the
// backend's "linked stages and modules must agree" refusal would fire on a perfectly good
// program.
//
// So the frame is [stage count][stage words][the codec's own bytes], written and read by
// this package on both sides, and `ProgramArchive` is what a decode produces: the record's
// OWN copy, with the lifetime of the record rather than of the staged run that carried it
// (rule C). Stages travel as Uint32 rather than as `ShaderStage` so this header keeps its
// include closure - `ProgramArtifacts.h` beside it may not reach `ShaderStage.h`.
struct ProgramArchive {
LinkArtifacts Link;
SpirvArtifacts Spirv;
// ShaderStage, one per Spirv.generatedSpirv entry, at the same index.
Vector<Uint32> LinkedStages;
};
// The bound a decode refuses past: six is what MGPProgramDesc::Spirv[] can name and what
// ProgramEmit.h's own truncation counter already watches, doubled so that a program this
// stack can build is never refused here for a reason the emitter did not already count.
inline constexpr SizeT kProgramArchiveMaxStages = 12;
// Appends the framed archive to `out` (not cleared, so a caller may frame it further).
// `linkedStages` must be index-aligned with `spirv.generatedSpirv`.
void EncodeProgramArchive(const LinkArtifacts& link, const SpirvArtifacts& spirv,
const Vector<Uint32>& linkedStages, Vector<Uint8>& out);
// Replaces `out` with what `bytes` describes. Same refusal terms as
// DecodeProgramArtifacts, plus: a stage count past kProgramArchiveMaxStages, and a stage
// count that does not match the decoded module count.
Bool DecodeProgramArchive(const Uint8* bytes, SizeT size, ProgramArchive& out);
// How many fields a type's VisitFields table actually visits. The codec walks exactly that
// table, so this is what pins "the codec did not quietly grow an arm of its own" - most of
// all for LinkArtifacts, whose 58th member is the live TProgram the table omits. It is a