Commit Graph
3537 Commits
Author SHA1 Message Date
swung0x48 394df89237 [scripts, ci] (P7): the memo purity gate asserts its own anchors and allow-list, and stops calling a policy exemption an absence
Rework round 3 of 4 after the D2 review (fable, ID-P7-8). Three defects, each of a kind
the gate could not have reported about itself.

(a) TWO STRUCTURAL SELF-CHECKS. family_spans() returned whatever matched and nothing ever
asserted that each of the five FAMILIES anchors found its declaration, so a renamed struct
would drop a whole family while every rule kept passing. And an allow-list entry no rule
can consult documents a carve-out that is not happening - a claim about the tree that
nothing verifies. main() now fails on either, naming the anchor or the entry and saying
which of the two repairs is wanted. Both are exercised: renaming an anchor and adding a
dead entry each red the gate.

(b) THE DOCSTRING AND THE test.yml COMMENT WERE WRONG about what the guards mean.
MOBILEGL_PIPE_LEGACY_MEMOS defaults ON (CMakeLists.txt:43) and no job here clears it, so
CI split builds DO compile those arms - TwinLookupMemo (DirectGLES.cpp:114) sits inside
one and is compiled into every lane this repo runs. The rule is "the legacy arm MAY KEEP
pointer keys", a policy exemption; only !MOBILEGL_BUILD_DISAGGREGATED is code a split
build genuinely never sees. Both now say so.

(c) FRONTEND HAD NO LEADING WORD BOUNDARY, so BackendSamplerObject* matched SamplerObject
and the gate reported the memos' backend VALUES as frontend keys - which the first version
answered with two allow-list entries, i.e. it documented a false positive instead of
fixing it. (?<!\w) added, with controls both ways: the backend twins must stay green and
the qualified MG_State::GLState::ProgramObject* must still match.

The allow list therefore goes from six entries to ONE. Four were dead on arrival - two
false positives now fixed at the source, one naming a typedef with no frontend type in it,
and const void* program / Uint64 programLifetimeId, which no rule could ever consult
because void is not in FRONTEND and neither line calls GetLifetimeId(). The
ResolvedTextureBindingMemo monolith key those last two described is still deliberate and
still there; it simply never needed an exemption, and saying it did was misleading. What
remains is the twin registry's UnorderedMap<StateObject*, Entry>, which really is
suppressed and really is the pull build's only arm.
2026-09-22 18:17:25 -04:00
swung0x48 0f3b82725d [MG_IntegrationTest, docs] (P7): the upload-shape gate was measuring its own bounding box; inset the scatter so the box/rect decision is reachable
Rework round 2 of 4 after the D2 review (fable, ID-P7-8), which found that this gate's
numbers were right and its SUBJECT was wrong.

The scatter wrote 40 2x2 rects at x=((i*7)%32)*2, y=((i*5)%32)*2 into a 64-px atlas.
Both strides are surjective onto 0..31 over 40 iterations - rect 9 lands at x=62, rect 19
at y=62 - so the rects' union box covered the WHOLE LEVEL, and the server's subRectEligible
(Managers.cpp:8486-8491) requires !dirtyRegion.CoversWholeLevel, a BOUNDING-BOX test
(MipmapStorage.h:30-33). Both scatter textures were rect-INELIGIBLE on every arm before
:8649's unpack-ring policy was ever consulted. The box-versus-rect decision the scenario
exists to pin was never reached for the two textures it scatters into, and the only
texture that could move under any red-once was the contiguous control band - which is
exactly what the old substitute red-once's box=6 rect=3 jobs=12 was.

kScatterMargin insets the scatter by one rect: same 40 writes, same coarse stride, union
box bounded at (2,2)..(62,54). The gold row is UNCHANGED at emit=9 box=9 rect=0 jobs=9 on
all four arms - but it now means what it says.

THE RED-ONCE IS NOW ZERO-PATCH, which is stronger than the throwaway source edit it
replaces: MOBILEGL_ESPRYT_DISABLE_UNPACK_RING=1 alone gives box=3 rect=6 jobs=183 with
bytes/f[tex] falling from 104448 to 9024, while all 26 pixel cases stay green. BOTH
SCATTER TEXTURES moved - box=3 is the contiguous band alone, rect=6 is the plain atlas
AND the atlas written through the glTextureView, and jobs=183 = 3 + 6x30 confirms thirty
rects per scatter emission. The same knob was green before the inset, which is the
evidence that CoversWholeLevel was short-circuiting the decision.

The three prose sites are rewritten accordingly: the scenario's gold-table rationale, the
MEASUREMENTS.md 13.1 reading and the package note's red-once section now explain
attempts 1-3 as mutations of a decision the client already makes correctly, and attempt 4
by CoversWholeLevel rather than by the ring.

Also PipeStatsWindow gains LastFrom{Client,Server}LogSince, and the gate reads its two
role windows since a mark taken before the counted workload. The server writes an EMPTY
teardown window after the counted one, so "the last line in the file" is one shutdown
away from reporting four zeroes as the workload's shape. Moving to a marked read exposed
a second latent defect in the same assertion: ASSERT_TRUE(clientWindow.found) over the
whole file was satisfied by the "counters ON" BANNER in all three split arms, claiming a
window had been read when none had. The assertion is now on the window that actually
carries the tex[] bracket.
2026-09-22 18:17:25 -04:00
swung0x48 b3334cf336 [MG_Test] (P7): SubsystemDepsTest skips its two resolver cases in the pull build, and says what keeps the sweep alive
Rework round 1 of 4 after the D2 review (fable, ID-P7-8). Mirrors the integrator's
6da19f71 so that later commits from this worktree apply cleanly, and takes the two
review notes attached to the same file.

THE GUARD. The four Resolve<Family>SubsystemArm() declarations live inside
did not compile in the pull build (ID-P7-11). Both now GTEST_SKIP under
!MOBILEGL_PIPE_PUSH the way the client-side case already did. Verified by re-running
the file's own compile command with -DMOBILEGL_PIPE_PUSH=0 and MOBILEGL_BUILD_DISAGGREGATED
undefined: -fsyntax-only rc 0.

WHAT KEPT THE MASK SWEEP ALIVE, now asserted instead of assumed: every resolver ends in
ClassifyPipeSubsystemArm, and a NoArm verdict calls StopOnArmlessPipeSubsystem
(Managers.cpp:2928-2938), which ABORTS. The sweep deliberately drives masks that refuse a
family's handle arm, so it survives only because MOBILEGL_PIPE_LEGACY_MEMOS defaults ON
(Config.h:399) and leaves a legacy arm standing. ASSERT_TRUE on that, with the reason -
the day the default flips, this file would have aborted with no explanation.

THE FRAMEBUFFER RESOLVER IS RECORDED, NOT ASSERTED, and the asymmetry is the finding:
ResolveFramebufferSubsystemArm folds D-C3's wire-caps refusal in beside the dependency
rule (Managers.cpp:4323-4325), and that half asks the server's published caps, which a
bare unit binary with no session cannot answer in the affirmative. So a false says nothing
about D-K2 and asserting true would pin whatever the binary happens to have linked. The
sweep still covers it in the direction that matters. Also adds the 0x5ff refusal lane
beside 0x7ff.
2026-09-22 18:17:25 -04:00
swung0x48 310d4e90d1 [docs] (P7): ID-P7-28 - package D3 landed; the texture-view exclusion is retired 2026-09-22 18:16:55 -04:00
swung0x48 00d0239696 [docs] (P7): package D3's note, and the census row its fix falsified
P3b/P4b wave 2-D package D3, slice 3.

docs/Disaggregated/notes/p34b/espryt-d3.md, in espryt-d1.md's shape: the mechanism end to end,
the decision between resolving the predicate and indexing the reverse edge with the argument
that decided it (deletion ordering), the red-once evidence with the exact ctest invocations and
the red and green lines per arm, the gate numbers, and six things that remain.

The decision, stated once here because the note argues it at length: the gate resolves through
Desc.ViewOf to the storage record; the reverse index was rejected because it needs pruning on
VIEW death or an owner upload bumps a recycled slot's Serial, because it makes
ApplyTextureUpload write a record other than the one the call names, and because
VkTextureManager::ResolveWireTextureStorage already resolves the same way on the Magma side -
an index would have given the two backends two answers to one question.

notes/p34b/split-scenario-census.md is updated where the fix falsified it - the summary row,
the texture-family row and the exclusion section - and NOT rewritten anywhere else. D2's text
stands and D3's resolution sits above it in a quoted block, because a package note is a record
of what that package did rather than a live statement of the tree. Two citation corrections
taken while reading the device-window baseline: the CTS block is KHR-GL46 and not KHR-GL43, and
texture_view is 1/7 fail there with the failing case view_sampling, NOT coherency - coherency
reads Pass, which is what D2 claimed and is now checked per case against report-texture.json
rather than against a summary line.

espryt-d2.md itself is left alone and the note says why it is left alone.

Gates on this head, all measured rather than carried over: G1 .text 0xa52203, nm --defined-only
30570 symbols 0 added / 0 removed, -D 0/0, ninja -C build-linux rc 0. fatal_census rc 0 (79
abort sites, 0 unmarked - unchanged, rule I). link_ratchet --assert-monotone rc 0 (unchanged at
186). spawn_lane_parity rc 0. Lanes: unit 2418/2418, integration-split 264/264,
integration-spawn 180/180, integration-tcp 183/183, integration-magma-split 75/75, all 100%.
Name sets against a real ctest -N at a7c06f4f: +1 on each of split/spawn/tcp (the un-excluded
case), 0 elsewhere, 0 removed anywhere.
2026-09-22 18:10:56 -04:00
swung0x48 1c36987e84 [MG_Backend, MG_Pipe, MG_Impl] (P7): D-K2's dependency rule has one statement, and every reader reads it
P3b/P4b wave 2-D package D3, slice 2 - D2's two loose ends, now in-partition because D1 has
landed on feat/disaggregated and MG_Backend/DirectGLES/ is free.

THE STALE PROSE, in the header of the file that implements the refusal.
MG_Backend/DirectGLES/Managers.h's resolver block said "THREE OF THEM CARRY A DEPENDENCY"
(there are six rows; bits 8 and 13 were the two it did not know about) and listed
"10 without 11" among the mirror pairs that are FINE - when 10-without-11 is precisely D-K2's
fourth row (ID-14/ID-15), refused twenty lines below by ResolveTextureResourceSubsystemArm and
withheld by the client. MGPipe.h lost the identical pair of defects with R-5; this copy was
left behind because the file belonged to a package in flight. It does not restate the rows
again: the file that states them is the file the code reads.

BOTH READERS NOW READ MG_Pipe/SubsystemDeps.def. More than the two one-line changes D2
estimated, because the rule had six statements and the estimate counted two:

  PipeFill.cpp P4aFamilyDependencyBits   its own four-row kMGPipeP4aFamilyDependencies[]
                                         -> MGPipeSubsystemRequires(); table deleted
  PipeFill.cpp P5eFamilyIsLive           hand-coded (pushMask & bit 7) == 0
                                         -> MGPipeSubsystemDependenciesAreSet(bit 13, mask)
  Managers.cpp PipeSubsystemDependencyMissing   took a literal dependency BIT per call site
                                         -> takes the FAMILY and asks the table
  Managers.cpp the four resolvers        five calls, five hand-written sentences
                                         -> four calls, one per family, naming only the family
  Managers.cpp ResolveVertexInputSubsystemArm    hand-rolled bitSet && !resourcesBitSet
                                         -> the table
  DirectGLES.cpp ResolveBufferBindingSubsystemArm  the same hand-rolled pair -> the table

MGPipe.h gains one accessor, MGPipeSubsystemDependencyWhy(), so a reader that refuses prints
the ROW'S OWN sentence instead of a copy of it. The .def has carried a Why column since R-5
and nothing read it; its "WHO READS IT" section is rewritten because "THEY ARE NOT SWITCHED
OVER IN THIS COMMIT" is no longer true of anything.

THE CLIENT'S ARGUMENT IS MASKED TO kMGPipeP4aFamilySubsystems, and that mask is the whole
difference between a rewrite and a behaviour change. The .def carries SIX rows; the table
deleted from PipeFill.cpp carried FOUR, and that predicate is wants()'s fourth conjunct for
P4a's families only. Reading all six unmasked makes the CLIENT withhold bit 8's vertex-input
emissions at a mask with bit 7 clear, which is P3a's own rule and not this gate's - caught,
not reasoned about, by TextureEmitTest's
EveryDKTwoDependencyRowGatesItsOwnFamilyAndTheMirrorPairsStayLive going red on exactly that
assertion ("bit 8 alone, with bit 7 clear: P3a's own rule, which this table must not touch").
Bit 13 is not narrowed there either; it asks the table for itself in P5eFamilyIsLive, because
bit 13's conjunct is bit 13's own. The ROWS come from one place; WHO each gate speaks for is
still each gate's.

ONE BEHAVIOUR IMPROVEMENT FALLS OUT rather than being designed in: the texture family is the
only row with two required bits and used to be two guarded calls, so a mask missing bit 11 but
carrying bit 7 was reported with the bit-7 sentence. One call now computes required & ~mask
from the row, so the refusal names whichever bit is actually missing.

WHAT THIS COSTS, said out loud. SubsystemDepsTest used to compare two COPIES of the rule; it
now compares two READERS of one table and cannot catch a typo in a row. It is kept and the
.def says why: both sides still fold in reasons of their own - the server's own family bit and
D-C3's wire-caps refusal, the client's consumer and wired conjuncts - and those can still
disagree at a mask. The compile-time half of D2's red-once (drop the fourth row from the .def
-> MGPipe.h:225/:230 fail) is untouched and is now the load-bearing one.

SubsystemDepsTest 5/5, TextureEmit's D-K2 case green, unit lane 2418/2418.
G1: .text 0xa52203, nm --defined-only 30570 symbols, 0 added / 0 removed, -D 0/0. Every line
is inside #if MOBILEGL_PIPE_PUSH or #if MOBILEGL_BUILD_DISAGGREGATED, and MGPipe.h's new
accessor is inline constexpr and unreferenced in the pull build.
2026-09-22 18:10:56 -04:00
swung0x48 1e1f4bad27 [MG_Backend, MG_IntegrationTest, MG_Remote, docs] (P7): an owner-side upload shows through a sampled texture view, because the split clean gate resolves to the storage record
P3b/P4b wave 2-D package D3, slice 1. The defect package D2 found with its new
TextureViewAliasScenario gate and excluded by name because the fix is outside D2's footprint.

