Commit Graph
3507 Commits
Author SHA1 Message Date
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
swung0x48 4cbf78c269 [MG_Remote, MG_Test, docs] (P7): the persistent-map tracker answers faults in the kernel's address space, and a map hands back a range it can write
Wave 2 package X1. Three CI split-subset traces died on the Redmi under inproc and
passed under monolith on the same device and APK, on the client thread, in the
replayer's memcpy into a pointer MobileGL had handed it: SIGSEGV / SEGV_ACCERR on the
destination's FIRST byte, destination page-aligned.

The tombstone names the same address twice, in two different address spaces:

    x0  b4000073ac9a5000 ... x2 0000000001800000
    signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x00000073ac9a5000

x0 is the pointer the application writes through, carrying bionic's 0xb4 heap tag
(the same tombstone reports tagged_addr_ctrl PR_TAGGED_ADDR_ENABLE). fault addr is
si_addr, which the kernel reports UNTAGGED.

TrackWriteMap took shadowBase from MappedData() - a tagged heap pointer - kept the tag
through the page masking, mprotect()ed the mapped range PROT_READ and published the
TAGGED base/end into the slot. The handler compared si_addr against it:

    if (base <= kSlotSettingUp || address < base) continue;

0x73ac9a5000 < 0xb4000073ac9a5000 is true for every fault, forever. Ownership test (2)
never matched, and every fault on a page the tracker had itself protected was chained
to debuggerd. mprotect was fine throughout - do_mprotect_pkey untags its own argument -
so the right pages really were read-only; only the recognition was broken. x86-64 has
no top-byte tag, which is why every host lane has always been green.

The fix is two rows and one ordering:

  * UntagAddress(), applied to the shadow base at registration, to the shadow base in
    PushBlocksFor (so trackedBase - shadowBase stays in one space) and to si_addr in
    the handler. Written WITHOUT an #if defined(__aarch64__): masking bits 56-63 is
    the identity for every valid userspace address off arm64, and an arch guard would
    have put the only gate that can catch this on the one platform CI never runs.
    Dereferences keep the tagged pointer they came from.
  * SlotOwnsFault(), the ownership row the handler and the unit case both read.
  * REGISTRATION ARMS NOTHING. AcquireMemoryRange registered and only then returned
    Bytes() + range.start, so glMapBufferRange handed the application a PROT_READ
    pointer whose writability depended on a handler reaching the writing thread - and
    the application is free to write a mapped arena from a worker that blocks every
    signal. Nothing is lost by waiting: every page bit is already set at registration,
    so the first push ships the whole interior regardless, and it is that push which
    arms the pages. 6144 pointless faults per 24 MB arena go with it, and the
    invariant the file rests on (bit set <=> page writable) is now true from
    registration onward instead of only from the first push.

Diagnostic, independent of the fix: one MGLOG_I_ONCE at the first registration naming
the mapping the way the device run needed and did not have (allocator, pointer, ITS
TAG, extent, range, published span, pages, alignment arm), and a decline record in the
handler - relaxed atomic stores plus one async-signal-safe write(2) - readable through
DeclinedFaults(). A non-zero decline count whose si_addr lies inside nearest-tracked
once the top byte is masked off is this defect's signature and nothing else's.

Not the mechanism, and checked: libsigchain (the tombstone's "F libc : Fatal signal 11"
is bionic's own handler, so the chain ran), the 16 KiB-page refusal (the device is a
4 KiB kernel and the protection demonstrably took), RefaultOfANonWriteAccess (matched
is false long before it is consulted), and the P5d shadow allocator (correct - it is
what makes the destination page-aligned in the tombstone).

Red-once, two and orthogonal (MG_Test/Wire/PersistentMapFaultTest.cpp, 6 cases):
dropping the two UntagAddress calls in SlotOwnsFault reds only
AKernelsUntaggedFaultAddressIsOwnedByATaggedShadowsSlot, which runs the device's own
two addresses through the real predicate on an x86-64 host; restoring the mprotect at
the end of TrackWriteMap reds only AMapHandsBackARangeWritableWithoutTheFaultHandler
(the child dies by SIGSEGV) and RegistrationLeavesEveryPageOfTheRangeReadWrite.
AnArmedPageIsStillAnsweredByTheHandlerAndMadeWritable is the anti-vacuity case.

Gates: ctest -L unit 2408/2408; integration-split 186/186; integration-spawn 102/102;
integration-magma-split 73/73 (PersistentCoherentMapScenario green in the Split, Spawn,
PersistentMapArm and SmallRing variants); fatal_census.py 0 unmarked. G1 holds - pull
.text 10822147 = 0xa52203 unchanged, symbol report 0 added / 0 removed / 0 resized /
0 renamed, nm --defined-only identical at 30570 and nm -D at 11933. Every line is under
MOBILEGL_BUILD_DISAGGREGATED (CMakeLists.txt:592-659), so the pull build sees none of it.

Evidence and the device experiment: docs/Disaggregated/notes/p7/x1-persistent-map-segv.md
2026-09-22 15:16:37 -04:00
swung0x48 4f2d4c4e83 [MG_Remote, docs] (P7): CONTRACT-P7 carries wave 0's numbers, and device window #1a is on record
CONTRACT-P7: the lane counts (magma-split 73 / spawn 52 / tcp 54; full-suite 510 x 3
with one StreamLink x Magma red), the census baseline (79 abort sites over 20 files,
43 family words, 0 unmarked), the ratchet baseline (186 = p7-magma 101 / espryt 14 /
both 60 / core 11; espryt's clearable count is 9) and the @P7 count (15 sites / 4
files) replace the 待填 markers.

Device window #1a (Redmi 2f7cbe2e, APK p7w1-d260f110, reboot-clean): E0a monolith
39/39 deterministic, three below threshold on monolith itself (create-indirect,
1.21.11 main-menu, iris-photon - Adreno refuses its pipelines with VK_ERROR_UNKNOWN);
E0a inproc: OpenRA 0.976494 (run-ahead) / E1 lockstep 0.988998 - both historical
numbers were OpenRA, and its client log shows a TextureResources verb answered from
caps generation 0 (package F1); 26.3 / create-* SIGSEGV in the replayer's memcpy on
BOTH backends and with the persistent-map push off (package X1); iterationt x2 die
by name on Magma:mipmap-shader-format-or-shape (package B); bsl-esc-menu hits
scudo OOM under inproc. Gate 3's denominator is 36; run-ahead and present credit
are off the suspect list.
2026-09-22 14:49:42 -04:00
swung0x48 801f8edaa6 [scripts] (P7): the parity self-test patches the seam the Magma tiers read through
Package D added a test that patches lane_cases and hands spawn_lane_parity.py a
placeholder build dir; package L then made the three Magma tiers read through a new
lane_names, so the placeholder reached a real ctest and the unit label went red on the
integrated tree. Patching lane_names to an empty set on every arm makes those tiers
report "nothing to compare" and leaves the assertion about lane_cases intact.
2026-09-22 14:43:51 -04:00
swung0x48 8bb6309a1c [scripts, CI, docs] (P7): the 184-symbol link-closure ratchet lands as a gate
CONTRACT-P6.md 12.1 has carried "a ratchet recomputes the 184 in CI and asserts it only
ever falls" as an obligation since P6. The roadmap handed it to P6.5; the P6.5 line never
received it, and scripts/ci/ has never had anything that computes this number. ID-P7-7
gives it to P7 wave 0. This is it.

