[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.
This commit is contained in:
2026-09-22 16:07:13 -04:00
parent 07079c3c0e
commit dce9953d36
12 changed files with 270 additions and 35 deletions
+80
View File
@@ -0,0 +1,80 @@
# P7 wave 2 package C — Magma object death, resident sub-data, the reflection archive, the vertex-layout split
Base: `p7/magma-c` off `feat/disaggregated @ a686131e` (pipe HEAD at branch time).
Build: `build-split` = `-DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON
-DMOBILEGL_PIPE_PUSH=ON -DMOBILEGL_BUILD_INTEGRATION_TEST=ON`, ICD pinned to
`/usr/share/vulkan/icd.d/lvp_icd.json` (the same lavapipe ICD `~/w7/pipe/build-split` pins; the
worktree tool leaves `MOBILEGL_ITEST_VK_ICD` empty and CMake warns about it).
Contract rows: CONTRACT-P7 §5.5 (`StateObjectDeathOps`), §5.4 (OQ-10), §5.3 (OQ-8), §3.2
(`vertex-layout`, `vertex-format-conversion`). Discipline: rule I (no `std::abort()` on a Magma
wire arm), rule J / R-16 (every fix red once on a two-process arm first).
---
## Slice 1 — Magma `StateObjectDeathOps` (§5.5)
**What landed.** `MG_Backend/DirectVulkan/DirectVulkan.cpp` gains
`g_magmaStateObjectDeathOps` and `InstallStateObjectDeathOps()`, installed from
`BackendObject_DirectVulkan::Initialize()` — i.e. from `ServerLoop::CreateBackend`, step 1 of
`InitServerRoleCommon`, which both Magma server roles (inproc and the spawn/TCP child) run and
which is before any client session exists. Guarded `#if MOBILEGL_BUILD_DISAGGREGATED &&
MOBILEGL_PIPE_PUSH`, so the pull build compiles nothing new (G1).
**The one arm, and why it is only one.** Espryt's `g_glesStateObjectDeathOps` has two jobs: emit
the wire `object_death` record off the apply thread, and otherwise destroy a backend twin keyed
by lifetime id. Magma has no lifetime-id-keyed twin at all — its two client-minted kinds
(`VertexElementsCso`, `Buffer`) are keyed on `{slot, gen}` by `MagmaPipeIdentityTables` and the
other six are still reached from their frontend objects — so the Magma table is exactly the EMIT
arm and nothing else.
**Correction to the plan's wording (worth the integrator's attention).** Plan §1.3 and the task
brief describe the gap as a client SLOT leak provable with `PipeSlotPeek`. It is not, and has not
been since P4a: `MG_Impl/Pipe/PipeFill.cpp`'s `NotifyAndFree` frees the client slot
unconditionally and backend-neutrally, so `PipeSlotPeek` never saw a Magma leak and does not see
a change now. What leaked was the **server's twin**. A framebuffer has no wire delete opcode at
all (BRIEF-P4A D-I2), so `object_death` is its only death delivery; with no consumer installed
under Magma inproc, the server's record stayed `Live` for the life of the session. The
`CtWireScenario` framebuffer case now reads BOTH numbers — the server's `deaths` tally and the
client's `PipeSlotKind::Framebuffer` live count — so a future regression in either half is
attributable.
**Cleanup of the Magma special cases.** The two executable ones are the `CtWireScenario` skip
guards (`:153` / `:209`), both deleted. Everything else the audit counted is comment text, and it
is reworded rather than deleted because the underlying argument survives: `PipeSlotPeek.h`,
`MagmaPipeArms.h` (the age-sweep-instead-of-death-notice paragraph — still true, because the new
table never touches the per-renderer identity tables), `PipeMutation.h`, `PipeFill.cpp`,
`VertexArrayObject.cpp`, `DirectGLES/Managers.cpp`, `HandleRecycleScenario.cpp`,
`WireTables.cpp`.
**The install-order question, answered rather than assumed.** `SetStateObjectDeathOps` is a bare
last-writer-wins pointer with no stacking, so two installers could in principle race.
They cannot meet: 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 no backend (`InitSplitRoles` builds a `BackendObject_Remote` there). The
red-once below measures exactly that split of responsibility.
**Proof (green).** `ctest -R "DirectVulkan\.(Split|Spawn|Tcp)\.Ct\..*Death"` — 6/6 pass
(`integration-magma-all-{split,spawn,tcp}`), plus the two `TcpServer` fixtures. DirectGLES
control `ctest -R "DirectGLES\.(Split|Spawn|Tcp)\.Ct\..*Death"` — 6/6 pass, unchanged.
**RED-ONCE (executed).** `BackendObject_DirectVulkan::Initialize()`'s call changed to
`if (false) InstallStateObjectDeathOps();`, rebuilt, lane re-run:
```
1/8 DirectVulkan.Split.Ct.CtWireScenario.TextureDeath... ***Failed
2/8 DirectVulkan.Spawn.Ct.CtWireScenario.TextureDeath... Passed
4/8 DirectVulkan.Tcp.Ct.CtWireScenario.TextureDeath... Passed
5/8 DirectVulkan.Split.Ct.CtWireScenario.FramebufferDeath... ***Failed
6/8 DirectVulkan.Spawn.Ct.CtWireScenario.FramebufferDeath... Passed
7/8 DirectVulkan.Tcp.Ct.CtWireScenario.FramebufferDeath... Passed
75% tests passed, 2 tests failed out of 8
CtWireScenario.cpp:264: Failure
Expected: (afterCounters.deaths) > (deathsBefore), actual: 0 vs 0
```
Restored and rebuilt: 8/8 green again. This is precisely the shape package L's temporary probe
predicted (`notes/p7/magma-two-process-first-run.md` §6): spawn and tcp were already carried by
`InstallClientWireTables`' emitter, and **inproc was the only hole** — which is the hole this
table closes.