diff --git a/MobileGL/MG_Remote/CONTRACT-P5C.md b/MobileGL/MG_Remote/CONTRACT-P5C.md new file mode 100644 index 00000000..ab905185 --- /dev/null +++ b/MobileGL/MG_Remote/CONTRACT-P5C.md @@ -0,0 +1,450 @@ +# CONTRACT-P5C — the wire contract for zeroing the inproc split's direct cross-role memory access + +Authority: this file, beside `CONTRACT-P5.md` (table 0's encodings, the byte carriers, field +ownership, role/thread ownership, the knobs, R-1…R-17) and `CONTRACT-P5B.md` (the 25 migrated +class-C slots). Where it disagrees with either, this file is newer and wins, and §7 lists +every such place. + +**How to change it.** Package c0c's file, edited by the integrator first. A P5c package (tx / +ev / hd / ct / rv / gt) that needs a row changed goes through the integrator; the packages +compile against the rows below from day one, which is the whole reason the file exists. + +Base: `feat/disaggregated @ 11ac3de6` (the P5c plan commit; code-identical to the audit head +`a79a0af6`). Every `file:line` below was read at that commit. The audit this contract +discharges is `~/w7/notes/p5c/p5c-audit-v1.md` (59 sites); ROADMAP.md "P5c 计划" tabulates +them as T1–T5, B1–B4, R1–R5, C1, G1–G6, A1. + +--- + +## §0 What P5c is, and the one rule above every row + +P5/P5b put verb records, `SEG_STAGE` blobs, replies and caps snapshots on the wire. The audit +proved the two roles still read and write each other's memory directly at 59 sites, correct +only because the verb barrier and the shared address space happen to hold. P6 replaces how +the rings are delivered; it cannot replace a read of memory that does not exist in the other +process. **P5c makes `inproc` honest: between the two roles, nothing crosses except +`SEG_CMD` / `SEG_STAGE` / `SEG_REPLY` / `SEG_EVENT` / the caps snapshot / control frames — +so that P6 is a transport swap and nothing else.** + +Rules A (a content record declares its bytes), B (no host pointer crosses) and C (an applier +entry point may not hold a pointer past its return) are CONTRACT-P5 §0's and bind every row +here unchanged. Rule D (a verb crosses AS THE CALL) is CONTRACT-P5B §0's and is NOT relaxed: +P5c changes WHERE the sink resolves the call's objects from (the record's handles, §3), not +what the record carries. P5c adds one rule: + +**Rule E — a role may not name the other role's memory.** With an active transport, the +apply thread resolves every object from a handle the record carried (`§3`), every byte from +`SEG_STAGE` adopted into server-owned storage (`§2`), and every reverse notification is a +`SEG_EVENT` record (`§4`); the GL thread reaches server state only through replies, events +and the caps snapshot. A violation is `Fatal{RoleViolation, ""}` and the two guard +layers that raise it are §6's. "The barrier held" and "same address space" are no longer +accepted as mechanisms anywhere in this file. + +Refusal vocabulary is CONTRACT-P5B's, unchanged: `Fatal{ProtocolCorruption, "."}` +for a record the codec cannot prove, `Fatal{UnmigratedVerb, "[+]"}` for a +shape no sink implements, a null backend slot DECLINES (`return false`). P5c adds one family: +`Fatal{RoleViolation, ""}` (§6) and `Fatal{EventRingOverflow}` (§4.4). + +--- + +## §1 Table 0 additions — encodings the P5c rows introduce + +| field | the ruling | zero means | who reads it | +|---|---|---|---| +| **reverse-channel `MGPBlobRef`** | An `OnBufferWriteback` blobref on the wire names **SEG_EVENT**: `Seg = Wire::kSegEvent` (4, `PipeWireCodec.h:62`), `Offset` = the byte offset of the inline payload inside the SEG_EVENT mapping (`EventRingConsumer::OffsetInSegment`, `EventRing.h:182-185`), `Size` = the head's `Size`, cross-checked against the record's tail. **Never a host address** — rule B binds the reverse direction exactly as it binds `MGHostSpan` (R-2). Under monolith the blobref keeps its P3a shape (`Seg = kMGHostSpanSegNone`, `Offset` IS the mapped address); the two shapes are distinguished by `Seg`, not by the transport knob. | `Size == 0` with a live resource is legal (a zero-length writeback) | the client consumer (`ResourceTracker.h`, §4.3) | +| **`MGPObjectDeath` reuses `MGPHandleOnly`** | `{Handle, Kind, Pad0}` (16 B, `MGPipeTypes.h:93-98`). `Kind` is the `MGPipeKind` of the dead frontend object, widened to `Uint32` exactly as the existing rows widen it. One payload for all seven kinds the death switch handles (`Managers.cpp:224-257`): Texture, Framebuffer, Renderbuffer, SamplerCso, ShaderCso, SamplerViewCso, VertexElementsCso. | `Handle` null = "the object never crossed" — the client emits NOTHING in that case (§5.2), so a null handle arriving is `Fatal{ProtocolCorruption, "ObjectDeath.Handle"}` | `ServerVerbSink::OnObjectDeath` | +| **`MGPApplierReset::ContextSerial`** | `Uint64`. The client-side context's make-current serial at the edge that primed it. P5c has exactly one context per session, so the value is ASSERTED equal to the session's, not dispatched on; P6's multi-context shape reads it for real. | 0 = the first make-current | `ServerVerbSink::OnApplierReset` | +| **`MGPContextValues`** | The rv field table (§5.3), one fixed-width POD, `MGP_ASSERT_POD`-pinned. Whole-record hash-suppressed like every other `set_*` call: it crosses only when one of its source values moved since the last emission. There is no dirty mask in the payload — a suppressed record means "nothing moved", never "field invalid". | — | `MGPipeApplySetContextValues`, the server accessors it feeds | +| **texture per-level extent, server-side** | `max(1, base_extent >> level)` computed from the descriptor's `Width/Height/Depth` and `Levels` (`MGPResourceDesc`, `MGPipeTypes.h:295-360`). The client does NOT emit per-level extents; the derivation is the mip chain's definition and two honest ends compute the same number. | — | tx's `SyncMipmapsToBackend` (§2.2) | +| **`EventKind` = 4: `kEventGlError`** | `EventRing.h`'s enum gains `kEventGlError = 4` beside the existing three (:46-51). Head `EventGlErrorHead { Uint32 Code; Uint32 MessageBytes; }` (8 B) followed by the NUL-terminated message inline, `MessageBytes = strlen + 1`, **capped at 1024** (a longer message is truncated at the producer; the cap is a static_assert, not a check). `Code` is the frontend `ErrorCode`, widened. | `MessageBytes == 0` is `Fatal{ProtocolCorruption, "kEventGlError"}` (the NUL travels, rule A's twin) | `ClientSession::DrainEventRing` | + +--- + +## §2 tx — the server's texture staged shadow + +The texture half of `resource_subdata` stages the level bytes into `SEG_STAGE` today and +`ApplyTextureUpload` drops the pointer (`PipeApply.cpp:989-1036`: the gate, the accumulation +and the serial — the `bytes` parameter is used only by the non-null gate at :1006-1008). +Espryt then re-reads the client's `MipmapStorage` at sync time (Managers.cpp:6940, :7129, +:7198, :7315), reads per-level shape from the client object (:7314, :7332, +`DirectGLES.cpp:8502-8507`, :8528-8529), and falls back to the client for dirty regions with +no pending upload (Managers.cpp:7360, :7371). Magma WRITES the client's level storage +(`VulkanRenderer.cpp:1562-1609`: `AllocateStorage` + `MarkStorageDirty(..., false)`). +**tx ends all of it: the staged bytes are adopted, and the server's sync reads nothing but +its own shadow and the descriptor.** + +### 2.1 `StagedTextureStore` — ownership and coverage + +`MobileGL/MG_Remote/Server/StagedTextureStore.h`, the texture twin of `StagedShadow.h` +(R-11), and held to its four rulings: + +1. **Keyed by the texture resource twin's address** (stable, SharedPtr-held, exactly as + `StagedShadowStore`'s buffer keys, `StagedShadow.h:50-53`). Every event that ends a key's + life — respecify, destroy, context death — already has a call site to drop it from. +2. **Coverage is EXACTLY what the records declared, per (uploadTarget, level), never + widened.** `Adopt(key, uploadTarget, level, extent, regions, regionCount, bytes)` copies + the staged run into server-owned level storage sized to the level's extent and records the + region set. A sync that reaches a texel no record covered is + `Fatal{StageSnapshotTooNarrow, ""}` — the same words as the buffer half, and for the + same reason (`StagedShadow.h:96-116`: moving zeroes into the store is silent data loss, + not a missing optimisation). +3. **`copies` is a constructor parameter**, false reproducing the monolith expression + character for character (the client shadow answers, nothing is allocated). G1's pull-build + byte-identity is preserved the same way R-11 preserved it. +4. **A header with diagnostics**, not a block inside Managers.cpp, so a unit case builds one + store of each kind and asserts the difference (R-16 — `ServerLoopTest.cpp:616-760` is the + shape the tests copy). + +`PendingUpload` (`PipeApply.h:289-295`) gains NO byte pointer: the record's bytes move to the +store at apply time, and the pending set keeps its job of naming which (target, level, box) +is dirty-by-record. The store, not the pending set, is the byte owner; a pending upload with +no store coverage is the `Fatal{StageSnapshotTooNarrow}` case, not a silent re-read. + +### 2.2 What the sync path reads after tx + +| the read today | after tx | +|---|---| +| level texels from client `MipmapStorage` (`MapMipmapData`) | the store's adopted bytes | +| per-level extent from the client object | §1's derivation from the descriptor | +| texture target / levels count from the client object | the descriptor (`Target`, `Levels`; the `MGB_STORAGE_LEVELS` handle arm already reads `pushedStorage->Desc.Levels`, Managers.cpp:6651-6652) | +| dirty region, level HAS a pending upload | the record's regions (unchanged — already record-supplied, Managers.cpp:7358-7369) | +| dirty region, level has NO pending upload (GPU-generated mips, re-derivation) | **the server's own dirty mark on the shadow** (§2.3) — the client is never asked | + +### 2.3 GPU-generated mips (T5) — who owns "dirty" now + +A GPU-side generation (Magma's `GenerateMipmap`, Espryt's re-derivation arms) dirties the +SERVER's shadow, not the client's: `EnsureGenerateMipmapStorageAllocated` is replaced by a +store operation that allocates the level's server storage from the §1 extent derivation and +marks it dirty-in-shadow. The client's `TextureObjectMipmap` is not touched from the apply +thread — `AllocateStorage` and `MarkStorageDirty` on it are §6 layer-1 surfaces. + +The client shadow for such levels is stale and that is CORRECT: the two readers that could +observe the staleness are both named refusals under split — `glGetTexImage` served from the +client shadow (class C, P9) and `texture-remint-pull` (`Fatal{UnmigratedEmulation}`). This is +i1's copy-image-shadow-mirror ruling (CONTRACT-P5B §2) applied to the mip chain, and tx +reports which scenarios reach either Fatal. + +### 2.4 The audit gate (E-P5c #2) + +`MOBILEGL_IPC_AUDIT=1` fills retired staging with `0xDD` (R-11). After tx the texture bytes +are adopted before retire, so the `0xDD` fill covers them: **reverting the adoption to +pointer-dropping MUST turn one named texture scenario red on the four A/B traces.** That +revert is tx's red-once (R-16), and it is the gate that makes "the server consumes the staged +bytes" a checked fact rather than a design intention. + +--- + +## §3 hd — sinks and twins resolve by the handle the record carried + +The handle-keyed `GetOrCreate(MGPipeHandle)` (`SlotTables.h:278-326`) exists since P3a and +has deliberately no caller at the contract commit (:275-277). **P5c is its first caller.** +The rules: + +1. **With an active transport, `MGPipeSlots()` is a client-only surface.** `Acquire`, + `FindByLifetimeId`, `Free` called from the apply thread are `Fatal{RoleViolation, + "MGPipeSlots"}` (§6 layer 1). The minting `GetOrCreate(const StatePtr&)` + (`SlotTables.h:209-252`) and the death switch's `DestroyByLifetimeId` arms are + monolith-only; split paths call the handle forms (`GetOrCreate(handle)`, + `ReleaseByHandle`, :350-361). +2. **The records already carry the handles.** `MGPBlit::ReadFbo/DrawFbo`, `MGPMipPlan::Res`, + `MGPCopyFromFramebuffer::Dst`, `MGPDrawIndirect::Buffer` / `ParameterBuffer`, + `MGPGridInfo::IndirectBuffer` — hd's edit is to USE them, not to add fields. A site that + needs an object no record names is a missing handle and stops at the integrator, not a + quietly-added lifetime-id probe. +3. **Named blit.** `ServerVerbSink::OnBlit` resolves `ReadFbo`/`DrawFbo` through the FBO + twin table by handle and calls the NAMED backend entry; it no longer relies on "the read + and draw framebuffers are already bound" (`PipeApplier.cpp:232-237`). The client's + `ScopedBlitBindings` (`EmitTables.cpp:741-762`) is deleted from the split path — it exists + only to stage values for the server's binding-slot read, and the read is gone. The + record's `MGPBlit` payload is unchanged. +4. **CopyTex** (`DirectGLES.cpp:8560-8563`, :8655-8658) resolves the destination from + `MGPCopyFromFramebuffer::Dst`; the `GetTextureUnitObject` unit-slot read is a layer-1 + surface at these two sites. +5. **Indirect buffers.** Draw-indirect / parameter / dispatch-indirect buffer objects come + from the record's handle fields (CONTRACT-P5B §2 d1/i1 already put them there); the + `GetBufferBindingSlot(DrawIndirect)` read at `DirectGLES.cpp:6702-6703` and its dispatch + twin are layer-1 surfaces at those sites. +6. **`HasDefinedContent` is the descriptor's bit.** `MGPResourceDesc::HasDefinedContent` + already exists; the client publishes it on ResourceCreate/Respecify, and it is sufficient + because the bit only ever moves undefined→defined at a storage-defining call, which is + always a respecify. `EnsureBufferResourceForHandle` (Managers.cpp:3147-3148) reads + `record->Desc.HasDefinedContent` with an active transport; `bufferObject->HasDefinedContent()` + is a layer-1 surface. +7. **caps.** Magma's four reads of the client mirror (`DirectVulkan.cpp:713-715`, + `VulkanRenderer.cpp:667-676`, `VertexInputStateFactory.cpp:249-250`, :502-503) read + `ServerLoopInstance().Backend()->GetDynamicParameters()` — the server's own backend, the + same source Espryt's `ActiveBackendFormatCaps` already prefers (`Utils.cpp:45-48`). + `Utils.cpp:50`'s fallback arm to `pActiveBackendObject` is **refused** with an active + transport (`Fatal{RoleViolation, "caps-mirror"}`): the server's backend exists whenever + the session does, and a silent read of the mirror would hide a bring-up ordering defect. +8. **The legacy buffer arm is a named refusal under transport** (B3). `MappedData()` / + `IsMapped()` / `GetChangeSerial()` / `SyncPersistentMappedRange()` on the frontend + `BufferObject` (Managers.cpp:1012-1022, :1119-1121, :2944-2949, :3281-3327) take + `Fatal{RoleViolation, "buffer-legacy-arm"}` from the apply thread — the same shape as + `Fatal{PipeLegacyMemosDisabled}`, closing the arm that a cleared subsystem bit 7 currently + leaves reachable with one `MGLOG_D`. + +--- + +## §4 ev — SEG_EVENT is the only reverse channel + +The segment, the ring, the three event heads and the full consumer exist +(`EventRing.h`, `ClientSession.cpp:176-267`); production has **zero producers** +(`ServerSession::PublishEvents`, :484-492, has no caller). ev wires the three producers and +retires every direct call into the client. The ten `MGPipeCallbacks` members stay ten; P5c +arms three and a half (writeback, gpu-written, surface-changed, and gl-error as the fourth +kind) — the remaining six keep CONTRACT-P5's P9 phase. + +### 4.1 Producer ownership (Table 3 amendment) + +With an active transport, the three reverse entries of `gMGPipeCallbacks` are installed by +the SERVER session (producer callbacks that Reserve + Publish into SEG_EVENT), never by the +client; `MGPipeInstallClientResourceCallbacks` (`ResourceTracker.h:606-613`) is +monolith-only. A second installation over a live entry is `Fatal{RoleViolation, +"callback-double-install"}` — the "never over an entry a backend already claimed" comment +becomes a check. The apply thread calling a client-installed callback (the R1/R2 shape) is a +layer-1 violation; the GL thread calling a server-installed producer is layer 2. + +`DrainEventRing` calls the client consumers BY NAME (`MGPipeClientOnBufferWriteback` / +`MGPipeClientOnGpuWritten`, `ResourceTracker.h:553-601`), not through `gMGPipeCallbacks` — +the global table is a producer-side surface under split. The drain point is unchanged: +inside `EmitAndWait` after the barrier (`ClientSession.cpp:836`), the one instant the apply +thread is known to be outside the applier. + +### 4.2 The three (+1) producers + +| event | producer site (today's direct call) | payload | +|---|---|---| +| `kEventBufferWriteback` | `Ops_H_Readback`'s writeback (Managers.cpp:2339-2343): the server copies the mapped bytes INTO the event record's inline tail instead of casting the pointer into `MGPBlobRef::Offset` | `EventBufferWritebackHead` + inline bytes | +| `kEventGpuWritten` | `MarkBufferGpuWritten` (Managers.cpp:2722-2753) and Magma's three bypasses (`UniformManager.cpp:1075`, :1231, `VulkanRenderer.cpp:11618`) — Magma's direct `bufferObject->MarkGpuWritten()` calls are ROUTED THROUGH the callback, the bypass is deleted | `EventGpuWrittenHead` + `EventRange[RangeCount]` | +| `kEventSurfaceChanged` | `PublishDefaultFramebufferDepthStencilFormat` (`DirectGLES.cpp:11519-11610`) and the swapchain twin (`SwapchainObject.cpp:276-333`): the backend fills `MGPSurfaceInfo` and posts; it no longer calls `AllocateStorage`/`SetInternalFormat` on `pDefaultFramebufferInfo` | `EventSurfaceChangedHead` | +| `kEventGlError` | `PipeInputs::RecordError` (`PipeFill.cpp:1753-1756`) posts with `ErrorCode` + message (§1). **Ordering is P9's** (CONTRACT-P5 table 2 row `RecordError`): the event is observed at the next drain point, which preserves per-thread program order of error-then-read but not cross-verb interleaving; that is the accepted P5c shape and the contract says so rather than discovering it in P6. | `EventGlErrorHead` + inline message | + +`PipeInputs::InvalidateCompileEnv` (`PipeFill.cpp:1764-1772`) is DELETED with an active +transport: R-12's caps re-publication (`CapsMirror.cpp:78-80`) already invalidates the +compile environment on the client, and the forward is a write into the frontend with no wire +shape. Its FieldOwnership.def row moves to FATAL under split (§5.4). + +### 4.3 The consumer's segment arm (and the guard that has to die first) + +`MGPipeClientOnBufferWriteback`'s monolith guard (`ResourceTracker.h:560-564`) currently +REJECTS any `Seg != kMGHostSpanSegNone` with `MGLOG_E_ONCE` — wiring a producer without +removing it would drop every writeback event on arrival. The replacement arm: + +``` +Seg == kSegEvent -> resolve via the session's SegmentTable entry for kSegEvent + (installed at session start, ServerSession.cpp:333-335 and its + client twin); bounds-checked against the announced segment size +Seg == kMGHostSpanSegNone -> monolith only; with an active transport it is + Fatal{ProtocolCorruption, "OnBufferWriteback.Seg"} (rule B, + reverse direction) +anything else -> Fatal{ProtocolCorruption, "OnBufferWriteback.Seg"} +``` + +The surface-changed consumer (stub at `ClientSession.cpp:240-256` today) APPLIES the +`MGPSurfaceInfo` to the client-owned `pDefaultFramebufferInfo` — the allocate/format writes +happen on the GL thread against client memory, which is where R3's ownership always was. + +### 4.4 Overflow (P5c's ruling, P9 owns the policy) + +All four events are **lossless** in P5c. `Reserve` returning nullptr is +`Fatal{EventRingOverflow}`, not a drop: the ring is 256 KiB, events are drained at every verb +barrier, and a full ring under lockstep means a producer burst no measured workload has. +`CountDrop` and the `eventRingFull` latch stay built and stay unused-by-policy, exactly as +`EventRing.h:14-16` rules — P9 decides between waiting and dropping; P5c makes overflow a +defect instead. The exit gate `eventDropped == 0` at drain points (E-P5c #3) is what checks +this row. + +### 4.5 Round-trip red-once + +Each of the three P5 events gets a unit round-trip beside `SessionTest.cpp:747-823` (the +fixture already drives one producer and one consumer over real segments), and ONE integration +scenario where reverting a producer to the raw-pointer shape turns the run red (E-P5c #3). +`kEventGlError` gets the unit round-trip only; its ordered-error sibling is P9's. + +--- + +## §5 ct + rv — the two control records and the residual-value record + +### 5.1 `applier_reset` — opcode 77, appended + +`X(ApplierReset, MGPApplierReset, kScreen, kNone)`, appended after +`CopyFramebufferToTexture` (PipeCalls.def:233; opcode = position = 77). +`struct MGPApplierReset { Uint64 ContextSerial; };`, `MGP_ASSERT_POD(MGPApplierReset, 8)`. + +- **Producer:** the GL thread, at the `tracker.FreshlyPrimed()` edge + (`PipeFill.cpp:2654-2667`), with an active transport ONLY. The emit happens BEFORE the + block's client-side resets (CsoCache, hash suppressor, vertex-input emitter — all client + surfaces, they stay); the record's barrier is what orders the server's reset against every + verb that follows. +- **Sink:** `ServerVerbSink::OnApplierReset(const MGPApplierReset&)` → + `MGPipeApplierReset()` (`PipeApply.cpp:1202-1300`) after asserting `ContextSerial` against + the session's. `g_applier` stays server-private (`PipeApply.cpp:409`). +- **The GL thread calling `MGPipeApplierReset()` directly with an active transport is + `Fatal{RoleViolation, "g_applier"}`** (G2, layer 2). Monolith keeps the direct call — + G1's byte-identity is untouched because the emit table exists only under split. + +### 5.2 `object_death` — opcode 78, appended + +`X(ObjectDeath, MGPHandleOnly, kCtxObject, kNone)` — the framebuffer family's FIRST wire +delete opcode (G3). Emitted by the GL thread from +`OnFrontendStateObjectDestroyed` (Managers.cpp:206-256) with an active transport: + +1. Look the dying object's handle up in the CLIENT's own allocator + (`MGPipeSlots().FindByLifetimeId` — legal: client surface, client thread). +2. **No handle → emit nothing.** The server never saw the object, so there is no twin to + kill; this replaces the mailbox's unconditional delivery. +3. Handle → `EmitAndWait(ObjectDeath, {handle, kind})`. The wait keeps the death's + ordering against in-flight verbs that name the handle, which is the only property the + blocking `RunOnApplyThread` provided and the only one P5c keeps. + +Sink: `ServerVerbSink::OnObjectDeath(const MGPHandleOnly&)` → the per-kind release +(`ReleaseByHandle`, `SlotTables.h:350-361`; the SamplerViewCso arm's idempotent +secondary path, Managers.cpp:239-256, is keyed by the view's handle the same way). The +mailbox hop (Managers.cpp:208-222) is deleted under split; it stays for monolith, where it +never fired anyway (the guard at :208 is transport-gated). + +### 5.3 `set_context_values` — opcode 79, the rv field table + +`X(SetContextValues, MGPContextValues, kCtxState, kNone)`. One POD, emitted at validate when +any covered value moved (the tracker's value-class dirty accounting already watches all of +them — "值类零新增记账", ARCHITECTURE §5.2), hash-suppressed as a whole. The fields and what +each retires: + +| payload field | PipeInputs accessor it feeds | retires (FieldOwnership.def row) | +|---|---|---| +| `Uint32 ActiveTextureUnit` | `GetActiveTextureUnit` | BARRIER_PULLED "P3b/P4b" → RECORD_SUPPLIED | +| `Uint32 MaxTouchedTextureUnit` | `GetMaxTouchedTextureUnit` | BARRIER_PULLED "P3b/P4b" → RECORD_SUPPLIED (the hash-suppressed set's high-water mark rides THIS record's unsuppressed value) | +| `Uint32 TouchedBufferBindingPointCount[15]` (indexed by BufferTarget, the Coverage.def 15) | `GetTouchedBufferBindingPointCount(target)` | BARRIER_PULLED "P3b/P4b" → RECORD_SUPPLIED | +| `Uint8 IsTransformFeedbackActive`, `Uint8 IsTransformFeedbackPaused` | the two accessors | BARRIER_PULLED "P3b/P4b (Espryt), P7 (Magma)" → RECORD_SUPPLIED | +| `Uint64 TransformFeedbackGeneration`, `Uint64 BoundTransformFeedbackLifetimeId`, `Uint64 TransformFeedbackCapturedVertices` | the three accessors | BARRIER_PULLED → RECORD_SUPPLIED | + +- **`GetCurrentVertexAttribute` does NOT ride this record.** Its problem is the applier's + cross-view conversion (FieldOwnership.def:77-82), so `MGPVertexAttribDefaults` grows to + carry the frontend's CONVERTED value (all three views as the frontend computed them), the + same amendment shape as P5b's `MGPCopyRegion` 64 → 72 B (CONTRACT-P5B §2 i1). The row + becomes RECORD_SUPPLIED; `set_vertex_attrib_defaults` keeps its opcode. +- **The three shutters** (`GetSamplingResolutionGeneration`, `GetTextureBindGeneration`, + `GetTextureContextId`) carry NO wire field: they move to APPLIER_DERIVED, answered from the + applier's own Serial — which is what their rows have said since P5 ("a shutter, not a + value", FieldOwnership.def:94-102). rv's edit makes the accessor DO it. +- **`GetTransformFeedbackProgram` stays BARRIER_PULLED** (object-class: + `SharedPtr`; the twin tables are P3b/P4b/P7's). rv's exit line "值类行为 0" + (E-P5c #4) is checked against the value-class list pinned in `FieldOwnershipTest`, and the + object-class rows are re-annotated with their retiring phase in the same edit so the test + can pin that they are ALL the remaining rows. + +### 5.4 FieldOwnership.def amendments (one list, made by c0c) + +The generator's four classes are unchanged. c0c edits rows; rv/tx/hd land the code: + +- The §5.3 value rows → RECORD_SUPPLIED (derived, via Coverage.def's emitted list gaining + `SetContextValues` rows; the derivation's contradiction rule is what proves the edit + landed). +- The three shutters → APPLIER_DERIVED ("the applier's Serial"). +- `GetCurrentVertexAttribute` → RECORD_SUPPLIED (the amended payload). +- `InvalidateCompileEnv` (field + forward rows) → FATAL under split with mechanism "deleted; + R-12 caps re-publication" — a read after the deletion is a real defect and should abort, + not pull. +- **The texture family is NOT a set of new field rows** — a ruling, because the texel / + per-level extent / dirty reads are reads of frontend OBJECT internals reached through + `GetTextureUnitObject` / the `GetTextureObject` sticky forward, not `MGPipeInputField` + enum values, and a row naming a non-field stops the generator by design. What pins them + instead: (a) `GetTextureUnitObject`'s existing row is re-annotated — the tx-retired reads + leave its site list, what remains is the unit-object POINTER reads (P3b/P4b/P7); (b) the + object-surface list (texel bytes, per-level extent, dirty region, `HasDefinedContent`, + `MappedData`/`IsMapped`/`GetChangeSerial`) is pinned verbatim in `FieldOwnershipTest` as + the layer-1 surface set, and the §6 guard is what enforces it. This is the honest form of + ROADMAP's "把纹理家族补进 FieldOwnership.def": the .def's mechanism covers fields, and the + family's object reads get a pinned list plus a guard that can go red. + +--- + +## §6 gt — the role guards (semantics, two layers) + +With an active transport, in a split build only (`MOBILEGL_BUILD_DISAGGREGATED`; the pull +build's bytes do not move, G1): + +**Layer 1 — apply thread may not touch frontend surfaces.** `Fatal{RoleViolation, +""}` raised inside: `MGPipeSlots()` (all three entry points); the frontend +`BufferObject` legacy accessors (§3.8); the frontend `TextureObjectMipmap` / +`MipmapStorage` mutators and maps (`AllocateStorage`, `MarkStorageDirty`, `MapMipmapData` +and the storage-dirty queries); `pDefaultFramebufferInfo`'s attachment writes; the +client-caps-mirror fallback (`Utils.cpp:50`); any BARRIER-PULLED accessor whose row is no +longer BARRIER_PULLED after §5 (the stamp mechanism's `MGPipeInputUnfreshRead` already +aborts these — gt's edit is that it aborts as RoleViolation with the surface named, and the +VALUE-class set is empty at exit). Implementation surface: `ServerLoop::OnApplyThread()` +gated checks at the named entries; compiled out entirely in pull builds. + +**Layer 2 — GL thread may not touch server surfaces.** `Fatal{RoleViolation, ""}` +raised on: direct `MGPipeApplierReset()` (§5.1); `ServerLoop::Backend()` / +`MGPipeGetResourceOps()` reads AFTER session start (the bring-up window before +`ClientSessionInstance().Start()`, `MG_Backend/Init.cpp:100-247`, is the documented exception — it is +single-threaded and pre-dates the roles); a write into `gPipeInputs` outside the residual +fill, and any client-side read of a server-stamped field outside a barrier wait. + +**A1 is ruled: `InBarrierWait()` is WIRED, not deleted.** CONTRACT-P5 table 3's sentence — +"the client checks it when it touches `gPipeInputs` outside a barrier (`InBarrierWait` / +`ApplyThreadIsInsideApplier`)" — is currently half-true: `ApplyThreadIsInsideApplier` fires +at emit (`ClientSession.cpp:748`), `InBarrierWait` (`:880`) has zero readers. gt makes the +sentence true: the layer-2 `gPipeInputs` check consults `InBarrierWait()` exactly as the +contract describes. Deleting the sentence instead was considered and rejected: the check is +the only client-side half of the gPipeInputs single-writer rule, and P6 needs it armed, not +forgotten. + +**Red-once (R-16), per layer, mandatory:** each layer is switched off in one named +integration scenario and the run MUST go red with the layer's Fatal — this is E-P5c #1's +"关掉任一层守卫必须能在一条具名用例上变红", and it is what makes the guard a gate rather +than a comment. CI gains one `MOBILEGL_IPC_STRICT_ERRORS=1` + guards-armed +`integration-split` lane. + +**`rsp` at exit.** `ResidualPulls` enters the per-frame stats line and is MEASURED on the +four A/B traces; the remaining reads are exactly the object-class rows pinned by +`FieldOwnershipTest` (§5.3), value rows = 0. The known blind spot — sticky / non-verb +forwards bypassing the stamp counter (ROADMAP debt "rsp=35 只是下界") — is closed by gt: +the sticky-forward pull path (`MGPipeStickyForwardPull`, `PipeInputs.cpp:180-186`) counts +into `rsp` under the same verb stamp, so a read that escapes the stamp escapes nothing. + +--- + +## §7 Amendments to CONTRACT-P5 / P5B (the complete list) + +1. **Table 0** gains §1's six rows (reverse-channel blobref, ObjectDeath's payload reuse, + ApplierReset::ContextSerial, MGPContextValues, server-side level extent, kEventGlError). +2. **Table 1** gains three rows: `applier_reset` (77), `object_death` (78), + `set_context_values` (79) — all appended, opcode = position, never inserted. +3. **Table 2** (FieldOwnership.def): the §5.4 amendments; BARRIER_PULLED's value-class + membership goes to zero and `FieldOwnershipTest` pins the remainder. +4. **Table 3**: the three reverse `MGPipeCallbacks` entries move from "client installs" to + §4.1's producer ruling; `g_applier` gains "reset only via `applier_reset` under + transport"; the `InBarrierWait` sentence becomes true (§6); the bring-up window is named + as the one legal cross-role read. +5. **CONTRACT-P5B §2 f1's named blit**: "降为现有 bound backend 调用" is superseded by + §3.3 — the sink resolves `ReadFbo`/`DrawFbo` from the record and calls the named entry; + `ScopedBlitBindings` is deleted from the split path. P5b's "P5b 留下的 inproc 依赖" + list loses that row. +6. **`MGPVertexAttribDefaults` grows** (§5.3) — the second payload-size amendment after + P5b's `MGPCopyRegion`; the ABI fingerprint (`SessionRings.h:498-524`) is what catches a + desync, and `PipeCatalogueTest` pins the new size. +7. **`EventKind` gains `kEventGlError = 4`** — the first event-kind addition; the kind + space is append-only for the same reason the opcode space is. + +## §8 Exit gates this contract serves (E-P5c, restated for the packages) + +1. Guards armed: `integration-split` (107) green, broad inproc census with zero regressions + vs `348d22a4`, 79 traces with no new first blocker; each guard layer red-once (§6). +2. `MOBILEGL_IPC_AUDIT=1`'s `0xDD` covers texture staged bytes on the four A/B traces; + reverting adoption goes red (§2.4). +3. Three events round-trip; `eventDropped == 0` at drain points; producer→raw-pointer + revert goes red (§4.5). +4. `rsp` measured per frame on the four A/B traces; remaining pulls are exactly the pinned + object-class rows (§5.3, §6). +5. Redmi four-arm retest recorded (barrier tax re-measured after writeback / gpu-written + move to events) — recorded, not gated. +6. G1 0/0/0/0, G2, G14, G5 as usual. + +Explicitly NOT P5c (ROADMAP "留给后续阶段的"): object-class BARRIER_PULLED rows and the +frontend-keyed twin registry (P3b/P4b, P7); XFB scatter / `OnXfbScatterReady` / +`OnTexturePullRequest` / ordered `OnGlError` (P9); the EGL forwarders' control plane (P6); +copy-image shadow mirror and CopyTex texel writeback (P8/P9); `s_synced` / +`g_syncedRenderStateParameters` context-generation resets (P6).