scripts/link_ratchet.py mechanises a6's hand experiment: partition the per-object output
of the single MobileGL target into SERVER / FRONTEND / SHARED, then compute
(SERVER undefined) - (SERVER defined) - (SHARED defined) intersect (FRONTEND defined),
which is the undefined-symbol list a real link of a server-only image would print. The
partition is an explicit table with a rationale per row, because it IS the module boundary
the build system does not have (SOURCE_FILES is one flat list; boundaries are P13's work).
An object matching no row is a hard failure, as is a build whose CMakeCache lacks the three
disaggregated options, or an empty role set - every way this gate could go green without
having measured anything is an explicit exit 2.

Measured on 78b7d6be: 186 symbols. The three backend buckets reproduce a6's hand-made
101 / 14 / 60 exactly, as do its per-object referrer and definer distributions
(UniformManager 90, VulkanRenderer 60, Managers 53, DirectGLES 51; TextureObject 59,
SamplerObject 30, BufferObject 18, FramebufferObject 16, PipeFill 9). Nothing from a6's
184 has been cleared; the +2 are both P6.5's:
  Client::ClientSession::StartSpawned()                 - fork-per-session
  GLState::ProgramArtifactsSchemaFingerprint()          - archive v2 caps

The baseline stores the LIST, not a count (ID-P7-7): a count-only ratchet goes green on the
day one symbol is cleared and another added. --assert-monotone fails only on symbols absent
from the baseline and names each one's defining object and every referring object; symbols
that disappeared are reported with an instruction to re-baseline in the same commit, and
never fail.

The CI step goes in build-linux-split, not build-linux: build-linux takes the three options
from MOBILEGL_CI_DISAGGREGATED, which is ON only on feat/disaggregated and
codex/fix-github-ci, so on any other branch it builds a monolith and the gate would measure
a SERVER set with no transport half. The JSON is uploaded even when the gate fires.

Two findings for the integrator, both written up in the note:
  - a6's four published bucket totals sum to 181 against its own headline of 184. These
    buckets are a strict partition and always sum to the headline; the difference is in
    a6's core bucket, which it listed selectively.
  - 5 of the 14 "P3b/P4b" symbols are MG_Pipe slot-allocator and apply-thread plumbing
    that sits under MG_Impl/Pipe/ by file location alone. P3b/P4b cannot clear them by
    migrating Espryt functionality; they need P13's module move. That bucket's floor is
    5, not 0, so the plan's 0.5-day Tier 1 estimate for "D12's 14 symbols" should be
    re-cut against 9. All 7 symbols from that object carry a `# P13` annotation.

red-once (R-16) is recorded verbatim in the note. Worth knowing: the plan's own example
(SamplerObject::GetMinFilter) does NOT turn this gate red, because it is already in the
baseline - the ratchet reddens on new symbols, so a negative control has to pick one that
is not in the 186. A reach to GetBorderColorForm() from DirectGLES/Utils.cpp does, naming
the symbol, its definer and its referrer, exit 1; reverted and green again.
2026-09-22 14:26:04 -04:00
swung0x48 1de8f02ebe [MG_IntegrationTest, docs] (P7): the whole integration binary runs under Magma split, and it is one red
Tier 2 replays the split-arm registrations - ~100 scenarios, the set Espryt's disaggregation work
has carried since P5 - and it measured green on Magma across all three transports. That is a real
result but it is NOT exit gate 1. The gate says the INTEGRATION BINARY is green under Magma split,
and the set that names is the one the monolith DirectVulkan. arm runs: the whole suite minus
MGL_ITEST_MONOLITH_MECHANISM_FILTER. 510 cases against tier 2's 102, and the readback matrices,
the same-texture copy shapes and the odd-offset uniform ranges are in it and nowhere else in the
disaggregated registry.

integration-magma-full-{split,spawn,tcp}, in no gating step, private log per entry through a list
of its own. THE RESULT: 453/0/57 on inproc, 453/0/57 on spawn, 454/1/57 on tcp - and not one of
the 1530 entries reaches any of the tree's fifteen @P7 named refusals. The single red is
IterationRPProgram203Scenario.FixedCompleteInputProducesFixedCompleteGoldenOutput on the TCP arm:
one texel of 262656, texel 0, which is the case's own exposure value
(IterationRPProgram203Scenario.cpp:371, actual half bits (0x357a, 0x4b65) against golden
(0x3a74, 0x4821)). Deterministic 5/5 on tcp; 5/5 GREEN on inproc, on spawn and on the monolith
control. spawn and tcp differ only in MOBILEGL_IPC_DATA, so the suspicion is the STREAM data path
and not "two processes"; the GLES tcp lane is green in the same run, so it is not the fixture.
Not fixed here - it is what the census is for.

TWO ENVIRONMENT DIFFERENCES FROM THE GATED TIERS, both measured rather than assumed:

  * NO MGITEST_SPLIT_LANE=1. That marker arms ScenarioFixture's destructor assertion - a case in
    an armed split lane must have moved the client encoder's record ordinal - which is right for
    a curated lane and wrong for a whole-binary census. With it set, the first run produced 13
    reds, all `EmitSeq was 20 at SetUp and is 20 now`: AdvertisedLimitsScenario (8),
    SpirvShaderBinaryScenario (4) and ProgramPipelineScenario.AReservedName - a limits query is
    answered from the client's CapsMirror and a shader-binary error surface never reaches a verb.
    Those 13 would red on a DirectGLES census too. What it gives up is stated in the comment: this
    tier does not prove its records crossed, and the tcp topology proof is inert in it.
  * DualBlockScenario.* filtered out. Its green REQUIRES MOBILEGL_IPC_ROLE_SPLIT_STATE=1 and a
    census must not set that knob; without it the case is red by name, which is its documented
    negative-control behaviour. The monolith arm skips it for the same class of reason.

CtWireScenario gets MOBILEGL_TEST_SERVER_COUNTERS=1 in this tier - a switch that makes a counter
readable, not an oracle that asserts - because without it the spawn arm's ApplierReset case reds
on "server control-record telemetry missing", which says nothing about Magma. The strict-arming,
pmap and persistent-map-arm markers stay out.

The inproc-only exclusion in spawn_lane_parity.py is now PER TIER: in the gated tier the two
scenarios are registered on the inproc arm alone, in the census they are on every arm and simply
skip, so excluding them there would manufacture a spurious "extra".

Measured after the change: every pre-existing lane unchanged and green (magma-split 73,
spawn 52, tcp 54, all-* 102/102/104, GLES 186/102/104, buffers 19, runahead 14, caches 7), the
dual-block census 257 green / 2 skipped / 0 red with an empty ratchet, both new CI steps green,
parity RC=0 across all three tiers, G14 4155 -> 6095 with removed=0.

The note now carries the grouped red list, the 57 named skips by reason, the four pieces of
evidence that the tcp red is a defect and not plumbing, and six findings - the first being that
wave 2's clusters cannot be scheduled from this lane's red list, because on lavapipe there is
essentially none.
2026-09-22 14:24:19 -04:00
swung0x48 78e10ccf80 [CI] (P7): retrace-split exports every Magma knob the monolith retrace job exports
Integrator ruling, completing the alignment package L started. The split retrace job now also
exports MOBILEGL_MAGMA_R11G11B10F_FALLBACK for every DirectVulkan row and
MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE for improved-transparency-minecraft-26.3, copied
from the monolith job (:2180 / :2195) with the same guards.

The point is comparability, not the knobs: a split retrace whose monolith control ran a
different repair set cannot say whether a divergence is a transport fact. With the three
iterationRP repairs from the previous commit, every DirectVulkan row of the 154-cell split
matrix now runs the quirk configuration its control runs.

retrace-verify is left alone, as the ruling says.
2026-09-22 14:24:19 -04:00
swung0x48 a7425d95e0 [MG_IntegrationTest] (P7): every Magma lane pins the ICD the monolith arm has always pinned
Integrator ruling on the asymmetry package L reported. The six hand-laid Magma lanes (Fm,
Buffers, RunAhead, Caches, NamedBlit, P5fRsp) appended ${MGL_ITEST_COMMON_ENV}, so the monolith
DirectVulkan. arm ran with VK_ICD_FILENAMES pinned and the three lavapipe iterationRP repairs
forced on, while the 73 integration-magma-split entries ran against whatever ICD the loader
found first with those repairs at their device auto-detection default.

That is a latent flake rather than a semantic: the day a runner or a workstation grows a second
ICD, the lane measures a different driver from the gate it mirrors and nothing in its output
says so. Consistency wins - all six now append ${MGL_ITEST_VULKAN_ENV}, the same list the new
MGL_ITEST_VULKAN_{SPLIT,SPAWN,TCP}_ENVIRONMENT use.

The ICD pin follows the BACKEND, not the lane, in the two blocks that register both: NamedBlit
and P5fRsp loop over DirectGLES and DirectVulkan, and handing VK_ICD_FILENAMES to the DirectGLES
half would pin an ICD for a lane that never opens Vulkan. Verified in the discovery: all 73
integration-magma-split entries carry VK_ICD_FILENAMES and all three repair variables, and the
two DirectGLES.Split.NamedBlit. entries carry neither.

Re-run after the change, as the ruling asks: integration-magma-split 71 PASS / 0 FAIL / 2 SKIP
out of 73, unchanged - the two skips are the same ClipDistance pair with the same reason.
2026-09-22 14:24:19 -04:00
swung0x48 faca09d4ad [CI, docs] (P7): retrace-split exports the iterationrp knobs, and the two new Magma lanes gate
Three changes, one theme - making the two halves comparable, then gating what measured green.

retrace-split's "Retrace and validate under MOBILEGL_TRANSPORT" step exported no Magma knob at
all, while the monolith retrace job (:2183) exports three iterationRP repairs for
minecraft-1.21.4-fabric-iris-iterationrp-in-world x DirectVulkan. Those repairs are tri-state
quirks defaulting to device auto-detection and lavapipe is on no auto list, so that cell of the
split matrix (154 cells, and it contains that case x DirectVulkan x {inproc, spawn}) was being
compared against a monolith control running a different repair set. The same backend+case guard
is now copied verbatim into the split job.

Two gating steps for the lanes this wave added, and ONLY because they measured green on the
first run: integration-magma-{spawn,tcp} run like the existing magma-split step, and the
nineteen application-buffer cases get require_green BY NAME on both new arms - the tcp arm's
required list includes TcpServer.Start / Stop on purpose, because ctest lists the fixture
entries under the label and a lane whose supervisor failed to start has nothing to say about the
nineteen. The informational integration-magma-all-* tier is deliberately in no gating step.

Both steps were executed locally, not merely linted: spawn 19/19 and tcp 19/19 with
"require-green: N PASS, 0 skip, 0 failed, complete discovery". All 99 run: blocks in the
workflow pass bash -n.

The note records the first run in full: per-lane PASS/FAIL/SKIP, the two blocker groups the
first registration attempt produced (both lane plumbing, neither a server abort, wrong pixels
nor a hang), the arming evidence, both red-once demonstrations, the parity and G14 numbers,
what was NOT verified (no device, no retrace run, no G1) and six findings for the integrator -
the first of which is that P7 exit gate 1 has no host-side red list at all, which the wave 2
plan is built on top of.
2026-09-22 14:24:19 -04:00
swung0x48 e7e3265c47 [MG_Remote] (P7): the wire client's death notice is installed for any backend, not just Espryt
WireTables.cpp:740 installed the client-side object_death emitter only when
ActiveBackendType == DirectGLES. P6.5 wrote that because DirectGLES was the only backend with a
two-process lane, so the condition read as "the case this can happen in" rather than as a policy
- and under a Magma spawn or tcp client it is a silent hole of exactly the shape P6.5 closed for
Espryt: no notice installed, NotifyAndFree drops every object_death before freeing the slot, and
CtWireScenario's two death cases report ObjectDeaths=0 and prove nothing.

The two remaining conditions carry P6.5's intent, both halves of it: Transport == Spawn (this is
the remote client, the one with no local twin table; under inproc the backend's own dispatcher
is the right one) and GetStateObjectDeathOps() == nullptr (whoever installed first keeps the
notice).

