mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-18 09:08:31 +09:00
[MG_Remote] (Disaggregated): CONTRACT-P5C - the wire contract for zeroing the inproc split's 59 direct cross-role memory accesses (P5c package c0c)
Texture staged shadow ownership and exact per-level coverage (tx); SEG_EVENT blobref convention with Seg = kSegEvent plus the kEventGlError kind, producer ownership and the consumer segment arm that replaces the monolith guard (ev); handle-keyed sink and twin resolution with MGPipeSlots() ruled a client-only surface (hd); the applier_reset (77) and object_death (78) control records (ct); set_context_values (79) and the residual-value field table retiring every value-class BARRIER_PULLED row (rv); the two Fatal{RoleViolation} guard layers with InBarrierWait wired rather than deleted (gt); FieldOwnership.def and Table 3 amendments; the six E-P5c exit gates.
This commit is contained in:
@@ -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, "<surface>"}` 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, "<row>.<field>"}`
|
||||||
|
for a record the codec cannot prove, `Fatal{UnmigratedVerb, "<GL slot>[+<QUALIFIER>]"}` for a
|
||||||
|
shape no sink implements, a null backend slot DECLINES (`return false`). P5c adds one family:
|
||||||
|
`Fatal{RoleViolation, "<surface>"}` (§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, "<site>"}` — 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<ProgramObject>`; 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,
|
||||||
|
"<surface>"}` 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, "<surface>"}`
|
||||||
|
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).
|
||||||
Reference in New Issue
Block a user