THE HOLE. Once a glTextureView had been SAMPLED, a later glTexSubImage through the OWNER's
own name never became visible through the view, on inproc, spawn and tcp, green under
monolith. The client half was never at fault: the case's own emit-counter assertions pass on
the red run, so the records were emitted and the applier accepted them. A view owns no texels
and the client keeps ONE emission cursor per storage - an upload through a view's name is
remapped onto its storage owner before it is emitted (MG_Impl/Pipe/TextureEmit.h) - so every
resource_subdata for either name is keyed on the OWNER and ApplyTextureUpload moves the
OWNER's Serial and PendingUploads alone. BackendTextureObject::IsDrawSyncCleanByRecord read
the VIEW's Serial and PendingUploads, which nothing an upload does can move, so the gate
answered CLEAN for every owner write after the view's first sample and the sync that would
have drained the owner's pending uploads never ran.

THE FIX, AND WHY THIS ONE. The gate's two STORAGE clauses are asked of the STORAGE record -
this record for a texture that owns its texels, the record Desc.ViewOf names for a view -
through the new PipeTextureStorageRecordForRecord. The three PARAMETER clauses stay the
view's own record's, because a view has its own texture parameters and its own built-in
sampler, which is the whole reason the second name exists. SyncTextureViewToBackendByRecord
stamps that same storage serial at both its arms, so the stamp and the gate name one
quantity. This is the split equivalent of the monolith gate's GetContentVersion(), which
TextureObjectView forwards to its owner (TextureObjectView.cpp:100-102).

THE ALTERNATIVE WAS REJECTED. A reverse index owner->views, so an owner-side resource_subdata
bumps every viewing record's Serial, is the other shape D2 named. It is wrong here on three
counts: it puts new mutable state in MG_Pipe's applier, which both backends read, to answer a
question one backend's gate asks; it makes ApplyTextureUpload write a record other than the
one the call names, which breaks the applier's own serial discipline; and its index has to be
pruned on view death or an owner upload bumps a recycled slot's Serial - a silent
cross-texture hazard with no test that would catch it. The resolve direction needs no state,
no pruning, and is checked at every use: PipeTextureRecordForHandle tests Live and Gen.

DELETION ORDERING, WHICH DECIDED IT. A view holds a STRONG SharedPtr to its storage owner
(TextureObjectView::m_storageOwner) and the client emits resource_destroy from the DESTRUCTOR,
not from glDeleteTextures - "the last SharedPtr to this object dropping, not the glDelete*
that only marks the name and leaves a still-bound object very much alive"
(TextureObject.cpp:63). So an owner's record outlives every record that views it whatever
order the application deletes the two names in, and the forward edge cannot dangle. The walk
checks anyway, and a null answer is NOT clean - the direction that re-syncs.