RED-ONCE: with the install site short-circuited, both DirectGLES spawn death cases red on
CtWireScenario.cpp:187/:246 with

    Expected: (afterCounters.deaths) > (deathsBefore), actual: 0 vs 0

which is the silent zero this closes. Restored and green.

WHAT IT DOES NOT DO is give Magma a death table - that is wave 2 package C. But it already does
more than the plan predicted: with CtWireScenario's own BackendName() != "DirectGLES" skip guard
temporarily lifted (a probe, not committed), both DirectVulkan SPAWN death cases PASS, while the
DirectVulkan inproc arm still fails. So the two-process Magma death path works today and the
remaining gap is the inproc arm alone. Recorded in the package note; narrowing that skip guard
is package C's call and its file.
2026-09-22 14:24:19 -04:00
swung0x48 3d9a38a979 [MG_IntegrationTest, CI] (P7): the split arms take a backend, so Magma has two-process lanes at last
P7 exit gate 1 asks for integration green "under Magma push and split". Until now it was not
merely unmet, it was UNMEASURABLE: every arm of mgl_itest_register_split_arms hardcoded
MOBILEGL_BACKEND_TYPE=DirectGLES (:2002/:2025/:2031) and MGL_ITEST_MAGMA_WIRE_ENVIRONMENT
hardcoded MOBILEGL_TRANSPORT=inproc (:2456), so the shared integration-split / -spawn / -tcp
labels held ZERO DirectVulkan entries and the 73 integration-magma-split cases only ever ran in
one process.

