Compare commits

..
Author SHA1 Message Date
rereview 5175f9ccd5 [Docs] (MG_Remote): record R-15 and split the emit table's 71 slots into answered-locally, emitted and Fatal, with the forty-one null checks that are really capability probes 2026-09-11 13:23:46 -04:00
rereview 45b759e7a5 [Feat] (MG_Pipe): give resource_respecify's redefinition scope a wire carrier in the descriptor's two existing pad fields, so a per-level glTexImage cannot cross as a whole-resource redefinition and eat every other level's pending upload 2026-09-11 13:23:46 -04:00
rereview b4bbcc113a [Docs] (MG_Remote): CONTRACT-P5.md - the encoding, byte-carrier, field-ownership and role-ownership tables every P5 package is held to 2026-09-11 13:05:05 -04:00
rereview dd4afb0faf [Docs, Test] (MG_Remote, Wire): state the five sequence watermarks, the late-never-early batching rule and the pad-does-not-count rule on RingControl, and pin all three with five cases 2026-09-11 13:05:05 -04:00
rereview 72a4cba5ad [Fix, Test] (MG_Test): include the header that defines MOBILEGL_PIPE_POISON - seven emitter suites tested the macro without it, compiled the non-aborting arm, and turned eighteen expected refusals into aborts the moment a disaggregated build armed it 2026-09-11 13:05:05 -04:00
rereview 418e6f50be [Feat] (MG_Remote): land the P5 contract headers and their named-Fatal stubs so all seven parallel packages compile and link on day one 2026-09-11 13:05:05 -04:00
rereview 1ee6c8953a [Build] (CMake): add MOBILEGL_BUILD_DISAGGREGATED_INPROC, make DISAGGREGATED imply PIPE_PUSH, and list the new MG_Remote Wire/Client/Server sources 2026-09-11 13:05:05 -04:00
rereview ec20e7001d [Feat] (Config, ConfigLoader): parse MOBILEGL_TRANSPORT and the MOBILEGL_IPC_* family entirely behind the disaggregated option, so the pull build's symbol set cannot move 2026-09-11 13:05:05 -04:00
rereview 41b4f8dfe3 [Fix] (MG_Pipe, gen_pipe): export the per-opcode call flags and give GetCaps and CreateSamplerState the kHasBlob their payloads have always owned 2026-09-11 13:05:05 -04:00
rereview a29807cc48 [Docs] (Disaggregated): record P4a as landed - the fourteen family calls and six re-keyed kinds, the seven contract corrections and the defect each one closed, the three-arm A/B on a pinned Adreno 830 with active cooling, the upload shape that did not move, and the seam taxonomy the next brief has to carry 2026-09-11 11:37:08 -04:00
rereview 8c458cd594 [Fix, Test] (clientfb): heal a texture's record from its first set_texture_params - a texture born while the family was not live (the context's default textures are constructed before the backend registers its consumer) has no record, so the application's first glTexParameter* on texture 0 was refused, silently latched away before m-1 and the retrace census's one residual after it went loud; the params path now publishes the create, the storage if the texture has any, and then the parameters, the respecify path's own self-heal shape 2026-09-09 00:43:38 -04:00
rereview c2c6a655ea [Fix, Docs] (MG_Pipe, clientfb): latch set_texture_params on acceptance, and two comments - the emitter advanced its version latch before the applier answered, so a refused record (no record for the handle, no consumer) was not re-sent until the next glTexParameter* moved a version (final review m-1, audit F-7): the applier now returns whether the record took the parameters and the emitter latches on true, counting and naming a refusal; Config.h's mask table gains the fourth D-K2 row (0x400 requires 0x800), and MGPipeTypes.h's metadata paragraph says what the applier does (replaces the descriptor whole with the client's sticky OR) instead of "ORed, never replaced" 2026-09-09 00:16:35 -04:00
rereview 9f60aadc1d [Fix, Test] (clientsp, MG_Pipe, Espryt): produce kMGPipeBindSampler and kMGPipeBindShaderImage where D-A4 places them - nothing set either bit, so ImageBindableHint was always 0, the metadata respecify had no live trigger and the remint pull the hint prevents was neither prevented nor counted (final review M-A); the sampler-view resolution notes SAMPLER, glBindImageTexture's state setter notes SHADER_IMAGE at the bind (so the hint precedes the first sync) and the image walk notes it too, both through a contract door since neither may include TextureEmit.h, and Espryt counts every re-mint of storage it already held as tex-remint-pulls (trp= on the stats line, ROADMAP open question 2's number) 2026-09-09 00:09:30 -04:00
rereview a690032f85 [Fix, Test] (MG_Pipe, clientfb, clientsp): retire every emitter's entry at the object's death - the six death helpers freed the slot and told no emitter, so a dead-but-unrecycled texture handle still resolved to the freed ITextureObject* (the allocator's generation moves only at the next hand-out) and the drain list kept the level: glTexImage2D; glDeleteTextures; <any verb> called a virtual on freed memory from the next validate point (final review C-2); the helpers now forward to the texture, renderbuffer, framebuffer, sampler-view and shader-CSO emitters between the wire delete and the free, ResolveTexture refuses a dead slot loudly on IsLive, the sticky-mask producers stamp the generation they write under, and the delete-then-use sequence is pinned for every kind, for a recycled slot, and under MALLOC_PERTURB_ on both backends 2026-09-08 23:59:48 -04:00
rereview 173f1dd273 [Fix, Test] (clientfb, MG_Pipe): pass the level a texture respecify redefines - every per-level glTexImage*D and glGenerateMipmap grow took the applier's whole-resource arm and dropped the texture's every pending upload, including a level the applier had accepted at an earlier verb with the client's dirty flag already clear, so L0; draw(other); L1; draw(T) read a black level 0 on the handle arm (final review C-1); the storage entry points now state the scope (one level, a chain cut, the whole resource), the emitter builds wire's MGPRespecifiedLevel with the drain's packed target, a per-level call is never deduped on the descriptor, and the applier drops a named level whether or not the descriptor moved 2026-09-08 23:55:44 -04:00
58 changed files with 4745 additions and 151 deletions
+58
View File
@@ -21,6 +21,13 @@ option(MOBILEGL_IOS "Build MobileGL for iOS instead of macOS when
# That emptiness is one of the two byte-level equalities the plan's validation
# gates keep (section 10.3).
option(MOBILEGL_BUILD_DISAGGREGATED "Build the MG_Remote transport layer (two-process shape)" OFF)
# The CI / debugging shape (ARCHITECTURE.md:581): both roles in ONE process, talking over the
# same SEG_CMD ring and the same G3 codec a spawned server would use. It IMPLIES
# MOBILEGL_BUILD_DISAGGREGATED (below) and additionally admits the role-isolation shims that
# only make sense when the two roles share an address space. It is a SUPERSET, never a
# substitute: MOBILEGL_TRANSPORT=inproc is what selects the shape at run time, and this option
# only decides whether the shims are compiled in.
option(MOBILEGL_BUILD_DISAGGREGATED_INPROC "Compile the in-process (one-process, two-role) split shims; implies MOBILEGL_BUILD_DISAGGREGATED" OFF)
option(MOBILEGL_BUILD_SERVER_SPIKE "Build the P0 spike-A MobileGLServer delivery-chain executable (Android only)" OFF)
# The PipeInputs strangler (ARCHITECTURE.md 9.2). OFF is the pull build and must stay
# byte-identical to a tree without either option: MGB_CTX is the live GLContext, no
@@ -445,6 +452,17 @@ set(SOURCE_FILES
# option OFF not one file here is compiled and no include path is added.
# ---------------------------------------------------------------------------
# MOBILEGL_BUILD_DISAGGREGATED_INPROC implies MOBILEGL_BUILD_DISAGGREGATED. A normal
# variable, not a forced cache write, for the reason the two implications below give: an
# operator's cache entry stays theirs and only this configure is shadowed. Ordered BEFORE the
# flatbuffers guard so that a missing submodule turns BOTH off together - an INPROC build
# with the transport shadowed off would compile a role-isolation shim over no roles.
if (MOBILEGL_BUILD_DISAGGREGATED_INPROC AND NOT MOBILEGL_BUILD_DISAGGREGATED)
message(STATUS "MobileGL: MOBILEGL_BUILD_DISAGGREGATED_INPROC=ON forces "
"MOBILEGL_BUILD_DISAGGREGATED ON for this configure")
set(MOBILEGL_BUILD_DISAGGREGATED ON)
endif()
# FlatBuffers is a submodule and its runtime is header-only. Guard both ways:
# a checkout without the submodule must configure and build, just without the
# disaggregated shape, rather than fail with a missing-header error a hundred
@@ -462,6 +480,24 @@ if (MOBILEGL_BUILD_DISAGGREGATED AND
# all. Shadowing the cache entry for this configure only keeps the operator's ON where it
# was, so the next configure - with the submodule there - honours it.
set(MOBILEGL_BUILD_DISAGGREGATED OFF)
# And with it the shim option, or the `-DMOBILEGL_BUILD_DISAGGREGATED_INPROC=1` below
# would still be defined over a build with no MG_Remote in it at all.
set(MOBILEGL_BUILD_DISAGGREGATED_INPROC OFF)
endif()
# MOBILEGL_BUILD_DISAGGREGATED implies MOBILEGL_PIPE_PUSH (P5 c0). The split path IS the
# pushed path: MG_Remote's server decodes records into the MGPipeApply* entry points, which
# live in MG_Pipe/PipeApply.cpp, which the PIPE_PUSH block below is what compiles. Without
# this, `-DMOBILEGL_BUILD_DISAGGREGATED=ON` alone configures and then fails to link the
# applier - and the shape it fails in (MG_Remote compiled, no applier) is indistinguishable
# at the CMake level from a legitimate transport-only build, which is why it is stated here
# rather than left to whoever hits the link error. Same normal-variable form as the two
# implications above.
if (MOBILEGL_BUILD_DISAGGREGATED AND NOT MOBILEGL_PIPE_PUSH)
message(STATUS "MobileGL: MOBILEGL_BUILD_DISAGGREGATED=ON forces MOBILEGL_PIPE_PUSH ON for "
"this configure: the split path decodes into the MGPipe applier, and the "
"applier is what MOBILEGL_PIPE_PUSH compiles")
set(MOBILEGL_PIPE_PUSH ON)
endif()
# MOBILEGL_PIPE_VERIFY implies MOBILEGL_PIPE_PUSH: the comparator compares the pushed block
@@ -516,6 +552,28 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
# Keeps MG_Util/Debug/Log.h - and through it the GL frontend's
# umbrella header - out of the header-only wire code (WireLog.h).
MobileGL/MG_Remote/Transport/WireLog.cpp
# ---- P5: the three new directories ------------------------------
# Wire/ the G3 codec: MGPWireRec_* in and out of SEG_CMD, blobs and
# var-tails in and out of SEG_STAGE. [w1]
# Client/ the emitting role: session, the 69-slot emit table, the
# caps mirror. [c1]
# Server/ the applying role: session, the applier bridge onto the
# existing MGPipeApply* free functions, the apply thread. [v1]
#
# Every file below lands in P5 as a HEADER plus a .cpp of named
# Fatal stubs, so that all seven P5 packages compile and link on day
# one against signatures that cannot then move under them. A stub is
# MGLOG_F + std::abort, never a silent no-op: an unimplemented
# emitter that returns quietly is how a split lane runs monolith and
# goes green (ARCHITECTURE.md 10.3).
MobileGL/MG_Remote/CapsCodec.cpp
MobileGL/MG_Remote/Wire/PipeWireCodec.cpp
MobileGL/MG_Remote/Client/ClientSession.cpp
MobileGL/MG_Remote/Client/EmitTables.cpp
MobileGL/MG_Remote/Client/CapsMirror.cpp
MobileGL/MG_Remote/Server/ServerSession.cpp
MobileGL/MG_Remote/Server/PipeApplier.cpp
MobileGL/MG_Remote/Server/ServerLoop.cpp
)
endif()
+103 -1
View File
@@ -335,7 +335,10 @@ namespace MobileGL::MG_Config {
// 0x100 vertex input (vertex elements / vertex buffers / index buffer)
// 0x200 framebuffer (set_framebuffer_state) - requires 0x400
// 0x400 texture resources (texture + renderbuffer resource_*,
// set_texture_params) - requires 0x80
// set_texture_params) - requires 0x80 AND 0x800
// (the built-in sampler CSO a set_texture_params record names is minted by
// the sampler family alone, ID-15; the four rows are MG_Impl/Pipe/PipeFill.cpp's
// kMGPipeP4aFamilyDependencies, mirrored bit for bit by Espryt's resolvers)
// 0x800 samplers (sampler CSO, sampler view, set_sampler_views /
// bind_sampler_states / set_shader_images) - requires 0x400
// 0x1000 programs (shader CSO, set_draw/dispatch_program, global constants)
@@ -411,4 +414,103 @@ namespace MobileGL::MG_Config {
String PipeStatsFile;
};
extern FeaturesTable Features;
// ---------------------------------------------------------------------------------
// P5: the transport selector and the MOBILEGL_IPC_* family (ARCHITECTURE.md 16, 附 A)
// ---------------------------------------------------------------------------------
//
// MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>.
//
// WHY `Transport` IS NOT A FeaturesTable MEMBER. ARCHITECTURE.md:580 requires that with
// MOBILEGL_BUILD_DISAGGREGATED=OFF it be a `constexpr Monolith`, so that the single hook
// in MG_Backend/Init.cpp compiles away entirely rather than becoming a branch nobody can
// take. A FeaturesTable member is a runtime field in every build, which is the opposite
// of that; it would also resize MG_Config::Features and break G1 (the pull build's
// symbol set must not move) for the same reason the MOBILEGL_PIPE_VERIFY knobs above sit
// behind their own #if.
//
// ONE CONSEQUENCE, STATED SO IT IS NOT REDISCOVERED: in a build without the option,
// MOBILEGL_TRANSPORT=inproc is ACCEPTED BY THE ENVIRONMENT AND SILENTLY IGNORED - the
// parser below does not exist to complain about it, and putting a complaint in the
// unconditional part of ConfigLoader would move a pull-build symbol. That is the exact
// shape of "the split lane ran monolith and went green", so the gate against it is a
// BUILD-level check, not a runtime one: `nm --defined-only libMobileGL.so | grep -i
// MG_Remote` must be non-empty in build-split (CONTRACT-P5.md table 3, and the CI job
// P5 adds beside build-linux-verify).
enum class TransportMode : Uint8 {
Monolith = 0, // today's in-library backend; no MG_Remote object is constructed
InProcess = 1, // P5: a real apply thread in this process, over the same G3 codec
Spawn = 2, // P6: fork/exec MobileGLServer, socketpair
UnixSocket = 3,// P6: connect to an existing AF_UNIX endpoint (Endpoint = <path>)
NamedPipe = 4, // P6: Windows named pipe (Endpoint = <name>)
};
#if MOBILEGL_BUILD_DISAGGREGATED
// Parsed once by MG_ConfigLoader::Init(). Defaults to Monolith even here: building the
// transport in is not the same as using it, and every existing lane of a build-split
// must keep running monolith unless it is asked for one.
extern TransportMode Transport;
// The <path> of `unix:` / the <name> of `pipe:`. Empty for the other three modes.
extern String TransportEndpoint;
// The MOBILEGL_IPC_* family. A separate table rather than more FeaturesTable members,
// for the G1 reason above and because every field here is meaningless without the
// transport: a build that cannot reach the MG_Remote code cannot honour one of them.
//
// P5 lands exactly the knobs P5's own packages read. A later phase's knob is added HERE,
// through the integrator, and not invented at its call site - ARCHITECTURE.md:615 holds
// the full planned inventory (PRESENT_CREDIT, POLL_ESCALATE, SHADOW_SHM,
// INLINE_PAYLOADS, TRACE, ATTACH, RESPAWN, IDLE_EXIT_S), and every one of those belongs
// to P6 or later.
struct IpcTable {
// MOBILEGL_IPC_SERVER_PATH: where to find libMobileGLServer. P6 consumes it; P5
// lands the parse because t1's ctest ENVIRONMENT blocks and add_trace_replay_test's
// SPLIT variant already carry it, and an environment variable that nothing parses is
// indistinguishable from one that is parsed and ignored.
String ServerPath;
// MOBILEGL_IPC_RING_MB: SEG_CMD size. A RECORD MAY BE AT MOST HALF OF THIS
// (RingProducer::MaxRecordBytes), so 8 MiB caps one record at 4 MiB; R-10 makes the
// codec publish a max-record-bytes counter rather than assume that is enough.
Uint32 RingMb = 8;
// MOBILEGL_IPC_STAGE_MB: SEG_STAGE size. Every blob and every var-tail's bytes live
// here (R-10: no chunking in P5, so nothing may exceed it).
Uint32 StageMb = 32;
// MOBILEGL_IPC_SPIN_US: spin before parking on a doorbell, either direction.
Uint32 SpinUs = 50;
// MOBILEGL_IPC_PERSISTENT_BLOCK_KB: block granularity of the persistent-map push.
// 0 IS A NEGATIVE CONTROL, NOT "unlimited": it disables the push, and
// PersistentCoherentMapScenario must go RED under it (exit gate E3(a)).
Uint32 PersistentBlockKb = 64;
// MOBILEGL_IPC_ADOPT_TIER: 2 = emulate (client keeps the shadow and pushes), which
// is the only tier P5 implements and the reason persistent-map-push can be non-zero
// at all (R-6). 0 and 1 parse and are Fatal at use with "P11"; they exist now so the
// negative control has a spelling the day P11 writes it.
Uint32 AdoptTier = 2;
// MOBILEGL_IPC_VERB_BARRIER: 1 = the client blocks at every verb boundary until
// appliedSeq reaches its emitSeq (R-1). 0 is the negative control: it is EXPECTED to
// be red, because 31 of the 63 PipeInputs fields are still pulled from a live
// GLContext by the client's residual fill and a free-running queue lets the server
// read a FUTURE value of them.
Uint32 VerbBarrier = 1;
// MOBILEGL_IPC_STRICT_ERRORS: promote a BARRIER-PULLED field read - and, in a split
// build, the seven sticky forwards that are otherwise exempt - from "count it in
// rsp" to Fatal (R-7.3).
Bool StrictErrors = false;
// MOBILEGL_IPC_AUDIT: after a record retires, the server fills the SEG_STAGE bytes
// it referenced with 0xDD (R-2.5). This is the ONLY mechanical control that an
// inproc implementation did not quietly keep using a pointer past its lifetime.
Bool Audit = false;
// MOBILEGL_IPC_SERVER_AFFINITY: `auto` (the default, big-core detection borrowed
// from ShaderCompilePool), `off`, or an explicit CPU mask. Kept as the raw string
// because the resolved mask is logged by whoever starts the apply thread, and the
// string is what an operator typed.
String ServerAffinity = "auto";
};
extern IpcTable Ipc;
#else
// The whole point: in a build without MG_Remote this folds at compile time, so
// `if (MG_Config::Transport != MG_Config::TransportMode::Monolith)` in Init.cpp is a
// discarded statement and the pull build gains no symbol, no branch and no byte.
inline constexpr TransportMode Transport = TransportMode::Monolith;
#endif
} // namespace MobileGL::MG_Config
+103
View File
@@ -25,6 +25,15 @@ namespace MobileGL::MG_Config {
// Zero/default-initialized at static-init time (all fields have constexpr-friendly
// defaults), so it is safe to read even if MG_ConfigLoader::Init has not run yet.
FeaturesTable Features;
#if MOBILEGL_BUILD_DISAGGREGATED
// Same contract, and for the same reason: MG_Backend::Init() reads Transport, and a
// build order that put it before MG_ConfigLoader::Init() must see Monolith rather than
// a torn enum. Defined only here - in a pull build Config.h makes Transport a constexpr
// and there is nothing to define.
TransportMode Transport = TransportMode::Monolith;
String TransportEndpoint;
IpcTable Ipc;
#endif
} // namespace MobileGL::MG_Config
namespace MobileGL::MG_ConfigLoader {
@@ -297,12 +306,106 @@ namespace MobileGL::MG_ConfigLoader {
#undef ENTRY
}
#if MOBILEGL_BUILD_DISAGGREGATED
// MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>
// (ARCHITECTURE.md:583). Shaped after InitBackendType above: an exact-name table, then
// one fallback that names what it did instead. The two prefixed forms are the only
// reason this is not literally that function's ENTRY macro.
//
// spawn / unix: / pipe: PARSE AND THEN REFUSE. They are P6's, and the refusal is NAMED
// rather than silent, because the failure this avoids is a P6 lane that set
// MOBILEGL_TRANSPORT=spawn, fell back to monolith, and went green on the wrong arm.
// The mode is left at Monolith so nothing half-initializes.
inline void InitTransport() {
String value;
QueryEnvVariable("MOBILEGL_TRANSPORT", value, "monolith");
String lowered = value;
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
MG_Config::TransportEndpoint.clear();
if (lowered.empty() || lowered == "monolith") {
MG_Config::Transport = MG_Config::TransportMode::Monolith;
return;
}
if (lowered == "inproc") {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
MGLOG_I("Config: MOBILEGL_TRANSPORT=inproc - the MGPipe record stream crosses a real "
"ring to an apply thread");
return;
}
// The three P6 forms. Recognised precisely, so the diagnostic can say "not yet"
// rather than "unknown", which are different bugs on the operator's side.
if (lowered == "spawn" || lowered.compare(0, 5, "unix:") == 0 ||
lowered.compare(0, 5, "pipe:") == 0) {
MGLOG_E("Config: MOBILEGL_TRANSPORT='%s' names a transport P6 implements and P5 does "
"not; staying on monolith. This run is NOT a split run.",
value.c_str());
MG_Config::Transport = MG_Config::TransportMode::Monolith;
return;
}
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_TRANSPORT='%s'; expected "
"monolith|inproc|spawn|unix:<path>|pipe:<name>, using monolith",
value.c_str());
MG_Config::Transport = MG_Config::TransportMode::Monolith;
}
// The MOBILEGL_IPC_* family (Config.h IpcTable). Parsed unconditionally rather than only
// when Transport != Monolith: a knob that silently means nothing on one arm of an A/B is
// how an A/B stops being one, and the ranges below are the diagnostics.
inline void InitIpc() {
auto& ipc = MG_Config::Ipc;
QueryEnvVariable("MOBILEGL_IPC_SERVER_PATH", ipc.ServerPath, "");
// Both ring floors are 1 MiB, not 0: a ring caps ONE record at half its size, and
// the catalogue's largest fixed payload (MGPFramebufferState, 304 bytes) plus a
// create_shader_state archive already needs far more than a toy ring. The ceilings
// are sanity, not policy.
ipc.RingMb = QueryEnvUint32("MOBILEGL_IPC_RING_MB", 8, 1, 1024);
ipc.StageMb = QueryEnvUint32("MOBILEGL_IPC_STAGE_MB", 32, 1, 4096);
ipc.SpinUs = QueryEnvUint32("MOBILEGL_IPC_SPIN_US", 50, 0, 1000000);
// 0 is admitted ON PURPOSE and is the negative control of exit gate E3(a): it turns
// the persistent-map push OFF, and PersistentCoherentMapScenario must go red.
ipc.PersistentBlockKb = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_BLOCK_KB", 64, 0, 65536);
// 2 is the only tier P5 implements (R-6). 0 and 1 parse here and are refused at the
// point of use, which is where the "P11" in the message belongs.
ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", 2, 0, 2);
ipc.VerbBarrier = QueryEnvUint32("MOBILEGL_IPC_VERB_BARRIER", 1, 0, 1);
ipc.StrictErrors = QueryEnvFlag("MOBILEGL_IPC_STRICT_ERRORS");
ipc.Audit = QueryEnvFlag("MOBILEGL_IPC_AUDIT");
QueryEnvVariable("MOBILEGL_IPC_SERVER_AFFINITY", ipc.ServerAffinity, "auto");
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
// One line, on the arm where these numbers decide behaviour, because every one of
// them is a number a bug report has to quote.
MGLOG_I("Config: IPC ring=%uMiB stage=%uMiB spin=%uus persistent-block=%uKiB "
"adopt-tier=%u verb-barrier=%u strict=%d audit=%d affinity='%s'",
ipc.RingMb, ipc.StageMb, ipc.SpinUs, ipc.PersistentBlockKb, ipc.AdoptTier,
ipc.VerbBarrier, static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
ipc.ServerAffinity.c_str());
if (ipc.VerbBarrier == 0) {
MGLOG_W("Config: MOBILEGL_IPC_VERB_BARRIER=0 is the R-1 NEGATIVE CONTROL and is "
"expected to fail: the client still pulls 31 of 63 PipeInputs fields from a "
"live GLContext, so an unbarriered queue lets the server read future values");
}
if (ipc.PersistentBlockKb == 0) {
MGLOG_W("Config: MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 is the E3(a) NEGATIVE CONTROL: "
"the persistent-map push is OFF and a coherent-map scenario must go red");
}
}
#endif
void Init() {
MGLOG_D("Loading configuration from environment variables...");
InitializeAcceptedEnvVariables();
InitBackendType();
InitFeatures();
#if MOBILEGL_BUILD_DISAGGREGATED
// After InitFeatures, so the one line InitIpc logs is the last word on this run's
// configuration, and before the accepted-env map is destroyed just below.
InitTransport();
InitIpc();
#endif
// Destroy the map since we won't need it anymore
acceptedEnvVariablesMap.reset();
@@ -5252,6 +5252,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (m_imageBindableStorageRequired) {
return;
}
#if MOBILEGL_PIPE_PUSH
// Whether this transition re-mints storage that ALREADY EXISTED on the backend: that
// is the remint PULL (the levels below are replayed from the client's shadow to fill
// the new carrier), and it is what ROADMAP open question 2 counts. A texture reaching
// here uninitialised is allocated image-bindable up front and pulls nothing.
const Bool hadBackendStorage = m_isInitialized;
#endif
m_imageBindableStorageRequired = true;
m_isInitialized = false;
// Every level this object has ALREADY uploaded has to be replayed, because the
@@ -5326,6 +5333,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!markedRemintPull) {
MG_Pipe::MGPipeUnmigratedEmulation("texture-remint-pull");
markedRemintPull = true;
// THE COUNTER BEHIND ROADMAP OPEN QUESTION 2 (final review M-A): one per
// transition that replays a level of storage the backend already held.
if (hadBackendStorage && MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureRemintPulls, 1);
}
}
if (!MG_Pipe::MGPipeHandleIsNull(rearmRes)) {
const MG_Pipe::MGPBox wholeLevel{0,
+20
View File
@@ -389,6 +389,26 @@ namespace MobileGL::MG_Pipe {
// ---- what a unit case reads. The emitter builds INTO these and hands the applier the
// same objects, so "what was emitted" costs no copy. ----
// ---- the death half (P4a final review C-2) ----
//
// Called by the contract's death helper before the slot is freed (there is no wire
// delete for this kind, D-I2, so this is the only client-side thing a framebuffer's
// death has to do). The per-object Named latch is the entry: a recycled handle's Gen
// already refuses the stale latch, so this is hygiene rather than a fix - the rule
// (ID-8) is that whatever mints a handle retires everything it keeps under it at the
// death, and every P4a kind takes the same shape. Gen-keyed for a late notice.
void NoteFramebufferDied(MGPipeHandle handle) {
const SizeT slot = handle.Slot;
if (MGPipeHandleIsNull(handle) || slot >= m_named.size()) return;
if (m_named[slot].Gen == handle.Gen) m_named[slot] = NamedEntry{};
}
// "Does this emitter hold a Named-record latch for this handle at its generation."
Bool NamedRecordIsLatched(MGPipeHandle handle) const {
const SizeT slot = handle.Slot;
if (MGPipeHandleIsNull(handle) || slot >= m_named.size()) return false;
return m_named[slot].Has && m_named[slot].Gen == handle.Gen;
}
const MGPFramebufferState& LastDraw() const { return m_lastDraw; }
const MGPFramebufferState& LastRead() const { return m_lastRead; }
const MGPFramebufferState& LastNamed() const { return m_lastNamed; }
+8
View File
@@ -38,6 +38,7 @@
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/PipeApply.h>
#include <MG_Pipe/PipeMutation.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/TextureState/TextureState.h>
#include <MG_Util/Metrics/PipeStats.h>
@@ -97,6 +98,13 @@ namespace MobileGL::MG_Pipe {
entry.Res = binding.Texture ? MGPipeSlots().Acquire(MGPipeKind::Texture,
binding.Texture->GetLifetimeId())
: kMGPipeNullHandle;
// D-A4: a texture named in an emitted MGPImageView is SHADER-IMAGE-bound from
// then on - the bit ImageBindableHint is derived from. The bind itself noted it
// first (TextureState.h, so the hint precedes the first sync); this is the
// letter of the rule and a one-compare early-out once the bit is set.
if (!MGPipeHandleIsNull(entry.Res)) {
MGPipeNoteTextureBoundAs(entry.Res, static_cast<Uint32>(kMGPipeBindShaderImage));
}
// THE APPLICATION's format and access, verbatim. The bind-format recast and the
// buffer-texture split view are server-side and stay there; so does
// SupportsLayeredImageBinding's rule, which asks the BACKEND target after
+48 -2
View File
@@ -1255,10 +1255,12 @@ namespace MobileGL::MG_Pipe {
MGPipeTextureEmitterInstance(), [&](auto& emitter) { emitter.EmitResourceCreate(texture); });
}
void MGPipeEmitTextureResourceRespecify(ITextureObject& texture) {
void MGPipeEmitTextureResourceRespecify(ITextureObject& texture, MGPipeTextureRespecifyScope scope,
Uint32 uploadTarget, Uint32 level) {
if (!FamilyIsLive(kMGPipeSubsystemTextureResources, kMGPipeWiredTextureSubsystem)) return;
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
MGPipeTextureEmitterInstance(), [&](auto& emitter) { emitter.EmitResourceRespecify(texture); });
MGPipeTextureEmitterInstance(),
[&](auto& emitter) { emitter.EmitResourceRespecify(texture, scope, uploadTarget, level); });
}
void MGPipeEmitTextureParams(ITextureObject& texture) {
@@ -1288,6 +1290,21 @@ namespace MobileGL::MG_Pipe {
[&](auto& emitter) { emitter.EmitRenderbufferRespecify(renderbuffer); });
}
void MGPipeNoteTextureBoundAs(MGPipeHandle texture, Uint32 bindBit) {
// Not gated on FamilyIsLive: the mask is client state (see the declaration), and the
// emitter gates the emission it causes.
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
MGPipeTextureEmitterInstance(),
[&](auto& emitter) { emitter.NoteTextureBoundAs(texture, static_cast<Uint16>(bindBit)); });
}
void MGPipeNoteTextureImageBound(ITextureObject& texture) {
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(MGPipeTextureEmitterInstance(), [&](auto& emitter) {
emitter.NoteTextureBoundAs(emitter.AcquireTexture(texture.GetLifetimeId(), &texture),
static_cast<Uint16>(kMGPipeBindShaderImage));
});
}
void MGPipeEmitSamplerCsoCreate(SamplerObject& sampler) {
if (!FamilyIsLive(kMGPipeSubsystemSamplers, kMGPipeWiredSamplerSubsystem)) return;
ForwardWhenWired<kMGPipeWiredSamplerSubsystem>(
@@ -1368,11 +1385,24 @@ namespace MobileGL::MG_Pipe {
}
} // namespace
// THE EMITTER IS TOLD BETWEEN THE WIRE DELETE AND THE FREE (P4a final review C-2), for
// every kind that keeps client state under a handle: a texture's drain entries, pointer,
// cache reference and latches; a renderbuffer's entry; a framebuffer's Named latch; a
// sampler view's and a shader CSO's record memo. Before this the six helpers freed the slot
// and told nobody, so the texture emitter kept the freed ITextureObject* and the level on
// the drain list, and `glTexImage2D; glDeleteTextures; <verb>` called a virtual on freed
// memory from the next validate point. The forward is the P3a shape
// (MGPipeEmitVertexElementsDestroyAndFree's emitter.NoteRecordDestroyed) applied to the
// five P4a kinds that have an entry to retire; the content-addressed sampler CSO keeps
// none per object (its death is the cache's LRU, ID-17). Unconditional in a push build,
// like the mints: the entries exist whether or not the family bit is set.
Bool MGPipeEmitSamplerViewCsoDestroyAndFree(Uint64 lifetimeId) {
const MGPipeHandle handle =
MGPipeSlots().FindByLifetimeId(MGPipeKind::SamplerViewCso, lifetimeId);
const Bool published =
EmitDeleteIfPublished(MGPipeKind::SamplerViewCso, handle, &MGPipeApplyDeleteSamplerView);
ForwardWhenWired<kMGPipeWiredSamplerSubsystem>(
MGPipeSamplerEmitterInstance(), [&](auto& emitter) { emitter.NoteRecordDestroyed(handle); });
// THE NOTICE IS RAISED FOR THIS KIND TOO, and the reason it once was not is wrong:
// NotifyStateObjectDestroyed takes a KIND and a lifetime id, not an object
// (StateObjectDeathNotice.h - one entry point for every kind rather than one ops table
@@ -1392,6 +1422,11 @@ namespace MobileGL::MG_Pipe {
const MGPipeHandle handle = MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, lifetimeId);
const Bool published =
EmitDeleteIfPublished(MGPipeKind::Texture, handle, &MGPipeApplyResourceDestroy);
// The emitter retires its entry while the handle still resolves (C-2): the drain list
// drops the dead texture's levels, the raw pointer goes, the built-in sampler's cache
// reference is given back, the latches and the sticky mask are cleared.
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
MGPipeTextureEmitterInstance(), [&](auto& emitter) { emitter.NoteTextureDied(handle); });
NotifyAndFree(MGPipeKind::Texture, lifetimeId, handle);
// THE SAMPLER VIEW DIES WITH ITS TEXTURE, because it is minted off the same lifetime
// id: one SamplerViewCso per ITextureObject (D-F2), re-issued on the same handle
@@ -1428,6 +1463,8 @@ namespace MobileGL::MG_Pipe {
MGPipeSlots().FindByLifetimeId(MGPipeKind::Renderbuffer, lifetimeId);
const Bool published =
EmitDeleteIfPublished(MGPipeKind::Renderbuffer, handle, &MGPipeApplyResourceDestroy);
ForwardWhenWired<kMGPipeWiredTextureSubsystem>(
MGPipeTextureEmitterInstance(), [&](auto& emitter) { emitter.NoteRenderbufferDied(handle); });
NotifyAndFree(MGPipeKind::Renderbuffer, lifetimeId, handle);
return published;
}
@@ -1452,6 +1489,8 @@ namespace MobileGL::MG_Pipe {
// whatever it owed", which for a framebuffer is the death notice this just raised.
const MGPipeHandle handle =
MGPipeSlots().FindByLifetimeId(MGPipeKind::Framebuffer, lifetimeId);
ForwardWhenWired<kMGPipeWiredFramebufferSubsystem>(
MGPipeFramebufferEmitterInstance(), [&](auto& emitter) { emitter.NoteFramebufferDied(handle); });
NotifyAndFree(MGPipeKind::Framebuffer, lifetimeId, handle);
return false;
}
@@ -1461,6 +1500,11 @@ namespace MobileGL::MG_Pipe {
MGPipeSlots().FindByLifetimeId(MGPipeKind::SamplerCso, lifetimeId);
const Bool published =
EmitDeleteIfPublished(MGPipeKind::SamplerCso, handle, &MGPipeApplyDeleteSamplerState);
// NOTHING TO RETIRE IN AN EMITTER FOR THIS KIND, stated rather than implied: a sampler
// CSO is content-addressed and belongs to a value, so no emitter keeps an entry under
// a SamplerObject's handle - the cache's entries are keyed by value and reference
// count, and the death of a bound sampler object releases its unit's reference at the
// next bind_sampler_states pass (SamplerEmit.h's reconciliation).
NotifyAndFree(MGPipeKind::SamplerCso, lifetimeId, handle);
return published;
}
@@ -1477,6 +1521,8 @@ namespace MobileGL::MG_Pipe {
MGPipeSlots().FindByLifetimeId(MGPipeKind::ShaderCso, lifetimeId);
const Bool published =
EmitDeleteIfPublished(MGPipeKind::ShaderCso, handle, &MGPipeApplyDeleteShaderState);
ForwardWhenWired<kMGPipeWiredProgramSubsystem>(
MGPipeProgramEmitterInstance(), [&](auto& emitter) { emitter.NoteRecordDestroyed(handle); });
NotifyAndFree(MGPipeKind::ShaderCso, lifetimeId, handle);
return published;
}
+6 -8
View File
@@ -324,14 +324,12 @@ namespace MobileGL::MG_Pipe {
// The memo's other half, and the bound-mirror clearing beside it.
//
// NO PRODUCTION CALLER TODAY, stated rather than implied: since c0b the death path
// reads the contract's latch and never asks an emitter. It is kept because the memo
// above needs a way to be told, and because everything it clears SELF-HEALS if it is
// not called - the slot's Gen moves on reuse, so `RecordGen == handle.Gen` refuses a
// stale record latch, and the three bound mirrors below hold a handle whose generation
// can never be handed out again, so the next EmitShaderState compares against a
// different handle and re-binds. Clearing them here is the cheaper answer, not the
// load-bearing one.
// THE CALLER IS THE CONTRACT's DEATH HELPER (P4a final review C-2): the death path
// reads the contract's latch for the wire delete and then forwards here, before the
// slot is freed, so a dead handle no longer reads as published in this memo between
// the death and the recycle and the three bound mirrors never name a dead program.
// Gen-keyed, so a late notice for a slot already handed out again clears nothing of
// the successor's.
void NoteRecordDestroyed(MGPipeHandle handle) {
if (MGPipeHandleIsNull(handle)) return;
Vector<Latch>& table = TableOf(handle);
+10 -5
View File
@@ -767,6 +767,11 @@ namespace MobileGL::MG_Pipe {
if (MG_State::GLState::SamplesAsIncompleteTexture(texture.get(), effective)) continue;
entry.Texture = MGPipeSlots().Acquire(MGPipeKind::Texture, texture->GetLifetimeId());
// D-A4: a texture the sampler-view resolution names in an emitted MGPBoundView
// is SAMPLER-bound from then on (sticky; the texture emitter's contract door,
// since this header is included BY TextureEmit.h). One early-out per unit per
// pass once the bit is set.
MGPipeNoteTextureBoundAs(entry.Texture, static_cast<Uint32>(kMGPipeBindSampler));
entry.View = AcquireSamplerView(*texture, entry.Texture, bytes);
}
@@ -986,11 +991,11 @@ namespace MobileGL::MG_Pipe {
}
// The memo's other half, for a caller that knows the applier has dropped this record.
// NO PRODUCTION CALLER TODAY, and that is stated rather than implied: the death path
// goes through the contract's latch, not through here. It is kept because the memo
// above needs a way to be told, and because leaving the latch standing SELF-HEALS
// anyway - the slot's Gen moves on reuse, so the `RecordGen == handle.Gen` test in
// RecordIsPublished and in AcquireSamplerView already refuses a stale entry.
// THE CALLER IS THE CONTRACT's DEATH HELPER (P4a final review C-2): the texture's
// helper drops the sampler view minted off the texture's lifetime id and forwards here
// before the slot is freed, so a dead handle no longer reads as published in this memo
// between the death and the recycle. Gen-keyed, so a late notice for a slot already
// handed out again clears nothing of the successor's.
void NoteRecordDestroyed(MGPipeHandle handle) {
if (MGPipeHandleIsNull(handle)) return;
const SizeT slot = handle.Slot;
+266 -51
View File
@@ -441,24 +441,89 @@ namespace MobileGL::MG_Pipe {
// The texture a handle names, or null. A RAW pointer is exact here for
// MGPipeResourceTracker::Resolve's reason: the entry exists only between the create the
// constructor emits and the destroy the destructor emits, and the Gen compare is what
// refuses a stale handle rather than resolving it to whatever now occupies the slot.
// constructor emits and the destroy the destructor emits - and since the final review's
// C-2 that sentence is ESTABLISHED rather than assumed: the contract's death helper
// forwards to NoteTextureDied below before it frees the slot, so a dead handle finds a
// null pointer here. The Gen compare refuses a RECYCLED handle rather than resolving it
// to whatever now occupies the slot.
//
// A DEAD SLOT IS REFUSED, LOUDLY. The allocator's generation moves only at the NEXT
// hand-out, so between a death and a recycle a dead handle compares equal to the slot's
// generation - which is why the guard is IsLive and not GenOfSlot (the review's C-2:
// that compare guarded a recycled slot and never a dead one, and the drain then called
// a virtual on the freed object once per verb). Reaching this arm at all means a death
// path skipped the emitter, which is a seam defect and not traffic: it is counted and
// logged once, and the answer is null.
ITextureObject* ResolveTexture(MGPipeHandle handle) const {
const SizeT slot = handle.Slot;
if (MGPipeHandleIsNull(handle) || slot >= m_textures.size()) return nullptr;
const Entry& entry = m_textures[slot];
if (entry.Texture == nullptr || entry.Gen != handle.Gen) return nullptr;
if (MGPipeSlots().GenOfSlot(MGPipeKind::Texture, handle.Slot) != handle.Gen) return nullptr;
if (!MGPipeSlots().IsLive(MGPipeKind::Texture, handle)) {
++m_deadResolves;
MGLOG_E_ONCE("MGPipe: texture handle {slot=%u, gen=%u} is dead but the emitter still holds its "
"object - the death path did not retire the entry; refused rather than resolved",
handle.Slot, handle.Gen);
return nullptr;
}
return entry.Texture;
}
// ---- the death half (P4a final review C-2) ----
//
// CALLED BY THE CONTRACT'S DEATH HELPER, after the wire delete went out and BEFORE the
// slot is freed (ID-8's order: delete, notice, free - this sits between the first two).
// v2 had no such door: the helper freed the slot, the emitter kept the freed
// ITextureObject* and the level on the drain list, and `glTexImage2D; glDeleteTextures;
// <any verb>` walked freed memory at the next validate point - a SIGABRT ("pure virtual
// method called") at the shipping mask. Everything the entry owns goes here: the drain
// entries (nothing is owed for a dead texture - its record is gone with the wire delete),
// the built-in sampler's cache reference (ID-17: one per entry, released at the death
// and no longer at the recycle), the latches and the sticky mask. RetireIfRecycled stays
// as the belt for a slot whose death this emitter was never told about.
//
// Keyed on the GENERATION so a late notice for a slot that has already been handed out
// again cannot retire the successor's entry.
void NoteTextureDied(MGPipeHandle handle) {
const SizeT slot = handle.Slot;
if (MGPipeHandleIsNull(handle) || slot >= m_textures.size()) return;
Entry& entry = m_textures[slot];
if (entry.Gen != handle.Gen) return;
if (!entry.DrainKeys.empty()) {
// A death inside the drain cannot happen (no SharedPtr drops there), but if one
// ever did the loop below is iterating m_drain: the null pointer the reset
// leaves is what makes EmitOneLevel answer "nothing owed" and drop the entry.
if (!m_draining) {
SizeT kept = 0;
for (SizeT i = 0; i < m_drain.size(); ++i) {
if (m_drain[i].Handle == handle) continue;
m_drain[kept++] = m_drain[i];
}
m_drain.resize(kept);
}
entry.DrainKeys.clear();
}
MGPipeSamplerCsoCacheInstance().Release(entry.BuiltinSampler);
entry = Entry{};
}
void NoteRenderbufferDied(MGPipeHandle handle) {
const SizeT slot = handle.Slot;
if (MGPipeHandleIsNull(handle) || slot >= m_renderbuffers.size()) return;
Entry& entry = m_renderbuffers[slot];
if (entry.Gen != handle.Gen) return;
entry = Entry{};
}
// ---- the sticky bind mask (D-A4) ----
//
// ORed, never cleared, and emitted on BOTH resource_create and every
// resource_respecify, exactly as P3a's buffer mask is. The four bits nothing set before
// P4a get their producers here and in the framebuffer emitter: RENDER_TARGET and
// DEPTH_STENCIL from an attachment point, SAMPLER from a resolved sampler view and
// SHADER_IMAGE from a resolved image unit (the sampler package's two).
// DEPTH_STENCIL from an attachment point (FramebufferEmit.h), SAMPLER from a resolved
// sampler view (SamplerEmit.h) and SHADER_IMAGE from glBindImageTexture's state setter
// and the resolved image unit (TextureState.h, ImageEmit.h) - the last two through the
// contract's MGPipeNoteTextureBoundAs door, since neither may include this header
// (final review M-A: before the fix round nothing produced them and the hint was dead).
// A MASK CHANGE AFTER THE ALLOCATION IS A METADATA RESPECIFY (ID-18 M4), and without it
// the sticky half of D-A4 is a no-op for exactly the textures it was written for. The
// mask rides resource_create and every resource_respecify - and an IMMUTABLE texture has
@@ -474,6 +539,11 @@ namespace MobileGL::MG_Pipe {
void NoteTextureBoundAs(MGPipeHandle handle, Uint16 bit) {
if (MGPipeHandleIsNull(handle)) return;
Entry& entry = EntryFor(m_textures, handle);
// The entry is stamped with the generation it is written under, and a predecessor's
// entry on a recycled slot is retired first (the same door AcquireTexture takes): a
// texture born while the family bit was clear has no create to have done it.
RetireIfRecycled(entry, handle);
entry.Gen = handle.Gen;
const Uint16 before = entry.BindMask;
const Uint16 now = static_cast<Uint16>(before | bit);
if (now == before) return;
@@ -491,6 +561,8 @@ namespace MobileGL::MG_Pipe {
void NoteRenderbufferBoundAs(MGPipeHandle handle, Uint16 bit) {
if (MGPipeHandleIsNull(handle)) return;
Entry& entry = EntryFor(m_renderbuffers, handle);
RetireIfRecycled(entry, handle);
entry.Gen = handle.Gen;
const Uint16 before = entry.BindMask;
const Uint16 now = static_cast<Uint16>(before | bit);
if (now == before) return;
@@ -523,14 +595,36 @@ namespace MobileGL::MG_Pipe {
PublishCreate(MGPipeKind::Texture, handle, entry, desc);
}
// resource_respecify, from every storage-defining entry point. DEDUPED ON THE
// DESCRIPTOR ITSELF rather than on a version, because the entry points that reach here
// are the ones that move the SHAPE and several of them do not move the descriptor at
// all (glTexParameter TEXTURE_BASE_LEVEL bumps the shape version and changes no field
// this record carries). A byte compare of an 88-byte POD is cheaper than the emission
// it avoids, and it is the same "version-first skip before anything expensive" shape
// every other P4a emission takes.
void EmitResourceRespecify(ITextureObject& texture) {
// resource_respecify, from every storage-defining entry point, WITH THE SCOPE OF THE
// STORAGE IT REPLACES (P4a final review C-1; the scopes are PipeMutation.h's).
//
// THE LEVEL IS PASSED, AND IT IS WIRE'S KEY. The applier keeps a pending-upload set per
// (uploadTarget, level) - the client's dirty flags, inverted - and a respecify drops the
// entries against the storage it REPLACES: with a null MGPRespecifiedLevel every entry,
// with a level exactly that one. v2 passed null at every call, so a level the applier
// had ACCEPTED at one verb (the client flag already clear, D-D5 step 1) and that the
// next verb's glTexImage2D(level 1) or glGenerateMipmap grow defined AROUND was dropped
// with nobody owing its texels: `L0; draw(other); L1; draw(T)` read a black level 0.
// The key is built from the SAME packed MGPSubData::Target the drain puts in that
// level's record (wire-v3 §5 item 5), so what this drops is what that emission made.
//
// THREE SCOPES, one call each for the first two and one call PER REMOVED LEVEL for the
// chain cut: the applier's key is one (uploadTarget, level), so "every level from N"
// is spelled as N.., each after the first landing on an unchanged descriptor - which
// the applier classifies as a metadata update that drops nothing but the level it
// names. That is the refinement wire's W11 clause takes this round.
//
// DEDUPED ON THE DESCRIPTOR ITSELF for the whole-resource form only: the entry points
// that reach it move the SHAPE and several of them do not move the descriptor at all
// (glTexParameter TEXTURE_BASE_LEVEL bumps the shape version and changes no field this
// record carries), and a byte compare of an 88-byte POD is cheaper than the emission
// it avoids. A PER-LEVEL form is never deduped: the level it redefines is not in the
// descriptor (a non-base level's extent moves no field), so an unchanged descriptor
// cannot say whether the applier still holds a box against the OLD level - and a box
// kept across a shrink is uploaded past the end of the new one. One applier call per
// level definition is the cost, and the sub-data that follows moves the serial anyway.
void EmitResourceRespecify(ITextureObject& texture, MGPipeTextureRespecifyScope scope,
Uint32 uploadTarget, Uint32 level) {
const MGPipeHandle handle = AcquireTexture(texture.GetLifetimeId(), &texture);
// THE VIEW'S OWNER IS ACQUIRED FIRST, and no Entry& is held across it (m3): the
// owner's slot can be higher than this table's size, so AcquireTexture would
@@ -568,7 +662,37 @@ namespace MobileGL::MG_Pipe {
const MGPResourceDesc desc = MGPipeBuildTextureResourceDesc(
texture, handle, entry.BindMask, /*storageDefined=*/true, viewOf, bufferHandle, bufOffset,
bufSize);
if (entry.HasLastDesc && std::memcmp(&entry.LastDesc, &desc, sizeof(desc)) == 0) return;
const Bool unchanged = entry.HasLastDesc && std::memcmp(&entry.LastDesc, &desc, sizeof(desc)) == 0;
// THE KEYS THIS CALL DROPS. `keyCount == 0` is the whole resource (a null level
// pointer); otherwise `keyCount` keys from `firstLevel` up, all on `uploadTarget`.
Uint32 firstLevel = 0;
Uint32 keyCount = 0;
switch (scope) {
case MGPipeTextureRespecifyScope::OneLevel:
firstLevel = level;
keyCount = 1;
break;
case MGPipeTextureRespecifyScope::LevelsFrom: {
// A cut at 0 leaves nothing: the whole resource. Otherwise the removed levels
// are [level, the level count the applier last accepted): LastDesc mirrors
// acceptance, and a sub-data for a level the accepted descriptor does not
// describe is refused by the applier, so no key above that count can exist. A
// cut that removes nothing the applier could hold is deduped like the
// whole-resource form; if the descriptor moved anyway the first key carries it.
if (level == 0) break;
const Uint32 previous = entry.HasLastDesc ? static_cast<Uint32>(entry.LastDesc.Levels) : 0u;
if (previous <= level && unchanged) return;
firstLevel = level;
keyCount = previous > level ? previous - level : 1u;
break;
}
case MGPipeTextureRespecifyScope::WholeResource:
default:
if (unchanged) return;
break;
}
// SELF-HEALING IN BOTH DIRECTIONS, the P3a m12 shape: a texture born while the
// subsystem bit was clear has no applier record, and every later respecify would be
// REFUSED. A create rather than a respecify, because that is what the record's
@@ -589,24 +713,24 @@ namespace MobileGL::MG_Pipe {
// about what the APPLIER holds, so a refused respecify must leave LastDesc naming
// the descriptor that actually landed, or the next identical call is suppressed
// against a record that was never stored.
Bool accepted = ApplyRespecify(desc);
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
if (!accepted) {
// THE SECOND HALF OF THE SELF-HEAL, and the publication latch cannot give
// it: the latch answers "did a create for this handle GO OUT", which stays
// true after MGPipeApplierReleaseObjectRecords has dropped every object
// record - the scope a served context's teardown takes while the frontend
// objects live on in the share group. The applier's REFUSAL is the only
// signal that says "I hold nothing for this handle", and the acceptance
// return is what makes it visible from here at all. One retry, never a
// loop: a descriptor the applier refuses on its own merits (a target that
// names no resource kind) is refused again and the flags stay set.
const MGPResourceDesc healDesc = MGPipeBuildTextureResourceDesc(
texture, handle, entry.BindMask, /*storageDefined=*/false, viewOf,
bufferHandle, bufOffset, bufSize);
NoteDesc(healDesc, /*isCreate=*/true);
PublishCreate(MGPipeKind::Texture, handle, entry, healDesc);
accepted = ApplyRespecify(desc);
//
// THE PACKED TARGET IS THE DRAIN's (wire-v3 §5 item 5): the contract's packer takes
// two Uint32s, low byte the resource target, high byte the upload target (a cube
// face), and the applier matches the key against the sub-data records verbatim.
const Uint16 packedTarget = MGPipePackSubDataTarget(
static_cast<Uint32>(MGPipeResourceTargetForTextureTarget(texture.GetTarget())), uploadTarget);
Bool accepted = false;
if (keyCount == 0) {
accepted = RespecifyOnce(texture, handle, entry, desc, nullptr, viewOf, bufferHandle, bufOffset,
bufSize);
} else {
for (Uint32 i = 0; i < keyCount; ++i) {
MGPRespecifiedLevel key{};
key.UploadTarget = packedTarget;
key.Level = static_cast<Uint16>(firstLevel + i);
accepted = RespecifyOnce(texture, handle, entry, desc, &key, viewOf, bufferHandle, bufOffset,
bufSize);
if (!accepted) break;
}
}
NoteRespecified(entry, desc, accepted);
@@ -638,9 +762,10 @@ namespace MobileGL::MG_Pipe {
entry.SamplerVersion == samplerVersion && !entry.ForceParamsResync) {
return;
}
entry.HasParamsLatch = true;
entry.ParamsVersion = paramsVersion;
entry.SamplerVersion = samplerVersion;
// THE LATCH IS TAKEN BELOW, ON ACCEPTANCE (final review m-1, audit F-7) - like the
// sub-data and respecify paths, and unlike v2, which advanced it here and left a
// refused record (no applier record for the handle, the SD-1/SD-3 shape) unsent
// until the next glTexParameter* moved a version.
// ID-14 / ID-17: THE BUILT-IN SAMPLER COMES FROM C's CONTENT-ADDRESSED CACHE and is
// never minted here. v1 took MGPipeSlots().Acquire(SamplerCso, the SamplerObject's
@@ -654,9 +779,9 @@ namespace MobileGL::MG_Pipe {
// what stops the LRU pulling a handle out from under a standing MGPTextureParams
// record: the applier deliberately does not resolve BuiltinSampler, and an eviction
// is not a parameter change, so nothing would refuse and nothing would re-emit. The
// previous handle is released when the content moves it, and the last one when the
// slot is recycled (RetireIfRecycled) - which is the only moment this package can
// see a texture die, the death helper being A's.
// previous handle is released when the content moves it, and the last one at the
// texture's death (NoteTextureDied, reached from the contract's death helper) - or
// at the recycle, as the belt, for a death this emitter was not told about.
MGPipeSamplerCsoCache& cache = MGPipeSamplerCsoCacheInstance();
Uint64 samplerBytes = 0;
const MGPipeHandle builtinSampler =
@@ -673,10 +798,53 @@ namespace MobileGL::MG_Pipe {
const MGPTextureParams params =
MGPipeBuildTextureParams(texture, handle, entry.BuiltinSampler, entry.ForceParamsResync);
entry.ForceParamsResync = false;
m_lastParams = params;
++m_paramSets;
MGPipeApplySetTextureParams(params);
// Not behind MGPipeTextureRecordsReachTheApplier() (see its comment): the call is
// dispatched whenever this emitter runs, so the answer is always a real one.
Bool accepted = MGPipeApplySetTextureParams(params);
if (!accepted) {
// THE SELF-HEAL, the respecify path's shape, and the parameters are the one
// publication that may be a texture's FIRST: the context's default textures are
// constructed before the backend registers its consumer, so no create ever went
// out for them, and the application's first glTexParameter* on texture 0 found
// no record (the retrace census's residual once this refusal went loud). A
// create with no storage gives the record its identity, the storage follows if
// the texture has any (a respecify against the create's descriptor is never
// deduped away), and the parameters land on the record that now exists. The
// same repair covers the served context's teardown scope, where the records are
// dropped while the objects live on. One retry, never a loop.
const MGPResourceDesc healDesc = MGPipeBuildTextureResourceDesc(
texture, handle, entry.BindMask, /*storageDefined=*/false, kMGPipeNullHandle,
kMGPipeNullHandle, 0, 0);
NoteDesc(healDesc, /*isCreate=*/true);
PublishCreate(MGPipeKind::Texture, handle, entry, healDesc);
const auto* mipmap = MG_State::GLState::AsMipmapTexture(&texture);
const Bool hasStorage = mipmap != nullptr
? mipmap->GetMipmapLevelCount() > 0
: texture.GetStorageType() == MobileGL::TextureStorageType::Buffer;
if (hasStorage) {
// Can grow the table (a view's owner is acquired inside): no Entry& is held
// across it - `entry` is re-fetched below.
EmitResourceRespecify(texture, MGPipeTextureRespecifyScope::WholeResource, 0, 0);
}
accepted = MGPipeApplySetTextureParams(params);
}
Entry& latched = EntryFor(m_textures, handle);
if (!accepted) {
// Refused on its merits (a null built-in sampler, no consumer). Nothing latched:
// the same versions re-send at the next call. Loud for the reason the sub-data
// refusal is loud.
++m_refusedParamSets;
MGLOG_E_ONCE("MGPipe: set_texture_params for texture %u {slot=%u, gen=%u} was refused; the "
"latch is not taken and the parameters are re-sent at the next call",
texture.GetExternalIndex(), handle.Slot, handle.Gen);
return;
}
latched.HasParamsLatch = true;
latched.ParamsVersion = paramsVersion;
latched.SamplerVersion = samplerVersion;
latched.ForceParamsResync = false;
}
void EmitRenderbufferCreate(RenderbufferObject& renderbuffer) {
@@ -713,7 +881,8 @@ namespace MobileGL::MG_Pipe {
PublishCreate(MGPipeKind::Renderbuffer, handle, entry, createDesc);
}
NoteDesc(desc, /*isCreate=*/false);
Bool accepted = ApplyRespecify(desc);
// A renderbuffer's storage is always the whole object: no levels, so no key.
Bool accepted = ApplyRespecify(desc, nullptr);
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
if (!accepted) {
// See the texture twin: the applier's refusal is the only thing that can
@@ -722,7 +891,7 @@ namespace MobileGL::MG_Pipe {
renderbuffer, handle, entry.BindMask, /*storageDefined=*/false);
NoteDesc(healDesc, /*isCreate=*/true);
PublishCreate(MGPipeKind::Renderbuffer, handle, entry, healDesc);
accepted = ApplyRespecify(desc);
accepted = ApplyRespecify(desc, nullptr);
}
}
NoteRespecified(entry, desc, accepted);
@@ -821,11 +990,16 @@ namespace MobileGL::MG_Pipe {
// Records the applier REFUSED. The dirty flag survives one of these, which is the whole
// of D-D5 step 1 - so a case that wants to prove the flag survived asserts on this.
Uint64 RefusedSubDataCount() const { return m_refusedSubDatas; }
// set_texture_params records the applier refused; the latch survives one of these (m-1).
Uint64 RefusedParamCount() const { return m_refusedParamSets; }
// What create_sampler_state put on the wire on this emitter's behalf, so the csob-blob
// accounting does not under-report 100 bytes per built-in sampler mint. set_texture_params
// itself returns no byte count - it is not emitted from the validate point's payload
// histogram - so this is where the cache's answer lands.
Uint64 SamplerCsoPayloadBytes() const { return m_samplerCsoPayloadBytes; }
// Dead handles that still held an object when resolved: a death path that skipped the
// emitter. 0 on a healthy tree; a case that drives every death path asserts it.
Uint64 DeadResolveCount() const { return m_deadResolves; }
MGPipeHandle BuiltinSamplerOf(MGPipeHandle handle) const {
const SizeT slot = handle.Slot;
if (MGPipeHandleIsNull(handle) || slot >= m_textures.size()) return kMGPipeNullHandle;
@@ -847,7 +1021,9 @@ namespace MobileGL::MG_Pipe {
void ResetCounters() {
m_creates = m_respecifies = m_paramSets = m_subDatas = 0;
m_refusedSubDatas = 0;
m_refusedParamSets = 0;
m_samplerCsoPayloadBytes = 0;
m_deadResolves = 0;
}
// A unit fixture's per-case reset; the library never calls it. See
@@ -915,7 +1091,8 @@ namespace MobileGL::MG_Pipe {
}
static Uint16 MaskOf(const Vector<Entry>& table, MGPipeHandle handle) {
const SizeT slot = handle.Slot;
return slot < table.size() ? table[slot].BindMask : Uint16{0};
if (slot >= table.size() || table[slot].Gen != handle.Gen) return Uint16{0};
return table[slot].BindMask;
}
// A SLOT THE ALLOCATOR HAS HANDED OUT AGAIN CARRIES ITS PREDECESSOR'S ENTRY, and every
// field in it is a lie about the new object (m4). The sticky BindMask is the one that
@@ -924,10 +1101,10 @@ namespace MobileGL::MG_Pipe {
// the dead one's mask and its first descriptor said so. The generation is what
// distinguishes them and the reset is here because AcquireTexture is the one door.
//
// IT IS ALSO THE ONLY MOMENT THIS PACKAGE CAN SEE A TEXTURE DIE. The death helper is
// A's (MGPipeEmitTextureDestroyAndFree) and does not forward to this emitter, so the
// built-in sampler's cache reference is dropped here - bounded by the number of live
// texture slots rather than unbounded, which is the shape ID-17 rule 3 names.
// IT IS THE BELT, NOT THE PATH (final review C-2): the death helper forwards to
// NoteTextureDied, which retires the entry - drain entries, cache reference, latches,
// mask - at the death itself. This stays for a slot whose death this emitter was never
// told about, and drops the same reference if one is still standing.
void RetireIfRecycled(Entry& entry, MGPipeHandle handle) {
if (entry.Gen == handle.Gen) return;
MGPipeSamplerCsoCacheInstance().Release(entry.BuiltinSampler);
@@ -952,13 +1129,43 @@ namespace MobileGL::MG_Pipe {
entry.HasLastDesc = true;
}
static Bool ApplyRespecify(const MGPResourceDesc& desc) {
// `level` is null for the whole resource and a key for exactly one level; every caller
// says which (final review C-1), and RepublishMask's null is deliberate - a mask move
// replaces no storage at all.
static Bool ApplyRespecify(const MGPResourceDesc& desc, const MGPRespecifiedLevel* level) {
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
return MGPipeApplyResourceRespecify(desc, nullptr);
return MGPipeApplyResourceRespecify(desc, nullptr, level);
}
(void)level;
return false;
}
// One respecify with one key, and the refusal self-heal beside it. THE SECOND HALF OF
// THE SELF-HEAL, and the publication latch cannot give it: the latch answers "did a
// create for this handle GO OUT", which stays true after
// MGPipeApplierReleaseObjectRecords has dropped every object record - the scope a
// served context's teardown takes while the frontend objects live on in the share
// group. The applier's REFUSAL is the only signal that says "I hold nothing for this
// handle", and the acceptance return is what makes it visible from here at all. One
// retry, never a loop: a descriptor the applier refuses on its own merits (a target
// that names no resource kind) is refused again and the flags stay set.
Bool RespecifyOnce(ITextureObject& texture, MGPipeHandle handle, Entry& entry, const MGPResourceDesc& desc,
const MGPRespecifiedLevel* key, MGPipeHandle viewOf, MGPipeHandle bufferHandle,
Uint64 bufOffset, Uint64 bufSize) {
Bool accepted = ApplyRespecify(desc, key);
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
if (!accepted) {
const MGPResourceDesc healDesc = MGPipeBuildTextureResourceDesc(
texture, handle, entry.BindMask, /*storageDefined=*/false, viewOf, bufferHandle,
bufOffset, bufSize);
NoteDesc(healDesc, /*isCreate=*/true);
PublishCreate(MGPipeKind::Texture, handle, entry, healDesc);
accepted = ApplyRespecify(desc, key);
}
}
return accepted;
}
static void NoteRespecified(Entry& entry, const MGPResourceDesc& desc, Bool accepted) {
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
if (!accepted) return;
@@ -982,7 +1189,10 @@ namespace MobileGL::MG_Pipe {
desc.ImageBindableHint = (entry.BindMask & kMGPipeBindShaderImage) != 0 ? 1 : 0;
if (std::memcmp(&entry.LastDesc, &desc, sizeof(desc)) == 0) return;
NoteDesc(desc, /*isCreate=*/false);
NoteRespecified(entry, desc, ApplyRespecify(desc));
// A NULL LEVEL, DELIBERATELY (wire-v3 §5 item 6): a mask move replaces no storage,
// and the applier classifies the identical storage fields as a metadata update
// that drops nothing. A key here would name a level this call did not touch.
NoteRespecified(entry, desc, ApplyRespecify(desc, nullptr));
}
void NoteDesc(const MGPResourceDesc& desc, Bool isCreate) {
@@ -1113,7 +1323,9 @@ namespace MobileGL::MG_Pipe {
Uint64 m_paramSets = 0;
Uint64 m_subDatas = 0;
Uint64 m_refusedSubDatas = 0;
Uint64 m_refusedParamSets = 0;
Uint64 m_samplerCsoPayloadBytes = 0;
mutable Uint64 m_deadResolves = 0;
};
inline MGPipeTextureEmitter& MGPipeTextureEmitterInstance() {
@@ -1148,7 +1360,10 @@ namespace MobileGL::MG_Pipe {
// keeping the inline definition here would not have compiled at all;
// * step 1 of the death order is inside MGPipeEmitTextureDestroyAndFree /
// ...RenderbufferDestroyAndFree, which read MGPipeHandleIsPublished and emit the
// resource_destroy themselves, so the destructors call ONE helper and not two;
// resource_destroy themselves, so the destructors call ONE helper and not two - and
// since the final review's C-2 the helper then forwards to NoteTextureDied /
// NoteRenderbufferDied above, so no ITextureObject* survives its object in this table
// and no dead level survives on the drain list;
// * the publication latch is PipeFill.cpp's {kind, slot, gen} table, written by
// PublishCreate above and read by those helpers.
//
@@ -55,6 +55,7 @@ add_executable(MobileGLIntegrationTest
Harness/PipeSlotPeek.cpp
Harness/PipeApplyPeek.cpp
Harness/P4aSeamPeek.cpp
Harness/P4aFinalFixPeek.cpp
Scenarios/OrientationScenario.cpp
Scenarios/CrossFrameBufferScenario.cpp
Scenarios/ResidentIndexScenario.cpp
@@ -139,6 +140,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/TextureUploadShapeScenario.cpp
Scenarios/ObjectSubsystemControlScenario.cpp
Scenarios/P4aSeamAuditScenario.cpp
Scenarios/P4aFinalFixScenario.cpp
)
target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -822,6 +824,36 @@ gtest_discover_tests(MobileGLIntegrationTest
ENVIRONMENT "${MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT}"
)
# P4a final review C-2: the dirty-then-delete case, with the allocator SCRIBBLING every freed
# block. The defect this pins was a client emitter resolving a dead-but-not-recycled texture
# handle to the freed ITextureObject* and calling a virtual on it from the next verb's drain;
# whether that reads the object's ghost or faults depends on what the allocator did with the
# block, so the ambient registrations above run the case as the application would see it and
# these two run it with MALLOC_PERTURB_ set, where a resolved-but-dead pointer faults rather
# than passes. Both backends: the death path is backend-neutral by ruling (ID-8).
mgl_itest_join_environment(MGL_ITEST_GLES_MALLOC_PERTURB_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MALLOC_PERTURB_=165" ${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_VULKAN_MALLOC_PERTURB_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MALLOC_PERTURB_=165" ${MGL_ITEST_VULKAN_ENV})
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.MallocPerturb."
TEST_FILTER "P4aFinalFixScenario.ADirtyTextureDeletedBeforeAnyVerbIsWalkedByTheNextDrain"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_MALLOC_PERTURB_ENVIRONMENT}"
)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.MallocPerturb."
TEST_FILTER "P4aFinalFixScenario.ADirtyTextureDeletedBeforeAnyVerbIsWalkedByTheNextDrain"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS integration-gpu
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_VULKAN_MALLOC_PERTURB_ENVIRONMENT}"
)
# AsyncCompileScenario, with asynchronous compilation PINNED ON per backend.
#
# Not a duplicate of what the two ambient registrations already run: they run whatever
@@ -0,0 +1,71 @@
// MobileGL - MobileGL/MG_IntegrationTest/Harness/P4aFinalFixPeek.cpp
// Copyright (c) 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
#include "P4aFinalFixPeek.h"
#if !defined(__ANDROID__)
#include <MG_Pipe/MGPipe.h>
#if MOBILEGL_PIPE_PUSH
#include <MG_Pipe/MGPipeTypes.h>
#include <MG_Pipe/PipeApply.h>
#include <MG_Util/Metrics/PipeStats.h>
#define MGITEST_P4A_FINALFIX_PEEK_LIVE 1
#endif
#endif
namespace MGITest {
#if defined(MGITEST_P4A_FINALFIX_PEEK_LIVE)
namespace {
namespace MGP = MobileGL::MG_Pipe;
} // namespace
bool PeekPipeTextureResourceRecord(unsigned glTextureName, PipeTextureResourceRecordPeek* out) {
if (out == nullptr) return false;
const MGP::MGPipeApplierState& applier = MGP::MGPipeApplier();
// Slot 0 is the reserved null slot; the walk is the same shape PipeApplyPeek.cpp's
// params reading takes. A GL name is never an identity on the wire, which is exactly
// why it is the right key for a harness that starts from the application's view.
for (MobileGL::SizeT slot = 1; slot < applier.TextureResources.size(); ++slot) {
const MGP::MGPipeResourceRecord& record = applier.TextureResources[slot];
if (!record.Live) continue;
if (record.Desc.GlNameForDiag != static_cast<MobileGL::Uint32>(glTextureName)) continue;
out->Slot = static_cast<unsigned>(slot);
out->Gen = static_cast<unsigned>(record.Gen);
out->Serial = static_cast<unsigned long long>(record.Serial);
out->BindMask = static_cast<unsigned>(record.Desc.BindMask);
out->ImageBindableHint = static_cast<unsigned>(record.Desc.ImageBindableHint);
out->Levels = static_cast<unsigned>(record.Desc.Levels);
out->PendingUploads = static_cast<unsigned>(record.PendingUploads.size());
return true;
}
return false;
}
bool PeekPipeStatsTextureRemintPulls(unsigned long long* out) {
if (out == nullptr) return false;
namespace Stats = MobileGL::MG_Util::PipeStats;
if (!Stats::Enabled()) Stats::SetEnabledForTesting(true);
*out = static_cast<unsigned long long>(Stats::TotalCalls(Stats::CallClass::TextureRemintPulls));
return true;
}
bool PeekPipeStatsTextureUploadEmissions(unsigned long long* out) {
if (out == nullptr) return false;
namespace Stats = MobileGL::MG_Util::PipeStats;
if (!Stats::Enabled()) Stats::SetEnabledForTesting(true);
*out = static_cast<unsigned long long>(Stats::TotalCalls(Stats::CallClass::TextureUploadEmissions));
return true;
}
#else
bool PeekPipeTextureResourceRecord(unsigned, PipeTextureResourceRecordPeek*) { return false; }
bool PeekPipeStatsTextureRemintPulls(unsigned long long*) { return false; }
bool PeekPipeStatsTextureUploadEmissions(unsigned long long*) { return false; }
#endif
} // namespace MGITest
@@ -0,0 +1,41 @@
// MobileGL - MobileGL/MG_IntegrationTest/Harness/P4aFinalFixPeek.h
// Copyright (c) 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 white-box readings P4aFinalFixScenario.cpp takes, in a translation unit of their own for
// P4aSeamPeek.h's reason: a scenario TU includes the GL prototype headers and cannot include
// MG_Pipe/PipeApply.h or the Espryt managers beside them, and PipeApplyPeek.cpp is the gates
// package's file. Every entry point answers false where the reading cannot be taken (a pull
// build, Android, or an applier that holds no record for the name), and a false teaches the
// caller nothing - the case declines that half by name and keeps its public-GL verdict.
#pragma once
namespace MGITest {
// The applier's resource record for a texture, found by its GL name (GlNameForDiag - a
// diagnostics-only field, which is exactly what a test harness is).
struct PipeTextureResourceRecordPeek {
unsigned Slot;
unsigned Gen;
unsigned long long Serial;
unsigned BindMask;
unsigned ImageBindableHint;
unsigned Levels;
unsigned PendingUploads;
};
bool PeekPipeTextureResourceRecord(unsigned glTextureName, PipeTextureResourceRecordPeek* out);
// The process-wide texture-remint pull count (PipeStats "tex-remint-pulls", `trp=` on the
// summary line; ROADMAP open question 2). Arms the PipeStats counters for this process on
// the first call, which is what lets a case read the number without a stats-enabled lane.
bool PeekPipeStatsTextureRemintPulls(unsigned long long* out);
// Espryt's count of texture uploads it actually issued (PipeStats "tex-upload-emissions"):
// what tells a CONSUMED pending upload apart from a DROPPED one, since the record's set is
// empty either way. Arms the counters the same way.
bool PeekPipeStatsTextureUploadEmissions(unsigned long long* out);
} // namespace MGITest
@@ -0,0 +1,658 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/P4aFinalFixScenario.cpp
// Copyright (c) 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
//
// Scenario - THE THREE FINDINGS OF THE P4a FINAL WHOLE-DIFF REVIEW (final-review-v1.md C-1, C-2,
// M-A), each pinned by the public-GL sequence that was red on the tree the review read and is
// green with its fix. Every sequence here is legal GL and none of the 80-odd scenarios before
// this file drove it, which is how two criticals shipped through a green gate.
//
// C-1 The client never passed the applier the LEVEL a respecify redefines, so every per-level
// glTexImage*D / glGenerateMipmap grow took the applier's whole-resource arm and dropped
// EVERY pending upload of the texture - including a level the applier had already
// accepted and whose client-side dirty flag was therefore already clear (D-D5 step 1).
// Nobody owed those texels any more. The window is "accepted but not yet consumed":
// a verb the texture is not reached by (a draw with another texture) drains the level
// into the applier, Espryt does not sync the texture, and the next level definition eats
// the entry. Two hazard cases (a level-1 definition, a glGenerateMipmap) read a black
// level 0 on the handle arm; the three controls beside them (no verb between, level 0
// consumed first, an immediate generate) are red on every arm, which is what pins the
// window rather than the mip path.
// C-2 A dead-but-not-recycled texture handle still resolved to the freed ITextureObject*
// inside the client's drain: the death helper freed the slot without telling the emitter,
// the drain list kept the level, and the next verb's drain called virtual
// GetStorageType() on freed memory - `glTexImage2D; glDeleteTextures; <any verb>` was a
// SIGABRT ("pure virtual method called") at the shipping default mask. The same
// delete-then-use shape is driven for every kind P4a mints (renderbuffer, sampler object,
// program, framebuffer) and for a slot recycled straight after the death (ABA), on both
// backends: the death path is backend-neutral by ruling (ID-8) and the DirectVulkan lane
// must see it too.
// M-A Nothing produced kMGPipeBindSampler / kMGPipeBindShaderImage, so ImageBindableHint was
// dead: the applier never saw a texture become image-bound, the metadata respecify
// (ID-18 M4) had no live trigger, and the remint pull the hint exists to prevent was
// neither prevented nor counted. The case here reads the applier's record around a
// glBindImageTexture: the hint arrives as a metadata update that keeps the pending upload
// standing beside it, and the picture after the transition is the texels that upload
// carried.
//
// A WHITE-BOX READING THAT CANNOT BE TAKEN IS DECLINED BY NAME AND THE CASE CONTINUES with its
// public-GL half (P4aSeamAuditScenario.cpp's shape): a pull build or a backend with no P4a
// consumer holds no record to read, and skipping the whole case there would delete the verdict
// those lanes carry. The C-1 and M-A cases assert their pictures on DirectGLES only - Espryt is
// the one consumer of the texture records this phase wires, so on any other backend the handle
// arm is inert by design and the picture proves nothing about it.
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/P4aFinalFixPeek.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kInset = 2;
constexpr const char* kVS = R"(#version 330 core
in vec2 aPos;
out vec2 vUv;
void main() {
vUv = aPos * 0.5 + 0.5;
gl_Position = vec4(aPos, 0.0, 1.0);
}
)";
constexpr const char* kFS = R"(#version 330 core
in vec2 vUv;
uniform sampler2D uTex;
out vec4 oColor;
void main() { oColor = texture(uTex, vUv); }
)";
struct Vertex {
float x, y;
};
class P4aFinalFixScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
std::string error;
m_program = CompileProgram(kVS, kFS, &error);
ASSERT_NE(m_program, 0u) << error;
static const Vertex quad[6] = {{-1.0f, -1.0f}, {1.0f, -1.0f}, {1.0f, 1.0f},
{-1.0f, -1.0f}, {1.0f, 1.0f}, {-1.0f, 1.0f}};
glGenBuffers(1, &m_quadBuffer);
glBindBuffer(GL_ARRAY_BUFFER, m_quadBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);
glBindVertexArray(0);
glDisable(GL_BLEND);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
// The "other" texture: a complete, single-level white texture, so a draw that
// samples it is a verb the texture under test is not reached by.
m_other = MakeLevel0(255, 255, 255, /*maxLevel=*/0);
while (glGetError() != GL_NO_ERROR) {
}
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (m_other != 0) glDeleteTextures(1, &m_other);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_quadBuffer != 0) glDeleteBuffers(1, &m_quadBuffer);
if (m_program != 0) glDeleteProgram(m_program);
while (glGetError() != GL_NO_ERROR) {
}
}
// The C-1 and M-A pictures are about Espryt's consumption of the texture records;
// Magma registers no consumer for the P4a families (c0f), so the handle arm is inert
// there by design and a green picture proves nothing about the finding. Marks the
// case skipped; the caller tests IsSkipped() and returns.
void SkipUnlessEspryt(const char* what) {
if (Gl().BackendName() == "DirectGLES") return;
GTEST_SKIP() << what << " is consumed by DirectGLES only; backend is " << Gl().BackendName();
}
static std::vector<std::uint8_t> Solid(int size, std::uint8_t r, std::uint8_t g, std::uint8_t b) {
std::vector<std::uint8_t> texels(static_cast<std::size_t>(size) * size * 4);
for (std::size_t i = 0; i < texels.size(); i += 4) {
texels[i] = r;
texels[i + 1] = g;
texels[i + 2] = b;
texels[i + 3] = 255;
}
return texels;
}
// A 4x4 level 0 of one colour, NEAREST_MIPMAP_NEAREST with the level range clamped
// to `maxLevel`, so a single-level texture is complete and a chain is complete once
// its levels exist.
static GLuint MakeLevel0(std::uint8_t r, std::uint8_t g, std::uint8_t b, int maxLevel, int size = 4) {
const std::vector<std::uint8_t> texels = Solid(size, r, g, b);
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, size, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, maxLevel);
glBindTexture(GL_TEXTURE_2D, 0);
return texture;
}
static void DefineLevel1(GLuint texture, std::uint8_t r, std::uint8_t g, std::uint8_t b) {
const std::vector<std::uint8_t> texels = Solid(2, r, g, b);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
glBindTexture(GL_TEXTURE_2D, 0);
}
// A full-viewport draw sampling `texture` on unit 0 through `program` (the fixture's
// by default). The viewport is far larger than the 4x4 base level, so this is
// MAGNIFICATION and reads LEVEL 0 whatever the chain holds above it.
Image DrawSampled(GLuint texture, GLuint program = 0) {
if (program == 0) program = m_program;
BindDefaultFramebuffer();
glViewport(0, 0, Gl().Width(), Gl().Height());
glUseProgram(program);
glUniform1i(glGetUniformLocation(program, "uTex"), 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLES, 0, 6);
Image image = ReadPixels(Gl().Width(), Gl().Height());
glBindTexture(GL_TEXTURE_2D, 0);
glBindVertexArray(0);
Gl().EndFrame();
return image;
}
::testing::AssertionResult Mostly(const Image& image, const char* color, const std::string& when) {
return RegionIsMostly(image, kInset, image.Width() - kInset, kInset, image.Height() - kInset, color,
0.0, when);
}
void Report(const char* caseName, const Image& image) {
const char* mask = std::getenv("MOBILEGL_PIPE_PUSH");
const int cx = image.Width() / 2;
const int cy = image.Height() / 2;
std::cout << "[ P4aFinalFix ] case=" << caseName << " backend=" << Gl().BackendName()
<< " MOBILEGL_PIPE_PUSH=" << (mask ? mask : "(unset)") << " centre=" << image.At(cx, cy)
<< " (" << image.ColorName(cx, cy) << ")" << std::endl;
}
// The white-box gate of the M-A case: true when the applier holds a record for the
// texture in this process. Prints the decline.
bool RecordIsReadable(unsigned glTextureName, const char* what, PipeTextureResourceRecordPeek* out) {
if (PeekPipeTextureResourceRecord(glTextureName, out)) return true;
std::cout << "[ P4aFinalFix ] white-box reading DECLINED for " << what
<< ": the applier holds no record for texture " << glTextureName
<< " (a pull build, or a backend with no P4a consumer); the public-GL half of "
"the case still runs"
<< std::endl;
RecordProperty("p4a_finalfix_white_box", "declined");
return false;
}
GLuint m_program = 0;
GLuint m_vao = 0;
GLuint m_quadBuffer = 0;
GLuint m_other = 0;
};
// ======================================================================================
// C-1: a per-level definition around a verb the texture is not reached by
// ======================================================================================
// THE HAZARD. L0's upload is accepted at the unrelated draw's validate point (the client
// clears its flag), Espryt never syncs T there (it is bound nowhere), then the level-1
// definition respecifies the resource. Before the fix that respecify carried no level and
// the applier dropped every pending upload; level 0 was allocated undefined.
TEST_F(P4aFinalFixScenario, PerLevelDefinitionAcrossAnUnrelatedDraw) {
if (!Ready()) return;
SkipUnlessEspryt("C-1's per-level respecify");
if (IsSkipped()) return;
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/0);
const Image unrelated = DrawSampled(m_other);
EXPECT_TRUE(Mostly(unrelated, "white", "the unrelated draw"));
DefineLevel1(texture, 255, 0, 0);
const Image image = DrawSampled(texture);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
Report("PerLevelDefinitionAcrossAnUnrelatedDraw", image);
EXPECT_TRUE(Mostly(image, "red",
"level 0 after a level-1 definition that followed a draw the texture was not "
"reached by - its accepted-but-unconsumed upload was dropped by the whole-"
"resource arm"));
GLuint cleanup = texture;
glDeleteTextures(1, &cleanup);
}
// CONTROL: both levels defined before any verb; both are pending at the first sync.
TEST_F(P4aFinalFixScenario, ConsecutiveDefinitionsNoVerbBetween) {
if (!Ready()) return;
SkipUnlessEspryt("C-1's per-level respecify");
if (IsSkipped()) return;
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/0);
DefineLevel1(texture, 255, 0, 0);
const Image image = DrawSampled(texture);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
Report("ConsecutiveDefinitionsNoVerbBetween", image);
EXPECT_TRUE(Mostly(image, "red", "level 0 with both levels defined back to back"));
GLuint cleanup = texture;
glDeleteTextures(1, &cleanup);
}
// CONTROL: level 0 is consumed by Espryt (T is sampled) before level 1 is defined.
TEST_F(P4aFinalFixScenario, LevelZeroConsumedBeforeLevelOne) {
if (!Ready()) return;
SkipUnlessEspryt("C-1's per-level respecify");
if (IsSkipped()) return;
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/0);
const Image first = DrawSampled(texture);
EXPECT_TRUE(Mostly(first, "red", "level 0 alone"));
DefineLevel1(texture, 255, 0, 0);
const Image image = DrawSampled(texture);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
Report("LevelZeroConsumedBeforeLevelOne", image);
EXPECT_TRUE(Mostly(image, "red", "level 0 after level 1 was added to a synced texture"));
GLuint cleanup = texture;
glDeleteTextures(1, &cleanup);
}
// THE HAZARD, glGenerateMipmap flavour: the frontend grows the level chain (one
// AllocateStorage -> respecify per level) BEFORE the backend generate runs, with level 0
// accepted-but-unconsumed. The driver then built the chain from an undefined level 0.
TEST_F(P4aFinalFixScenario, GenerateMipmapAcrossAnUnrelatedDraw) {
if (!Ready()) return;
SkipUnlessEspryt("C-1's per-level respecify");
if (IsSkipped()) return;
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/1000);
const Image unrelated = DrawSampled(m_other);
EXPECT_TRUE(Mostly(unrelated, "white", "the unrelated draw"));
glBindTexture(GL_TEXTURE_2D, texture);
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
const Image image = DrawSampled(texture);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
Report("GenerateMipmapAcrossAnUnrelatedDraw", image);
EXPECT_TRUE(Mostly(image, "red",
"level 0 after a glGenerateMipmap that followed a draw the texture was not "
"reached by"));
GLuint cleanup = texture;
glDeleteTextures(1, &cleanup);
}
// CONTROL for the generate: no verb between the upload and the generate.
TEST_F(P4aFinalFixScenario, GenerateMipmapImmediately) {
if (!Ready()) return;
SkipUnlessEspryt("C-1's per-level respecify");
if (IsSkipped()) return;
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/1000);
glBindTexture(GL_TEXTURE_2D, texture);
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
const Image image = DrawSampled(texture);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
Report("GenerateMipmapImmediately", image);
EXPECT_TRUE(Mostly(image, "red", "level 0 after an immediate glGenerateMipmap"));
GLuint cleanup = texture;
glDeleteTextures(1, &cleanup);
}
// ======================================================================================
// C-2: delete-then-use, for every kind P4a mints, on both backends
// ======================================================================================
// A level goes dirty, the texture dies before any verb, and the next verb's drain walks
// the entry. Before the fix the emitter resolved the dead handle to the freed object and
// the drain called a virtual on it: SIGABRT in the first round. Eight rounds, and the
// lane registered with MALLOC_PERTURB_ scribbles every freed block so a resolved-but-
// dead pointer faults rather than reads the object's ghost.
TEST_F(P4aFinalFixScenario, ADirtyTextureDeletedBeforeAnyVerbIsWalkedByTheNextDrain) {
if (!Ready()) return;
for (int round = 0; round < 8; ++round) {
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
const std::vector<std::uint8_t> texels = Solid(4, 255, 0, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
glBindTexture(GL_TEXTURE_2D, 0);
glDeleteTextures(1, &texture); // the last reference: the frontend object dies here
// Something else is allocated between the death and the drain, so the freed
// storage is not simply re-handed to the next object.
std::vector<std::uint8_t> churn(4096 + round * 1024, static_cast<std::uint8_t>(round));
(void)churn;
const Image image = DrawSampled(m_other); // the validate point: the drain runs here
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
if (round == 0) Report("ADirtyTextureDeletedBeforeAnyVerbIsWalkedByTheNextDrain", image);
EXPECT_TRUE(Mostly(image, "white", "the draw after a dirty texture died"));
}
}
// ABA: the slot the dead texture held is handed straight to the next texture (the free
// list is LIFO). The new texture's picture must be its own, and the dead one's drain
// entry must not be replayed onto it.
TEST_F(P4aFinalFixScenario, ATextureRecycledOntoTheDeadSlotDoesNotInheritItsDrainEntry) {
if (!Ready()) return;
{
GLuint dead = 0;
glGenTextures(1, &dead);
glBindTexture(GL_TEXTURE_2D, dead);
const std::vector<std::uint8_t> texels = Solid(4, 255, 0, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
glBindTexture(GL_TEXTURE_2D, 0);
glDeleteTextures(1, &dead); // dirty, dead, no verb between
}
const GLuint successor = MakeLevel0(0, 0, 255, /*maxLevel=*/0, /*size=*/8);
const Image image = DrawSampled(successor);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
Report("ATextureRecycledOntoTheDeadSlotDoesNotInheritItsDrainEntry", image);
EXPECT_TRUE(Mostly(image, "blue", "the successor of a dead dirty texture on the recycled slot"));
const Image other = DrawSampled(m_other);
EXPECT_TRUE(Mostly(other, "white", "an unrelated draw after the recycled slot was used"));
GLuint cleanup = successor;
glDeleteTextures(1, &cleanup);
}
// A renderbuffer with defined storage, attached, cleared through its framebuffer, then
// both die before the next verb.
TEST_F(P4aFinalFixScenario, ARenderbufferAndItsFramebufferDeletedAfterAClearLeaveTheNextDrawIntact) {
if (!Ready()) return;
GLuint renderbuffer = 0;
glGenRenderbuffers(1, &renderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 8, 8);
GLuint fbo = 0;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
glViewport(0, 0, 8, 8);
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
const Image cleared = ReadPixels(8, 8);
EXPECT_TRUE(RegionIsMostly(cleared, 0, 8, 0, 8, "green", 0.0, "the renderbuffer after the clear"));
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
glDeleteFramebuffers(1, &fbo);
glDeleteRenderbuffers(1, &renderbuffer); // the attachment's last reference went with the FBO
const Image image = DrawSampled(m_other);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
Report("ARenderbufferAndItsFramebufferDeletedAfterAClearLeaveTheNextDrawIntact", image);
EXPECT_TRUE(Mostly(image, "white", "the draw after a renderbuffer and its framebuffer died"));
}
// A sampler object bound to the unit the draw samples through, deleted while bound: GL
// unbinds it from every unit at glDeleteSamplers, and the texture's own parameters apply
// again. Both draws must be the texture's colour.
TEST_F(P4aFinalFixScenario, ASamplerObjectDeletedWhileBoundLeavesTheNextDrawIntact) {
if (!Ready()) return;
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/0);
GLuint sampler = 0;
glGenSamplers(1, &sampler);
glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindSampler(0, sampler);
const Image withSampler = DrawSampled(texture);
EXPECT_TRUE(Mostly(withSampler, "red", "the draw through the bound sampler object"));
glDeleteSamplers(1, &sampler); // bound: unbound by the delete, then dies
const Image image = DrawSampled(texture);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
Report("ASamplerObjectDeletedWhileBoundLeavesTheNextDrawIntact", image);
EXPECT_TRUE(Mostly(image, "red", "the draw after the bound sampler object died"));
glBindSampler(0, 0);
GLuint cleanup = texture;
glDeleteTextures(1, &cleanup);
}
// A second program, in use when it is deleted (GL keeps it alive until it is no longer
// current), then released by a glUseProgram of the fixture's program: it dies there, and
// the draw that follows runs through the survivor.
TEST_F(P4aFinalFixScenario, AProgramDeletedWhileInUseLeavesTheNextDrawIntact) {
if (!Ready()) return;
std::string error;
const GLuint second = CompileProgram(kVS, kFS, &error);
ASSERT_NE(second, 0u) << error;
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/0);
const Image throughSecond = DrawSampled(texture, second);
EXPECT_TRUE(Mostly(throughSecond, "red", "the draw through the second program"));
glDeleteProgram(second); // current: flagged for deletion, still very much alive
const Image stillCurrent = DrawSampled(texture, second);
EXPECT_TRUE(Mostly(stillCurrent, "red", "the draw through a program flagged for deletion"));
const Image image = DrawSampled(texture); // glUseProgram(m_program): the second dies here
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
Report("AProgramDeletedWhileInUseLeavesTheNextDrawIntact", image);
EXPECT_TRUE(Mostly(image, "red", "the draw after the deleted program was released"));
GLuint cleanup = texture;
glDeleteTextures(1, &cleanup);
}
// A framebuffer handed to the server BY NAME (a DSA clear emits a Named record, ID-19(c))
// and deleted before the next verb; its attachment lives on and carries the clear.
TEST_F(P4aFinalFixScenario, AFramebufferDeletedAfterADsaClearLeavesItsAttachmentIntact) {
if (!Ready()) return;
const GLuint texture = MakeLevel0(255, 0, 0, /*maxLevel=*/0);
GLuint fbo = 0;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
glBindFramebuffer(GL_FRAMEBUFFER, 0);
const GLfloat green[4] = {0.0f, 1.0f, 0.0f, 1.0f};
glClearNamedFramebufferfv(fbo, GL_COLOR, 0, green);
glDeleteFramebuffers(1, &fbo); // unbound and named: dies here
const Image image = DrawSampled(texture);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
Report("AFramebufferDeletedAfterADsaClearLeavesItsAttachmentIntact", image);
EXPECT_TRUE(Mostly(image, "green", "the attachment of a framebuffer that died after a DSA clear"));
GLuint cleanup = texture;
glDeleteTextures(1, &cleanup);
}
// ======================================================================================
// M-A: an image bind after the allocation is a metadata respecify with the hint set
// ======================================================================================
// glTexStorage2D (immutable: no later respecify to ride), a red upload consumed by a draw,
// then a blue upload drained by a verb the texture is not reached by (accepted, standing
// in the applier's pending set), then glBindImageTexture. The bind must reach the record
// as a metadata update - ImageBindableHint 1, the pending upload still standing - and the
// draw after it must show the blue that upload carried through the widened carrier the
// hint schedules.
TEST_F(P4aFinalFixScenario, AnImageBindAfterAllocationReachesTheApplierAsAMetadataRespecify) {
if (!Ready()) return;
SkipUnlessEspryt("M-A's image-bindable hint");
if (IsSkipped()) return;
GLint maxImageUnits = 0;
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
while (glGetError() != GL_NO_ERROR) {
}
if (maxImageUnits < 1) {
GTEST_SKIP() << "no image units";
return;
}
// THE NUMBER ROADMAP OPEN QUESTION 2 ASKS FOR: a texture Espryt allocated BEFORE the
// hint reached it is re-minted image-bindable at the bind and its levels replayed
// from the client's shadow - one remint pull, counted. Arming the counter here is
// what makes it readable without a stats-enabled lane.
unsigned long long pullsBefore = 0;
const bool pullsReadable = PeekPipeStatsTextureRemintPulls(&pullsBefore);
GLuint texture = 0;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4);
const std::vector<std::uint8_t> red = Solid(4, 255, 0, 0);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, red.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindTexture(GL_TEXTURE_2D, 0);
const Image before = DrawSampled(texture); // allocated and consumed, NOT image-bindable
EXPECT_TRUE(Mostly(before, "red", "the immutable texture before the image bind"));
PipeTextureResourceRecordPeek record{};
const bool readable = RecordIsReadable(texture, "M-A's image-bindable hint", &record);
if (readable) {
EXPECT_EQ(record.ImageBindableHint, 0u) << "nothing has image-bound this texture yet";
EXPECT_EQ(record.PendingUploads, 0u) << "the red upload was consumed by the draw";
}
// A blue upload, drained by a verb that does not reach T: accepted, unconsumed.
const std::vector<std::uint8_t> blue = Solid(4, 0, 0, 255);
glBindTexture(GL_TEXTURE_2D, texture);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
glBindTexture(GL_TEXTURE_2D, 0);
const Image unrelated = DrawSampled(m_other);
EXPECT_TRUE(Mostly(unrelated, "white", "the unrelated draw"));
if (readable) {
ASSERT_TRUE(PeekPipeTextureResourceRecord(texture, &record));
EXPECT_EQ(record.PendingUploads, 1u) << "the blue upload was not drained into the applier";
}
const unsigned long long serialBeforeBind = record.Serial;
unsigned long long uploadsBeforeBind = 0;
const bool uploadsReadable = PeekPipeStatsTextureUploadEmissions(&uploadsBeforeBind);
// THE TRANSITION. An immutable texture has no storage-defining respecify left, so the
// hint can only arrive as a metadata update (ID-18 M4). Espryt syncs the texture
// eagerly inside glBindImageTexture and the widening re-mints its storage, replaying
// every defined level from the shadow (the remint pull the counter below counts), so
// the standing upload is consumed by that regeneration here and the picture that
// follows is blue whatever the metadata respecify did to the record - the KEPT
// property is proved further down, on a texture no remint stands in front of.
(void)uploadsBeforeBind;
(void)uploadsReadable;
glBindImageTexture(0, texture, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
if (readable) {
ASSERT_TRUE(PeekPipeTextureResourceRecord(texture, &record));
EXPECT_EQ(record.ImageBindableHint, 1u)
<< "glBindImageTexture did not reach the applier's record as ImageBindableHint";
EXPECT_NE(record.BindMask & (1u << 6), 0u) << "kMGPipeBindShaderImage was not produced";
EXPECT_GT(record.Serial, serialBeforeBind) << "the metadata respecify moved no serial";
}
const Image image = DrawSampled(texture);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
Report("AnImageBindAfterAllocationReachesTheApplierAsAMetadataRespecify", image);
EXPECT_TRUE(Mostly(image, "blue", "the texture after the image bind that followed an unconsumed upload"));
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
unsigned long long pullsAfter = 0;
if (pullsReadable && readable && PeekPipeStatsTextureRemintPulls(&pullsAfter)) {
EXPECT_EQ(pullsAfter, pullsBefore + 1)
<< "the re-mint of a texture allocated before its hint was not counted as a remint pull "
"(trp= on the stats line is ROADMAP open question 2's number)";
}
// THE PREVENTION HALF, measured the other way round: a texture whose hint arrives at
// the bind, BEFORE its first sync, is allocated image-bindable up front and pulls
// nothing - the counter does not move.
GLuint early = 0;
glGenTextures(1, &early);
glBindTexture(GL_TEXTURE_2D, early);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, red.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindTexture(GL_TEXTURE_2D, 0);
glBindImageTexture(0, early, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8); // before any sync
const Image earlyImage = DrawSampled(early);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
EXPECT_TRUE(Mostly(earlyImage, "red", "a texture image-bound before its first sync"));
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
unsigned long long pullsEarly = 0;
if (pullsReadable && readable && PeekPipeStatsTextureRemintPulls(&pullsEarly)) {
EXPECT_EQ(pullsEarly, pullsAfter)
<< "a texture whose hint preceded its first sync was still re-minted (the prevention "
"half of the hint did not fire)";
}
// THE METADATA RESPECIFY KEEPS A STANDING UPLOAD, end to end and with no remint in the
// way: `early` is image-bindable already, so a NEW sticky bit reaching it - the
// RENDER_TARGET bit a DSA attachment produces at its setter (a Named record, ID-19(c)),
// with no sync of the texture in between - is a pure metadata update. The blue upload
// drained before it must still stand in the record afterwards (or, if a sync did run,
// have been uploaded rather than dropped) and reach the driver at the next draw.
glBindTexture(GL_TEXTURE_2D, early);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
glBindTexture(GL_TEXTURE_2D, 0);
const Image unrelatedAgain = DrawSampled(m_other);
EXPECT_TRUE(Mostly(unrelatedAgain, "white", "the unrelated draw"));
PipeTextureResourceRecordPeek earlyRecord{};
const bool earlyReadable = PeekPipeTextureResourceRecord(early, &earlyRecord);
if (earlyReadable) {
EXPECT_EQ(earlyRecord.PendingUploads, 1u) << "the blue upload was not drained into the applier";
}
const unsigned long long earlySerialBefore = earlyRecord.Serial;
unsigned long long uploadsBeforeAttach = 0;
const bool uploadsCounted = PeekPipeStatsTextureUploadEmissions(&uploadsBeforeAttach);
GLuint namedFbo = 0;
glCreateFramebuffers(1, &namedFbo);
glNamedFramebufferTexture(namedFbo, GL_COLOR_ATTACHMENT0, early, 0);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
if (earlyReadable) {
ASSERT_TRUE(PeekPipeTextureResourceRecord(early, &earlyRecord));
EXPECT_NE(earlyRecord.BindMask & (1u << 7), 0u)
<< "the DSA attachment did not produce kMGPipeBindRenderTarget";
EXPECT_GT(earlyRecord.Serial, earlySerialBefore) << "the mask move reached the record as no respecify";
unsigned long long uploadsAfterAttach = 0;
if (earlyRecord.PendingUploads == 0 && uploadsCounted &&
PeekPipeStatsTextureUploadEmissions(&uploadsAfterAttach)) {
EXPECT_GT(uploadsAfterAttach, uploadsBeforeAttach)
<< "the standing upload vanished from the record without Espryt uploading anything: "
"the metadata respecify dropped it";
} else {
EXPECT_EQ(earlyRecord.PendingUploads, 1u)
<< "the metadata respecify dropped the pending upload standing beside it";
}
}
const Image earlyAfter = DrawSampled(early);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
EXPECT_TRUE(Mostly(earlyAfter, "blue", "the upload that stood across a metadata respecify"));
glDeleteFramebuffers(1, &namedFbo);
GLuint cleanup = texture;
glDeleteTextures(1, &cleanup);
GLuint cleanupEarly = early;
glDeleteTextures(1, &cleanupEarly);
}
} // namespace
} // namespace MGITest
+87 -9
View File
@@ -300,7 +300,30 @@ namespace MobileGL::MG_Pipe {
Uint32 StorageFlags; // glBufferStorage flags
Uint8 HasDefinedContent; // false after a NULL-data respecify - STORAGE-DEFINING
Uint8 ImageBindableHint; // client-side everImageBound; pre-emptive allocation
Uint16 Pad0;
// THE SCOPE OF A RESPECIFY (P5 R-13.3b, CONTRACT-P5.md table 1 row 19b). Zero means
// "this respecify redefines the WHOLE resource"; non-zero means it redefines exactly
// the (RespecifiedUploadTarget, RespecifiedLevel) pair below and nothing else.
//
// It exists because the applier ALREADY takes that scope - as a trailing
// const MGPRespecifiedLevel* (PipeApply.h:792-795) - and MGPResourceDesc could not
// express it, so it was the second of resource_respecify's two companions with no wire
// carrier. The difference is not cosmetic: a null scope drops EVERY pending upload,
// because every level's coordinate system has just been replaced, while a per-level
// one drops only that key. Crossing without the scope would make every mutable
// per-level glTexImage*D on the far side look like a whole-resource redefinition and
// silently eat the texels of every other level - exactly the loss the server-side
// pending-upload set exists to prevent.
//
// READ IT THROUGH THE THREE HELPERS BELOW, never by touching the fields: the
// presence byte and the pair are one value in three pieces, and an open-coded reader
// that forgets the presence byte reads level 0 of upload target 0 as a real scope.
//
// NOT STORAGE-DEFINING, and not metadata either: it does not describe the resource at
// all, it describes what this CALL replaces. MGPipeResourceRespecifyNeedsAck and the
// storage-defining field set below are unaffected by it, which is why it is not named
// in either.
Uint8 HasRespecifiedLevel;
Uint8 Pad0;
// ImageBindableHint and BindMask above are the two METADATA fields the rule exists
// for: a respecify that moves only them - every storage-defining field equal to the
// stored descriptor - is a metadata update, with no reallocation ack and no
@@ -312,12 +335,62 @@ namespace MobileGL::MG_Pipe {
// of a content hash (section 4.2.1). Widened from the plan's two bytes, which
// cannot hold one.
Uint32 GlNameForDiag;
Uint32 Pad1;
// The pair HasRespecifiedLevel above gates. Byte for byte the two members of
// MGPRespecifiedLevel (PipeApply.h:792-795), in that order, so the applier's existing
// struct and this carrier are one layout - but spelled as two Uint16s rather than as
// that type, because PipeApply.h includes THIS header and not the other way round.
Uint16 RespecifiedUploadTarget;
Uint16 RespecifiedLevel;
MGPipeHandle ViewOf; // storage owner for a texture view
MGPipeHandle BufferForTexBuffer; // texture-buffer backing store
Uint64 BufOffset, BufSize; // kWholeBuffer == ~0, resolved live
};
MGP_ASSERT_POD(MGPResourceDesc, 88);
// The scope fields went into the two existing pads, so the descriptor did not grow and this
// number did not move. That was the deciding argument against giving MGPResourceDesc an
// MGPBlobRef for the OTHER uncarried companion (initialBytes): a blobref is 24 bytes, the
// pads are 6, and it would have taken the struct to 112. The scope needs 5 and fits.
static_assert(offsetof(MGPResourceDesc, RespecifiedUploadTarget) + sizeof(Uint16) ==
offsetof(MGPResourceDesc, RespecifiedLevel),
"the respecify scope pair must stay adjacent and in MGPRespecifiedLevel's order");
// ---- the only supported reads of the respecify scope --------------------------------
//
// Three fields, one value. Open-coding them is how a reader that forgets the presence byte
// turns "whole resource" into "upload target 0, level 0" - a real scope, and the wrong one.
// True when this respecify replaces the whole resource, which is every glBufferData, every
// glBufferStorage, every glTexStorage* and every texture view. The applier drops every
// pending upload for it.
inline constexpr Bool MGPipeRespecifyIsWholeResource(const MGPResourceDesc& desc) {
return desc.HasRespecifiedLevel == 0;
}
// The single (uploadTarget, level) a per-level respecify replaces. Reading either half of a
// whole-resource descriptor is a caller error; both answer 0 so that a misuse is at least
// deterministic rather than whatever the pad happened to hold.
inline constexpr Uint16 MGPipeRespecifiedUploadTargetOf(const MGPResourceDesc& desc) {
return MGPipeRespecifyIsWholeResource(desc) ? Uint16(0) : desc.RespecifiedUploadTarget;
}
inline constexpr Uint16 MGPipeRespecifiedLevelOf(const MGPResourceDesc& desc) {
return MGPipeRespecifyIsWholeResource(desc) ? Uint16(0) : desc.RespecifiedLevel;
}
// The two writers. A producer sets the scope with one call so the presence byte cannot be
// left behind, and clears it with the other; a descriptor built by value-initialization is
// already whole-resource, which is the safe default and the only one P5 produces.
inline constexpr void MGPipeSetRespecifiedLevel(MGPResourceDesc& desc, Uint16 uploadTarget,
Uint16 level) {
desc.HasRespecifiedLevel = 1;
desc.RespecifiedUploadTarget = uploadTarget;
desc.RespecifiedLevel = level;
}
inline constexpr void MGPipeClearRespecifiedLevel(MGPResourceDesc& desc) {
desc.HasRespecifiedLevel = 0;
desc.RespecifiedUploadTarget = 0;
desc.RespecifiedLevel = 0;
}
inline constexpr Uint64 kMGPipeWholeBuffer = ~0ull;
struct MGPFenceWait {
@@ -1100,14 +1173,19 @@ namespace MobileGL::MG_Pipe {
// answers the per-record question, but a metadata update allocates nothing, so a
// record it classifies as metadata is not acked even when that predicate says the
// call may require one.
// - NO PendingUploads clear - not the whole vector, and not the redefined level either.
// This REFINES the level-scoped clear: identical storage fields clear NOTHING. (The
// - NO PendingUploads clear when the call names NO level. This REFINES the whole-resource
// clear: identical storage fields with a null MGPRespecifiedLevel clear NOTHING. (The
// level-scoped rule exists because clearing the whole vector on a level-1 definition
// silently dropped level 0's accepted texels; a metadata update must drop neither.)
// - The stored descriptor's BindMask and ImageBindableHint ARE updated - BindMask is
// sticky and therefore ORed, never replaced - and the twin re-derives its storage
// flags from the new mask on its next sync, recreating backend storage only where the
// backend actually needs it. The record itself is not a request to recreate.
// silently dropped level 0's accepted texels; a metadata update must drop neither.) A
// call that NAMES a level is that level's redefinition whatever the descriptor says -
// a non-base level's extent is not a descriptor field - and drops exactly that level
// (P4a final review C-1); the client's mask republish passes null on purpose.
// - The stored descriptor's BindMask and ImageBindableHint ARE updated: the applier
// replaces the descriptor WHOLE with the one the client sent (PipeApply.cpp), and the
// mask in it is the CLIENT's sticky OR (TextureEmit.h's entry, never cleared), so the
// replacement can never lose a bit the record once carried. The twin re-derives its
// storage flags from the new mask on its next sync, recreating backend storage only
// where the backend actually needs it. The record itself is not a request to recreate.
//
// THE STORAGE-DEFINING FIELD SET, named here so that neither side has to guess and a
// later field cannot join it by silence. It is every MGPResourceDesc member except the
+53 -18
View File
@@ -852,6 +852,30 @@ namespace MobileGL::MG_Pipe {
void PinNoLiveHostWrites(const MGPipeResourceRecord&, MGPipeHandle, const char*) {}
#endif
#if MOBILEGL_PIPE_VERIFY
// P5's pin, the same shape and for the same reason. The respecify SCOPE now has a wire
// carrier (MGPResourceDesc's HasRespecifiedLevel + the pair) and no producer: every
// descriptor P5 builds is whole-resource, and the per-level scope still arrives the old
// way, as the trailing MGPRespecifiedLevel* this function does not look at.
//
// The two must not disagree, and when a later package wires the carrier it will set the
// fields at a call site that also still passes the pointer - so the first thing that can
// go wrong is exactly one of the two moving. A verify build refuses to let that arrive
// unannounced, because a descriptor that says "whole resource" while the pointer says
// "level 1" drops every other level's pending upload with nothing saying so.
void PinWholeResourceRespecifyScope(const MGPResourceDesc& desc, MGPipeHandle res, const char* call) {
if (MGPipeRespecifyIsWholeResource(desc)) return;
MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("PipeRespecifyScope")
" %s {slot=%u, gen=%u}: the descriptor carries a per-level respecify "
"scope (target=%u, level=%u), and no path in this phase may set one",
call, res.Slot, res.Gen,
static_cast<unsigned>(MGPipeRespecifiedUploadTargetOf(desc)),
static_cast<unsigned>(MGPipeRespecifiedLevelOf(desc)));
}
#else
void PinWholeResourceRespecifyScope(const MGPResourceDesc&, MGPipeHandle, const char*) {}
#endif
// The one gate every content-carrying buffer write goes through. resource_subdata and
// buffer_subdata_resident differ only in which backend hook takes the bytes and in the
// fact that one of them is allowed to be absent, so a second copy of this arithmetic
@@ -1628,6 +1652,7 @@ namespace MobileGL::MG_Pipe {
MGPipeResourceRecord* record = ResolveResourceIn(*table, "resource_respecify", desc.Resource);
if (record == nullptr) return false;
PinNoLiveHostWrites(*record, desc.Resource, "resource_respecify");
PinWholeResourceRespecifyScope(desc, desc.Resource, "resource_respecify");
// IS THIS A REDEFINITION AT ALL? Asked BEFORE the descriptor is replaced, because the
// stored one is the only thing there is to compare against (ID-18 M4). See
@@ -1667,23 +1692,28 @@ namespace MobileGL::MG_Pipe {
// the arm this set exists for) -> glTexImage2D(1, data), which under a blanket
// clear destroys level 0's entry before anything ever uploaded it.
//
// - and a METADATA update (ID-18 M4) drops NOTHING, whatever `level` says. It is the
// third arm and it refines the first two rather than contradicting them: the rule
// is "the uploads against the storage this call REPLACES go with it", and a call
// whose storage-defining fields all equal the stored descriptor replaces no
// storage, so no level's coordinate system has moved and every pending box is still
// described in the space it was accumulated in. B re-emits the descriptor when a
// sticky bind bit moves, which can land between a glTexSubImage2D and the sync that
// consumes it; eating those texels there would be C1's bug with a different
// trigger, and just as silent.
// - and a METADATA update (ID-18 M4) with a NULL level drops NOTHING. It refines the
// whole-resource arm rather than contradicting it: the rule is "the uploads against
// the storage this call REPLACES go with it", and a call whose storage-defining
// fields all equal the stored descriptor replaces no storage, so no level's
// coordinate system has moved and every pending box is still described in the
// space it was accumulated in. B re-emits the descriptor when a sticky bind bit
// moves - with a null level, deliberately - which can land between a
// glTexSubImage2D and the sync that consumes it; eating those texels there would be
// C1's bug with a different trigger, and just as silent.
//
// - A NAMED LEVEL IS DROPPED WHETHER OR NOT THE DESCRIPTOR MOVED (P4a final review
// C-1, refining wire's W11 clause). The level pointer is the CALLER's statement that
// it reallocated that level, and the descriptor cannot contradict it: a non-base
// level redefined at a new size moves no descriptor field at all (the descriptor
// carries the base extent and the level count), so "identical storage fields" says
// nothing about that level's coordinate system, and a box kept against the old
// level would be uploaded past the end of the new one. The client's mask republish
// passes null, so this arm can never eat a standing upload on its behalf.
//
// A buffer never has a pending upload at all, so all three arms are inert for P3a's
// half - which is also why a buffer is never classified as metadata-only (below).
if (metadataOnly) {
// nothing to drop, deliberately.
} else if (level == nullptr) {
record->PendingUploads.clear();
} else {
if (level != nullptr) {
// The keys are unique by AccumulatePendingUpload's construction - it looks for the
// pair before it appends - so this erases at most one entry and stops.
for (auto it = record->PendingUploads.begin(); it != record->PendingUploads.end(); ++it) {
@@ -1691,6 +1721,10 @@ namespace MobileGL::MG_Pipe {
record->PendingUploads.erase(it);
break;
}
} else if (metadataOnly) {
// nothing to drop, deliberately.
} else {
record->PendingUploads.clear();
}
// resource_respecify is the catalogue's only kNeedsAck call, and the per-record half
@@ -2470,11 +2504,11 @@ namespace MobileGL::MG_Pipe {
record->Gen = gen;
}
void MGPipeApplySetTextureParams(const MGPTextureParams& params) {
Bool MGPipeApplySetTextureParams(const MGPTextureParams& params) {
// P4a's belt, and FIRST here because this call's first act is a resolution: with no
// consumer no texture create was accepted, so resolving would report the absence as
// RefusedObjectCalls - the counter that means a seam defect - for the designed state.
if (NoP4aConsumer()) return;
if (NoP4aConsumer()) return false;
// ADDRESSED BY RESOURCE AND BY NOTHING ELSE, which is the whole point of the call: a
// texture that is only an FBO attachment, only an image-unit binding or only a
@@ -2483,7 +2517,7 @@ namespace MobileGL::MG_Pipe {
// moment the parameters move, whether or not anything is bound.
MGPipeResourceRecord* record =
ResolveObject(g_applier.TextureResources, "set_texture_params", params.Res);
if (record == nullptr) return;
if (record == nullptr) return false;
// EVERY ITextureObject OWNS A SamplerObject, so the built-in sampler CSO is not
// optional and a null handle is not "no sampler" - it is a record that would have the
@@ -2496,7 +2530,7 @@ namespace MobileGL::MG_Pipe {
" set_texture_params {slot=%u, gen=%u, glName=%u}: the record names no "
"built-in sampler CSO, and every texture object owns one",
params.Res.Slot, params.Res.Gen, record->Desc.GlNameForDiag);
return;
return false;
}
// AND THE CSO IT NAMES IS NOT RESOLVED. The sampler subsystem is its own bit and may be
// clear while the texture bit is set, so a record that names a CSO this applier has not
@@ -2511,6 +2545,7 @@ namespace MobileGL::MG_Pipe {
// bytes are CARRIED, never cleared here: the server ORs them into its own flags and
// clears its own copy, and the client never clears a server flag.
++record->ParamsSerial;
return true;
}
// The three of them, and NO STAGE DIMENSION on any of them: MobileGL's texture-unit space
+13 -1
View File
@@ -784,6 +784,11 @@ namespace MobileGL::MG_Pipe {
// same value the emission of that level put in the record. A per-face respecify therefore
// drops the face it redefines and leaves the other five standing, and a caller that packs
// the pair differently here than it packs it there simply matches nothing.
// A NAMED LEVEL IS DROPPED EVEN WHEN EVERY STORAGE-DEFINING FIELD IS UNCHANGED (P4a final
// review C-1): the pointer is the caller's statement that it reallocated that level, and
// a non-base level's extent is not in the descriptor. Only a NULL level with unchanged
// fields is the metadata update that drops nothing (ID-18 M4); the client's mask republish
// is the one caller of that shape and passes null on purpose.
struct MGPRespecifiedLevel {
Uint16 UploadTarget = 0;
Uint16 Level = 0;
@@ -1008,7 +1013,14 @@ namespace MobileGL::MG_Pipe {
// glCopyImageSubData endpoint carry its parameters at all. params.BuiltinSampler may never
// be the null handle - every ITextureObject owns a sampler object - so a null is
// Fatal{ProtocolCorruption} rather than "no sampler".
void MGPipeApplySetTextureParams(const MGPTextureParams& params);
//
// Returns true when the record took the parameters (P4a final review m-1, audit F-7): the
// emitter's version latch advances on this answer and on nothing else, the way the
// sub-data and respecify paths latch on theirs, so a refused record - no consumer, no
// record for the handle, a null sampler - is re-sent at the next call rather than at the
// next glTexParameter*. Source-compatible for the same reason the three resource returns
// are: a Bool is ignorable and gen_pipe never parses this header.
Bool MGPipeApplySetTextureParams(const MGPTextureParams& params);
// set_sampler_views / bind_sampler_states / set_shader_images: `tail` is hdr.Count entries
// starting at hdr.Start, and hdr.Start + hdr.Count above the unit bound is
+33 -2
View File
@@ -23,6 +23,14 @@
// (MGPipeTypes.h) is what says so, which is why the same call still carries
// every glBufferData without acknowledging one.
//
// kHasBlob MEANS "THE PAYLOAD OWNS AN MGPBlobRef MEMBER", nothing weaker (P5
// R-13.1). It is not "this call carries bytes": three calls carry bytes with no
// MGPBlobRef at all and they are named in CONTRACT-P5.md table 1, not flagged
// here, because a decoder that trusts kHasBlob has to find a member to read.
// The flags are EXPORTED, once, as kMGPipeCallFlags[] in generated/PipeWire.inc
// (P5 R-13.4): before that table existed every consumer hard-coded its own copy,
// which is how the two carriers below came to disagree with their payloads.
//
// RECORD NUMBERING NEVER CHURNS. Entries that are not implemented yet still occupy their
// line (plan section 11, P0: "the complete call catalogue, placeholders included"). A new
// call is APPENDED to its group; a retired call keeps its slot with a comment. The wire
@@ -77,8 +85,20 @@
// clang-format off
#define MGP_CALL_LIST(X) \
/* ---- screen: caps, resources, persistent map, fences (plan 4.4.1) ---- */ \
X(GetCaps, MGPCaps, kScreen, kReplySlot) \
/* kHasBlob because MGPCaps owns TWO MGPBlobRef members - FormatCapabilities and */ \
/* RendererInfo (MGPipeTypes.h). It carried none until P5 R-13.1; the flag is a */ \
/* property of the payload, so its absence was simply wrong. Adding a flag does not */ \
/* move an opcode: the opcode is this line's 1-based position. */ \
X(GetCaps, MGPCaps, kScreen, kReplySlot|kHasBlob) \
X(ResourceCreate, MGPResourceDesc, kScreen, kNone) \
/* NO kHasBlob, BY RULING (P5 R-13.3; CONTRACT-P5.md table 1 row 19). MGPResourceDesc */ \
/* owns no MGPBlobRef and gains none: under split `initialBytes` is always nullptr and */ \
/* the initial content of a glBufferData(size, data) / glTexImage*(..., data) arrives */ \
/* as resource_subdata records IMMEDIATELY AFTER this one. Reuses a path that is */ \
/* already chunked (MGPipeForEachSubDataRecordRange) and already acceptance-gated; the */ \
/* cost is one extra record. NOTE the SECOND uncarried companion on this call, the */ \
/* MGPRespecifiedLevel* scope (PipeApply.h:792) - it is NOT bytes and NOT covered by */ \
/* this ruling; table 1 row 19 gives it the two pad fields of MGPResourceDesc. */ \
X(ResourceRespecify, MGPResourceDesc, kScreen, kNeedsAck) \
X(ResourceDestroy, MGPHandleOnly, kScreen, kNone) \
X(MapPersistent, MGPHandleOnly, kScreen, kReplySlot|kOptional) \
@@ -101,7 +121,11 @@
X(CreateVertexElements, MGPVertexElements, kCtxCso, kHasBlob) \
X(BindVertexElements, MGPHandleOnly, kCtxCso, kNone) \
X(DeleteVertexElements, MGPHandleOnly, kCtxCso, kNone) \
X(CreateSamplerState, MGPSamplerDesc, kCtxCso, kNone) \
/* kHasBlob because MGPSamplerDesc owns an MGPBlobRef member, `Parameters` - the */ \
/* SamplerParameters POD byte for byte, borderColorForm included (P5 R-13.1). The */ \
/* companion pointer beside the record today is a TYPED frontend pointer */ \
/* (const SamplerParameters*), which is exactly what split may not carry. */ \
X(CreateSamplerState, MGPSamplerDesc, kCtxCso, kHasBlob) \
X(DeleteSamplerState, MGPHandleOnly, kCtxCso, kNone) \
X(CreateSamplerView, MGPSamplerView, kCtxCso, kNone) \
X(DeleteSamplerView, MGPHandleOnly, kCtxCso, kNone) \
@@ -134,6 +158,13 @@
X(ResourceSubData, MGPSubData, kCtxObject, kHasBlob|kVarTail) \
X(BufferSubDataResident, MGPSubData, kCtxObject, kHasBlob|kOptional) \
X(ResourceSubDataComplete, MGPSubDataComplete, kCtxObject, kNone) \
/* NO kHasBlob AND NO MGPBlobRef, BY RULING (P5 R-13.2; CONTRACT-P5.md table 1 row 21). */ \
/* It takes a content pointer today, but under split it CARRIES NO BYTES AT ALL: it is */ \
/* a {range, AccessFlags} control record, and the bytes of [Offset, Offset+Size) arrive */ \
/* AHEAD of it as resource_subdata records covering exactly that range. The ladder this */ \
/* drives rewrites its range "from the authoritative shadow" (Managers.cpp:1047-1076), */ \
/* and under split the authoritative shadow is server-owned (R-11), so subdata is the */ \
/* only way bytes reach it - a blobref here would be a SECOND way to say the same thing. */ \
X(ResourceFlushRange, MGPFlushRange, kCtxObject, kNone) \
X(ResourceReadback, MGPReadback, kCtxObject, kReplySlot) \
X(ResourceCopyRegion, MGPCopyRegion, kCtxObject, kNone) \
+7 -1
View File
@@ -41,10 +41,16 @@
#define MGP_FIELDS_MGPCaps(F) \
F(Dynamic) F(CallMask) F(FormatCapabilities) F(RendererInfo)
// The three respecify-scope fields are listed like any other member: they occupy what used to be
// Pad0's high byte and Pad1, and the generator's cover check only excludes members actually
// NAMED Pad*, so leaving them out would fail --check rather than pass it. They are also real
// G4 comparands - a verify build that let the scope differ silently would be blind to the one
// thing that decides whether a respecify drops one pending upload or all of them.
#define MGP_FIELDS_MGPResourceDesc(F) \
F(Resource) F(Target) F(StorageKind) F(BindMask) F(InternalFormat) F(Width) F(Height) F(Depth) \
F(ArrayLayers) F(Levels) F(Samples) F(FixedSampleLocations) F(Immutable) F(Usage) F(StorageFlags) \
F(HasDefinedContent) F(ImageBindableHint) F(GlNameForDiag) F(ViewOf) F(BufferForTexBuffer) \
F(HasDefinedContent) F(ImageBindableHint) F(HasRespecifiedLevel) F(GlNameForDiag) \
F(RespecifiedUploadTarget) F(RespecifiedLevel) F(ViewOf) F(BufferForTexBuffer) \
F(BufOffset) F(BufSize)
#define MGP_FIELDS_MGPFenceWait(F) \
+57 -2
View File
@@ -312,12 +312,44 @@ namespace MobileGL::MG_Pipe {
//
// Entry points MGPipeTextureEmitter must provide, all taking the frontend object by
// reference and returning void:
// EmitResourceCreate(ITextureObject&) / EmitResourceRespecify(ITextureObject&)
// EmitResourceCreate(ITextureObject&)
// EmitResourceRespecify(ITextureObject&, MGPipeTextureRespecifyScope, Uint32 uploadTarget,
// Uint32 level)
// EmitTextureParams(ITextureObject&)
// NoteLevelDirty(ITextureObject& storageOwner, Uint32 uploadTarget, Uint32 level)
// EmitRenderbufferCreate(RenderbufferObject&) / EmitRenderbufferRespecify(RenderbufferObject&)
void MGPipeEmitTextureResourceCreate(MG_State::GLState::ITextureObject& texture);
void MGPipeEmitTextureResourceRespecify(MG_State::GLState::ITextureObject& texture);
// WHICH STORAGE A TEXTURE RESPECIFY REPLACES (P4a final review C-1). The applier scopes
// its pending-upload clear on this answer and not on the descriptor, because the
// descriptor cannot give it: AllocateStorage is per (uploadTarget, level) and
// TruncateMipmapLevels removes every level at or above a cut, while MGPResourceDesc
// carries only the base extent and the level count. A level the applier had ACCEPTED at
// one verb (the client's dirty flag already clear, D-D5 step 1) and that a later per-level
// definition redefined AROUND was dropped by the whole-resource arm with nobody owing its
// texels - so every respecify states its scope, and "whole resource" is said, never
// defaulted. The emitter builds wire's MGPRespecifiedLevel from the pair, packed exactly
// as the drain packs a sub-data record's Target (MGPipePackSubDataTarget), so the key it
// drops is the key that level's emission made.
enum class MGPipeTextureRespecifyScope : Uint32 {
// The whole store is redefined or restated: a format, sample-count or
// fixed-sample-locations change, an immutable allocation completing
// (SetImmutableLevels), a texture view's creation. Every pending upload goes.
WholeResource = 0,
// ONE (uploadTarget, level) was (re)allocated: glTexImage*D, glCompressedTexImage*D,
// glCopyTexImage*D, one level of a glTexStorage* loop, one level of a generated-mipmap
// grow. That level's pending upload goes; every other level's stays. `uploadTarget` and
// `level` name it.
OneLevel = 1,
// The chain was cut: every level of `uploadTarget` at or above `level` is gone and the
// levels below it are untouched (glGenerateMipmap fitting the chain, a base-level
// redefinition discarding its tail, glTexStorage* fitting the chain to its level
// count). `level` is the first level removed; a cut at 0 is the whole resource.
LevelsFrom = 2,
};
void MGPipeEmitTextureResourceRespecify(MG_State::GLState::ITextureObject& texture,
MGPipeTextureRespecifyScope scope, Uint32 uploadTarget,
Uint32 level);
void MGPipeEmitTextureParams(MG_State::GLState::ITextureObject& texture);
// The DRAIN LIST's append, on a level's FIRST dirty mark, keyed on the STORAGE OWNER from
// day one (D-D4: a view and its owner already share one dirty state, so an upload through
@@ -329,6 +361,29 @@ namespace MobileGL::MG_Pipe {
void MGPipeEmitRenderbufferResourceCreate(MG_State::GLState::RenderbufferObject& renderbuffer);
void MGPipeEmitRenderbufferResourceRespecify(MG_State::GLState::RenderbufferObject& renderbuffer);
// ---- D-A4's two sticky bind-mask producers (P4a final review M-A) ----
//
// kMGPipeBindSampler is "any texture the sampler-view resolution names in an emitted
// MGPBoundView" and kMGPipeBindShaderImage "any texture named in an emitted MGPImageView"
// - both the SAMPLER package's emitters (SamplerEmit.h, ImageEmit.h), which the texture
// emitter's header includes and which therefore cannot include it back - and, earliest of
// all, glBindImageTexture's state setter (TextureState.h, MG_State), which may include no
// emit header at all. So the note goes through this door, exactly as the birth hooks do.
// Nothing produced either bit before the fix round: ImageBindableHint was always 0, the
// metadata respecify (ID-18 M4) had no live trigger, and the remint pull the hint exists to
// prevent was neither prevented nor counted.
//
// UNCONDITIONAL IN A PUSH BUILD, like the mints: the mask is CLIENT state the framebuffer
// emitter ORs into whether or not the texture family is on, and the emission a mask move
// causes (the metadata respecify) is gated inside the emitter on the family's own pair.
void MGPipeNoteTextureBoundAs(MGPipeHandle texture, Uint32 bindBit);
// glBindImageTexture. The hint is the PREVENTION half of the texture-remint stall class -
// a texture the server knows may be image-bound is allocated image-bindable up front - so it
// has to reach the applier before the texture's first sync, i.e. at the bind itself, not at
// the validate point's image walk (which notes it as well, D-A4's letter).
void MGPipeNoteTextureImageBound(MG_State::GLState::ITextureObject& texture);
// ---- sampler CSOs and sampler views: MG_Impl/Pipe/SamplerEmit.h, package C ----
//
// Entry points MGPipeSamplerEmitter must provide, returning void:
+121
View File
@@ -118,6 +118,127 @@ enum class MGPWireOp : Uint16 {
kOpCount = 72,
};
// THE FLAGS, EXPORTED ONCE, INDEXED BY OPCODE (P5 R-13.4). MGPWireRecHeader::Flags is
// documented as "MGPipeCallFlags of the call", and until this table existed nothing
// generated said what those were: every consumer that needed to know whether a record owns
// an MGPBlobRef, a variable tail or a reply slot had to hard-code its own copy of
// PipeCalls.def's fourth column, and six of them were about to. A hard-coded copy is how
// GetCaps and CreateSamplerState came to carry an MGPBlobRef member with no kHasBlob on
// their line at all - nothing compared the two, because nothing had both in one place.
//
// Index 0 is MGPWireOp::kInvalid and is kNone: the catalogue is 1-based, and an encoder
// that reads flags for an opcode it never got from the catalogue must see the empty set
// rather than another call's flags.
//
// kHasBlob here means EXACTLY "the payload owns an MGPBlobRef member". Three calls carry
// bytes without one - resource_respecify, resource_flush_range and map_persistent, whose
// companion pointers have no carrier - and they are deliberately NOT flagged; MG_Remote's
// CONTRACT-P5.md table 1 is where those live, because a decoder that trusts kHasBlob has
// to find a member to read.
inline constexpr Uint32 kMGPipeCallFlags[static_cast<SizeT>(MGPWireOp::kOpCount)] = {
/* 0 kInvalid */ static_cast<Uint32>(kNone),
/* 1 GetCaps */ static_cast<Uint32>(kReplySlot | kHasBlob),
/* 2 ResourceCreate */ static_cast<Uint32>(kNone),
/* 3 ResourceRespecify */ static_cast<Uint32>(kNeedsAck),
/* 4 ResourceDestroy */ static_cast<Uint32>(kNone),
/* 5 MapPersistent */ static_cast<Uint32>(kReplySlot | kOptional),
/* 6 UnmapPersistent */ static_cast<Uint32>(kOptional),
/* 7 FenceCreate */ static_cast<Uint32>(kNone),
/* 8 FenceStatus */ static_cast<Uint32>(kReplySlot),
/* 9 FenceWait */ static_cast<Uint32>(kReplySlot),
/* 10 FenceDestroy */ static_cast<Uint32>(kNone),
/* 11 QueryCreate */ static_cast<Uint32>(kNone),
/* 12 QueryBegin */ static_cast<Uint32>(kNone),
/* 13 QueryEnd */ static_cast<Uint32>(kNone),
/* 14 QueryAvailable */ static_cast<Uint32>(kReplySlot),
/* 15 QueryResult */ static_cast<Uint32>(kReplySlot),
/* 16 QueryDestroy */ static_cast<Uint32>(kNone),
/* 17 CreateRenderState */ static_cast<Uint32>(kHasBlob),
/* 18 BindRenderState */ static_cast<Uint32>(kNone),
/* 19 DeleteRenderState */ static_cast<Uint32>(kNone),
/* 20 CreateVertexElements */ static_cast<Uint32>(kHasBlob),
/* 21 BindVertexElements */ static_cast<Uint32>(kNone),
/* 22 DeleteVertexElements */ static_cast<Uint32>(kNone),
/* 23 CreateSamplerState */ static_cast<Uint32>(kHasBlob),
/* 24 DeleteSamplerState */ static_cast<Uint32>(kNone),
/* 25 CreateSamplerView */ static_cast<Uint32>(kNone),
/* 26 DeleteSamplerView */ static_cast<Uint32>(kNone),
/* 27 CreateShaderState */ static_cast<Uint32>(kHasBlob),
/* 28 BindShaderState */ static_cast<Uint32>(kNone),
/* 29 DeleteShaderState */ static_cast<Uint32>(kNone),
/* 30 SetDynamicState */ static_cast<Uint32>(kHasBlob),
/* 31 SetFramebufferState */ static_cast<Uint32>(kNone),
/* 32 SetVertexBuffers */ static_cast<Uint32>(kVarTail),
/* 33 SetIndexBuffer */ static_cast<Uint32>(kNone),
/* 34 SetIndirectBuffers */ static_cast<Uint32>(kNone),
/* 35 SetSamplerViews */ static_cast<Uint32>(kVarTail),
/* 36 BindSamplerStates */ static_cast<Uint32>(kVarTail),
/* 37 SetShaderImages */ static_cast<Uint32>(kVarTail),
/* 38 SetShaderBuffers */ static_cast<Uint32>(kVarTail | kHostSpan),
/* 39 SetStreamOutputTargets */ static_cast<Uint32>(kVarTail),
/* 40 SetGlobalConstants */ static_cast<Uint32>(kHasBlob),
/* 41 SetVertexAttribDefaults */ static_cast<Uint32>(kVarTail),
/* 42 SetPixelPackState */ static_cast<Uint32>(kNone),
/* 43 SetPatchState */ static_cast<Uint32>(kNone),
/* 44 SetDrawProgram */ static_cast<Uint32>(kNone),
/* 45 SetDispatchProgram */ static_cast<Uint32>(kNone),
/* 46 SetResidualValueState */ static_cast<Uint32>(kHasBlob),
/* 47 SetTextureParams */ static_cast<Uint32>(kNone),
/* 48 ResourceSubData */ static_cast<Uint32>(kHasBlob | kVarTail),
/* 49 BufferSubDataResident */ static_cast<Uint32>(kHasBlob | kOptional),
/* 50 ResourceSubDataComplete */ static_cast<Uint32>(kNone),
/* 51 ResourceFlushRange */ static_cast<Uint32>(kNone),
/* 52 ResourceReadback */ static_cast<Uint32>(kReplySlot),
/* 53 ResourceCopyRegion */ static_cast<Uint32>(kNone),
/* 54 GenerateMipmap */ static_cast<Uint32>(kNone),
/* 55 GetTextureImage */ static_cast<Uint32>(kReplySlot),
/* 56 Blit */ static_cast<Uint32>(kNone),
/* 57 Clear */ static_cast<Uint32>(kNone),
/* 58 ReadPixels */ static_cast<Uint32>(kReplySlot),
/* 59 DrawVbo */ static_cast<Uint32>(kHostSpan | kVarTail),
/* 60 LaunchGrid */ static_cast<Uint32>(kNone),
/* 61 MemoryBarrier */ static_cast<Uint32>(kNone),
/* 62 BeginStreamOutput */ static_cast<Uint32>(kNone),
/* 63 EndStreamOutput */ static_cast<Uint32>(kNone),
/* 64 PauseStreamOutput */ static_cast<Uint32>(kNone),
/* 65 ResumeStreamOutput */ static_cast<Uint32>(kNone),
/* 66 Flush */ static_cast<Uint32>(kNone),
/* 67 Present */ static_cast<Uint32>(kNone),
/* 68 SetSwapInterval */ static_cast<Uint32>(kOptional),
/* 69 QueryTimestamp */ static_cast<Uint32>(kReplySlot),
/* 70 QueryCounter */ static_cast<Uint32>(kNone),
/* 71 FenceWaitServer */ static_cast<Uint32>(kNone),
};
static_assert(sizeof(kMGPipeCallFlags) / sizeof(kMGPipeCallFlags[0]) ==
static_cast<SizeT>(MGPWireOp::kOpCount),
"the flags table and the opcode space disagree");
// The only supported read of the table. Out-of-range is kNone rather than undefined
// behaviour, because the one caller that can pass a bad opcode is a decoder holding bytes
// off a stream, and it must reach its own Fatal{ProtocolCorruption} rather than read past
// the array on the way there.
inline constexpr Uint32 MGPipeCallFlagsFor(MGPWireOp op) {
const SizeT index = static_cast<SizeT>(op);
return index < static_cast<SizeT>(MGPWireOp::kOpCount) ? kMGPipeCallFlags[index]
: static_cast<Uint32>(kNone);
}
// Spot checks the generator states about its own output, so that a catalogue edit that
// silently drops a flag is a build break here and not a wrong decode six packages away.
static_assert(MGPipeCallFlagsFor(MGPWireOp::kInvalid) == static_cast<Uint32>(kNone),
"opcode 0 is not a call and carries no flags");
static_assert((MGPipeCallFlagsFor(MGPWireOp::GetCaps) & static_cast<Uint32>(kHasBlob)) != 0,
"MGPCaps owns two MGPBlobRef members; R-13.1 gave the call its flag");
static_assert((MGPipeCallFlagsFor(MGPWireOp::CreateSamplerState) & static_cast<Uint32>(kHasBlob)) != 0,
"MGPSamplerDesc owns an MGPBlobRef member; R-13.1 gave the call its flag");
static_assert((MGPipeCallFlagsFor(MGPWireOp::ResourceFlushRange) & static_cast<Uint32>(kHasBlob)) == 0,
"R-13.2: resource_flush_range carries no bytes on the wire and owns no blobref");
static_assert((MGPipeCallFlagsFor(MGPWireOp::ResourceRespecify) & static_cast<Uint32>(kHasBlob)) == 0,
"R-13.3: initial bytes follow as resource_subdata; MGPResourceDesc owns no blobref");
static_assert((MGPipeCallFlagsFor(MGPWireOp::DrawVbo) &
static_cast<Uint32>(kHostSpan | kVarTail)) == static_cast<Uint32>(kHostSpan | kVarTail),
"draw_vbo is the conditional-tail plus host-span shape the codec is measured on");
struct alignas(8) MGPWireRec_GetCaps {
MGPWireRecHeader Header;
MGPCaps Payload;
+575
View File
@@ -0,0 +1,575 @@
# CONTRACT-P5 — the four tables every P5 package is held to
Authority: this file. `~/w7/notes/p5/BRIEF-P5.md` states the rulings R-1…R-14; this file is
where they become implementable, and where the rulings the brief left to the contract package
are made. Where the two disagree, this file is newer and this file wins — and §6 lists every
place they disagree, so nobody has to diff them.
**How to change it.** `MG_Remote/CONTRACT-P5.md` is c0's file. A package that needs a row
changed goes through the integrator, who edits here first and only then lets the package land.
P4a's contract was corrected seven times and each correction cost a package a rework round;
the point of this file existing at all is that a correction is a one-line diff here rather than
a rediscovery in six places.
It lives beside the code, not in `~/w7/notes/`, because it describes bytes on a wire and has to
move with the branch that defines them.
Base: `feat/disaggregated @ a29807cc`. Every `file:line` below was read at that commit.
---
## §0 The three rules that sit above every table
These are R-2 and R-11 in their formal wording. They apply to every row of table 1 without
restatement, and every reviewer's first three questions (BRIEF §11.1, §11.4) are these three.
**Rule A — a content record must declare its bytes.** Under
`MG_Config::Transport != Monolith`, a record whose payload owns an `MGPBlobRef` and which
carries content must set `Seg` to a real segment, `Offset` to a byte offset **within that
segment**, and `Size` to a **non-zero** byte count. `Blob.Size == 0` on such a record is
`Fatal{ProtocolCorruption}`.
This inverts today's legal state, and deliberately. `MGPipeTypes.h:398-410` says in so many
words that a zero `Blob.Size` means *"this record does not declare its blob"* and *"is not a
fault"* — which is right for monolith, where the bytes travel beside the record as a companion
pointer. Under split there is no beside.
**Rule B — no host pointer crosses.** Under split, `MGHostSpan::Ptr` is written `nullptr` by
the encoder and any non-null `Ptr` seen by the apply side is `Fatal{ProtocolCorruption}`.
Today `Ptr` is the fast path (`MGPipeHostSpan.h:51`), which is exactly why this needs saying.
P5's reduced path should produce **zero** host spans at all — see table 0's cap-bit row.
**Rule C — an applier entry point may not hold a pointer past its return.** A `SEG_STAGE` run
is valid from publish until `retiredSeq` passes the record that named it. The tree has exactly
one violation and it is named rather than tolerated: Espryt's `GLESBufferResource::hostBytes`
(`Managers.h:839`), written by `Ops_H_SubData` (`Managers.cpp:1980-1983`) and `Ops_H_FlushRange`
(`:2035`), read by six later drains (`:2000`, `:2062`, `:2080`, `:2111`, `:2741`, `:2843`).
Under split those two writes must **copy into server-owned storage**. `MOBILEGL_IPC_AUDIT=1`'s
`0xDD` fill over retired staging bytes (R-2.5) is the mechanical control that says whether they
did; without it, an `inproc` implementation that kept the pointer is indistinguishable from one
that copied.
---
## §1 Table 0 — the encoding table
One row per wire field that is **not a handle**. Handles are `{slot, gen}` and are settled by
P2/P3a; everything else that is not a plain scalar is here.
| field | the ruling | zero means | who reads it | evidence / note |
|---|---|---|---|---|
| **segment id space** | `SEG_CMD=1, SEG_STAGE=2, SEG_REPLY=3, SEG_EVENT=4, SEG_SHADOW=5, SEG_ADOPT=6`. Identical values to `Protocol::SegmentKind`. | **"no segment", always.** 0 is never a real segment id. | encoder, decoder, `gMGPipeSegmentResolver` | `protocol.fbs:36-44`; `kMGHostSpanSegNone = 0` at `MGPipeHostSpan.h:21`. The two are tied by `static_assert` in `Wire/PipeWireCodec.cpp`, which is the only place the flatbuffers header and the codec enum meet. `kMGHostSpanSegFromServerIndexMirror = 0xFFFFFFFF` (`:26`) stays reserved for P8. |
| **`MGPBlobRef{Offset, Size, Seg, Pad0}`** (24 B, `MGPipeTypes.h:55-61`) | `Seg` from the row above. `Offset` is a **byte offset inside that segment**, never a host address. `Size != 0` ⇔ "this record declares its blob", and under split a content record **must** declare it (rule A). | `Size == 0` = "no blob declared" — legal in monolith, `Fatal` under split for a content record. | decoder, every bounds cross-check | Today every emitter writes `{Seg=None, Offset=<host address>, Size=0 or real}`. Five of the eleven blob members do declare a real size today (`CsoCache.h:156`, `VertexInputEmit.h:398`, `ResourceTracker.h:216`, `PipeFill.cpp:2259`); the rest write 0. See table 1's "length" column for which. |
| **`MGHostSpan{Ptr, Seg, Pad0, Size, Offset}`** (32 B, `MGPipeHostSpan.h:28-37`) | Split: `Ptr == nullptr` always (rule B). **The 32-byte layout must not be reordered** (`:29-31`). | `Seg == 0` with `Size != 0` is `Fatal`. | decoder | **P5's reduced path must produce none at all.** See the cap-bit row below. |
| **`kCapNeedsHostIndexBytes` (1<<7), `kCapNeedsHostUboBytes` (1<<8)** | **Both are 0 for the whole of P5**, by ruling. | — | client emitters | `MGPipeTypes.h:120-123`. This is the cheapest way to keep every `MGHostSpan` out of the first IPC frame: the two bits are the only things that ask for one. `TriangleScenario` must therefore use a **VBO-backed draw and not client-array indices**, or `kDrawHasUserIndices` (`MGPipeTypes.h:1212`) produces a span the split filling for which is P8's. |
| **`MGPCaps`** (4 members, `MGPipeTypes.h:126-139`) | **One carrier, not two.** `MGPCaps` is the model; `protocol.fbs`'s `CapsSnapshot` is its transport form. `Dynamic` and `CallMask` cross as POD bytes; `FormatCapabilities` and `RendererInfo` cross as the two blobs whose serializers are P5's new work (`MG_Remote/CapsCodec.h`). | — | `CapsMirror` (client), `ServerSession` (server) | `MGPCaps` has only a **compositional** size assertion (`MGPipeTypes.h:145-146`) because `DynamicBackendParameters` still carries `SizeT` and `GLenum`; P0.5's fixed-width rewrite never happened. **P5 does not rewrite it** — see the ABI row. |
| **`CapsSnapshot` redundancy** | `tableSlotMask` (`protocol.fbs:94`) is **DELETED**, not renamed. `maxComputeWorkGroupCount` / `maxComputeWorkGroupSize` (`:92-93`) and `prefersCpuXfbPrimitiveAccounting` (`:95`) are **deleted** too: the first two ride inside `Dynamic` already (`BackendObject.h:392-393`), and the third is answered by `kCapCpuXfbPrimitiveAccounting`. | — | s1 (the schema), c1 (the mirror) | R-8 offered rename-or-delete for `tableSlotMask`; **delete**, for two reasons and the second is decisive. `ARCHITECTURE.md:114` says `CallMask` *replaces* "is this table slot null" as the capability probe, so a field whose comment is "which `GLFunctionsTable` slots the peer registered" re-introduces precisely what it replaced. And `GLFunctionsTable` has **69** function-pointer slots (`BackendObject.h:117-292`), so a `ulong` mask cannot address it and never could — it is five bits short on day one. |
| **`MGPCaps::CallMask` layout** | bits **0..8** = `MGPCapBit`, unchanged. bits **9..31** reserved. bits **32..47** = the **consumer mask**: bit `32+n` means "the server has a consumer for MGPipe subsystem bit `n`". bits **48..63** reserved. | a clear consumer bit = "this server does not consume that family; emit nothing for it". | `CapsMirror::ServerConsumes` — the **only** legal client-side source | c0's ruling, and the thing that makes R-8 implementable at all: R-8 says the client's liveness gates must read the `CallMask` mirror, but `CallMask` as declared has only nine feature bits and no per-family bit. Constants and the two fold/test helpers are in `MG_Remote/CapsCodec.h`; `CapsCodec.cpp` asserts the block does not collide with `MGPCapBit` and that P4a's `0x1fff` fits sixteen bits. |
| **ABI agreement** | `Hello`/`Welcome` assert both peers agree on `sizeof(DynamicBackendParameters)`, `sizeof(MGPCaps)`, `sizeof(GLFunctionsTable)` and `buildFingerprint`. A mismatch is `Fatal{AbiMismatch}` and **never** a downgrade. | — | s1 | The compositional assertion above means the caps block's literal size is ABI-dependent. P6's spawn is same-machine, same-binary and inherits this unchanged. A fixed-width rewrite of `DynamicBackendParameters` is **P7's** account, not P5's. |
| **`MGPSubData::Target`** | Packed: **low byte = `MGPipeResourceTarget`, high byte = the cube-face upload target**. Read only through `MGPipeSubDataResourceTargetOf` / `MGPipeSubDataUploadTargetOf`. Whole field `== 0` is the **buffer** half; a low byte naming `Buffer`, `Renderbuffer` or `>= MGPipeResourceTarget::Count` is `Fatal{ProtocolCorruption}`. | whole field 0 = buffer upload | applier | P4a ID-12. Already settled; copied here because a decoder that open-codes it is the class-1 defect. |
| **`MGPImageView::Access`** | The three-value encoding at `ImageEmit.h:146-159`, verbatim. | — | decoder, applier | **This encoding has only ever lived in a package header** (P4a R-3). This table is its first wire reader, so it is now contract. |
| **`MGPSamplerView::Target`** | The encoding at `SamplerEmit.h:900`, verbatim. | — | decoder, applier | Same status as the row above. |
| **`MGPFramebufferState::DrawBuffers[8]`** | The `-1` / default-token narrowing at `FramebufferEmit.h:146-163`, verbatim. A draw-buffer entry naming a colour attachment outside the record's own array is `Fatal{ProtocolCorruption}` (the applier already says so). | `-1` = "no attachment" | decoder, applier | Same status. |
| **`MGPReplySlot::Id`** | **= the record's sequence number** (R-3). No new id space, no allocator. The server writes the answer into `SEG_REPLY[seq % slots]` and **stamps `seq` back into the slot header** so a wrong-slot read is detectable rather than plausible. | seq is 1-based; `0` = "no record / not encoded" | client barrier wait | `ARCHITECTURE.md:124`: the wire carries no per-record seq field, so seq *is* the ordinal. `MGPReplySlot` exists (`MGPipeTypes.h:78-81`) and **no payload of the ten `kReplySlot` calls contains one** — which is exactly why the id must be derived rather than carried. P9 generalises this to "seq is the id's initial value", which extends the rule rather than overturning it. |
| **reply slot header** | `{Uint64 Seq; Int32 Status; Uint32 Size;}` — 16 bytes, then the payload. `Status`: **0 = OK, 1 = DECLINED, 2 = ERROR**. | — | client | **`DECLINED` is a real answer, not a failure.** It is how `MapPersistent` says `nullptr` (R-6) and how the four `Bool` acceptance entry points say `false` (R-5). A client that treats DECLINED as an error re-creates ID-39's 66 lost uploads from the other side. |
| **`kRecPad` and seq** | A wrap filler **does not advance seq**, on either side. | — | both | R-9. `RingConsumer::Pop` already skips fillers; the rule is stated because the *counter* is the caller's, not `Pop`'s. A side that counts pads drifts by one per wrap, for ever — and since seq is the reply-slot id, a drifted seq reads another call's answer instead of failing. Pinned by `RingTest.AWrapFillerDoesNotAdvanceTheRecordSequence`. |
| **per-opcode flags** | `kMGPipeCallFlags[MGPWireOp::kOpCount]` in `generated/PipeWire.inc`, read only through `MGPipeCallFlagsFor(op)`. Index 0 (`kInvalid`) is `kNone`. | `kNone` = no flags | every package | R-13.4. Before this table existed nothing generated exported the flags, so six packages were each about to hard-code `PipeCalls.def`'s fourth column — which is how `GetCaps` and `CreateSamplerState` came to own an `MGPBlobRef` with no `kHasBlob` on their line. `gen_pipe.py` now also refuses a flag token that is not an `MGPipeCallFlags` enumerator, with two negative controls in `--self-test`. |
| **`kHasBlob`'s meaning** | **Exactly "the payload owns an `MGPBlobRef` member"** — nothing weaker. | — | decoder | `PipeApply.h:78-79` already says so. Three calls carry bytes with **no** `MGPBlobRef`; they are table 1 rows 1921 and are deliberately unflagged, because a decoder that trusts `kHasBlob` has to find a member to read. |
---
## §2 Table 1 — the byte carriers
**23 rows, not 19.** BRIEF §3 lists 19 and `scout-premortem:§3` lists a different 19; the
union is 23 and the four that only the premortem lists — `MapPersistent`, `ResourceReadback`,
`ReadPixels`, `GetTextureImage` — are precisely the ones whose bytes travel **server → client**.
Leaving them out of the byte-carrier table is how a phase discovers in week three that it never
decided where readback pixels land. They are rows 2023 and are marked with their owning phase.
Columns: **flags** · **blob member** · **companion pointer today** · **which segment the bytes
live in** · **who owns that memory** · **when the slot retires** · **who declares the length,
who cross-checks it** · **reply name** (for `kReplySlot` rows).
`apply` = retires when `DecodeAndApply` returns. `submit` = when the server has handed the bytes
to the driver. `gpu` = `completedFrameSerial`.
### Group A — `kHasBlob`, the payload owns an `MGPBlobRef`
| # | call (op) | flags | blob member | companion today | segment | owner | retires | length declared / cross-checked |
|---|---|---|---|---|---|---|---|---|
| 1 | `CreateRenderState` (17) | `kHasBlob` | `Blob` (`MGPipeTypes.h:364`) | `const void* chunkBytes` (`PipeApply.h:740`), passed `CsoCache.h:157` | `SEG_STAGE` | client stages, server copies on apply | **apply** | declared real (`CsoCache.h:156`, `= kMGPipePipelineChunkBytes`); **nothing reads it today**`MGPipeApplyCreateRenderState` (`PipeApply.cpp:1280`) never touches `Blob.Size`. Decoder must cross-check against `ChunkMask`. |
| 2 | `CreateVertexElements` (20) | `kHasBlob` | `Blob` (`:419`) | `const void* blobBytes` (`PipeApply.h:932`), passed `VertexInputEmit.h:399` | `SEG_STAGE` | client stages, server copies | **apply** | declared real (`VertexInputEmit.h:398`); **cross-checked, and this is the model for every other row**: `PipeApply.cpp:1990-1999` recomputes `AttributeCount*sizeof(MGPVertexAttribWire) + BindingPointCount*sizeof(MGPVertexBindingPointWire)` and refuses a disagreement; both counts bounded by `kMGPipeMaxVertexAttribs`. |
| 3 | `CreateShaderState` (27) | `kHasBlob` | **seven**: `Spirv[6]` (`:507`) + `Reflection` (`:508`) | **two typed frontend pointers**`const LinkArtifacts*` + `const SpirvArtifacts*` (`PipeApply.h:1039-1041`), passed `ProgramEmit.h:257` | `SEG_STAGE`, seven independent runs | client stages, server copies | **apply** | all seven declare `Size = 0` today and `Reflection.Offset` is literally `(Uint64)&link` (`ProgramEmit.h:254`). **The serializer already exists and no package may write a second one**: `EncodeProgramArtifacts`/`DecodeProgramArtifacts` (`ProgramArtifactsCodec.{h:53,60,cpp:252,264}`), its own suite, and the verify build already round-trips **every real program it links** (`PinProgramArchiveRoundTrip`, `PipeApply.cpp:1056-1087`, called `:2638`). |
| 4 | `SetDynamicState` (30) | `kHasBlob` | `Blob` (`:389`) | `const void* chunkBytes` (`PipeApply.h:748`), passed `PipeFill.cpp:2260` | `SEG_STAGE` | client stages, server copies | **apply** | declared real (`PipeFill.cpp:2259`); **nothing reads it** (`PipeApply.cpp:1363-1370` scatters by `ChunkMask`). Same fix as row 1. |
| 5 | `SetGlobalConstants` (40) | `kHasBlob` | `Blob` (`:848`) | `const void* bytes` = `MapUBO()`'s image (`PipeApply.h:1052`), passed `ProgramEmit.h:195` | `SEG_STAGE` | client stages, server copies | **apply** | declares **0** (`ProgramEmit.h:194`); cross-check exists at `PipeApply.cpp:2784` against `Desc.GlobalUboSize` but is **inert while Size is 0**. Under rule A it becomes live. Per program per frame, unbounded length — the row most worth watching against R-10's max-record counter. |
| 6 | `SetResidualValueState` (46) | `kHasBlob` | `Blob` (`:932`) | **none, and no record either**: the entry point is `MGPipeApplySetResidualValueState(const ResidualValueBlock&)` (`PipeApply.h:760`), passed `PipeFill.cpp:2184`. **`MGPResidualValueState` is never instantiated on the live path.** | `SEG_STAGE` | client | **apply** | nothing declares it. The encoder must invent **both** the record fill and the blob fill. `sizeof(ResidualValueBlock) == MGL_RESIDUAL_BLOCK_SIZE == 8` is statically asserted (`MGPipeTypes.h:924-927`) and only ever ratchets **down**. **This is the hardest row in the table** and neither scout flagged it; see §6. |
| 7 | `ResourceSubData` (48) | `kHasBlob\|kVarTail` | `Blob` (`:1009`) | `const void* bytes` + `const MGPSubRegion* regions` (`PipeApply.h:897-899`); buffer half `PipeFill.cpp:705`, texture half `TextureEmit.h:1278-1280` | `SEG_STAGE` | client stages, **server must copy** (rule C names this call) | **apply** | **the two halves disagree today**: buffer declares real (`ResourceTracker.h:216`), cross-checked at `PipeApply.cpp:702`; texture declares **0** (`TextureEmit.h:1265-1267`) on the grounds that the byte count is *"the server's to compute once it has picked box-or-rects"* — which cannot be a bounds check. **Under rule A the texture half must declare too.** Tail: `MGPSubRegion[RegionCount]` (`:1007`). |
| 8 | `BufferSubDataResident` (49) | `kHasBlob\|kOptional` | `Blob` (`:1009`, same payload) | `const void* bytes` = application staging, *"valid for the duration of the call only"* (`PipeApply.h:90`, `:901`); one caller, `PipeFill.cpp:727` | `SEG_STAGE` | client stages, server copies | **apply** | declared real via `MGPipeBuildSubDataRecord`, cross-checked at `PipeApply.cpp:702`. `kOptional` is a **capability** question under split, not a null-pointer question: the client must gate on `kCapResidentSubData` through the caps mirror, never on a table slot (R-8). |
### Group B — `kVarTail`, a tail and no blob member
Every row here declares its tail by a **count**, and `MGP_WIRE_CHECK_BOUNDS` **cannot see the
tail at all** — it only proves `size >= sizeof(MGPWireRec_X)`, so a record declaring
`Count = 4000` while carrying 8 bytes passes today. The decoder must recompute the total from
the declared count(s) and require it to **equal** `MGPWireRecHeader::Size`.
| # | call (op) | flags | tail element × count | companion today | segment | owner | retires | length |
|---|---|---|---|---|---|---|---|---|
| 9 | `SetVertexBuffers` (32) | `kVarTail` | `MGPVertexBuffer` × `Count` (`:730`) | `const MGPVertexBuffer*` (`PipeApply.h:941`), `VertexInputEmit.h:245` | `SEG_STAGE` (tail follows the payload in `SEG_CMD` only if it fits the record bound) | emitter-owned `Vector`, **reused next emission** | **apply** | `Count`; no tail cross-check today |
| 10 | `SetSamplerViews` (35) | `kVarTail` | `MGPBoundView` × `Count` (`:778`) | `const MGPBoundView*` (`PipeApply.h:1028`), `SamplerEmit.h:786` | as above | as above | **apply** | `Count`; `Start+Count` past the unit bound is already `Fatal` (`PipeApply.h:1024-1026`) — a *slot* bound, not a byte-length check |
| 11 | `BindSamplerStates` (36) | `kVarTail` | `MGPipeHandle` × `Count` (`:785`) | `const MGPipeHandle*` (`PipeApply.h:1029`), `SamplerEmit.h:860` | as above | as above | **apply** | as above |
| 12 | `SetShaderImages` (37) | `kVarTail` | `MGPImageView` × `Count` (`:802`) | `const MGPImageView*` (`PipeApply.h:1030`), `ImageEmit.h:129` | as above | as above | **apply** | as above |
| 13 | `SetShaderBuffers` (38) | `kVarTail\|kHostSpan` | **two tails**: `MGPBufferRange` × `Count` (`:826`), then `MGHostSpan` × `HostSpanCount` (`:828`) | **none — no applier entry point exists.** P5 writes the first producer *and* the first consumer. | `SEG_STAGE` | — | **apply** | `HostSpanCount` is 0 **or** `Count`, never anything else (`MGPipeTypes.h:820-823`), so the two arrays stay index-aligned. `kCapNeedsHostUboBytes` is 0 for all of P5, so the second tail is **always absent** in this phase. |
| 14 | `SetStreamOutputTargets` (39) | `kVarTail` | **two tails**: `MGPBufferRange` × `Count`, then `Uint32` × `Count` (`:836-838`) | **none — no applier entry point exists.** | `SEG_STAGE` | — | **apply** | one `Count` sizes both tails; off the reduced path in P5 |
| 15 | `SetVertexAttribDefaults` (41) | `kVarTail` | `MGPAttribValue` × `Count` (`:864`) | `const MGPAttribValue*` (`PipeApply.h:756`), `PipeFill.cpp:2115` | as above | as above | **apply** | **two declarants that must agree**: `Count` and `popcount(Mask)` (`:863`, contract at `PipeApply.h:754-755`). A disagreement is a wire fault nothing checks today; the decoder must. |
| 16 | `DrawVbo` (59) | `kHostSpan\|kVarTail` | `MGPDrawRange` × `NumDraws` (`:1239`), then a **conditional** `MGHostSpan` when `Flags & kDrawHasUserIndices` (`:1212`, `:1231`) | **none — no applier entry point exists.** | `SEG_STAGE` | — | **apply** | `NumDraws`; the span carries its own `Size`. **The only `kHostSpan` on the hot path, and P5 must produce none of them**`TriangleScenario` uses a VBO-backed draw precisely so this tail never appears. `MGPipeTypes.h:1221-1224` defers the fixed-head-versus-tail question to this phase: **P5 keeps it in the tail, unchanged**; there are no per-draw byte histograms yet to justify moving it, and moving it would be a wire-format change with no measurement behind it. |
### Group C — carries content with **no** `MGPBlobRef` and **no** `kHasBlob`
These three are the reason `kHasBlob` had to be given an exact meaning (table 0).
| # | call (op) | flags | ruling | evidence |
|---|---|---|---|---|
| 17 | `CreateSamplerState` (23) | **now `kHasBlob`** (R-13.1) | `MGPSamplerDesc` **does** own an `MGPBlobRef Parameters` (`:429`) and the flag was simply missing. The blob is `memcpy(sizeof(SamplerParameters))` — a POD, and **`borderColorForm` must survive byte for byte** (`MGPipeTypes.h:425-428`), because all three colour representations are always numerically populated and it is the only thing that says which one the backend must use. | companion today is a **typed frontend pointer**, `const SamplerParameters*` (`PipeApply.h:1000`), passed `SamplerEmit.h:458`. Declares `Size = 0` (`SamplerEmit.h:433-435`); cross-check at `PipeApply.cpp:2353` is inert until rule A arms it. **Padding trap:** `SamplerEmit.h:437-445` — assignment leaves three trailing padding bytes stale, and the bytes staged must be the bytes a later `memcmp` compares. |
| 18 | `GetCaps` (1) | **now `kReplySlot\|kHasBlob`** (R-13.1) | `MGPCaps` owns **two** `MGPBlobRef`s, `FormatCapabilities` and `RendererInfo` (`:137-138`), and carried no `kHasBlob` at all. | `PipeCalls.def:80` before the fix. Serializers are P5's new work (`MG_Remote/CapsCodec.h`); the header itself defers them to this phase (`MGPipeTypes.h:134-136`). |
| 19 | `ResourceRespecify` (3) | stays `kNeedsAck`, **no `kHasBlob`** (R-13.3) | **`initialBytes` is always `nullptr` under split. Initial content arrives as `ResourceSubData` records immediately after this one.** `MGPResourceDesc` owns no `MGPBlobRef` and gains none. | The alternative was costed and rejected: `MGPBlobRef` is 24 bytes, `MGPResourceDesc`'s two pads are `Uint16 Pad0` (`:303`) + `Uint32 Pad1` (`:315`) = **6 bytes**, so a blob member takes the struct 88 → 112 and moves `MGP_ASSERT_POD(MGPResourceDesc, 88)` (`:320`). The chosen route reuses a path that is already chunked (`MGPipeForEachSubDataRecordRange`, `PipeFill.cpp:694-713`) and already acceptance-gated; it costs one extra record. `HasDefinedContent` (`:301`) is the field the encoder branches on, and it already exists. **The texture path already does exactly this**`TextureEmit.h:1137` passes `nullptr` and relies on a following upload — so this generalises today's texture behaviour to buffers rather than inventing anything. |
| 19b | `ResourceRespecify`'s **second** uncarried companion | — | **`const MGPRespecifiedLevel* level` (`PipeApply.h:792-795`, 4 bytes: `Uint16 UploadTarget; Uint16 Level;`) has no wire carrier either, and it is not bytes — R-13.3 does not cover it.** Ruling: it rides in `MGPResourceDesc`'s existing pads — `Pad1` (4 B, `:315`) becomes `{Uint16 RespecifiedUploadTarget; Uint16 RespecifiedLevel;}` and one byte of `Pad0` (`:303`) becomes `Uint8 HasRespecifiedLevel`. **Zero size change, `MGP_ASSERT_POD(..., 88)` does not move**, and `PipeFields.def`'s `MGP_FIELDS_MGPResourceDesc` gains the two named members (pads are excluded from field lists, so this is required, not optional). | Null means "this respecify redefines the **whole** resource" and drops every pending upload; non-null names the single `(uploadTarget, level)` and drops **only** that key. Clearing the whole set for a per-level `glTexImage2D` loses exactly the texels the server-side set exists to protect (`PipeApply.h:805-820`). Without a carrier, every OpenRA per-level respecify would silently take the whole-resource arm. **LANDED** (integrator ruling A made `MGPipeTypes.h` c0's file): `Uint8 HasRespecifiedLevel` in Pad0's high byte, `Uint16 RespecifiedUploadTarget; Uint16 RespecifiedLevel;` in Pad1, `MGP_ASSERT_POD(MGPResourceDesc, 88)` unmoved, plus an `offsetof` assertion that the pair stays adjacent and in `MGPRespecifiedLevel`'s order. **Read it only through `MGPipeRespecifyIsWholeResource` / `MGPipeRespecifiedUploadTargetOf` / `MGPipeRespecifiedLevelOf`, and write it only through `MGPipeSetRespecifiedLevel` / `MGPipeClearRespecifiedLevel`**: three fields are one value, and an open-coded reader that forgets the presence byte reads level 0 of upload target 0 as a real scope. **The carrier has no producer** — P5 builds only whole-resource descriptors, and a verify build pins that (`PinWholeResourceRespecifyScope`, `PipeApply.cpp`, the `PinNoLiveHostWrites` shape) so the phase that wires it cannot arrive unannounced. |
| 20 | `ResourceFlushRange` (51) | stays `kNone` (R-13.2) | **It carries no bytes at all under split.** It is a `{range, AccessFlags}` control record; the bytes of `[Offset, Offset+Size)` arrive **ahead of it** as `ResourceSubData` records covering exactly that range. | R-13.2 offered "add a blobref" or "write the convention down". Neither, and for a reason: the ladder this record drives rewrites its range *"from the authoritative shadow"* (`Managers.cpp:1047-1076`), and under split the authoritative shadow is **server-owned** by rule C — so `resource_subdata` is already the only way bytes reach it, and a blobref here would be a second, forgeable way to say the same thing. `AccessFlags` must still cross **verbatim**, not normalised (`PipeApply.h:902-903`). **Overturn condition:** if the tier-1 `INVALIDATE_RANGE` arm turns out to need the bytes and the range in the *same* record — i.e. an intervening record could stale the subdata — this needs its own blobref. It cannot happen while the verb barrier holds, because nothing interleaves; **revisit when the barrier retires for the buffer family.** |
### Group D — the four server → client rows the brief's list omitted
| # | call (op) | flags | ruling | reply name |
|---|---|---|---|---|
| 21 | `MapPersistent` (5) | `kReplySlot\|kOptional` | **Returns `nullptr` under split, always** (R-6/R-2.4). Its `const void* seedBytes` companion (`PipeApply.h:917`) therefore never crosses in P5 and needs no carrier. The three frontend sites already tolerate a decline (`BufferObject.cpp:238`, `:603-606`, `:657-660`). Answer travels as `Status = DECLINED` with a zero-length payload. | `map_persistent.decline` |
| 22 | `ResourceReadback` (52) | `kReplySlot` | Bytes go **server → client** in `SEG_EVENT` via `OnBufferWriteback` (#3), not in the reply slot: the destination is the client's shadow and the size is the resource's, not a fixed slot's. The reply slot carries only completion. **The ordering rule is load-bearing:** the writeback is applied **before** the mutation epoch bumps, never after (`ARCHITECTURE.md:292-294`, `Managers.cpp:2120-2136`). | `resource_readback.done` |
| 23 | `ReadPixels` (58) / `GetTextureImage` (55) | `kReplySlot` | **`ReadPixels` blocks in P5** and its pixels come back in the reply slot, which is why `ReplyPool::SlotBytes()` is sized from the scenario's largest read rather than guessed. `MGPReadbackInfo` has `DstOffset`/`DstSize` but **no `Seg`** (`MGPipeTypes.h:1197-1206`): ruling — the destination is **always `SEG_REPLY`** in P5, so no `Seg` field is added; the PBO destination (fire-and-forget plus a client-side `MarkGpuWritten`) is b1's and also needs none, because a PBO destination is a resource handle rather than a segment. `GetTextureImage` is **not on P5's reduced path** and its slot stays `Fatal{UnmigratedVerb}`. | `read_pixels.pixels` |
---
## §3 Table 2 — `PipeInputs` field ownership
**This section is the SPEC and the four class definitions. The authoritative instance is
generated**: package p1 writes the generator that emits `generated/PipeFieldOwnership.inc` plus
a `--check`, in the shape of `gen_pipe_dirty_surface.py`. **A field in none of the four classes
is a build failure** (R-7.1) — that is the whole mechanism, and a hand-maintained table would
be wrong within a week.
The domain is **63 fields** (`kMGPipeInputFieldCount`, asserted `generated/PipeFilled.inc:96`)
**plus the 7 sticky forwards**, which are among those 63 but are exempted from the poison and
so need their own row. 70 rows, each in exactly one class.
### The four classes
**RECORD-SUPPLIED** — a pushed record supplies the **whole** field, so the server never needs
the client for it.
Membership is `kMGPipeFieldEmittedBy` (`generated/PipeFilled.inc:336-402`,
`kMGPipeEmittedFieldCount = 40`) **minus** the nine for which
`EmittedCallSuppliesTheWholeField` returns false (`MG_Impl/Pipe/PipeFill.cpp:1924-1939`;
reasons `:1875-1923`). **31 fields today.**
The nine excluded, with the generator's own reason: `GetPixelStoreParameters` ("only the PACK
half has a carrier"); `GetCurrentVertexAttribute` ("the applier cannot reproduce GLContext's
cross-view conversion"); `GetMaxTouchedTextureUnit` ("the set is hash-suppressed while the
high-water mark still moves"); and six sharing one reason — "the storage is a frontend heap
reference and the record carries an 8-byte `{slot, gen}`" — `GetBoundVertexArray`,
`GetFramebufferBindingSlot`, `GetImageTextureBinding`, `GetTextureUnitObject`,
`GetProgramForDraw`, `GetProgramForDispatch`.
**APPLIER-DERIVED** — the applier writes it from the records it already applies; no client
participation at all. Today: the render-state mirrors, `m_pixelStore[0]` (the **pack** half),
capability bits, the current vertex attribute, and the patch fields — written at
`PipeApply.cpp:177-184`, `:1338-1437`, plus everything `MGPipeDeriveRenderStateFields`
(`PipeApply.h:1084`, `PipeApply.cpp:2157`) derives.
**BARRIER-PULLED** — **P5's debt, and every row names the phase that retires it.** The server
answers by reading a value the client's residual fill put into the single shared `gPipeInputs`
while the verb barrier holds both threads apart. It is correct only because of that barrier,
which is why the barrier is load-bearing rather than cautious.
Each read increments `PipeStats::CallClass::ResidualPulls` (short name `rsp`, inside the
`#if MOBILEGL_PIPE_PUSH` block so G1 holds), published per frame. **`rsp`'s value at the end of
P5 is the size of the P6/P7/P8 debt** and goes into MEASUREMENTS.
`MOBILEGL_IPC_STRICT_ERRORS=1` promotes every read in this class to `Fatal`, and a named test
asserts the abort actually happens — an instrumentation that cannot go red is decoration.
**FATAL** — no carrier, and the reduced path never reads it, so a read is a real defect.
`Fatal{UnmigratedPipeInput, "<Field>@<verb>"}` (`generated/PipeFilled.inc:407-413`), live at
every log level on purpose (`PipeInputs.h:29-31`: *"this is not `MOBILEGL_ASSERT`, which is
inert in INFO builds"*).
### The known BARRIER-PULLED rows — the 21 the reduced path actually reads
Union of `kClear` (7 of its 18 own fields), `kDraw` (19 of 47) and `kReadback` (12 of 17).
OpenRA adds no field to this set — it widens the **site** set, not the field set, and is the
first thing to reach the read-attachment sites (`Managers.cpp:8603`, `:8966`) and the
`maxTouchedUnit >= 0` texture-unit walks.
| field | class | retires in | note |
|---|---|---|---|
| `GetBoundVertexArray` | O | **P8** | `DirectGLES.cpp:4486`, `PrepareForDraw`, **unconditional on every draw**. `PipeFill.cpp:1902-1905` says the pull retires at P8, not here. |
| `GetProgramForDraw` | O | **P8** (Espryt), P7 (Magma) | `DirectGLES.cpp:4497`, same site, also unconditional. |
| `GetBufferBindingSlot` | O | P8 (indirect half), P9 (readback), P13 (transfer) | 18 Espryt sites; the 7 of 15 `BufferTarget`s no call covers (`Coverage.def:37-70`). |
| `GetBufferBindingPoint` | O | P3b/P4b + P7 | |
| `GetTouchedBufferBindingPointCount` | V | P3b/P4b | |
| `GetFramebufferBindingSlot` | O | P3b/P4b (Espryt), **P7** (Magma) | 8 Espryt sites through `GetFramebufferBindingSlotChecked`; `SyncCurrentFBO` (`:2995`) is self-declared monolith glue (`DirectGLES.cpp:2961-2965`) while `BindCurrentFBO` (`:4303-4353`) is already split-clean. |
| `GetTextureUnitObject` | O | P3b/P4b, P7 | 13 Espryt + 8 Magma sites. |
| `GetImageTextureBinding` | O | P3b/P4b, P7 | |
| `GetActiveTextureUnit` | V | P3b/P4b | server answers from its own state (`Coverage.def:215-219`). |
| `GetMaxTouchedTextureUnit` | V | P3b/P4b | hash-suppressed set, high-water mark still moves. |
| `GetTextureContextId` | V | P3b/P4b | **not a value to migrate**: the server answers from its own `Serial`. `Coverage.def:220-224` is explicit that no call carries these three and none should. |
| `GetTextureBindGeneration` | V | P3b/P4b | as above |
| `GetSamplingResolutionGeneration` | V | P3b/P4b | as above |
| `GetCurrentVertexAttribute` | V | P3b/P4b | the applier cannot reproduce GLContext's cross-view conversion. |
| `GetPixelStoreParameters` | V | **split in P5 — see below** | |
| `IsTransformFeedbackActive`, `IsTransformFeedbackPaused`, `GetTransformFeedbackProgram`, `GetTransformFeedbackGeneration`, `GetBoundTransformFeedbackLifetimeId`, `GetTransformFeedbackCapturedVertices` | V/O | P3b/P4b (Espryt XFB scatter), P7 (Magma) | read on `kDraw`; XFB itself is off the reduced path but these are read on the draw walk regardless. |
**`GetPixelStoreParameters` is split into pack and unpack in this phase** (R-7's one small
migration, and it is c0's ruling to keep rather than soften). The field is `m_pixelStore[2]` and
only `[0]` (pack) has a carrier — which the applier does write (`PipeApply.cpp:1373`) — so the
whole field reads as unmigrated while the half a readback needs is fine. Splitting it is what
stops the readback path from taking a whole-field `Fatal` for a half it never touches. The five
Espryt read sites whose `isUnpack` argument decides which half they want are
`DirectGLES.cpp:7924`, `:9399`, `:10893`, `:11272` and `Utils.cpp:2302`; **the scout named them
and did not open them**, so p1 reads the argument at each before it writes the two rows.
### The FATAL rows
Three non-sticky fields, each off the reduced path for a different, checkable reason:
| field | why it is FATAL rather than pulled |
|---|---|
| `GetBoundTransformFeedbackName` | **dead** — read by no backend since D21 (`PipeInputs.h:232-234`). |
| `GetTransformFeedbackPausedPrimitiveCounter` | reachable only from class `kQuery`, which the reduced path never enters. |
| `GetProgramForDispatch` | reachable only from `kDispatch`; there is no compute on the reduced path. Sites: `DirectGLES.cpp:5779`, `VulkanRenderer.cpp:7327`, `:7379`. |
Plus **`MGPipeUnmigratedEmulation`'s five call sites**, which in a split build stop being
`(void)name;` and become `Fatal`: `Managers.cpp:5334` ("texture-remint-pull"),
`DirectGLES.cpp:8051` ("generate-mipmap-storage"), `:8702` ("generate-mipmap-cpu-fallback"),
`:8997` ("copy-image-shadow-mirror"), `:10623` ("get-tex-image-shadow"). **One function grows
teeth and five sites get them** — `PipeApply.cpp:2820`, today a no-op, which
`PipeApply.h:1055-1066` and `PipeApply.cpp:2812-2817` both already say is waiting for this
phase. None of the five is on the reduced path. (ROADMAP's P4a row says six sites; there are
**five calls and one comment** — `Managers.cpp:5287` is the comment.)
### The seven sticky forwards
`GetBufferBindingPointCount`, `GetProgramObject`, `GetTextureObject`,
`HasOpenTransformFeedbackSpan`, `ValidateProgramName`, `InvalidateCompileEnv`, `RecordError`
(`PipeInputs.h:569-577`; identity asserted `:135-137`; argued `Coverage.def:138-150`).
P5's assignment:
- the first five → **BARRIER-PULLED** (counted in `rsp`, `Fatal` under strict);
- `InvalidateCompileEnv`**`OnCapsInvalidated`**, i.e. the re-arriving caps snapshot (R-12);
- `RecordError`**BARRIER-PULLED**, counted in `rsp`; its *ordering* is P9's (`OnGlError`).
**And their exemption is cancelled in a split build.** Today F-class accessors carry no
`MGP_INPUT_CHECK` at all (`PipeInputs.h:563-568`) and `MGPipeInputFieldIsFresh`
(`generated/PipeFilled.inc:418-426`) answers "fresh" for a sticky field regardless of
generation. That is exactly backwards for this phase: these seven are the ones that hand a
**frontend object or a frontend write** straight to the backend, so the exit gate "an
unmigrated field read is Fatal" is structurally blind on the seven most dangerous fields.
Under `MOBILEGL_BUILD_DISAGGREGATED` the exemption is lifted so they enter `rsp`, and under
`MOBILEGL_IPC_STRICT_ERRORS=1` they become `Fatal` like any other BARRIER-PULLED row.
### The prerequisite nobody else owns: someone must stamp
`MGPipeApplyAccess` **deliberately does not stamp** the poison generations
(`PipeInputs.h:612-618`): *"a stamp says the filler published this for THIS verb, which is the
walk's statement, not the applier's"*. Under split the filler is in the other role, so nothing
stamps, every `FilledGen[]` stays 0, `MGPipeInputFieldIsFresh` returns false for **everything**,
and a purely-server-side read aborts on the **first** field inside `SyncRenderState`
`Fatal{UnmigratedPipeInput, "GetRenderStateParameters@<none>"}` — long before any interesting
case. **The server stamps at the verb boundary**: p1 defines what is stamped and for which
verb, v1 places the call (`Server/PipeApplier::StampVerbBoundary`). Neither half works alone,
and this is not in the ROADMAP row.
### Two sets with no field ids
**The conservative client GPU-write set.** One row per backend `MarkGpuWritten` site, mapped to
the client predicate that must fire, one unit case each:
| backend site | what it marks | when |
|---|---|---|
| `DirectGLES.cpp:570` | every SSBO binding point | draw/dispatch about to go out |
| `DirectGLES.cpp:618` | every bound atomic counter | every conformance case reads the increment back |
| `DirectGLES.cpp:2603` | buffer textures on image units, **only when `Access != GL_READ_ONLY`** | from draw preparation, deliberately not from `glBindImageTexture` |
| `UniformManager.cpp:1075` | storage texel buffer, `Access != GL_READ_ONLY` | after `EnsureGpuResidentStorage()` |
| `UniformManager.cpp:1231` | SSBO block, unconditional | after `EnsureGpuResidentStorage()` |
| `VulkanRenderer.cpp:11618` | the XFB capture targets | "the capture is a GPU write like any shader's" |
Plus **two new producers P5 adds**, both client-side with no server participation
(`ARCHITECTURE.md:508`): `glReadPixels` into a pack PBO becomes fire-and-forget plus a client
`MarkGpuWritten` (strictly better than monolith's unconditional stall), and
`glEndTransformFeedback` drops its unbounded fence wait and marks the capture targets instead.
`SyncGpuWrites` gains a **third state** it cannot express today — *emitted, answer not back*
and under split it must **block until `OnBufferWriteback` lands** rather than clear the flag
optimistically (`BufferObject.cpp:372-374` clears unconditionally; `ARCHITECTURE.md:509` lists
this among the unavoidable blocking points, because monolith already `glFinish()`es here).
**No narrowing in P5**: `ResourceTracker.h:587-592`'s `rangeCount == 1` assertion **stays**.
**The persistent-map reachable set: the census is 21 sites, not 20.** `MEASUREMENTS.md:111`
records 20 and `Managers.cpp:5047-5048` speaks of "the eleven Espryt" sites; the actual count
is **9 Espryt + 12 Magma = 21**, and Espryt's own count is 12 (9 + 3 `SyncGpuWrites`), so both
published numbers are exactly one low and the missing one is an Espryt line. **Ruling: 21 is
the number, and `DirectGLES.cpp:361` (`ResolveIndirectCommandBytes`) is in the set.** It is a
shared helper rather than a draw-path site, which is the most likely reason it was excluded,
but a helper that reaches a persistently-mapped range is exactly as able to read stale bytes as
a draw site is; excluding it would be a shutter that cannot see its own subject. The
per-site attribution table `ARCHITECTURE.md:290` refers to as "§5.7" **does not exist in the
tree** — b1 should not go looking for it. The nine Espryt sites: `DirectGLES.cpp:361`, `:6185`,
`:6439`, `:6440`, `:6541`, `:6542`, `Managers.cpp:2817`, `:2994`, `MultiDraw.cpp:511`. The
twelve Magma: `DirectVulkan.cpp:281`, `:472`, `:796`, `UniformManager.cpp:2024`,
`VulkanRenderer.cpp:3542`, `:3621`, `:4013`, `:7428`, `:12423`, `:12424`,
`VkBufferManager.cpp:628`, `:679`. **Correcting `MEASUREMENTS.md:111` from 20 to 21 is b1's
line to write**, in the same commit that lands the tracker.
`m_livePersistentMaps` is defined by `SyncPersistentMappedRange`'s own early-out chain read as
a membership test (`BufferObject.cpp:346-349`): persistent, write, **not** flush-explicit,
**not** GPU-resident.
---
## §4 Table 3 — role and thread ownership of every process global
`ARCHITECTURE.md:578-581` claims MGPipe brings the globals a role split must duplicate down
from four to two. **That census is at least seven short.** Every row below also answers what
make-current and teardown do to it — the column `ARCHITECTURE.md` does not have.
| global | where | who writes | P5 ruling | make-current / teardown |
|---|---|---|---|---|
| `gPipeInputs` (~20 KB) | `PipeInputs.h:706` | client residual fill (`PipeFill.cpp:530`, `:2131`, `:2604`) + applier (`PipeApply.cpp:1336`, `:1364`, `:1373`, `:1377`, `:1436`, `:1512`) | **One instance is legal, but only under the verb barrier.** The barrier makes at most one of {GL thread, apply thread} runnable at a time, so there is exactly one writer at any instant. **No second writer may be introduced before the barrier retires.** The invariant is a runtime assertion in debug/verify builds, not only a sentence here: the apply thread raises a flag on entering the applier and the client checks it when it touches `gPipeInputs` outside a barrier (`ClientSession::InBarrierWait` / `ApplyThreadIsInsideApplier`). | make-current: unchanged. teardown: nothing — it is POD in the image. |
| `g_applier` | `PipeApply.cpp:396` | applier | **Server-exclusive.** Its own header already says "under split there is one per served context" (`PipeApply.h:684`). | It is `*new MGPipeApplierState{}` and never destroyed, deliberately (`PipeApply.cpp:392-395`): `resource_destroy` is raised from `~BufferObject`, which runs from exit handlers after this TU's globals are gone. |
| `g_resourceOps` | `PipeApply.cpp:402` | the backend, at register time | **Server-exclusive, and the client must NEVER read it** (R-8). Under `inproc` a client reading it is right *by accident*; under spawn it is null and the four P4a families plus P3a's buffers emit **nothing at all**, silently. The client asks `CapsMirror::ServerConsumes` instead. | registered around `DirectGLES.cpp:11933`, nulled from `OnBackendContextDestroyed` (`Managers.cpp:2584`) — so it moves on every context loss, which is another reason the client cannot key on it. |
| `gMGPipeSegmentResolver` | `MGPipeHostSpan.h:47` — a **plain non-atomic inline variable** | `MG_Remote` installs | **One process-wide slot, so it cannot be per-role.** Ruling: **the server role installs it and the client never resolves a span at all** — the client only ever *writes* `Ptr = nullptr`. `SegmentTable::InstallProcessResolver()` asserts if one is already installed, so two roles racing on it is loud rather than silent. Install **before** the apply thread starts. | teardown: uninstall after the join, never before — a record still in flight can still resolve. |
| the ten `MG_Impl/Pipe` `*Instance()` singletons | `fable-seam-audit.md:120-135` | client | **Client-exclusive.** One correction carried forward: the texture **drain list `m_drain` is process-wide**, not per-context as D-D4 claims; the audit already booked "one drain per client context" as a P5 item. | `FreshlyPrimed` (`PipeFill.cpp:2414-2440`) resets them on make-current; leak at exit (ID-8). |
| `ScopedDefaultUnpackState::s_synced` + **six** value shadows | `Managers.cpp:5490-5496` | backend | **Server-exclusive — the sixth global the four→two census missed.** Latent rather than live in P5 only because the client role never touches GL on the reduced path. (`Managers.cpp:5491-5496` is six `GLint`s, not five: `s_skipImages` at `:5496` is the one usually dropped.) | **Never reset on context death**`OnBackendContextDestroyed` resets the rings and the binding caches and not this. Benign while a lost context returns the driver to GL defaults; not benign the day a server re-attaches to a context something else moved. Register it now. |
| `pActiveBackendObject` | `GlobalObjects.cpp:23` | `MG_Backend::Init()` | **Client installs `BackendObject_Remote`; the server's `BackendObject_DirectGLES` is held privately by `ServerLoop`.** No thread-keyed shim, and therefore `MOBILEGL_BUILD_DISAGGREGATED_INPROC` needs none — but the cost is **seven** backend-internal reads across **six** functions, not the one the scout reported: `BackendObject_DirectGLES.cpp:815`, `:819` (`ClampSamplesToBackendSupport`) and `Utils.cpp:74`, `:82`, `:126`, `:220`, `:260`. All seven are format-capability lookups, so "pass the format cache down" still works. **`DirectGLES.cpp:12446` is NOT `ClampSamplesToBackendSupport`** — it is `Present()`'s fence poll, and `DirectGLES.cpp` contains no `pActiveBackendObject` reference at all. | `GetFormatCapabilities()` is **non-virtual** (`BackendObject.h:594`), so the remote object must **fill** `m_formatCapabilities` rather than override the accessor. Teardown: `pActiveBackendObject.reset()` (`MobileGL/Init.cpp:68`) runs `~BackendObject_DirectGLES``DestroyEGLContext()`, so under split it must be a **blocking** request onto the apply thread. |
| `gBackendFunctionsTable` | `GlobalObjects.cpp:24`, assigned `Init.cpp:44` | `MG_Backend::Init()` | **Client = the emit table (R-4); the server holds its real table directly and never goes through this global.** | cleared at `MobileGL/Init.cpp:91`. |
**Teardown order**, `ARCHITECTURE.md:537` plus the sentence it omits:
1. client publishes and waits for the server to drain and acknowledge;
2. **`Doorbell::Kill()`** — *the only thing that can wake an apply thread parked on
`kWaitForever`* (`Doorbell.h:211-221`; the shape is already pinned by
`InProcessTransportTest.cpp:344`);
3. **join**, bounded (that test uses 5 s) so a regression is a red test and not a hung CI job;
4. only then may the client free anything an emitter owns — a tail still referenced by an
unapplied record is a use-after-free the join is what prevents;
5. then the existing order (`MobileGL/Init.cpp:38-98`).
**ID-8 applies once per role-local singleton, not once overall**: every new
`MG_Remote/Client/*` and `MG_Remote/Server/*` singleton leaks at exit. The proof recipe is
inherited: both lanes run `GLIBC_TUNABLES=glibc.malloc.tcache_count=0`.
**Known open item, flagged not resolved.** `ARCHITECTURE.md:537`'s required order puts the
client's sync/query handle release **after** the transport closes, while today
`DestroyAllSyncObjects` / `DestroyAllQueryObjects` (`MobileGL/Init.cpp:62`, `:67`) deliberately
run **before** `pActiveBackendObject.reset()` (`:68`). The two are only reconcilable if a split
sync handle is client-minted and needs no backend call — which is P10's, not P5's. **P5 keeps
today's order** and v1 records which way it went.
---
## §5 The knobs
Parsed in `ConfigLoader.cpp`, declared in `Config.h`. All of them live behind
`#if MOBILEGL_BUILD_DISAGGREGATED` — including the parser — because `MG_ConfigLoader::Init()`
is a pull-build symbol and G1 admits **no resize**, which is the same reason the
`MOBILEGL_PIPE_VERIFY` knobs sit behind their own `#if`.
| knob | default | notes |
|---|---|---|
| `MOBILEGL_TRANSPORT` | `monolith` | `monolith\|inproc\|spawn\|unix:<path>\|pipe:<name>`. The three P6 forms **parse and are then refused by name**, staying on monolith: a P6 lane that set `spawn`, fell back silently and went green on the wrong arm is the failure this wording avoids. |
| `MOBILEGL_IPC_SERVER_PATH` | `""` | P6 consumes it; P5 parses it because t1's ctest `ENVIRONMENT` blocks and `add_trace_replay_test`'s SPLIT variant already carry it, and an unparsed variable is indistinguishable from a parsed-and-ignored one. |
| `MOBILEGL_IPC_RING_MB` | 8 | SEG_CMD. **One record may be at most half of this** (`RingProducer::MaxRecordBytes`), so 8 MiB caps a record at 4 MiB. R-10 makes the codec publish a max-record-bytes counter rather than assume that is enough. |
| `MOBILEGL_IPC_STAGE_MB` | 32 | SEG_STAGE. Every blob and every var-tail's bytes. |
| `MOBILEGL_IPC_SPIN_US` | 50 | spin before parking, either direction. |
| `MOBILEGL_IPC_PERSISTENT_BLOCK_KB` | 64 | **0 is the E3(a) negative control, not "unlimited"**: it turns the push off and `PersistentCoherentMapScenario` must go red. |
| `MOBILEGL_IPC_ADOPT_TIER` | 2 | 2 = emulate, the only tier P5 implements. 0 and 1 parse and are `Fatal` at use, naming P11. |
| `MOBILEGL_IPC_VERB_BARRIER` | 1 | 0 is R-1's negative control and is **expected** to be red. |
| `MOBILEGL_IPC_STRICT_ERRORS` | 0 | promotes BARRIER-PULLED reads — and, in a split build, the seven sticky forwards — to `Fatal`. |
| `MOBILEGL_IPC_AUDIT` | 0 | `0xDD` over retired staging bytes (rule C's mechanical control). |
| `MOBILEGL_IPC_SERVER_AFFINITY` | `auto` | kept as the raw string; whoever starts the apply thread logs the **resolved mask**, because an affinity that silently did nothing looks exactly like one that worked. |
**One consequence, stated so it is not rediscovered.** In a build *without*
`MOBILEGL_BUILD_DISAGGREGATED`, `MOBILEGL_TRANSPORT=inproc` is accepted by the environment and
**silently ignored** — the parser does not exist there, and putting a complaint in the
unconditional part of `ConfigLoader` would move a pull-build symbol and break G1. That is
precisely the shape of "the split lane ran monolith and went green", so the guard against it is
a **build-level** check, not a runtime one: `nm --defined-only libMobileGL.so | grep -i
MG_Remote` must be non-empty in `build-split`, and it is t1's CI job to assert that.
CMake gained `MOBILEGL_BUILD_DISAGGREGATED_INPROC` (implies `DISAGGREGATED`) and, new here,
**`MOBILEGL_BUILD_DISAGGREGATED` now implies `MOBILEGL_PIPE_PUSH`**: the split path decodes
into the MGPipe applier and `MOBILEGL_PIPE_PUSH` is what compiles the applier, so
`-DMOBILEGL_BUILD_DISAGGREGATED=ON` alone used to configure cleanly and then fail to link — a
shape indistinguishable at the CMake level from a legitimate transport-only build.
---
## §6 Rulings this file makes that the brief did not, and where the brief is wrong
Each entry says what would overturn it.
1. **`CallMask` bits 32..47 are the consumer mask.** R-8 says the client's liveness gates read
the `CallMask` mirror, but `CallMask` as declared has only nine feature bits and no
per-family bit, so R-8 was not implementable as written. Overturned by: a decision to carry
a second mask field in `CapsSnapshot` instead — which costs a schema field and gains
nothing, since 16 bits is enough through P8.
2. **`tableSlotMask` is deleted, not renamed** (R-8 allowed either). Decisive evidence:
`GLFunctionsTable` has **69** slots and `ulong` is 64 bits, so the field cannot address the
table its own comment names. Overturned by: widening the schema field *and* a reason to
keep an explicit slot probe after `ARCHITECTURE.md:114` retired the concept.
3. **`ResourceFlushRange` carries no bytes at all** (R-13.2 offered "add a blobref" or "write
the convention down"; this is a third answer, and a stronger one). The ladder it drives
rewrites from the authoritative shadow, which under rule C is server-owned, so
`resource_subdata` is already the only path bytes take. Overturned by: evidence that the
tier-1 `INVALIDATE_RANGE` arm needs bytes and range in one record. Cannot arise while the
verb barrier holds; **revisit when the barrier retires for the buffer family.**
4. **`ResourceRespecify` also has a SECOND uncarried companion, and the brief does not mention
it.** `const MGPRespecifiedLevel* level` (`PipeApply.h:792-795`) is the *scope* of the
redefinition and `MGPResourceDesc` cannot express it. Without a carrier every per-level
`glTexImage2D` in OpenRA silently takes the whole-resource arm and drops every pending
upload — the exact texel loss the server-side set exists to prevent. Ruling: two named
fields in the existing pads, zero size change. **c0 rules and specifies; the integrator
lands the `MGPipeTypes.h` + `PipeFields.def` edit before w1 encodes this record.**
5. **Table 1 is 23 rows, not 19.** The brief's 19 and `scout-premortem:§3`'s 19 are different
lists; the four only the premortem carries are the server → client ones, and a phase that
omits them discovers in week three that it never decided where readback pixels land.
6. **`SetResidualValueState` is a fourth typed companion, and neither scout nor the brief names
it.** `MGPipeApplySetResidualValueState` takes `const ResidualValueBlock&` — not a payload,
not a `const void*` — and `MGPResidualValueState` is **never instantiated on the live path**.
The encoder has to invent both the record fill and the blob fill. Budget it as w1's hardest
row, not as one of the easy `kHasBlob` eight.
7. **Three of the brief's 19 have no applier entry point at all**`SetShaderBuffers` (38),
`SetStreamOutputTargets` (39), `DrawVbo` (59). `scout-premortem:§3` cites
`PipeApply.h:756, 941, 1028-1030` for a six-call row; those five citations cover five *other*
calls. For these three, "what crosses today" is **nothing**, and P5 writes the first producer
*and* the first consumer.
8. **`CreateRenderState` and `SetDynamicState` declare a `Blob.Size` that nothing ever reads.**
`scout-wire-codec:§4.2`'s "`Size = 0`" column is stale for four rows (add
`CreateVertexElements` and `ResourceSubData`'s buffer half). The applier's only four
`Blob.Size` reads are `PipeApply.cpp:702`, `:1998`, `:2353`, `:2784`. A fifth
flags-vs-payload-vs-signature disagreement for the reviewer's list.
9. **The emit table is 71 function pointers, not 69.** R-4 says 69 slots; that is
`GLFunctionsTable`'s count. The table the client actually installs is
`GlobalBackendFunctionsTable` = 69 + `Present` + `SetSwapInterval`. R-4's rule (no null slot,
no pass-through) applies to all 71, and `Present` is on the reduced path. The Bool member is
not a verb and is answered from `kCapCpuXfbPrimitiveAccounting`.
10. **`prefersCpuXfbPrimitiveAccounting` is a member of `GLFunctionsTable`
(`BackendObject.h:274`), not of `DynamicBackendParameters`.** So it does **not** ride inside
`MGPCaps::Dynamic`, and R-8's "same redundancy as (6)" is the wrong frame — it has three
spellings and no carrier in `MGPCaps` except the cap bit. Its one non-test client reader is
`GL_Query.cpp:221`.
11. **The persistent-map census is 21 sites, and `MEASUREMENTS.md:111`'s 20 is wrong.** Ruling
and the missing site named above. **`ARCHITECTURE.md`'s cited "§5.7" attribution table does
not exist in the tree.**
12. **`ScopedDefaultUnpackState` has six value shadows, not five** (`Managers.cpp:5491-5496`);
both the brief and the scout say five.
13. **`CanTouchGLNow()` guards 16 call sites, not 19.** 19 is the raw grep: 1 definition
(`Managers.cpp:928`) + 2 comment mentions (`:1967`, `:1979`) + 16 calls. The brief's "19
sites" over-counts. `IsBackendContextCurrentOnThisThread`'s 16 is right.
14. **`MGPipeApply*` is 37 entry points and 41 call sites**, not "~45 entry points" — and
`DirectGLES.cpp:12446` is `Present()`'s fence poll, **not** `ClampSamplesToBackendSupport`
(which is `BackendObject_DirectGLES.cpp:807-828`). Table 3's `pActiveBackendObject` row is
still correct but the diff is six functions, not one line.
15. **The 18 `build-split` unit aborts are not a poison problem.** They are
`Fatal{ProtocolCorruption}` trip wires the tests *expect*; seven test TUs test
`MOBILEGL_PIPE_POISON` without including the only header that defines it, so the macro reads
as 0 and they compile the "logs and carries on" arm while `PipeApply.cpp` compiles the
aborting one. Invisible in a push build (where it really is 0) and in a verify build (where
`-DMOBILEGL_PIPE_VERIFY=1` is on the command line); `MOBILEGL_BUILD_DISAGGREGATED` is the one
arming condition behind the header. Fixed in c0's own commit, test-local, no p1 surface.
16. **`MGHostSpan`, not `MGPHostSpan`.** The header is `MGPipeHostSpan.h`; the struct is
`MGHostSpan` (`:28`). `MGPHostSpan` does not exist.
17. **`ARCHITECTURE.md`'s own corrections, carried here so they are not lost**: `:83` says 61
`PipeInputs` fields, it is 63; `:492` cites `PipeStats.h:126` for
`MapPersistentRoundtrips`, it is `:141`; ROADMAP's P4a row says six
`MGPipeUnmigratedEmulation` sites, it is five calls plus one comment; `ARCHITECTURE.md:19`
says eight EGL lifecycle virtuals, there are nine (`ResizeEGLWindowSurface` is the
uncounted one).
---
## §7 R-15 — getter-shaped slots are answered locally, and the emit table's three classes
**R-15 (integrator ruling, made after the verb census).** A `GLFunctionsTable` slot whose answer
is a **static property of the server's device** is answered on the client **from the caps
mirror**. It is never emitted and never `Fatal`. The gate already exists and already runs on
every lane: `AdvertisedLimitsScenario.ComputeWorkGroupLimitsAreTheCapsBlocksAnswer`
(`MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp:580-623`) pins that the caps copy and
`glGetIntegeri_v` give one number.
This settles the census's sharpest finding: `GetIntegeri_v` is reached by the **first
`glCompileShader` of every context** (`CompileEnv.cpp:134-138``Core.cpp:39`), not by any verb,
so an all-`Fatal` table would abort every scenario before it drew anything — and an emitter for
it would be a round trip for six constants the snapshot already carries.
### The three classes of the 71 slots. c1 does not re-derive this.
**Class A — answered locally from the caps mirror (2 slots). No record, ever.**
| slot | answered from |
|---|---|
| `GetIntegeri_v` (`BackendObject.h:205`) | `MGPCaps::Dynamic.MaxComputeWorkGroupCount` / `MaxComputeWorkGroupSize` (`BackendObject.h:392-393`) — the only indexed pnames the device owns. Every other indexed pname is frontend state and is answered before any table is consulted. |
| `IsTimerQuerySupported` (`:245`) | `kCapTimerQuery` (`MGPipeTypes.h:114`). A capability predicate, not a call: today a null slot means `COUNTER_BITS = 0` (`GL_Query.cpp:792`). |
`GLFunctionsTable::PrefersCpuXfbPrimitiveAccounting` (`:274`) is in the same class by the same
argument — `kCapCpuXfbPrimitiveAccounting` — and is not a slot.
**Class B — emitted in P5 (5 slots).** The verb census's answer, and nothing else:
`Clear`, `DrawArrays`, `ReadPixels`, `BlitFramebuffer`, `Present`.
`Present` is in this class despite having **zero `MG_Impl` call sites** — it is reached through
`EGLImpl.cpp:178``BackendObject.cpp:396`, so c1 cannot find it by mirroring GLImpl.
**Class C — `Fatal{UnmigratedVerb, "<slot>"}` (64 slots).** Everything else, including
`SetSwapInterval`, `GetGpuTimestampNs` (a live GPU timestamp, not a static property, so **not**
class A), and the whole sync / query / transform-feedback / compute / copy / mipmap surface.
### The cross-cutting rule R-4 would otherwise break
**Forty-one of the 69 slots are null-checked at their call site, and several of those null checks
are CAPABILITY PROBES rather than safety checks.** R-4 forbids a null slot — so in the emit table
every one of those probes answers "supported" and the fallback behind it silently disappears.
That is not a theoretical risk: it is how a split lane produces a plausible picture for the wrong
reason. Three named cases; the rule generalises to all 41.
| probe site | what it decides today | reads instead |
|---|---|---|
| `GL_Query.cpp:481`, `:785``BeginOcclusionQuery != nullptr` | whether the target is rejected outright | `kCapOcclusionQuery` |
| `GL_Query.cpp:534` — the `BeginXfbPrimitivesQuery` ternary | GPU query vs CPU primitive accounting | `kCapXfbPrimitivesQuery` |
| the `SubDataResident` op-table slot | whether the resident-upload path exists at all | `kCapResidentSubData` |
**A null check on a slot may never survive into the client under split.** It becomes a caps-mirror
read — class A's mechanism — whatever class the slot itself is in. That is exactly
`ARCHITECTURE.md:114`'s "`CallMask` replaces 'is this table slot null' as the implicit capability
probe", now with a concrete list of what has to move.
---
## §8 Ownership amendments
- **`MobileGL/MG_Pipe/MGPipeTypes.h` is c0's file** (integrator ruling A; the BRIEF §5 ownership
table is amended). It was unowned, which is how the respecify-scope gap in table 1 row 19b had
no one to close it. A package that needs a payload struct shape changed goes through the
integrator, as with the three `.def` files.
- Consequently the row-19b carrier is **landed, not merely specified** — see §2 table 1 row 19b
and `MGPipeTypes.h`'s `HasRespecifiedLevel` / `RespecifiedUploadTarget` / `RespecifiedLevel`
and the five `MGPipeRespecify*` helpers beside them.
+56
View File
@@ -0,0 +1,56 @@
// MobileGL - MobileGL/MG_Remote/CapsCodec.cpp
// 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
#include "CapsCodec.h"
#include <MG_Util/Debug/Log.h>
#include <cstdlib>
namespace MobileGL::MG_Remote {
// The consumer mask may not collide with the MGPCapBits below it. kCapNeedsHostUboBytes
// is 1<<8 today; this asserts the gap stays a gap rather than trusting the comment.
static_assert((static_cast<Uint64>(MG_Pipe::kCapNeedsHostUboBytes) & kMGCapsConsumerMask) == 0,
"an MGPCapBit has grown into CallMask's consumer block (bits 32..47)");
static_assert(MGCapsServerConsumes(MGCapsConsumerBits(MG_Pipe::kMGPipeSubsystemResources),
MG_Pipe::kMGPipeSubsystemResources),
"the consumer encoding does not round-trip");
static_assert(!MGCapsServerConsumes(MGCapsConsumerBits(MG_Pipe::kMGPipeSubsystemResources),
MG_Pipe::kMGPipeSubsystemPrograms),
"the consumer encoding answers yes for a family it was not given");
// P4a's highest allocated subsystem bit must fit the sixteen-bit block. This is the
// assertion that turns "room to P8" from a comment into a build break.
static_assert(MG_Pipe::kMGPipeSubsystemsMigratedAtP4a <= 0xFFFFull,
"the subsystem mask no longer fits CallMask's sixteen consumer bits");
#define MGP5_C0_STUB(what) \
do { \
MGLOG_F("MGPipe: Fatal{UnimplementedCapsCodec, \"%s\"} - P5 package w1 has not landed " \
"this yet; c0 shipped the signature only", \
what); \
std::abort(); \
} while (0)
Bool EncodeFormatCapabilities(const MG_Backend::FormatCapabilityCache&, Vector<Uint8>&) {
MGP5_C0_STUB("EncodeFormatCapabilities");
}
Bool DecodeFormatCapabilities(const void*, Uint64, MG_Backend::FormatCapabilityCache&) {
MGP5_C0_STUB("DecodeFormatCapabilities");
}
Bool EncodeRendererInfo(const RendererInfo&, Vector<Uint8>&) { MGP5_C0_STUB("EncodeRendererInfo"); }
Bool DecodeRendererInfo(const void*, Uint64, RendererInfo&) { MGP5_C0_STUB("DecodeRendererInfo"); }
Uint64 CapsAbiFingerprint() { MGP5_C0_STUB("CapsAbiFingerprint"); }
#undef MGP5_C0_STUB
} // namespace MobileGL::MG_Remote
+99
View File
@@ -0,0 +1,99 @@
// MobileGL - MobileGL/MG_Remote/CapsCodec.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 MGPCaps snapshot's serializers, and the CallMask encoding. Owner: package w1 (the two
// blob codecs) and s1 (the handshake asserts). Signatures by c0.
//
// MGPCaps (MG_Pipe/MGPipeTypes.h:126-139) has four members. Two are flat and cross by memcpy
// (DynamicBackendParameters Dynamic, Uint64 CallMask); two are MGPBlobRefs over containers
// and HAVE NO SERIALIZER IN THE TREE - the header says so itself at MGPipeTypes.h:134-136,
// "Their serializers land with the transport (P5)". They are:
//
// FormatCapabilities -> FormatCapabilityCache (MG_Backend/BackendObject.h:93-98):
// FullCaps + CaveatCaps (bitfield tables) + SampleCounts, a Vector<Int> per
// (target, format) pair. The Vector is why this cannot be a memcpy.
// RendererInfo -> RendererInfo (MG_Util/Types.h:317): three Strings, an
// Optional<String>, and a Vector<GLExtension> inside GLInfo.
//
// THE ABI RULING (table 0). MGPCaps has only a COMPOSITIONAL size assertion
// (MGPipeTypes.h:145-146) because DynamicBackendParameters still carries SizeT and GLenum
// members - P0.5's fixed-width rewrite did not happen. P5 does NOT rewrite it. Instead the
// handshake asserts that both peers agree on sizeof(DynamicBackendParameters),
// sizeof(MGPCaps) and the build fingerprint, and Fatal{AbiMismatch} otherwise. P6's spawn is
// same-machine and same-binary, so it inherits this unchanged; the fixed-width rewrite is on
// P7's account.
#pragma once
#include <Includes.h>
#include <MG_Backend/BackendObject.h>
#include <MG_Pipe/MGPipe.h>
namespace MobileGL::MG_Remote {
// ---- CallMask's layout (c0's ruling, extending R-8) ---------------------------------
//
// R-8 requires the client's liveness gates - MGPipeResourceSubsystemEnabled() and
// P4aFamilyHasItsConsumer() - to answer from MGPCaps::CallMask instead of from
// MGPipeGetResourceOps(), because that op table is the SERVER's registration: under
// inproc a client reading it is right by accident, and under spawn it is null and five
// whole record families silently emit nothing.
//
// But CallMask as declared carries only the nine MGPCapBit FEATURE bits
// (MGPipeTypes.h:108-124) and has no per-family bit at all, so "read the CallMask" was
// not yet an implementable instruction. It is now:
//
// bits 0..8 MGPCapBit, unchanged. kCapNeedsHostIndexBytes and kCapNeedsHostUboBytes
// are BOTH ZERO in P5 by ruling (table 0), which is what keeps every
// MGHostSpan out of the first IPC frame.
// bits 9..31 reserved for further MGPCapBits.
// bits 32..47 THE CONSUMER MASK: bit (32 + n) means "the server has a consumer for
// MGPipe subsystem bit n" - i.e. the server's own subsystem mask, shifted.
// Sixteen bits covers bits 0..12 allocated through P4a with room to P8.
// bits 48..63 reserved.
//
// protocol.fbs:94's `tableSlotMask: ulong` is DELETED rather than renamed (R-8 offered
// either). Two reasons, and the second is decisive: ARCHITECTURE.md:114 says CallMask
// REPLACES "is this table slot null" as the capability probe, so a field whose comment is
// "which GLFunctionsTable slots the peer registered" re-introduces exactly what it
// replaced; and GLFunctionsTable has SIXTY-NINE function-pointer slots
// (BackendObject.h:117-292), so a 64-bit mask cannot address it and never could.
inline constexpr Uint32 kMGCapsConsumerBitShift = 32;
inline constexpr Uint64 kMGCapsConsumerMask = 0xFFFFull << kMGCapsConsumerBitShift;
// Server side: fold the subsystems this server actually consumes into a CallMask.
inline constexpr Uint64 MGCapsConsumerBits(Uint64 subsystemMask) {
return (subsystemMask & 0xFFFFull) << kMGCapsConsumerBitShift;
}
// Client side: the ONE legal spelling of "does the server consume this family".
// `subsystemBit` is a kMGPipeSubsystem* constant (MG_Pipe/MGPipe.h), not an index.
inline constexpr Bool MGCapsServerConsumes(Uint64 callMask, Uint64 subsystemBit) {
return (callMask & MGCapsConsumerBits(subsystemBit)) != 0;
}
// ---- the two blob codecs ------------------------------------------------------------
//
// Byte-stable within one build; the handshake's fingerprint is what makes that enough.
// Both decoders must tolerate a truncated or over-long buffer by returning false, never
// by reading past `size`: these bytes arrive over the wire.
Bool EncodeFormatCapabilities(const MG_Backend::FormatCapabilityCache& cache, Vector<Uint8>& out);
Bool DecodeFormatCapabilities(const void* bytes, Uint64 size, MG_Backend::FormatCapabilityCache& out);
Bool EncodeRendererInfo(const RendererInfo& info, Vector<Uint8>& out);
Bool DecodeRendererInfo(const void* bytes, Uint64 size, RendererInfo& out);
// ---- the ABI assertion the handshake carries ----------------------------------------
//
// Mixes sizeof(DynamicBackendParameters), sizeof(MGPCaps), sizeof(GLFunctionsTable) and
// the compile-time build fingerprint. Compared in Hello/Welcome; a mismatch is
// Fatal{AbiMismatch} and never a downgrade, because every alternative silently reads one
// struct as another.
Uint64 CapsAbiFingerprint();
} // namespace MobileGL::MG_Remote
+63
View File
@@ -0,0 +1,63 @@
// MobileGL - MobileGL/MG_Remote/Client/CapsMirror.cpp
// 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
// P5 c0 stubs for package c1. Every body is MGLOG_F + std::abort and never a silent no-op: a
// caps accessor that answers a default is how a split lane runs on the wrong device's limits.
#include "CapsMirror.h"
#include <MG_Util/Debug/Log.h>
#include <cstdlib>
namespace MobileGL::MG_Remote::Client {
#define MGP5_C0_STUB(what) \
do { \
MGLOG_F("MGPipe: Fatal{UnimplementedCapsMirror, \"%s\"} - P5 package c1 has not landed " \
"this yet; c0 shipped the signature only", \
what); \
std::abort(); \
} while (0)
void CapsMirror::Adopt(const MG_Pipe::MGPCaps&, const MG_Backend::FormatCapabilityCache&,
const RendererInfo&, const String&, BackendType) {
MGP5_C0_STUB("CapsMirror::Adopt");
}
// Not stubs: the two the placeholder contract above promises are readable before the first
// snapshot. Everything else aborts, so nothing can accidentally answer from a zeroed mirror.
Bool CapsMirror::Valid() const { return m_generation != 0; }
Uint64 CapsMirror::Generation() const { return m_generation; }
const RendererInfo& CapsMirror::Renderer() const { MGP5_C0_STUB("CapsMirror::Renderer"); }
const MG_Backend::DynamicBackendParameters& CapsMirror::Dynamic() const {
MGP5_C0_STUB("CapsMirror::Dynamic");
}
const MG_Backend::FormatCapabilityCache& CapsMirror::Formats() const {
MGP5_C0_STUB("CapsMirror::Formats");
}
const String& CapsMirror::ApiVersion() const { MGP5_C0_STUB("CapsMirror::ApiVersion"); }
BackendType CapsMirror::Backend() const { MGP5_C0_STUB("CapsMirror::Backend"); }
Uint64 CapsMirror::CallMask() const { MGP5_C0_STUB("CapsMirror::CallMask"); }
Bool CapsMirror::HasCap(MG_Pipe::MGPCapBit) const { MGP5_C0_STUB("CapsMirror::HasCap"); }
Bool CapsMirror::ServerConsumes(Uint64) const { MGP5_C0_STUB("CapsMirror::ServerConsumes"); }
Bool CapsMirror::PrefersCpuXfbPrimitiveAccounting() const {
MGP5_C0_STUB("CapsMirror::PrefersCpuXfbPrimitiveAccounting");
}
CapsMirror& CapsMirrorInstance() {
// ID-8: leak at exit. No frontend destructor may reach pipe or backend state from an
// exit handler, and that rule applies once per role-local singleton, not once overall.
static CapsMirror& instance = *new CapsMirror{};
return instance;
}
#undef MGP5_C0_STUB
} // namespace MobileGL::MG_Remote::Client
+100
View File
@@ -0,0 +1,100 @@
// MobileGL - MobileGL/MG_Remote/Client/CapsMirror.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's copy of the server's capabilities. Owner: package c1. Signatures by c0.
//
// WHY A MIRROR AND NOT A ROUND TRIP. There are 56 client-side caps read points
// (40 GetDynamicParameters + 7 GetRendererInfo + 4 GetFormatCapabilities + 3 GetBackendType +
// 2 GetBackendAPIVersionString), and several of them - GL_Getter.cpp:2400 and
// ShaderTranspiler/CompileEnv.cpp:120-124 - bind a reference and then read many members, so a
// partial snapshot is not an option. Every one of the 56 must be answerable locally, with no
// record on the wire.
//
// GetRendererInfo() RETURNS A REFERENCE (BackendObject.h:590), so the mirror must OWN a
// RendererInfo instance to hand back - including before the first snapshot arrives, because
// LogBackendInfo() reads it at MG_Backend/Init.cpp:21, during MG_Backend::Init(), long before
// any context exists. Ruling (scout-caps-reply §1.2 option (a)): the mirror answers with a
// placeholder until the first snapshot, P5 accepts one inaccurate startup log line, and
// MG_Backend::Init() is NOT restructured.
//
// GetFormatCapabilities() is NON-VIRTUAL (BackendObject.h:594), so a remote backend object
// cannot override the accessor: it must FILL BackendObject::m_formatCapabilities from this
// mirror instead.
//
// INVALIDATION IS RE-ARRIVAL (R-12). DirectGLES has no OnCapsInvalidated producer at all - it
// re-runs UpdateAdvertisedCapabilityExtensions + UpdateDynamicBackendParameters at
// BackendObject_DirectGLES.cpp:865-871 and tells the frontend nothing, which is correct in
// monolith and a silent bug under split. Rather than add a DirectGLES-side callback (a
// dev-shaped backend edit), the SERVER re-sends the whole snapshot on every InitCapabilities
// re-run and the CLIENT treats a second arrival as the invalidation. Generation() is what a
// client-side memo keys on, and it is also the re-open signal for the server-context-death
// case that MGPipeCallbacks has no eleventh slot for (MGPipeCallbacks.h:56-58).
#pragma once
#include <Includes.h>
#include <MG_Backend/BackendObject.h>
#include <MG_Pipe/MGPipe.h>
namespace MobileGL::MG_Remote::Client {
class CapsMirror {
public:
// Replaces the whole mirror and bumps Generation(). Called once per CapsSnapshot,
// including the re-sends that mean "invalidate" (R-12).
void Adopt(const MG_Pipe::MGPCaps& caps, const MG_Backend::FormatCapabilityCache& formats,
const RendererInfo& renderer, const String& apiVersion,
BackendType backend);
// False until the first snapshot. The placeholder answers below are still safe to
// read - that is the point - but a caller that can wait should.
Bool Valid() const;
// ++ on every Adopt. A client memo that survives a server context loss must key on
// this; nothing else on the client can see that the server's context died.
Uint64 Generation() const;
const RendererInfo& Renderer() const;
const MG_Backend::DynamicBackendParameters& Dynamic() const;
const MG_Backend::FormatCapabilityCache& Formats() const;
const String& ApiVersion() const;
// The SERVER's backend type, never a new "Remote" enumerator: frontend branches
// switch on this (GL_Framebuffer.cpp:47, GL_Texture.cpp:6536, CompileEnv.cpp:122) and
// a value they do not know silently takes the wrong arm.
BackendType Backend() const;
Uint64 CallMask() const;
Bool HasCap(MG_Pipe::MGPCapBit bit) const;
// R-8. `subsystemBit` is a kMGPipeSubsystem* constant. THIS IS THE ONLY LEGAL SOURCE
// of the answer on the client under split: MGPipeGetResourceOps() is the SERVER's
// registration and is null in the client process, which would silently disable the
// whole push path in the one mode that matters.
Bool ServerConsumes(Uint64 subsystemBit) const;
// GLFunctionsTable::PrefersCpuXfbPrimitiveAccounting (BackendObject.h:274) does NOT
// ride in MGPCaps::Dynamic - it is a member of the function table, which is precisely
// the thing a split client never receives. Its only non-test client reader is
// GL_Query.cpp:221, and under split it must be answered from kCapCpuXfbPrimitiveAccounting.
Bool PrefersCpuXfbPrimitiveAccounting() const;
private:
MG_Pipe::MGPCaps m_caps{};
MG_Backend::FormatCapabilityCache m_formats{};
RendererInfo m_renderer{};
String m_apiVersion;
BackendType m_backend = BackendType::Unknown;
Uint64 m_generation = 0;
};
// Per client context in principle; one per process in P5, because P5 serves one context.
// Leak-at-exit like every other MG_Remote singleton (ID-8): no frontend destructor may
// reach pipe or backend state from an exit handler.
CapsMirror& CapsMirrorInstance();
} // namespace MobileGL::MG_Remote::Client
@@ -0,0 +1,56 @@
// MobileGL - MobileGL/MG_Remote/Client/ClientSession.cpp
// 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
// P5 c0 stubs for packages s1 (construction, handshake) and c1 (barrier, reply read).
#include "ClientSession.h"
#include <MG_Util/Debug/Log.h>
#include <cstdlib>
namespace MobileGL::MG_Remote::Client {
#define MGP5_C0_STUB(what) \
do { \
MGLOG_F("MGPipe: Fatal{UnimplementedClientSession, \"%s\"} - P5 packages s1/c1 have not " \
"landed this yet; c0 shipped the signature only", \
what); \
std::abort(); \
} while (0)
// Null, not a Fatal: MG_Backend::Init() asks whether a session exists before it decides to
// install the remote backend object, and that question has a legitimate "no" - it is the
// monolith answer. Every call that PRESUMES a session aborts instead.
ClientSession* ClientSession::Active() { return nullptr; }
MobileGLResult ClientSession::Start(MG_Config::TransportMode, const String&) {
MGP5_C0_STUB("ClientSession::Start");
}
void ClientSession::Stop() { MGP5_C0_STUB("ClientSession::Stop"); }
Wire::PipeWireEncoder& ClientSession::Encoder() { return m_encoder; }
CapsMirror& ClientSession::Caps() { return CapsMirrorInstance(); }
Uint64 ClientSession::EmitAndWait(MG_Pipe::MGPWireOp, const void*, Uint64, const void*, Uint64,
void*, Uint64, Int32*) {
MGP5_C0_STUB("ClientSession::EmitAndWait");
}
Bool ClientSession::BarrierArmed() const { return m_barrierArmed; }
// False, not a Fatal, for both: these are the R-1 mutual-exclusion assertion's two probes,
// and an assertion helper that aborts when asked is worse than useless.
Bool ClientSession::InBarrierWait() { return false; }
Bool ClientSession::ApplyThreadIsInsideApplier() { return false; }
#undef MGP5_C0_STUB
} // namespace MobileGL::MG_Remote::Client
+95
View File
@@ -0,0 +1,95 @@
// MobileGL - MobileGL/MG_Remote/Client/ClientSession.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 half of a session: the rings, the handshake, the verb barrier. Owner: package s1
// (construction and handshake) with c1 (the barrier and the reply read). Signatures by c0.
//
// INPROC USES ShmSegment AND THE RING, NOT new[] AND NOT InProcessTransport's deques. That is
// half of what "inproc runs the same G3 codec as spawn" means: InProcessTransport
// (InProcessTransport.cpp:38-97) is two deque<vector<uint8_t>> plus two condvar doorbells, it
// touches neither a ring nor a codec, and building the session on top of it instead of on top
// of the ring would make the whole phase unfalsifiable. The transport supplies the two
// DOORBELLS and the control plane; the records go through SEG_CMD.
//
// THE VERB BARRIER (R-1). After emitting a verb the client blocks until
// RingControl::appliedSeq >= the seq it just got back from the encoder. It is not caution: 31
// of the 63 PipeInputs fields are still filled by the client's residual pass out of a live
// GLContext, so an unbarriered queue lets the server read a FUTURE value of them. Two
// consequences that must be stated because both are load-bearing:
// - while the barrier holds, at most one of {GL thread, apply thread} is runnable, which is
// what makes a single process-wide gPipeInputs legal (table 3);
// - the barrier is a RETIRING object, not a design. It opens family by family as table 2's
// BARRIER-PULLED column empties, and each later package reports how many rows it left.
//
// THE BARRIER'S WAIT IS ALSO THE REPLY'S WAIT (R-3/R-5). The reply slot id IS the record seq,
// so "wait for appliedSeq >= mySeq" and "wait for my answer" are one wait and the four Bool
// acceptance returns, ReadPixels' pixels and MapPersistent's decline cost ZERO extra round
// trips. The client MUST NOT re-derive any of those four answers locally - that is the c0f/c0g
// defect P4a paid two contract corrections for, and "always accept" is ID-39's 66 lost uploads.
#pragma once
#include <Includes.h>
#include <Config.h>
#include <MG_Pipe/MGPipe.h>
#include "../Wire/PipeWireCodec.h"
#include "CapsMirror.h"
namespace MobileGL::MG_Remote::Client {
class ClientSession {
public:
// Null until Start() succeeds; MG_Backend::Init() is the only caller of Start().
static ClientSession* Active();
// Builds the four segments, performs Hello/Welcome, takes the first CapsSnapshot, and
// - for TransportMode::InProcess - starts the server role's apply thread. Returns a
// named error rather than falling back to monolith: a fallback here is the "split lane
// ran monolith and went green" failure, and it must be loud.
MobileGLResult Start(MG_Config::TransportMode mode, const String& endpoint);
// Teardown order matters and is table 3's fourth column: publish and let the server
// drain, Doorbell::Kill() (the ONLY thing that wakes an apply thread parked on
// kWaitForever, Doorbell.h:211-221), then join, and only then release anything an
// emitter owns - a tail still referenced by an unapplied record is a use-after-free
// the join is what prevents.
void Stop();
Wire::PipeWireEncoder& Encoder();
CapsMirror& Caps();
// Emit one record and, if the barrier is armed, wait for it. `replyOut`/`replyBytes`
// name where a kReplySlot answer lands; pass {nullptr, 0} for a call that has none.
// Returns the record's seq, which is also its reply-slot id.
//
// Waiting is spin(MOBILEGL_IPC_SPIN_US) then park, through Doorbell::Wait, with
// producerParked set before blocking - the shape Doorbell.h:121 already implements.
Uint64 EmitAndWait(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes,
const void* varTail, Uint64 varTailBytes, void* replyOut,
Uint64 replyBytes, Int32* statusOut);
// MOBILEGL_IPC_VERB_BARRIER. False is the R-1 negative control and is EXPECTED to be
// red; it must be run once and the way it goes red recorded.
Bool BarrierArmed() const;
// R-1's invariant made checkable rather than only written down: true while this
// thread is inside a barrier wait. The apply thread sets its own flag on entry to the
// applier; a debug/verify build asserts the two are never both true, and that the
// client never touches gPipeInputs while the server is inside the applier.
static Bool InBarrierWait();
static Bool ApplyThreadIsInsideApplier();
private:
Wire::PipeWireEncoder m_encoder;
Wire::SegmentTable m_segments;
CapsMirror* m_caps = nullptr;
Bool m_barrierArmed = true;
};
} // namespace MobileGL::MG_Remote::Client
+47
View File
@@ -0,0 +1,47 @@
// MobileGL - MobileGL/MG_Remote/Client/EmitTables.cpp
// 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
// P5 c0 stubs for package c1.
#include "EmitTables.h"
#include <MG_Util/Debug/Log.h>
#include <cstdlib>
namespace MobileGL::MG_Remote::Client {
// The slot arithmetic, asserted rather than commented. GlobalBackendFunctionsTable is
// GLFunctionsTable plus Present plus SetSwapInterval; GLFunctionsTable is 69 function
// pointers plus one Bool (PrefersCpuXfbPrimitiveAccounting, BackendObject.h:274). A slot
// added to either without a decision here is a build break, which is the point: R-4 forbids
// a null slot, so a new slot needs an owner on the day it appears.
static_assert(sizeof(MG_Backend::GlobalBackendFunctionsTable) ==
sizeof(MG_Backend::GLFunctionsTable) + 2 * sizeof(void (*)()),
"GlobalBackendFunctionsTable is no longer GLFunctionsTable + Present + SetSwapInterval");
static_assert(sizeof(MG_Backend::GlobalBackendFunctionsTable) ==
kRemoteEmitSlotCount * sizeof(void (*)()) + sizeof(void (*)()),
"the emit table's 71 slots plus the packed Bool no longer describe the table");
[[noreturn]] void UnmigratedVerbFatal(const char* slot) {
// The same shape as MGPipeInputPoisonFatal (generated/PipeFilled.inc:407-413): names the
// slot, live at every log level, aborts. Deliberately NOT MOBILEGL_ASSERT, which is
// inert in an INFO build - and INFO is what every device lane runs.
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"%s\"}", slot);
std::abort();
}
const MG_Backend::GlobalBackendFunctionsTable& RemoteEmitTable() {
MGLOG_F("MGPipe: Fatal{UnimplementedEmitTable, \"RemoteEmitTable\"} - P5 package c1 has "
"not landed this yet; c0 shipped the signature only");
std::abort();
}
Uint32 ImplementedVerbCount() { return 0; }
} // namespace MobileGL::MG_Remote::Client
+80
View File
@@ -0,0 +1,80 @@
// MobileGL - MobileGL/MG_Remote/Client/EmitTables.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's emitting function table. Owner: package c1. Signatures by c0.
//
// MG_Backend/Init.cpp:44 assigns gBackendFunctionsTable from the active backend object, and
// 91 MG_Impl/GLImpl sites call through it directly. So a BackendObject_Remote has to return a
// COMPLETE table, and "complete" is a bigger number than R-4's headline:
//
// GLFunctionsTable (BackendObject.h:117-292) = 69 function pointers
// + Bool PrefersCpuXfbPrimitiveAccounting
// GlobalBackendFunctionsTable (BackendObject.h:293-299) = the above, + Present, + SetSwapInterval
// = 71 function pointers in total
//
// R-4's rule, restated over all 71: NO SLOT MAY BE NULL, and no slot may fall through to a
// driver. A null slot is 91 potential null calls; a pass-through slot is a split lane quietly
// running monolith and going green, which is the one outcome every gate in this phase exists
// to prevent. A verb P5 does not implement gets a slot that raises
// Fatal{UnmigratedVerb, "<slot>"} - the same shape as MGPipeInputPoisonFatal, live at every
// log level, MGLOG_F + std::abort.
//
// WHICH SLOTS GET A REAL EMITTER IS DECIDED BY THE VERB CENSUS (R-4), not guessed here:
// ~/w7/notes/p5/verb-census.md. CONTRACT-P5.md §7 carries the resulting THREE-CLASS SPLIT and
// it is not to be re-derived:
//
// A. ANSWERED LOCALLY from the caps mirror, never emitted and never Fatal (R-15) - two
// slots, GetIntegeri_v and IsTimerQuerySupported, plus the Bool member
// PrefersCpuXfbPrimitiveAccounting, which is not a slot. GetIntegeri_v is the one that
// would otherwise sink the phase: it is reached by the FIRST glCompileShader of every
// context (CompileEnv.cpp:134-138 <- Core.cpp:39), not by any verb, so a Fatal there
// aborts every scenario before it draws anything.
// B. EMITTED in P5 - five slots: Clear, DrawArrays, ReadPixels, BlitFramebuffer, Present.
// Present has ZERO MG_Impl call sites: it is reached through EGLImpl.cpp:178 ->
// BackendObject.cpp:396, so mirroring GLImpl will not find it.
// C. Fatal{UnmigratedVerb} - the remaining 64, SetSwapInterval and GetGpuTimestampNs among
// them.
//
// AND THE RULE R-4 WOULD OTHERWISE BREAK. 41 of the 69 slots are null-checked at their call
// site, and several of those checks are CAPABILITY PROBES, not safety checks - BeginOcclusionQuery
// (GL_Query.cpp:481, :785), BeginXfbPrimitivesQuery (:534), SubDataResident. With no null slot
// in this table every one of them answers "supported" and the fallback behind it silently
// disappears. A null check on a slot may not survive into the client: it becomes a caps-mirror
// read, which is what ARCHITECTURE.md:114 means by "CallMask replaces 'is this table slot null'".
//
// NOTE the asymmetry this table does not resolve: the resource, CSO, framebuffer, texture,
// sampler and program families do NOT come through here. They are emitted from
// MG_Impl/Pipe/* by direct MGPipeApply* calls (37 entry points, 41 call sites), and under
// split each of those becomes an encode. This table covers only the verbs - the draws,
// clears, blits, readbacks, queries, fences and present.
#pragma once
#include <Includes.h>
#include <MG_Backend/BackendObject.h>
namespace MobileGL::MG_Remote::Client {
// The table MG_Backend::Init() installs into gBackendFunctionsTable for the remote role.
// A reference to a never-destroyed block, like every other MG_Remote singleton (ID-8).
const MG_Backend::GlobalBackendFunctionsTable& RemoteEmitTable();
// Called by the Fatal slots. Named separately so a death test can filter on it and so
// that the message wording lives in exactly one place.
[[noreturn]] void UnmigratedVerbFatal(const char* slot);
// How many of the 71 slots have a real emitter. Reported at bring-up and asserted by the
// gate: a table that silently loses an emitter should not be able to look the same as one
// that never had it.
Uint32 ImplementedVerbCount();
// The total the count above is out of. Asserted against the struct in EmitTables.cpp, so
// a slot added to GLFunctionsTable without a decision here is a build break.
inline constexpr Uint32 kRemoteEmitSlotCount = 71;
} // namespace MobileGL::MG_Remote::Client
+51
View File
@@ -0,0 +1,51 @@
// MobileGL - MobileGL/MG_Remote/Server/PipeApplier.cpp
// 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
// P5 c0 stubs for package v1 (with p1 for the stamp rule).
#include "PipeApplier.h"
#include <MG_Util/Debug/Log.h>
#include <cstdlib>
namespace MobileGL::MG_Remote::Server {
#define MGP5_C0_STUB(what) \
do { \
MGLOG_F("MGPipe: Fatal{UnimplementedPipeApplier, \"%s\"} - P5 package v1 has not landed " \
"this yet; c0 shipped the signature only", \
what); \
std::abort(); \
} while (0)
ReplyPool::ReplyPool(void* base, Uint64 sizeBytes, Uint32 slotCount, Uint32 slotBytes)
: m_base(static_cast<Uint8*>(base)), m_size(sizeBytes), m_slots(slotCount), m_slotBytes(slotBytes) {}
void ReplyPool::PostReply(Uint64, Int32, const void*, Uint64) { MGP5_C0_STUB("ReplyPool::PostReply"); }
Uint32 ReplyPool::SlotBytes() const { return m_slotBytes; }
PipeApplier::PipeApplier(Wire::SegmentTable* segments, ReplyPool* replies)
: m_segments(segments), m_replies(replies) {}
Bool PipeApplier::ApplyOne(const Transport::RingRecordView&) { MGP5_C0_STUB("PipeApplier::ApplyOne"); }
void PipeApplier::StampVerbBoundary(MG_Pipe::MGPWireOp) {
MGP5_C0_STUB("PipeApplier::StampVerbBoundary");
}
Uint64 PipeApplier::ResidualPullCount() const { return m_residualPulls; }
void PipeApplier::PoisonRetiredStageBytes(Uint64, Uint64) {
MGP5_C0_STUB("PipeApplier::PoisonRetiredStageBytes");
}
#undef MGP5_C0_STUB
} // namespace MobileGL::MG_Remote::Server
+102
View File
@@ -0,0 +1,102 @@
// MobileGL - MobileGL/MG_Remote/Server/PipeApplier.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 server's applier bridge. Owner: package v1, with p1 for the stamp rule. Signatures by c0.
//
// IT IS A BRIDGE, NOT AN APPLIER. The applier already exists and is not edited by this phase:
// MG_Pipe/PipeApply.{h,cpp}, 37 MGPipeApply* free functions. This class owns the three things
// that only exist once records arrive over a wire rather than by direct call:
//
// 1. THE VERB STAMP. This is the phase's prerequisite, and it is not in the ROADMAP row.
// MGPipeApplyAccess deliberately does not stamp the poison generations
// (PipeInputs.h:612-618): "a stamp says the filler published this for THIS verb, which is
// the walk's statement, not the applier's". Under split the filler is in another role, so
// NOTHING stamps, every FilledGen[] stays 0, MGPipeInputFieldIsFresh returns false for
// everything, and a pure server aborts on the FIRST read inside SyncRenderState with
// Fatal{UnmigratedPipeInput, "GetRenderStateParameters@<none>"} - before reaching any
// interesting case. So: the server stamps at the verb boundary. p1 defines what is
// stamped and for which verb; v1 places the call. Neither half works alone.
//
// 2. ACCEPTANCE. Four applier entry points return Bool - ResourceCreate, ResourceRespecify,
// ResourceSubData, SetTextureParams - and MapPersistent returns void*. Those returns are
// what the CLIENT gates destructive state changes on (clearing per-level dirty flags,
// latching parameters, adopting a pointer). They go back through the reply slot, id =
// record seq (R-3), and are collected in the barrier's existing wait (R-5). The client
// may not recompute any of them.
//
// 3. R-11, THE BORROWED-POINTER RULE. A SEG_STAGE run is valid from publish until retiredSeq
// passes the record naming it. NO APPLIER ENTRY POINT MAY HOLD A POINTER PAST ITS RETURN.
// The tree has exactly one violation and it is named: GLESBufferResource::hostBytes
// (Managers.h:839), written by Ops_H_SubData (Managers.cpp:1980-1983) and Ops_H_FlushRange
// (:2035), read by six later drains (:2000, :2062, :2080, :2111, :2741, :2843). Under split
// those two must copy into server-owned storage. MOBILEGL_IPC_AUDIT=1's 0xDD fill (R-2.5)
// is the mechanical control that says whether they did.
#pragma once
#include <Includes.h>
#include <MG_Pipe/MGPipe.h>
#include "../Transport/Ring.h"
#include "../Wire/PipeWireCodec.h"
namespace MobileGL::MG_Remote::Server {
// Writes answers into SEG_REPLY at seq % slots, stamping the seq back into the slot header
// so a wrong-slot read is detectable rather than plausible (table 0's slot header row:
// {Uint64 Seq; Int32 Status; Uint32 Size;}).
class ReplyPool final : public Wire::ReplySink {
public:
ReplyPool() = default;
ReplyPool(void* base, Uint64 sizeBytes, Uint32 slotCount, Uint32 slotBytes);
void PostReply(Uint64 seq, Int32 status, const void* bytes, Uint64 size) override;
// A reply larger than one slot is Fatal rather than chunked: P5's only large answer is
// ReadPixels, whose size the client already knows before it emits, so the slot size is
// chosen from that and an overflow means the two sides disagree about the frame.
Uint32 SlotBytes() const;
private:
Uint8* m_base = nullptr;
Uint64 m_size = 0;
Uint32 m_slots = 0;
Uint32 m_slotBytes = 0;
};
class PipeApplier {
public:
PipeApplier() = default;
PipeApplier(Wire::SegmentTable* segments, ReplyPool* replies);
// Decode one record, stamp the verb, apply, post the reply if the call has one, then
// advance appliedSeq by exactly one. P5 FORBIDS BATCHING appliedSeq (R-9): the barrier's
// waiter reads it, and a batched watermark promises work that has not run.
Bool ApplyOne(const Transport::RingRecordView& record);
// p1's rule, v1's call site. Called at the verb boundary, before the record's applier
// runs, with the verb the record belongs to.
void StampVerbBoundary(MG_Pipe::MGPWireOp op);
// R-7.2's counter, read by the gate. A BARRIER-PULLED field read on the server side
// increments PipeStats::CallClass::ResidualPulls (short name `rsp`); its value at the
// end of P5 IS the size of the P6/P7/P8 debt and goes into MEASUREMENTS.
Uint64 ResidualPullCount() const;
// R-11's audit: after a record retires, fill the SEG_STAGE bytes it referenced with
// 0xDD. Only under MOBILEGL_IPC_AUDIT=1, because it costs a write of every staged byte.
void PoisonRetiredStageBytes(Uint64 offset, Uint64 size);
private:
Wire::SegmentTable* m_segments = nullptr;
ReplyPool* m_replies = nullptr;
Wire::PipeWireDecoder m_decoder;
Uint64 m_residualPulls = 0;
};
} // namespace MobileGL::MG_Remote::Server
+49
View File
@@ -0,0 +1,49 @@
// MobileGL - MobileGL/MG_Remote/Server/ServerLoop.cpp
// 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
// P5 c0 stubs for package v1 - the phase's highest-risk package.
#include "ServerLoop.h"
#include <MG_Util/Debug/Log.h>
#include <cstdlib>
namespace MobileGL::MG_Remote::Server {
#define MGP5_C0_STUB(what) \
do { \
MGLOG_F("MGPipe: Fatal{UnimplementedServerLoop, \"%s\"} - P5 package v1 has not landed " \
"this yet; c0 shipped the signature only", \
what); \
std::abort(); \
} while (0)
MobileGLResult ServerLoop::Start(ServerSession&) { MGP5_C0_STUB("ServerLoop::Start"); }
void ServerLoop::Stop() { MGP5_C0_STUB("ServerLoop::Stop"); }
// Not a stub: teardown asks this to decide whether to Kill and join at all, and a teardown
// helper that aborts when the thread was never started is a hang in the shutdown path.
Bool ServerLoop::Running() const { return m_running; }
MG_Backend::BackendObject* ServerLoop::Backend() { MGP5_C0_STUB("ServerLoop::Backend"); }
MobileGLResult ServerLoop::RunOnApplyThread(ControlWork, void*) {
MGP5_C0_STUB("ServerLoop::RunOnApplyThread");
}
ServerLoop& ServerLoopInstance() {
// ID-8: leak at exit, like every MG_Remote singleton.
static ServerLoop& instance = *new ServerLoop{};
return instance;
}
#undef MGP5_C0_STUB
} // namespace MobileGL::MG_Remote::Server
+91
View File
@@ -0,0 +1,91 @@
// MobileGL - MobileGL/MG_Remote/Server/ServerLoop.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 apply thread and the server's private backend object. Owner: package v1 - the highest
// risk item in P5. Signatures by c0.
//
// WHY THE THREAD IS THE POINT. DirectGLES has 16 IsBackendContextCurrentOnThisThread() guards
// (DirectGLES.cpp:12034..12428) and Managers.cpp has 16 CanTouchGLNow() guards (:1494..:3428);
// every one of them DEGRADES when the answer is false - fences become always-signaled, queries
// return null handles, Present creates no frame fence so the buffer pool's recycle watermark
// never advances, and the two persistent-map acquisitions (Managers.cpp:1494, :2170) DECLINE,
// which would make PersistentCoherentMapScenario unreachable. Making the apply thread the
// context owner for life turns all 32 of those answers true on the server and removes the
// whole degradation class at once. It is also exactly the shape P6's spawned server inherits.
//
// P5 BUILDS ONE THREAD, NOT TWO. No mgl-srv-io: inproc's control plane is in the same process.
// P6 splits it.
//
// PARKING AND SHUTDOWN. The thread parks on Doorbell::Wait(consumerParked, ready, spinUs,
// kWaitForever) and shuts down when Wait returns false with Dead() set. Doorbell::Kill()
// (Doorbell.h:211-221) IS THE ONLY THING that wakes a thread parked on kWaitForever - a fact
// ARCHITECTURE.md's teardown order (:537) omits and InProcessTransportTest.cpp:344 already
// pins. Kill BEFORE join; join before the client frees any emitter-owned Vector; and the join
// must be bounded (that test uses 5 s) so a regression is a red test and not a hung CI job.
//
// THE EGL OWNERSHIP MOVE. eglMakeCurrent runs ONCE on this thread and is never released
// (DirectGLES.cpp:11925 plus the six cache invalidations at :11933-11953, which become a
// one-time startup cost instead of a per-make-current storm). The client's nine EGL virtuals
// become BLOCKING control requests executed here. ReleaseEGLResources and
// ~BackendObject_DirectGLES MUST be blocking: MobileGL::Destroy() (MobileGL/Init.cpp:68)
// otherwise walks on while the server still holds the context.
//
// THE FALLBACK IS PRE-DECLARED, NOT INVENTED UNDER PRESSURE (R-1). If the context migration is
// still not running ClearThenReadPixels at the end of v1's fourth working day, the integrator -
// not the package - declares `inproc-inline`: the client thread drains the ring itself, no
// thread is created, no context migrates, and a second package picks up the thread arm.
#pragma once
#include <Includes.h>
#include "ServerSession.h"
namespace MobileGL::MG_Remote::Server {
class ServerLoop {
public:
// Creates the apply thread, names it mgl-srv-apply, applies
// MOBILEGL_IPC_SERVER_AFFINITY (borrowing ShaderCompilePool's big-core detection) and
// LOGS THE RESOLVED MASK - an affinity that silently did nothing is indistinguishable
// from one that worked, and the split's whole performance claim rests on both halves
// landing on fast cores.
MobileGLResult Start(ServerSession& session);
// Kill the doorbell, join the thread (bounded), then destroy the private backend object
// ON THAT THREAD before it exits. Blocking by contract - see the header note.
void Stop();
Bool Running() const;
// The server role's own backend object. NOT pActiveBackendObject: that global holds the
// client's BackendObject_Remote. Table 3's ruling is that the server holds its
// BackendObject_DirectGLES privately here, and that the seven backend-internal reads of
// pActiveBackendObject - ClampSamplesToBackendSupport (BackendObject_DirectGLES.cpp:815,
// :819) and five in Utils.cpp (:74, :82, :126, :220, :260), all of them format-capability
// lookups - take the format cache as a parameter instead. That is six functions across
// two files, and it is why no thread-keyed shim is needed for MOBILEGL_BUILD_DISAGGREGATED_INPROC.
MG_Backend::BackendObject* Backend();
// Run one blocking control request on the apply thread and wait for it. This is how all
// nine EGL lifecycle virtuals cross; it is deliberately NOT a queue of async messages,
// because every one of them has a return value the caller acts on immediately.
//
// A raw function pointer plus a user pointer, not std::function: this runs on the
// teardown path too, and the teardown path may not allocate - ID-8's leak-at-exit rule
// exists because frontend destructors reach here from exit handlers.
using ControlWork = MobileGLResult (*)(void* user);
MobileGLResult RunOnApplyThread(ControlWork work, void* user);
private:
ServerSession* m_session = nullptr;
Bool m_running = false;
};
ServerLoop& ServerLoopInstance();
} // namespace MobileGL::MG_Remote::Server
@@ -0,0 +1,50 @@
// MobileGL - MobileGL/MG_Remote/Server/ServerSession.cpp
// 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
// P5 c0 stubs for package s1.
#include "ServerSession.h"
#include <MG_Util/Debug/Log.h>
#include <cstdlib>
namespace MobileGL::MG_Remote::Server {
#define MGP5_C0_STUB(what) \
do { \
MGLOG_F("MGPipe: Fatal{UnimplementedServerSession, \"%s\"} - P5 package s1 has not landed " \
"this yet; c0 shipped the signature only", \
what); \
std::abort(); \
} while (0)
ServerSession* ServerSession::Active() { return nullptr; }
MobileGLResult ServerSession::Accept(Transport::ITransport&) { MGP5_C0_STUB("ServerSession::Accept"); }
MobileGLResult ServerSession::PublishCapsSnapshot() {
MGP5_C0_STUB("ServerSession::PublishCapsSnapshot");
}
Transport::RingConsumer& ServerSession::CommandRing() { return m_commands; }
Transport::RingControl& ServerSession::Control() { MGP5_C0_STUB("ServerSession::Control"); }
Wire::SegmentTable& ServerSession::Segments() { return m_segments; }
PipeApplier& ServerSession::Applier() { return m_applier; }
ReplyPool& ServerSession::Replies() { return m_replies; }
Transport::Doorbell& ServerSession::ConsumerDoorbell() {
MGP5_C0_STUB("ServerSession::ConsumerDoorbell");
}
Transport::Doorbell& ServerSession::ProducerDoorbell() {
MGP5_C0_STUB("ServerSession::ProducerDoorbell");
}
#undef MGP5_C0_STUB
} // namespace MobileGL::MG_Remote::Server
+72
View File
@@ -0,0 +1,72 @@
// MobileGL - MobileGL/MG_Remote/Server/ServerSession.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 server half of a session: the consumer side of the rings, the handshake reply, the caps
// snapshot. Owner: package s1. Signatures by c0.
//
// The four segment sizes are already pinned by ProtocolSmokeTest.cpp:72 and are not up for
// re-derivation here: SEG_CMD 8 MiB, SEG_STAGE 32 MiB, SEG_REPLY 8 MiB, SEG_EVENT 256 KiB.
// MOBILEGL_IPC_RING_MB and MOBILEGL_IPC_STAGE_MB move the first two; the ring caps ONE record
// at half its size, so the default 8 MiB caps a record at 4 MiB (R-10).
//
// THE TWO DOORBELL ACCESSORS ARE ON THE CONCRETE CLASS, NOT ON ITransport
// (InProcessTransport.h:64-68). P5 decides this now rather than letting P6 discover it: the
// SESSION owns the pair and hands out references, so ITransport stays the dumb control-plane
// interface its header says it is and SocketTransport does not have to grow two accessors it
// has no natural home for. Discovering this in P6 would mean re-laying one package's call sites.
#pragma once
#include <Includes.h>
#include <MG_Pipe/MGPipe.h>
#include "../Transport/Doorbell.h"
#include "../Transport/ITransport.h"
#include "../Transport/Ring.h"
#include "../Wire/PipeWireCodec.h"
#include "PipeApplier.h"
namespace MobileGL::MG_Remote::Server {
class ServerSession {
public:
static ServerSession* Active();
// Maps the four segments, answers Hello with Welcome, and publishes the first
// CapsSnapshot. The ABI assertion (CapsCodec.h) happens HERE, before a single record is
// decoded: sizeof(DynamicBackendParameters), sizeof(MGPCaps), sizeof(GLFunctionsTable)
// and the build fingerprint must match, and a mismatch is Fatal{AbiMismatch}.
MobileGLResult Accept(Transport::ITransport& transport);
// Re-publishes the whole snapshot. R-12: a SECOND arrival IS the invalidation signal,
// which is how DirectGLES - which has no OnCapsInvalidated producer - tells the client
// its InitCapabilities re-ran, without any dev-shaped backend edit. It is also the
// re-open signal for "the server's ES context died and its rings were dropped", the
// event MGPipeCallbacks has no eleventh slot for (MGPipeCallbacks.h:56-58).
MobileGLResult PublishCapsSnapshot();
Transport::RingConsumer& CommandRing();
Transport::RingControl& Control();
Wire::SegmentTable& Segments();
PipeApplier& Applier();
ReplyPool& Replies();
// The client rings this one; the apply thread parks on it.
Transport::Doorbell& ConsumerDoorbell();
// The server rings this one, but only when producerParked is set (a store to a shared
// cache line otherwise burns a big core for a whole frame on a phone).
Transport::Doorbell& ProducerDoorbell();
private:
Transport::RingConsumer m_commands;
Wire::SegmentTable m_segments;
PipeApplier m_applier;
ReplyPool m_replies;
};
} // namespace MobileGL::MG_Remote::Server
+43
View File
@@ -41,6 +41,49 @@
// (Records.def / PipeCalls.def) is a separate deliverable; the ring itself
// only needs kind/flags/size, so it can carry the real records the day they
// land without changing shape.
//
// ---------------------------------------------------------------------------
// THE FIVE WATERMARKS (P5 R-9). One sentence each, and they are a contract:
// every one of the five was declared here at P0 and written by nobody but
// InitRingControl, so until P5 there was nothing to disagree with.
//
// submittedSeq Advanced by the PRODUCER after it publishes. NOBODY
// WAITS ON IT - it is diagnostic, the answer to "how far
// ahead of the server is the client right now".
// appliedSeq Advanced by the CONSUMER for EVERY SINGLE RECORD it
// applies. The client's verb barrier and every reply wait
// read it, so it is the one watermark P5 FORBIDS BATCHING:
// the sixty-four-record batching this ring was designed
// for makes a waiter block on work that already ran, or -
// far worse - resume on work that has not.
// retiredSeq Advanced by the CONSUMER once it has finished with the
// SEG_STAGE bytes a record referenced. The staging
// allocator reclaims behind it, and nothing else may.
// completedFrameSerial Advanced by the SERVER when a present completes. What
// recycling and ageing wait on; it trails appliedSeq by
// the GPU's own depth and must never be conflated with it.
// presentAckSerial Advanced by the SERVER when it returns a present credit.
// The client's present throttle waits on it; it is the
// only back-pressure that bounds latency rather than bytes.
//
// Every wait on all five is `>=`, never `==`: a waiter that tests equality
// misses the wakeup the moment a producer or consumer moves by more than one.
//
// BATCHING MAY ONLY MAKE A WATERMARK LATE. All five except appliedSeq may be
// published lazily, because a waiter that sees an old value waits longer than
// it had to and is still correct. NONE of them may ever be published EARLY: a
// watermark that reports more than was actually done turns every waiter into a
// silent use of work that has not happened, and there is no checksum anywhere
// on this ring that would catch it.
//
// kRecPad DOES NOT ADVANCE SEQ. A wrap filler is framing, not a record: it has
// no opcode, no payload meaning and no reply slot. Both sides must skip it
// BEFORE counting. If one side counts it and the other does not, the two seq
// spaces drift by one at every wrap - and because seq IS the reply-slot id
// (P5 R-3), a drifted seq silently reads another call's answer rather than
// failing. Nothing on this ring would detect that, which is why the rule is
// stated here rather than left to each side's loop.
// ---------------------------------------------------------------------------
#pragma once
+116
View File
@@ -0,0 +1,116 @@
// MobileGL - MobileGL/MG_Remote/Wire/PipeWireCodec.cpp
// 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
// P5 c0's stubs for package w1. Every body is MGLOG_F + std::abort and NOT a silent no-op:
// an unimplemented codec that returns quietly is exactly how a split lane runs monolith and
// goes green, which is the failure the whole phase is built to make impossible.
#include "PipeWireCodec.h"
#include <MG_Remote/Protocol/generated/protocol_generated.h>
#include <MG_Util/Debug/Log.h>
#include <cstdlib>
namespace MobileGL::MG_Remote::Wire {
// Table 0's first row, mechanised: this enum and the schema's SegmentKind are ONE id
// space, and the only place they are compared is here. A schema edit that renumbers a
// segment is a build break rather than a wrong pointer on a ring.
//
// Fully qualified from the global namespace on purpose: the generated header's namespace
// is `MobileGL::Wire` and we are inside `MobileGL::MG_Remote::Wire`, so a bare `Wire::`
// resolves to THIS namespace and the assertion would silently be about the wrong enum -
// or, as it first was, fail to compile for a reason that looks unrelated.
static_assert(static_cast<Uint32>(::MobileGL::Wire::SegmentKind::None) == kSegNone);
static_assert(static_cast<Uint32>(::MobileGL::Wire::SegmentKind::Cmd) == kSegCmd);
static_assert(static_cast<Uint32>(::MobileGL::Wire::SegmentKind::Stage) == kSegStage);
static_assert(static_cast<Uint32>(::MobileGL::Wire::SegmentKind::Reply) == kSegReply);
static_assert(static_cast<Uint32>(::MobileGL::Wire::SegmentKind::Event) == kSegEvent);
static_assert(static_cast<Uint32>(::MobileGL::Wire::SegmentKind::Shadow) == kSegShadow);
static_assert(static_cast<Uint32>(::MobileGL::Wire::SegmentKind::Adopt) == kSegAdopt);
// And the other half of table 0's rule: MG_Pipe's "no segment" sentinel is the same 0.
static_assert(static_cast<Uint32>(MG_Pipe::kMGHostSpanSegNone) == kSegNone,
"kMGHostSpanSegNone and SegmentId::kSegNone must be the same value");
#define MGP5_C0_STUB(what) \
do { \
MGLOG_F("MGPipe: Fatal{UnimplementedWireCodec, \"%s\"} - P5 package w1 has not landed " \
"this yet; c0 shipped the signature only", \
what); \
std::abort(); \
} while (0)
void SegmentTable::Install(SegmentId, SegmentView) { MGP5_C0_STUB("SegmentTable::Install"); }
SegmentView SegmentTable::Get(SegmentId) const { MGP5_C0_STUB("SegmentTable::Get"); }
const void* SegmentTable::Resolve(Uint32, Uint64, Uint64) const {
MGP5_C0_STUB("SegmentTable::Resolve");
}
void SegmentTable::InstallProcessResolver() { MGP5_C0_STUB("SegmentTable::InstallProcessResolver"); }
void SegmentTable::UninstallProcessResolver() {
MGP5_C0_STUB("SegmentTable::UninstallProcessResolver");
}
// NOT a stub: the two Fatal helpers are the one thing every package needs on day one, and
// a Fatal that is itself unimplemented would report the wrong failure.
void WireProtocolFatal(const char* what, const char* detail) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s\"} %s", what, detail != nullptr ? detail : "");
std::abort();
}
void WireProtocolFatalAt(const char* what, Uint64 got, Uint64 expected) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s\"} got=%llu expected=%llu", what,
static_cast<unsigned long long>(got), static_cast<unsigned long long>(expected));
std::abort();
}
void CheckBlobIsHonest(MG_Pipe::MGPWireOp, const MG_Pipe::MGPBlobRef&, const SegmentTable&) {
MGP5_C0_STUB("CheckBlobIsHonest");
}
void CheckHostSpanIsHonest(const MG_Pipe::MGHostSpan&) { MGP5_C0_STUB("CheckHostSpanIsHonest"); }
PipeWireEncoder::PipeWireEncoder(Transport::RingControl* control, Transport::RingProducer* cmd,
Transport::RingProducer* stage, SegmentTable* segments)
: m_control(control), m_cmd(cmd), m_stage(stage), m_segments(segments) {}
Bool PipeWireEncoder::Valid() const { return m_control != nullptr && m_cmd != nullptr; }
MG_Pipe::MGPBlobRef PipeWireEncoder::StageBytes(const void*, Uint64) {
MGP5_C0_STUB("PipeWireEncoder::StageBytes");
}
Uint64 PipeWireEncoder::EncodeRecord(MG_Pipe::MGPWireOp, const void*, Uint64, const void*, Uint64) {
MGP5_C0_STUB("PipeWireEncoder::EncodeRecord");
}
void PipeWireEncoder::Publish() { MGP5_C0_STUB("PipeWireEncoder::Publish"); }
Uint64 PipeWireEncoder::EmitSeq() const { return m_emitSeq; }
Uint64 PipeWireEncoder::MaxRecordBytesSeen() const { return m_maxRecordBytes; }
PipeWireDecoder::PipeWireDecoder(Transport::RingControl* control, SegmentTable* segments,
ReplySink* replies)
: m_control(control), m_segments(segments), m_replies(replies) {}
Bool PipeWireDecoder::Valid() const { return m_control != nullptr && m_segments != nullptr; }
Bool PipeWireDecoder::DecodeAndApply(const Transport::RingRecordView&) {
MGP5_C0_STUB("PipeWireDecoder::DecodeAndApply");
}
Uint64 PipeWireDecoder::AppliedSeq() const { return m_applySeq; }
#undef MGP5_C0_STUB
} // namespace MobileGL::MG_Remote::Wire
+230
View File
@@ -0,0 +1,230 @@
// MobileGL - MobileGL/MG_Remote/Wire/PipeWireCodec.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
// G3: the MGPipe record codec. Owner: package w1.
//
// This header is the CONTRACT (MG_Remote/CONTRACT-P5.md) in C++ form; P5's c0 package wrote
// it so the other seven could compile on day one against signatures that cannot then move
// under them. Every body below is a named Fatal until w1 lands the real one.
//
// WHAT THIS LAYER IS, AND WHAT IT IS NOT
//
// It turns one MGPipe call into bytes in SEG_CMD (+ SEG_STAGE), and bytes back into ONE CALL
// OF AN EXISTING MGPipeApply* FREE FUNCTION. It owns NO semantics: MG_Pipe/PipeApply.cpp is
// not edited by this package, and a decoder arm that "handles" a record itself rather than
// delegating is a review failure (R-4's rule, one level down).
//
// THE FIVE HONESTY RULES (R-2), because they are what make `inproc` worth running at all.
// In the same address space every shortcut works: MGHostSpan::Ptr dereferences, a blobref
// whose Offset is a host address resolves, and MGPipeApplyMapPersistent's return value is a
// usable pointer. So the codec is held to the SPAWN rules even when it does not need to be:
// 1. encoder writes MGHostSpan::Ptr == nullptr and points Seg/Offset at SEG_STAGE;
// 2. encoder fills a real Seg, a real in-segment Offset and a NON-ZERO Size for every
// MGPBlobRef that carries content;
// 3. decoder Fatal{ProtocolCorruption} on: Ptr != nullptr; a content record with
// Blob.Size == 0; Size != 0 with Seg == kSegNone; Offset + Size past the segment;
// 4. MGPipeApplyMapPersistent returns nullptr under split (R-6; b1's half);
// 5. with MOBILEGL_IPC_AUDIT=1 the server fills a retired record's SEG_STAGE bytes with
// 0xDD, so an implementation that kept a pointer past apply reads 0xDD next frame.
//
// SEQ. The record ordinal IS the sequence number and IS the reply-slot id (R-3): there is no
// per-record seq field on the wire (ARCHITECTURE.md:124) and no second id space. Seq is
// 1-based so that 0 can mean "nothing encoded". A kRecPad wrap filler DOES NOT ADVANCE SEQ -
// both sides must skip it before counting, or every ring wrap offsets the two sides'
// numbering permanently and nothing checksums it (R-9, Ring.h's header).
#pragma once
#include <Includes.h>
#include <MG_Pipe/MGPipe.h>
#include "../Transport/Ring.h"
namespace MobileGL::MG_Remote::Wire {
// ---- table 0: the segment id space -------------------------------------------------
//
// The SAME VALUES as Protocol::SegmentKind (protocol.fbs:36-44); PipeWireCodec.cpp
// static_asserts the two agree, which is the only place the flatbuffers header and this
// enum meet. 0 is ALWAYS "no segment" and is never a real segment id, which is what lets
// MGPBlobRef{Seg == 0, Size != 0} be a detectable fault rather than a legal shape.
enum SegmentId : Uint32 {
kSegNone = 0,
kSegCmd = 1, // client-owned command ring (RingControl + records)
kSegStage = 2, // client-owned bulk staging: every blob and every var-tail's bytes
kSegReply = 3, // server-owned reply pool, addressed seq % slots (R-3)
kSegEvent = 4, // server-owned event ring (the reverse channel)
kSegShadow = 5, // client-owned per-object shadow (P8+)
kSegAdopt = 6, // server-owned adopted store, client RW (P11)
};
// Seq is 1-based. 0 is "no record", never a valid reply-slot id.
inline constexpr Uint64 kInvalidSeq = 0;
// One mapped segment as this ROLE sees it. Two roles in one process have two different
// SegmentTables over the same memory on purpose: a client that can resolve SEG_REPLY as
// if it owned it is the inproc cheat R-2 exists to kill.
struct SegmentView {
void* Base = nullptr;
Uint64 Size = 0;
};
// ---- the per-role segment table, and the process resolver hook ---------------------
//
// gMGPipeSegmentResolver (MG_Pipe/MGPipeHostSpan.h:47) is a plain non-atomic inline
// variable and there is exactly ONE of it per process, so under inproc the two roles
// cannot both install their own into it. TABLE 3's ruling: the resolver is installed by
// the SERVER role only, before the apply thread starts, and the client never resolves a
// span at all (it only ever writes Ptr = nullptr). Install() therefore takes the role.
class SegmentTable {
public:
void Install(SegmentId seg, SegmentView view);
SegmentView Get(SegmentId seg) const;
// Bounds-checked resolve. Returns nullptr when seg is unknown, size is 0, or
// offset + size runs past the segment; the CALLER escalates that to
// Fatal{ProtocolCorruption} (R-2.3) rather than this returning into a Fatal, so a
// unit test can exercise the arithmetic without dying.
const void* Resolve(Uint32 seg, Uint64 offset, Uint64 size) const;
// Points MG_Pipe::gMGPipeSegmentResolver at this table. Server role only; asserts if
// a resolver is already installed, because two roles racing on one inline variable is
// the failure this function exists to make loud.
void InstallProcessResolver();
static void UninstallProcessResolver();
private:
SegmentView m_views[kSegAdopt + 1];
};
// ---- the four Fatal arms, worded once ----------------------------------------------
//
// One function so encoder, decoder and every package's own bounds check produce the SAME
// log line. `what` is the record or field; `detail` is the number that was wrong.
[[noreturn]] void WireProtocolFatal(const char* what, const char* detail);
[[noreturn]] void WireProtocolFatalAt(const char* what, Uint64 got, Uint64 expected);
// R-2.3 arms 1-4 over one record's blobref. Split only; a monolith emission is exempt by
// construction because it never reaches this layer.
void CheckBlobIsHonest(MG_Pipe::MGPWireOp op, const MG_Pipe::MGPBlobRef& blob,
const SegmentTable& segments);
// R-2.3 arm for MGHostSpan. P5's reduced path should produce ZERO host spans
// (kCapNeedsHostIndexBytes / kCapNeedsHostUboBytes are both 0 in P5, table 0), so this
// firing at all is a finding, not just a corruption check.
void CheckHostSpanIsHonest(const MG_Pipe::MGHostSpan& span);
// ---- encoder -----------------------------------------------------------------------
//
// Not thread safe: one encoder per client context, driven by the GL thread, by
// construction (SPSC is the ring's contract too).
class PipeWireEncoder {
public:
PipeWireEncoder() = default;
PipeWireEncoder(Transport::RingControl* control, Transport::RingProducer* cmd,
Transport::RingProducer* stage, SegmentTable* segments);
Bool Valid() const;
// Copies `size` bytes into SEG_STAGE and returns the blobref that names them:
// {Seg = kSegStage, Offset = in-segment byte offset, Size = size}. R-2.2 - Size is
// NEVER 0 for a content blob, and a 0-size call is a programming error that Fatals
// rather than returning an empty ref, because "the record declared no blob" and "the
// record declared an empty blob" must not be spelled the same way on a wire.
//
// The bytes are valid until retiredSeq passes the record that names them (R-11).
MG_Pipe::MGPBlobRef StageBytes(const void* bytes, Uint64 size);
// Writes one record: header (op, MGPipeCallFlagsFor(op), total size), then the fixed
// payload, then the variable tail. Returns the record's SEQ, which is also its
// reply-slot id (R-3), or kInvalidSeq if the ring refused it.
//
// A record larger than RingProducer::MaxRecordBytes() is Fatal{RingOverrun}, NOT a
// wait: R-10 says P5 does no chunking and must instead PROVE it never needs any, so
// this is where the proof fails loudly if it is wrong. MaxRecordBytesSeen() is the
// counter that feeds that proof into MEASUREMENTS.
Uint64 EncodeRecord(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes,
const void* varTail = nullptr, Uint64 varTailBytes = 0);
// Release-stores the head cursor, then rings the consumer doorbell IF PARKED. The
// order is pinned by RingTest.cpp:446 and must not be swapped: notify-then-publish
// loses the wakeup.
void Publish();
// The highest seq this encoder has produced. The verb barrier (R-1) waits for
// RingControl::appliedSeq to reach it.
Uint64 EmitSeq() const;
// R-10's proof obligation: the largest single record this encoder has written.
Uint64 MaxRecordBytesSeen() const;
private:
Transport::RingControl* m_control = nullptr;
Transport::RingProducer* m_cmd = nullptr;
Transport::RingProducer* m_stage = nullptr;
SegmentTable* m_segments = nullptr;
Uint64 m_emitSeq = kInvalidSeq;
Uint64 m_maxRecordBytes = 0;
};
// ---- decoder -----------------------------------------------------------------------
// Where a kReplySlot answer goes. Declared HERE and not in Server/ so the codec does not
// depend on the server session: the decoder's job ends at "produce the answer bytes".
//
// The slot is addressed seq % slots and the server writes the seq back into the slot
// header for self-check (table 0's slot header row). Status: 0 = OK, 1 = DECLINED,
// 2 = ERROR. DECLINED IS A REAL ANSWER, not a failure - it is how MapPersistent says
// nullptr (R-6) and how the four Bool acceptance entry points say false (R-5).
class ReplySink {
public:
virtual ~ReplySink() = default;
static constexpr Int32 kStatusOk = 0;
static constexpr Int32 kStatusDeclined = 1;
static constexpr Int32 kStatusError = 2;
virtual void PostReply(Uint64 seq, Int32 status, const void* bytes, Uint64 size) = 0;
};
// Not thread safe: one decoder on the apply thread, by construction.
class PipeWireDecoder {
public:
PipeWireDecoder() = default;
PipeWireDecoder(Transport::RingControl* control, SegmentTable* segments,
ReplySink* replies);
Bool Valid() const;
// Decodes ONE record and calls the matching MGPipeApply* free function.
//
// TWO BOUNDS CHECKS, NOT ONE. The generated MGP_WIRE_CHECK_BOUNDS only proves
// `size >= sizeof(MGPWireRec_X)` - IT CANNOT SEE THE TAIL, so a record declaring
// Count = 4000 while carrying 8 bytes passes it today. The decoder must recompute the
// total from the declared count(s) and require it to EQUAL MGPWireRecHeader::Size.
// The three double-tailed shapes are SetShaderBuffers (MGPBufferRange[Count] then
// MGHostSpan[HostSpanCount]), SetStreamOutputTargets (MGPBufferRange[Count] then
// Uint32[Count]) and DrawVbo (MGPDrawRange[NumDraws] then a conditional MGHostSpan).
//
// Returns whether the record was applied. False is reserved for a record this build
// deliberately does not implement; a MALFORMED record never returns, it Fatals.
//
// A kRecPad record must be skipped by the CALLER before this is reached; passing one
// here Fatals, because a pad that reached the decoder has already been counted.
Bool DecodeAndApply(const Transport::RingRecordView& record);
// Advanced by exactly one per applied non-pad record. P5 FORBIDS BATCHING IT (R-9):
// the verb barrier's waiter reads it, and a batched watermark makes the client wait
// for records the server has not run.
Uint64 AppliedSeq() const;
private:
Transport::RingControl* m_control = nullptr;
SegmentTable* m_segments = nullptr;
ReplySink* m_replies = nullptr;
Uint64 m_applySeq = kInvalidSeq;
};
} // namespace MobileGL::MG_Remote::Wire
@@ -66,7 +66,8 @@ namespace MobileGL {
// ---- P4a's three client emission points (see TextureObject.h) ----
void TextureObjectBase::PipePublishDescriptor() {
MG_Pipe::MGPipeEmitTextureResourceRespecify(*this);
MG_Pipe::MGPipeEmitTextureResourceRespecify(*this, MG_Pipe::MGPipeTextureRespecifyScope::WholeResource,
0, 0);
// AND THE FRAMEBUFFER AGGREGATE MOVES (P4a fable seam F-3). The resource record
// above is only half of what a storage definition changes: set_framebuffer_state
// INLINES an attachment's InternalFormat, TextureTarget, extent, Samples and
@@ -83,6 +84,24 @@ namespace MobileGL {
MGP_NOTE_AGGREGATE(FramebufferAttachment);
}
void TextureObjectBase::PipePublishLevelDescriptor(TextureUploadTarget uploadTarget, Uint mipmapLevel) {
// ONE level was (re)allocated: only that level's pending upload is against
// storage that is gone (P4a final review C-1). Every other level's stays.
MG_Pipe::MGPipeEmitTextureResourceRespecify(*this, MG_Pipe::MGPipeTextureRespecifyScope::OneLevel,
static_cast<Uint32>(uploadTarget),
static_cast<Uint32>(mipmapLevel));
MGP_NOTE_AGGREGATE(FramebufferAttachment); // an attached level's extent is inlined (F-3)
}
void TextureObjectBase::PipePublishTruncatedDescriptor(TextureUploadTarget uploadTarget, Uint levelCount) {
// The chain was cut at `levelCount`: the levels above the cut are gone with their
// pending uploads, the levels below it are untouched and keep theirs.
MG_Pipe::MGPipeEmitTextureResourceRespecify(*this, MG_Pipe::MGPipeTextureRespecifyScope::LevelsFrom,
static_cast<Uint32>(uploadTarget),
static_cast<Uint32>(levelCount));
MGP_NOTE_AGGREGATE(FramebufferAttachment);
}
void TextureObjectBase::PipePublishParams() {
MG_Pipe::MGPipeEmitTextureParams(*this);
}
@@ -492,8 +511,10 @@ namespace MobileGL {
// storage-defining GL entry point - glTexImage*, glCompressedTexImage*,
// glTexStorage*, glTextureView and the generated-mip storage grow - reaches
// storage through here, which is what makes the emission complete without one call
// site per entry point in MG_Impl/GLImpl.
PipePublishDescriptor();
// site per entry point in MG_Impl/GLImpl. AND IT NAMES THE LEVEL (final review
// C-1): this call replaced ONE level's storage, and only that level's pending
// upload may go with it.
PipePublishLevelDescriptor(uploadTarget, mipmapLevel);
#endif
}
@@ -501,7 +522,9 @@ namespace MobileGL {
BumpShapeVersion();
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
#if MOBILEGL_PIPE_PUSH
PipePublishDescriptor();
// The levels at and above the cut are gone; the ones below keep their pending
// uploads (final review C-1).
PipePublishTruncatedDescriptor(uploadTarget, levelCount);
#endif
}
@@ -221,11 +221,23 @@ namespace MobileGL::MG_State::GLState {
// members rather than free calls so the cube's, the view's and the buffer texture's
// translation units keep calling an inherited helper.
//
// resource_respecify. Called from BumpShapeVersion and from the three parameter
// resource_respecify, WHOLE-RESOURCE scope: the format setter and the three parameter
// setters that move a DESCRIPTOR field without moving the shape (immutable levels,
// sample count, fixed sample locations). The emitter dedupes on the built descriptor,
// so an over-call costs one 88-byte compare and never an extra record.
// sample count, fixed sample locations), and a view's creation. The emitter dedupes
// this form on the built descriptor, so an over-call costs one 88-byte compare and
// never an extra record.
void PipePublishDescriptor();
// The PER-LEVEL and the CHAIN-CUT forms of the same call (P4a final review C-1). The
// applier keeps a pending-upload set per (uploadTarget, level) and drops the entries
// against the storage a respecify REPLACES - and the descriptor cannot tell it which:
// AllocateStorage is per level and TruncateMipmapLevels removes a tail, while the
// descriptor carries the base extent and the level count only. So the storage entry
// points state the scope themselves; the whole-resource form above is for the calls
// that really redefine the whole store. A per-level form is NOT deduped on the
// descriptor: a non-base level redefined at a new size moves no descriptor field, and
// the applier's box against the old level has to go regardless.
void PipePublishLevelDescriptor(TextureUploadTarget uploadTarget, Uint mipmapLevel);
void PipePublishTruncatedDescriptor(TextureUploadTarget uploadTarget, Uint levelCount);
// set_texture_params, from every mutator that bumps m_textureParamsVersion.
void PipePublishParams();
// The sub-data DRAIN LIST's append, on a level's first dirty mark. There is no clean
@@ -34,8 +34,9 @@ namespace MobileGL {
#if MOBILEGL_PIPE_PUSH
// AFTER the allocation, for TextureObjectWithOneMipmap's reason: BumpShapeVersion
// runs first and a descriptor built there would describe the level set this call
// is about to change.
PipePublishDescriptor();
// is about to change. The FACE rides in `uploadTarget`, so the key the emitter
// drops is that face's level and no other face's (final review C-1).
PipePublishLevelDescriptor(uploadTarget, mipmapLevel);
#endif
}
@@ -43,7 +44,7 @@ namespace MobileGL {
BumpShapeVersion();
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
#if MOBILEGL_PIPE_PUSH
PipePublishDescriptor();
PipePublishTruncatedDescriptor(uploadTarget, levelCount);
#endif
}
@@ -33,6 +33,15 @@ namespace MobileGL::MG_State::GLState {
Access = access;
Format = format;
++Version;
#if MOBILEGL_PIPE_PUSH
// P4a D-A4 / final review M-A: the EARLIEST producer of kMGPipeBindShaderImage. The
// ImageBindableHint the bit feeds is the prevention half of the texture-remint stall
// class (a texture the server knows may be image-bound is allocated image-bindable
// up front), so it has to reach the applier before the texture's first sync - at
// the bind, not at the next validate point's image walk. Push-only through the
// contract's door, like every other hook in this directory (G1).
if (Texture) MG_Pipe::MGPipeNoteTextureImageBound(*Texture);
#endif
}
};
@@ -51,6 +51,13 @@
#include <MG_Pipe/MGPipe.h>
#if MOBILEGL_PIPE_PUSH
#include "Init.h"
// MOBILEGL_PIPE_POISON is DERIVED in the header below (PipeInputs.h:20-26) and nowhere
// else, so a TU that tests it without this include silently reads it as 0. That is
// invisible in a push build (where it really is 0) and in a verify build (where
// -DMOBILEGL_PIPE_VERIFY=1 is on the command line); MOBILEGL_BUILD_DISAGGREGATED is the
// one arming condition that lives behind the header, so a split build is the first place
// the refusals below stop being fatal while the expectations still say they are.
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Impl/GLImpl/Program/GL_Program.h>
#include <MG_Impl/GLImpl/Program/GL_ProgramPipeline.h>
#include <MG_Impl/Pipe/CompositeResolver.h>
+30 -1
View File
@@ -57,6 +57,13 @@
#include "Includes.h"
#include <MG_Pipe/MGPipe.h>
#if MOBILEGL_PIPE_PUSH
// MOBILEGL_PIPE_POISON is DERIVED in the header below (PipeInputs.h:20-26) and nowhere
// else, so a TU that tests it without this include silently reads it as 0. That is
// invisible in a push build (where it really is 0) and in a verify build (where
// -DMOBILEGL_PIPE_VERIFY=1 is on the command line); MOBILEGL_BUILD_DISAGGREGATED is the
// one arming condition that lives behind the header, so a split build is the first place
// the refusals below stop being fatal while the expectations still say they are.
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Impl/Pipe/FramebufferEmit.h>
#include <MG_Impl/Pipe/SetHashSuppressor.h>
#include <MG_Pipe/PipeApply.h>
@@ -234,7 +241,8 @@ TEST(FramebufferEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
X(FramebufferEmit, ANamedRecordIsSuppressedPerObjectAndNeverAgainstABoundRecord) \
X(FramebufferEmit, ADrawBufferTokenAboveTheWireWidthIsRefusedNotTruncated) \
X(FramebufferEmit, ALayeredCubeAttachmentDoesNotAssertAFaceItCannotKnow) \
X(FramebufferEmit, EveryNonTexturePointCarriesTheUnknownSentinelsRatherThanZero)
X(FramebufferEmit, EveryNonTexturePointCarriesTheUnknownSentinelsRatherThanZero) \
X(FramebufferEmit, ADeadFramebuffersNamedRecordLatchIsRetired)
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
@@ -772,6 +780,27 @@ TEST(FramebufferEmit, EveryNonTexturePointCarriesTheUnknownSentinelsRatherThanZe
"only moved field is the attachment's texture target would be suppressed";
(void)probe;
}
// ============================ final review C-2 ============================
//
// A framebuffer has no wire lifetime (D-I2), so the only client state under its handle is this
// emitter's per-object Named latch - and the death helper retires it before the slot is freed,
// the shape every P4a kind takes (ID-8). A recycled handle's Gen already refused the stale
// latch, so this pins the hygiene rather than a picture.
TEST(FramebufferEmit, ADeadFramebuffersNamedRecordLatchIsRetired) {
FramebufferScope scope;
MGPipeHandle handle{};
{
const auto fbo = MakeShared<FramebufferObject>(31);
const auto color = MakeColorTexture(32, 8);
fbo->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D);
handle = MGPipeFramebufferEmitter::HandleFor(*fbo);
ASSERT_GT(Framebuffers().EmitFramebufferByName(*fbo), 0u) << "the Named record did not go out";
ASSERT_TRUE(Framebuffers().NamedRecordIsLatched(handle));
}
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::Framebuffer, handle));
EXPECT_FALSE(Framebuffers().NamedRecordIsLatched(handle))
<< "the dead framebuffer's Named latch survived its death";
}
#endif // MOBILEGL_PIPE_PUSH
// =========================================================================================
+47 -1
View File
@@ -45,9 +45,17 @@
#include <MG_Pipe/MGPipe.h>
#if MOBILEGL_PIPE_PUSH
#include "Init.h"
// MOBILEGL_PIPE_POISON is DERIVED in the header below (PipeInputs.h:20-26) and nowhere
// else, so a TU that tests it without this include silently reads it as 0. That is
// invisible in a push build (where it really is 0) and in a verify build (where
// -DMOBILEGL_PIPE_VERIFY=1 is on the command line); MOBILEGL_BUILD_DISAGGREGATED is the
// one arming condition that lives behind the header, so a split build is the first place
// the refusals below stop being fatal while the expectations still say they are.
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Impl/GLImpl/Program/GL_Program.h>
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
#include <MG_Impl/Pipe/ImageEmit.h>
#include <MG_Impl/Pipe/TextureEmit.h>
#include <MG_Impl/Pipe/SetHashSuppressor.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Pipe/PipeApply.h>
@@ -347,7 +355,8 @@ TEST(ImageEmit, AMakeCurrentClearsTheImageSetAndAdvancesItsSerial) {
X(ImageEmit, AZeroHighWaterMarkEmitsNothingWithoutHashing) \
X(ImageEmit, AnAccessModeChangeAloneStillEmitsTheSet) \
X(ImageEmit, AnInternalFormatChangeAloneStillEmitsTheSet) \
X(ImageEmit, TheApplicationsFormatAndAccessTravelUnrecast)
X(ImageEmit, TheApplicationsFormatAndAccessTravelUnrecast) \
X(ImageEmit, AnImageBoundTextureIsMarkedShaderImageBoundAtTheBind)
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
@@ -516,6 +525,43 @@ void main() { imageStore(img, ivec2(0), vec4(1.0)); }
EXPECT_EQ(Emitter().LastImageViews()[1].InternalFormat, static_cast<Uint32>(GL_RGBA8UI));
GL::UseProgram(0);
}
// FINAL REVIEW M-A: glBindImageTexture IS THE EARLIEST PRODUCER OF kMGPipeBindShaderImage -
// the bit the ImageBindableHint is derived from - and the emitted image set's walk is D-A4's
// (any texture named in an emitted MGPImageView). The hint is the PREVENTION half of the
// texture-remint stall class: a texture the server knows may be image-bound is allocated
// image-bindable up front, so it has to arrive before the first sync, i.e. at the bind.
// Nothing produced the bit before the fix round.
TEST(ImageEmit, AnImageBoundTextureIsMarkedShaderImageBoundAtTheBind) {
EmitterScope scope;
MGPipeTextureEmitterInstance().ResetForTest();
const GLuint name = MakeImageTexture();
const auto& texture = Ctx().GetTextureObject(name);
ASSERT_TRUE(texture);
const MGPipeHandle handle = MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, texture->GetLifetimeId());
ASSERT_FALSE(MGPipeHandleIsNull(handle));
EXPECT_EQ(MGPipeTextureEmitterInstance().TextureBindMask(handle) & kMGPipeBindShaderImage, 0)
<< "nothing has image-bound this texture yet";
GL::BindImageTexture(0, name, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
EXPECT_NE(MGPipeTextureEmitterInstance().TextureBindMask(handle) & kMGPipeBindShaderImage, 0)
<< "glBindImageTexture did not mark the texture image-bound";
static const char* kOneImage = R"(#version 430 core
layout(local_size_x = 1) in;
layout(binding = 0, rgba8) uniform image2D img;
void main() { imageStore(img, ivec2(0, 0), vec4(1.0)); }
)";
const GLuint program = MakeComputeProgram(kOneImage);
GL::UseProgram(program);
Emitter().EmitShaderImages(Ctx());
ASSERT_GE(Emitter().Window(), 1u);
EXPECT_TRUE(Emitter().LastImageViews()[0].Res == handle);
EXPECT_NE(MGPipeTextureEmitterInstance().TextureBindMask(handle) & kMGPipeBindShaderImage, 0)
<< "the emitted image set's walk does not carry the bit either";
GL::UseProgram(0);
GL::BindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
}
} // namespace
#endif // MOBILEGL_PIPE_PUSH
+29 -1
View File
@@ -50,6 +50,13 @@
#include <MG_Pipe/MGPipe.h>
#if MOBILEGL_PIPE_PUSH
#include "Init.h"
// MOBILEGL_PIPE_POISON is DERIVED in the header below (PipeInputs.h:20-26) and nowhere
// else, so a TU that tests it without this include silently reads it as 0. That is
// invisible in a push build (where it really is 0) and in a verify build (where
// -DMOBILEGL_PIPE_VERIFY=1 is on the command line); MOBILEGL_BUILD_DISAGGREGATED is the
// one arming condition that lives behind the header, so a split build is the first place
// the refusals below stop being fatal while the expectations still say they are.
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Impl/GLImpl/Program/GL_Program.h>
#include <MG_Impl/Pipe/ProgramEmit.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
@@ -501,7 +508,8 @@ TEST(ProgramEmit, TheProgramRecordSurvivesAMakeCurrentWhileTheThreeBindingsDoNot
X(ProgramEmit, AReLinkReIssuesOnTheSameHandle) \
X(ProgramEmit, TheDrawAndDispatchProgramsAreTwoIndependentSlots) \
X(ProgramEmit, AnUnchangedProgramEmitsNothingAtAll) \
X(ProgramEmit, AReIssuedCreateReSendsTheDefaultUniformBlock)
X(ProgramEmit, AReIssuedCreateReSendsTheDefaultUniformBlock) \
X(ProgramEmit, ADeadProgramsRecordLatchIsRetiredAtItsDeath)
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
@@ -774,6 +782,26 @@ void main() { gl_Position = vec4(0.0); EmitVertex(); }
}
EXPECT_GT(Emitter().GlobalConstantsSetCount(), setsBefore);
}
// FINAL REVIEW C-2: the death helper forwards to this emitter before the slot is freed, so
// a dead program's handle no longer reads as published in the record memo between the death
// and the recycle (the memo's own Gen test covers only the recycle).
TEST(ProgramEmit, ADeadProgramsRecordLatchIsRetiredAtItsDeath) {
EmitterScope scope;
const GLuint name = MakeVsFsProgram();
MGPipeHandle handle{};
{
const SharedPtr<ProgramObject>& program = Ctx().GetProgramObject(name);
ASSERT_TRUE(program);
Uint64 bytes = 0;
handle = Emitter().AcquireShaderCso(*program, bytes);
ASSERT_FALSE(MGPipeHandleIsNull(handle));
ASSERT_TRUE(Emitter().RecordIsPublished(handle));
}
GL::DeleteProgram(name); // not in use: the frontend object dies here
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::ShaderCso, handle));
EXPECT_FALSE(Emitter().RecordIsPublished(handle))
<< "a dead program still reads as published in the program emitter's memo";
}
} // namespace
#endif // MOBILEGL_PIPE_PUSH
+13 -1
View File
@@ -56,6 +56,13 @@
#include <MG_Pipe/MGPipe.h>
#if MOBILEGL_PIPE_PUSH
#include <Config.h>
// MOBILEGL_PIPE_POISON is DERIVED in the header below (PipeInputs.h:20-26) and nowhere
// else, so a TU that tests it without this include silently reads it as 0. That is
// invisible in a push build (where it really is 0) and in a verify build (where
// -DMOBILEGL_PIPE_VERIFY=1 is on the command line); MOBILEGL_BUILD_DISAGGREGATED is the
// one arming condition that lives behind the header, so a split build is the first place
// the refusals below stop being fatal while the expectations still say they are.
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Impl/Pipe/ResourceTracker.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Impl/Pipe/VertexInputEmit.h>
@@ -1246,7 +1253,12 @@ namespace {
TEST(ResourceEmit, TheLiveHostWritesWireFiresOnTheCallAPersistentMapProducerWouldSetItOn) {
#if !MOBILEGL_PIPE_PUSH
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
#elif !(MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY)
// MOBILEGL_PIPE_VERIFY alone, NOT `POISON || VERIFY`. PinNoLiveHostWrites is compiled
// under `#if MOBILEGL_PIPE_VERIFY` only (PipeApply.cpp:838-853), so in a split build -
// where POISON is armed by MOBILEGL_BUILD_DISAGGREGATED but VERIFY is off - the wire
// genuinely is compiled out and this case must skip. The wrong disjunction was masked
// until now by POISON being invisible in this TU at all (see the include at the top).
#elif !MOBILEGL_PIPE_VERIFY
GTEST_SKIP() << "Fatal{PipeLiveHostWrites} is a MOBILEGL_PIPE_VERIFY wire and is compiled out here";
#elif !MGTEST_HAVE_FORK
GTEST_SKIP() << "no fork on this platform; the wire's verdict is std::abort()";
+48 -1
View File
@@ -56,8 +56,16 @@
#include <cstring>
#include "Init.h"
// MOBILEGL_PIPE_POISON is DERIVED in the header below (PipeInputs.h:20-26) and nowhere
// else, so a TU that tests it without this include silently reads it as 0. That is
// invisible in a push build (where it really is 0) and in a verify build (where
// -DMOBILEGL_PIPE_VERIFY=1 is on the command line); MOBILEGL_BUILD_DISAGGREGATED is the
// one arming condition that lives behind the header, so a split build is the first place
// the refusals below stop being fatal while the expectations still say they are.
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Impl/GLImpl/Program/GL_Program.h>
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
#include <MG_Impl/Pipe/TextureEmit.h>
#include <MG_Impl/Pipe/SamplerEmit.h>
#include <MG_Impl/Pipe/SetHashSuppressor.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
@@ -566,7 +574,8 @@ TEST(SamplerEmit, AMakeCurrentTakesTheUnitSetsAndLeavesTheCsoAndViewRecordsStand
X(SamplerEmit, ABoundSamplerStateHoldsItsCsoUntilTheUnitMoves) \
X(SamplerEmit, AReferencedCsoIsNeverTheLruVictim) \
X(SamplerEmit, AFullyPinnedCacheMintsBeyondItsCapacityAndCountsIt) \
X(SamplerEmit, AReleaseThisCacheNeverHandedOutIsCountedRatherThanAbsorbed)
X(SamplerEmit, AReleaseThisCacheNeverHandedOutIsCountedRatherThanAbsorbed) \
X(SamplerEmit, AResolvedSamplerViewMarksItsTextureAsSamplerBound)
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
@@ -1088,6 +1097,44 @@ namespace {
// turned from a compiled-out assert into a number.
EXPECT_EQ(Cache().GetCounters().ReferencedEvictions, 0u);
}
// FINAL REVIEW M-A: THE SAMPLER-VIEW RESOLUTION IS D-A4's PRODUCER OF kMGPipeBindSampler.
// "Any texture the sampler-view resolution names in an emitted MGPBoundView" carries the
// sticky bit from then on; a texture bound to a unit no sampler uniform resolves does not.
// Nothing produced the bit before the fix round.
TEST(SamplerEmit, AResolvedSamplerViewMarksItsTextureAsSamplerBound) {
EmitterScope scope;
MGPipeTextureEmitterInstance().ResetForTest();
namespace GL = MobileGL::MG_Impl::GLImpl;
const Uint program = MakeSamplerProgram();
GLint linked = 0;
GL::GetProgramiv(program, GL_LINK_STATUS, &linked);
ASSERT_EQ(linked, GL_TRUE);
GL::UseProgram(program);
const GLint location = GL::GetUniformLocation(program, "sampled");
ASSERT_GE(location, 0);
GL::Uniform1i(location, 3);
GLuint sampledName = 0;
GLuint unsampledName = 0;
const SharedPtr<ITextureObject> sampled = MakeCompleteTexture(sampledName, 4);
const SharedPtr<ITextureObject> unsampled = MakeCompleteTexture(unsampledName, 4);
BindTextureToUnit(3, sampled);
BindTextureToUnit(5, unsampled);
ASSERT_GT(Emitter().EmitSamplerViews(Ctx()), 0u);
const MGPBoundView& resolved = Emitter().LastBoundViews()[3];
ASSERT_FALSE(MGPipeHandleIsNull(resolved.Texture));
EXPECT_NE(MGPipeTextureEmitterInstance().TextureBindMask(resolved.Texture) & kMGPipeBindSampler, 0)
<< "the texture a sampler view was resolved for does not carry kMGPipeBindSampler";
const MGPipeHandle unsampledHandle =
MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, unsampled->GetLifetimeId());
if (!MGPipeHandleIsNull(unsampledHandle)) {
EXPECT_EQ(MGPipeTextureEmitterInstance().TextureBindMask(unsampledHandle) & kMGPipeBindSampler, 0)
<< "a texture no sampler uniform resolves to was marked sampler-bound";
}
GL::UseProgram(0);
}
} // namespace
#endif // MOBILEGL_PIPE_PUSH
+339 -8
View File
@@ -57,6 +57,13 @@
#include "Includes.h"
#include <MG_Pipe/MGPipe.h>
#if MOBILEGL_PIPE_PUSH
// MOBILEGL_PIPE_POISON is DERIVED in the header below (PipeInputs.h:20-26) and nowhere
// else, so a TU that tests it without this include silently reads it as 0. That is
// invisible in a push build (where it really is 0) and in a verify build (where
// -DMOBILEGL_PIPE_VERIFY=1 is on the command line); MOBILEGL_BUILD_DISAGGREGATED is the
// one arming condition that lives behind the header, so a split build is the first place
// the refusals below stop being fatal while the expectations still say they are.
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Impl/Pipe/PipeFill.h>
#include <MG_Impl/Pipe/SamplerEmit.h>
#include <MG_Impl/Pipe/TextureEmit.h>
@@ -216,7 +223,16 @@ TEST(TextureEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
X(TextureEmit, WithTheSamplerBitClearTheTextureFamilyGateIsFalseAndNothingReachesTheApplier) \
X(TextureEmit, \
WithTheBufferResourceBitClearTheTextureFamilyGateIsFalseAndNothingReachesTheApplier) \
X(TextureEmit, EveryDKTwoDependencyRowGatesItsOwnFamilyAndTheMirrorPairsStayLive)
X(TextureEmit, EveryDKTwoDependencyRowGatesItsOwnFamilyAndTheMirrorPairsStayLive) \
X(TextureEmit, ALevelDefinedAfterAnEmittedButUnconsumedUploadKeepsThatUpload) \
X(TextureEmit, AChainTruncationKeepsTheSurvivingLevelsPendingUploads) \
X(TextureEmit, ARedefinitionOfANonBaseLevelAtANewSizeDropsOnlyThatLevelsPendingUpload) \
X(TextureEmit, ADeadTexturesHandleResolvesToNothingAndLeavesTheDrainList) \
X(TextureEmit, ATextureRecycledOntoADeadSlotDoesNotInheritTheDrainEntry) \
X(TextureEmit, ADeadRenderbuffersEntryIsRetiredWithItsSlot) \
X(TextureEmit, ARefusedParamsRecordDoesNotAdvanceTheLatch) \
X(TextureEmit, ADeadTexturesSamplerViewLatchIsRetiredAtItsDeath) \
X(TextureEmit, ATextureBornBeforeTheConsumerRegisteredGetsItsRecordFromItsFirstParamsPublication)
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
@@ -1233,6 +1249,11 @@ TEST(TextureEmit, ARecycledTextureSlotDoesNotInheritItsPredecessorsBindMask) {
Textures().NoteTextureBoundAs(firstHandle, kMGPipeBindShaderImage);
ASSERT_NE(Textures().TextureBindMask(firstHandle) & kMGPipeBindRenderTarget, 0);
}
// Since the final review's C-2 the reference goes back AT THE DEATH, through the death
// helper's forward, and not first at the recycle.
EXPECT_EQ(cache.RefCountOf(firstCso), 0u)
<< "the dead texture's cache reference survived its death; the death helper did not reach the emitter";
EXPECT_EQ(Textures().TextureBindMask(firstHandle), 0u) << "a dead handle still reads its sticky mask";
const auto second = MakeTexture2D(22, 8);
const MGPipeHandle secondHandle = Textures().FindTexture(*second);
ASSERT_EQ(secondHandle.Slot, firstHandle.Slot) << "the slot was not recycled; the case proves nothing";
@@ -1275,6 +1296,304 @@ TEST(TextureEmit, ALevelMarkedCleanIsCollectedAtTheNextDrain) {
EXPECT_EQ(Textures().DrainListSize(), 1u)
<< "a re-dirtied level did not go back on the drain list, so its texels are owed for ever";
}
// ============================ final review C-1 ============================
//
// THE CLIENT PASSES THE LEVEL IT REDEFINES. AllocateStorage is per (uploadTarget, level) while
// the descriptor carries only the base extent and the level count, so only the caller can tell
// the applier WHICH storage a respecify replaces (wire C1's MGPRespecifiedLevel); before the fix
// every texture respecify took the whole-resource arm and dropped every pending upload of the
// texture - including a level the applier had already accepted and whose client flag was
// therefore already clear (D-D5 step 1). Driven through the real AllocateStorage.
TEST(TextureEmit, ALevelDefinedAfterAnEmittedButUnconsumedUploadKeepsThatUpload) {
TextureScope scope;
const auto texture = MakeShared<TextureObject2D>(90);
texture->SetInternalFormat(TextureInternalFormat::RGBA8);
// glTexImage2D(level 0, data)
texture->AllocateStorage(TextureUploadTarget::Texture2D, 0, MipmapInput{IntVec3{64, 64, 1}, 64 * 64 * 4});
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, IntVec3{64, 64, 1});
// A verb the texture is not reached by: the drain emits level 0, the applier accepts, the
// client clears its flag. Nothing has consumed the entry.
Textures().DrainTextureSubData(Ctx());
const MGPipeHandle handle = Textures().FindTexture(*texture);
const MGPipeResourceRecord* record = AppliedTexture(handle);
ASSERT_NE(record, nullptr);
ASSERT_EQ(Textures().RefusedSubDataCount(), 0u);
ASSERT_EQ(record->PendingUploads.size(), 1u);
ASSERT_FALSE(texture->IsStorageDirty(TextureUploadTarget::Texture2D, 0));
// glTexImage2D(level 1, data): a DIFFERENT level.
texture->AllocateStorage(TextureUploadTarget::Texture2D, 1, MipmapInput{IntVec3{32, 32, 1}, 32 * 32 * 4});
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 1, IntVec3{0, 0, 0}, IntVec3{32, 32, 1});
record = AppliedTexture(handle);
ASSERT_NE(record, nullptr);
Bool levelZeroPending = false;
for (const auto& pending : record->PendingUploads) {
if (pending.Level == 0) levelZeroPending = true;
}
EXPECT_TRUE(levelZeroPending)
<< "defining level 1 dropped level 0's accepted-but-unconsumed pending upload (PendingUploads.size()="
<< record->PendingUploads.size() << ") while level 0's client dirty flag is "
<< (texture->IsStorageDirty(TextureUploadTarget::Texture2D, 0) ? "set" : "CLEAR - the texels are owed by nobody");
// And after the next drain both levels stand in the set.
Textures().DrainTextureSubData(Ctx());
record = AppliedTexture(handle);
ASSERT_NE(record, nullptr);
Bool zeroAfter = false;
Bool oneAfter = false;
for (const auto& pending : record->PendingUploads) {
if (pending.Level == 0) zeroAfter = true;
if (pending.Level == 1) oneAfter = true;
}
EXPECT_TRUE(oneAfter);
EXPECT_TRUE(zeroAfter) << "level 0's texels are lost: not pending, flag clear";
}
// A chain truncation - glGenerateMipmap fitting the chain, a base redefinition discarding its
// tail - removes the levels at and above the cut and nothing below it. Before the fix it was a
// whole-resource respecify and took level 0's standing upload with the tail.
TEST(TextureEmit, AChainTruncationKeepsTheSurvivingLevelsPendingUploads) {
TextureScope scope;
const auto texture = MakeTexture2D(93, 64, /*levels=*/3);
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, IntVec3{64, 64, 1});
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 2, IntVec3{0, 0, 0}, IntVec3{16, 16, 1});
Textures().DrainTextureSubData(Ctx());
const MGPipeHandle handle = Textures().FindTexture(*texture);
const MGPipeResourceRecord* record = AppliedTexture(handle);
ASSERT_NE(record, nullptr);
ASSERT_EQ(record->PendingUploads.size(), 2u);
ASSERT_FALSE(texture->IsStorageDirty(TextureUploadTarget::Texture2D, 0));
texture->TruncateMipmapLevels(TextureUploadTarget::Texture2D, 1);
record = AppliedTexture(handle);
ASSERT_NE(record, nullptr);
EXPECT_EQ(record->Desc.Levels, 1u);
Bool zeroPending = false;
Bool twoPending = false;
for (const auto& pending : record->PendingUploads) {
if (pending.Level == 0) zeroPending = true;
if (pending.Level == 2) twoPending = true;
}
EXPECT_TRUE(zeroPending) << "truncating the chain above level 0 dropped level 0's standing upload";
EXPECT_FALSE(twoPending) << "a level the truncation removed kept a pending upload against storage that is gone";
}
// A non-base level redefined at a new size moves NO descriptor field (the descriptor carries
// the base extent and the level count), so the emitter's descriptor dedupe used to swallow the
// respecify and the applier kept a box sized for the OLD level - which Espryt would have
// uploaded past the end of the new one. A per-level respecify reaches the applier whether or
// not the descriptor moved, and drops exactly that level.
TEST(TextureEmit, ARedefinitionOfANonBaseLevelAtANewSizeDropsOnlyThatLevelsPendingUpload) {
TextureScope scope;
const auto texture = MakeTexture2D(96, 16, /*levels=*/2);
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, IntVec3{16, 16, 1});
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 1, IntVec3{0, 0, 0}, IntVec3{8, 8, 1});
Textures().DrainTextureSubData(Ctx());
const MGPipeHandle handle = Textures().FindTexture(*texture);
const MGPipeResourceRecord* record = AppliedTexture(handle);
ASSERT_NE(record, nullptr);
ASSERT_EQ(record->PendingUploads.size(), 2u);
const Uint64 serialBefore = record->Serial;
// glTexImage2D(level 1) at 4x4: the base is still 16x16 and the chain still two levels.
texture->AllocateStorage(TextureUploadTarget::Texture2D, 1, MipmapInput{IntVec3{4, 4, 1}, 4 * 4 * 4});
record = AppliedTexture(handle);
ASSERT_NE(record, nullptr);
EXPECT_GT(record->Serial, serialBefore) << "the per-level respecify never reached the applier";
Bool zeroPending = false;
Bool onePending = false;
for (const auto& pending : record->PendingUploads) {
if (pending.Level == 0) zeroPending = true;
if (pending.Level == 1) onePending = true;
}
EXPECT_TRUE(zeroPending) << "redefining level 1 dropped level 0's standing upload";
EXPECT_FALSE(onePending) << "level 1's 8x8 box survived its redefinition onto a 4x4 level";
}
// ============================ final review C-2 ============================
//
// A DEAD HANDLE RESOLVES TO NOTHING AND THE DRAIN LIST DROPS IT AT THE DEATH. The allocator's
// generation moves only at the next hand-out, so between a death and a recycle the dead handle
// compared equal to the slot's generation and the emitter answered the freed ITextureObject*;
// the drain then called a virtual on it once per verb until something recycled the slot.
TEST(TextureEmit, ADeadTexturesHandleResolvesToNothingAndLeavesTheDrainList) {
TextureScope scope;
MGPipeHandle handle{};
{
const auto texture = MakeShared<TextureObject2D>(91);
texture->SetInternalFormat(TextureInternalFormat::RGBA8);
texture->AllocateStorage(TextureUploadTarget::Texture2D, 0, MipmapInput{IntVec3{16, 16, 1}, 16 * 16 * 4});
// glTexSubImage2D: the level goes on the drain list; NO verb follows before the delete.
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, IntVec3{16, 16, 1});
handle = Textures().FindTexture(*texture);
ASSERT_FALSE(MGPipeHandleIsNull(handle));
ASSERT_EQ(Textures().DrainListSize(), 1u);
} // glDeleteTextures: the last SharedPtr drops, ~TextureObjectBase frees the slot
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::Texture, handle));
EXPECT_EQ(Textures().ResolveTexture(handle), nullptr)
<< "ResolveTexture hands back the freed ITextureObject* of a dead-but-not-recycled handle";
EXPECT_EQ(Textures().DrainListSize(), 0u)
<< "the dead texture's level is still on the drain list, so the next verb walks it";
// And the next drain has nothing to say about it: no record, no refusal, no emission.
Textures().DrainTextureSubData(Ctx());
EXPECT_EQ(Textures().SubDataCount(), 0u);
EXPECT_EQ(Textures().RefusedSubDataCount(), 0u);
// The null came from the DEATH PATH retiring the entry, not from the loud refusal that
// guards a death path that skipped the emitter.
EXPECT_EQ(Textures().DeadResolveCount(), 0u)
<< "the dead handle was refused by ResolveTexture's guard, so the death helper never told the emitter";
}
// The sampler view minted off the texture's lifetime id (D-F2) has its own record memo in the
// sampler emitter; the texture's death retires it through the view's death helper.
TEST(TextureEmit, ADeadTexturesSamplerViewLatchIsRetiredAtItsDeath) {
TextureScope scope;
MGPipeHandle viewHandle{};
{
const auto texture = MakeTexture2D(89, 8);
const MGPipeHandle handle = Textures().FindTexture(*texture);
Uint64 bytes = 0;
viewHandle = MGPipeSamplerEmitterInstance().AcquireSamplerView(*texture, handle, bytes);
ASSERT_FALSE(MGPipeHandleIsNull(viewHandle));
ASSERT_TRUE(MGPipeSamplerEmitterInstance().RecordIsPublished(viewHandle));
}
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::SamplerViewCso, viewHandle));
EXPECT_FALSE(MGPipeSamplerEmitterInstance().RecordIsPublished(viewHandle))
<< "a dead sampler view still reads as published in the sampler emitter's memo";
}
// ABA: the recycled slot's new texture owns the drain list entry it makes and none of its
// predecessor's.
TEST(TextureEmit, ATextureRecycledOntoADeadSlotDoesNotInheritTheDrainEntry) {
TextureScope scope;
MGPipeHandle deadHandle{};
{
const auto dead = MakeTexture2D(97, 8);
dead->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, IntVec3{8, 8, 1});
deadHandle = Textures().FindTexture(*dead);
ASSERT_EQ(Textures().DrainListSize(), 1u);
}
EXPECT_EQ(Textures().DrainListSize(), 0u) << "the death did not retire the drain entry";
const auto successor = MakeTexture2D(98, 32);
const MGPipeHandle handle = Textures().FindTexture(*successor);
ASSERT_EQ(handle.Slot, deadHandle.Slot) << "the slot was not recycled; the case proves nothing";
ASSERT_NE(handle.Gen, deadHandle.Gen);
EXPECT_EQ(Textures().DrainListSize(), 0u) << "the successor inherited a drain entry it never made";
successor->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, IntVec3{32, 32, 1});
EXPECT_EQ(Textures().DrainListSize(), 1u);
Textures().DrainTextureSubData(Ctx());
EXPECT_EQ(Textures().SubDataCount(), 1u) << "exactly the successor's level went out";
EXPECT_TRUE(Textures().LastSubData().Res == handle);
EXPECT_EQ(Textures().LastSubData().UnionBox.W, 32u);
EXPECT_EQ(Textures().DrainListSize(), 0u);
}
// The renderbuffer table has no pointer to dangle but the same stale entry: a dead
// renderbuffer's sticky mask must not be readable through its dead handle.
TEST(TextureEmit, ADeadRenderbuffersEntryIsRetiredWithItsSlot) {
TextureScope scope;
MGPipeHandle handle{};
{
const auto renderbuffer = MakeShared<RenderbufferObject>(94);
handle = Textures().FindRenderbuffer(*renderbuffer);
ASSERT_FALSE(MGPipeHandleIsNull(handle));
Textures().NoteRenderbufferBoundAs(handle, kMGPipeBindRenderTarget);
ASSERT_NE(Textures().RenderbufferBindMask(handle) & kMGPipeBindRenderTarget, 0);
}
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::Renderbuffer, handle));
EXPECT_EQ(Textures().RenderbufferBindMask(handle), 0u)
<< "a dead renderbuffer's entry still answers through its dead handle";
const auto successor = MakeShared<RenderbufferObject>(99);
const MGPipeHandle successorHandle = Textures().FindRenderbuffer(*successor);
ASSERT_EQ(successorHandle.Slot, handle.Slot);
EXPECT_EQ(Textures().RenderbufferBindMask(successorHandle), 0u);
}
// ============================ final review m-1 (audit F-7) ============================
//
// set_texture_params LATCHES ON ACCEPTANCE, like the sub-data and respecify paths. A record the
// applier refused used to advance the version latch anyway, so the parameters were not re-sent
// until the next glTexParameter* moved a version. A refusal for a missing record is HEALED now
// (the case after this one), so the property is driven through a refusal on the merits: with no
// backend consumer the applier's belt refuses the parameters AND the healing create, and the
// emitter is driven directly (the contract hook would not even emit without the consumer).
TEST(TextureEmit, ARefusedParamsRecordDoesNotAdvanceTheLatch) {
TextureScope scope;
const auto texture = MakeTexture2D(95, 8);
const MGPipeHandle handle = Textures().FindTexture(*texture);
ASSERT_NE(AppliedTexture(handle), nullptr);
const Uint64 serialBefore = AppliedTexture(handle)->ParamsSerial;
// A parameter moves (a LOD write on the built-in sampler, which the format setter's earlier
// publication did not carry) while no consumer is registered: refused, and not healable.
texture->GetSamplerObject()->SetLodBias(0.5f);
const Uint64 paramsBefore = Textures().ParamCount();
{
ScopedNoResourceOps noConsumer;
Textures().EmitTextureParams(*texture);
}
EXPECT_EQ(Textures().ParamCount(), paramsBefore + 1) << "the record was not even emitted";
EXPECT_EQ(Textures().RefusedParamCount(), 1u) << "the emitter did not see the refusal";
EXPECT_EQ(AppliedTexture(handle)->ParamsSerial, serialBefore) << "the refused record moved the serial";
// The consumer is back and the same parameters, no version moved, are published again:
// with the latch taken on the REFUSED call this returns early and the record never learns
// the LOD write.
MG_Pipe::MGPipeEmitTextureParams(*texture);
EXPECT_EQ(Textures().ParamCount(), paramsBefore + 2)
<< "a refused set_texture_params advanced the latch, so the parameters are not re-sent";
const MGPipeResourceRecord* record = AppliedTexture(handle);
ASSERT_NE(record, nullptr);
EXPECT_EQ(record->ParamsSerial, serialBefore + 1) << "the record never learned the LOD write";
EXPECT_EQ(record->Params.LodBias, 0.5f);
}
// THE RETRACE CENSUS's ONE RESIDUAL after m-1 went loud: a texture born while the family was
// not live - the context's default textures are constructed before the backend registers its
// consumer - has no record, and when the application's first glTexParameter* lands on it
// (texture 0) the record is refused and, before m-1, silently latched away for ever. The
// params path now heals the record the way the respecify path does: a create with no storage
// for the identity, the storage itself if the texture has any, then the parameters.
TEST(TextureEmit, ATextureBornBeforeTheConsumerRegisteredGetsItsRecordFromItsFirstParamsPublication) {
TextureScope scope;
SharedPtr<TextureObject2D> texture;
{
ScopedNoResourceOps noConsumer;
texture = MakeTexture2D(88, 8); // born, formatted and allocated with no consumer: no create
}
const MGPipeHandle handle = Textures().FindTexture(*texture);
ASSERT_FALSE(MGPipeHandleIsNull(handle));
ASSERT_EQ(AppliedTexture(handle), nullptr) << "the case needs a texture the applier never heard of";
ASSERT_FALSE(MGPipeHandleIsPublished(MGPipeKind::Texture, handle));
// glTexParameterf(GL_TEXTURE_LOD_BIAS) on it, with the consumer present now.
texture->GetSamplerObject()->SetLodBias(0.25f);
MG_Pipe::MGPipeEmitTextureParams(*texture);
EXPECT_EQ(Textures().RefusedParamCount(), 0u)
<< "the parameters of a texture born before the consumer were refused instead of healing its record";
const MGPipeResourceRecord* record = AppliedTexture(handle);
ASSERT_NE(record, nullptr) << "no record was healed";
EXPECT_TRUE(MGPipeHandleIsPublished(MGPipeKind::Texture, handle));
EXPECT_EQ(record->Desc.Width, 8u) << "the healed record carries no storage although the texture has some";
EXPECT_EQ(record->Desc.Levels, 1u);
EXPECT_EQ(record->ParamsSerial, 1u);
EXPECT_EQ(record->Params.LodBias, 0.25f);
// And a texture with NO storage at all - the default texture's shape - heals to a record
// with the identity only, which is what its parameters need and all a create says.
const auto bare = MakeShared<TextureObject2D>(87);
{
// its create went out with the consumer present, so take the record away again to model
// a birth the applier never saw
MGPipeApplierReleaseObjectRecords();
}
bare->GetSamplerObject()->SetLodBias(0.75f);
MG_Pipe::MGPipeEmitTextureParams(*bare);
const MGPipeHandle bareHandle = Textures().FindTexture(*bare);
const MGPipeResourceRecord* bareRecord = AppliedTexture(bareHandle);
ASSERT_NE(bareRecord, nullptr) << "a storage-less texture's parameters healed no record";
EXPECT_EQ(bareRecord->Desc.Width, 0u);
EXPECT_EQ(bareRecord->Params.LodBias, 0.75f);
EXPECT_EQ(Textures().RefusedParamCount(), 0u);
}
#endif // MOBILEGL_PIPE_PUSH
// =========================================================================================
@@ -1902,7 +2221,7 @@ TEST(TextureEmit, ARespecifyThatRedefinesNoStorageCarriesTheStickyMaskAndKeepsTh
// glTexStorage2D: an IMMUTABLE store, which is the whole reason this arm exists.
MGPResourceDesc allocated = TextureDesc(texture, 64, 151);
allocated.Immutable = 1;
allocated.Levels = 1;
allocated.Levels = 2; // level 1 exists for the named-level clause below
allocated.InternalFormat = 0x8058u; // GL_RGBA8
allocated.BindMask = static_cast<Uint16>(kMGPipeBindSampler);
ASSERT_TRUE(MGPipeApplyResourceRespecify(allocated, nullptr));
@@ -1931,17 +2250,29 @@ TEST(TextureEmit, ARespecifyThatRedefinesNoStorageCarriesTheStickyMaskAndKeepsTh
<< "the serial is the whole publication of a metadata update - the twin re-derives its "
"storage flags from the new mask on the strength of it";
// AND THE LEVEL POINTER DOES NOT CHANGE THE ANSWER. This is where ID-18 M4 refines C1:
// C1's rule drops the uploads against the storage a call REPLACES, and a call that replaces
// no storage replaces no level's coordinate system either, whatever level it names.
const MGPRespecifiedLevel levelZero{kTex2D, 0};
// A SECOND MASK MOVE WITH A NULL LEVEL STILL DROPS NOTHING - the client's mask republish
// passes null on purpose (wire-v3 §5 item 6) and this is its shape.
MGPResourceDesc maskedAgain = masked;
maskedAgain.BindMask = static_cast<Uint16>(masked.BindMask | kMGPipeBindRenderTarget);
ASSERT_TRUE(MGPipeApplyResourceRespecify(maskedAgain, nullptr, &levelZero));
ASSERT_TRUE(MGPipeApplyResourceRespecify(maskedAgain, nullptr, nullptr));
ASSERT_EQ(TextureRecordOf(11).PendingUploads.size(), 1u)
<< "a metadata update dropped the level it named";
<< "a metadata update with no level dropped a standing upload";
EXPECT_EQ(TextureRecordOf(11).Desc.BindMask, maskedAgain.BindMask);
// BUT A NAMED LEVEL IS DROPPED WHETHER OR NOT THE DESCRIPTOR MOVED (P4a final review C-1,
// refining the W11 clause that stood here): the pointer is the caller's statement that it
// reallocated that level, and the descriptor cannot contradict it - a non-base level
// redefined at a new size moves no descriptor field, so "identical storage fields" says
// nothing about that level's coordinate system. Level 1's entry goes; level 0's stays.
ASSERT_TRUE(MGPipeApplyResourceSubData(TextureUpload(texture, 1, MGPBox{0, 0, 0, 32, 32, 1}, 0), texels));
ASSERT_EQ(TextureRecordOf(11).PendingUploads.size(), 2u);
const MGPRespecifiedLevel levelOne{kTex2D, 1};
ASSERT_TRUE(MGPipeApplyResourceRespecify(maskedAgain, nullptr, &levelOne));
ASSERT_EQ(TextureRecordOf(11).PendingUploads.size(), 1u)
<< "a level-scoped respecify on an unchanged descriptor did not drop the level it named";
EXPECT_EQ(TextureRecordOf(11).PendingUploads[0].Level, 0u) << "it dropped the wrong level";
const MGPRespecifiedLevel levelZero{kTex2D, 0};
// THE NEGATIVE CONTROL, in the same case: move ONE storage-defining field and the same call
// is a redefinition again, which takes the level it names with it.
MGPResourceDesc reallocated = maskedAgain;
+152
View File
@@ -517,3 +517,155 @@ TEST(RingTest, DoorbellHandoffWakesBothSidesOnEveryPublish) {
EXPECT_TRUE(ok.load());
EXPECT_TRUE(ring.Invariants());
}
// ---------------------------------------------------------------------------
// P5 R-9: the five watermarks and the pad rule, from Ring.h's header comment.
//
// Nothing in the tree advanced any of the five before P5 - InitRingControl zeroed
// them and that was all - so these five cases pin the RULES against the sessions
// that are about to start writing them, rather than testing today's (absent)
// writers. Each one is the negative control for one sentence of that comment.
// ---------------------------------------------------------------------------
// R-9, sentence 0: all five start at zero, so "has not moved" and "moved to zero"
// are the same state and a waiter that starts before its peer cannot be fooled by
// a stale non-zero value left over from a previous session.
TEST(RingTest, WatermarksAreAllZeroUntilSomeoneAdvancesThem) {
alignas(4096) RingControl control{};
InitRingControl(control);
EXPECT_EQ(control.submittedSeq.load(), 0u);
EXPECT_EQ(control.appliedSeq.load(), 0u);
EXPECT_EQ(control.retiredSeq.load(), 0u);
EXPECT_EQ(control.completedFrameSerial.load(), 0u);
EXPECT_EQ(control.presentAckSerial.load(), 0u);
// ... while the two GENERATIONS start at one, because for them zero means
// "uninitialized" and must never be a legal value. The two conventions are
// opposite on purpose and are next to each other in the same struct.
EXPECT_EQ(control.serverEpoch.load(), 1u);
EXPECT_EQ(control.ringGeneration.load(), 1u);
}
// R-9, "every wait is >=, never ==". Both sides advance in jumps - a consumer that
// applies two records before republishing, a server that completes two frames in one
// poll - so an equality test misses its wakeup and the waiter hangs until the next
// coincidence. This case is that hang, made deterministic.
TEST(RingTest, AWatermarkWaiterMustTestGreaterOrEqualRatherThanEqual) {
alignas(4096) RingControl control{};
InitRingControl(control);
const std::uint64_t mySeq = 7;
// The peer jumps straight past the value this waiter cares about.
control.appliedSeq.store(mySeq + 1, std::memory_order_release);
const std::uint64_t seen = control.appliedSeq.load(std::memory_order_acquire);
EXPECT_FALSE(seen == mySeq) << "an equality waiter is still asleep at this point";
EXPECT_TRUE(seen >= mySeq) << "the >= waiter this contract mandates has been released";
}
// R-9, appliedSeq's row: advanced by the consumer for EVERY SINGLE RECORD, and P5
// forbids the 64-record batching the ring was designed for, because the verb barrier
// and every reply wait read it. The invariant that must hold after each Pop is
// `appliedSeq == records applied so far` - not "eventually", every time.
TEST(RingTest, AppliedSeqAdvancesOncePerRecordAndIsNeverBatchedInP5) {
RingFixture ring(1024);
constexpr int kRecords = 12;
for (int i = 0; i < kRecords; ++i) {
ASSERT_TRUE(ring.WriteRecord(static_cast<std::uint16_t>(i + 1), 16,
static_cast<std::uint8_t>(i)));
}
std::uint64_t applied = 0;
RingRecordView view{};
while (ring.Consumer().Pop(view)) {
++applied;
ring.Control().appliedSeq.store(applied, std::memory_order_release);
// The reader's guarantee, checked at EVERY record rather than at the end:
// a batched watermark would sit at 0 here for 63 of every 64 iterations,
// and a client barrier reading it would block on work that already ran.
EXPECT_EQ(ring.Control().appliedSeq.load(std::memory_order_acquire), applied);
}
EXPECT_EQ(applied, static_cast<std::uint64_t>(kRecords));
EXPECT_TRUE(ring.Invariants());
}
// R-9, "batching may only make a watermark LATE, never early". retiredSeq is the one
// the staging allocator reclaims behind, so a value published ahead of the actual
// drain hands live bytes back to the producer. Late is merely slow; early is a
// use-after-free that nothing on this ring checksums.
TEST(RingTest, ALazyWatermarkMayTrailTheWorkButMustNeverLeadIt) {
RingFixture ring(1024);
constexpr int kRecords = 8;
for (int i = 0; i < kRecords; ++i) {
ASSERT_TRUE(ring.WriteRecord(static_cast<std::uint16_t>(i + 1), 16,
static_cast<std::uint8_t>(i)));
}
std::uint64_t drained = 0;
RingRecordView view{};
while (ring.Consumer().Pop(view)) {
++drained;
// A deliberately lazy publisher: only every third record. This is legal.
if (drained % 3 == 0) {
ring.Control().retiredSeq.store(drained, std::memory_order_release);
}
EXPECT_LE(ring.Control().retiredSeq.load(std::memory_order_acquire), drained)
<< "retiredSeq ran ahead of the drain; those staged bytes are still live";
}
// Trailing at the end is fine and is what "late" means.
EXPECT_LE(ring.Control().retiredSeq.load(), static_cast<std::uint64_t>(kRecords));
EXPECT_TRUE(ring.Invariants());
}
// R-9's last sentence, and the one with no other detector: kRecPad DOES NOT ADVANCE
// SEQ. A wrap filler is framing - no opcode, no payload, no reply slot - so a side
// that counts it drifts from the side that does not, by one per wrap, for ever. And
// because seq IS the reply-slot id (R-3), a drifted seq reads ANOTHER CALL'S ANSWER
// instead of failing. Here the ring is sized so the last record cannot fit before the
// wrap boundary, which forces the producer to emit a filler; the consumer must count
// the records and not the filler.
TEST(RingTest, AWrapFillerDoesNotAdvanceTheRecordSequence) {
RingFixture ring(256);
constexpr std::uint64_t kPayload = 56; // 8-byte header + 56 = 64 per record
// Three records fill 192 of 256 bytes; the fourth needs 64 and only 64 remain, so
// it lands exactly at the boundary. The fifth is what forces the filler.
std::uint64_t written = 0;
for (int i = 0; i < 3; ++i) {
ASSERT_TRUE(ring.WriteRecord(static_cast<std::uint16_t>(i + 1), kPayload,
static_cast<std::uint8_t>(i)));
++written;
}
const std::uint64_t headAfterThree = ring.Producer().LocalHead();
std::uint64_t popped = 0;
RingRecordView view{};
while (ring.Consumer().Pop(view)) {
// Pop skips fillers by contract, so a pad must never reach a caller that is
// about to number it. If one ever does, that is the drift itself.
EXPECT_EQ(view.flags & kRecPad, 0u) << "a wrap filler reached the record counter";
EXPECT_NE(view.kind, kRingPadRecordKind);
++popped;
}
EXPECT_EQ(popped, written) << "the consumer numbered something the producer did not send";
ring.Consumer().PublishApplied();
ring.Consumer().PublishRetired();
// Now drive the producer across the wrap and prove a filler really was emitted:
// the head advances by MORE than the records' own bytes, and that surplus is the
// pad. The record count still has to match.
for (int i = 0; i < 3; ++i) {
ASSERT_TRUE(ring.WriteRecord(static_cast<std::uint16_t>(i + 10), kPayload,
static_cast<std::uint8_t>(i + 10)));
++written;
}
const std::uint64_t headAfterSix = ring.Producer().LocalHead();
EXPECT_GE(headAfterSix - headAfterThree, 3u * (kPayload + sizeof(RingRecordHeader)));
while (ring.Consumer().Pop(view)) {
EXPECT_EQ(view.flags & kRecPad, 0u) << "a wrap filler reached the record counter";
++popped;
}
EXPECT_EQ(popped, written)
<< "the two sides' sequence spaces have drifted by the fillers between them";
EXPECT_TRUE(ring.Invariants());
}
+5 -1
View File
@@ -179,7 +179,7 @@ namespace MobileGL::MG_Util::PipeStats {
#if MOBILEGL_PIPE_PUSH
"render-state-cso-mints", "render-state-cso-binds", "map-persistent-roundtrips",
"framebuffer-emissions", "sampler-view-emissions", "sampler-state-emissions",
"shader-image-emissions", "client-tex-upload-emissions",
"shader-image-emissions", "client-tex-upload-emissions", "tex-remint-pulls",
#endif
};
const char* const kGateNames[kGateCount] = {
@@ -453,6 +453,10 @@ namespace MobileGL::MG_Util::PipeStats {
line += " sie=" + std::to_string(calls[static_cast<Uint32>(CallClass::ShaderImageEmissions)]);
line += " ctu=" +
std::to_string(calls[static_cast<Uint32>(CallClass::ClientTextureUploadEmissions)]);
// trp is the texture-remint pull count (ROADMAP open question 2): every one is a texture
// Espryt had already allocated and then had to re-mint image-bindable, replaying its
// levels from the client's shadow, because ImageBindableHint reached it too late.
line += " trp=" + std::to_string(calls[static_cast<Uint32>(CallClass::TextureRemintPulls)]);
#endif
line += "] gates[";
for (Uint32 i = 0; i < kGateCount; ++i) {
+8
View File
@@ -156,6 +156,14 @@ namespace MobileGL::MG_Util::PipeStats {
// hides is ~+6 ms/frame, so an emission-shape divergence has to be a difference of two
// numbers rather than something only a GPU can see.
ClientTextureUploadEmissions,
// THE TEXTURE-REMINT PULL RATE (ROADMAP open question 2; P4a final review M-A). Counted
// by Espryt once per transition in which a texture that ALREADY HAD backend storage is
// re-minted image-bindable and its defined levels are replayed from the client's shadow
// (RequireImageBindableStorage) - the reach-back a split cannot make (D-M) and the one
// ImageBindableHint exists to prevent. A texture whose hint arrived before its first
// sync is allocated image-bindable up front and never counts. `trp=` on the summary
// line; the number that decides MOBILEGL_PIPE_TEXEL_RETAIN_MB's default.
TextureRemintPulls,
#endif
Count
};
+14 -7
View File
@@ -62,6 +62,8 @@
CSO 在 client 侧内容寻址(Mesa `cso_cache` 先例):每类一张 `ska::flat_hash_map<xxHash, MGPipeHandle>`,容量上限 render-state 64 / vertex-elements 1024 / sampler 256 / sampler-view 4096 / shader 跟随 `ProgramObject` 生命周期,LRU 淘汰时发 `delete_*`。两个不同 program 设置了相同状态时 server 零状态转换。
> **[deviation] D-F2P4a 落地)**:上面这行的 **sampler-view 4096 项内容寻址是 P7Magma)的形状**Espryt 这一波把 sampler **view** 做成**按纹理对象身份寻址**——一纹理一视图,按纹理自己的 lifetime id 铸造,视图限制变了就在**同一个句柄**上重发。理由是 Espryt 的视图没有可共享的驱动侧对象,内容寻址只会多一张表和一次哈希。sampler **state** 的 256 项内容寻址照做,并且 P4a 给它加了**引用计数**:`MGPTextureParams::BuiltinSampler` 指着的项不允许被 LRU 挤掉(全被引用时超容铸造,计数在 `OverCapacityMints`),否则一次驱逐就会让一条标准记录指向一个已经换代的句柄。
**[deviation] D-G1P3a 落地):vertex-elements CSO 在 P3a 是身份寻址,不是内容寻址。** Espryt 根本没有 vertex-elements CSO,它有的是**逐 VAO 的 twin**`BackendVertexArrayObject``MobileGL/MG_Backend/DirectGLES/Managers.h:957-1163`),twin 持有一个驱动 VAO 名(`:1096`)、32 个 client-array scratch buffer id`:1097`)与 32 个 fp64 scratch id`:1101`);两个格式相同的前端 VAO 不能共享它,因为驱动 VAO 同时持有 element-array 绑定与逐属性缓冲绑定,共享 CSO 会把它们变成每次 `BindVertexElements` 都要重发——严格比今天更慢。所以 P3a 逐前端 `VertexArrayObject` 铸一个 `VertexElementsCso` 句柄(配置变化时**在同一句柄上重发** `CreateVertexElements``MGPipeHandle::Gen` 只在槽位复用时递增),**这是一个命中率恒为 1 的合法内容寻址缓存**。上面那张 1024 项的内容寻址表是 **P7** 的活——Magma 的 `VertexInputStateFactory` 接管 CSO 时,`VkPipelineVertexInputStateCreateInfo` 要的正是内容寻址;它加在 P3a 同一组 `CreateVertexElements`/`BindVertexElements`/`DeleteVertexElements` 与同一个 slot 分配器**之上**P3a 的线上形状与 applier 记录都不妨碍它。
## 3. 调用目录(P0 已落地)
@@ -97,7 +99,7 @@ Flags`kNeedsAck`(调用方等 server 确认;目录里目前无条目携
- 今天 20 个 draw 入口塌成 `DrawVbo` 一条,`MGPDrawRange[]` 就是 `MultiDraw*` 族今天的形状;`Clear` 一条判别式合并 `glClear` + 4 个 `glClearBuffer*` + 4 个 `glClearNamedFramebuffer*`
- `SetSamplerViews` / `BindSamplerStates` **没有 stage 维度**:MobileGL 的纹理单元空间是合并的(`TextureState::m_textureUnits` 是 192 个单元的一个数组,每 stage 32 只是广告数字),同一单元可被两个 stage 采样;stage 只在目标 API 需要时由 server 从反射归档推导。
- `SetTextureParams` 按资源寻址、与 sampler view 分开(D10):只作 FBO attachment / image 单元 / `glCopyImageSubData` 端点的纹理没有 sampler view,但 Espryt 对 attachment 也同步纹理参数,且 `RequireImageBindableStorage` 需要在前端参数版本不动时强制重同步。
- `SetTextureParams` 按资源寻址、与 sampler view 分开(D10):只作 FBO attachment / image 单元 / `glCopyImageSubData` 端点的纹理没有 sampler view,但 Espryt 对 attachment 也同步纹理参数,且 `RequireImageBindableStorage` 需要在前端参数版本不动时强制重同步。**P4a 有意把这个缺口关掉了**Espryt 从 `SyncNeccessaryTextures` 侧补上 READ-only attachment 的参数同步),并且**证明它此前确实是坏的**:证据不是公共 GL 的图——回读模拟自己会写 `GL_DEPTH_STENCIL_TEXTURE_MODE``IsDrawSyncClean` 又在首次采样时把参数推下去,所以任何纯 GL 序列都看不见它——而是 `MG_IntegrationTest/Harness/PipeApplyPeek.{h,cpp}` 的白盒断言(读 applier 的参数记录与 Espryt 已应用状态),变异注入下 4/4 变红。
- `SetIndexBuffer` 独立于 VAO 配置版本(D5):索引 slot 重绑不移动 VAO config version。
- `SetGlobalConstants` 只覆盖默认 uniform blockD6):`globalUboScratch` 是 link phase B 的 CPU 数组,没有 GL name、没有 `BufferObject`
@@ -131,9 +133,9 @@ Flags`kNeedsAck`(调用方等 server 确认;目录里目前无条目携
| `MGPRenderStateDesc` / `MGPBindRenderState` / `MGPDynamicState` | 48 / **12** / 32 | §5.3 |
| `MGPVertexElements` | 40 | blob 同时带解析后的 `MGPVertexAttribWire[]` **和** `MGPVertexBindingPointWire[]`(P3a 落地的两个 POD 线上形,24 B / 16 B`MobileGL/MG_Pipe/MGPipeValueTypes.h:568-600`)。**两个视图都过线的理由是记录自洽,不是 stride 消歧**:前端已经把 pointer 调用的 stride 0 解析成 element size,一个活到 `VertexAttribute::Stride` 的 0 只可能来自 binding 模型(`MobileGL/MG_Pipe/MGPipeValueTypes.h:496-503`),后端从来不读 binding point`MG_Backend``VertexBufferBindingPoint` / `GetAttributeBindingIndex` / `GetAttributeRelativeOffset` 零命中);真正承重的是 `MGPVertexElements` **声明**了 `BindingPointCount`,一条不描述自己 blob 的记录会让 applier 的边界门永远无法收口。代价按配置变化付一次、不按 draw 付(blob 只搭 `CreateVertexElements`),裁掉第二个视图是 P13 的重调项。`IsLong``Type == Float64` 分开携带;`Divisor` 不在属性视图里(走 `MGPVertexBuffer::Divisor`);仅供查询的 `LegacyStride/LegacyPointer` 留在 client |
| `MGPSamplerDesc` | 32 | `SamplerParameters` 逐字节过线**含 `borderColorForm`**(三种 border color 表示永远都被数值填满,没有它后端无法在 `Iiv`/`fv``VkBorderColor` 家族间选择) |
| `MGPSamplerView` / `MGPTextureParams` | 36 / 32 | view 只带视图限制(min/num level、min/num layer、别名格式);纹理参数(base/max level、swizzle、depth-stencil mode、LOD 钳、`ForceResync`)挂在纹理对象上 |
| `MGPSamplerView` / `MGPTextureParams` | 36 / **40**P4a | view 只带视图限制(min/num level、min/num layer、别名格式);纹理参数(base/max level、swizzle、depth-stencil mode、LOD 钳、`ForceResync`)挂在纹理对象上。**[deviation] D-E1**`MGPTextureParams` 从 32 涨到 **40**,多出 `BuiltinSampler`(该纹理自带 `SamplerObject` 所对应的 sampler CSO 句柄,**不允许是空句柄**——每个 `ITextureObject` 都有一个 sampler 对象,空句柄是协议损坏而不是"没有 sampler")与第二个 resync 位 `SamplerResync`;参数本身一项没变。句柄从 client 的**内容寻址 cache** 取(`MGPipeSamplerCsoCacheInstance().Acquire`),不是按纹理身份铸的——这条缝在 P4a 里被踩了两次:客户端一度按对象身份铸、Espryt 的 twin 一度按身份查,两边都拿不到那条内容寻址的记录 |
| `MGPProgramDesc` | 192 | 逐 stage SPIR-V blob ×6 + 反射归档 blob + `StageMask`/`GlobalUboSize`/`ReservedNumSamplesOffset` + 四个状态字节,§7 |
| `MGPFramebufferState` | 304 | 8 color + depth + stencil + **client 解析后的 `ReadSurface`**(按结构消灭 read-buffer-shared-FBO 缺陷类);`MGPSurface::InternalFormat` 内联(四个跨对象 mask 推送时零查表);`ContentHash` 既是 server 的 render-pass memo 键也是 client 的发射抑制器 |
| `MGPFramebufferState` | 304 | 8 color + depth + stencil + **client 解析后的 `ReadSurface`**(按结构消灭 read-buffer-shared-FBO 缺陷类);`MGPSurface::InternalFormat` 内联(四个跨对象 mask 推送时零查表——P4a 发现只有格式不够,`MGPSurface``Pad0` 因此变成 `TextureTarget`,否则 `ShouldUseCaveatTextureFormat` 这类 mask 仍得回前端问纹理目标);`ContentHash` 既是 server 的 render-pass memo 键也是 client 的发射抑制器。**P4a:多一个 `Target` 字节,记录按绑定目标发**`Draw` / `Read` / `Both``Complete` 是后端无关的 `CheckCompleteness()` 答案,D-C3),**并且 applier 把记录按 framebuffer 句柄存成每对象一张表**外加两个"当前绑定"句柄——第四个取值 `Named = 3` 就是"这条记录描述它点名的那个 framebuffer,不动任何绑定"`BlitNamedFramebuffer` 与四个 `ClearNamedFramebuffer*` 之前正是因为只有两条绑定记录而打进了一个从未收到附件的 FBO |
| `MGPSubData` / `MGPSubRegion` | 72 / 40 | §6 |
| `MGPDrawInfo` / `MGPDrawRange` / `MGPDrawIndirect` | **56** / 12 / 40 | `Flags` 门控 `MinIndex/MaxIndex`(只在 client-memory 数组路径算)与 `XfbCpuCapturedVertices`(只在 XFB scatter 路径读)——不是每 draw 都算;`NumDraws``MGPDrawRange` 在变长尾;用户索引的 `MGHostSpan` 只在 `kDrawHasUserIndices` 时进变长尾;indirect 的 `DrawCount` 由 client 解析,server 永不读 indirect 命令块来数 draw |
| `MGPShaderBuffers` / `MGPBufferRange` | 32 / 24 | range 不内联 host span`kCapNeedsHostUboBytes` 下 Uniform 类带第二个变长尾 `MGHostSpan[HostSpanCount]`,与 range 数组下标对齐 |
@@ -209,7 +211,7 @@ GL 是每 unit 每 target 各一个绑定;shader 看见哪一个取决于 samp
- `resource_create` 在前端对象构造时发,存储由 `resource_respecify` 惰性定义;`resource_destroy` 在析构时发。三条顺序约束由 payload 表达:view 先于存储属主销毁(`ViewOf` + server keep-alive)、FBO attachment 钉住纹理(surface handle 隐含 keep-alive)、buffer texture 钉住 buffer`BufferForTexBuffer`,范围实时解析)。
- 共享组:v1 一个 screen、一个 context、一条 flow`eglMakeCurrent` 是 flow 所有权转移,在既有 `EGLOperationMutex` 下发射(顺手让 `ReleaseThread``SwapInterval` 也取该锁)。
- program pipeline 合成体:`GLContext::GetProgramForDraw()` 今天就完全在前端合成(join、签名查 cache、`Link(true)`)。tracker 拿到 `SharedPtr<ProgramObject>` 推**一个** handleslot 从 `ShaderCso` 保留高位段分配,pipeline cache 淘汰时释放 slot、`gen++`、发 `delete_shader_state`。合成体从不过线,server 不需要任何"解析后的 draw program"钩子;副带收益是阻塞的 `JoinLinkAndSpirv()` 离开 server 的 draw path。
- program pipeline 合成体**P4a 落地:`MobileGL/MG_Impl/Pipe/CompositeResolver.h`**`GLContext::GetProgramForDraw()` 今天就完全在前端合成(join、签名查 cache、`Link(true)`)。tracker 拿到 `SharedPtr<ProgramObject>` 推**一个** handleslot 从 `ShaderCso` 保留高位段分配(段基 983040,独立稠密表——按 slot 索引的向量在这个段上要 ~236 MB)pipeline cache 淘汰时释放 slot、`gen++`、发 `delete_shader_state`**释放有两条路,都必须恰好一次**:合成体自己被换掉时由 resolver 释放,程序对象死亡时由 `~ProgramObject``MGPipeEmitShaderCsoDestroyAndFree` 释放(D-H7)。**记忆按 `(ContextId, 管线 GL 名)` 键控**——resolver 是进程级单例而 GL 名是每上下文的,只按名字记会在两个上下文用同一个管线名时把对方还活着的合成体释放掉(复审在真实 make-current 序列上打出来的);`Reset()` 只清"新鲜度",不清"该释放"这件事,否则一次 make-current 之后释放路径就永久失灵。合成体从不过线,server 不需要任何"解析后的 draw program"钩子;副带收益是阻塞的 `JoinLinkAndSpirv()` 离开 server 的 draw path。
### 5.7 emulation 的归属
@@ -252,13 +254,15 @@ GL 是每 unit 每 target 各一个绑定;shader 看见哪一个取决于 samp
- `MGPSubRegion` 显式携带 `SrcRowStride/SrcSliceStride``MGPSubData::SourceIsVerbatimLevelShadow` 显式携带原来由 `uploadData == mipData` 指针比较回答的问题:"这批字节是未经转换的 level shadow 吗"。split 下 client 既不发整 level 也不在 server 留整 level 镜像,指针比较不成立;Espryt 的上传路径改为从描述符取步长,`UNPACK_ROW_LENGTH``SrcRowStride/bpp` 设。形状照抄已存在的 `UnpackStagingBlock`(ring 路径本来就紧密重打包、不发 `glPixelStorei`)。
- **dirty 归属反转**client 保留 rect 模型、维护一份发射游标、发射后清自己的标志,server 从不碰 client 的标志。安全,因为 `MG_Impl` 里没有任何 `IsStorageDirty/GetStorageDirtyRects/GetStorageDirtyRegion` 调用点(前端从不读自己的 dirty 状态)。逐 level "server 权威位"与纹理 ack 协议因此不必存在。
- 发射游标按**存储属主**键控 `(storageOwnerHandle, ownerUploadTarget, ownerLevel)``TextureObjectView` 把 dirty 查询/清除全部转发给属主并做索引重映射,view 与属主共用同一份 dirty 状态。门:通过 view 上传、经属主采样(及反向),跨 draw 边界各一次。
> **P4a 落到哪一步****排水列表(drain list)是这一波的**——client 在 validate 点把每个脏的 `(存储属主, 上传目标, level)` 发成**一条** `resource_subdata`,逐区带 `SrcOffset`/`SrcRowStride`/`SrcSliceStride``RegionCount == 0` 合法且表示"并集框就是全部"。**上面那条按存储属主键控的发射游标与索引重映射仍然是 P3b/P4b 的**,P4a 没有做。两条 P4a 学到的规矩写在这里:(1) **清 dirty 要等 applier 的 acceptance**,不是发射即清——`MGPipeApplyResourceSubData` 返回 `Bool`,被拒的记录必须让 level 留在脏表里(否则拉取路径再也不会补它);(2) **服务端自己的重新变脏是服务端的事**——`RequireImageBindableStorage` 会让一个已经上传过的 level 重新需要上传,client 的标志早就清了,所以 twin 直接重新武装 applier 记录里的待上传项。`TextureUploadShapeScenario`(形状金标)**已经建好但只记录不设门**,要等 Mali 侧的帧时增量才升级成门。
- 后端真正在 shadow 里写字节的两处——CPU 回退生成 mipRGB16F/RGB32F)与 `glCopyImageSubData` 目的地镜像——分别由 `OnTextureWriteback` 与"CopyImage 镜像搬到 client"处理。
- Unpack PBO 完全在 client 解析;压缩纹理永不到达后端;`glCopyTexSubImage*``glClearTexImage` 整体留在 client(今天就是纯前端操作:借一次 `ReadPixels` 进 CPU scratch 再写 shadow),拆分后恰好是一次阻塞 ReadPixels round trip,脏区按普通 subdata 下发。
## 7. Shader state = SPIR-V + 反射归档
- `CreateShaderState` 的 payload 是逐 stage SPIR-V + 反射归档(`LinkArtifacts` + `SpirvArtifacts` 全结构体),**不是源码**。"server 从源码重新 link"这条路显式关闭:链接真 `ProgramObject` 就链接 glslang。glslang 全在 clientSPIRV-Cross`TranspileSpirvToEssl`)全在 server,文件级切割。没有 `MOBILEGL_IPC_PROGRAM` 开关、没有 server 侧 compile pool。
- 归档机制:`Visit()` + `sizeof` 绊线(`static_assert(sizeof(LinkArtifacts) == MGL_LINKARTIFACTS_SIZE)`),一份字段表服务序列化两个方向。必须覆盖四个 `ResourceReflection`(各带 `TypeFacts`)、`uniformSamplerOrImageUnitIndex``uniformBlockBinding``shaderStorageBlockBinding`(按名字)、`explicitOpaqueUniformBindings``xfbVaryings/xfbStrides/xfbPackedStride/xfbNeedsScatteredCapture``computeLocalSize`、GS/TCS/TES 事实、`usesReservedNumSamples``uniformOffsets``XfbVarying` 带两套拼写(GL 名字 + block 实例/成员/元素)。
- 归档机制:`Visit()` + `sizeof` 绊线(`static_assert(sizeof(LinkArtifacts) == MGL_LINKARTIFACTS_SIZE)`),一份字段表服务序列化两个方向。**P4a 落地:序列化器在树里了**(`MobileGL/MG_Pipe/ProgramArtifactsCodec.{h,cpp}`),但 **monolith 只在 verify 构建里调它**——句柄臂上 server 读的仍是前端自己的归档(D-H3:制品不在 monolith 过线,编解码因此不在热路径上),真正逐帧走 codec 是 P5 之后的事。**未完的一件配套**:`ProgramArtifacts.h` 的 libc++/NDK 尺寸钉仍是惰性的(`MGL_ARTIFACT_SIZES_LIBCXX_PINNED` 没定义),也就是说 Android 构建上这个 58 成员结构还没有绊线——记在 P3b/P4b 的清单里。必须覆盖四个 `ResourceReflection`(各带 `TypeFacts`)、`uniformSamplerOrImageUnitIndex``uniformBlockBinding``shaderStorageBlockBinding`(按名字)、`explicitOpaqueUniformBindings``xfbVaryings/xfbStrides/xfbPackedStride/xfbNeedsScatteredCapture``computeLocalSize`、GS/TCS/TES 事实、`usesReservedNumSamples``uniformOffsets``XfbVarying` 带两套拼写(GL 名字 + block 实例/成员/元素)。
- **P0.5 硬前置**:反射类型今天声明在 `ProgramObject.h` 里,而它 include `ShaderObject.h`(→ glslang)与 `SpvcSession.h`(→ spirv_reflect)。P0.5 把 `TypeFacts``ResourceReflection``XfbVarying``LinkArtifacts``SpirvArtifacts` 抽到 `MG_State/GLState/ProgramState/ProgramArtifacts.h`(只 include `<Includes.h>` 与容器),8 个 includer 靠类内 `using` 别名零改动,加 CI `-H` 闭包断言(已落地,见 `ROADMAP.md` P0.5 行;`DynamicBackendParameters` 留在 `BackendObject.h`,所以闭包门 A 断言的是 `MGPipeValueTypes.h` 而不是 `MGPipeTypes.h`)。同批抽取 `MG_Pipe/MGPipeValueTypes.h``MAX_DRAW_BUFFERS``PerBufferBlendState``StencilFaceState``PixelStoreParameters``RenderStateParameters``SamplerParameters``BorderColorForm``VertexAttribute``VertexBufferBindingPoint`),它不 include `MG_State/GLState` 任何东西;`MGPipeTypes.h` 今天为此临时 include 了 `BackendObject.h``RenderState.h`(文件头注明为 P0.5 债务)。没有这一步,P7 的 `nm -D | grep glslang` 判据不可达。
- server 侧惰性特化(D-B2):后端 program 还依赖 8 个额外输入(draw FBO 的 snorm/unorm clamp mask、fragColor 广播数、storage-block 绑定签名、atomic counter 集、活的 image 格式、patch 参数;Magma 另加 FragCoord-Y-flip 的 default-FB 高度与 XFB 布局),`create_shader_state` 发布**制品**server 在 verb 时刻从已推送状态特化——正是两个后端今天的做法,也是 gallium `st_variant` 的做法。
- 后端 link/compile 失败不需要同步返回:今天只是一行 `MGLOG_E` 加 bind program 0 的空 draw`GL_LINK_STATUS` 永不撤回,同步查询由 client 从 `ProgramObject` 回答。`OnLog` 逐字复现——由此要求日志按严重级分级(§8.3)。
@@ -292,7 +296,7 @@ GL 是每 unit 每 target 各一个绑定;shader 看见哪一个取决于 samp
### 8.3 错误、ack 与日志
- 纹理分配的 OOM 在 monolith 里就已推迟到 sync 时刻(`glTexImage*`/`glTexStorage*``MarkStorageDirty`Espryt 惰性分配;连 `glRenderbufferStorage*` 也在 `SyncToBackend` 里惰性做),拆分不改变可观察行为,这批不同步 ack。
- **唯一允许同步 ack 的入口是 `glBufferStorage`(真同步分配)**`glRenderbufferStorage*` 不 ack41 个 trace fixture 里 OOM 探测惯用法出现 0 次(9 次调用散在 5 个 fixture,无一在 3 个调用内跟 `glGetError`;语料里的成功性检查是 `glCheckFramebufferStatus`client 本地作答)。**P3a 起 `ResourceRespecify` 携带 `kNeedsAck`,并且带一个逐记录谓词**:flag 是**逐调用**的静态属性,而 `ResourceRespecify` 同时服务 `glBufferData``glBufferStorage`,裸 flag 会把 Minecraft 的整块 chunk 上传变成每 store 一次往返。所以真正拍板的是 `MGPipeResourceRespecifyNeedsAck(desc) == (desc.Immutable != 0)``MobileGL/MG_Pipe/MGPipeTypes.h:680`),`PipeCalls.def:18-24` 的图例把这条规则写在目录里(调用行 `:82`),`MG_Test/Pipe/PipeCatalogueTest.cpp:552-585``ResourceRespecifyAcksOnlyImmutableStorage` 对两种惯用法各钉一次。monolith 下 ack 是 `((void)0)`(applier 只隔一次函数调用),P5 把门铃接到这个谓词上。
- **唯一允许同步 ack 的入口是 `glBufferStorage`(真同步分配)**`glRenderbufferStorage*` 不 ack41 个 trace fixture 里 OOM 探测惯用法出现 0 次(9 次调用散在 5 个 fixture,无一在 3 个调用内跟 `glGetError`;语料里的成功性检查是 `glCheckFramebufferStatus`client 本地作答)。**P3a 起 `ResourceRespecify` 携带 `kNeedsAck`,并且带一个逐记录谓词**:flag 是**逐调用**的静态属性,而 `ResourceRespecify` 同时服务 `glBufferData``glBufferStorage`,裸 flag 会把 Minecraft 的整块 chunk 上传变成每 store 一次往返。所以真正拍板的是 `MGPipeResourceRespecifyNeedsAck(desc) == (desc.Immutable != 0)``MobileGL/MG_Pipe/MGPipeTypes.h:680`),`PipeCalls.def:18-24` 的图例把这条规则写在目录里(调用行 `:82`),`MG_Test/Pipe/PipeCatalogueTest.cpp:552-585``ResourceRespecifyAcksOnlyImmutableStorage` 对两种惯用法各钉一次。monolith 下 ack 是 `((void)0)`(applier 只隔一次函数调用),P5 把门铃接到这个谓词上。**P4a 把这个谓词收窄到 buffer 目标(D-A2**:纹理与 renderbuffer 从这一波起也走 `resource_respecify`,而 `glTexStorage*``Immutable` 同样是 1——不收窄的话每一次不可变纹理分配都会变成一次同步往返。收窄之后 `ResourceRespecify` 还多了两条 P4a 的语义:一是**逐 level 作用域**(带 `MGPRespecifiedLevel` 尾,说明这次重定义的是哪一级;空指针才是"整个资源"),二是**元数据更新**(存储定义字段与已存描述符逐项相同时,只换 `BindMask`/`ImageBindableHint`,不 ack、不清待上传)——前者是终审抓出的 critical:不带 level 时 `glTexImage2D(level 1)` 会把同一纹理上已经被 applier 接受、客户端也已清了 dirty 标记的第 0 级待上传一起丢掉,画面读回全黑。
- 其余错误一律晚到,走有序的 `OnGlError`
- `OnLog` 分级:≤WARN 有损(覆盖最旧 + `eventDropped` 计数);≥ERROR 无损,加入触发 `eventRingFull` + 停止 apply 的语义事件集;每秒 ERROR 速率限制器,超限发一条 "N errors suppressed"`MGLOG_E_ONCE` 的 latch 变 per-server。理由:后端 link 失败只以一行 ERROR 呈现,统一有损会让最有诊断价值的那一行在日志压力下消失。
@@ -320,6 +324,8 @@ Magma`VulkanRenderer` 全部 memo 与 scratch、`PipelineFactory`、`ProgramF
从"不动"里移出的一项:Espryt 的 sub-rect 上传判定与跨步计算(§6,从描述符取步长)。
**P4a 的字节一致行**`scripts/p4a_untouched_regions.sh`,17 个区 / 3 个文件,8 个阴性对照按名变红;与 P3a 的十一函数并列):Espryt 的 depth-stencil 采样模拟与格式 caveat 两族——`ShouldUseCaveatTextureFormat``BackendTextureFormatAddsAlpha``StageBlocksIntoUnpackRing``UnpackRingAvailable``UnpackRingAllocate``RecomputeBackendColorSlots` 等——它们是"被推送的记录改变了输入、但算法本身不许动"的那一类,字节一致是唯一能证明这点的门。**仍在 `MG_Backend` 里的前端类型**`g_rawDepthFetchSamplerState` 是一个前端 `SamplerObject`(Espryt 自己为原始深度取样铸的,client 从没见过它,因此永远不会有记录——句柄臂上"对象即权威"的那条分支就是为它留的),它的原生化归 P3b/P4b,是句柄化之后 Espryt 侧最大的一处残留。
唯一两处必须真改的 `MG_State` 类型内部用法(都在 Magma):占位纹理(构造真的 `TextureObject2D*` 只为复用 `SyncTextureAndGetDescriptor(ITextureObject&)` 签名,~120 行木偶戏 → ~60 行原生 `VkImage`+view+descriptor34 个 `MOBILEGL_ASSERT(pGLContext)` 里的 9 个随之消失);两个内部 shader 烘焙(§7)。Espryt 的小号同类:`g_rawDepthFetchSamplerState` → 后端原生 sampler。
### 9.2 strangler 脚手架:`PipeInputs` + 逐 verb 填充 + poison 世代(P1
@@ -372,6 +378,7 @@ Track V 的 55% 不需要逐字段接口条目就能跑起来,所以 P2 发一
- **buffer 家族的 pre-handle 臂是 `Ops_*` 表加 `g_glesBufferBackendOps`,它们无条件编译**,不在 `MOBILEGL_PIPE_LEGACY_MEMOS` 之下——所以位 7 单独清零永远有一条真臂可跑,`NoArm` 对这个家族不可达(`Managers.cpp:2294-2298` 把这句写在代码里)。前端的十一处分发点(`MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:45``:71``:87``:103``:237``:375``:438``:450``:493``:602``:656`)按 `MGPipeResourceSubsystemEnabled()` 二选一。
- **VAO twin 的前端读取臂是有条件的**pre-handle 的 `SyncToBackend` 本体与它读的那组 memo`m_syncedIndexBufferVersion` / `m_syncedIndexBufferObject` / `m_hasSyncedConfigVersion` / `m_syncedConfigVersion` / `m_syncedAttributeVersions`)都在 `MOBILEGL_PIPE_LEGACY_MEMOS` 里(`MobileGL/MG_Backend/DirectGLES/Managers.h:1109-1130`)。位 8 清零 + `LEGACY_MEMOS=0` 是**没有任何 vertex-input 臂**的配置,`Managers.cpp:2343-2351` 明确报 `PipeLegacyMemosDisabled` 而不是静默。位 8 还要求位 7(属性的缓冲 id 经资源 slot 表解析),`0x17f` 会点名拒绝并回落到 legacy 臂。
- **P4a 的六种 twin 同样有条件臂**(纹理、renderbuffer、framebuffer、sampler、sampler view、program):pre-handle 的本体与它读的那批身份 memo 都在 `MOBILEGL_PIPE_LEGACY_MEMOS` 之下,`#if !MOBILEGL_PIPE_LEGACY_MEMOS` 的分支留着但不可达(arm resolver 在第一次查找就停进程),保持**响亮**。这条开关给这一波多加了约一天:pull 构建被 `CMakeLists.txt` 强制打开它,所以每个"删掉旧 memo"的动作都只是让新臂不再读它、`sizeof` 一点不动——G1 的 0/0/0/0 正是这条的度量,真正的删除跟着 pull 路径在 P13 发生。
- **base-instance 的 ambient 作用域同在其中**`SetPendingFetchBaseInstance` / `GetPendingFetchBaseInstance` / `ScopedFetchBaseInstance``DirectGLES.cpp:5293` 的三个 scope`Managers.h:1189-1200`)。句柄臂改由 `MGPipeApplierState::VertexFetchBaseInstance` 供给,但这三个声明**不能删**:删掉会从 pull 构建移走两个符号(G1)。
P13:删 `SnapshotFromGLContext()` 的非 verify 分支、`MGB_CTX``MOBILEGL_PIPE_PUSH``MOBILEGL_PIPE_LEGACY_MEMOS`**保留 `MOBILEGL_PIPE_VERIFY` 连同它需要的 `SnapshotFromGLContext()``MG_State` include**D-B5verify 构建永不出货);三道纯度门在非 verify 构建上转绿。
@@ -595,7 +602,7 @@ CMake
| 变量 | 默认 | 说明 |
|---|---|---|
| `MOBILEGL_PIPE_PUSH` | pull 构建 `0`**push 构建 `0x1ff`**`kMGPipeSubsystemsMigratedAtP3a``MobileGL/MG_Pipe/MGPipe.h:95`,读入点 `MobileGL/ConfigLoader.cpp:257`P2 的默认是 `0x7f` = `kMGPipeSubsystemsMigratedAtP2``MGPipe.h:94`,保留作分阶段对照 | 子系统位图,十进制或 `0x`;位按 ROADMAP 顺序分配、永不复用(`MobileGL/MG_Pipe/MGPipe.h:72-95`):`0x01` 渲染状态、`0x02` pixel pack、`0x04` patch state、`0x08` vertex attrib defaults、`0x10` residual values、`0x20` Espryt slotsTrack H)、`0x40` Magma vertex inputTrack H)、**`0x80` 位 7 resourcesP3a`resource_*` 家族,`kMGPipeSubsystemResources``MGPipe.h:84`**、**`0x100` 位 8 vertex inputP3avertex elements / vertex buffers / index buffer`kMGPipeSubsystemVertexInput``MGPipe.h:85`)**;位 9..62 留给后续阶段。**位 8 依赖位 7**:属性的缓冲 id 经资源 slot 表解析,只有位 7 填那张表,所以 `0x17f` 会打一行 ERROR 点名拒绝位 8 并回落到 legacy vertex-input 臂(`MobileGL/MG_Backend/DirectGLES/Managers.cpp:2312`)。**位 63 不是子系统而是行为**:`kMGPipeBehaviourNoCsoContentAddressing``MGPipe.h:90`)关掉 client 侧 CSO 内容寻址(每次 pipeline 版本变化都铸新 CSO、永不探测 map),即 P2 的负面对照。`0` = 全 pull,但 P2 之后只有在 `MOBILEGL_PIPE_LEGACY_MEMOS` 编进了 pre-handle 臂时才是有效对照 |
| `MOBILEGL_PIPE_PUSH` | pull 构建 `0`**push 构建 `0x1fff`**`kMGPipeSubsystemsMigratedAtP4a``0x1ff` = `kMGPipeSubsystemsMigratedAtP3a``0x7f` = `...AtP2` 都保留作分阶段对照`MobileGL/MG_Pipe/MGPipe.h`,读入点 `MobileGL/ConfigLoader.cpp`)。**P4a 的四位与它们的三条依赖拒绝**:位 9 framebuffer、位 10 纹理资源、位 11 sampler、位 12 program;位 11 要位 10(每个 `MGPBoundView::Texture` / `MGPImageView::Res` 都是纹理句柄)、位 9 要位 10`MGPSurface::Res` 同理)、位 10 要位 7buffer texture 的 `BufferForTexBuffer`)**外加 P4a 加的第四条:位 10 要位 11**(`MGPTextureParams::BuiltinSampler` 是 sampler CSO 句柄,只有位 11 铸它,空句柄是 `Fatal{ProtocolCorruption}`)。**两侧都要拒**:服务端在各族的 arm resolver 里拒绝并跑旧臂,客户端在 `PipeFill.cpp` 的族门里**根本不发射**——只在服务端拒会出现"客户端已按 acceptance 清了 dirty、服务端却走旧臂"的丢上传(`0x7ff` 一度 438/491)。同一个族门上还挂着**消费者条件**:没有任何后端注册 `MGPipeResourceOps` 时四族一条不发(Magma 就是这种情形 | 子系统位图,十进制或 `0x`;位按 ROADMAP 顺序分配、永不复用(`MobileGL/MG_Pipe/MGPipe.h:72-95`):`0x01` 渲染状态、`0x02` pixel pack、`0x04` patch state、`0x08` vertex attrib defaults、`0x10` residual values、`0x20` Espryt slotsTrack H)、`0x40` Magma vertex inputTrack H)、**`0x80` 位 7 resourcesP3a`resource_*` 家族,`kMGPipeSubsystemResources``MGPipe.h:84`**、**`0x100` 位 8 vertex inputP3avertex elements / vertex buffers / index buffer`kMGPipeSubsystemVertexInput``MGPipe.h:85`)**;位 9..62 留给后续阶段。**位 8 依赖位 7**:属性的缓冲 id 经资源 slot 表解析,只有位 7 填那张表,所以 `0x17f` 会打一行 ERROR 点名拒绝位 8 并回落到 legacy vertex-input 臂(`MobileGL/MG_Backend/DirectGLES/Managers.cpp:2312`)。**位 63 不是子系统而是行为**:`kMGPipeBehaviourNoCsoContentAddressing``MGPipe.h:90`)关掉 client 侧 CSO 内容寻址(每次 pipeline 版本变化都铸新 CSO、永不探测 map),即 P2 的负面对照。`0` = 全 pull,但 P2 之后只有在 `MOBILEGL_PIPE_LEGACY_MEMOS` 编进了 pre-handle 臂时才是有效对照 |
| `MOBILEGL_PIPE_HANDLE_ABA_CONTROL` | 0 | 负面对照 C(push 构建才有,`MobileGL/Config.h:360-371`):故意打掉句柄身份,让 `HandleRecycle` 的 ABA 臂重现旧的 A-B-A 污染。它变绿即为控制失效 |
| `MOBILEGL_PIPE_VERIFY` | 0 | 逐 draw 逐字段影子比对 |
| `MOBILEGL_PIPE_STATS` | 0 | 边界计数器(§附 B) |
+107 -1
View File
@@ -462,7 +462,7 @@ $ ctest --test-dir build-push -R 'HandleRecycle' --no-tests=error -j 4 --output-
所有入表运行前后 `pin_device.sh check` 都是 PINNED(小米 rd12/Magma 崩溃后 GPU pwrlevel 被重置,其后的 sodium/create-instancing 行两臂同状态)。
**读法。** (1) **P2 的边界在 Release 下的真实代价是 +612%**`0x7f` 臂),两机两后端一致,比 -O0 表的 +8–18% 小但同量级;(2) **P3a 在 26.3 与 sodium 上几乎不再加价**P3a 臂与 P2 臂在 26.3 上相差 −0.1 +2 个百分点),**但在 rd12 上把差距从 +11% 推到 +2730%**——rd12Odin Lite 世界)每帧的 VAO/buffer 绑定切换远多于 26.326.3 的 1350 draw/帧大多复用同一 VAO),每次切换都走一遍 `set_vertex_buffers` 构造 + `ContentHash` + applier 记录 + Espryt 侧逐属性走查;Magma 上没有句柄消费者也多 15 个百分点,说明 client 侧发射本身就是大头;(3) **MC 26.3 在 Adreno 上的 p99**`ROADMAP.md:19` 点名的那个数):pull 25.46 ms → P3a 26.32 ms+3.4%),finish 开 25.48 → 26.29;对着 `MEASUREMENTS.md:87` 的采纳基线(p99 163 → 21 ms)仍在 21–26 ms 档,没有回到采纳前的形态——按口径记录,不判门;(4) `mpr`map-persistent-roundtrips,按窗口累加):26.3 两机都是 8(首窗 4,之后两次 2——都是 ≥16 MiB store 定义时的采纳),sodium 1rd12 与 create-instancing 0P2 臂上恒 0(子系统关)——G10 在设备上成立;(5) `CreateVertexElements` 每帧字节数:统计行没有这一类(`vtxc` 是 client 数组),**未测**,留给 P4a 给汇总行加类(6) 计数器(`acc/draw`、六个 memo 门、`resid=``csom/csob`)在 pull/P2/P3a 三臂间逐字相同——它们数的是代码路径,P3a 没有改它们的定义。
**读法。** (1) **P2 的边界在 Release 下的真实代价是 +612%**`0x7f` 臂),两机两后端一致,比 -O0 表的 +8–18% 小但同量级;(2) **P3a 在 26.3 与 sodium 上几乎不再加价**P3a 臂与 P2 臂在 26.3 上相差 −0.1 +2 个百分点),**但在 rd12 上把差距从 +11% 推到 +2730%**——rd12Odin Lite 世界)每帧的 VAO/buffer 绑定切换远多于 26.326.3 的 1350 draw/帧大多复用同一 VAO),每次切换都走一遍 `set_vertex_buffers` 构造 + `ContentHash` + applier 记录 + Espryt 侧逐属性走查;Magma 上没有句柄消费者也多 15 个百分点,说明 client 侧发射本身就是大头;(3) **MC 26.3 在 Adreno 上的 p99**`ROADMAP.md:19` 点名的那个数):pull 25.46 ms → P3a 26.32 ms+3.4%),finish 开 25.48 → 26.29;对着 `MEASUREMENTS.md:87` 的采纳基线(p99 163 → 21 ms)仍在 21–26 ms 档,没有回到采纳前的形态——按口径记录,不判门;(4) `mpr`map-persistent-roundtrips,按窗口累加):26.3 两机都是 8(首窗 4,之后两次 2——都是 ≥16 MiB store 定义时的采纳),sodium 1rd12 与 create-instancing 0P2 臂上恒 0(子系统关)——G10 在设备上成立;(5) `CreateVertexElements` 每帧字节数:统计行原本没有这一类(`vtxc` 是 client 数组),**P4a 已补上并测了**——汇总行的 `bytes/f[...]` 多了一个 **`csob-blob`**`cso-blob-bytes`:所有 CSO create 调用的 blob 字节,含 vertex-elements、sampler、shader)。79 例 retrace、`MOBILEGL_PIPE_STATS=1 MOBILEGL_PIPE_STATS_PERIOD=60`、push 构建(`8c458cd5`):**DirectGLES 27 个用例逐用例窗口均值的中位数 2928 B/帧、均值 42.8 KB/帧、无一为零**,最大 `rd12-odinlite` **1.06 MB/帧**(它每帧换 VAO 的次数远多于别人,正是 §20 读法 (2) 里 rd12 多花 17–19 个百分点的同一根因,这下有了字节口径);其后依次 `minecraft-1.21.4-in-world` 11.4 KB、`common-mods-inventory` 8.3 KB、`common-mods-in-world` 7.8 KB、`rei-inventory` 7.2 KB。**DirectVulkan 侧恒 0**——Magma 没有注册 `MGPipeResourceOps`,P4a 的消费者门因此让四族一条不发(见 `ARCHITECTURE.md``MOBILEGL_PIPE_PUSH` 行),这也是这个计数器第一次把那条门量化出来(6) 计数器(`acc/draw`、六个 memo 门、`resid=``csom/csob`)在 pull/P2/P3a 三臂间逐字相同——它们数的是代码路径,P3a 没有改它们的定义。
**小米 rd12 + Magma 的崩溃**:三臂(含 pull)都在启动后数秒 `SIGABRT``scudo::reportMapError``remapImpl``scudo_calloc``libMobileGL.so`0x818b14 / 0x7e2c44,已剥符号),当时 MemAvailable 6.1 GB——一次巨大或负尺寸的 calloc,在 Adreno 830 + Magma + 这条 fixture 上;pull 库与 P3a 前的 Magma 路径符号一致,所以是**既有 bug**,不入 P3a 账,已开独立任务(先符号化再修)。Oppo/Magma 与小米/Espryt 上同一 fixture 正常。
@@ -518,6 +518,8 @@ $ ctest --test-dir build-push -R 'HandleRecycle' --no-tests=error -j 4 --output-
**`g_uploadRing` 不被重置的不对称:原样保留,记为 `dev` 侧跟进。** `OnBackendContextDestroyed``MobileGL/MG_Backend/DirectGLES/Managers.cpp:2481`)对 `g_uboRing``g_unpackRing``ResetRingForNewContext``:2492-2493`),**不对 `g_uploadRing` 调**`RingAvailable``:3186`)在首次使用时按 `contextGeneration` 自愈,所以它是良性的。P3a **故意不在飞地顺手修**`ROADMAP.md:98` 那条纪律:拆分不得借机修不相关的 `dev` 问题),把它作为 `dev` 侧跟进项留在这里。
**P4a 在它旁边加第二条同类项:`ScopedDefaultUnpackState::s_synced` 没有失效路径(D-O)。** `Managers.cpp``ScopedDefaultUnpackState` 用一个**进程级**影子记住"默认 unpack 状态已经同步过",那个影子被写、被读,**却没有任何地方让它失效**——换上下文、别的代码路径自己调 `glPixelStorei`,它都不知道。P4a 既不修它也没让它更糟(句柄臂的 ring 路径一条 `glPixelStorei` 都不发,唯一的外部写点仍被 `!ringStaged` 挡着),按同一条纪律记为 `dev` 侧跟进。**两条并列的理由是同一个**:它们都是"缓存了一个事实、却没有让这个事实失效的路径",而 P4a 自己在 `Tracker.h` 上被同一类问题咬了三次(`glBindSampler` 不动位 13 的快门、SSO 下 `GetCurrentProgram()` 恒 null、`glBindImageTexture` 只换 level 时三个计数器都不动)——所以下一阶段的 brief 必须带一张"记录字段 → 写它的 setter → emitter 读的快门"的完备表,而不是让每个包各自去发现。
**`FlushPendingRangesNow` 定义一次,句柄臂另有一条自己的档梯。** G5 的第十项与 G1 的空 resize 集之间有一处真冲突:就地重构那几个 helper 会 resize 五个 pull 符号(`FlushPendingRangesNow +14` 在内),G1 不允许。落地形状是:**`FlushPendingRangesNow` 只定义一次、对 `5cb826b0` 逐字节相同**(在 `#if MOBILEGL_PIPE_PUSH``#else` 臂里,`Managers.cpp:1316`,调用点 `:1723``:2906`),句柄臂另有一个 `FlushPendingRangesFrom(twin, hostBase, size)``:1051`,调用点 `:1721``:2044``:2766``:2904`)。**在 P3a 接受档梯在 push 构建里被复制一份**(与 respecify 核心已经用过的形状相同),代价是两条梯子会漂移;对冲是 `CrossFrameBufferScenario` 的十三条加 `StreamedArenaScenario` 的两条 recycle 用例,以及 §20 的 MC 26.3 p99。**它随 pull 臂在 P13 退役**`ARCHITECTURE.md:367`)。共享模板加访问器接口的方案被否决:它同样 resize pull 符号(G1)。
**`MG_Test/Buffer/BufferTest.cpp` 的 fixture 只 scope 了一半的表。** push 构建下后端在 bring-up 同时装 `BufferBackendOps``MGPipeResourceOps`,而 `ScopedBackendOps` 只 scope 前者,于是 86 条 `BufferBackendOps` 分发用例里有 **26 条**被路由进了 pipe(症状是 `EnsureGpuResidentStorage()` 返回 `false`、mock 从没被调用过)。这是**合并缝**的典型形态——两个分支各自绿、合起来红:espryt 那边没有东西经 pipe 发射,client 那边没有东西注册表。集成者落了单 scope 的修法(fixture 现在像 `MG_Test/Pipe/ResourceEmitTest.cpp` 的 `ApplierGuard` scope applier 那样,保存 / 置空 / 恢复 pipe 表):**修完 86/86,整套单元 1619/1619**。它不削弱任何东西——那 86 条是 `BufferBackendOps` 的分发测试,pipe 侧的分发有 `ResourceEmitTest` 自己的覆盖。**跟进(不属于本阶段)**:给这个 fixture 一个 pipe 形的 mock,让同样的 86 条断言在句柄路径上再跑一遍。
@@ -526,3 +528,107 @@ $ ctest --test-dir build-push -R 'HandleRecycle' --no-tests=error -j 4 --output-
**峰值 RSSpush vs pull79 例 retrace):工具不报,所以没有数。** `~/w7/retrace_gate.py` 只有五个参数(`--tree --lib --out -j --only`),代码里没有任何 `rss` / `maxrss` / `getrusage` 引用。这个数原本是用来盯第七张 slot 表泄漏的——一个没人销毁的 buffer 会永远漏掉它的 twin,而这对每一个正确性门都不可见;**这一轮拿不到它**,要拿必须先给那个工具加测量。对冲仍在:`ResourceDestroy` 是从 `~BufferObject` **无条件**发射的、不是靠清扫,顺序(先发射、后 `MGPipeSlots().Free`)由 `HandleRecycleScenario` 的三个臂把关(§16、§17)。
---
## 22. P4a 五部分门(`6035c9d7` 全量 + `8c458cd5` 复跑,基线 `37da3c3a`
P4a 的代码头是 **`8c458cd5`**;下面的"全量"一列跑在 `6035c9d7`(终审修复轮之前的那个头,`wsl_p4a_gate.sh` 完整五部分含三次 retrace),"复跑"一列是修复轮落地后在 `8c458cd5` 上重跑的同一组。两次之间只差终审那五个提交,门的口径没变。
| 门 | `6035c9d7`(全量) | `8c458cd5`(复跑) |
|---|---|---|
| **G1** pull 符号(认定 resize 集为空) | 0 增 / 0 删 / 0 重命名 / 0 resize`.text` 字节不变 | 同上 |
| **G5** 字节一致区 | P3a 十一函数 rc 0;P4a 自己 **17 区 / 3 文件** rc 0self-test **8 个阴性对照全部按名变红** | 同上 |
| **G2** pull vs push 测试名 | 差 0 | 差 0**2902** 条) |
| **G14** 测试名增删 | 0 删除 / +275 | 0 删除 / **+314** |
| 单元 | 1772 × 3linux / push / verify | **1785 × 3** |
| `integration-gpu` | **1091/1091 × 七臂**(默认 `0x1fff``0x1ff``0``0x7f`、pull、`ESPRYT_DISABLE_INVALIDATE_FLUSH=1`、族正则 497/497 | **1117/1117 × 七臂**(多了 `0x9ff``0x5ff` 两个依赖拒绝臂;DirectVulkan **559/559** |
| `integration-verify` | **896/896,零 `Fatal{`** | **920/920,零 `Fatal{`** |
| retrace79 例) | verify 臂 **79/79 全 armed、零分歧、零 Fatal**push 臂 **79/79**G3b 具名 **12/12** | push 臂 **79/79** |
| 三个阴性对照脚本 | 全部 rc 0:`g7_negative_control.sh``p3a_vertex_input_negative_control.sh` 点名 `IsBgra`、**`p4a_descriptor_negative_control.sh` 点名 `Layered``borderColorForm`** | 同上 |
| 子系统对照套件 | `CsoContentAddressing` + `ResourceSubsystemControl` + `ObjectSubsystemControl` **24/24**`0x9ff` 依赖拒绝臂 **14/14**`HandleRecycle`verify**180/180** | 同上 + `184/184` controls |
| 八族拒绝普查 | — | **0**16 条 needle × retrace 日志、13 行 × itest 日志) |
**两处口径,都是这一波踩出来的**
1. **`ctest -V` 做拒绝普查是假零。** console sink 在发布配置里被编译掉,`ctest -V` 抓不到任何 `MGLOG_E`;必须逐用例单跑并读它自己的日志文件(工具 `~/w7/notes/tools/wsl_p4a_refusal_census.sh`)。一份"零拒绝"的普查如果是用 `-V` 取的,它证明的只是 sink 被关了。
2. **普查的短语表必须覆盖全部八族**,而且要小心跨字符串字面量换行的句子——最初那版漏了 sampler 与 renderbuffer 两族,正好是后来真出问题的那两族。
## 23. 这一波真正的产出:缝的分类
P4a 的契约改了七次(`c0b``c0g`),外加一轮缝类审计与一轮终审修复。把它们按**类**记下来,比按包记有用得多——每一类都在多个包里重复出现过,而下一阶段的 brief 应当在开工前就把这几张表写死:
| 类 | 这一波的实例 | 症状 | 预防 |
|---|---|---|---|
| **编码没定死** | `MGPSubData::Target`(低字节资源目标 + 高字节上传目标 vs 裸枚举)、`DepthStencilMode``MGPSurface::Kind`、缺 `TextureTarget` | 两侧各自发明一套;`TextureUploadTarget::Texture1D == 0``kMGPipeResourceTargetBuffer == 0` 撞上,applier 的"这是 buffer 吗"判定被静默污染 | **编码表**:每个字段一行,写清位布局与零值含义,放进契约而不是包头 |
| **身份 vs 内容** | 内置 samplerclient 按对象身份铸、cache 按内容铸;Espryt twin 按身份查那条内容寻址的记录 | 查找永远落空 → 整族拒绝(本波两次,其中一次让 17 条 Iris 光影 trace 全部用驱动默认采样器) | **每 kind 两侧 handle 规则表**:谁铸、按什么键、谁查、按什么键 |
| **记录键错了维度** | framebuffer 记录按"当前绑定"存,DSA 的 `BlitNamedFramebuffer` / `ClearNamedFramebuffer*` 按名字来 | 打进一个从没收到附件的 FBO;SSIM 看得见但没有任何拒绝 | 记录按**对象**存,绑定另存句柄;第四个 target 值 `Named` |
| **进程级单例 vs 每上下文命名** | `CompositeResolver` 按管线 GL 名记忆,GL 名是每上下文的 | 一次 make-current 就释放掉另一个上下文还活着的合成体 | 单例的键必须含上下文身份 |
| **破坏性客户端动作缺前置条件** | 按 acceptance 清 dirty,但 (a) Magma 根本没有消费者,(b) D-K2 依赖位只在服务端拒 | 上传丢失:66 条 DirectVulkan 用例、`0x7ff` 下 438/491 | **消费者门 + 依赖门都要在客户端侧**:没消费者/依赖不满足时**一条不发**,而不是发了再在服务端拒 |
| **快门看不见自己的主体** | `glBindSampler` 只动位 12 的世代;SSO 下 `GetCurrentProgram()` 恒 null`glBindImageTexture` 只换 level 时三个计数器都不动 | 记录停在上一次的值,第一个真读该字段的消费者画错(`create-indirect` ssim 0.887 | **"记录字段 → setter → 快门"完备表**;新增计数器会撑大 pull 对象、G1 不允许,所以优先混入已有世代 |
| **清得太宽** | `resource_respecify` 清掉整张待上传表 | 已被接受、客户端标志已清的那一级永久丢失(读回全黑) | 作用域随调用走:一级 / 截断链 / 整资源 |
| **死亡没通知发射方** | 六个死亡 helper 只释放 slot | 已删但未复用的句柄仍解析到已释放的前端对象 → 下一个 validate 点对已释放内存调虚函数 | 死亡在 wire delete 与 free 之间转发给每个 emitter;查找按"活着"判定而不只按世代 |
| **门不能变红** | `G7` 脚本因 scoped enum 写 0 而永远编译不过、`HighWater(ShaderCso)` 取的是段顶、G9 的红前态公共 GL 不可见 | 绿得毫无意义 | 每个门都要有阴性对照并**真跑过一次红**;公共 GL 看不见的,改白盒断言(`PipeApplyPeek` |
**一个方法论上的结论**:本波六个包的 v1 全部通过了自己的门,**六份对抗性复审全部判 REWORK**,而其中最贵的两个缺陷(丢上传、delete 后 UAF)是**整体 diff 终审**才抓到的——因为它们跨包:发射方、applier、twin 各自都自洽。所以"每包一审 + 集成后整体终审"这条流程里,**终审不是形式**,它是唯一能看见跨包契约的那一轮。
## 24. P4a 设备配对 A/B(三臂)、MC 26.3 的 p99、上传形状(记录项)
**先说口径,再看数(`MEASUREMENTS.md:440` 那条告诫在 P4a 上再次成立)**:`acc/draw` 数的是**约十个热入口上的静态计数点**,所以"把读点搬走"和"把工作去掉"在它上面长得一模一样。P4a 恰好是**搬**的一波:79 例语料上 DirectGLES 的 `acc/draw` 普遍下降(26.3 `10.49 → 8.34``rei-inventory` `13.33 → 11.25``rd12` `8.09 → 6.09`),而同一批运行的逐线程 CPU 是**上升**的。**这不是矛盾,是这个计数器的定义**:后端不再每 draw 去 `pGLContext` 上取,改成读被推送的记录,站点自然少计——工作搬到了客户端的发射侧。要判性能只看 CPU 时间序列与门的命中/未命中对,`acc/draw` 只能与站点常量表一起读。
**设备与协议(与 §20 的两台机不同,这里换了机器)**:红米 M332BF(`2f7cbe2e`SM8750 / **Adreno 830v2**,与 §20 的小米同 SoC 同定频点,数值可比)。reboot-clean → 大核 `policy6` 钉 1958400、小核 `policy0` 钉 1555200、GPU `pwrlevel 0`;**这台的 GPU 有效定频是 1050 MHz 不是 1100**——厂商把 `kgsl-3d0/thermal_pwrlevel` 常驻 1root 写 0 无效(33 °C + 风扇全速下验证),`pin_device.sh` 按 1050 判定。全程**主动风扇恒定 level 2~14.5k rpm)**:它对两臂是同一个常量,作用是把每用例之间的降温从 20–30 分钟压到 1 分钟以内,**40 个样本 40 个 `pin check` 全是 PINNED**(§20 那轮有 22 个样本因热漂移作废重跑)。APK 是 `8c458cd5` 的 Release trace 双臂(pull 8504077 B / push 8626957 B)。
**三臂表**`--benchmark-no-finish`,尾 200 帧、best-of-3、逐线程 CPU p50 ms`0x1ff` = P2+P3a 边界,`0x1fff` = P4a 默认):
| 用例 | 后端 | pull | `0x1ff` | `0x1fff` | Δ P2+P3a | Δ 合计 | **P4a 自己** |
|---|---|---|---|---|---|---|---|
| improved-transparency-26.3 | Espryt | 10.716 | 11.754 | 12.124 | +9.7% | +13.1% | **+3.4 pt / +0.37 ms** |
| improved-transparency-26.3 | Magma | 10.603 | 11.543 | 11.585 | +8.9% | +9.3% | +0.4 pt(噪声) |
| rd12-odinlite | Espryt | 8.210 | 10.798 | 11.182 | +31.5% | +36.2% | **+4.7 pt / +0.38 ms** |
| rd12-odinlite | Magma | — | — | — | — | — | 三臂全 `rc=1`,见下 |
| fabric-sodium | Espryt | 1.312 | 1.406 | 1.454 | +7.2% | +10.8% | +3.6 pt / +0.05 ms |
| fabric-sodium | Magma | 0.474 | 0.502 | 0.507 | +5.9% | +7.0% | +1.1 pt(噪声) |
| 1.21.4-in-world | Espryt | 2.369 | 2.732 | 2.867 | +15.3% | +21.0% | **+5.7 pt / +0.14 ms** |
| 1.21.4-in-world | Magma | 1.028 | 1.147 | 1.145 | +11.6% | +11.4% | 0.2 pt(噪声) |
| fabric-iris-bsl | Espryt | 1.727 | 1.740 | 1.811 | +0.8% | +4.9% | +4.1 pt / +0.08 ms |
| fabric-iris-bsl | Magma | 0.742 | 0.788 | 0.786 | +6.2% | +5.9% | 0.3 pt(噪声) |
**读法。** (1) **P4a 自己在 Espryt 上是 +3.4 +5.7 个百分点**(绝对值 0.05–0.38 ms/帧),大头仍然是 P2+P3a 那条边界——rd12 上 36.2% 里有 31.5% 是它。(2) **Magma 的 `0x1ff` 与 `0x1fff` 两臂在四个用例上逐个落在噪声内**+1.1 / +0.4 / 0.2 / 0.3 pt),这是 c0f 那道"没有后端注册 `MGPipeResourceOps` 就一条不发"的门在设备上的读数——Magma 仍然要付 P2+P3a 的客户端发射(它消费那些族),但 P4a 的四族对它完全免费。(3) **`vanilla`1.21.4-in-world)是 Espryt 上 P4a 占比最高的用例**+5.7 pt),它 draw 少、状态切换密,正是句柄化最不划算的形状;`sodium`/`iris-bsl` 这种把状态压平的语料几乎不受影响。
**头条一:MC 26.3 在 Adreno 上的 p99。** pull **25.297** → P4a **26.841 ms+6.1%**`0x1ff` 臂 26.387Magma 侧 24.979 → 26.021。对照 §20 的 P3a 读数(25.457 → 26.322+3.4%)与 `MEASUREMENTS.md:87` 的采纳基线(p99 163 → 21 ms),**仍在 21–26 ms 档内、没有回到采纳前的形态**——按口径记录,不判门。
**头条二:GUI/atlas 的纹理上传形状,pull 与 push 逐项相同。** 79 例语料两臂各跑一遍带 `MOBILEGL_PIPE_STATS=1` 的 retrace`8c458cd5`,客户端计数器 `tex[emit/box/rect/jobs]`):
| | emit | box | rect | jobs |
|---|---|---|---|---|
| pull | 18451 | 16060 | 2391 | 39926 |
| push | 18453 | 16062 | 2391 | 39928 |
| 差 | **+2** | **+2** | **0** | **+2** |
**整份语料上唯一的形状差异是 2 次**,而且正是那 2 次 `trp`(纹理重铸拉取,见 `ROADMAP.md` 开放问题 2)带来的重放上传——即"盒 vs 矩形"的分解一格没动。这是 SSIM 看不见、Mali 那道 ~+6 ms/帧的悬崖就藏在里面的那个数(`ARCHITECTURE.md` §6),P4a 在这里是**中性**的。逐用例看也一致:`rei-inventory` Espryt 两臂都是 16/16/0/16Magma 两臂都是 47/46/1/75。
**一条留给优化阶段的线索(不是缺陷,是读数)**:设备上 26.3 Espryt 的 `sve`(真正发出去的 sampler-view 集合数)**≈ draw 数**9143 次 / 9138 draw,每窗口 120 帧),而桌面同一 fixture 只有 ~0.07/draw。`PipeStats.h` 给这四个集合计数器写的用途正是这个——"抑制器不再抑制时,它的计数会跟着 draw 数走而不是跟着状态变化走"。桌面与设备的差异说明这跟负载形状有关而不是无条件失效,但 **26.3 在设备上每 draw 重发一次 sampler-view 集合**是 Espryt 侧 P4a 那 +0.37 ms 最值得先查的去处,列进 P3b/P4b 的优化清单。
**`rd12` + Magma 在这台机上照样崩**:三臂(含 pull)全部 `rc=1`,与 §20 在小米上的记录一致(`scudo::reportMapError``remapImpl``scudo_calloc``libMobileGL.so`)。**换了一台同 SoC 的机器仍然复现,进一步确认它是 `dev` 侧的问题而不是设备个例**;按 `ROADMAP.md:7` 的纪律不在本分支顺手修,处置沿用 §20:排除在 A/B 之外、留在桌面语料里(桌面两臂均通过)。
## 25. P4a DriverBenchT1 / T2 / T3(桌面,lavapipe + llvmpipe,记录项)
`wsl_p4a_bench.sh``8c458cd5` 上重跑(原始表 `~/w7/notes/p4a/bench/driverbench.{csv,md}`repeats=5 / frames=240,空闲机)。臂:`pull``push``0x1fff`)、**`push7f` 一列在 P4a 里装的是 `0x1ff`**P2+P3a 边界 = 设备侧那个 T2 的桌面对应物)、`push0``PIPE_PUSH=0`)、`nocso`(关 CSO 内容寻址的负面对照)。
| 臂 | `mc_vanilla_draw` | `mc_state_toggle` | `mc_pass_switch` |
|---|---|---|---|
| native | 4398.9 | 21387.3 | 412657.8 |
| espryt-pull | 4684.4 | 22010.4 | 410936.2 |
| espryt-push`0x1fff` | 5760.5 | 22974.9 | 419703.1 |
| espryt-`0x1ff` | 5749.1 | 23768.2 | 419288.0 |
| espryt-push0 | 5355.2 | 23618.8 | 420362.2 |
| espryt-nocso | 5929.0 | 23685.0 | 418677.0 |
| magma-pull | 15394.9 | 31678.5 | 420197.6 |
| magma-push`0x1fff` | 16023.9 | 32417.4 | 415641.9 |
| magma-`0x1ff` | 15847.5 | 32100.3 | 416670.4 |
| magma-push0 | 15946.6 | 31610.2 | 417410.5 |
| magma-nocso | 15846.2 | 32495.9 | 411306.4 |
`mc_vanilla_draw` 上:**espryt T1`0x1fff` pull= +1076.1 ns/drawT2`0x1ff` pull= +1064.7T1 T2 = +11.4**magma T1 = +629.0、T2 = +452.6、T1 T2 = +176.4。blend toggleespryt +964.5 / magma +738.9 ns per toggle pairpass switchespryt +8766.9、magma 4555.7(后者符号为负,属该项的噪声量级)。
**桌面这台机上,"P4a 自己"落在本 bench 的噪声底以下,所以不要单独引用它。** 同一份脚本在 `6035c9d7`(终审修复前)上跑出的是 espryt T1 +1269.7 / T2 +1135.8 / **T1 T2 = +133.9**,本轮是 +1076.1 / +1064.7 / **+11.4**——**两臂的绝对值在两轮之间各自漂了 ~200 ns,而它们的差只有 10130 ns**,也就是说这个 bench 分辨不出 P4a 这一档的增量。真正可引用的是:(1) **T1 ≈ +1.1 µs/draw 的总边界**(对 pull 基线,Espryt;这条在两轮之间是稳的);(2) **设备侧的三臂表(§24**——那里 P4a 自己是 +3.4 +5.7 个百分点、0.05–0.38 ms/帧,样本全部在验证过的定频窗口里。**Magma 的 T1 T2 = +176.4 ns 不是"Magma 在跑 P4a"**c0f 的消费者门让它一条 P4a 记录都不发(设备侧 §24 的 Magma 两臂差也在噪声内),这 176 ns 是 tracker 多算的那几个快门加噪声。
+2 -2
View File
@@ -1,6 +1,6 @@
# MGPipeMobileGL 前后端拆分
> 状态:**P0、P0.5、P1、P2、P3a 已落地**`feat/disaggregated@fde5fda3`,基线 `dev@9eae9858`)。第 43 天 GO/NO-GO 判定为**继续**。P3ahandle wave 1Espryt 的 buffer 与 VAO)已交付,**下一步 P4a**handle wave 2FBO / 纹理 / sampler / program 的身份与描述符)。见 `ROADMAP.md`
> 状态:**P0、P0.5、P1、P2、P3a、P4a 已落地**`feat/disaggregated@8c458cd5`,基线 `dev@9eae9858`)。第 43 天 GO/NO-GO 判定为**继续**。P4ahandle wave 2Espryt 的 FBO / 纹理 / sampler / program 身份与描述符)已交付,Espryt 的对象类读点至此全部走句柄;**下一步 P3b/P4b**(深化:memo 重键、发射游标、raw-depth-fetch sampler 原生化)与 **P5**(传输 + inproc applier,也只依赖 P4a)。见 `ROADMAP.md`
>
> 性能纪律(2026-09-08 起):逐线程 CPU 与 tracker 绝对 ns **对着 pull 臂基线记录**,不再作阻塞门(push 比 pull 多约 10% 逐线程 CPU 已被接受;该读数出自 -O0 APK,Release 基准线见 `MEASUREMENTS.md` §20),专门的优化阶段排在路线图推完之后。
@@ -31,7 +31,7 @@ MGPipe 是 MobileGL 前端(`MG_State` + `MG_Impl`)与后端(`MG_Backend`
|---|---|
| `ARCHITECTURE.md` | 已定稿的设计与架构:句柄与世代、调用目录、记录约定、tracker、纹理路径、shader 制品、反向通道、后端改造、传输、persistent map 分档、进程/EGL/平台、构建与纯度门、验证策略 |
| `ROADMAP.md` | P0…P13 阶段表、两条跑道、GO/NO-GO 清单、再基线检查点、仍然开放的问题 |
| `MEASUREMENTS.md` | 逐阶段实测:P0(spike A/B、双设备边界计数器基线、桌面数据点、语料事实)、P1(verify harness 门)、P2(五部分门、两机配对 A/B、DriverBench T1/T2、计数器)、P3a(门、接缝缺陷、Track H 普查、两机 A/B)与复现命令 |
| `MEASUREMENTS.md` | 逐阶段实测:P0(spike A/B、双设备边界计数器基线、桌面数据点、语料事实)、P1(verify harness 门)、P2(五部分门、两机配对 A/B、DriverBench T1/T2、计数器)、P3a(门、接缝缺陷、Track H 普查、两机 A/B)、P4a(门、契约七次修正与两轮终审修复、缝类审计、三臂设备 A/B、DriverBench T1/T2/T3与复现命令 |
代码地图(P0 已落地的部分):
File diff suppressed because one or more lines are too long
+90
View File
@@ -487,6 +487,30 @@ def check_call_payloads_have_field_lists(calls, payloads):
"would be blind to them: %s" % ", ".join(missing))
# The MGPipeCallFlags enumerators, MGPipe.h:39-54. Kept here rather than parsed out of the
# header because this list is what the generated kMGPipeCallFlags[] table spells into C++:
# a flag token in PipeCalls.def that is not one of these would generate an expression that
# does not compile, and a build break several minutes later is a worse diagnosis than this
# one line. kNone is listed but is NOT a flag - it is the empty set, and it may not be
# combined with anything.
KNOWN_CALL_FLAGS = ("kNeedsAck", "kHasBlob", "kVarTail", "kHostSpan", "kReplySlot", "kOptional")
def check_call_flags_are_known(calls):
"""Every flag token in PipeCalls.def must be an MGPipeCallFlags enumerator, and kNone may
not be combined with one. Runs in both modes, --check included: P5 R-13.4 exports these
flags as a table six packages read, so a typo here is a wrong decode rather than a
compile error in the one consumer that used to hard-code its own copy."""
for call in calls:
for flag in call.Flags:
if flag != "kNone" and flag not in KNOWN_CALL_FLAGS:
sys.exit("PipeCalls.def: %s carries flag %s, which is not an MGPipeCallFlags "
"enumerator (%s)" % (call.Name, flag, ", ".join(KNOWN_CALL_FLAGS)))
if "kNone" in call.Flags and len(call.Flags) != 1:
sys.exit("PipeCalls.def: %s combines kNone with %s; kNone is the empty set"
% (call.Name, "|".join(f for f in call.Flags if f != "kNone")))
def parse_coverage():
text = read(os.path.join(PIPE_DIR, "Coverage.def"))
accessors = []
@@ -641,6 +665,59 @@ enum class MGPWireOp : Uint16 {
out.append(" %s = %d," % (call.Name, call.Index))
out.append(" kOpCount = %d," % (len(calls) + 1))
out.append("};\n")
out.append("""// THE FLAGS, EXPORTED ONCE, INDEXED BY OPCODE (P5 R-13.4). MGPWireRecHeader::Flags is
// documented as "MGPipeCallFlags of the call", and until this table existed nothing
// generated said what those were: every consumer that needed to know whether a record owns
// an MGPBlobRef, a variable tail or a reply slot had to hard-code its own copy of
// PipeCalls.def's fourth column, and six of them were about to. A hard-coded copy is how
// GetCaps and CreateSamplerState came to carry an MGPBlobRef member with no kHasBlob on
// their line at all - nothing compared the two, because nothing had both in one place.
//
// Index 0 is MGPWireOp::kInvalid and is kNone: the catalogue is 1-based, and an encoder
// that reads flags for an opcode it never got from the catalogue must see the empty set
// rather than another call's flags.
//
// kHasBlob here means EXACTLY "the payload owns an MGPBlobRef member". Three calls carry
// bytes without one - resource_respecify, resource_flush_range and map_persistent, whose
// companion pointers have no carrier - and they are deliberately NOT flagged; MG_Remote's
// CONTRACT-P5.md table 1 is where those live, because a decoder that trusts kHasBlob has
// to find a member to read.""")
out.append("inline constexpr Uint32 kMGPipeCallFlags[static_cast<SizeT>(MGPWireOp::kOpCount)] = {")
out.append(" /* 0 %-24s*/ static_cast<Uint32>(kNone)," % "kInvalid")
for call in calls:
out.append(" /* %2d %-24s*/ static_cast<Uint32>(%s),"
% (call.Index, call.Name, " | ".join(call.Flags)))
out.append("};")
out.append("static_assert(sizeof(kMGPipeCallFlags) / sizeof(kMGPipeCallFlags[0]) ==")
out.append(" static_cast<SizeT>(MGPWireOp::kOpCount),")
out.append(" \"the flags table and the opcode space disagree\");")
out.append("""
// The only supported read of the table. Out-of-range is kNone rather than undefined
// behaviour, because the one caller that can pass a bad opcode is a decoder holding bytes
// off a stream, and it must reach its own Fatal{ProtocolCorruption} rather than read past
// the array on the way there.
inline constexpr Uint32 MGPipeCallFlagsFor(MGPWireOp op) {
const SizeT index = static_cast<SizeT>(op);
return index < static_cast<SizeT>(MGPWireOp::kOpCount) ? kMGPipeCallFlags[index]
: static_cast<Uint32>(kNone);
}
// Spot checks the generator states about its own output, so that a catalogue edit that
// silently drops a flag is a build break here and not a wrong decode six packages away.
static_assert(MGPipeCallFlagsFor(MGPWireOp::kInvalid) == static_cast<Uint32>(kNone),
"opcode 0 is not a call and carries no flags");
static_assert((MGPipeCallFlagsFor(MGPWireOp::GetCaps) & static_cast<Uint32>(kHasBlob)) != 0,
"MGPCaps owns two MGPBlobRef members; R-13.1 gave the call its flag");
static_assert((MGPipeCallFlagsFor(MGPWireOp::CreateSamplerState) & static_cast<Uint32>(kHasBlob)) != 0,
"MGPSamplerDesc owns an MGPBlobRef member; R-13.1 gave the call its flag");
static_assert((MGPipeCallFlagsFor(MGPWireOp::ResourceFlushRange) & static_cast<Uint32>(kHasBlob)) == 0,
"R-13.2: resource_flush_range carries no bytes on the wire and owns no blobref");
static_assert((MGPipeCallFlagsFor(MGPWireOp::ResourceRespecify) & static_cast<Uint32>(kHasBlob)) == 0,
"R-13.3: initial bytes follow as resource_subdata; MGPResourceDesc owns no blobref");
static_assert((MGPipeCallFlagsFor(MGPWireOp::DrawVbo) &
static_cast<Uint32>(kHostSpan | kVarTail)) == static_cast<Uint32>(kHostSpan | kVarTail),
"draw_vbo is the conditional-tail plus host-span shape the codec is measured on");
""")
for call in calls:
out.append("struct alignas(8) MGPWireRec_%s {" % call.Name)
out.append(" MGPWireRecHeader Header;")
@@ -1232,12 +1309,24 @@ def self_test(accessors):
calls_for_control = parse_calls()
controls.append(("emitted row naming a call that does not exist", lambda: gen_emitted_by(
[("GetViewport", "SetDynamicState")], calls_for_control, [("GetViewport", "NotACall")])))
# P5 R-13.4's gate. The flags are now a GENERATED TABLE six packages read instead of six
# hard-coded copies, so a token that is not an MGPipeCallFlags enumerator has to stop the
# generator rather than emit an expression that fails to compile minutes later - and
# kNone, the empty set, may not be OR'd with a real flag and quietly read as one.
flag_typo = Call(1, "Canned", "MGPHandleOnly", "kScreen", ["kHasBlobb"])
flag_kNone = Call(1, "Canned", "MGPHandleOnly", "kScreen", ["kNone", "kHasBlob"])
controls.append(("call flag that is not an MGPipeCallFlags enumerator",
lambda: check_call_flags_are_known([flag_typo])))
controls.append(("kNone combined with a real flag",
lambda: check_call_flags_are_known([flag_kNone])))
trips = 0
for name, fn in controls:
trips += expect_trip(name, fn)
# The positive control: the canned struct's exact list passes, and the parser sees the
# padding member as padding and the function as not a member.
check_field_lists_cover_struct_members({"Canned": ["A", "B", "C"]}, ["Canned"], [canned_struct])
# ... and the real catalogue's real flags pass the same gate.
check_call_flags_are_known(calls_for_control)
if trips == 0:
sys.exit("gen_pipe: self-test: no negative control tripped - the gates are not checking anything")
if trips != len(controls):
@@ -1256,6 +1345,7 @@ def main():
calls = parse_calls()
payloads = parse_verify_payloads()
check_call_flags_are_known(calls)
check_call_payloads_have_field_lists(calls, payloads)
check_field_lists_cover_struct_members(parse_field_lists(), payloads)
accessors, deltas, sticky, emitted = parse_coverage()