[MG_Backend, MG_State, MG_Remote] (Disaggregated): P5e tx2 - the texture family by handle, the unit window, and the guard that notices

- SyncTextureToBackendByHandle and SyncMipmapsToBackendByHandle get their bodies
  (the c0e seams): record first, twin by GetOrCreateByHandle, three syncs from the
  record, no frontend touch and no allocator probe on the path.
- BackendTextureObject carries m_pushedSyncHandle (CONTRACT-P5E §4.1's m_handle), which
  is what the three sync prologues read instead of probing the client allocator with
  HandleOf, and what selects the by-handle arm inside them.
- IsDrawSyncCleanByRecord replaces IsDrawSyncClean(frontend) on the memo-hit path: the
  same conjunction over the record's serials and the twin's own, with BOTH halves of each
  resync pair read and neither clearing the other's (D-E2, scout R4).
- The unit work list is built from st.BoundSamplerViews[] and keyed on
  (ContextSerial, SamplerViewsSerial, window, backend context generation); PairingsIntact
  is not carried on it, because a {slot, gen} has no stale-pairing hazard to catch.
- ResolveAndBindUnitTextures is one pass over the window with the target from the view
  CSO, then an unbind driven by g_boundTexturesCache; the alias arbiter dies (the client
  arbitrated, SamplerEmit.h).
- BindCurrentTextures' memo key is ruling 6's both halves, entry selection included.
- The sampler pass reads BoundSamplerStates / the built-in sampler CSO / the view's
  InternalFormat; the apply-thread mint and ResolveUnitSamplerBackend become a named
  refusal under a transport (§4.2).
- GenerateMipmap resolves VerbMipRes and never the active unit; the detach sweep is keyed
  by the texture handle over the applier's framebuffer records. The row is NOT flipped to
  kWaitNone: the RGB16F/RGB32F CPU filter still reaches the client's level shadows, so
  ruling 14's escape applies and the flip is trailing.
- RequireImageBindableStorage gets a by-handle entry whose re-dirty is
  Fatal{UnmigratedEmulation, "image-bindable-redirty"} at the entry rather than three
  frames deep at a MarkStorageDirty guard.
- The buffer-texture arm resolves scout R5 with Desc.BufferForTexBuffer / BufOffset /
  BufSize and EnsureBufferResourceForHandle(nullptr, h) - that function is client-free
  under an active transport (kimi audit row 29).
- CheckUnitWindows(st) at draw and dispatch makes the §5.3 window promise checkable and
  checked: narrower than [0, MaxTouchedTextureUnit] is
  Fatal{ProtocolCorruption, "SetSamplerViews.Count"} / "BindSamplerStates.Count".
- MipmapStorage's guard list gains the read-only shape accessors P5c deliberately
  exempted, now that nothing on the draw path reads them.
This commit is contained in:
2026-09-18 07:56:34 -04:00
parent f6cfcbd36b
commit 72e7902080
5 changed files with 1287 additions and 167 deletions
+69
View File
@@ -427,6 +427,66 @@ namespace MobileGL::MG_Remote::Server {
// with a span (the client refuses "MultiDrawElements+CLIENT_INDICES" first; P8's
// HostResolve.cpp flattens it) and a multi-draw that claims instancing (no GL entry point
// produces one; the client never sends it).
#if MOBILEGL_BUILD_DISAGGREGATED
namespace {
// P5e (tx2), CONTRACT-P5E §5.3 / ruling 19 (ID-95, A8 closed). THE TWO UNIT WINDOWS MUST
// COVER [0, MaxTouchedTextureUnit], AND THIS IS WHERE THAT PROMISE IS CHECKED.
//
// Everything the texture and sampler families do per draw now reads
// [SamplerViewStart, +SamplerViewCount) and [SamplerStateStart, +SamplerStateCount).
// Under run-ahead a window NARROWER than the frontend's high-water mark silently drops a
// sync of a texture the draw is about to sample, or leaves an earlier draw's sampler
// object on a unit - and a decline is exactly what run-ahead cannot take, because the
// client has already moved on and there is no wait in which to notice.
//
// A SERVER-SIDE RE-DERIVATION IS REFUSED, and that is the ruling's point: the client owns
// the high-water mark (it is the `count` argument SamplerEmit.h passes, Start=0 /
// Count=maxTouched+1) and the server's job is to check the promise, not to invent a
// second authority for it. So this is a CHECK and its failure is corruption.
//
// WHERE THE MARK COMES FROM: MGPContextValues::MaxTouchedTextureUnit, applied by
// set_context_values, which precedes the verb on the ring - the same ordering §2.1's
// XFB clause leans on. It is RECORD_SUPPLIED, so reading it is reading what a record
// put there and not client memory (rule F).
//
// WIDER IS FINE. A window larger than the mark costs a walk over provably-empty units;
// only SMALLER is unrepresentable. An empty window on a draw that touches no unit at all
// (mark 0 with nothing ever touched) is the correct and only possible emission, so a
// count of 0 is admitted exactly while the applied counters say no unit was touched.
//
// THE ONE ADMITTED SILENCE, and it is the A/B rather than a hole: a client whose sampler
// subsystem bit is CLEAR emits neither record, the backend then runs its pre-handle
// frontend walk, and there is no window to check because there is no window. That is
// "both counts are still 0", and it is distinguishable from the narrowing this refuses
// (a narrowed window carries a non-zero count that is merely too small). The moment
// either set has been received, the rule binds.
void CheckUnitWindows(const MG_Pipe::MGPipeApplierState& st, const char* verb) {
if (st.SamplerViewCount == 0 && st.SamplerStateCount == 0) return;
const Int maxTouched = MG_Pipe::gPipeInputs.GetMaxTouchedTextureUnit();
if (maxTouched < 0) return;
const Uint32 required = static_cast<Uint32>(maxTouched) + 1u;
if (st.SamplerViewStart != 0 || st.SamplerViewCount < required) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"SetSamplerViews.Count\"} - %s applies with "
"units 0..%d touched, so set_sampler_views must carry Start=0 and Count >= %u "
"(CONTRACT-P5E.md §5.3); the applied window is Start=%u Count=%u, which drops "
"the sync of at least one texture this draw samples",
verb, static_cast<int>(maxTouched), required, st.SamplerViewStart,
st.SamplerViewCount);
std::abort();
}
if (st.SamplerStateStart != 0 || st.SamplerStateCount < required) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"BindSamplerStates.Count\"} - %s applies with "
"units 0..%d touched, so bind_sampler_states must carry Start=0 and Count >= %u "
"(CONTRACT-P5E.md §5.3); the applied window is Start=%u Count=%u, which leaves "
"an earlier draw's sampler object on at least one unit",
verb, static_cast<int>(maxTouched), required, st.SamplerStateStart,
st.SamplerStateCount);
std::abort();
}
}
} // namespace
#endif
Bool ServerVerbSink::OnDrawVbo(const MG_Pipe::MGPDrawInfo& info,
const MG_Pipe::MGPDrawRange* ranges,
const MG_Pipe::MGHostSpan* userIndices,
@@ -449,6 +509,11 @@ namespace MobileGL::MG_Remote::Server {
}
++m_drawRecords;
#if MOBILEGL_BUILD_DISAGGREGATED
// Ruling 19 / ID-95: the window promise, checked at every draw, before the backend is
// asked to resolve anything out of the windows.
CheckUnitWindows(MG_Pipe::MGPipeApplier(), "draw_vbo");
#endif
const MG_Backend::GlobalBackendFunctionsTable* table = Table("draw_vbo");
if (table == nullptr) return false;
const MG_Backend::GLFunctionsTable& gl = table->GL;
@@ -668,6 +733,10 @@ namespace MobileGL::MG_Remote::Server {
// identical - and it is also the honest statement of the debt, which `rsp` counts.
Bool ServerVerbSink::OnLaunchGrid(const MG_Pipe::MGPGridInfo& grid) {
#if MOBILEGL_BUILD_DISAGGREGATED
// Ruling 19 / ID-95: a dispatch samples through the same unit windows a draw does.
CheckUnitWindows(MG_Pipe::MGPipeApplier(), "launch_grid");
#endif
const MG_Backend::GlobalBackendFunctionsTable* table = Table("launch_grid");
if (table == nullptr) return false;
const MG_Backend::GLFunctionsTable& gl = table->GL;