The registration macro now takes the backend as its first argument; the old name is a thin
wrapper, so all 32 call sites are untouched and every DirectGLES name, label and private log
path is byte-for-byte what it was. Two tiers come out of it, and the CMake comment states both:

  * GATED - integration-magma-{split,spawn,tcp}. The six hand-laid Magma lanes (Fm, Buffers,
    RunAhead, Caches, NamedBlit, P5fRsp) grow arms through one shared shape macro; the inproc
    arm keeps its names, its labels and its log paths, so the CI steps and the two magma_*
    checker scripts that name them by hand keep working unchanged.
  * INFORMATIONAL - integration-magma-all-{split,spawn,tcp}, in no gating step. Every scenario
    the split arms run, on Magma, on all three transports, generated by REPLAYING the macro's
    recorded call list - so a scenario added to the split arms tomorrow joins the Magma census
    the same day. Its job is to be able to go red while CI stays green; a case moves up into the
    gated tier with the commit that retires its blocker.

The label spellings are chosen against ctest -L's regex (ID-131 again): the tier word goes in
the MIDDLE, because integration-magma-spawn-all WOULD have been selected by
-L integration-magma-spawn and pulled ~100 expected-red entries into a gated lane. Sub-labels
mirror it: the inproc arm keeps integration-magma-buffers, the others put the arm word first.

MagmaRunAheadScenario and MagmaWireCacheScenario stay on the inproc arm alone, and that is
MEASURED rather than assumed: with all three arms registered, all 21 entries red on spawn and
tcp with no Fatal in either role log - the first arms its hold through
ServerLoopInstance().SetBeforeRetireHookForTesting, a hook on THIS process's server loop, and
the second asserts transportName == "inproc" in its SetUp. Both are named in the CMakeLists and
in spawn_lane_parity.py rather than allowed to widen a tolerance.

split_log_paths.py's is_split prefixes gain DirectVulkan.Spawn. / .Tcp. - on those arms two
processes append to one path, so a shared log does not merely blur attribution, it interleaves
two sessions. spawn_lane_parity.py gains the Magma dimension, normalising only the ARM segment:
RunAhead.Credit1. and .Credit3. are two entries of one case and a tail-stripping normaliser
would fold them together and then report an equal-sized set that had lost one.

