[Feat] (Pipe): land the P3a contract - the handle-shaped resource op table, the applier's resource and vertex-elements records, the two vertex wire views, an explicit baseInstance on set_vertex_buffers, and the first kNeedsAck

This commit is contained in:
2026-09-08 02:43:19 -04:00
parent c036900d72
commit e01c0ccc53
21 changed files with 1049 additions and 23 deletions
+7 -3
View File
@@ -322,12 +322,16 @@ namespace MobileGL::MG_Config {
// 0 - the only shipped value until the migration lands - is "pull everything",
// i.e. exactly today's behaviour, and is the default of a PULL build, where the
// knob is meaningless anyway. A PUSH build defaults to every subsystem migrated so
// far (MG_Pipe::kMGPipeSubsystemsMigratedAtP2), so MOBILEGL_PIPE_PUSH=0 in the
// environment is the all-pull control. Accepts decimal or 0x-prefixed hex, and
// operators pass it as hex, so the bits are listed here (MG_Pipe/MGPipe.h owns them):
// far (MG_Pipe::kMGPipeSubsystemsMigratedAtP3a), so MOBILEGL_PIPE_PUSH=0 in the
// environment is the all-pull control and 0x7f (kMGPipeSubsystemsMigratedAtP2) is
// the "P2 only" control P3a's A/B is run against. Accepts decimal or 0x-prefixed
// hex, and operators pass it as hex, so the bits are listed here (MG_Pipe/MGPipe.h
// owns them):
// 0x01 render state (create/bind_render_state + set_dynamic_state)
// 0x02 pixel pack 0x04 patch state 0x08 vertex attrib defaults
// 0x10 residual values 0x20 Espryt slots 0x40 Magma vertex input
// 0x80 resources (the resource_* family: the seven BufferBackendOps hooks)
// 0x100 vertex input (vertex elements / vertex buffers / index buffer)
// 1<<63 NOT a subsystem, a BEHAVIOUR: turn OFF client-side content addressing of
// CSOs, so every pipeline-version change mints a fresh CSO and the map is
// never probed. The negative control the CSO design is measured against.
+4 -2
View File
@@ -250,8 +250,10 @@ namespace MobileGL::MG_ConfigLoader {
#if MOBILEGL_PIPE_PUSH
// A push build with the knob unset runs every subsystem migrated so far, so the
// shipped path is the one the gates measure; MOBILEGL_PIPE_PUSH=0 in the
// environment is the all-subsystems-pull control that reproduces P1 exactly.
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP2);
// environment is the all-subsystems-pull control that reproduces P1 exactly, and
// kMGPipeSubsystemsMigratedAtP2 (0x7f) is the phase-by-phase control - P3a's two
// subsystems off, everything P2 landed still on.
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP3a);
#else
// Meaningless in a pull build: there is nothing to push. Config.h documents 0 as
// "pull everything" and that stays literally true.
@@ -245,6 +245,46 @@ namespace MobileGL::MG_Backend::DirectGLES {
return entry.backend;
}
// P3a: resolve-or-create BY HANDLE, and it is the shape that discharges the debt this
// header records against itself at the top of the file.
//
// The overload above mints - it calls MGPipeSlots().Acquire off a frontend object's
// lifetime id, from inside MG_Backend - which is monolith glue: a handle is minted by
// the CLIENT, and under a real split neither the object nor its lifetime id exists on
// this side. This overload never touches the allocator at all. The handle ARRIVED, in
// the call's payload, already minted by the side that owns minting; all this does is
// index the slot, notice a generation that no longer matches (the slot was recycled,
// so the twin at it describes driver ids the new resource never made) and hand back
// the twin pointer. FindByHandle beside it is the same shape and already existed.
//
// No StatePtr, therefore no Entry::stateRef: the weak pointer is liveness for
// ForEachLive() and a handle-keyed entry has no frontend object to weakly hold. Such
// an entry is therefore invisible to ForEachLive, which is correct - the one direct
// iteration site walks texture twins, and it is not one of these tables.
//
// Death stays ANNOUNCED, as it is on the other overload: for a handle-keyed kind the
// announcement is the family's own destroy call, not the shared death notice, and the
// slot is freed by the CLIENT after that call returns.
//
// UNUSED AT THE CONTRACT COMMIT, deliberately: it is a member of a class template, so
// an uninstantiated one costs nothing anywhere, and the backend package is what gives
// it its first caller.
BackendPtr& GetOrCreate(MG_Pipe::MGPipeHandle handle) {
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
"GetOrCreate(handle) named the reserved null handle");
if (MG_Pipe::MGPipeHandleIsNull(handle)) return m_nullTwin;
// Same arming as the minting overload, and for the same reason: twin creation is
// the moment a driver-owned id starts needing a guarded destructor.
EnsureProcessTeardownSentinel();
Entry& entry = EntryAt(handle.Slot);
if (entry.Live && entry.Gen != handle.Gen) entry.backend.reset();
entry.Gen = handle.Gen;
entry.Live = true;
return entry.backend;
}
// Null when no live twin of this object exists. Unlike the registry's Find this NEVER
// mutates the table, so the returned pointer survives any later Find on it; only a
// GetOrCreate that grows the vector can move it, and callers that hold one across a
+93
View File
@@ -631,6 +631,12 @@ namespace MobileGL::MG_Pipe {
return kMGPipeSubsystemPatchState;
case MGPipeFieldEmitter::SetVertexAttribDefaults:
return kMGPipeSubsystemVertexAttribDefaults;
// P3a. bind_vertex_elements is the vertex-input family's only emitter row today
// (Coverage.def says why the other two candidates are not there); the resource
// family has none at all, because its calls are dispatched at the GL call that
// causes them rather than filled into a PipeInputs field.
case MGPipeFieldEmitter::BindVertexElements:
return kMGPipeSubsystemVertexInput;
case MGPipeFieldEmitter::kNone:
break;
}
@@ -662,10 +668,53 @@ namespace MobileGL::MG_Pipe {
MGPipeSubsystemForDirty(MGPipeDirty::NewVertexAttribDefaults),
"set_vertex_attrib_defaults and NEW_VERTEX_ATTRIB_DEFAULTS must name one subsystem");
// P3a's pairing, in the two halves the contract commit can actually state.
//
// The four above compare the two maps directly, which is only possible once BOTH
// sides name the subsystem. MGPipeSubsystemForDirty is MG_Impl/Pipe/Tracker.h's and
// its bit 5 / 9 / 10 arms land with the client emitters, not here - so the direct
// form would fail at this commit for a reason that is not a defect. What is stated
// instead is exactly as strong in the direction that matters:
//
// (a) the emitter half names the vertex-input subsystem, so a later edit that moved
// it onto a different one fails here;
// (b) the two maps AGREE OR THE DIRTY HALF IS NOT MAPPED YET. The escape hatch is
// the not-yet-mapped case only: the moment Tracker.h maps NEW_VERTEX_ELEMENTS
// onto anything at all, this becomes the equality the four above are;
// (c) and while the dirty half is unmapped the subsystem is NOT in
// kMGPipeWiredSubsystems below, so no field can be skipped on the strength of a
// call nobody emits. (c) is what makes (b)'s hatch safe rather than convenient.
static_assert(SubsystemForEmitter(MGPipeFieldEmitter::BindVertexElements) ==
kMGPipeSubsystemVertexInput,
"bind_vertex_elements must name the vertex-input subsystem");
static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewVertexElements) == 0 ||
MGPipeSubsystemForDirty(MGPipeDirty::NewVertexElements) ==
SubsystemForEmitter(MGPipeFieldEmitter::BindVertexElements),
"bind_vertex_elements and NEW_VERTEX_ELEMENTS must name one subsystem");
static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewVertexBuffers) == 0 ||
MGPipeSubsystemForDirty(MGPipeDirty::NewVertexBuffers) ==
kMGPipeSubsystemVertexInput,
"set_vertex_buffers and NEW_VERTEX_BUFFERS must name one subsystem");
static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewIndexBuffer) == 0 ||
MGPipeSubsystemForDirty(MGPipeDirty::NewIndexBuffer) ==
kMGPipeSubsystemVertexInput,
"set_index_buffer and NEW_INDEX_BUFFER must name one subsystem");
// The two vertex views' capacity is one number on both sides of the boundary. This is
// the one translation unit that sees the frontend constant and the MG_Pipe one, so it
// is where they are pinned together; MGPipeTypes.h says so in place.
static_assert(kMGPipeMaxVertexAttribs ==
MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS,
"the MGPipe vertex-attribute capacity and the frontend's have drifted");
// Which of those subsystems THIS BUILD actually emits for. It grows one commit at a
// time, and a field whose emitter is not wired here keeps being pulled - so adding a
// row to Coverage.def can never silently drop a field on the floor before the call
// that carries it exists.
//
// P3a's two are DELIBERATELY ABSENT at the contract commit: the three emitters below
// are stubs that emit nothing and the applier's fourteen entry points are stubs that
// apply nothing, so wiring either bit here would retire a pull for a call that does
// not happen yet. The commit that gives the emitters their bodies adds them.
constexpr Uint64 kMGPipeWiredSubsystems = kMGPipeSubsystemRenderState |
kMGPipeSubsystemPixelPack |
kMGPipeSubsystemPatchState |
@@ -1044,6 +1093,35 @@ namespace MobileGL::MG_Pipe {
}
return payloadBytes;
}
// ---- P3a's three vertex-input emitters. STUBS AT THE CONTRACT COMMIT. ----
//
// They exist here, and are called from the validate point below, for the same reason
// the applier's fourteen entry points exist as stubs: this file's enum-coupled block
// is the contract commit's and everything else in it belongs to the commit that
// fills the bodies in, so the two must not have to touch the same lines. What lands
// here is the SHAPE - three functions, in the emission order the design fixes
// (elements, then buffers, then index, after the four P2 emitters) - and the bodies
// replace `return 0` without moving a call site.
//
// THEY ARE UNREACHABLE, not merely empty: MGPipeSubsystemForDirty maps NEW_VERTEX_
// ELEMENTS / _BUFFERS / _INDEX_BUFFER onto no subsystem yet, so `wants()` is false
// for all three whatever MOBILEGL_PIPE_PUSH says. Returning 0 keeps them out of the
// payload histogram, which must not gain a bucket for bytes nobody sent.
Uint64 EmitVertexElements(GLContext& ctx) {
(void)ctx;
return 0;
}
Uint64 EmitVertexBuffers(GLContext& ctx) {
(void)ctx;
return 0;
}
Uint64 EmitIndexBuffer(GLContext& ctx) {
(void)ctx;
return 0;
}
} // namespace
Uint64 MGPipeVertexAttribDefaultRepairCount() { return g_attribDefaultRepairs; }
@@ -1127,6 +1205,21 @@ namespace MobileGL::MG_Pipe {
payloadBytes += EmitVertexAttribDefaults(*ctx, tracker.FreshlyPrimed());
}
// P3a's vertex segment, in the order the design fixes: vertex elements, then the
// vertex buffers that fill them, then the index binding. All three still resolve to
// false today - their dirty bits map to no subsystem until the tracker's arms land -
// and all three emitters are stubs; the call sites are here so the commit that gives
// them bodies does not also have to edit the validate point.
if (wants(MGPipeDirty::NewVertexElements)) {
payloadBytes += EmitVertexElements(*ctx);
}
if (wants(MGPipeDirty::NewVertexBuffers)) {
payloadBytes += EmitVertexBuffers(*ctx);
}
if (wants(MGPipeDirty::NewIndexBuffer)) {
payloadBytes += EmitIndexBuffer(*ctx);
}
// ---- step 4: the residual fill, for what an emitted call did NOT supply ----
const Bool applierDerives = ApplierDerivesRenderStateFields();
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
+37 -5
View File
@@ -34,11 +34,26 @@
/* dead: no backend reads it since D21; kept for inventory row 594 */ \
X(GetBoundTransformFeedbackName, SetStreamOutputTargets) \
X(GetBoundVertexArray, BindVertexElements) \
/* Polymorphic over BufferTarget: its rows split across set_vertex_buffers, */ \
/* set_index_buffer, set_indirect_buffers and set_shader_buffers when the */ \
/* inventory is re-vendored carrying the target argument (deferred out of P1: */ \
/* the extractor lives in MobileGL-CS). Named for the plan's explicit */ \
/* replacement of the DrawIndirect/Parameter pair. */ \
/* Polymorphic over BufferTarget, and P3a SPLITS it - not by re-vendoring the */ \
/* inventory (the extractor lives in MobileGL-CS and still does not carry the */ \
/* target argument), but by supplying the target from the EMISSION SITE, which */ \
/* knows it exactly. The split, target by target: */ \
/* ArrayBuffer, and the per-attribute buffer of a VAO -> set_vertex_buffers */ \
/* ElementArrayBuffer -> set_index_buffer */ \
/* DrawIndirectBuffer, ParameterBuffer -> set_indirect_buffers */ \
/* Uniform/ShaderStorage/AtomicCounter/TransformFeedback */ \
/* -> set_shader_buffers */ \
/* CopyRead/CopyWrite/PixelPack/PixelUnpack/Texture -> still pulled: the */ \
/* transfer and pixel-store targets have no call of their own yet. */ \
/* THE ROW STAYS ONE ROW, and that is structural rather than a shortcut: this */ \
/* list IS the MGPipeInputField enum and the PipeInputs field set, and the */ \
/* field is ONE array (m_bufferBindingSlot[kBufferTargetCount]) that a second */ \
/* row of the same name could only duplicate. So the split lives here and in */ \
/* the emitters, and the row keeps naming set_indirect_buffers for the plan's */ \
/* explicit replacement of the DrawIndirect/Parameter pair. It is deliberately */ \
/* NOT in the EMITTED list below: five targets above are still pulled, and a */ \
/* row there says "the whole field is supplied", which for this field would be */ \
/* the same half-truth GetPixelStoreParameters is kept out for. */ \
X(GetBufferBindingSlot, SetIndirectBuffers) \
X(GetBufferBindingPoint, SetShaderBuffers) \
X(GetBufferBindingPointCount, SetShaderBuffers) \
@@ -151,8 +166,25 @@
// nothing while its poison stamp said it was published, so neither the poison nor the verify
// comparator could see it. Until the field is split, the whole of it keeps going through the
// fill loop and the pack half is simply written twice.
//
// P3a ADDS ONE ROW, GetBoundVertexArray -> BindVertexElements, and it is the vertex-input
// family's only candidate: GetBufferBindingSlot is polymorphic over a target set P3a covers
// only part of (see its comment above) and GetCurrentVertexAttribute has been here since P2.
// The row is INERT until the vertex-input subsystem is wired - MG_Impl/Pipe/PipeFill.cpp's
// kMGPipeWiredSubsystems does not carry that bit at the contract commit, because the emitters
// beside it are still stubs - which is exactly the guard that lets a row land before the call
// that carries it exists.
//
// A NOTE FOR THE COMMIT THAT WIRES IT, because it is not visible from this file: the field is
// a shared pointer to the frontend VAO, and the vertex-input calls supply the CONFIGURATION
// (the applier's MGPipeVertexElementsRecord), not the object. So the row is shape-only in the
// same sense GetCurrentVertexAttribute's is, and it stays so until the backend's twin
// resolution reads the applier's BoundVertexElements instead of the object - at which point
// PipeFill.cpp's EmittedCallSuppliesTheWholeField arm is where that is decided, deliberately
// rather than silently by this row's presence.
#define MGP_COVERAGE_EMITTED_LIST(X) \
X(GetBlendColor, SetDynamicState) \
X(GetBoundVertexArray, BindVertexElements) \
X(GetBlendEquationIndexed, CreateRenderState) \
X(GetBlendFuncIndexed, CreateRenderState) \
X(GetClampReadColor, SetDynamicState) \
+13 -3
View File
@@ -76,13 +76,23 @@ namespace MobileGL::MG_Pipe {
inline constexpr Uint64 kMGPipeSubsystemResidualValues = 1ull << 4;
inline constexpr Uint64 kMGPipeSubsystemEsprytSlots = 1ull << 5; // Track H, Espryt 0b
inline constexpr Uint64 kMGPipeSubsystemMagmaVertexInput = 1ull << 6; // Track H, Magma subsystem 4
// bits 7..62 reserved for the later phases, allocated in ROADMAP order.
// P3a's two. Resources is the seven BufferBackendOps hooks turned into the handle-shaped
// resource_* family; VertexInput is vertex elements, vertex buffers and the index buffer.
// They are separate bits because they are separate A/Bs: a buffer path that regressed and
// a vertex path that regressed are different findings, and clearing one must not disarm
// the other.
inline constexpr Uint64 kMGPipeSubsystemResources = 1ull << 7;
inline constexpr Uint64 kMGPipeSubsystemVertexInput = 1ull << 8;
// bits 9..62 reserved for the later phases, allocated in ROADMAP order.
// NOT a subsystem, a BEHAVIOUR: turn OFF client-side content addressing of CSOs, so
// every pipeline-version change mints a fresh CSO and the map is never probed. This is
// the negative control the whole CSO design is measured against (ROADMAP.md P2).
inline constexpr Uint64 kMGPipeBehaviourNoCsoContentAddressing = 1ull << 63;
// The default of a push build with the knob unset (ConfigLoader.cpp).
inline constexpr Uint64 kMGPipeSubsystemsMigratedAtP2 = 0x7full; // bits 0..6
// The default of a push build with the knob unset (ConfigLoader.cpp). Each phase's
// constant STAYS, because it is the A/B control for the phase after it: P3a's
// "everything P2 had and nothing of mine" arm is spelled MOBILEGL_PIPE_PUSH=0x7f.
inline constexpr Uint64 kMGPipeSubsystemsMigratedAtP2 = 0x7full; // bits 0..6
inline constexpr Uint64 kMGPipeSubsystemsMigratedAtP3a = 0x1ffull; // bits 0..8
// The catalogue itself. Only macros, so it is safe to expand inside the namespace, and
// consumers (the unit test, later the transport) get MGP_CALL_LIST from this header.
+40 -1
View File
@@ -353,6 +353,13 @@ namespace MobileGL::MG_Pipe {
};
MGP_ASSERT_POD(MGPFramebufferState, 304);
// GL_MAX_VERTEX_ATTRIBS as MobileGL advertises it, on the MG_Pipe side of the boundary.
// It bounds the two declared counts of MGPVertexElements, the applier's two per-CSO
// arrays and the vertex-buffer set. It MUST equal VertexArrayObject::MAX_VERTEX_ATTRIBS;
// MG_Impl/Pipe/PipeFill.cpp is the one translation unit that sees both and carries the
// static_assert, because this header may not include a frontend one.
inline constexpr Uint32 kMGPipeMaxVertexAttribs = 32;
struct MGPVertexBuffer {
MGPipeHandle Res;
Uint64 Offset;
@@ -366,9 +373,25 @@ namespace MobileGL::MG_Pipe {
// Var-tail header: MGPVertexBuffer[Count] follows.
struct MGPVertexBuffers {
Uint32 Start, Count;
// The vertex-FETCH base instance these offsets are valid for (P3a, D-H1). It is DRAW
// state, not VAO state, and it is NOT the same thing as MGPDrawInfo::StartInstance:
// that one is the GL draw's baseInstance and feeds gl_BaseInstance, this one is the
// shift the fetch address of an instanced array needs when the device has no native
// base-instance support. The server decides whether to emulate it or let
// GL_EXT_base_instance do the work - emulation is server-owned - so the client sends
// the draw's raw value and never a pre-shifted offset.
//
// IT IS A ContentHash INPUT, and that is a requirement rather than a nicety:
// set_vertex_buffers is suppressed on an unchanged hash (MG_Impl/Pipe/
// SetHashSuppressor.h's SetVertexBuffers slot), so a baseInstance that moved while
// the buffer set did not would be suppressed and the server would keep the previous
// shift. It rides ONE PER EMITTED SET rather than per entry: per entry the shift is
// redundant and lets a malformed record disagree with itself.
Uint32 BaseInstance;
Uint32 Pad0;
Uint64 ContentHash;
};
MGP_ASSERT_POD(MGPVertexBuffers, 16);
MGP_ASSERT_POD(MGPVertexBuffers, 24);
// An independent call, NOT a subset of the VAO configuration version (D5).
struct MGPIndexBuffer {
@@ -616,6 +639,22 @@ namespace MobileGL::MG_Pipe {
}
inline Uint64 MGPipeSubDataBufferSize(const MGPSubData& record) { return record.UnionBox.W; }
// P3a, D-A5: the per-record half of resource_respecify's kNeedsAck.
//
// Flags are a PER-CALL static property and resource_respecify serves BOTH glBufferData
// and glBufferStorage. A bare kNeedsAck on the call would acknowledge every glBufferData
// in a world upload; only glBufferStorage is a real synchronous allocation and only it is
// allowed a synchronous ack. So kNeedsAck on the call means "records of this call MAY
// require an acknowledgement" and THIS predicate decides per record. In monolith the ack
// is ((void)0) - the applier is one function call away - and the transport wires the
// doorbell to this predicate when it lands.
//
// Immutable is exactly the right discriminator: it is set iff the store came from a
// glBufferStorage* entry point, which is the definition of the allowed case.
inline Bool MGPipeResourceRespecifyNeedsAck(const MGPResourceDesc& desc) {
return desc.Immutable != 0;
}
// The forward terminator for a server-initiated texture pull (section 7.1). May carry
// zero regions - that is how a pull that needs nothing is answered.
struct MGPSubDataComplete {
+66
View File
@@ -544,6 +544,61 @@ namespace MobileGL {
};
} // namespace MG_State::GLState
// ---- P3a: the WIRE forms of the two views above (ARCHITECTURE.md section on vertex
// elements; brief D-G2). Neither VertexAttribute nor VertexBufferBindingPoint can travel
// as itself: both hold a SharedPtr<BufferObject>, and a payload never contains a pointer.
// They live here rather than in MGPipeTypes.h so the structs they mirror are one screen
// away and a member added above has its wire twin in view; MGPipeTypes.h includes this
// header, so MG_Pipe sees them unqualified like every other value type.
//
// Both ride the create_vertex_elements BLOB, in ascending index order, attributes first:
// MGPVertexAttribWire[AttributeCount] then MGPVertexBindingPointWire[BindingPointCount],
// each count <= VertexArrayObject::MAX_VERTEX_ATTRIBS (32). The applier refuses a record
// whose declared counts do not match the blob's declared size.
// The resolved flat attribute view. Buffer identity does NOT travel here - it travels in
// set_vertex_buffers, which is what keeps this record stable while buffers change under
// it. Stride is the RESOLVED distance and a surviving 0 can only have come from the
// binding model (see VertexAttribute::Stride above); collapsing it back into the element
// size is what made KHR-GL43.vertex_attrib_binding.basic-input-case7/8 read past the
// buffer. Divisor is deliberately ABSENT: it is resolved per binding point and travels in
// MGPVertexBuffer::Divisor, which is where the backend's glVertexAttribDivisor reads it.
// LegacyStride / LegacyPointer are likewise absent - they are the glGetVertexAttrib*
// query answers and stay client-side, because nothing but the query path reads them.
struct MGPVertexAttribWire {
Uint64 Offset; // 0
Int32 Stride; // 8
Uint32 Type; // 12 DataType
Uint8 Size; // 16 1..4; GL_BGRA keeps 4
Uint8 Enabled; // 17
Uint8 Normalized; // 18
Uint8 IsInteger; // 19
// CARRIED SEPARATELY from Type == Float64, and it has to be: VertexAttribFormat(
// GL_DOUBLE) also reads doubles from memory but asks for them converted to float,
// while VertexAttribLFormat keeps all 64 bits. The backend's fp64 narrowing and its
// Adreno disabled-attribute workaround both key on telling the two apart.
Uint8 IsLong; // 20
Uint8 IsBgra; // 21
Uint8 BindingIndex; // 22 which MGPVertexBuffer entry feeds it (< MAX_VERTEX_ATTRIBS)
Uint8 Pad0; // 23
};
// The ARB_vertex_attrib_binding view. Buffer identity is again in set_vertex_buffers.
//
// WHY IT TRAVELS AT ALL, since no backend has ever read a binding point (the frontend
// resolves them eagerly into the flat view above, and grep finds zero backend reads of
// VertexBufferBindingPoint / GetAttributeBindingIndex / GetAttributeRelativeOffset): the
// record DECLARES BindingPointCount, PipeFields.def names it, and a record whose declared
// counts do not describe its own blob is a shape the applier's bounds gate would have to
// police forever. Carrying both views keeps the record self-describing, and the cost is
// paid once per configuration change rather than per draw - the blob rides only on
// create_vertex_elements.
struct MGPVertexBindingPointWire {
Uint64 Offset; // 0
Int32 Stride; // 8 GL 4.6 core table 23.4: the INITIAL value is 16, not 0
Uint32 Divisor; // 12
};
// ---- trip wires (P0.5). Sizes are what every ABI MobileGL ships on produces: every
// member is a fixed-width scalar, an enum of one, or an array of those - no pointer, no
// SizeT - except the vertex types, which carry SharedPtr<BufferObject> by design and are
@@ -560,5 +615,16 @@ namespace MobileGL {
static_assert(std::is_trivially_copyable_v<SamplerParameters> && sizeof(SamplerParameters) == 100);
static_assert(std::is_trivially_copyable_v<MG_State::GLState::VertexAttributeVersion> &&
sizeof(MG_State::GLState::VertexAttributeVersion) == 6);
// The two P3a wire views. Unlike the structs they mirror these ARE flat PODs with
// explicit padding, so the trip wire is the same one every MGPipe payload carries: the
// blob they ride in is memcpy'd, and a field silently changing width is a protocol break
// no test would otherwise see. (MGP_ASSERT_POD is MGPipeTypes.h's and that header
// includes this one, so the assertions are spelled out here instead.)
static_assert(std::is_trivially_copyable_v<MGPVertexAttribWire> &&
sizeof(MGPVertexAttribWire) == 24);
static_assert(std::is_standard_layout_v<MGPVertexAttribWire>);
static_assert(std::is_trivially_copyable_v<MGPVertexBindingPointWire> &&
sizeof(MGPVertexBindingPointWire) == 16);
static_assert(std::is_standard_layout_v<MGPVertexBindingPointWire>);
} // namespace MobileGL
#endif // MOBILEGL_MG_PIPE_VALUE_TYPES_H
+94
View File
@@ -379,6 +379,12 @@ namespace MobileGL::MG_Pipe {
MGPipeApplierState g_applier{};
// The installed handle-shaped resource table. Null until a backend registers one,
// which is what makes the client half landable on its own: with nothing here every
// frontend dispatch falls through to the op table this one replaces, and the tree
// behaves exactly as it did.
const MGPipeResourceOps* g_resourceOps = nullptr;
MGPipeRenderStateCsoRecord* FindCso(MGPipeHandle handle) {
if (handle.Slot >= g_applier.RenderStateCsos.size()) return nullptr;
MGPipeRenderStateCsoRecord& record = g_applier.RenderStateCsos[handle.Slot];
@@ -392,6 +398,9 @@ namespace MobileGL::MG_Pipe {
MGPipeApplierState& MGPipeApplier() { return g_applier; }
void MGPipeSetResourceOps(const MGPipeResourceOps* ops) { g_resourceOps = ops; }
const MGPipeResourceOps* MGPipeGetResourceOps() { return g_resourceOps; }
void MGPipeApplierReset() {
g_applier.RenderStateCsos.clear();
g_applier.BoundRenderStateCso = kMGPipeNullHandle;
@@ -402,6 +411,22 @@ namespace MobileGL::MG_Pipe {
g_applier.ResidualDivergences = 0;
g_applier.PatchCarrierComparisons = 0;
g_applier.PatchCarrierDivergences = 0;
// P3a. A fresh context is a fresh server: the records describe objects the new
// context never made, and the three serials are per-context MGGens that must not
// carry a previous context's count into a twin's "have I synced this?" compare.
// The OP TABLE is deliberately NOT cleared here - it is installed and uninstalled by
// the backend's own bring-up and teardown, not by a state reset.
g_applier.Resources.clear();
g_applier.VertexElementsCsos.clear();
g_applier.BoundVertexElements = kMGPipeNullHandle;
g_applier.VertexBuffers = {};
g_applier.VertexBufferStart = 0;
g_applier.VertexBufferCount = 0;
g_applier.VertexFetchBaseInstance = 0;
g_applier.VertexBuffersSerial = 0;
g_applier.IndexBuffer = MGPIndexBuffer{};
g_applier.IndexBufferSerial = 0;
g_applier.MapPersistentRoundtrips = 0;
}
void MGPipeApplyCreateRenderState(const MGPRenderStateDesc& desc, const void* chunkBytes) {
@@ -654,6 +679,75 @@ namespace MobileGL::MG_Pipe {
}
}
// ================================================================================
// P3a: the fourteen new entry points, AT THE CONTRACT COMMIT ONLY.
//
// Every body below is a deliberate no-op. The contract commit's job is the SHAPE - the
// signatures the client, the backend and the gates compile against, the records they
// write into and the op table they dispatch through - and the bodies land in the two
// commits that follow on this branch, before anything emits a single one of these calls.
//
// NOTHING REACHES THEM HERE, and that is checked rather than hoped: the two subsystem
// bits are not in MG_Impl/Pipe/PipeFill.cpp's kMGPipeWiredSubsystems, the dirty bits that
// would gate the emission still map to no subsystem, and the emitters beside them are
// stubs that emit nothing. A no-op that could be reached would be worse than an
// unimplemented one - it would silently drop a mutation - which is exactly why the two
// halves land in one commit apiece rather than one half at a time.
// ================================================================================
void MGPipeApplyResourceCreate(const MGPResourceDesc& desc) { (void)desc; }
void MGPipeApplyResourceRespecify(const MGPResourceDesc& desc, const void* initialBytes) {
(void)desc;
(void)initialBytes;
}
void MGPipeApplyResourceSubData(const MGPSubData& record, const void* bytes) {
(void)record;
(void)bytes;
}
void MGPipeApplyBufferSubDataResident(const MGPSubData& record, const void* bytes) {
(void)record;
(void)bytes;
}
void MGPipeApplyResourceFlushRange(const MGPFlushRange& record, const void* bytes) {
(void)record;
(void)bytes;
}
void MGPipeApplyResourceReadback(const MGPReadback& record) { (void)record; }
void MGPipeApplyResourceDestroy(const MGPHandleOnly& handle) { (void)handle; }
// A DECLINE, which is a real answer rather than a failure: the persistent-map acquisition
// is allowed to say no, the caller already has that branch, and null is what it reads.
void* MGPipeApplyMapPersistent(const MGPHandleOnly& handle, Uint64 size, const void* seedBytes) {
(void)handle;
(void)size;
(void)seedBytes;
return nullptr;
}
void MGPipeApplyUnmapPersistent(const MGPHandleOnly& handle) { (void)handle; }
void MGPipeApplyCreateVertexElements(const MGPVertexElements& desc, const void* blobBytes) {
(void)desc;
(void)blobBytes;
}
void MGPipeApplyBindVertexElements(const MGPHandleOnly& handle) { (void)handle; }
void MGPipeApplyDeleteVertexElements(const MGPHandleOnly& handle) { (void)handle; }
void MGPipeApplySetVertexBuffers(const MGPVertexBuffers& hdr, const MGPVertexBuffer* tail) {
(void)hdr;
(void)tail;
}
void MGPipeApplySetIndexBuffer(const MGPIndexBuffer& record) { (void)record; }
void MGPipeDeriveRenderStateFields(PipeInputs& inputs) {
// The derivation itself lives in MGPipeApplyAccess above, because that is the one
// struct PipeInputs names as a friend - see D5 there for what it recomputes and which
+198
View File
@@ -45,6 +45,94 @@ namespace MobileGL::MG_Pipe {
Array<Uint8, kMGPipePipelineChunkBytes> PipelineBytes{};
};
// ---------------------------------------------------------------------------------
// P3a: the handle-shaped resource op table (D-A1)
// ---------------------------------------------------------------------------------
// The SECOND backend op table, beside BufferBackendOps. Registered by the active backend
// at bring-up and cleared at shutdown, exactly as that one is; a null table means "this
// backend has not taken the resource family over", and the frontend then dispatches the
// old way, which is what lets the client half land on its own and what keeps a backend
// whose buffer path is a later phase untouched.
//
// NO FRONTEND TYPE APPEARS HERE, and that is the whole point of the conversion: every
// hook it replaces took a frontend heap reference and four of them read that object's
// shadow bytes. A resource is an MGPipeHandle plus a payload record plus, where the call
// carries content, a companion `const void*`.
//
// THE COMPANION POINTER IS NOT A NEW IDEA - MGPipeApplyCreateRenderState already carries
// a blob beside its POD for the same reason: in monolith a blob needs no MGPBlobRef and
// the pointer is the client's own shadow base, so the call is zero-copy and behaviour is
// unchanged. How those bytes cross under a real transport is that phase's problem and
// that phase's flag edit; resource_respecify deliberately does NOT carry kHasBlob here,
// because a kHasBlob record must own an MGPBlobRef member and MGPResourceDesc has none.
//
// SubDataResident MAY BE NULL and stays nullable on purpose: one backend deliberately
// does not implement it (kOptional in the catalogue), the frontend checks it exactly as
// it checks the op table it replaces, and giving that backend a real implementation is a
// behaviour change that belongs in its own change, not in this migration.
struct MGPipeResourceOps {
void (*Create)(MGPipeHandle res, const MGPResourceDesc& desc);
void (*Respecify)(MGPipeHandle res, const MGPResourceDesc& desc, const void* initialBytes);
void (*SubData)(MGPipeHandle res, const MGPSubData& record, const void* bytes);
// kOptional: may be null. `bytes` is the application's staging store and is valid for
// the duration of the call only.
void (*SubDataResident)(MGPipeHandle res, const MGPSubData& record, const void* bytes);
void (*FlushRange)(MGPipeHandle res, const MGPFlushRange& record, const void* bytes);
void (*Readback)(MGPipeHandle res, const MGPReadback& record);
void (*Destroy)(MGPipeHandle res);
void* (*MapPersistent)(MGPipeHandle res, Uint64 size, const void* seedBytes);
void (*UnmapPersistent)(MGPipeHandle res);
};
// Install / read the table. A null argument uninstalls, which is what a backend does at
// context teardown and what every build that has not migrated the family sits at.
void MGPipeSetResourceOps(const MGPipeResourceOps* ops);
const MGPipeResourceOps* MGPipeGetResourceOps();
// ---------------------------------------------------------------------------------
// P3a: the applier's own records (D-G4)
// ---------------------------------------------------------------------------------
// One record per live resource, indexed by MGPipeHandle::Slot, kind Buffer; slot 0 is the
// reserved null handle and is never live.
struct MGPipeResourceRecord {
Uint32 Gen = 0;
Bool Live = false;
// The last create/respecify, verbatim. The backend reads its Width / Usage /
// StorageFlags / HasDefinedContent instead of asking the frontend object.
MGPResourceDesc Desc{};
// SERVER-OWNED, monotone, and it never crosses the line: an MGGen-class counter, ++ on
// every mutation this applier applies (respecify, sub-data, flush range, resident
// sub-data). It is what replaces the frontend change serial the backend used to
// mirror, and no MGPipe call may require the client to provide or know one.
Uint64 Serial = 0;
// ALWAYS FALSE IN P3a, AND WRITTEN BY NOBODY. It exists so the phase that pushes
// persistent-mapped host writes can set it with zero new record kinds; a verify build
// pins that it is false, so that phase cannot land a silent semantic change under it.
Bool HasLiveHostWrites = false;
};
// The vertex-elements CSO as the applier holds it: the unpacked blob, both views, plus
// the serial the backend's per-VAO twin compares against instead of a wrapping Uint16
// configuration version plus an identity patch.
//
// The 32 is GL's MAX_VERTEX_ATTRIBS as MobileGL advertises it (kMGPipeMaxVertexAttribs,
// MGPipeTypes.h), which is also the bound the record's two declared counts are checked
// against before the blob is unpacked.
struct MGPipeVertexElementsRecord {
Uint32 Gen = 0;
Bool Live = false;
Uint32 AttributeCount = 0;
Uint32 BindingPointCount = 0;
Array<MGPVertexAttribWire, kMGPipeMaxVertexAttribs> Attributes{};
Array<MGPVertexBindingPointWire, kMGPipeMaxVertexAttribs> BindingPoints{};
// Server-owned MGGen, ++ on every create_vertex_elements applied to this handle -
// including a RE-create on the same handle, which is how a configuration change
// travels (the handle is minted per frontend VAO and Gen moves only on slot reuse).
Uint64 ContentSerial = 0;
};
struct MGPipeApplierState {
// Indexed by slot; slot 0 is the reserved null handle and is never live
// (MGPipeHandles.h kMGPipeFirstAllocatableSlot).
@@ -84,6 +172,42 @@ namespace MobileGL::MG_Pipe {
Uint32 ResidualDivergences = 0; // cumulative
Uint32 PatchCarrierComparisons = 0; // cumulative, armed set_patch_state calls only
Uint32 PatchCarrierDivergences = 0; // cumulative
// ---- P3a (D-G4). All per context, like everything above. ----
// Indexed by MGPipeHandle::Slot of kind Buffer / VertexElementsCso.
Vector<MGPipeResourceRecord> Resources;
Vector<MGPipeVertexElementsRecord> VertexElementsCsos;
// The last bind_vertex_elements. Null is legal and means "no VAO bound".
MGPipeHandle BoundVertexElements = kMGPipeNullHandle;
// The last set_vertex_buffers, as received: the entries, the window they describe,
// and the fetch base instance they are valid for.
Array<MGPVertexBuffer, kMGPipeMaxVertexAttribs> VertexBuffers{};
Uint32 VertexBufferStart = 0;
Uint32 VertexBufferCount = 0;
// The RAW value the client sent (MGPVertexBuffers::BaseInstance) resolved by the
// server's own decision about whether to emulate the fetch shift. The client never
// pre-shifts an offset and never learns the answer: emulation is server-owned.
Uint32 VertexFetchBaseInstance = 0;
// Server-owned MGGen, ++ on every applied set_vertex_buffers. It is what retires the
// backend twin's wrapping-Uint16-plus-identity patches.
Uint64 VertexBuffersSerial = 0;
// The last set_index_buffer. Independent of the vertex-elements configuration by
// design (D5): the index slot is not part of a VAO's configuration version.
MGPIndexBuffer IndexBuffer{};
Uint64 IndexBufferSerial = 0;
// Every map_persistent EMISSION, i.e. every acquisition attempt - mint OR decline -
// because every one of them needs an answer from the resource owner. In monolith the
// answer is free; under a transport it is a real round trip. The number is therefore
// the same in both modes and is "one per storage definition", which is what makes it
// assertable today instead of a counter that can only ever read zero. The counter an
// operator greps is PipeStats' map-persistent-roundtrips (mpr); this member is the
// applier-side observable a unit case reads without a stats window.
Uint64 MapPersistentRoundtrips = 0;
};
// The monolith's single applier. Under split there is one per served context.
@@ -121,6 +245,80 @@ namespace MobileGL::MG_Pipe {
// block - which is the point. A disagreement is Fatal{PipeResidualDiverged, "<Cap>"}.
void MGPipeApplySetResidualValueState(const ResidualValueBlock& block);
// ---------------------------------------------------------------------------------
// P3a: the nine resource entry points (D-A1, D-A2)
// ---------------------------------------------------------------------------------
//
// These are the ONE exception to push-at-validate: they are applied at the GL call that
// causes them, from the same dispatchers that call the old op table today, because that
// is already where those hooks run. Nothing about buffers moves to validate time here.
//
// The `bytes` companion of the three content-carrying calls is the client's shadow base,
// never a copy (see MGPipeResourceOps). A null is a real answer wherever the payload says
// the content is undefined.
//
// AT THE CONTRACT COMMIT EVERY BODY BELOW IS A STUB. The signatures are what the client,
// the backend and the gates compile against, and the records above are what they write
// into; the bodies land in the two commits that follow this one on the same branch.
// resource_create: mints the record and marks the slot Live. Emitted from the buffer
// object's CONSTRUCTOR, so a resource exists before anything can name it; storage is
// defined lazily by the first respecify and a backend tolerates a resource with none.
void MGPipeApplyResourceCreate(const MGPResourceDesc& desc);
// resource_respecify: replaces the stored descriptor and bumps Serial. `initialBytes` is
// the shadow when desc.HasDefinedContent, else null. kNeedsAck on the call,
// MGPipeResourceRespecifyNeedsAck(desc) per record - only an immutable store acks.
void MGPipeApplyResourceRespecify(const MGPResourceDesc& desc, const void* initialBytes);
// resource_subdata, buffer half: the destination range rides in the record's box through
// MGPipeSetSubDataBufferRange, and a false from that helper is where the EMITTER split.
// The applier stores nothing per record - contents are the backend's - and bumps Serial.
void MGPipeApplyResourceSubData(const MGPSubData& record, const void* bytes);
// buffer_subdata_resident: same shape; `bytes` is the application's staging store and is
// valid for the duration of the call only. The op-table entry may be null.
void MGPipeApplyBufferSubDataResident(const MGPSubData& record, const void* bytes);
// resource_flush_range: record.AccessFlags are the application's REAL mapping flags, not
// a normalised subset - the backend reads them per call to choose its upload shape.
void MGPipeApplyResourceFlushRange(const MGPFlushRange& record, const void* bytes);
// resource_readback: whole-buffer by contract. The answer travels back through the
// reverse channel, and the writeback happens BEFORE the mutation epoch bumps, never
// after - the ordering is a correctness rule, not a preference.
void MGPipeApplyResourceReadback(const MGPReadback& record);
// resource_destroy: clears Live and drops the record, then the backend frees its twin.
// The CLIENT frees the slot afterwards, in that order, because the allocator forgets the
// lifetime id on free and a notice resolved twice finds nothing the second time.
void MGPipeApplyResourceDestroy(const MGPHandleOnly& handle);
// map_persistent: bumps MapPersistentRoundtrips and asks the backend. Returns the
// coherent host pointer the resource owner donated, or null for a DECLINE - which is a
// real answer and the reason the call is kOptional as well as kReplySlot. `seedBytes` is
// the shadow, still live at this point, for the backends that seed the new store from it.
void* MGPipeApplyMapPersistent(const MGPHandleOnly& handle, Uint64 size, const void* seedBytes);
// unmap_persistent: the donation ends. Never emitted by P3a's own paths; the call exists
// so the pair is complete and the transport has both halves.
void MGPipeApplyUnmapPersistent(const MGPHandleOnly& handle);
// ---------------------------------------------------------------------------------
// P3a: the five vertex-input entry points (D-G, D-H, D-I)
// ---------------------------------------------------------------------------------
// create_vertex_elements. `blobBytes` is MGPVertexAttribWire[desc.AttributeCount]
// immediately followed by MGPVertexBindingPointWire[desc.BindingPointCount], both in
// ascending index order. The applier REFUSES a record whose declared counts do not
// describe its own blob, and both counts are bounded by kMGPipeMaxVertexAttribs.
// Re-issuing on the same handle is how a configuration change travels; it bumps
// ContentSerial and does not rebind.
void MGPipeApplyCreateVertexElements(const MGPVertexElements& desc, const void* blobBytes);
// bind_vertex_elements. The null handle is legal and means "no VAO bound".
void MGPipeApplyBindVertexElements(const MGPHandleOnly& handle);
// delete_vertex_elements: emitted from ONE place, the frontend object's death notice.
void MGPipeApplyDeleteVertexElements(const MGPHandleOnly& handle);
// set_vertex_buffers: `tail` is hdr.Count MGPVertexBuffer entries starting at hdr.Start.
// hdr.BaseInstance is the DRAW's raw base instance; the applier resolves whether to shift
// and stores the answer in VertexFetchBaseInstance. Bumps VertexBuffersSerial.
void MGPipeApplySetVertexBuffers(const MGPVertexBuffers& hdr, const MGPVertexBuffer* tail);
// set_index_buffer: an independent call, NOT a subset of the vertex-elements
// configuration. Bumps IndexBufferSerial.
void MGPipeApplySetIndexBuffer(const MGPIndexBuffer& record);
// ---------------------------------------------------------------------------------
// The derivation step (ARCHITECTURE.md 5.3, P2 brief D5)
// ---------------------------------------------------------------------------------
+7 -1
View File
@@ -16,6 +16,12 @@
// kScreen lands in struct MGPipeScreen, every other class in struct
// MGPipeContext (plan section 4.3).
// Flags : kNone | kNeedsAck | kHasBlob | kVarTail | kHostSpan | kReplySlot | kOptional
// kNeedsAck on a call means records of this call MAY require an ack; a
// per-record predicate decides. resource_respecify carries it for
// glBufferStorage - a real synchronous allocation, and the only entry point
// allowed a synchronous ack - and MGPipeResourceRespecifyNeedsAck(desc)
// (MGPipeTypes.h) is what says so, which is why the same call still carries
// every glBufferData without acknowledging one.
//
// 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
@@ -73,7 +79,7 @@
/* ---- screen: caps, resources, persistent map, fences (plan 4.4.1) ---- */ \
X(GetCaps, MGPCaps, kScreen, kReplySlot) \
X(ResourceCreate, MGPResourceDesc, kScreen, kNone) \
X(ResourceRespecify, MGPResourceDesc, kScreen, kNone) \
X(ResourceRespecify, MGPResourceDesc, kScreen, kNeedsAck) \
X(ResourceDestroy, MGPHandleOnly, kScreen, kNone) \
X(MapPersistent, MGPHandleOnly, kScreen, kReplySlot|kOptional) \
X(UnmapPersistent, MGPHandleOnly, kScreen, kOptional) \
+18 -2
View File
@@ -97,7 +97,7 @@
F(Res) F(Offset) F(Stride) F(Divisor) F(BindingIndex)
#define MGP_FIELDS_MGPVertexBuffers(F) \
F(Start) F(Count) F(ContentHash)
F(Start) F(Count) F(BaseInstance) F(ContentHash)
#define MGP_FIELDS_MGPIndexBuffer(F) \
F(Res) F(Offset) F(IndexSize)
@@ -295,6 +295,21 @@
#define MGP_FIELDS_MGHostSpan(F) \
F(Ptr) F(Seg) F(Size) F(Offset)
// P3a's two vertex wire views (MGPipeValueTypes.h). They are not call payloads either: they
// are the ELEMENTS of create_vertex_elements' blob, and the comparator has to see into them
// for the same reason it sees into the value structs - a blob compared with memcmp would
// false-differ on MGPVertexAttribWire::Pad0. Divisor is deliberately not in the attribute
// list (it travels in MGPVertexBuffer) and the two Legacy* query answers are deliberately not
// on the wire at all; both absences are argued in MGPipeValueTypes.h and both are enforced
// here by gen_pipe.py's "every list names exactly its struct's direct members" rule.
#define MGP_FIELDS_MGPVertexAttribWire(F) \
F(Offset) F(Stride) F(Type) F(Size) F(Enabled) F(Normalized) F(IsInteger) F(IsLong) F(IsBgra) \
F(BindingIndex)
#define MGP_FIELDS_MGPVertexBindingPointWire(F) \
F(Offset) F(Stride) F(Divisor)
// Every payload above, in the order the comparator is generated. Keep in sync with the
// macros; gen_pipe.py reads THIS list to know what to emit.
#define MGP_VERIFY_PAYLOAD_LIST(P) \
@@ -312,6 +327,7 @@
P(MGPXfbAccounting) P(MGPStreamOutputControl) P(MGPFlush) P(MGPPresent) P(MGPSwapInterval) \
P(MGPSurfaceInfo) \
P(RenderStateParameters) P(PixelStoreParameters) P(PerBufferBlendState) P(StencilFaceState) \
P(DynamicBackendParameters) P(MGHostSpan)
P(DynamicBackendParameters) P(MGHostSpan) \
P(MGPVertexAttribWire) P(MGPVertexBindingPointWire)
// clang-format on
+4 -2
View File
@@ -306,6 +306,7 @@ inline constexpr const char* kMGPipeInputFieldFilledBy[kMGPipeInputFieldCount] =
enum class MGPipeFieldEmitter : Uint8 {
kNone = 0,
BindRenderState,
BindVertexElements,
CreateRenderState,
SetDynamicState,
SetPatchState,
@@ -315,6 +316,7 @@ enum class MGPipeFieldEmitter : Uint8 {
inline constexpr const char* kMGPipeFieldEmitterNames[] = {
"kNone",
"BindRenderState",
"BindVertexElements",
"CreateRenderState",
"SetDynamicState",
"SetPatchState",
@@ -327,7 +329,7 @@ inline constexpr MGPipeFieldEmitter kMGPipeFieldEmittedBy[kMGPipeInputFieldCount
MGPipeFieldEmitter::CreateRenderState, // GetBlendEquationIndexed
MGPipeFieldEmitter::CreateRenderState, // GetBlendFuncIndexed
MGPipeFieldEmitter::kNone, // GetBoundTransformFeedbackName
MGPipeFieldEmitter::kNone, // GetBoundVertexArray
MGPipeFieldEmitter::BindVertexElements, // GetBoundVertexArray
MGPipeFieldEmitter::kNone, // GetBufferBindingSlot
MGPipeFieldEmitter::kNone, // GetBufferBindingPoint
MGPipeFieldEmitter::kNone, // GetBufferBindingPointCount
@@ -386,7 +388,7 @@ inline constexpr MGPipeFieldEmitter kMGPipeFieldEmittedBy[kMGPipeInputFieldCount
MGPipeFieldEmitter::kNone, // GetBoundTransformFeedbackLifetimeId
MGPipeFieldEmitter::kNone, // HasOpenTransformFeedbackSpan
};
inline constexpr SizeT kMGPipeEmittedFieldCount = 33;
inline constexpr SizeT kMGPipeEmittedFieldCount = 34;
struct MGPipeFilledState {
Uint64 CurrentVerbSerial;
+17 -1
View File
@@ -113,6 +113,8 @@ inline Bool MGPipeVerify(const PerBufferBlendState& a, const PerBufferBlendState
inline Bool MGPipeVerify(const StencilFaceState& a, const StencilFaceState& b, const char** outField);
inline Bool MGPipeVerify(const DynamicBackendParameters& a, const DynamicBackendParameters& b, const char** outField);
inline Bool MGPipeVerify(const MGHostSpan& a, const MGHostSpan& b, const char** outField);
inline Bool MGPipeVerify(const MGPVertexAttribWire& a, const MGPVertexAttribWire& b, const char** outField);
inline Bool MGPipeVerify(const MGPVertexBindingPointWire& a, const MGPVertexBindingPointWire& b, const char** outField);
template <>
struct MGPipeHasFieldVerifier<MGPBlobRef> : std::true_type {};
@@ -252,6 +254,10 @@ template <>
struct MGPipeHasFieldVerifier<DynamicBackendParameters> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGHostSpan> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPVertexAttribWire> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPVertexBindingPointWire> : std::true_type {};
template <class T>
inline Bool MGPipeFieldEqual(const T& a, const T& b) {
@@ -643,6 +649,16 @@ inline Bool MGPipeVerify(const MGHostSpan& a, const MGHostSpan& b, const char**
return true;
}
inline Bool MGPipeVerify(const MGPVertexAttribWire& a, const MGPVertexAttribWire& b, const char** outField) {
MGP_FIELDS_MGPVertexAttribWire(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPVertexBindingPointWire& a, const MGPVertexBindingPointWire& b, const char** outField) {
MGP_FIELDS_MGPVertexBindingPointWire(MGP_VERIFY_FIELD)
return true;
}
#undef MGP_VERIFY_FIELD
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 69;
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 71;
+34
View File
@@ -135,8 +135,42 @@ if (MSVC)
target_compile_options(MagmaPipeIdentityTest PRIVATE /Zc:preprocessor)
endif()
# P3a's two suites. Their targets and this registration are the CONTRACT commit's, for the
# same reason the four P2 suites' are: their CONTENTS belong to two later packages each, and
# neither of them should have to come back to this file to add a case.
#
# They link gtest rather than gtest_main and carry their own main(), like PipeInputsTest and
# RenderStateSpansTest: the applier's bounds and protocol trip wires report through a log line
# in a shipped push build and std::abort() in a poison or verify one, so a case that drives
# one reads the line back out of a file the process names before anything logs. Deciding that
# HERE is what keeps the later packages out of this file.
foreach(pipeTest ResourceEmitTest VertexInputEmitTest)
add_executable(${pipeTest} ${pipeTest}.cpp)
target_include_directories(${pipeTest} PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
${MGL_ROOT}/MobileGL/MG_Pipe
${MGL_ROOT}/3rdparty/xxHash
${MGL_ROOT}/3rdparty/Vulkan-Headers/include
${MGL_ROOT}/3rdparty/SPIRV-Reflect
)
target_link_libraries(${pipeTest} PRIVATE
GTest::gtest
${LINK_LIBRARIES}
)
if (MSVC)
target_compile_options(${pipeTest} PRIVATE /Zc:preprocessor)
endif()
endforeach()
include(GoogleTest)
gtest_discover_tests(PipeCatalogueTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
foreach(pipeTest ResourceEmitTest VertexInputEmitTest)
gtest_discover_tests(${pipeTest} DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
endforeach()
gtest_discover_tests(MagmaPipeIdentityTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(PipeInputsTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(RenderStateSpansTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
+117 -2
View File
@@ -349,15 +349,20 @@ TEST(PipeCatalogue, FloatVectorsCompareBitwise) {
}
// The six value structs have field lists of their own (P1 brief D8): 63 + 6 payloads, and
// the struct that used to memcmp is compared member by member.
// the struct that used to memcmp is compared member by member. P3a added the two vertex wire
// views as a seventh and eighth non-payload entry (63 + 8), for the same reason: they are the
// elements of create_vertex_elements' blob, and a memcmp over that blob would false-differ on
// MGPVertexAttribWire::Pad0.
TEST(PipeCatalogue, SixValueStructsHaveFieldLists) {
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 69u);
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 71u);
static_assert(MGPipeHasFieldVerifier<RenderStateParameters>::value);
static_assert(MGPipeHasFieldVerifier<PixelStoreParameters>::value);
static_assert(MGPipeHasFieldVerifier<PerBufferBlendState>::value);
static_assert(MGPipeHasFieldVerifier<StencilFaceState>::value);
static_assert(MGPipeHasFieldVerifier<DynamicBackendParameters>::value);
static_assert(MGPipeHasFieldVerifier<MGHostSpan>::value);
static_assert(MGPipeHasFieldVerifier<MGPVertexAttribWire>::value);
static_assert(MGPipeHasFieldVerifier<MGPVertexBindingPointWire>::value);
PixelStoreParameters p{};
PixelStoreParameters q{};
const char* field = nullptr;
@@ -468,3 +473,113 @@ TEST(PipeCatalogue, SubDataBufferRangeRidesInTheUnionBox) {
EXPECT_EQ(MGPipeSubDataBufferOffset(record), 0x7FFFFFFFull);
EXPECT_EQ(MGPipeSubDataBufferSize(record), 0xFFFFFFFFull);
}
// P3a, D-H1: set_vertex_buffers carries the vertex-FETCH base instance explicitly, one per
// emitted set rather than one per entry, and the header grew 16 -> 24 bytes to hold it.
//
// The size is the cheap half. The half a compiler cannot catch is the PipeFields.def row:
// MGPVertexBuffers still HAS a ContentHash and still asserts its size whether or not the
// field list names BaseInstance, and a comparator blind to the field would let a
// baseInstance-only divergence through under MOBILEGL_PIPE_VERIFY - which is the one gate
// that would otherwise have seen the suppression bug the ContentHash rule exists to prevent.
// So the field list is pinned the only way it can be: by making the comparator name it.
TEST(PipeCatalogue, VertexBufferSetCarriesAnExplicitBaseInstance) {
static_assert(sizeof(MGPVertexBuffers) == 24);
static_assert(sizeof(MGPVertexBuffer) == 32); // the per-entry struct did NOT change
EXPECT_EQ(sizeof(MGPVertexBuffers), 24u);
MGPVertexBuffers a{};
MGPVertexBuffers b{};
const char* field = nullptr;
EXPECT_TRUE(MGPipeVerify(a, b, &field));
b.Pad0 = 0x5A; // padding is not a field
EXPECT_TRUE(MGPipeVerify(a, b, &field));
b.Pad0 = 0;
b.BaseInstance = 7;
EXPECT_FALSE(MGPipeVerify(a, b, &field));
EXPECT_STREQ(field, "BaseInstance");
// The set still carries no fetch shift per entry: an entry that disagreed with its own
// header is a shape the applier would have to police, and MGPVertexBuffer's Pad0 stays
// padding rather than becoming a second copy of the same number.
MGPVertexBuffer left{};
MGPVertexBuffer right{};
right.Pad0 = 0x5A;
EXPECT_TRUE(MGPipeVerify(left, right, &field));
}
// P3a, D-G2: the two vertex wire views. They are what create_vertex_elements' blob is made
// of, so their sizes are the blob's stride and the applier's bounds arithmetic; and IsLong is
// carried SEPARATELY from Type, because a GL_DOUBLE format converted to float and a long
// format that keeps all 64 bits are different requests that a backend has to tell apart.
TEST(PipeCatalogue, VertexWireViewsAreFlatAndCarryIsLongSeparately) {
static_assert(sizeof(MGPVertexAttribWire) == 24);
static_assert(sizeof(MGPVertexBindingPointWire) == 16);
EXPECT_EQ(sizeof(MGPVertexAttribWire), 24u);
EXPECT_EQ(sizeof(MGPVertexBindingPointWire), 16u);
MGPVertexAttribWire a{};
MGPVertexAttribWire b{};
const char* field = nullptr;
EXPECT_TRUE(MGPipeVerify(a, b, &field));
b.Pad0 = 0x5A;
EXPECT_TRUE(MGPipeVerify(a, b, &field));
b.Pad0 = 0;
// Type unchanged, IsLong moved: a comparator that folded the two would miss this.
b.IsLong = 1;
EXPECT_FALSE(MGPipeVerify(a, b, &field));
EXPECT_STREQ(field, "IsLong");
MGPVertexBindingPointWire p{};
MGPVertexBindingPointWire q{};
EXPECT_TRUE(MGPipeVerify(p, q, &field));
q.Divisor = 2;
EXPECT_FALSE(MGPipeVerify(p, q, &field));
EXPECT_STREQ(field, "Divisor");
}
// P3a, D-A5: the tree's FIRST kNeedsAck, and the reason it is not a bare flag.
//
// Flags are a PER-CALL static property and resource_respecify serves both glBufferData and
// glBufferStorage. A bare kNeedsAck on the call would acknowledge every glBufferData in a
// world upload - a round trip per chunk store the moment a transport is under it. So the flag
// declares that records of this call MAY need one and MGPipeResourceRespecifyNeedsAck decides
// per record: only an immutable store, which is a real synchronous allocation.
//
// This is the negative control for a future flag that over-acks: in monolith the ack is a
// no-op, so the mistake cannot be shipped from here, and the phase where it would bite
// inherits this pin rather than the guess.
TEST(PipeCatalogue, ResourceRespecifyAcksOnlyImmutableStorage) {
Uint32 flags = 0;
#define MGP_FLAGS_OF_RESOURCE_RESPECIFY(Name, Payload, Class, Flags) \
if (std::strcmp(#Name, "ResourceRespecify") == 0) flags = static_cast<Uint32>(Flags);
MGP_CALL_LIST(MGP_FLAGS_OF_RESOURCE_RESPECIFY)
#undef MGP_FLAGS_OF_RESOURCE_RESPECIFY
EXPECT_EQ(flags & static_cast<Uint32>(kNeedsAck), static_cast<Uint32>(kNeedsAck));
// And it is the ONLY call that carries it: a second one would be a second decision, and
// this predicate answers for exactly one call.
Uint32 ackingCalls = 0;
#define MGP_COUNT_ACKING_CALLS(Name, Payload, Class, Flags) \
if ((static_cast<Uint32>(Flags) & static_cast<Uint32>(kNeedsAck)) != 0) ++ackingCalls;
MGP_CALL_LIST(MGP_COUNT_ACKING_CALLS)
#undef MGP_COUNT_ACKING_CALLS
EXPECT_EQ(ackingCalls, 1u);
// glBufferStorage: an immutable store, and the one entry point allowed a synchronous ack.
MGPResourceDesc immutable{};
immutable.Immutable = 1;
EXPECT_TRUE(MGPipeResourceRespecifyNeedsAck(immutable));
// glBufferData through the same call: never acknowledged, whatever else the descriptor
// says. The usage hint and a defined initial content are the two things a "well it looks
// synchronous" reading would key on, so both are set here on purpose.
MGPResourceDesc mutableStore{};
mutableStore.Immutable = 0;
mutableStore.Usage = 0x88E4; // GL_STATIC_DRAW, i.e. the most "final-looking" hint there is
mutableStore.HasDefinedContent = 1;
mutableStore.Width = 64u * 1024u;
EXPECT_FALSE(MGPipeResourceRespecifyNeedsAck(mutableStore));
// And the opcode did not move: a flag-word edit is not a catalogue edit.
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::ResourceRespecify), 3);
}
+117
View File
@@ -0,0 +1,117 @@
// MobileGL - MobileGL/MG_Test/Pipe/ResourceEmitTest.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
// P3a's resource family: the applier's record lifecycle and the client's emission of it.
//
// THE TARGET AND ITS ctest REGISTRATION ARE THE CONTRACT COMMIT'S; THE CONTENTS ARE NOT.
// Two packages fill this file in and neither of them touches MG_Test/Pipe/CMakeLists.txt to
// do it: the applier-side cases (a create marks the slot live, a respecify replaces the
// descriptor and bumps Serial, a destroy clears Live, a stale generation resolves to nothing,
// HasLiveHostWrites is false on every path this phase has, and the sub-data range encoding at
// both of its bounds with the emitter's splitter over it) belong to the branch that gives the
// entry points their bodies; the emitter-side cases (the sticky BindMask over every buffer
// target, on create AND on a following respecify; the slot released at destruction) belong to
// the client branch. They are disjoint TEST bodies in one file.
//
// THE SUITE IS `ResourceEmit`, not `ResourceEmitTest`: the file is XTest.cpp and the suite is
// X, which is this directory's convention (RenderStateSpansTest.cpp -> RenderStateSpans), and
// it is what the phase's gate greps for (`ctest -R '...|ResourceEmit\.'`).
//
// IT HAS ITS OWN main(), like PipeInputsTest and RenderStateSpansTest, and that is a decision
// taken here so that nobody has to come back to the CMake file for it: the applier's bounds
// gate reports through a trip wire whose verdict is a log line in a shipped push build and
// std::abort() in a poison or verify one, so a case that drives it reads the line back out of
// a file this process points MOBILEGL_LOG_FILE_PATH at before anything logs.
//
// Every case is a visible SKIP in a pull build rather than a vanishing test - the applier is
// compiled only under MOBILEGL_PIPE_PUSH - so `ctest -N` stays name-for-name identical
// between the pull and the push trees.
#include <gtest/gtest.h>
#include <filesystem>
#include <string>
#include <system_error>
#if defined(_WIN32)
#include <process.h>
#else
#include <unistd.h>
#endif
#include "Includes.h"
#include <MG_Pipe/MGPipe.h>
#if MOBILEGL_PIPE_PUSH
#include <MG_Pipe/PipeApply.h>
#endif
using namespace MobileGL;
using namespace MobileGL::MG_Pipe;
namespace {
String g_logPath;
int ProcessId() {
#if defined(_WIN32)
return _getpid();
#else
return static_cast<int>(getpid());
#endif
}
// The op table is INSTALLED BY A BACKEND, at its own bring-up, and uninstalled at its
// teardown - it is not part of the applier's state and MGPipeApplierReset deliberately
// does not clear it. A process with no backend in it therefore has none, and that is the
// fact the whole family's landability rests on: with no table registered every frontend
// dispatch falls through to the op table this one replaces, so the client half can land
// on its own without changing a single observable.
//
// It is also the negative control for the registration itself. A Set that did not stick
// would leave the family permanently dark, and nothing else in the tree would say so.
TEST(ResourceEmit, TheResourceOpTableIsUnregisteredUntilABackendInstallsOne) {
#if !MOBILEGL_PIPE_PUSH
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
#else
ASSERT_EQ(MGPipeGetResourceOps(), nullptr)
<< "something registered a resource op table in a unit-test process";
static const MGPipeResourceOps ops{};
MGPipeSetResourceOps(&ops);
EXPECT_EQ(MGPipeGetResourceOps(), &ops);
// A state reset is not a teardown: the table survives it, because the backend that
// installed it is still there.
MGPipeApplierReset();
EXPECT_EQ(MGPipeGetResourceOps(), &ops);
MGPipeSetResourceOps(nullptr);
EXPECT_EQ(MGPipeGetResourceOps(), nullptr);
#endif
}
} // namespace
int main(int argc, char** argv) {
// Before anything logs: the logger reads this variable once, on its first write, and
// caches the handle. The name carries this process's pid, and the file is removed on the
// way out.
namespace fs = std::filesystem;
const fs::path path =
fs::temp_directory_path() / ("mobilegl-resourceemit-test-" + std::to_string(ProcessId()) + ".log");
std::error_code ec;
fs::remove(path, ec);
g_logPath = path.string();
#if defined(_WIN32)
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
#else
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
#endif
::testing::InitGoogleTest(&argc, argv);
const int rc = RUN_ALL_TESTS();
fs::remove(path, ec);
return rc;
}
@@ -0,0 +1,122 @@
// MobileGL - MobileGL/MG_Test/Pipe/VertexInputEmitTest.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
// P3a's vertex-input family: create/bind/delete_vertex_elements, set_vertex_buffers and
// set_index_buffer, on both sides of the call.
//
// THIS SUITE IS A NAMED GATE. The phase's G6 is "for every VAO configuration the emitted
// MGPVertexElements blob + MGPVertexBuffers set + MGPIndexBuffer reproduce exactly the values
// the backend's VAO twin reads from the frontend today, field by field, for all 32 attribute
// slots", and it is spelled `ctest -R 'VertexInputEmit\.'`; G7 is its negative control, a
// script that stops the wire conversion copying ONE field and expects this suite to go red
// NAMING that field. So a case here must fail by field name, never by a bare count, or the
// control cannot answer.
//
// THE SUITE IS `VertexInputEmit`, not `VertexInputEmitTest`: the file is XTest.cpp and the
// suite is X, this directory's convention (RenderStateSpansTest.cpp -> RenderStateSpans), and
// it is what both gates grep for.
//
// THE TARGET AND ITS ctest REGISTRATION ARE THE CONTRACT COMMIT'S; THE CONTENTS ARE NOT - the
// client package writes the conversion cases, the base-instance suppression pair and the
// create/bind ping-pong pair into this file without touching MG_Test/Pipe/CMakeLists.txt.
//
// IT HAS ITS OWN main() for the same reason ResourceEmitTest does: the applier refuses a
// vertex-elements record whose declared counts do not describe its own blob, and that verdict
// is a log line in a shipped push build and std::abort() in a poison or verify one.
//
// Every case is a visible SKIP in a pull build rather than a vanishing test, so `ctest -N`
// stays name-for-name identical between the pull and the push trees.
#include <gtest/gtest.h>
#include <filesystem>
#include <string>
#include <system_error>
#if defined(_WIN32)
#include <process.h>
#else
#include <unistd.h>
#endif
#include "Includes.h"
#include <MG_Pipe/MGPipe.h>
#if MOBILEGL_PIPE_PUSH
#include <MG_Pipe/PipeApply.h>
#endif
using namespace MobileGL;
using namespace MobileGL::MG_Pipe;
namespace {
String g_logPath;
int ProcessId() {
#if defined(_WIN32)
return _getpid();
#else
return static_cast<int>(getpid());
#endif
}
// A FRESH CONTEXT IS A FRESH SERVER, and for this family that is not a nicety: the three
// serials are per-context MGGens and the backend's VAO twin decides "have I already
// synced this?" by comparing its own memo against them. A reset that carried a previous
// context's count over would let a twin believe it had synced a configuration it has
// never seen - the one shape the tracker's complete-state rule exists to forbid.
//
// The bound handle is null rather than "whatever was bound", the window is empty rather
// than 32 stale entries, and the fetch shift is 0 rather than the last draw's.
TEST(VertexInputEmit, AResetApplierCarriesNoVertexInputStateOver) {
#if !MOBILEGL_PIPE_PUSH
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
#else
MGPipeApplierState& applier = MGPipeApplier();
applier.BoundVertexElements = MGPipeHandle{7, 3};
applier.VertexBufferStart = 1;
applier.VertexBufferCount = 5;
applier.VertexFetchBaseInstance = 9;
applier.VertexBuffersSerial = 42;
applier.IndexBufferSerial = 43;
applier.MapPersistentRoundtrips = 44;
MGPipeApplierReset();
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundVertexElements));
EXPECT_EQ(MGPipeApplier().VertexBufferStart, 0u);
EXPECT_EQ(MGPipeApplier().VertexBufferCount, 0u);
EXPECT_EQ(MGPipeApplier().VertexFetchBaseInstance, 0u);
EXPECT_EQ(MGPipeApplier().VertexBuffersSerial, 0u);
EXPECT_EQ(MGPipeApplier().IndexBufferSerial, 0u);
EXPECT_EQ(MGPipeApplier().MapPersistentRoundtrips, 0u);
EXPECT_TRUE(MGPipeApplier().VertexElementsCsos.empty());
EXPECT_TRUE(MGPipeApplier().Resources.empty());
#endif
}
} // namespace
int main(int argc, char** argv) {
// Before anything logs: the logger reads this variable once, on its first write, and
// caches the handle. The name carries this process's pid, and the file is removed on the
// way out.
namespace fs = std::filesystem;
const fs::path path =
fs::temp_directory_path() / ("mobilegl-vertexinputemit-test-" + std::to_string(ProcessId()) + ".log");
std::error_code ec;
fs::remove(path, ec);
g_logPath = path.string();
#if defined(_WIN32)
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
#else
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
#endif
::testing::InitGoogleTest(&argc, argv);
const int rc = RUN_ALL_TESTS();
fs::remove(path, ec);
return rc;
}
+5
View File
@@ -156,6 +156,10 @@ namespace {
// the pull build has no CSO to mint and must stay symbol-identical.
EXPECT_NE(line.find("cso[csom="), String::npos) << line;
EXPECT_NE(line.find("csob="), String::npos) << line;
// P3a's persistent-map acquisition attempts ride the same bracket. It is the counter
// the storage-regrow gate reads, so its short name is pinned where an operator's
// grep would break.
EXPECT_NE(line.find("mpr="), String::npos) << line;
#endif
}
@@ -276,6 +280,7 @@ namespace {
#if MOBILEGL_PIPE_PUSH
EXPECT_STREQ(PS::NameOf(PS::CallClass::RenderStateCsoMints), "render-state-cso-mints");
EXPECT_STREQ(PS::NameOf(PS::CallClass::RenderStateCsoBinds), "render-state-cso-binds");
EXPECT_STREQ(PS::NameOf(PS::CallClass::MapPersistentRoundtrips), "map-persistent-roundtrips");
#endif
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytRenderState), "espryt-render-state");
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytTextureSyncList), "espryt-texture-sync-list");
+5 -1
View File
@@ -174,7 +174,7 @@ namespace MobileGL::MG_Util::PipeStats {
"draws", "accessor-calls", "tex-upload-emissions", "tex-upload-box", "tex-upload-rect",
"tex-upload-jobs",
#if MOBILEGL_PIPE_PUSH
"render-state-cso-mints", "render-state-cso-binds",
"render-state-cso-mints", "render-state-cso-binds", "map-persistent-roundtrips",
#endif
};
const char* const kGateNames[kGateCount] = {
@@ -425,6 +425,10 @@ namespace MobileGL::MG_Util::PipeStats {
// line an operator greps.
line += "] cso[csom=" + std::to_string(calls[static_cast<Uint32>(CallClass::RenderStateCsoMints)]);
line += " csob=" + std::to_string(calls[static_cast<Uint32>(CallClass::RenderStateCsoBinds)]);
// P3a's persistent-map acquisition attempts, on the same bracket and for the same
// reason: it is push-only, and a window with an unexpected mpr= is the one number
// that says an adoption is happening per draw rather than per storage definition.
line += " mpr=" + std::to_string(calls[static_cast<Uint32>(CallClass::MapPersistentRoundtrips)]);
#endif
line += "] gates[";
for (Uint32 i = 0; i < kGateCount; ++i) {
+11
View File
@@ -113,6 +113,17 @@ namespace MobileGL::MG_Util::PipeStats {
// the cache's hit rate, and it is the number the CSO content-addressing negative
// control moves.
RenderStateCsoBinds,
// P3a's, and push-only for the same reason as the two above.
//
// EVERY map_persistent EMISSION, i.e. every acquisition ATTEMPT - a mint or a decline
// - because every one of them needs an answer from the resource owner. Counted that
// way on purpose: "round trips actually taken" is 0 by construction in a monolith and
// could never go red, which is not a counter, it is a decoration. Counted as attempts
// the number is identical in both modes, it is exactly "one per storage definition",
// and a regression that acquires per DRAW instead of per definition shows up on the
// first window. Counted at the client emitter, behind the usual Enabled() predicate;
// no timer anywhere.
MapPersistentRoundtrips,
#endif
Count
};