Files
MobileGL/MobileGL/MG_Remote/Client/WireTables.h
T

188 lines
12 KiB
C++

// MobileGL - MobileGL/MG_Remote/Client/WireTables.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// THE CLIENT ARM OF R-17's ROUTING: the encode twin of `gMGPipeWireRecordApply`. Owner: c1.
//
// Thirty-eight thin emitters over `ClientSession::EmitAndWait`, installed over the two
// generated tables and the escape table that `MG_Pipe/PipeRoute.h` declares, so that under
// split every resource, CSO, texture and program record leaves the GL thread as a WIRE RECORD
// instead of executing synchronously against a context the apply thread now owns.
//
// WHAT ARMS `integration-split`. The 21 `DirectGLES.Split.*` entries skip on
// `ClientSession::Active() == nullptr`. `Install()` below is called from the END of a
// successful `ClientSession::Start()`, on the GL thread (the thread that called
// `MG_Backend::Init()`), AFTER the Hello/Welcome handshake, after the first CapsSnapshot has
// been adopted, and after the in-process server role's apply thread has been started. That
// order is not a preference:
// - after the handshake, because an emitter that published before Welcome would be writing
// into a ring the peer has not mapped;
// - after the first snapshot, because R-8's liveness gates read the caps mirror and a
// placeholder mirror consumes nothing, so a record emitted before it would be emitted to a
// server this client has not yet been told consumes that family;
// - after the apply thread exists, because `EmitAndWait` BLOCKS on `appliedSeq` and nothing
// would advance it - a barrier wait with no applier is the 30-second Fatal, not a hang;
// - on the GL thread, because that is the only thread that may touch `gPipeInputs` while the
// barrier holds (table 3), and installing from the apply thread would publish the table to
// the GL thread with no synchronisation at all.
// `Uninstall()` runs at the TOP of `Stop()`, before the rings go away, so the last thing any
// straggling GL-thread call reaches is a named refusal rather than a dangling session.
#pragma once
#include <Includes.h>
#include <MG_Pipe/MGPipeHandles.h>
#if MOBILEGL_BUILD_DISAGGREGATED
namespace MobileGL::MG_Remote::Client {
// Installs the thirty-eight wire emitters over gMGPipeScreen / gMGPipeContext /
// gMGPipeRouteEscapes and records the arm. Idempotent.
void InstallClientWireTables();
// Marks the routed tables uninstalled so a routed call refuses by name
// (Fatal{ClientTablesUninstalled, "<row>"}) rather than running the applier on the caller
// (codex 4). It does NOT restore the monolith adapters - that is ReinstallMonolithAfterTeardown
// below, run only once the session's rings are freed. Idempotent.
void UninstallClientWireTables();
// Shared by routed rows and the five class-B emitters, before session/ring access.
void RequireClientTablesInstalled(const char* row);
// The LAST step of ClientSession::Stop: after the rings, segments and transport are gone,
// puts the monolith adapters back and clears the refusal flag, so the at-exit ~BufferObject
// deletes that reach a process with no session run the applier as they do under monolith.
void ReinstallMonolithAfterTeardown();
// How many records the thirty-eight emitters have published. It counts the ROUTED rows
// only - a resource_create, a set_vertex_buffers, a create_shader_state - and never the
// five class-B verbs, so it is the one number that says "the resource/CSO/state path really
// ran" as opposed to "a Clear crossed".
//
// WHAT IT IS NOT (M7, corrected). This is NOT what arms the split lane. WireTables.h round 2
// claimed it was "t1's FOURTH arming fact", and that was wrong: the harness reads the
// encoder's EmitSeq (Harness/SplitRuntimePeek.cpp:50, Harness/ScenarioFixture.h:85), which
// moves for the class-B verbs too. So an armed Clear-only lane is green on EmitSeq while this
// counter stays 0. Making the harness read THIS instead is t1's file (SplitRuntimePeek), so
// the honest statement is the one here: c1 counts the routed ordinal at the only place that
// can, PipeFill's InitialBytesNotCarried self-check reads it (PipeFill.cpp:755), and the
// lane's own arming remains EmitSeq until t1 re-points it. c1-v3.md M7 has the full note.
Uint64 ClientWireRecordsEmitted();
// How many of those were refused by the server, by acceptance row. Counted rather than
// inferred, R-8's rule one level out.
Uint64 ClientWireRecordsDeclined();
// ---- P12: the CREATE WINDOW (MOBILEGL_IPC_CREATE_WINDOW) ---------------------------------
//
// The resource_create row defers its answer instead of dropping it: up to N creates go out
// fire-and-forget, the caller reads a provisional accept, and the answers are taken in order.
// The four numbers below are what a test can say about that without reaching into the row:
// how many answers are outstanding, the ceiling the code actually applies (the config value
// clamped to the array), the array's own size, and the two tallies - objects whose create came
// back DECLINED and are therefore out of the window, and how many refusals there have been.
Uint32 ClientCreateWindowPending();
// How many deferred answers the window has read back. THE NUMBER THAT SAYS IT WORKED: a
// drain that never collects leaves Pending at the ceiling and every other reading unchanged,
// which is exactly what the device measured before the stream link learned to answer by seq.
Uint64 ClientCreateWindowTaken();
Uint32 ClientCreateWindowEffective();
Uint32 ClientCreateWindowArraySize();
Uint32 ClientCreateWindowSuspects();
Uint64 ClientCreateWindowRefusals();
// TRUE ON THE SERVER ROLE's OWN THREAD (the apply thread), false everywhere else. It is
// v1's ServerLoop::OnApplyThread(), exposed here because it is table 3's role split made
// into one predicate and TWO packages read it: the wire emitters below (a routed call that
// finds itself on the apply thread runs the monolith adapter, because on that thread this
// process IS the server), and MG_Impl/Pipe/PipeFill.cpp's split-only respecify/flush
// branches (M5: those branches emit CLIENT wire records and must not run on the server's
// apply thread, which produces none - the InitialBytesNotCarried self-check would abort the
// server otherwise). Declared here so PipeFill does not have to include a server header.
Bool RunsAsTheServerRole();
// ---- P5c (ct), CONTRACT-P5C.md §5: the two control records' client halves --------------
//
// NOT ROUTED ROWS - the two are appended to PipeCalls.def with no MGPipeApply* entry point
// and no table slot at all, so their producers call these directly. PipeFill.cpp's
// FreshlyPrimed arm calls the first; DirectGLES' OnFrontendStateObjectDestroyed calls the
// second.
// applier_reset (§5.1). Emitted on the GL thread at the tracker.FreshlyPrimed() edge,
// BEFORE the block's client-side resets, so the record's barrier orders the server's
// MGPipeApplierReset() against every verb that follows. ContextSerial is the client's own
// count of applier_reset emissions (0 = the first make-current): nothing in the frontend
// numbers a make-current today, and the sink asserts the value against its own count of
// the same records rather than dispatching on it (P5c: one context per session; P6 reads
// it for real). Returns false - nothing emitted - when no live session could carry the
// record (the bring-up window before Start, a server-role-only fixture, teardown); the
// caller then makes the direct call the record replaced.
Bool EmitApplierResetRecord();
// A fresh session replaces an ended one: the create window's deferred answers, its suspect
// list and the applier-reset serial all belonged to the ended session (ClientSession's
// ForgetEndedSession).
void ForgetWireTablesOfEndedSession();
// ---- P14 S1: the EGL context lifecycle's control-plane half -----------------------------
//
// NOT ring records and NOT routed rows: eglCreateContext/eglDestroyContext are the EGL
// lifecycle, which the catalogue deliberately keeps off the record plane (PipeCalls.def's
// "the eight EGL lifecycle entry points stay virtual functions on pActiveBackendObject"),
// so they cross as two SurfaceOp kinds through the same blocking frame channel the nine
// surface forwarders use. Called by MG_Impl's EGLImpl on the app thread, in the same order
// as the EGL calls themselves - which is the ordering the server's context table needs.
//
// THREE ANSWERS, NOT TWO, for the reason ObjectDeathEmit below has three: "the server said
// no" and "there is no wire for the question to cross" are different facts and the caller's
// answer to each is different. NoWire is the bring-up window before ClientSession::Start and
// the server role's own thread - in both, the pre-S1 shape (the client keeps its own EGL
// books and no server table exists) is the correct one and the context must be KEPT; Refused
// is a server that answered, and eglCreateContext turns it into EGL_NO_CONTEXT, because a
// context whose token the server did not table is one the first bind_context after it would
// be refused for.
enum class ContextFrameEmit : Uint8 { Sent, NoWire, Refused };
ContextFrameEmit SendCreateContextFrame(Uint64 clientContextToken, Uint64 shareGroupToken, Uint32 flags);
// A destroy has the same three answers; the client's EGL book is dropped either way (that is
// the caller's own state machine), and what a refusal changes is only what the server holds.
ContextFrameEmit SendDestroyContextFrame(Uint64 clientContextToken);
// P14 S1 (docs/Disaggregated/design/11-state-ownership.md): bind_context, opcode 84.
//
// Emitted on the GL thread from EGLImpl::MakeCurrent, after the switch has succeeded and
// before eglMakeCurrent returns - which puts it ahead of the applier_reset the NEXT GL call's
// validate emits (PipeFill.cpp's FreshlyPrimed arm), so the reset is already attributed to
// the context that primed. `clientContextToken` is the token
// EGLState minted for the context the calling thread now has current (0 = none, the release
// edge) - the caller reads it rather than this table owning a second copy, so the two planes
// cannot disagree about which context is bound. Returns false - nothing emitted - in exactly
// the windows EmitApplierResetRecord documents (no live session, the pre-Start bring-up, a
// server-role-only fixture, teardown), where there is no wire for the binding to cross and
// the session's single-context default is what the server already answers.
Bool EmitBindContextRecord(Uint64 clientContextToken);
// P5c (rv), CONTRACT-P5C.md §5.3: whether set_context_values can cross right now (a live,
// started session, tables not being torn down). PipeFill.cpp gates the record's emission
// AND the residual-fill skip for its eight fields on this one answer, so the two halves
// can never disagree about who supplies them.
Bool ContextValuesWireLive();
// The three answers a death notice can produce (§5.2): the record crossed; the client's
// own allocator cannot resolve the dying object (the server never saw it, so there is no
// twin to kill and NOTHING crossed); or no live session could carry the record. The
// third is NOT folded into the second: a server role with no wire at all - a ServerLoop
// fixture's shape, never a real split's - still has a server loop the caller delivers to,
// where a handle that never existed has nothing to deliver.
enum class ObjectDeathEmit : Uint8 { Emitted, NoHandle, NoSession };
ObjectDeathEmit EmitObjectDeathRecord(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId);
} // namespace MobileGL::MG_Remote::Client
#endif // MOBILEGL_BUILD_DISAGGREGATED