Measured on this tree (lavapipe): magma-split 73 unchanged and green, magma-spawn 52/52,
magma-tcp 52/52, the three informational lanes 102 each, GLES split 186 / spawn 102 / tcp 104
all unchanged and green. G14 against ~/w7/p7-before: 4155 to 4565, removed 0, added 410.
2026-09-22 14:24:19 -04:00
swung0x48 9e8fdacd21 [MG_Remote] (P7): gate 3 is judged per case against the monolith arm, and its denominator names what monolith already fails
Device window #1 E0a: monolith x DirectVulkan on the Redmi is 0.996-1.000 against the
desktop goldens (only OpenRA is 1.0), so "monolith 1.0" was an OpenRA fact, not a rule.
Gate 3 now reads: per case, inproc and spawn bit-identical over three repeats, within
the case threshold, and within 0.0005 of the same-session monolith reading. The
denominator excludes what monolith itself fails - create-indirect (OQ-17), the
1.21.11 main-menu trace artifact, and iris-photon, whose pipelines Adreno 830 refuses
with VK_ERROR_UNKNOWN (PipelineFactory.cpp:660) - which leaves 36. Gate 5 records the
real block sources: UBO lives only in the GTF module, SSBO joins, gl33 is the fallback.
2026-09-22 14:23:00 -04:00
swung0x48 8b02feb245 [android-plugin] (P7): the result verdict hands native python a host path whatever the caller set MSYS_NO_PATHCONV to
On Git Bash the script protects its own adb calls with MSYS_NO_PATHCONV=1, but the
two python invocations relied on the caller NOT having exported it; a session that
did (for the adb-side /data/... arguments the runbook mentions) got json.load failing
on /c/... for a result.json that existed - every case reported "trace replay failed"
over a passed result. host_path_for_adb already exists for exactly this; use it.
2026-09-22 14:17:42 -04:00
swung0x48 c890f0ad1a [docs] (P7): the runbook spells the side-by-side package the way the flavour builds it, and its two shell traps are removed
The application id is top.mobilegl.plugin.p7w1.trace (flavour suffix after the id
suffix), pgrep -f dex2oat matched the shell running the loop and never exited, and
adb.exe cannot stat an MSYS /c/ path once MSYS_NO_PATHCONV is set. All three were
hit on the first execution of section 3.
2026-09-22 14:15:29 -04:00
swung0x48 9be62cbc21 [tools] (P7): the UBO caselist comes from the GTF module, and the SSBO block joins gate 5
KHR-GL46 has no uniform_buffer_object group; the 90 UBO cases are
GTF-GL46.gtf31.GL3Tests.uniform_buffer_object*, present only when the deployed glcts
was built with GTF - the device probe decides. KHR-GL46.shader_storage_buffer_object
(124) is the block that exercises what cluster A actually refuses today: unaligned
and over-range SSBO bindings, not only UBO byte tails.
2026-09-22 14:03:58 -04:00
swung0x48 4e7989f1c2 [tools] (P7): the five CTS caselists for exit gate 5 are committed with their provenance
Cut from VK-GL-CTS 0b04c470ed78569b0b778ece1262d88455ab2b00 (main; the deployed glcts build's source) mustpass
gl46-main.txt and gl33-main.txt: texture_*, shader_image*, packed_pixels,
direct_state_access, uniform_buffer*. The gl33 variants are the fallback the runbook
names for an Android runner that cannot open a 4.6 core context; whichever version the
probe selects, BASE and AFTER read the same list. A 0.5 pp gate needs a committed
denominator, not a glob over someone's checkout.
2026-09-22 14:02:07 -04:00
swung0x48 484a56a5ae [scripts, MG_Pipe, MG_Backend, MG_Test] (P7): the census docstring's triage follows what the same wave retired, and the comments stop joining the @P7 census
Two corrections to the two commits before this one.

The widening's docstring was written against the 83 sites it found, and the commit that
routed five of them through Session::Fail did not revisit the prose. It now describes the
79-site floor the baseline actually records, says which five went and where, and states that
the enforceable artefact is the baseline's per-file map rather than the counts in the prose -
so a reader who finds the two disagreeing knows which one to believe. The three buckets are
restated too: `(ii)` is no longer "owned by a Ph / P7 row", because with the Magma wire arms
routed what is left there is the peer-reachable deaths in the applier and the two backends,
which is exactly the set ID-P7-1 scopes to Ph's fuzz arm 2 AFTER P7.

And the routing commit's own comments spelled the at-sign refusal marker while explaining
what routes through them, which put SEVEN comment lines into the retirement census: `grep
@P7` went from 15 sites in 4 files to 22 in 6, and that grep is how wave 2 will measure which
refusals it has retired. The prose says the word instead, and the count is 15/4 again.
2026-09-22 14:01:06 -04:00
swung0x48 a5dcbb444f [MG_Remote, scripts, MG_IntegrationTest] (P7): the supervisor names a faulted session, and the smoke that proves it is finally a ctest entry
Ph slice (1) (ID-P7-1). Three `::waitpid(active, nullptr, WNOHANG)` calls in the `--serve`
loop threw the status away, so a session child that died of SIGABRT inside SessionFail and
one that returned from its control loop and _exit(0)'d were the SAME EVENT to the
supervisor: `active = -1`, no line, nothing to count. ID-P7-1 declines the literal 98-site
latch translation because P6.5's fork-per-session already delivers "the next connection is
served as usual"; what was missing is that anyone could TELL a faulted session from a
finished one, which is what an operator, a fuzz arm and a flake triage all actually need.

One `ReapActiveSession()` where there were three call sites, asking WIFEXITED / WIFSIGNALED /
WTERMSIG - the same three questions ServerSpawn.cpp:289 asks of the same kind of child - and
logging `pid=<n> reaped exit=<c>` or `signal=<n>` with the supervisor's lifetime
`sessionsFaulted` beside it. A refused handshake exits 0 and is NOT a fault, which is right:
protocol.fbs:86 says a rejection is a normal session outcome. The running total is on every
reap line as well as on the summary, because the fixtures end a supervisor with SIGTERM and a
figure only printed at a chosen exit would never be seen.

THE IDENTIFIER ROW'S PREMISE IS WRONG, MEASURED. The plan asks for
CtrlEnvelopeBufferHasIdentifier at ServerMain.cpp:193 because ServerSession::ParseEnvelope
asks it and this loop did not - but the generated VerifyCtrlEnvelopeBuffer already IS
`VerifyBuffer<CtrlEnvelope>(CtrlEnvelopeIdentifier())` (protocol_generated.h:2378), so a
wrong identifier was refused here and ParseEnvelope's second call is redundant too. Recorded
for the integrator. What was real is that both refusals were SILENT: a peer whose
post-Welcome frame was garbage had its session ended with no line in the log, and a fuzz arm
that cannot tell "refused, by name" from "the child went away" measures nothing. So the
identifier is now asked first - it is the specific diagnosis where the verifier's is the
general one - with the length guard that asking it first requires, and both say so.

scripts/ci/tcp_supervisor_smoke.py has been in the tree since P6.5 asserting four supervisor
controls and was referenced by NOTHING - no ctest entry, no workflow step - so nobody could
have said whether those four were green. It is now `TcpLane.SupervisorProtocolControls`
beside the TCP lane's own fixtures, labelled integration-tcp, under the lane's resource lock
(a second supervisor's child would answer the other's peers with Busy). It needs flatc, which
is deliberately not in the build graph, so it resolves one gen_protocol.py's way and exits 77
- ctest SKIP - when there is none, rather than passing on a run that did not happen.

Two cases added to it: a session child killed with SIGABRT must be named by pid and
`signal=6` with `sessionsFaulted` one higher than before, and the next Hello must still be
welcomed; and a steady-state frame carrying the wrong file identifier must be named rather
than silently dropped.

Red-once, both ways, against the real image: making the reap discard the status again reds
with "the supervisor did not name session pid=N as killed by signal=6", and deleting the
identifier branch reds with "read a frame with the wrong file identifier without saying so".
2026-09-22 14:01:06 -04:00
swung0x48 e1ec69341a [MG_Pipe, MG_Backend, MG_Remote, MG_Test, scripts] (P7): the Magma wire fatals and the two Staged aborts go through Session::Fail
Five deaths in the split server image raised their own std::abort(). Three of them are
funnels - MagmaWireFatal (Renderer/WireFramebuffer.inc), WireDescriptorFatal
(Renderer/UniformManager.cpp) and WireBufferLegacyFatal (Renderer/WireDraw.inc) - and the
fifteen `@P7` refusals on the tree (8 in UniformManager.cpp, 4 in WireFramebuffer.inc, 2 in
WireDraw.inc, 1 in VulkanRenderer.cpp; plan section 1.3's count confirmed) all die through
two of them. Every one of those deaths published NO SessionFault frame, so the peer read a
bare EOF; bumped NO SessionFaultCount(), so exit gate S8 could neither confirm nor falsify
it; and carried a hand-typed family word with no path to FatalFamilies.def's projection.

MG_Backend may not name an MG_Remote symbol on its own account, so the route is a seam in
MG_Pipe with MGP_TRIP_WIRE_REPORT's shape (PipeApply.cpp:66, and the new code sits beside
it): MG_Pipe/PipeSessionFail.h declares MGPipeSessionFail, PipeApply.cpp defines it inside
`#if MOBILEGL_BUILD_DISAGGREGATED`, the DEFAULT behaviour is byte-identical to what the
three funnels did on their own, and MG_Remote::InstallPipeSessionFailHook() points it at
SessionFail. The install is in InitServerRoleCommon (MG_Backend/Init.cpp), step 0, because
that is the one function BOTH server roles run - the inproc role and the spawn/TCP session
child - so there is no per-transport install to get wrong.

MGPipeFatalFamily is a two-value boundary enum, NOT MGFatalFamily cast to an integer: the
.def's enumerators are positional and a row inserted in its middle would silently re-point
every hook call at a different family. FatalFunnel.cpp's adapter is the one place the two
vocabularies meet, as a switch with a named default.

The family word stays UnmigratedVerb rather than becoming a new MagmaWireRefusal. Three
reasons: the message strings are required to stay byte-identical (5.2), and the retrace
refusal censuses recorded since P5b count these deaths by that word - a rename invalidates
every `@P7` measurement taken against them; `Magma:` in the detail already separates the
Magma arm from the GLES UnmigratedVerb deaths, so a new word adds no discrimination a grep
does not have; and its wire projection would be ProtocolCorruption, exactly UnmigratedVerb's.
Recorded for the integrator.

Ph slice (2) in the same commit: Server/StagedShadow.h's RequireCovered and
Server/StagedTextureStore.h's RequireLevelBytes were MGLOG_F + bare abort. Both are inside
MG_Remote already, so they call SessionFail directly, message verbatim;
check_include_closure.py's four probes do not reach either header and the gate is green.

Census: 83 abort sites -> 79 over 20 files (five sites retired, one gained for the seam's
no-hook default, which is argued into FUNNEL_SITES), baseline rewritten in this commit - the
downward ratchet's notice is what asked for it. Two gate bugs the widening exposed and this
commit fixes: the SessionFail call regex also matched `MGPipeSessionFail(` (a lookbehind
now), and a funnel's anchor needed a wider window than a marker's (kFunnelAnchorWindow).

Red-once, three ways. Pointing MagmaWireFatal back at its own abort reds the census naming
WireFramebuffer.inc 0 -> 1. Making the seam skip the hook reds both new death cases -
`TheMagmaWireVerbDeathReachesSessionFailAndBumpsTheFaultCount` on the exit code, because it
reads SessionFaultCount() out of a SIGABRT handler and gets 0 instead of 1, and
`TheSeamHandsTheInstalledHookItsFamilyAndItsLineVerbatim` on the line the hook echoed.
2026-09-22 14:01:06 -04:00
swung0x48 f141918aa1 [scripts, MG_Remote, MG_Impl, MG_Test] (P7): the fatal census gate widened to the split server image
`scripts/ci/fatal_census.py` scanned `MG_Remote/` and saw four abort sites. CONTRACT-P6
§12.1 already says what the server image is - "P6's server image is the whole
libMobileGL.so" - and the server process was executing seventy-nine more deaths the gate
could not see: 15 in DirectGLES.cpp, 12 in Managers.cpp, 11 in VkBufferManager.cpp, 7 in
PipeRoute.cpp. SCAN_ROOTS is now MG_Remote / MG_Pipe / MG_Backend / MG_Impl/Pipe /
MG_State, `.inc` joins `.cpp`/`.h` (the Magma wire arms and the generated wire records are
.inc files, and 20 sites are reachable only through one), and MG_Benchmark /
MG_IntegrationTest join MG_Test in the exclusion.

Four rules now, where there were two:

  1. an abort names its family within twelve lines, OR it is argued into FUNNEL_FILES /
     the new FUNNEL_SITES table. FUNNEL_SITES exempts an individual funnel inside a file
     that has ordinary named deaths around it, and only when one of the file's anchors
     appears in the same window - so the exemption follows the funnel if it moves, expires
     if it is deleted, and does not cover an unrelated abort added to the same file. Two
     entries, one line of reason each: PipeApply.cpp's MGP_TRIP_WIRE_REPORT and
     PipeWire.inc's MGPipeWireProtocolFatal.
  2. unchanged: a WireLogFatal / SessionFail call carries a `Fatal{` in its string.
  3. NEW: every family word found in the perimeter has a row in FatalFamilies.def. Eleven
     words did not, and now do (see below).
  4. NEW: every `Refuse{Word}` names a RefuseCode enumerator. This is what catches
     `Refuse{AuthenticationRequired}` (SocketTransport.cpp:161), a word protocol.fbs has
     never had - PH-7 (2). Renamed to the enum's own `Authentication`, and
     SocketTransportTest pins the spelling to `EnumNameRefuseCode` so the two cannot
     drift again.

`abort_sites` was recorded and never compared. It is now a DOWNWARD ratchet with a
per-file map beside it, so "83 became 84" reports as "VkBufferManager.cpp went 11 -> 12,
here are its lines"; fewer sites - the direction `@P7` retirement moves the number - asks
for a re-baseline rather than blocking one.

Eleven new `.def` rows for the eleven rowless words. Ten are ProtocolCorruption for the
reason that bucket already states. ResourceUnavailable is ServerCrashed, not SegmentMismatch:
SegmentMismatch means a shared SEGMENT was the wrong size, and telling a peer that when a
VkBuffer allocation failed would be a lie about whose geometry was wrong. A twelfth word was
collapsed rather than given a row - `InitialBytesNotCarried` (PipeFill.cpp) said the same
thing as `UncarriedInitialBytes` (WireTables.cpp) about the same record, and two words for
one family is the drift a6 censused.

Red-once, four ways: an unnamed `std::abort()` appended under MG_State reds naming the file
and the line; deleting the `Fatal{` from PipeRoute.cpp's ResidualBlockSize reds; putting
`Refuse{AuthenticationRequired}` back reds rule 4; a typo'd family word reds rule 3 and the
vocabulary ratchet together.
2026-09-22 14:01:06 -04:00
swung0x48 1e2d076633 [docs] (P7): the wave-1 device window runbook
One reboot-clean session on Redmi 2f7cbe2e, copy-pasteable, with the shell each
block runs in marked: pin and wake/Doze, APK install identity, P7-7 E0-E3, the
P6.5 residual 39-case DirectGLES TCP golden matrix, the CTS $BASE for monolith x
DirectVulkan, and the temp-environment restoration. Every step states in one line
which result supports which hypothesis, and where the evidence is archived.

Four things the runbook had to settle rather than restate:

E1's proof. `running lockstep` is NOT the line that proves lockstep took effect:
ClientSession.cpp:1873 guards it with `RunAhead != 0 && m_barrierArmed`, so it is
the "the knob is set but the server lacks kCapRunAheadApply" warning and
MOBILEGL_IPC_RUN_AHEAD=0 prints nothing at all from LatchRunAheadFromCaps. The
proof is a line present and a line absent: `Config: IPC ... run-ahead=0 ...`
(ConfigLoader.cpp:459) with zero `run-ahead ARMED` - the shape p5f/magma-runahead.md
already recorded for the RA=0 arm.

E1 will exit non-zero, by design. --transport inproc adds --require-inproc, whose
gate demands run-ahead=1 in the Config: IPC line (trace-replay-ci.sh:601), and the
proof runs AFTER result.json is pulled and checked. So the SSIM and the archive are
usable and the arm is verified by hand against the table above; dropping the --env
to make it green would delete the experiment.

E0's budget. 39 cases x 2 transports x 3 repeats is 234 replays, roughly 3-4 h,
which does not fit beside the TCP matrix and CTS in one window. It is split into a
one-pass census that produces the divergent-case list and a three-repeat pass over
that list plus two controls - the determinism question only has meaning for cases
that already diverge.

CTS is flagged, not promised. No caselist is in-tree, and the local VK-GL-CTS
checkout has an empty external/openglcts/data with no mustpass file anywhere, so
the five blocks cannot be cut in-tree; the exact pre-window procedure to fetch a
tagged checkout and commit tools/cts/caselists/p7-*.txt is written out instead.
And KHR-GL46 is unproven on the Android runner - every device reference result in
the skill is KHR-GL33, the 4.5 numbers are desktop lavapipe, and the platform hands
the package's version straight to eglCreateContext - so the runbook carries a
five-minute pre-window probe and an explicit integrator decision: if 4.6 is
unreachable the baseline is taken on KHR-GL33 and the AFTER reading must use the
same version, because 0.5 pp between two different API versions means nothing.
2026-09-22 13:58:07 -04:00
swung0x48 3ba150d021 [tools, android-plugin, CI] (P7): the device matrix runner selects DirectVulkan, repeats are archived, and the spawn env parse is entry-aware
wave 0 item 5. Three instruments P7's device window needs, and the CI steps that
can make each of them red.

run_tcp_matrix.py --backend DirectVulkan end to end. --backend already existed and
already filtered by each case's ci_backends; what did not exist is a per-backend
GOLDEN. The two backends shared `golden` plus an optional `alternate_golden`, and
alternate_golden is an OR across backends, not a per-backend key - so a
DirectVulkan run that matched the DirectGLES picture passed, which is the "went
green on the wrong arm" shape the exit gate exists to catch. trace_cases.json
gains `backend_overrides.<backend>` ({golden, alternate_golden, ssim_threshold}
only: restating target_call or a crop would stop the two backends retracing the
same frame through the same window). A per-backend golden REPLACES the shared
pair, matching another backend's declared image is refused BY NAME, and a --case
named explicitly that does not run a requested backend is an error instead of one
plan fewer and no message. The CMake emitter and the APK matrix refuse a case that
declares the key rather than silently emitting one golden for both arms.

DirectGLES is unchanged and that is asserted, not asserted-in-prose: a case with no
override resolves to itself, and the key is dropped from the resolved case even for
the backend that overrides nothing - so giving DirectVulkan its own golden does not
move the DirectGLES identity fingerprint and does not invalidate its checkpoints.
Each results.json row now records the golden, alternate golden and threshold it was
scored against.

run_android_retrace_local.py gets --matrix (the CI split subset, 39 cases, each
running only its own ci_backends - not --all, which adds the rd12 workload ID-P7-4
excludes), --repeat N, and --archive-dir. The archive is the point: the result root
is keyed by case and backend and by NOTHING else, so repeat 2 overwrites repeat 1
and the only artefact that survives a three-repeat run is the last one, which makes
"were the three repeats the same picture?" unanswerable after the fact - in the one
place where re-running to find out is expensive. It also refuses --benchmark with a
non-monolith --transport, which has always produced a monolith run under a spawn
label (run_benchmark_case passes args.env alone; p6/README.md records the pitfall).

compare_actuals.py computes the pair result.json cannot: SSIM between two actual
PNGs, plus a case x repeat x ssim-vs-golden x ssim-vs-first-repeat table over an
archive. It is the retrace gate's OWN SSIM - a transcription of ComputeChannelSsim
/ ComputeRgbSsim (trace_replay_core.cpp:630/:671), the only SSIM in the tree - and
the crop comes from each repeat's own result.json, so the two columns describe the
same rectangle. PNG decode is stdlib zlib, because an unavailable Pillow in the
device window would turn "the repeats diverged" into "the tool did not run".

TraceReplayActivity: resolveSpawnServerPath's two decisions were String.contains()
on the whole joined K=V;K=V list and neither was entry-aware. The one that bit:
`--env MOBILEGL_IPC_SERVER_PATH` with no '=' is the documented UNSET spelling
(trace_env_overrides_test.cpp:92) and the only way to ask for "spawn with no image"
and watch LaunchServer refuse by name - it did not match "MOBILEGL_IPC_SERVER_PATH="
so a resolved path was appended AFTER it and, because ApplyEnvOverrides applies in
order with setenv(...,1), the deliberate unset was silently undone. That is exactly
the "silently replacing it would make that knob untestable" the method's own comment
forbids. Two more: the key inside another entry's VALUE flipped both decisions the
wrong way, and the transport match was case-sensitive while ConfigLoader lower-cases
the value before matching (ConfigLoader.cpp:322). The parse moves to
SpawnServerPath, a class with no android.* import, so test_android_lifecycle.py
compiles and runs it with plain javac - the parse had no host-side coverage at all,
and its on-device failure mode is "the knob had no effect".

Only spawn gets a path, deliberately: it is the only transport that execs an image.

Red-once, all four: a stub case whose result names the DirectGLES golden fails a
--backend DirectVulkan run with "matched a golden another backend declares"; a
synthetic PNG pair with a closed-form SSIM (identical = 1.0 exactly, constant
black vs white = C1/(255^2+C1), inverted halves = (C2-2v)/(2v+C2)); the pre-fix
resolveSpawnServerPath body fails eight of SpawnServerPathTest's checks; and the
DirectVulkan TCP proof battery is the DirectGLES one under the other prefix,
because an empty prefix selection raises nothing and reads as a clean pass.

CI: test_run_tcp_matrix.py and test_compare_actuals.py join the R-16 step. The
matrix driver's stub regressions had NO CI step - they were run by hand and
recorded in a note - and they guard the tool that decides whether a device golden
run passed.

Counts in WSL: test_run_tcp_matrix 12 -> 18, tcp_lane_tools 4 -> 6,
test_compare_actuals 12 new, test_android_lifecycle green with the new Java unit.
2026-09-22 13:58:07 -04:00
swung0x48 e19095269b [docs, MG_Remote] (P7): the roadmap rows say what the tree says, and CONTRACT-P7 is drafted
ROADMAP.md: the Ph / P3b-P4b / P7 rows carry the audit's verdicts and the factual
corrections it found - the @P7 refusal list is 15 sites in 4 files (the row printed 8),
the 29-commit DirectVulkan shape wave was never recorded, the device divergence is two
observations with no per-case attribution and no lockstep control, SetupDrawSnapshot /
content-addressed CSO are declined for this stage, "XFB scatter to client" is retired
in favour of the two real holes, readback/pack state splits into closed and open
halves, the NDK size pin is obsolete, PACK-PBO readback landed in 70df57bb, the ABI
fingerprint landed in P6.5, and OQ-8 / OQ-10 / OQ-13 / OQ-21 get their current answers.
P6-ENDSTATE-REVIEW.md:284 qualifies an Init.cpp citation the lint reported ambiguous.

MG_Remote/CONTRACT-P7.md: rules I (no std::abort on the Magma wire arm) and J (every
fix reds once on a two-process arm), the five exit gates in testable form, the
retire-vs-decline table for every @P7 name, the ratchet's definition and P7 target,
the nine-subsystem denominator for the 40% checkpoint, the wave-2 file partition,
and the amendments to CONTRACT-P6. Census, ratchet and lane numbers are marked
待填 until wave 0's packages report them.
2026-09-22 13:26:34 -04:00
swung0x48 e8b2c4bd05 [docs, MG_Test] (P7): the Ph / P3b-P4b / P7 program plan, and the pull flavour builds its tests again
Plan for driving the roadmap to P7 close-out, from a per-item audit of the three
rows against a96875bf (19 reader groups, two refuters per done/obsolete verdict,
one completeness critic): what is landed, what is declined and why, what remains
with file anchors, five waves, the P7 subsystem denominator for the 40%
re-baseline checkpoint, and eleven integrator decisions (ID-P7-1..11).

The three findings the roadmap text did not carry: Magma has zero two-process
integration coverage (every split arm hardcodes DirectGLES), the 184-symbol
ratchet has no instrument, and the device DirectVulkan divergence was never
diagnosed (the lockstep control was never run under golden).

MG_Test/Util/PipeStatsTest.cpp: the pull flavour (MOBILEGL_PIPE_PUSH=OFF) did not
compile since the P6 wrap-up - the window-arithmetic case used the push-only
PipeStats::Gauge API unguarded. CI's pull control builds with
MOBILEGL_BUILD_TEST=OFF and never saw it. The gauge lines are now #if
MOBILEGL_PIPE_PUSH; the case name stays registered in both flavours for G2/G14.
2026-09-22 13:03:20 -04:00
swung0x48 b95f5f1a20 [docs] (P6.5): the first wave recorded as landed - review + fixes + representative re-validation, residual to CI 2026-09-22 11:58:22 -04:00
swung0x48 fe28bdb5f3 [MG_Remote, MG_Pipe, MG_Test, CI, tools, docs] (P6.5): the first wave completed and reviewed - archive v2, publication boundary, half-close, AcceptPair/role fixes, device re-validation 2026-09-22 10:56:19 -04:00
swung0x48 018fab0f5b [WIP] (MG_Remote): implement P6.5 TCP and stream transport 2026-09-22 07:19:18 -04:00
swung0x48 242fd45cd7 [Fix] (CI): read role logs and retry device disconnects 2026-09-22 07:19:17 -04:00
swung0x48 9043442454 [MG_Test, CI] (P6): the last two readers the role split left behind
build-linux-verify could not compile PipeStatsTest: RoleDerivedJsonDumpPathForTesting is DISAGGREGATED-only and the verify flavour is deliberately monolith. The case name stays registered and GTEST_SKIPs there, matching the PIPE_PUSH pattern, so ctest -N still matches between flavours.

runtime_mode_proof.py logs opened MOBILEGL_LOG_FILE_PATH as a file. After the split that name is a base and the sink writes <base>.client.log / <base>.server.log, so the inproc proof died on FileNotFoundError after a green lane. It now reads through split_log_paths' role helpers and names the paths it looked for when none exist.
2026-09-22 04:40:03 -04:00
swung0x48 35b3283bd9 [docs] (P6): the closeout - gates 7 and 8 are measured and P6 is closed
The device gate: Redmi 2f7cbe2e, reboot-clean, paired A/B in one thermal
window (arms interleaved, best-of-3 then best-of-2), rd12 + DirectGLES.
inproc vs spawn client CPU p50 7.880/7.889 vs 7.876/7.869 (-0.15%) and
7.987/7.989 vs 7.992/8.009 (+0.16%) - a tie inside the 0.2-4.2% within-
arm spread, sign flipping with arm order, recorded and not gated.
monolith is ~30% dearer. 22/22 runner exits clean, 44/44 PINNED, zero
Fatal.

The three mandatory numbers: socket doorbell vs condvar is a tie too -
client 121.1 waits/frame identical across transports, server 10 866-11
963 (inproc) vs 10 415-10 416 (spawn) waits/frame, and the contract's
'converting that into ~4 blocking reads may be cheaper' did not pay off;
the only column where the socket is worse is clipark/f (6.07 vs 5.60/5.75).
SEG_STAGE bytes/frame: 130.8 KB median / 8.01 MB steady (OpenRA), 816.2
KB / 1.53 MB (rd12). Records/frame post-chunking: 211/206 and 7 497/7
504. maxrec stays 1808 - the 8 MiB chunk budget is never reached. Both
framings reported throughout (the startup window runs ~7x the steady
state) rather than the flattering one.

MEASUREMENTS.md gains section 12 with the full protocol and tables; the
6.3 'no post-chunking distribution exists' gap now points there.
CURRENT_STAGE_PROGRESS.md marks gates 7/8 met and records the closeout
work plus two structural limits (spawn client windowed fields are zero
by construction - OnPresent runs in the server process - though its run
totals are valid stores; the stats dump is role-derived). ROADMAP marks
P6 closed in the stage table, the header and the milestone.

Evidence: notes/p6/gate7-device-ab.md, gate8-doorbell-device.md,
gate8-wire-counters.md, git-worktree-fix.md. check_doc_citations: 82
citations in the three documents, 0 problems.
2026-09-22 03:38:17 -04:00
swung0x48 06b61bd84e [MG_Util, MG_Remote, tools] (P6): the wire counters and the spawn server's stats latch
Gate 8's last two numbers (SEG_STAGE bytes/frame, records/frame post-
chunking) had no producer at all: the summary line's byte classes are
backend-to-driver bytes and maxrec is a max, not a per-frame. Two push-
guarded counters answer them at their single choke points - ByteClass::
StageSegmentBytes at PipeWireCodec's StageAllocate (the only allocation
behind StageBytes, so every buffer walk, texture slab and CSO archive
lands there and emitters cannot double-count) and CallClass::WireRecords
on EncodeRecord's committed path (post-chunking an emitter no longer
knows how many records its one call produced). A staged-blob histogram
rides along. G1 holds: symbols 0/0/0/0, .text byte-identical, pull build
carries no MG_Remote symbols.