VIEWS OF VIEWS. ONE hop always reaches storage: glTextureView composes a view-of-a-view onto
the ROOT at creation (the spec's additive min-level rule), the client's emitter says so and
TextureObjectView's invariant is that its owner is never a view. The walk is transitive
anyway and BOUNDED at four hops, because this side may not depend on a client invariant to
terminate; a cyclic chain the wire could forge ends as a null answer, which is "not clean",
not a refusal. No new abort site and no new Fatal (rule I): fatal_census abort_sites stays 79.

RED-ONCE (rule J), the case D2 excluded, with the exclusion lifted and nothing else changed,
at D2's head a7c06f4f:

    ctest -R 'DirectGLES\.(Split|Spawn|Tcp)\.TextureViewAliasScenario' -j 3
    ->  79% tests passed, 3 tests failed out of 14
        4042 - DirectGLES.Split.TextureViewAliasScenario.TheOwnerUploadDrainRunsAgainAfterADrawBoundary (Failed)
        4046 - DirectGLES.Spawn.TextureViewAliasScenario.TheOwnerUploadDrainRunsAgainAfterADrawBoundary (Failed)
        4050 - DirectGLES.Tcp.TextureViewAliasScenario.TheOwnerUploadDrainRunsAgainAfterADrawBoundary (Failed)

    the first owner write, through the view: 11408 of 11408 pixels disagree;
      first at (2, 2) is rgba(50,160,30,255), expected rgba(0,0,255,255) +/- 2
    the second owner write, through the view: 11408 of 11408 pixels disagree;
      first at (2, 2) is rgba(50,160,30,255), expected rgba(255,255,0,255) +/- 2

Only the PIXEL channel is red; the drain's emit counters pass, which is what places the
defect on the server. The monolith control DirectGLES.TextureViewAliasScenario.* is 4/4 in
the same run. With the fix: 14/14, 100%.

CONTRACT-P5E.md 5.2's clean gate is amended to state the rule the code now implements, and
says out loud that the rec->Serial form it carried through P5e was wrong rather than
restating it. The CMake exclusion is lifted; the scenario registers all four cases on all
three arms again (G14: the name never left any lane).

G1: .text 0xa52203, nm --defined-only 30570 symbols, 0 added / 0 removed, -D 0/0. Every line
touched in Managers.cpp is inside #if MOBILEGL_PIPE_PUSH (the gate and the resolver) or
#if MOBILEGL_BUILD_DISAGGREGATED (the view sync), and Managers.h's declaration is inside the
PIPE_PUSH block, so the pull build's preprocessed text is unchanged.
2026-09-22 18:10:56 -04:00
swung0x48 550f81d5f8 [docs] (P7): W4 §3b - the OpenRA hole is one draw (call 30376) reading vertex bytes other than call 30366's
Lens D placed 100 % of the wrong pixels inside the screen-space union of a
single glDrawArrays whose vertex data was the frame's first glBufferSubData
into the streaming buffer; the palette respecify is not the mechanism. The
candidates are WriteWireBuffer's serial-gated branch and the mid-frame drain
that bumps the frame serial without a fence.
2026-09-22 18:00:19 -04:00
swung0x48 ebd73da06c [docs] (P7): the W4 evidence files are plain files, not executables
Copied off a Windows mount, they came in as 100755; nothing in them runs.
2026-09-22 17:57:57 -04:00
swung0x48 770f1a0b3a [docs] (P7): ID-P7-26/27 - D2 lands with a rework round, and bsl-esc-menu is a memory defect, not an exclusion
D2's fable review says land with fixes: the TUS gate's scatter workload has a
union box that covers the whole level (rect=0 is geometry, not the unpack-ring
policy), the memo purity script fails open on a renamed family, and the census
numbers were not reproducible - all sent back to D2 as one rework round; the
excluded TextureViewAlias case gets its §12 row (owner D3).

bsl-esc-menu-854 on the spawn arm: monolith passes at 807 MiB peak RSS, the
server process dies at ~1.2 GiB in scudo's secondary allocator with 8 GB of
system memory free, inproc dies at 1.6 GiB. That is the wire arm holding about
twice the bytes (or the mappings) of the monolith, a disaggregation defect
under ID-P7-12's rule, so it stays in the gate-3 denominator; M1 attributes it.
2026-09-22 17:57:22 -04:00
swung0x48 dd6c541af7 [docs] (P7): ID-P7-25 - W4 on p7w4: 34/36 match monolith, and OpenRA is a silent draw drop, not a cold cache
Device window 1 W4 (APK p7w4 = 8c5dd6fc): iterationt x2 pass, the 39-case
inproc matrix has 34/36 of the denominator within 0.0005 of the monolith
reading, bsl-esc-menu still OOMs under inproc (spawn arm pending).

ID-P7-19's cold-driver-cache reading of OpenRA is withdrawn: 26 runs show a
~50 % per-run coin flip that ignores cache temperature, IPC_AUDIT and FIF=1,
reproduces on the two-process spawn arm, and only ever yields three pictures
(golden / 9836 px / 14658 px). The monolith snapshot at the previous swap is
the golden and the palette texture is byte-correct at the snapshot, so the
wrong pictures are frames with a bounded run of draws silently skipped (nine
sprite blobs absent over intact terrain). Gate 3's acceptance for OpenRA is
now six run-ahead and six lockstep runs all equal to the golden; the fix is
package B3.
2026-09-22 17:51:56 -04:00
swung0x48 c2cc3a9673 [MG_Test] (P7): the subsystem-deps server-gate cases compile in the pull build too
D2's SubsystemDepsTest called the four Resolve*SubsystemArm resolvers, which
Managers.h declares only under MOBILEGL_PIPE_PUSH (:664), so build-linux
(the pull flavour, ID-P7-11) stopped compiling while the stale libMobileGL.so
kept G1 looking green. Cases 3 and 4 now skip in the pull flavour the way
case 2 already did; the unit name set is the same in both flavours. Pull
build rc 0, .text 0xa52203, symbols 0 added / 0 removed; push flavour 5/5.
2026-09-22 17:37:59 -04:00
swung0x48 519445f946 [MG_Pipe, MG_Impl, MG_Test, docs] (P7): D-K2's dependency rule in one table, with the case that fails when the two readers disagree
P3b/P4b R-5 (and R-3's cheap half), landing under the P7 program (PLAN-PH-P34B-P7.md
1.2, wave 2-D package D2).

The rule had SIX statements and nothing compared them, and two of the four prose ones
were wrong in the same way: MG_Pipe/MGPipe.h and MG_Backend/DirectGLES/Managers.h both
said "THREE OF THEM HAVE A DEPENDENCY" (there are six rows) and both said "the mirror
pairs (10 without 11, ...) are all fine" - when 10-without-11 is precisely D-K2's FOURTH
row (ID-14/ID-15), refused by the server and withheld by the client, with PipeFill.cpp
saying so out loud: "The brief's original 'bit 10 without 11 is fine' is WITHDRAWN for
P4a as built." Neither wrong comment could fail anything; Managers.h's copy sits in the
header of the file that implements the refusal.

The rows, once, in MG_Pipe/SubsystemDeps.def (X-macro, FatalFamilies.def's convention),
expanded by MGPipe.h into kMGPipeSubsystemDependencies[] / MGPipeSubsystemRequires() /
MGPipeSubsystemDependenciesAreSet() with four static_asserts. Six rows: 8->7, 9->10,
10->{7,11}, 11->10, 12->none (a ROW, not an absence), 13->7. Bits 8 and 13 are the two
the old comment did not know about; bit 13's row lived fifteen lines further down in the
same header, which is how "THREE OF THEM" survived two phases.

THE TWO READERS DO NOT YET READ IT, deliberately: MG_Impl/Pipe/PipeFill.cpp is the
contract package's file for the whole phase (TextureEmit.h states that rule) and
MG_Backend/DirectGLES/Managers.cpp belongs to wave 2-D package D1 while it is in flight.
Switching them is two one-line changes for the integrator. The anti-drift property does
not wait: MG_Test/Backend/DirectGLES/SubsystemDepsTest.cpp is the first thing in the tree
that calls BOTH readers at the same mask - sweeping the phase constants, the two
documented refusal lanes and one mask per row - and compares each against the .def. It
lives in that binary because that binary links both sides.

Both comparisons are one-directional, each for a measured reason. The server resolvers
also fold in the family's own bit and D-C3's wire-caps refusal, so a resolver may say NO
for its own reasons but never YES where the table refuses. MGPipeP4aFamilyEmits is a
QUADRUPLE and the first version of this case asserted equality and reddened at 0x1fff on
the sampler and program families because a bare unit binary registers no consumer - the
assertion being wrong, not the product, and recorded in the note because the temptation
to assert equality here is strong.

Red-once (R-16), twice: dropping the fourth row from the .def fails at COMPILE time
(MGPipe.h's static_asserts); making the server's ResolveSamplerSubsystemArm stop checking
bit 10 (throwaway, reverted) reds the runtime case and names the mask - 0x9ff, which is
exactly the lane CMakeLists.txt already pins for G12.

R-3's cheap half: CONTRACT-P5.md claimed the image-access, sampler-view-target and
draw-buffer encodings as contract by pointing at package-header LINE RANGES, and all
three had drifted (ImageEmit.h:146-159 no longer holds the encoding at all). All three
rows now state the encoding by enumerator and function name, and the two confusable
siblings are named. The remaining half - MGPSamplerView::Target still has no Count bound,
no ...IsValid and no Fatal for an out-of-range value - is stated in the row rather than
closed, being an MG_Pipe change outside this package's footprint.

Managers.h:690-695's copy of the wrong prose is NOT corrected here: it is package D1's
file, and its two defects are named in notes/p34b/espryt-d2.md so the integrator can take
them in one line.

G1 after this change: pull .text 10822147 = 0xa52203, nm --defined-only identical to
~/w7/p7-before/pull-syms.txt (30570 symbols) - the new inline constexpr table does not
reach the pull build.
2026-09-22 17:31:51 -04:00
swung0x48 779ff21b3e [MG_Test] (P7): the raw-depth-fetch sampler case runs both arms; the deletion half is P13
P3b/P4b deepening, landing under the P7 program (PLAN-PH-P34B-P7.md 1.2, "raw-depth-fetch
sampler 的 monolith 清理,步骤 1+2", wave 2-D package D2).

STEP 1 DONE. ContextEpochTest gains a case that drives GetRawDepthFetchSampler() on BOTH
arms in one process and asserts they are two different BackendSamplerObjects, which is the
observable form of the claim DirectGLES.cpp:334-336 makes in prose - "the legacy pair is
not constructed or consulted by the server". A server that had fallen through to
g_rawDepthFetchSamplerBackend would hand back the same pointer under both transports. It
also asserts the transport arm's cached native sampler survives a round trip through the
monolith arm, so neither arm is rebuilding state the other owns.

STEP 2 NOT DONE, AND THE PACKAGE'S OWN CONDITION IS WHY. The instruction was to delete what
the transport arm makes unreachable "ONLY if it keeps the pull build's .text identical".
It does not. Every candidate is UNCONDITIONAL code - the two file-static SharedPtrs
(DirectGLES.cpp:82-83), the monolith arm (:361-371), NeedsRawDepthFetchSampler (:393-399)
and the pre-handle sampler-pass branch (:8015-8024). None sits behind
#if !MOBILEGL_BUILD_DISAGGREGATED; the pull build configures DISAGGREGATED=OFF and
PIPE_PUSH=OFF and links all of it, and the monolith arm is the ENTIRE body
GetRawDepthFetchSampler has there. Deleting any of it moves pull .text and fails G1's
0/0/0/0 - the rule ARCHITECTURE.md:340 states for this exact class of retired-but-compiled
code. Recorded as P13, beside the existing S7 entry in notes/p5f/f0-statics.md, rather
than attempted and reverted.

G1 on this head: .text 10822147 = 0xa52203, nm --defined-only identical to
~/w7/p7-before/pull-syms.txt (30570 symbols).
2026-09-22 17:31:51 -04:00
swung0x48 9a69e08365 [MG_IntegrationTest, docs] (P7): twelve texture and program scenarios join the split arms, with the census that put them there
P3b/P4b deepening, landing under the P7 program (PLAN-PH-P34B-P7.md 1.2, "门:纹理 /
program 场景 split 覆盖", wave 2-D package D2). Each candidate was run once under
MOBILEGL_TRANSPORT=inproc on build-split and its first blocker recorded; the row per
scenario is in notes/p34b/split-scenario-census.md.

Registered (all green on inproc, spawn and tcp): TextureView,
TextureParamsWithoutASamplerView, SampledSetStaleness, ImageSizeAfterRespec,
ImageTargetKind, IntegerBorderColor, SwizzleAccessRoutine, PostLinkAttach,
RelinkStageSet, UniformInitializer, Glsl420Declaration, IoBlockNameCollision. 71 cases
per arm, 213 new gating entries; the recorded split-arm registration count the tier-2
Magma replay walks goes 33 -> 46.

TextureParamsWithoutASamplerView settles the debt table's 待核 row: all four G9 cases
pass under all three transports, so the P6 inspection-forwarder debt is closed for this
half - and it is now re-taken on every CI run rather than by inspection.

ProgramPipelineScenario beyond *StorageBlock* is NOT registered, and that is the rule
working rather than a blocker to retire: the other nine cases are pure name/state cases
that draw nothing, and an armed DirectGLES.Split. case whose workload produced no record
is red by design (ScenarioFixture.h:84-97 - the client encoder's record ordinal must
move). Registering them would assert "records crossed" about cases with no records to
send.

DirectVulkan arms default to none, deliberately, and the note says why: the
informational tier-2 block already replays every recorded registration on DirectVulkan
under integration-magma-all-{split,spawn,tcp}, so these scenarios do get a Magma x
three-transport reading - just not a gating one. Gating them would assert P7 wave 2's
exit gate 1 from inside a P3b/P4b package, on a backend whose own @P7 refusals are still
being retired in waves A/B/C.

D1 owns PixelStoreSweep, DepthStencilReadbackMatrix, PackedWordReadback and
LayeredTextureReadback; they are not in this census.
2026-09-22 17:31:51 -04:00
swung0x48 6567bc1e16 [scripts, ci] (P7): a purity gate for Espryt's re-keyed memo families
P3b/P4b deepening, landing under the P7 program (PLAN-PH-P34B-P7.md 1.2, wave 2-D
package D2): "一条纯度 grep 门:split 构建下这些 memo 键里不得出现前端指针类型;接 CI".

scripts/ci/espryt_memo_purity.py, a sibling of link_seam_purity.py in the same style:
a hard-coded source list, a code_only() comment strip, (pattern, message) rules, and
a --self-test that runs the negative controls and then the real scan in one
invocation - which is what lets test.yml pass only --self-test and still get the
gate. Wired in next to link_seam_purity.py.

It scopes the rules to the BRACES of the families the plan names -
ResolvedTextureBindingMemo, SamplerPassMemo, UnitSamplerLookupMemo, TwinLookupMemo,
StateBackendObjectRegistry (the program registry and its five siblings) - plus the
image sweep, which is seven file statics rather than a struct and is matched by name.
Scoping matters: these two files pass frontend pointers around legitimately in
hundreds of places, and a file-wide grep would be a list of exceptions rather than a
gate.

Guard-awareness is a small conditional walker, not a preprocessor: it recognises
#if MOBILEGL_PIPE_LEGACY_MEMOS, #if !MOBILEGL_BUILD_DISAGGREGATED and the #else of
#if MOBILEGL_BUILD_DISAGGREGATED, which is every spelling these files use, and says
so in the docstring.

Six allow-list entries, each carrying its one-line reason. The two that matter:
StateBackendObjectRegistry's UnorderedMap<StateObject*, Entry> and
ResolvedTextureBindingMemo's type-erased `const void* program` are the PULL build's
only arms - deleting them moves pull .text and fails G1's 0/0/0/0, so they retire
with the pull path (P13), not here. The other four are memo VALUES rather than keys.

Seven negative controls turn red; four positive controls stay green, including a
LEGACY_MEMOS arm, the #else of a DISAGGREGATED arm, the allow-listed type-erased key
and a frontend pointer outside every memo family. Verified once more by injecting a
fresh unguarded ProgramObject* field into the real ResolvedTextureBindingMemo: the
gate reddens and names it.
2026-09-22 17:31:51 -04:00
swung0x48 5617c5c3b3 [MG_IntegrationTest, docs] (P7): the texture upload shape becomes a gate, with the view remap inside the workload
P3b/P4b R-11 / D-D4, landing under the P7 program (PLAN-PH-P34B-P7.md 1.2, wave 2-D
package D2). P4a recorded the emission shape rather than gating it because the
per-storage-owner cursor with view/owner remapping was the next phase's and gating an
unfinished shape pins a non-answer. That work landed, so the RecordProperty block is
now EXPECT_EQ against a gold row MEASURED on this cursor, and the workload carries a
third texture written through a glTextureView so the remap is inside the counted
window.

The gold row, identical on monolith / inproc / spawn / tcp:
  emit=9 box=9 rect=0 jobs=9, client emitter 9
keyed on the workload constants by static_assert. P4a's emit=6 box=6 rect=0 jobs=6
was a two-texture workload with no client emitter and is not comparable.

The client half is read from MGPipeTextureEmitter::SubDataCount() rather than from
ctu=, and that is a correction: under spawn and tcp the two roles are two processes
whose PipeStats windows close on different Present cadences (measured ctu=13 against
emit=9, and usually not yet written when the case reads), so ctu == emit is not a
well-posed comparison there. The log field is kept as a record and asserted only
where the transport says the two are window-aligned - asking the NUMBER was the first
mistake, because the server publishes a readable ctu=0 under spawn and the comparison
silently became 0 == 9. PipeStatsWindow gains LastFromClientLog/LastFromServerLog.

Split/spawn/tcp arms added as explicit per-arm blocks rather than through
mgl_itest_register_split_arms: the macro hands every arm the shared environment, and
this case needs a lane marker, PipeStats period 1, a push mask and above all a
MOBILEGL_LOG_FILE_PATH private to the arm - the library opens the log "w", so two
entries sharing a path truncate each other and the failure looks exactly like "the
counter was never emitted".

Red-once (R-16): the prescribed flip of summedArea*4 >= unionArea*3 leaves the gate
GREEN, and that is a finding. Two gates downstream dominate it - the server needs
dirtyRectCount >= 2, and UnpackRingAvailable() zeroes the rect list by design ("one
box, one job", the existing Mali-cliff mitigation). So rect=0 is the policy's
intended output, not an accident of the scatter pattern. The red-once that bites
splits the union box in two AND disables the unpack ring: box=6 rect=3 jobs=12
against the gold, while 26 pixel cases stay green. Four attempts and their reasons
are in the note.

The Mali frame-time delta D-D4 asks for beside the gold is NOT schedulable - one
Redmi, an Adreno, no Mali part - and is recorded as a declared deviation in the
scenario header and MEASUREMENTS.md 13.3 rather than modelled.

Also corrects the three places that said the client emitter had not landed: the
scenario header, the CMake probe's comment, and gates-v1.md's re-run table.
TextureEmit.h's EmitOneLevel has been incrementing it.
2026-09-22 17:31:51 -04:00
swung0x48 da30c1301e [MG_IntegrationTest, docs] (P7): the owner-keyed emission cursor gets the scenario that can see it
P3b/P4b deepening, landing under the P7 program (PLAN-PH-P34B-P7.md 1.2, wave 2-D
package D2). The code half of "按存储属主键控的发射游标与 view 索引重映射" landed in
P4a/P5e; this is the verification half it never had.

TextureViewAliasScenario, four cases: an upload through a layer-restricted view
sampled through the owner and the reverse, each once and again across draw
boundaries so the drain runs three times. Each case asserts on TWO channels at
once - sampled texels AND MGPipeTextureEmitter's SubDataCount / RefusedSubDataCount
/ DrainListSize, read through the new Harness/TextureEmitPeek. The counters are the
load-bearing half: a level that never made it onto the drain list is still uploaded
by the next whole-level respecify, by a glGenerateMipmap or by the monolith sync
path, so the picture comes out right one frame late and a pixel-only case goes green
over the regression on most trees.

Registered in the three GLES split arms, and MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1
added to MGL_ITEST_GLES_{SPLIT,SPAWN,TCP,TCPDEVICE}_ENVIRONMENT - views are Espryt's
opt-in and without it every case green-skips. The Tcp arm needed the knob on the
TcpServer.Start fixture as well: that lane's server is pre-started rather than
spawned from the client, so it withheld GL_ARB_texture_view and all four cases
SKIPPED while the identical Split and Spawn arms ran.

Red-once (R-16): TextureObjectView::MarkStorageDirtyRegion made to call
PipeNoteLevelDirty on ITSELF instead of forwarding to the storage owner - the
two-cursor shape. TheViewUploadDrainRunsAgainAfterADrawBoundary goes red on both
channels simultaneously (stale owner texels AND SubDataDelta() == 0). The plan
predicted case (a); it is (c) that reds, because the first drain of a texture's life
carries the whole level anyway - recorded in the note.

TheOwnerUploadDrainRunsAgainAfterADrawBoundary is EXCLUDED BY NAME from the split
arms: it is red on all three transports and green under monolith, because an
owner-side resource_subdata moves only the owner's record.Serial and nothing
propagates that to the records viewing it. Cause and the two candidate fix sites are
named in the CMake comment and in notes/p34b/espryt-d2.md. The name stays in every
lane (G14).
2026-09-22 17:31:51 -04:00
swung0x48 ae3f43ab56 [docs] (P7): ID-P7-24 - package D1 landed, and the readback half was already closed 2026-09-22 16:42:34 -04:00
swung0x48 ba81453f19 [MG_Backend, MG_IntegrationTest, docs] (P7): the readback family runs on the split arms, and it was already green
P3b/P4b espryt D1 slice 4, landing under the P7 program (notes/p7/PLAN-PH-P34B-P7.md
§1.2 row "回读 / pack state", §3 wave 2-D).

(a) THE CENSUS, AND IT CAME BACK CLEAN. Twenty-two cases per arm now run on the three split
arms: the two F1 pack-state cases, PixelStoreSweepScenario, FramebufferChurnScenario,
PackedWordReadbackScenario, LayeredTextureReadbackScenario and
DepthStencilReadbackMatrixScenario. The row expected reds and said so ("Expect reds - that
is the point"); the first run was 65/65 before any of this commit's code changes. That is
the finding: the 回读 half was assumed open because nothing had ever run it under a
transport, and the assumption was what was wrong - ScatterTightReadbackIntoPackState and the
server's neutral-pack read were already carrying it. No case needed a fix and no case needed
a named deferral.

DepthStencilReadbackMatrixScenario gets a per-arm block of its own, because the registration
macro cannot carry MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1 and without that pin the
scenario is unfalsifiable here: ES has no core depth/stencil readback, Mesa accepts the
reads anyway, and on llvmpipe every case goes green through a path the device does not have.
Its SeparateDepthAndStencilAttachmentsAreBothReadable SKIPS on all three arms and on monolith
too, naming the driver reason itself - a driver fact, not a transport one.

(b) THE TWO std::abort EMULATIONS, checked rather than assumed. copy-image-shadow-mirror
ALREADY cannot abort: MirrorCopyImageIntoDestinationShadow returns under a transport before
reaching MGPipeUnmigratedEmulation, and has since P5b package i1. get-tex-image-shadow is
retired here, and the reason is that its own premise did not hold: P4a wrote "a split server
holds no shadow to convert", but the level storage this converts IS the server's own, fed by
the staged texture store. When MapMipmapData answers, the conversion reads bytes that exist
on this side. So it converts when the shadow is there and DECLINES BY NAME when it is not
(rule I: MGLOG_E_ONCE + GL_INVALID_OPERATION, destination untouched). std::abort inside a
server answering a client's readback takes the session down for a level it happens not to
hold, and bypasses Session::Fail, which is the funnel the census gate watches. The funnel
keeps its teeth for the other four sites - generate-mipmap-storage,
generate-mipmap-cpu-fallback, generate-mipmap-cpu-filter, texture-remint-pull - which really
do reach into a client address space; fatal_census is unchanged at 79 abort sites because
the abort lives in MGPipeUnmigratedEmulation and that function stays. Stated plainly in the
notes: no case in the armed census reaches this site under a transport (the server answers
GetTextureImage with ReadTextureImageWire, never through the backend's GetTexImage slot), so
the change is defence in depth and has no integration red-once of its own.

(c) THE CASE NOTHING COVERED, and this slice's red-once:
PixelStoreSweepScenario.ReadPixelsIntoAPackBufferHonoursPackStateAndLeavesTheGapsAlone -
glReadPixels into a bound GL_PIXEL_PACK_BUFFER at a non-zero buffer offset with
PACK_ALIGNMENT/ROW_LENGTH/SKIP_ROWS/SKIP_PIXELS all non-default, read back with
glMapBufferRange. It is the only thing that drives EmitTables.cpp's `if (pbo)` branch, the
client servicing the pack PBO itself after the reply lands; the existing sweep case reads
into a client pointer, which is the other half and has unit cover. Every number makes a term
matter: ROW_LENGTH 5 at 4 bpp is 20, which ALIGNMENT 8 rounds up to a 24-byte stride;
SKIP_ROWS 1 and SKIP_PIXELS 2 put the first pixel at image byte 32; the read is issued at
buffer offset 16, so it lands at 48. The whole buffer starts 0xCD and the case asserts both
halves - the rows land where the pack state says, and every other byte is still poison.

Red-once (R-16): dropping just the buffer-offset term from EmitTables.cpp's `start` reds the
three split entries with "readback[at] Which is: '\xCD' (205)" - the destination still poison
because the rows landed 16 bytes early - while the ambient monolith entries pass, since the
driver services its own PBO there. EmitTables.cpp is not part of this commit.

(d) SplitReadbackPackBuffer's comment claimed "The client refuses PACK_BUFFER before
emission". It does not: it KEEPS the PBO and services it after the reply, and a refusal would
have been observable as a GL error. The conclusion (the server must see no pack buffer) was
right and the reason was backwards; rewritten to ID-49's actual split of the work.

Gates: integration-split 225/225 (base 186), integration-spawn 141/141 (base 102),
integration-tcp 144/144, ctest -L unit 2408/2408, integration-magma-split 73/73,
spawn_lane_parity / fatal_census (79 sites, 0 unmarked) / link_ratchet (186, unchanged)
green, G1 .text 0xa52203 with both nm lists byte-identical to ~/w7/p7-before.
2026-09-22 16:41:54 -04:00
swung0x48 834a8917ff [MG_Backend, docs] (P7): a transport with no record arm stops by name instead of dying mid-frame
P3b/P4b espryt D1 slice 5, landing under the P7 program (notes/p7/PLAN-PH-P34B-P7.md
§1.2 row "g_fboTextureSyncList", §3 wave 2-D).

ResolveFramebufferSubsystemArm and its three siblings classify a family's arm as Handles,
Legacy or NoArm and stop only on NoArm. Under an active transport Legacy is not a slower
answer, it is a postponed crash: all four pre-handle arms reach frontend state through
accessors that are BARRIER_PULLED rows (FieldOwnership.def), and under a transport that
state belongs to a client that is no longer parked behind this apply, so the accessor's
honest answer is Fatal{UnmigratedPipeInput, "<field>@<verb>"} on the first verb that reaches
it. That is the audit's own claim about g_fboTextureSyncList - "separation-safe, because the
record arm returns first" - which holds only while the record arm is ARMED, and nothing
checked that.

Each of the four now stops, under #if MOBILEGL_BUILD_DISAGGREGATED, when the transport is
not monolith and the verdict is not Handles, through one shared
StopOnTransportWithoutRecordArm(bit, detail) that formats both causes and hands them to
BufferImpl::StopOnArmlessPipeSubsystem - this file's existing single voice for "the
configuration left no arm to run". A transport with the record arm off IS that condition
with a different cause: the legacy arm is present but unreachable. Reusing the voice keeps
the census's family word and puts the cause in the message rather than in a new word the
census would have to learn.

Said precisely, this is a RESOLVE-TIME stop and not a startup one. The four resolvers are
function-local statics resolved lazily at first use, deliberately - Managers.cpp's own
comment says a stop inside eglMakeCurrent reads to the harness as "no usable GPU" and skips
- so the stop still lands in a scenario body where ctest reports it. What it no longer does
is wait for a verb to dereference the client's state and then blame the accessor.

Red-once (R-16), and it is the safety argument the audit asserted but never ran:
MOBILEGL_PIPE_PUSH=0x1dff (the Handles lane's 0x1fff minus 0x200,
kMGPipeSubsystemFramebuffer) with MOBILEGL_TRANSPORT=inproc, running
ClearThenReadPixelsScenario.ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels. Both
runs abort; what changes is what the abort says. Without the stop:
Fatal{UnmigratedPipeInput, "GetFramebufferBindingSlot@Clear"} - mid-frame, naming an
accessor and a verb, with nothing pointing at the mask. With it:
Fatal{PipeLegacyMemosDisabled, "MOBILEGL_TRANSPORT is not monolith and
kMGPipeSubsystemFramebuffer (bit 9) is clear (or refused), so the record arm is off and the
legacy arm is unreachable: ..."}.

Gates: integration-split 202/202, integration-spawn 118/118, integration-tcp 121/121,
ctest -L unit 2408/2408, integration-magma-split 73/73, spawn_lane_parity / fatal_census (79
sites, 0 unmarked) / link_ratchet (186, unchanged) green, G1 .text 0xa52203 with both nm
lists byte-identical - the four blocks are inside MOBILEGL_BUILD_DISAGGREGATED and the pull
build compiles none of them.
2026-09-22 16:41:54 -04:00
swung0x48 767e6c8084 [MG_Pipe, MG_Test, docs] (P7): the reverse channel is nine callbacks, and the prose says what the tree does
P3b/P4b espryt D1 slice 3, landing under the P7 program (notes/p7/PLAN-PH-P34B-P7.md
§1.2 row "XFB scatter" item 3; CONTRACT-P7.md §139 already records the deletion).

Plan B section 7.1 named ten reverse-channel callbacks. The tenth, OnXfbScatterReady,
belonged to a design the tree does not have: plan B put the XFB scatter on the CLIENT - the
server would hand back the packed scratch and the client would run the patch loop over its
own shadow - so something had to tell the client the layout. P5c/P5f went the other way,
and the declaration was left behind with zero producers, zero consumers and no EventKind of
its own, while the static_assert beside kMGPipeCallbackCount made it look load-bearing.
Deleted; kMGPipeCallbackCount 10 -> 9.

Three pieces of prose that still described the old design go with it. MGPipeCallbacks.h's
own header said "ten callbacks", and now says nine and why the tenth went.
MGPipeTypes.h's MGPXfbAccounting comment said the scatter "is a read-modify-write of the
client's shadow and lives there"; it lives on the server's staged shadow, and nothing about
the scatter crosses in that record. ARCHITECTURE.md §8.5, titled "XFB scatter 搬到 client",
is rewritten to what the tree does: the pre-capture bytes are the server's own staged
shadow (rule C, since MappedData() on the apply thread is Fatal{RoleViolation,
"buffer-legacy-arm"}), the reconciled whole range comes back as one OnBufferWriteback
sliced against SEG_EVENT's capacity (slice 2), and an orphaned target is a legal one (slice
1). Its row in §8.1's callback table goes too - leaving it would be a new staleness rather
than an inherited one.

PipeCatalogue.ReverseChannelHasTenCallbacks now asserts 9. The CASE NAME IS DELIBERATELY
NOT RENAMED: G2/G14 say the ctest name set only ever grows, so renaming would delete a name
the gate watches. The count it asserts is what has to be right and the comment carries the
number.

Red-once (R-16): with the member and the count restored the case fails with
"kMGPipeCallbackCount Which is: 10 / 9u Which is: 9", which is what makes the struct's
shrink a measured fact - the static_assert beside the constant only proves the two agree
with each other.

G1 matters for this slice in particular, because MGPipeCallbacks.h is included from
MG_Backend and the struct is NOT behind MOBILEGL_BUILD_DISAGGREGATED, so
sizeof(gMGPipeCallbacks) really does shrink by 8 bytes in the pull build. The shrink is in
.bss and nothing indexes the table by count: .text is still 0xa52203 and both nm lists are
byte-identical to ~/w7/p7-before.

Gates: integration-split 202/202, integration-spawn 118/118, integration-tcp 121/121,
ctest -L unit 2408/2408, spawn_lane_parity / fatal_census / link_ratchet (186, unchanged)
green.
2026-09-22 16:41:54 -04:00
swung0x48 96fa569250 [MG_Backend, MG_Pipe, MG_Remote, MG_IntegrationTest, docs] (P7): the XFB writeback event is sliced, so a large capture stops killing the server
P3b/P4b espryt D1 slice 2, landing under the P7 program (notes/p7/PLAN-PH-P34B-P7.md
§1.2 row "XFB scatter", §3 wave 2-D).

Both XFB writeback producers posted ONE unbounded OnBufferWriteback covering the whole
bound range - ScatterCapturedRecords from its staged vector, ReadbackCapturedRanges from a
live glMapBufferRange mapping. ServerOnBufferWriteback turns each into one SEG_EVENT record
of sizeof(EventBufferWritebackHead) + bytes, and RingProducer::Reserve refuses any record
above Capacity()/2 outright - not "when full" but always, because above half a capacity a
record is placeable at some head offsets and not at others and waiting for room would be a
hang. EventRingBytes defaults to 256 KiB, so any capture target wider than ~128 KiB was
Fatal{EventRingOverflow} and the SERVER PROCESS DIED, on a buffer size, with nothing else
wrong. The producers' own Fatal text already said what to do: raise MOBILEGL_IPC_EVENT_KB
or slice the event at its producer, "as the writeback path already does" - which was true
of the client's request path and of nothing on this side.

MG_Pipe now carries the one number, published the way gMGPipeSegmentResolver is published:
gMGPipeEventRingCapacityBytes, installed by ServerSession::Accept beside the four reverse
producers and released at Close only if it is still ours, and
MGPipeBufferWritebackSliceBytes() which turns it into a per-record width - a quarter of the
ring with a 4 KiB floor, the same number and the same reasoning as the client-linkage
BufferWritebackSliceBytes one ring over. The backend can reach neither MG_Remote::Client
(wrong linkage) nor ServerSession (wrong layer), so the hook is the seam; 0 means "do not
slice", which is monolith, where the callback is a direct call with no ring under it.

Both producers loop against ONE helper, XfbWritebackSliceStep, because a second spelling of
the width is how one of them ends up still killing the server. The readback site slices
inside its mapping and still unmaps once; the scatter site slices the staged vector and
keeps the single glBufferSubData whole, since the ES store has no ring and cutting it would
change how many backend calls one application call makes. In-order delivery makes the last
slice's landing imply every earlier one, so the client needs nothing new.

Red-once (R-16): XfbAfterClipDistanceScenario.{AScatteredCapture,APlainCapture}
IntoAMegabyteRangeReachesTheApplication - a 1 MiB bound range, about eight times what one
record can ever be, with the same six capture records; glBindBufferBase over the whole
store is what makes the mirrored range a megabyte. Two cases for the two producers. With
the slicing reverted all six split entries go red on Fatal{EventRingOverflow}
("kEventBufferWriteback needs 1048600 bytes and SEG_EVENT caps ONE record at 131072") -
aborting the test process under inproc, losing the peer under spawn and tcp - while both
monolith controls stay green.

XfbAfterClipDistanceScenario and XfbRepeatedCaptureScenario now join the three split arms
in full; slice 1 could register only OrphanedCaptureTarget* because the rest could not
survive the unsliced event. XfbRepeatedCaptureScenario's gl_NextBuffer compacted-target
shape is the first thing outside the census to exercise the archive-served stride and
varyings (ProgramArchive->Link's xfb fields) on the server, and it passed on all three arms
with no further change.

Gates: integration-split 202/202, integration-spawn 118/118, integration-tcp 121/121,
ctest -L unit 2408/2408, the monolith and split XFB families 120/120, spawn_lane_parity /
fatal_census (79 sites, 0 unmarked) / link_ratchet (186, unchanged) green, G1 .text
0xa52203 with both nm lists byte-identical to ~/w7/p7-before.
2026-09-22 16:41:54 -04:00
swung0x48 faf0a1f902 [MG_Backend, MG_IntegrationTest, docs] (P7): an orphaned XFB capture target keeps its varyings
P3b/P4b espryt D1 slice 1, landing under the P7 program (notes/p7/PLAN-PH-P34B-P7.md
§1.2 row "XFB scatter", §3 wave 2-D).

ScatterCapturedRecords is a read-modify-write of the capture destination's pre-capture
bytes - a gl_SkipComponents hole has to come back holding whatever the application left
there - and under a transport those bytes are the server's staged shadow rather than a
frontend MappedData(). The split arm read a null shadow as "this target has no
pre-capture bytes, discard it". A null shadow is not an error: MGPResourceDesc's
HasDefinedContent == 0 is the store the application ORPHANED with glBufferData(size,
NULL), and Managers.cpp's own M-3 comment says every byte it has not staged since is
undefined by its own declaration. So the continue threw a whole capture away for the
ordinary streaming idiom (orphan, upload part of it, capture into it), leaving the
application's buffer holding its pre-draw bytes under GL_NO_ERROR. The partly uploaded
shape did not even reach the continue: it has a shadow, so it asked RequireStagedCoverage
for the whole bound range and died on Fatal{StageSnapshotTooNarrow,
"xfb_scatter_pre_capture"}.

Monolith has always been right here and is right by construction: `Vector<Uint8>
staged(rangeBytes)` is value-initialised, so scattering over an unreadable source scatters
over zeroes and uploads them, which is byte for byte what a fresh MappedData() hands back.
The split arm now skips only the Memcpy, never the target. Discard-with-log is kept for
splitResource == nullptr alone, which really is impossible and has no backendId to upload
into either. RequireStagedCoverage is guarded by ResourceContentIsDeclared, exactly as
Managers.cpp's pool-reuse ladder guards its own whole-store MGL_SERVER_STAGED_REQUIRE: a
declared store still Fatals on a coverage gap, an orphaned one does not.

ResourceContentIsDeclared moves out of Managers.cpp's anonymous namespace to namespace
scope beside RequireStagedCoverage and is declared in Managers.h. The two callers must not
be able to answer "did the application declare these bytes" differently. Both live inside
#if MOBILEGL_BUILD_DISAGGREGATED, so the pull build compiles neither.

Red-once (R-16): XfbAfterClipDistanceScenario.OrphanedCaptureTarget{WithAPartialUpload,
WithNoUploadAtAll}KeepsItsVaryings, registered on the three split arms under the Xfb.
prefix. With the fix reverted the six split entries go red - the partial-upload three on
Fatal{StageSnapshotTooNarrow, "xfb_scatter_pre_capture"} ([0, 696) of a buffer whose
staged coverage does not include it), the whole-orphan three on a captured varying that is
still zero - while the two ambient monolith entries pass, which is what makes it a
transport fact. Details and the lane numbers in docs/Disaggregated/notes/p34b/espryt-d1.md.

The narrow filter is deliberate: the rest of XfbAfterClipDistanceScenario is a DirectVulkan
allocation-pattern regression that has never run under a transport, and it joins the arms
in slice 2 once the writeback event it posts is sliced.

Gates: integration-split 188/188 (186 + 2), integration-spawn 104/104, integration-tcp
107/107, ctest -L unit 2408/2408, the monolith DirectGLES XFB + readback families 66/66,
spawn_lane_parity / fatal_census / link_ratchet (186, unchanged) all green, G1 .text
0xa52203 with both nm lists byte-identical to ~/w7/p7-before.
2026-09-22 16:41:54 -04:00
swung0x48 09ba88e70d [MG_Remote, docs] (P7): CONTRACT-P7 §3.2 records package B (readback race, copy-image, default blit, depth mip bake, mip diagnostic), ID-P7-23 2026-09-22 16:40:02 -04:00
swung0x48 76b7469389 [MG_Backend, MG_IntegrationTest, docs] (P7): the wire readback waits for the frame it is reading, instead of inferring who wrote it
Exit gate 3, OpenRA on the Redmi. The mechanism, in two lines of this tree:

  WireFramebuffer.inc, ReadWirePixels: `if (HasPendingRecordedWork() &&
  !FlushPendingCommands())` - FlushPendingCommands (VulkanRenderer.cpp:13534-13598)
  puts the frame's draws on the queue under a POOLED FENCE OF ITS OWN and returns
  without waiting; there is no vkWaitForFences on its success path. The
  vkCmdCopyImageToBuffer that follows is recorded into a fresh command buffer and
  becomes a LATER submission.

  VulkanRenderer.cpp:10707-10748, SubmitReadbackCommandsAndWait: it waited ONE fence,
  its own, and then called OnSubmitsCompletedUpTo(frame.lastSubmitIndex) - which
  RECORDED the earlier submission as complete and retired its fence, so afterwards
  nothing could observe that it had not been waited for. The comment beneath it said
  "the wait proved every submission complete". That is true of the monolith arm, which
  never flushes before a readback and therefore has its draws and its copy in one
  submission under one fence. It was not true of the wire arm.

The only thing tying the two submissions together was the pipeline barrier that the
readback's TransitionWireImage records - and that barrier derives its SOURCE scope from
the image's tracked layout, through GetImageTransitionSourceState
(VulkanRenderer.cpp:1784-1825). Two of its arms, VK_IMAGE_LAYOUT_UNDEFINED and
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR (:1787-1791), return VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT
with no access bits, which is Vulkan's spelling of an EMPTY first scope: wait for
nothing. A just-presented default framebuffer is in the second of those layouts. The
slower pipeline creation is, the longer the draws' submission runs, and the more of the
frame's LAST-drawn objects the copy can miss - which is what the device's diff image
shows.

Measured on this host (OpenRA, DirectVulkan, inproc), at the moment the copy is
recorded: submit=72 completed=71. The submission carrying the whole frame's draws is in
flight and unwaited. That is structural, not a coin flip, and it reproduces every run.

The fix is two halves, both wire-only:

 - RecordWireReadbackWriteBarrier (WireFramebuffer.inc, new): the readback records its
   own ALL_COMMANDS -> TRANSFER, MEMORY_WRITE -> TRANSFER_READ|MEMORY_READ barrier
   before the copy rather than inferring from a layout who it should wait for. A
   barrier's first scope is every command earlier in SUBMISSION order on the queue, so
   it reaches across the submission boundary.
 - SubmitReadbackCommandsAndWait now waits every in-flight fence at or below its own
   submission index. Under #if MOBILEGL_BUILD_DISAGGREGATED: the pull build has no wire
   arm, cannot flush before a readback, and is the image G1 pins - its single-fence wait
   is unchanged. The cost is nothing a readback does not already pay.

THE COORDINATOR'S REPRODUCTION PATH DOES NOT WORK ON THIS HOST, and that is recorded
rather than glossed. MOBILEGL_TEST_PIPELINE_CREATE_DELAY_MS is added as asked
(split-only, inert unless set, on the pipeline-creation MISS path in
GetOrCreatePipelineWithInput) and it does bite - the OpenRA DirectVulkan SPLIT retrace
goes from 1.88 s to 4.53 s at 25 ms - but ssim stays 1.000000. The reason is in the
probe above: this host's default framebuffer is tracked as VK_IMAGE_LAYOUT_GENERAL at
the readback, which takes GetImageTransitionSourceState's `default:` arm (ALL_COMMANDS /
MEMORY_WRITE) and orders the copy by accident. The device's is not GENERAL.

Red once (executed, reverted) is therefore a COUNT, not a picture, and it is
deterministic: instrumenting how many fences the readback actually waits on gives
fences=2 upTo=73 completed=71 with the fix (submission 72, the draws, plus its own 73)
and fences=1 without it, with completed=71 either way - i.e. 72 provably outstanding and
provably unwaited before.

Lane: three readback-after-work cases (VertexIdSamplerAndScalarUniformPixels,
ComputeImageStoreFramebufferPixels, GenerateMipmapPixels) registered on the three Magma
arms under MOBILEGL_TEST_PIPELINE_CREATE_DELAY_MS=25, prefix .PipelineDelay. They are a
REGRESSION GATE, not the reproduction: they pin that a readback still returns the draw's
pixels when every pipeline creation on the path costs 25 ms.

Gates: integration-magma-split 84 (+3), -spawn 63 (+3), -tcp 65 (+3), 0 red;
ctest -L unit 2410; informational integration-magma-full-split 516, 0 red, 59 skip;
fatal_census green; link_ratchet --assert-monotone unchanged at 186, p7-magma 101;
G1 pull .text 0xa52203, defined-symbol name list identical (30570); OpenRA DirectVulkan
SPLIT retrace ssim 1.000000 with and without the delay, two runs each.

Owed to the device: the three cold-cache repeats. If they still diverge, the next two
suspects are named in notes/p7/magma-b.md §5.6 - the eleven silent `return false` drop
sites in SetupWireDraw (one of which is `pipeline == VK_NULL_HANDLE`, on the same path
as the device log's three "handing vkCreateShaderModule an INVALID module" errors), and
SetImageContentDefined being set BEFORE the draw is recorded on the wire arm while the
monolith sets it from EndRenderPass, which silences the "no write is recorded" warning
exactly when it should fire. Neither is in this commit's scope.
2026-09-22 16:37:54 -04:00
swung0x48 583e12de76 [MG_Backend, MG_IntegrationTest, docs] (P7): the default-framebuffer blit stops refusing shapes the arm beneath it can do, and the 3D bug that hid behind the refusal is fixed
CONTRACT-P7 §3.2's `default-color-blit-shape@P7`, retired in three pieces, because one
Fatal was covering three unrelated situations.

The shader pass it guarded is there for ROTATION and nothing else: it can only SAMPLE
its source - sampler2D, a float result, one of two 2D view types - so an RGBA8UI
attachment, a 3D slice or an array layer made it return false. The refusal then fired
EVEN ON A SURFACE WITH NO ROTATION, where the ordinary vkCmdBlitImage arm twenty lines
below would have done the blit and does handle those shapes.

 - identity: fall through to that arm;
 - a non-identity rotation with a source the pass cannot sample: move the region into an
   owned 2D single-sample image of a sampled-float format in the SAME size-compatibility
   class and shader-blit that;
 - outside the four rotations (a mirror, or a mirror-rotation): decline by name, saying
   which transform. Neither arm can express a reflection and this package has no device
   that reports one to check a result against.

Reinterpretation is the point for the integer case and GL agrees: glBlitFramebuffer
between an integer and a normalized format is INVALID_OPERATION (4.6 core 18.3.1), so
there is no defined result to be wrong about - only a shape that used to end the session
and now produces the source's bits. For a float source of the wrong view type the copy
is a plain slice extraction and the result is exactly right.

THE RETIREMENT UNCOVERED A REAL BUG, which is the part worth reading. The ordinary blit
arm addressed a 3D source as `{aspect, level, source.layer, 1}` with z 0..1. A 3D image's
slice rides Z; its only subresource is layer 0 - which is how the copy endpoint's
CopyImageSliceMapping and the multisample resolve arm above already address one. So a
blit from slice 1 of a 3D texture read slice 0, out of a layer the image does not have.
It was invisible because that shape hit the Fatal first and never reached the arm: the
new scenario's first run had the scratch arm correct and the fall-through arm wrong in
all four quadrants, which is how it fell out. Both arms agree now.

The scenario's source is slice 1 of a 3D texture - ordinary GL, and exactly the shape the
shader pass cannot sample - with slice 0 filled in a colour that appears in no quadrant,
so a scratch copy that forgot the z origin would still be reading real texels and had to
be made distinguishable. It is registered twice on each arm: plain, taking the identity
fall-through, and under MGITEST_MAGMA_FORCE_DEFAULT_BLIT_SCRATCH, taking the rotated
arm's scratch path on an identity surface, where it must produce THE SAME PIXELS. That
equality is the assertion, and it is the only way the scratch arm is reachable from a
host lane at all: every surface this lane can create reports
VK_SURFACE_TRANSFORM_IDENTITY, as does the Redmi pbuffer this cluster's evidence comes
from. The knob does not fake a rotation - nothing downstream is told the surface turned.

Red once (executed, reverted): with MagmaWireFatal("default-color-blit-shape@P7")
restored, both entries die by that name; without it, 2/2 green.

Gates: integration-magma-split 81 (+2), -spawn 60 (+2), -tcp 62 (+2), 0 red;
ctest -L unit 2410; informational integration-magma-full-split 516, 0 red, 59 skip;
fatal_census green; link_ratchet --assert-monotone unchanged at 186, p7-magma 101;
G1 pull .text 0xa52203, defined-symbol name list identical (30570).

Owed to a device window: the scratch arm's geometry under a REAL ROTATE_90/180/270. The
knob proves it produces the fall-through's pixels on an identity surface, which exercises
every line of it except the rotation constant the vertex shader applies.

WireColorBlit.inc and VulkanRenderer.h are edited outside this package's named file list.
Both belong to this cluster (the colour blit's implementation, and four private
declarations beside the existing blit ones); flagged for the integrator.
2026-09-22 16:37:54 -04:00
swung0x48 4f857aa5c6 [MG_Backend, MG_IntegrationTest, docs] (P7): a same-image copy takes one GENERAL transition instead of the session
CONTRACT-P7 §3.2's `copy-image-in-place@P7`, retired.

glCopyImageSubData between two subresources of ONE texture is ordinary, defined GL -
4.6 core 18.3.2 spells out that a texture and a view of it are two objects over one
image and may be copied between. The wire arm took the session down for it, and the
reason it gave was true of the code beneath it and of nothing else: the
TRANSFER_SRC/TRANSFER_DST pair cannot describe one image, because both endpoints share
one tracked VkImageLayout and the second transition undoes the first.

VK_IMAGE_LAYOUT_GENERAL can describe it - both sides of vkCmdCopyImage accept it. So a
same-image copy now takes ONE transition of the whole image to GENERAL (the source level
and the destination level are both inside it), GENERAL on both sides of the command, and
ONE restore, with an access mask carrying both directions because that one barrier is
the edge for both. The monolith arm still declines every in-place copy a few lines up,
so the wire arm is the one that performs this - which is what §3.2 asks for rather than
parity. `inPlace` is constexpr false in the pull build so every branch it guards folds:
this function is in the monolith image and G1 pins that image's .text.

The one shape that stays a decline is same image + same level + overlapping slices +
overlapping rectangle, all three at once. GL leaves that undefined, and GENERAL makes
the command legal to RECORD rather than meaningful to execute - vkCmdCopyImage orders
nothing between its own reads and writes inside a region, so a tiler produces whichever
half of each texel it reached first. Level 0 to level 1, disjoint layers, and disjoint
rectangles of one level are all defined, and all performed.

Two split-only cases on the three Magma arms, over one 8x8x2 two-level array texture
whose every touched subresource is a different colour - a flat fill would let "copied to
the wrong level", "copied to the wrong layer" and "did not copy" all read as correct.
CopyImageInPlaceAcrossLevelsAndLayers does level 0 -> level 1 and then layer 0 -> layer 1
and checks the untouched subresources too; CopyImageInPlaceOverlapDeclinesAndLeavesTheLevelAlone
copies (0,0)-(4,4) onto (2,2) and pins every texel unchanged - an executed copy would
turn the texel at (4,4) from yellow to red, so the assertion can tell.

Red once (executed, reverted): with MagmaWireFatal("copy-image-in-place@P7") restored,
both entries die with Fatal{UnmigratedVerb, "Magma:copy-image-in-place@P7"}; without it,
2/2 green.

Gates: integration-magma-split 79 (+2), -spawn 58 (+2), -tcp 60 (+2), 0 red;
ctest -L unit 2410; informational integration-magma-full-split 515, 0 red, 59 skip;
fatal_census green (79 sites / 43 words / 0 unmarked); link_ratchet --assert-monotone
unchanged at 186, p7-magma 101; G1 pull .text 0xa52203 and the defined-symbol name list
identical (30570) - the constexpr-false branches fold as intended.

This tree's integration TCP endpoint moved to 127.0.0.1:40913: a sibling worktree's
libMobileGLServer was already listening on pipe's default 40613 and took the whole tcp
lane down at TcpServer.Start. It is a local configure option, not a lane semantic.
2026-09-22 16:37:54 -04:00
swung0x48 fdd611e83d [MG_Backend, MG_Test, MG_IntegrationTest, scripts, docs] (P7): the depth mip chain is baked, and every baked header in the tree now has to match its source
CONTRACT-P7 §5.1's (A) and (D), and (D) is what makes (A) safe to have.

(A) WireDepthMipmap.{vert,frag,inc}. VK_FORMAT_FEATURE_BLIT_DST is OPTIONAL for depth
formats, so a depth chain through a shader is the ordinary case and not the exotic
one - the monolith arm has carried such a fallback since before the split. The wire
arm had none, so a depth texture the device would not blit died on the first
disjunction of `mipmap-shader-format-or-shape`, and a DEPTH|STENCIL one died a guard
earlier as `depth-stencil-mipmap@P7` whether or not it could be blitted.

The monolith's fallback is not reusable, which is the whole of (A): it builds a GL
ProgramObject, two ShaderObjects and a GL sampler, binds through the uniform manager's
global UBO and writes uSrcTexelSize as a default-block uniform - four frontend objects
and a GL compiler on the server's apply thread. The baked program is the same shader
with the same arithmetic on a push constant and its own descriptor set. The fragment
stage is a PORT and not a rewrite: §3.2 retires a refusal by making the wire arm
observably the monolith's, so the 2x2 box lands where the monolith's lands, including
the half texel its ivec2(texCoord * srcTexelSize) sits off the destination centre.

`depth-stencil-mipmap@P7` therefore splits. DEPTH-only retires into the baked program.
DEPTH|STENCIL declines - MGLOG_E_ONCE and return, before the chain is grown, so a level
this verb will not fill is not created and then recorded as defined. A decline rather
than a Fatal because the monolith does no better: its fallback asserts depth-only, and
an assert is nothing in the release build both arms ship as. Nothing in GL promises a
stencil mip either, and this pass writes gl_FragDepth - it would leave the stencil
aspect of the level it just produced undefined.

(D) MOBILEGL_BAKED_INTERNAL_SHADERS. A baked header is a compiler output committed with
its input beside it and nothing connecting the two: edit the source and the build stays
green while the shader that runs is the one from before the edit. MG_Test's
BakedInternalShadersTest recompiles all eight arrays - WireColorBlit x2,
WireDepthMipmap x2, the iterationRP witness, the three probe stages - from their
committed GLSL with the in-tree glslang and compares word for word.

THE GATE IS THE BAKER. scripts/bake_internal_shaders.py compiles nothing; it runs this
binary with MOBILEGL_BAKE_INTERNAL_SHADERS and splices the words it emits. A gate that
used the glslang LIBRARY while the baker shelled out to a glslangValidator BINARY would
be two compilers with no rule about which is right, and the day their defaults diverge
the gate reds on a header nobody can regenerate.

What the first run measured, which is the one thing nobody could know beforehand:
WireColorBlitSpirv.h and all three arrays of PrimitivesGeneratedNoXfbProbeSpv.h were
already fresh - the hand-run glslangValidator words reproduce exactly. The iterationRP
witness was stale, as its own prose said; it is regenerated here (2142 -> 2143 words),
and the witness magic 0x50323033 is still there because it is a constant in the .comp
rather than a property of the words. All three provenance comments now point at the
script and the gate instead of at a tool on somebody's PATH.

Scenarios, on the three Magma arms: GenerateMipmapDepthWithoutNativeBlitPixels runs
DEPTH_COMPONENT24 and DEPTH_COMPONENT32F under MGITEST_MAGMA_FORCE_SHADER_MIPMAP=1 and
reads level one back through a real sampled attachment, asserting per half and leaving
row 3 - the straddle band - unpinned, because pinning it would assert the half-texel
offset rather than the mip. GenerateMipmapDepthStencilDeclinesAndKeepsTheSession takes
no knob (the decline is on the aspect, before blit support is asked about) and pins that
the session survives and level zero still holds what was cleared into it.

Red once, three of them, executed and reverted:
 - a one-character edit to WireDepthMipmap.frag (no rebuild - the gate reads the source
   at runtime) reds the WireDepthMipmapFragment row at word 279;
 - with the depth arm disabled, the DepthMip entries die with Fatal{UnmigratedVerb,
   "Magma:mipmap-shader-format-or-shape format=125 ... aspect=0x2 ...
   missing=COLOR_ATTACHMENT"} - format 125 is X8_D24_UNORM_PACK32, which slice 1's
   diagnostic names for us;
 - with the Fatal restored, the decline case dies as
   Fatal{UnmigratedVerb, "Magma:depth-stencil-mipmap@P7"}.

Gates: integration-magma-split 77 (+2), -spawn 56 (+2), -tcp 58 (+2), 0 red;
ctest -L unit 2410 (+8, the gate's rows); informational integration-magma-full-split
513, 0 red, 59 skip (base 510/0/57); fatal_census green (79 abort sites, 43 words, 0
unmarked); link_ratchet --assert-monotone unchanged at 186, p7-magma 101.

G1 holds and it was not obvious: the witness header is compiled into the pull build
too, and its word count changed. Pull .text is still 0xa52203 and the defined-symbol
name list is identical to ~/w7/p7-before/pull-syms.txt line for line (30570) - the
change lands in .rodata and widens no instruction encoding.

VulkanRenderer.h is edited outside this package's file list: the new pass needs four
private declarations next to the colour blit's. Flagged for the integrator.
2026-09-22 16:37:54 -04:00
swung0x48 c4be38005a [MG_Backend, MG_IntegrationTest, docs] (P7): the shader mip arm names the format it refused and renders through scratch when the image cannot be attached
Two Iris cases died on the Redmi (Adreno 830v2, DirectVulkan inproc) with
Fatal{UnmigratedVerb, "Magma:mipmap-shader-format-or-shape"}, deterministically and
under both run-ahead and lockstep, while the monolith arm and the whole host lane
stayed green. That one line was the entire evidence, and five disjunctions share the
name, so the one thing the device could have told us - which format, at what shape,
missing which feature bit - was the one thing it did not.

The name stays first and unprefixed, because the census, the retrace refusal tally
and every log grep key on the leading word; the driver's own answer follows it:
format enum, the view's storage format when it reinterprets, extent, levels, aspect,
view type, usage bits, layer range, and the names of the optimalTilingFeatures bits
that are ABSENT. The next device run reads a line instead of re-deriving it.

The refusal also loses half its territory. The shader arm rendered STRAIGHT INTO the
next level, which needs the image to carry COLOR_ATTACHMENT usage - and a texture
whose format the device declines as a colour attachment never gets that bit, so every
sampled-only texture without BLIT support fell into the refusal rather than into a
fallback. When the FORMAT can be rendered and only the IMAGE cannot, an owned scratch
attachment of the destination level's size takes the pass and vkCmdCopyImage moves the
mip in: a raw texel move, no conversion, texels still from GPU contents rather than a
staged shadow, and the scratch retires with the submit that consumes it - the shape the
multisample resolve arm above it already uses.

Neither shader arm had ever executed on the gate machine. lavapipe reports
BLIT_SRC|BLIT_DST for every colour format this binary can create, so GenerateWireMipmap
takes its native arm for all of them; GenerateMipmapPackedFloatPixels next door looks
like it covers the shader arm and does not, because R11F_G11F_B10F blits natively here
too. MGITEST_MAGMA_FORCE_SHADER_MIPMAP drops the native arm instead, one tier per shape
(1 = render into the level, 2 = render into scratch and copy back), read once and
compiled only into the disaggregated build.

F1WireScenario.GenerateMipmapWithoutNativeBlitPixels paints two different halves rather
than one flat colour - a pass that loses GL's row order averages to the same number
everywhere, and a single-colour assertion would call that correct - and asserts level
one per half. It is registered on the three Magma arms twice, ONE ENVIRONMENT EACH: the
knob changes how every mipmap in the process is generated, so a lane-wide flip would
silently retarget the four green mipmap entries beside it.

Red once (executed, reverted): with the scratch arm forced off, the tier-2 entry dies
with Fatal{UnmigratedVerb, "Magma:mipmap-shader-format-or-shape format=37 extent=8x8x1
levels=4 aspect=0x1 view=1 usage=0x17 layers=0+1/1 missing=DEPTH_STENCIL_ATTACHMENT"},
by name and with the shape in it; restored, the same filter is 8/8 green.

Gates: integration-magma-split 75 (+2), -spawn 54 (+2), -tcp 56 (+2), 0 red;
ctest -L unit 2402/2402; informational integration-magma-full-split 511, 0 red, 58 skip
(the new case skips where the knob is unset); fatal_census green (79 abort sites, 43
family words, 0 unmarked - no site added, the funnel is unchanged); link_ratchet
--assert-monotone unchanged at 186, p7-magma bucket 101.
2026-09-22 16:37:54 -04:00
swung0x48 5937b4fcf2 [MG_Remote, docs] (P7): CONTRACT-P7 §3.2 records package A (byte-tail retired, ranges clamped, alignment declines), ID-P7-21/22 2026-09-22 16:23:32 -04:00
swung0x48 2207b6d9ee [docs, MG_Backend, MG_IntegrationTest] (P7): package A's eight refusals are on record, and only real sites still carry @P7
notes/p7/magma-a.md: the per-site table (retired / declined / kept-fatal with the
reason), every red-once with its captured output, the gate readings, and what is
handed on.

The three places package A's own prose still named `uniform-buffer-byte-tail@P7`
lose the marker. They are comments, not refusal sites, and leaving the marker in
would have made a tree-wide `grep @P7` count them against packages B and C. The
census now reads 15 -> 7, and all seven are real: WireFramebuffer.inc x4 and
VulkanRenderer.cpp x1 (package B), WireDraw.inc x2 (package C).

Three findings worth the integrator's attention, all in the note:

1. Most of this cluster was unreachable, and that is provable rather than
   assumed. The frontend validates buffer-range offsets against the very caps the
   backend then re-checks - GL_{UNIFORM,SHADER_STORAGE,TEXTURE}_BUFFER_OFFSET_ALIGNMENT
   and GL_MAX_UNIFORM_BLOCK_SIZE are all published from the same Vulkan limits -
   so five of the eight sites could only ever have fired on a malformed peer. The
   two that CAN fire are `uniform-buffer-byte-tail` (GL constrains a range's
   offset but not its size) and `unaligned-atomic-buffer-range` (GL gives
   GL_ATOMIC_COUNTER_BUFFER no queryable alignment, so the frontend only enforces
   offset % 4 while the backend needs minStorageBufferOffsetAlignment).

2. fatal_census does not move for any of this, and no re-baseline is due. Wave 0
   already routed these funnels through MGPipeSessionFailHook, so the abort lives
   in FatalFunnel.cpp and retiring a WireDescriptorFatal call site changes no
   census number: 79 abort sites / 43 family words, unchanged, RC=0. The brief
   expected the count to fall because the sites were bare aborts before wave 0.

3. DirectVulkan.Split.Full.IterationRPProgram203Scenario.FixedCompleteInputProducesFixedCompleteGoldenOutput
   is red on this worktree's build-split and is NOT ours: it reproduces 3/3 with
   package A's backend diff fully reverted, and the mismatch bits are identical
   to the S1 exposure texel in magma-two-process-first-run.md. The contract books
   that one against the tcp arm, and ~/w7/pipe's own build-split passes it, so
   whether it is a split-arm regression between 460a3a72 and 610fa281 or a
   configure-dependent S1 needs the integrator.

Gates on the finished branch: integration-magma-split 75/75, -spawn 54/54, -tcp
56/56, -buffers 21/21, full-split 512 entries 454 PASS / 57 SKIP / 1 red (the
pre-existing one above; base was 453/57), ctest -L unit 2408/2408,
spawn_lane_parity RC=0, link_ratchet unchanged at 186, fatal_census RC=0, and G1
pull .text = a52203 with nm --defined-only identical to p7-before/pull-syms.txt
(added=0 removed=0).
2026-09-22 16:22:40 -04:00
swung0x48 a2d4bc9db6 [MG_Backend] (P7): an unsupported texel-buffer format reads zeros instead of killing the session
`texel-buffer-native-format@P7` becomes a decline (rule I (a)), and with it the
last refusal site in UniformManager.cpp is gone. GL's buffer-texture format list
is wider than what Vulkan mandates for texel buffers - GL_RGB32{F,I,UI} and the
16-bit UNORM members are the realistic misses on a real driver - so this is a
device capability gap, not anything the application did wrong and nothing it
could have sent differently.

The placeholder is the right answer here where it was not for the unaligned range
in 7c373d60, and the difference is whether a correct answer exists at all. There
the bytes are addressable and only the descriptor cannot name them, so a
placeholder would have hidden a fixable gap behind silently dropped writes. Here
the device cannot represent this format as a texel buffer in any form, so the
honest observable is monolith's for a missing view: the fetch reads zeros and the
draw survives. AcquireUnboundTexelBufferView already falls back to the R32 member
of the shader's numeric class, whose texel-buffer support is mandatory, so the
fallback cannot itself fail for want of device features.

red-once (R-16). lavapipe supports every format the buffer-texture cases use, so
the site is not reachable on the host - the red is the same case with the device
support check forced to fail, both shapes built and run back to back:

  DirectVulkan.{Split,Spawn}.Full.BufferTextureScenario.VertexStageTexelFetchReadsTheBufferAndTracksItsUpdates
    unforced  2/2 Passed (the format IS supported here)
    before (7c373d60 + forced)  Subprocess aborted -
                                Fatal{UnmigratedVerb, "Magma:texel-buffer-native-format@P7"}
    after  (this commit + forced)  ***Failed on the texel data and NOT on a Fatal:
                                the fetch read the zero placeholder, the session lived
    restored  2/2 Passed, no force markers left

Which format actually trips this is a device question, not a host one. Adreno is
where GL_RGB32F buffer textures are expected to miss, so the decline's real
observable wants a device pass; notes/p7/magma-a.md lists it.

Lanes: integration-magma-split 75/75, -spawn 54/54, -tcp 56/56,
integration-magma-buffers 21/21. `grep -n '@P7'` on this file now returns one
line, and it is the comment that records the A.1 retirement - no refusal sites
remain.
2026-09-22 16:22:40 -04:00
swung0x48 ae57d01168 [MG_Backend, MG_IntegrationTest] (P7): an unaligned counter range declines its binding instead of killing the session
`unaligned-atomic-buffer-range@P7`, `unaligned-storage-buffer-range@P7` and
`unaligned-texel-buffer-range@P7` become named declines (rule I (a)). The two
offset-overflow guards next to them keep their named Fatal under their own words:
those are arithmetic that cannot happen, not shapes an application can bind.

The atomic-counter one is the only member of this cluster an application can
actually reach, and it is a real live gap, not a hypothetical. GL 4.6 core 6.1.1
gives GL_ATOMIC_COUNTER_BUFFER no queryable offset alignment, so
ValidateBufferRangeOffsetAndSize only enforces offset % 4 on it - while glslang
lowers every atomic_uint onto a synthesized storage block whose descriptor offset
must be a multiple of minStorageBufferOffsetAlignment (16 on lavapipe, 64 on
Adreno). `glBindBufferRange(GL_ATOMIC_COUNTER_BUFFER, 0, buf, 4, 4)` is a legal
GL bind that this backend cannot express, and it used to take the session with
it. The plain SSBO and texel halves are unreachable by comparison:
GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT and GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT
are published from the same Vulkan limits, so the frontend already answers
GL_INVALID_VALUE.

DECLINED RATHER THAN COPIED, and this is the package's one real design choice.
Copying the window into an aligned transient slice reads correctly and then
silently DROPS every shader write. This site cannot tell a writable storage block
from a read-only one - there is no readonly bit in the storage-block reflection,
an atomic counter is written by definition, and MarkWireBufferGpuWritten right
below already assumes any binding here may be written. A correct copy needs a
post-dispatch copy-back from the transient slice to the unaligned source, ordered
with a barrier, and the place to record that is the draw/dispatch tail in
VulkanRenderer.cpp / WireDraw.inc - wave-2 packages B and C, not this file.
Returning stale counters with no error is worse than losing the dispatch with a
named line in the role log. notes/p7/magma-a.md carries the recipe for whoever
holds those files.

The texel half keeps the draw where it safely can: a read-only buffer texture
falls back to the same placeholder view an unbound one gets (zeros, which is what
a missing view gives in monolith), while a writable imageBuffer declines - the
placeholder is shared by every unbound binding, so absorbing real stores into it
would alias other bindings as well as lose the writes. Copying is not available
there either: the transient arena carries no {UNIFORM,STORAGE}_TEXEL_BUFFER
usage, so no VkBufferView can be made over it, and widening a shared per-frame
allocation to buy an unreachable path is the wrong trade.

red-once (R-16), a reachable case this time, all three Magma two-process arms:

    DirectVulkan.{Split,Spawn,Tcp}.Buffers.F1WireScenario.UnalignedAtomicCounterRangeDeclinesAndTheSessionLives
      before  Subprocess aborted - Fatal{UnmigratedVerb, "Magma:unaligned-atomic-buffer-range@P7"}
      after   3/3 Passed, and the role log carries
              "block 'gl_AtomicCounterBlock_0' is bound at offset 4, which is not a
               multiple of this device's minStorageBufferOffsetAlignment (16);
               atomic-counter ranges cannot be expressed as a descriptor ... DECLINED
               (unaligned-atomic-buffer-range)"

The case pins the decline's whole observable: the bind raises no GL error, the
dispatch does not run, every byte of the counter buffer is unchanged, and the
context still serves ordinary work afterwards. A future copy-back turns one
EXPECT into seed[1] + 1 and leaves the rest standing.

Lanes: integration-magma-split 75/75, -spawn 54/54, -tcp 56/56,
integration-magma-buffers 21/21.
2026-09-22 16:22:40 -04:00
swung0x48 de049f9ea0 [MG_Backend] (P7): a range past the device's native limit is clamped, not refused
`uniform-block-native-range@P7` and `storage-buffer-native-range@P7` both become
clamps with an MGLOG_E_ONCE (rule I (a)). Neither is reachable from a conformant
program: GL_MAX_UNIFORM_BLOCK_SIZE is published straight from the same
maxUniformBufferRange and GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT /
GL_MAX_SHADER_STORAGE_BLOCK_SIZE from the same storage limits
(BackendLoaders/Vulkan/Loader.cpp), so a block this big fails to link long before
a draw. Refusing was a Fatal guarding a shape nothing can produce.

The monolith arm is the argument. It binds whatever the GL range asked for and
never consults maxStorageBufferRange at all (ResolveStorageBufferDescriptor's
resident path), so the wire arm was inventing a death the other backend does not
have. If the two limits ever disagree, the honest answer is the bytes the device
CAN name: everything past the clamp is outside the descriptor and reads back as
zero under robustBufferAccess, which is inside GL's "undefined" for a block the
implementation never promised to hold. Clamping keeps the range constant across
draws, so the descriptor-set reuse hash is unaffected.

red-once (R-16). Neither site is reachable from a test, so the red is the same
two cases with the device limit forced down to 16 bytes, both shapes built and
run back to back on the Split and Spawn arms:

  uniform-block-native-range (reflected block 32, limit forced to 16)
    before  Subprocess aborted - Fatal{UnmigratedVerb, "Magma:uniform-block-native-range@P7"}
    after   BOTH ARMS PASSED. The clamp keeps every byte the descriptor can name,
            so the case's own pixel is unchanged, and the role log carries
            "reflected uniform block 0 is 32 bytes, past this device's
             maxUniformBufferRange of 16; the binding is clamped to the limit
             and the remainder reads zero"

  storage-buffer-native-range (block 'Output' 24 bytes, limit forced to 16)
    before  Subprocess aborted - Fatal{UnmigratedVerb, "Magma:storage-buffer-native-range@P7"}
    after   ***Failed on the data and NOT on a Fatal - the session lives, 8 of the
            24 bytes are genuinely past what the forced limit can name, and the
            role log carries "block 'Output' bound 24 bytes, past this device's
            maxStorageBufferRange of 16; the binding is clamped to the limit"

  restored (this commit as committed): 4/4 green, no force markers left.

Lanes: integration-magma-split 74/74, -spawn 53/53, -tcp 55/55,
integration-magma-buffers 20/20.
2026-09-22 16:22:40 -04:00
swung0x48 362b27df8c [MG_Backend] (P7): a transient ring past 4 GiB declines its uniform binding instead of killing the session
`uniform-buffer-dynamic-offset@P7` becomes a decline (rule I (a)). A Vulkan
dynamic offset is a uint32_t, so a transient uniform ring that grew past 4 GiB
inside one frame has nowhere to put the padded block - which is our ring's
problem, not a protocol fault, and nothing the peer could have sent differently.
Killing the session for it was the wrong shape twice over.

The monolith arm has the same ceiling and truncates silently on the way out
(ResolveDynamicUboDescriptor's static_cast<Uint32>), i.e. it binds a WRONG window
rather than none. Declining is the strictly better observable of the two: one
binding is skipped, the draw is lost, the session keeps running, and the next
frame's rewound ring resolves it.

red-once (R-16). The site is not reachable from a test - a 4 GiB transient arena
inside one frame is not something an integration case can build - so the red is
the same case with the ceiling condition forced true, both shapes built and run
back to back on the Split and Spawn arms:

  before (fb25ed6b + forced condition)
    DirectVulkan.Split.Buffers.F1WireScenario.ShortUniformBufferRangePadsMissingBytes
    DirectVulkan.Spawn.Buffers.F1WireScenario.ShortUniformBufferRangePadsMissingBytes
        Subprocess aborted - MGPipe: Fatal{UnmigratedVerb, "Magma:uniform-buffer-dynamic-offset@P7"}

  after (this commit + forced condition)
    both ***Failed on the pixel, NO Fatal and NO abort, and the role log carries
    ResolveWireUniformBufferPayload: the transient uniform ring reached offset 0,
    past the 4 GiB a Vulkan dynamic offset can name; uniform binding point 3 is
    declined for this draw

  restored (this commit as committed): both green, no force markers left.

Lanes: integration-magma-split 74/74, -spawn 53/53, integration-magma-buffers
20/20. -tcp not read this round: port 40613 is held by a concurrently running
lane in another worktree, which is a fixture collision and not a result.
2026-09-22 16:22:40 -04:00
swung0x48 450d30759e [MG_Backend, MG_IntegrationTest] (P7): a uniform range that ends mid-word is copied, not refused
`uniform-buffer-byte-tail@P7` is retired. It was the live boundary the Redmi
Minecraft run reported (notes/p5f/magma-inproc-fix.md #5) and, since wave 0, a
named Fatal that took the session with it.

Only the SIZE of a uniform range can land off a word. glBindBufferRange already
forces the offset onto GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT and records
GL_INVALID_VALUE otherwise (GL_Buffer.cpp ValidateBufferRangeOffsetAndSize), and
every device reports that as a multiple of four - but GL 4.6 core 6.1.1 puts no
such rule on size, so a 19-byte bind is legal and 19 % 4 is not 0. The sub-word
start is only reachable protocol-side; the new arm handles it anyway rather than
leaving a second shape to trip over later.

CopyWireBufferSubWordRangeToSlice rounds the read of the APPLICATION's store OUT
to whole words, clamped to the store's end so the widened window can never touch
a byte the application does not own, and lands it in a transient staging slice.
Only the staging -> padded-block shift is sub-word, and that happens inside our
own arena; vkCmdCopyBuffer places no alignment rule on a region's offsets or size
(unlike vkCmdUpdateBuffer / vkCmdFillBuffer), so the shift is a plain legal copy.
The block still starts at the padded slice's own offset, which is what lets the
descriptor keep a minUniformBufferOffsetAlignment-aligned dynamic offset.

red-once (R-16), all three Magma two-process arms, before the backend half:

    DirectVulkan.Split.Buffers.F1WireScenario.SubWordUniformBufferRangePadsMissingBytes
    DirectVulkan.Spawn.Buffers.F1WireScenario.SubWordUniformBufferRangePadsMissingBytes
    DirectVulkan.Tcp.Buffers.F1WireScenario.SubWordUniformBufferRangePadsMissingBytes
        Subprocess aborted - MGPipe: Fatal{UnmigratedVerb, "Magma:uniform-buffer-byte-tail@P7"}

and green after it. The case is the sibling of
ShortUniformBufferRangePadsMissingBytes with a 19-byte range; the byte just past
the range is poisoned, so a rounding that leaks it reads tail.x as 0.5f and the
red channel comes back 127 instead of 0.

Lanes: integration-magma-split 74 (was 73), -spawn 53 (52), -tcp 55 (54),
integration-magma-buffers 20 (19), all green. fatal_census unchanged at 79 abort
sites / 43 family words - these refusals stopped being bare aborts in wave 0, so
a retirement moves no census number and needs no re-baseline. link_ratchet
unchanged at 186.
2026-09-22 16:22:40 -04:00
swung0x48 cf7ca59fab [MG_Remote, docs] (P7): CONTRACT-P7 records package C - vertex-layout split (seven masked reasons), the death-ops debt retired, the opcode-49 white-box deferred to P11, ID-P7-20 2026-09-22 16:09:09 -04:00
swung0x48 074a072739 [docs] (P7): package C's note carries the gate matrix and the three things the integrator has to decide
The per-slice red-once records were written with each slice. This adds what only exists
once the four are together: the gate table (unit 2403, magma split/spawn/tcp 75/54/56 with
the two death-case skips now gone, integration-split 187, integration-spawn 103,
full-split 513 entries 0 red 55 skipped = 458 pass against the 453/57 bar, census
unchanged, parity green, @P7 15/4 -> 13/3, ratchet 186 monotone, G1 .text 0xa52203 with
every section +0 and the pull .so byte-identical), the ratchet bucket split with an honest
note that I did not re-measure the buckets at the branch point, and a numbered list of the
nine things this package changes that live outside its own files.

Three of those nine need an integrator decision rather than a note: §5.4's white-box case
is blocked by R-6 and not by this package; §5.3's g_programResourceCaches deletion is
achieved in the disaggregated build only, because the archive member has to be guarded for
G1; and buffer-window's family word is UnmigratedVerb where ProtocolCorruption is the
honest one, which costs a row in MG_Pipe and an adapter arm that §3.3 deliberately has not
spent.
2026-09-22 16:07:59 -04:00
swung0x48 0acfa9aab9 [MG_State, MG_Backend, MG_Test, docs] (P7): the reflection archive publishes the storage-block index space, and both magma consumers read it
CONTRACT-P7 §5.3, OQ-8. MagmaProgramSource::EnsureStorageBlocks re-ran SPIRV-Reflect over
every stage module of the program PER DRAW: the class is a borrowed view that
WireDraw.inc's WireProgramSource constructs afresh for each wire draw, so its `mutable`
memo was cold every time it was read. The list it rebuilt is a pure function of the linked
program, so it is now built once at link time and travels in the archive.

LinkArtifacts gains storageBlocks{name, binding, dataSize} with its own VisitFields table,
the codec version goes 2 -> 3, and the schema fingerprint and wireFingerprint move with
it - which is expected, and is what makes a mixed-version pair refuse at the handshake
instead of at the first program.

FILLED IN PHASE A, and that is not where it looks like it belongs. The only function that
sees both finished halves is ProgramSpirvTask's L1 cache insert, but its writes reach the
TRANSLATION CACHE only: phase A's artifacts have already been moved into the ProgramObject
by then. A field filled there would be present on a cache hit and absent on every cold
link. So the fill is glslang-derived, and the equivalence is MEASURED rather than assumed:
ProgramTest.TheArchivesStorageBlockOrderIsTheOneSpirvReflectProduces runs the old
consumer's own algorithm over the real modules and compares element for element, on a
multi-stage program with an arrayed SSBO and an atomic-counter block - the three cases
that could have diverged (the instance array collapses to one entry, the synthesized
counter block IS in the backend's space although GL's enumeration excludes it, and the
cross-stage dedupe walks stages in linked order).

MEASURED, same binary and same window (frames=1 draws=1 acc=18, identical bytes),
DirectVulkan x inproc over the SSBO scenarios: spvReflectCreateShaderModule on the wire
draw path 4 -> 0. The 0 is structural: the call site is gone and MagmaProgramSource.h no
longer includes spirv_reflect.h at all, which is the gate a future edit breaks on purpose.

THE MONOLITH CONSUMER IS SWITCHED TOO, under #if symmetry, so the disaggregated build
retires g_programResourceCaches, ProgramResourceCache, StorageBlockResource,
BufferVariableResource, AddBufferVariablesRecursive, NormalizeDescriptorName,
GetProgramResourceCache and the rehash hazard - the open-addressed-map dangling reference
that was a reproducible segfault in ProgramPipelineScenario's two storage-block cases.
Nothing outside DirectVulkan.cpp read any of it (measured; GL_BUFFER_VARIABLE and
GL_ACTIVE_VARIABLES are answered by ProgramInterface.cpp out of a different index space),
so the two arms differ in exactly the two functions their callers use.

THE PULL BUILD KEEPS ALL OF IT, and the integrator should see why: the archive member is
MOBILEGL_BUILD_DISAGGREGATED-only because one more Vector in LinkArtifacts changes its
size, its implicit destructor and its move constructor, and G1 pins the pull build's .text
byte for byte. So §5.3's deletion is achieved in the disaggregated build and not in the
pull one; retiring the pull half is a one-line change the day that pin is retired or the
member moves. Filed as P13 in notes/p7/magma-c.md.

One improvement rides along: the monolith binding getter now asks the program for a
rebind on every read, over an immutable list, where it used to read a binding patched into
a mutable cache - so a rebind can no longer be lost to a cache rebuild that raced a
state-version bump.

G1: .text 0xa52203 unchanged, 27837 -> 27837 defined symbols, 0 added / 0 removed / 0
resized / 0 renamed, file byte-identical. An unguarded call site caught the guard rule
once mid-slice by failing the pull compile, which is the loud version.

RED-ONCE: publishing nothing reds the order-equivalence case (with the reference list
printed) and 25 of 60 storage-block scenarios - the MONOLITH DirectVulkan arm beside all
three transports, which is the evidence that the monolith consumer really was switched.
Restored: 61/61.
2026-09-22 16:07:59 -04:00
swung0x48 f1b04edfc6 [MG_Backend, MG_IntegrationTest, docs] (P7): the magma vertex-layout refusal splits into one protocol fatal and seven masked attributes
CONTRACT-P7 §3.2's `vertex-layout` and `vertex-format-conversion` rows.
BuildWireVertexInput answered eight different conditions with one `false`, and the single
caller answered that with a P7-marked MagmaWireFatal. So a Magma split session that met a
GL_FIXED array, a dvec on a device with native fp64, or any format without
VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT did not decline the draw: it killed the process. The
monolith arm on the same device has always masked the attribute out and then declined the
draw loudly and once.

The split is by what the reason is ABOUT, not by severity. `buffer-window` is a statement
about the RECORD - an enabled attribute the program reads whose slot lies outside the
window set_vertex_buffers published - so it stays a named Fatal through the hook; it is
what caught the Redmi VAO hash collision fixed in 376c04be. Every other reason is a
statement about what Vulkan can express on this device, and those now set
unsupportedAttribMask and continue, with the monolith arm's own pre-flight in WireDraw.inc
declining the draw when a masked bit is one the program actually reads.

The contract says "the other six"; there are seven. `offset-overflow` post-dates the
audit's count and is an arithmetic guard rather than a shape one. It is masked with the
rest: an attribute whose base offsets cannot be added is one Vulkan cannot fetch, the
monolith arm never computes that sum at all, and masking is strictly safer than a Fatal
for a number no application can reach on purpose.

unsupportedAttribMask now enters the wire layout hash, for the reason the monolith entry's
hash already carries it: two VAOs can otherwise agree on every binding, attribute and
divisor and differ only in which enabled attribute was masked, and a blind hash would hand
one of them the other's pipeline. Omitting it was safe only while the mask was always zero.

vertex-format-conversion becomes a decline for a narrower reason: the converter is only
ever asked for a conversion BuildWireVertexInput already chose, so a refusal means the two
disagree - a defect on this side, not a peer's malformed record.

@P7 census: 15 sites / 4 files -> 13 / 3. Renderer/WireDraw.inc leaves the list entirely.
fatal_census.py unchanged at 79 abort sites / 43 family words / 0 unmarked, as expected:
wave 0 already funnelled these, so retiring a MagmaWireFatal CALL moves no abort count.

New MagmaVertexLayoutScenario, DirectVulkan only (Espryt hands GL_FIXED to a driver that
supports it, so the same name would assert a different fact there). DataType::Fixed32 has
no arm in ToVkVertexFormat at all, which makes it the one unmappable shape reachable on
host lavapipe - §2.5 records that no other P7-marked refusal is. One case has the program
READ the array and asserts the draw declines; the other leaves it enabled but unread and
asserts the draw lands, which is what stops the first being satisfied by "decline every
draw that mentions GL_FIXED". Registered on all three transports and picked up by the
ambient monolith DirectVulkan discovery, so the A/B is direct.

RED-ONCE: `format-map` put back on the protocol verdict reds the three split arms with
`Subprocess aborted` / `Fatal{UnmigratedVerb, "Magma:vertex-layout-buffer-window"}` while
the monolith control stays green - exactly the divergence this closes. Restored: 10/10.

The gating Magma lanes grow with the new entries: split 73 -> 75, spawn 52 -> 54, tcp
54 -> 56. Growth only.

Open for the integrator: the honest family word for buffer-window is ProtocolCorruption,
and MGPipeFatalFamily carries only UnmigratedVerb and RoleViolation. Adding a third costs
a row there plus an adapter arm, and §3.3 keeps the three Magma funnels on UnmigratedVerb
deliberately so the refusal census stays comparable to P5b. Flagged, not decided.
2026-09-22 16:07:59 -04:00
swung0x48 1255caa2fb [MG_Backend, MG_Impl, MG_Util, MG_Test, MG_IntegrationTest, docs] (P7): the server publishes kCapResidentSubData off its own wire resource table
CONTRACT-P7 §5.4, OQ-10. InitServerRoleCommon computes the whole capability mask and
never ORed this bit, so MGPipeResourceOpsHaveSubDataResident answered false under every
transport and BOTH backends were pinned to the ordered in-place host memcpy - which made
Magma's SubDataResident arm (VkBufferManager.cpp's g_vulkanWireResourceOps) dead code on
the wire even though the arm has been there since P5f.

The bit is derived from g_resourceOpsAtStep2->SubDataResident, i.e. from the pointer this
function already pinned at step 2 and that step 5 already refuses to see swapped - never
from ActiveBackendType. That is ID-39's lesson and not a style preference: a mask is a
statement about THIS backend's registered table, and a bit derived from the enum would
keep claiming the capability for a build, a flavour or a future backend whose table does
not carry the arm.

`rsd=` joins the emit[] bracket on the PipeStats summary line, counted at the emitter once
per buffer_subdata_resident record rather than once per call, because a write past the
content chunk cap is cut into several records. It is the only observable that separates
the resident arm from the memcpy beside it: the bytes in the buffer are identical either
way, which is exactly how the capability stayed unpublished for a phase with every lane
green.

WHAT THIS DOES NOT YET PROVE, and R-6 is the reason rather than an omission. §5.4 asks for
a white-box case showing opcode 49 on the wire. None can exist at this tree: the resident
arm is reached only from a GPU-RESIDENT store, residency comes only from
AdoptPersistentMap, and every persistent-map acquisition declines under split by R-6
(PipeApply.cpp:2331 "A SPLIT BUILD RUNS AT TIER T2 AND DECLINES EVERY ACQUISITION,
ALWAYS"; T0/T1 are P11's and AdoptTierIsEmulate is a named Fatal for them). So the
capability is wired AHEAD of its consumer, deliberately, and `rsd=` is what will show the
records the day P11 lands a tier that adopts. Filed for the integrator in
notes/p7/magma-c.md.

What IS proved is both halves of the bit itself. RemoteClientTest's Magma bootstrap case
now asserts the published bit EQUALS `ops->SubDataResident != nullptr` - the two compared
to each other rather than both to true, so the case also covers a backend without the arm
on the day one exists - and a new CtWireScenario case reads the bit out of the client's
own caps mirror, through the same accessor the frontend reads, on both backends and all
three transports.

RED-ONCE, both levels. Deleting the OR reds
CapsMirrorTest.MagmaTransportPublishesRealBufferConsumersWithoutRunAhead
(RemoteClientTest.cpp:692, "Which is: false / Which is: true") and all SIX arms of
DirectGLES/DirectVulkan x {Split,Spawn,Tcp} Ct.TheServerPublishesTheResidentSubData
CapabilityFromItsOwnTable ("the client's caps mirror did not receive
kCapResidentSubData"). Restored: green.

Also carries the P4a death-helper comment from the previous commit's sweep: it no longer
says Magma installs no death table, because it now does and still frees no slot.
2026-09-22 16:07:58 -04:00
swung0x48 dce9953d36 [MG_Backend, MG_State, MG_Pipe, MG_Remote, MG_Impl, MG_IntegrationTest, docs] (P7): magma installs a state-object death table, and the inproc arm stops losing every object_death
CONTRACT-P7 §5.5. DirectVulkan had no StateObjectDeathOps at all, so under a Magma
inproc session NotifyStateObjectDestroyed reached nobody and the server was never told
that a frontend object had died. Package L measured the shape: with the WireTables
install condition made backend-agnostic, CtWireScenario's two death cases PASS on Magma
spawn and tcp - the remote client has no local backend, so the client-side emitter
installed there answers the notice - and FAIL on inproc, where the install condition
deliberately stands aside for "the backend's own dispatcher" and Magma had none.

g_magmaStateObjectDeathOps is that dispatcher. It mirrors g_glesStateObjectDeathOps
(DirectGLES/Managers.cpp:335) with ONE arm rather than two: Espryt's other arm destroys
a backend twin keyed by lifetime id, and Magma keeps no such twin - its two
client-minted kinds are keyed on {slot, gen} by MagmaPipeIdentityTables and the other
six are still reached from their frontend objects - so what is left is exactly the emit
of object_death off the apply thread. Installed from
BackendObject_DirectVulkan::Initialize(), i.e. ServerLoop::CreateBackend, step 1 of
InitServerRoleCommon: the one place BOTH Magma server roles pass through, and before any
client session exists. #if MOBILEGL_BUILD_DISAGGREGATED && MOBILEGL_PIPE_PUSH, so the
pull build compiles nothing new.

The two skip guards in CtWireScenario go with it. Every other "Magma installs none" site
the audit found is comment text whose ARGUMENT survives, and each is reworded rather than
deleted: the table emits the wire record and frees no client slot, so P4a's
backend-neutral NotifyAndFree is still the only thing that returns one, HandleRecycle's
C-1 case still measures the only protection the allocator has, and MagmaPipeArms' age
sweep is still the only thing that retires an identity slot.

The framebuffer death case now reads the client's allocator beside the server's tally.
That correction matters: the plan called this a slot leak provable with PipeSlotPeek, and
it is not - it has been backend-neutral since P4a. What leaked was the SERVER's twin, and
a framebuffer has no wire delete opcode at all, so object_death is its only delivery.

Two installers cannot race although the global is last-writer-wins with no stacking:
Magma's runs only in a process that owns a DirectVulkan backend, and
InstallClientWireTables' arm runs only under Transport == Spawn, which is by construction
the process that owns none.

RED-ONCE: `if (false) InstallStateObjectDeathOps();` reds the two Split cases alone
(`Expected: (afterCounters.deaths) > (deathsBefore), actual: 0 vs 0`, CtWireScenario.cpp:264)
and leaves spawn and tcp green - the exact split of responsibility the comment claims.
Restored: 8/8. Recorded in docs/Disaggregated/notes/p7/magma-c.md.
2026-09-22 16:07:13 -04:00
swung0x48 07079c3c0e [docs] (P7): X1 verified on the phone, F1 verified but acquitted, OpenRA is a cold-cache timing race (ID-P7-18/19)
X1: 26.3 and create-instancing pass under inproc on both backends, zero declined
faults, tracker line confirms tag=0xb4. F1: the R-8 line is gone in every run, yet
OpenRA reads 0.976494 on a cold driver cache (three pm-clear runs identical), 1.0
warm, 1.0 cold monolith, and FRAMESINFLIGHT=1 flips it half the time - a GPU-timing
race on the wire readback/present path, handed to package B as slice 0.
2026-09-22 16:05:39 -04:00
swung0x48 3179c4972d [docs] (P7): F1's emitter, its three red-once arms and the one thing lavapipe cannot prove are on record
`notes/p7/f1-caps-placeholder.md`, per CONTRACT-P7 §9's "one red-once per package
recorded in notes/p7/". It carries the call chain from `MobileGL::Initialize` down
to `CapsMirror::ServerConsumes(0x400)`, the three corrections to the brief's reading
(it is not a race; P6.5's fix already covers inproc; a bounded wait alone deadlocks),
the three red-once arms, and the two things still owed to the integrator.

The arm worth reading twice is A. On lavapipe the BASE build carries the R-8 line on
every transport, with and without the delay knob, and OpenRA still scores
ssim 1.000000 / mismatch 0. So the eleven dropped resource_creates cost nothing
visible there, F1's red-once is the LOG LINE and the new Fatal rather than an SSIM,
and this package must not be read as having explained the device's 0.976494 /
0.988998. That still needs the phone.
2026-09-22 15:55:08 -04:00
swung0x48 22c7361c5f [MG_Test] (P7): the placeholder caps cases assert the refusal and the wait instead of the answer
`CapsMirrorTest.APlaceholderMirrorConsumesNothing` asserted the defect F1 closes -
"with no snapshot the mask is zero, every family answers no consumer, the client
emits nothing and the legacy pull path runs" - and it was green for five packages
because nothing on the client ever checked whether that pull path exists. It does
not. Four cases replace it, and each one is the control for a different way the
new gate could go wrong:

  * `APlaceholderMirrorRefusesToAnswerAndNamesTheFamily` forks, asks a placeholder
    mirror about TextureResources with no session anywhere, and requires the death
    to carry `Fatal{CapsBeforeFirstSnapshot, "TextureResources"}` AND `0x400` -
    R-16's rule, because "something aborted" is satisfied by any of the other 43
    family words.
  * `ThePlaceholderWaitsForASnapshotAnotherThreadIsFetching` drives the PRODUCTION
    bring-up (`MG_Backend::Init`) with `MOBILEGL_TEST_DELAY_FIRST_CAPS_MS=50` and
    asks from a second thread. The wait must return the server's real answer, not
    the placeholder's false and not a death. It is the only shape in which "block
    with a bounded wait" is a correct answer to F1.
  * `ARealSnapshotThatWithholdsAFamilyStillAnswersFalse` is what keeps this from
    being a callMask defaulted to all-ones: a server that really consumes nothing
    is still answered "no", counted and named. Only the PLACEHOLDER is refused.
  * `TheReadAccessorsStillAnswerFromThePlaceholder` pins the half of P5's ruling
    that F1 does NOT narrow.

`ResetCapsMirrorForTest()` is new and test-only: a fork child inherits whatever the
previous case adopted, so without it a case about the placeholder would be a case
about that snapshot.

`ServerLoopTest`'s main gains no code, only the sentence that explains why it has
none: it sets `MG_Config::Transport` by hand and never runs `MG_ConfigLoader::Init`,
which is exactly what keeps the client-side gate disarmed for a binary that builds
server sessions and the server's own frontend GLContext and never a client at all.
2026-09-22 15:55:08 -04:00
swung0x48 df17c416c0 [MobileGL, MG_Backend, MG_Remote, scripts] (P7): the split wire comes up before the first frontend object, and the caps mirror stops deciding a family's fate from a placeholder
F1. `MG_State::Init()` runs before `MG_Backend::Init()`, and the second is what
starts the session - so `GLContext`'s `TextureState` constructor materialised its
eleven default texture objects, each of them emitted `resource_create` through
`FamilyIsLive`, and `CapsMirror::ServerConsumes(0x400)` answered every one of them
from a generation-0 placeholder with `callMask=0`. All eleven records were dropped
and the whole event was one WARN line:

  MG_Remote client: the server does not consume MGPipe subsystem 0x400 - this
  family emits NOTHING and the legacy pull path runs for it (R-8). callMask=0x0,
  caps generation 0

It is NOT a race. The line reproduces byte-for-byte on lavapipe and on the Adreno
830, because the two Init calls are two statements on one thread. And "the legacy
pull path runs for it" is false on the client under split: there is no pull path
on this side of the wire, so withholding is not a fallback, it is a drop that only
a later respecify's self-healing create ever undoes.

Two of the three options the brief offered do not apply, and both facts are worth
recording. (b) "extend the P6.5 fix to inproc" is already the tree: `FinishStartup`
step 7 is the one funnel every transport reaches and it will not return without a
real snapshot. (a) "a bounded wait in Consumes()" cannot work alone and would
deadlock: at `MG_State::Init()` no handshake is in flight, and the one that will
land is started later BY THE SAME THREAD.

So the fix is the ORDER, with the rule behind it as the gate:

  * `MobileGL::Initialize` calls `MG_Backend::InitSplitRolesBeforeState()` before
    `MG_State::Init()`. `Init()`'s split arm becomes one body, `InitSplitBackend()`,
    shared with the direct callers that still reach `Init()`; the monolith arm keeps
    its place after `MG_State::Init()`, because moving it would move pull `.text`.
  * `CapsMirror::ServerConsumes()` refuses to answer from a placeholder. It waits,
    bounded, when a bring-up is genuinely in flight on ANOTHER thread, and otherwise
    dies as `Fatal{CapsBeforeFirstSnapshot, "<family>"}` through `SessionFail` -
    a new family word, one row in `FatalFamilies.def`, no new abort site.
  * Two arms are exempt and say why in place: the server arm (`MGPipeServerArm()` -
    the apply thread and the spawn server, where the client's mirror is the wrong
    object rather than a late one), and a process whose own configuration never
    asked for a split transport (`MG_Config::SplitTransportRequestedByConfig`, set
    by `MG_ConfigLoader::InitTransport`, which is what tells a real run apart from a
    fixture that assigns `MG_Config::Transport` by hand).

The read accessors keep P5's ruling untouched: `LogBackendInfo` reads
`GetRendererInfo()` during `MG_Backend::Init()` and an abort there is a process that
cannot start. Only the DECISION changed. The warning was not made quieter and the
callMask was not defaulted to all-ones.

`MOBILEGL_TEST_DELAY_FIRST_CAPS_MS` (server side, disaggregated builds, inert unless
set) holds back the FIRST CapsSnapshot. On spawn/tcp that is a genuinely late
snapshot in another process and it exercises the client's step-7 wait; on inproc it
only makes `Start` slower, which is itself the proof that the only window a client
can read a placeholder in is the one before `Start` is called.

G1: `.text` 0xa52203 and the 30570-symbol list identical to p7-before/pull-syms.txt.
Census: 79 abort sites unchanged, 44 family words (was 43). Link ratchet: 186,
unchanged.
2026-09-22 15:55:08 -04:00
swung0x48 206abdfa44 [docs] (P7): the gate-5 CTS base reading is on record - monolith x DirectVulkan x KHR-GL46 on the Redmi, five complete blocks
shader-image 63/56/6, ssbo 124/0/0, dsa 368/0/2, texture 815/221/18, packed-pixels
4732/0/0 (Pass/NotSupported/Fail); every block complete (no unrun, hung or crashed);
library p7w1-d260f110, glcts from VK-GL-CTS 0b04c470e with the FBO colour-texture
patch, API probe confirms GL 4.6. The AFTER reading compares inproc against this at
0.5 pp per block with new crashes as a separate hard red.
2026-09-22 15:33:52 -04:00
swung0x48 5c4235d7ba [docs, MG_Remote] (P7): ID-P7-12..17 - gate 3 per case against monolith, photon excluded, wave-0 naming ratified, X1 on the critical path, gate 5 is five blocks at GL46, E2/E0b folded into the F1 re-test 2026-09-22 15:25:20 -04:00
swung0x48 ec3ba00af3 [tools] (P7): the CTS skip list the runbook already passes exists
run_cts.py silently tolerates a missing --skip-file, so the known device-killers from
the skill (clip_distance.functional, the multisampled_to_singlesampled blit family,
the rgb565 repeat-mode case) would each have cost a reboot in the gate-5 window.
2026-09-22 15:16:37 -04:00