The spawn server never counted anything: PipeStats::Init() is a step of
MobileGL::Initialize(), which ServerMain never reaches, so every Enabled()
site in that process - including ServerLoop's own ServerWaits/ServerParks
publish - stayed false and the server log carried no summary line at all
(a zero that means 'another process' printed in the shape of one that
means 'never waited' is the milder misreading; the line was simply
absent). ServerMain now arms and closes PipeStats at its role boundary -
after MG_ConfigLoader::Init (Init latches from the parsed config), after
the server-role latch (the log sink's role), and before ::_exit(0).
Red-once: 0 server summary lines before, 29 after, SSIM 1.0 unchanged.

MOBILEGL_PIPE_STATS_FILE now derives per role through the log sink's
exported RoleLogPath rule - run.client.json / run.server.json, the bare
name resolves to no file - because both roles reach FormatJson() and ofstream
trunc made the second writer replace the first. G1 caught three separate
leaks on the way (a string temporary in the monolith body, one resized
Shutdown() from unequal #if arms, two rewritten MGLOG_W literals in
.rodata); the pull arm is statement-for-statement the original.

Device tooling for the paired A/B lands with it: pin_device.sh gains a
measured 2f7cbe2e entry (this board clamps the GPU at 1050 MHz -
thermal_pwrlevel writes 0 and reads back 1, max_gpuclk is read-only;
absolute times are not comparable with the 1100 MHz-era rows), and
tools/device_bench/p6/ carries the session collector and renderer beside
run_android_retrace_local.py's MOBILEGL_TRACE_PACKAGE override.

Unit 2367/2367 (PipeStats 25/25), spawn integration 102/102.
2026-09-22 03:37:51 -04:00