- The writer side recorded a member-rooted write bound to a reference (`for (auto& blendState : m_parameters.BlendStates)`) as the field alone, so seven RenderState setters read as writing nothing; the reader side resolved `render.PatchVertices` to the whole of m_parameters, so every setter that touched any byte of it "supported" NEW_PATCH_STATE and a row saying glClearColor publishes the patch state was green. Both were the same defect: the two sides did not resolve to the same token.
- Both sides now carry MEM:<member> and FIELD:<member>.<leaf>; a whole-member write or read is every field. A reference, pointer or range-for alias bound to a member-rooted lvalue is followed (rebinds and aliases of aliases included), a write through a call-result lvalue and a mutating call on a member-rooted lvalue count as whole writes, a const alias cannot be written through with `.`.
- A write, or a non-read-only method call, whose root the analysis cannot place - a reference parameter, a call result, a member without the m_ prefix, an unattributable assignment operator - taints the function; the taint rides the call-graph fixed point and every (row, bit) that depends on a tainted function is UNDECIDED, never a verdict. Nothing is trusted by name.
- Match rule: a writer supports a bit iff the two sides share a member and, both field-resolved, their field sets intersect; a member in common with no field information on one side is COARSE, reported and never counted; UNDER-FIRING only when both sides are resolved and disjoint for every member the shutter reads.
- --check counts only supported answers as derived, prints the COARSE and UNDECIDED tallies, and fails on an UNDECIDED row unless MGP_DIRTY_SURFACE_UNDECIDED_LIST in DirtySurface.def marks it; a mark on a row the derivation decides is a red gate too. The list is empty: all 8 non-render bit answers are supported at field level, and NEW_PATCH_STATE has exactly three legal carriers again.
- --self-test grows from 10 to 21 negative controls, including the synthetic bodies of every shape above through the real extractor, the NEW_PATCH_STATE analogue of the value-class control, the taint, COARSE and stale-mark paths, and positive controls for SetPixelStoreParam's pasted writes and the seven alias setters.
- the write analysis under-approximated in the exact direction its own claim forbids:
written_tokens recorded a write through a member's field (m_foo.bar = v) as FIELD:bar
and never as MEM:m_foo, while resolve_reader resolves a shutter's accessor to
MEM:m_foo, so for any struct-valued member the two halves could not meet; a new
MEMBER_ROOTED_WRITE_RE records both, for m_x.f, m_x[i].f, m_x->f and nested
- it could not see RenderState::SetPixelStoreParam's sixteen writes twice over, because
they are spelled with the token-pasting operator and the file was read raw - the
"field" it recorded was the macro parameter name, paramNameTail. The derivation now
expands the function-like macros defined under its two roots (directives blanked,
parameters substituted, ## pasted), which is also what makes SET_CAPABILITY's
m_parameters.capability##Enabled writes visible
- and it now DECLINES rather than answers wherever it cannot say it read every writer:
a body carrying a construct it does not model (an unexpandable token paste), anything
that reaches such a body through the call-graph fixed point the writes already travel,
and any shutter member with a write-shaped occurrence outside the analysed roots.
--check prints every decline with its site, plus how many bodies and files the absence
claim rests on and the one place it stays coarse
- the BitwiseEqual bits' shutter window now also starts at the last `}` before the
`dirty |=`, so the pack block's trailing `m_pack = pack;` no longer leaks the pixel
store into NEW_PATCH_STATE's reader set
- consequence in the map: X(SetPixelStoreParam, NEW_PIXEL_PACK) went from a verdict the
gate could not support - no function name in the tree could carry that bit - to an
accepted, checked answer, and the row it forced (kPulledEveryVerb, documented as "no
shutter exists, and none is needed yet") said that of the only mutator behind the
shipped set_pixel_pack_state. The row is now kPulledPartialShutter|NEW_PIXEL_PACK: the
pull is what holds on every mutating path, the bit moves on the eight Pack arms, and
both facts are machine-readable for the P3a reader D16 writes this file for
- --self-test grows from 7 negative controls to 10 - kPulledPartialShutter naming no
bit, a mutator whose write analysis is incomplete, and a shutter member written
outside the roots, the last two asserting a DECLINE and no verdict - and gains a
positive control that fails if SetPixelStoreParam's pasted writes ever go unread again
- X(SetNamedTransformFeedbackBinding, NEW_SO_TARGETS) was false on EVERY path: that
mutator binds a BufferState binding point or writes a saved-bindings entry, while the
bit's shutter mixes the buffer-CONTENT aggregate with the transform-feedback
generation, and a binding moves neither. It answers kPulledEveryVerb, which is what
reaches the backend today (GetBufferBindingPoint, in the class's may-read mask).
- X(SetPixelStoreParam, NEW_PIXEL_PACK) was false on the eight Unpack arms: the setter
writes both halves and the tracker's bit 2 is a byte compare of the PACK half alone,
because set_pixel_pack_state deliberately has no unpack counterpart. It answers
kPulledEveryVerb, the one publisher every arm has.
- --check no longer rubber-stamps the 28 rows the RenderState derivation cannot reach.
It reads Tracker.h's Update() for what each bit's shutter READS, resolves those
accessors through MG_State's getters to the members behind them, computes what every
mutator transitively WRITES as a fixed point over MG_State/GLState and MG_Impl/Pipe
(expanding MGP_NOTE_AGGREGATE through MGPipeNoteAggregate's own switch rather than
assuming the hop), and fails a row naming a bit whose shutter its mutator moves on no
path. One-directional by construction: the write analysis over-approximates, so it can
prove absence and not presence, and absence is the under-firing direction.
- the enumerator spelling and the NEW_* spelling are paired BY POSITION out of Tracker.h,
so the enum and kMGPipeDirtyNames drifting apart is itself a gate failure.
- two more self-test negative controls, one per family, both built from the defect that
was really in the file; 7 controls now, all tripping.
- --check prints what it did NOT check: how many rows carry a prose answer, and every row
the derivation declined, so "all mapped" cannot be read as "all verified".
- render_state_publishers() folds the bodies of one name with INTERSECTION, so two
overloads - one BumpVersions, one bare ++m_version - can no longer derive as "both
always fire" and bless an under-firing row.
- the header states what "every path" means: every path that MUTATES, so a redundant-write
guard does not make its publisher conditional, while a publisher reached on only some
mutating paths must not be named.
- the kUnpublishedDestroy note said the three kinds were outside the six D13 scopes explicit
destroy to; the tree's reason is sharper and does not depend on reading D13's prose one way
or the other: those three have no per-object handle on the wire at all, so their DirectGLES
twins are still reclaimed by the backend's own registry teardown
- records the brief/tree disagreement in place: D13 says "six kinds" while the Core.cpp line
ranges it cites also cover MarkProgram/MarkShaderForDeletion
- SetCapability named NEW_PIPELINE_STATE, but its ClipDistance0..7 arms write
ClipDistanceEnabledMask (dynamic chunk D7) and deliberately do not BumpVersions, so that
publisher does not fire at all for glEnable(GL_CLIP_DISTANCE0); SetStencilFunc named it
too, while ++m_pipelineStateVersion there is conditional on Func moving, so a ref-only
glStencilFunc does not move it either. Both are now NEW_RENDER_STATE, the answer that
holds on every path
- a row may now carry several publishers joined with '|', which is what lets the 18 setters
that call BumpVersions on every path state both counters, and the patch trio state its own
bit and the two render counters it also moves
- --check no longer validates only row existence and answer vocabulary: it reads
RenderState.cpp, derives per setter which of the two counters moves on EVERY path
(BumpVersions moves both, a bare ++m_version only the first, a setter with both kinds of
path only the first, a delegating setter inherits its callee's) and fails when a row
claims a publisher that under-fires or omits one that always fires
- two new self-test negative controls, one per direction, both built from the defects that
were actually in the file
- MarkProgram/MarkProgramPipeline/MarkShaderForDeletion answered kExplicitDestroy, a
mechanism D13 does not build for them: Espryt 0b's explicit destroy is scoped to six
object kinds that exclude programs, pipelines and shaders. They answer kUnpublishedDestroy
now - a recorded hole rather than a mechanism that does not exist
- MG_Pipe/DirtySurface.def: 73 rows, one per distinct mutator the scanner finds, each answering
"what publishes this". The answer vocabulary is a MGPipeDirty bit name or one of five
non-bit answers, and each of the five is documented in the file's header rather than left to
be inferred: kImmediate, kReverseChannel, kNoBackendRead, kExplicitDestroy and
kPulledEveryVerb. Where a mutator has more than one true answer the row carries the COARSER
one - the one that cannot under-fire.
- gen_pipe_dirty_surface.py --check is the gate and it fails in BOTH directions: an unmapped
mutator renders stale, and a row naming a mutator the scan no longer finds keeps a real hole
looking covered. It also rejects an answer that is neither a documented non-bit answer nor a
bit name read out of Tracker.h's own kMGPipeDirtyNames, so a renamed bit cannot leave a row
silently pointing at nothing.
- --self-test runs three canned negative controls - a withheld mutator, a stale row, a bad
answer - and each must trip; trips == 0 is itself an error, the shape
check_include_closure.py and gen_pipe.py --self-test already use. ROADMAP.md's rule is that
every gate must be able to go red for the reason it exists.
- --summary keeps working unchanged, because the CI file that still calls it belongs to
another package until it lands.
- The human report prints the mapped answer where it printed UNMAPPED.
- FillPoints.def: the verdict on the eight statically over-approximated rows, recorded per
group in the def's own comment. All eight are KEPT and the reason is the same in all three
groups - each row names a concrete backend path (the depth/stencil read emulation's paused
capture, VkClearManager::PreCompensateSrgbClearColor's GL_FRAMEBUFFER_SRGB read, the shader
blit's viewport / provoking vertex / binding-point reads), and the only evidence that could
retire one is dynamic. A corpus that never reaches a path proves nothing about it, and a row
dropped on that basis turns a rare path into Fatal{UnmigratedPipeInput} in a shipped build.
The contract's new FramebufferSrgb storage in fact makes one of the eight MORE load-bearing
than it was, not less: it used to read a compile-time constant.
- MGP_NOTE_AGGREGATE(Aggregate) next to MGP_NOTE_MUTATION in MG_Pipe/PipeMutation.h, ((void)0)
in the pull build for the same reason and with the same shape. It answers a DIFFERENT
question from MGP_NOTE_MUTATION - "did any object of this class move since the tracker last
looked", not "did a backend move a frontend value inside its own verb" - which is why it is a
second macro rather than an overload.
- The counters are members of the owning MG_State container (VertexArrayState,
FramebufferState, TextureState x2, BufferState) and are reached through a push-only GLContext
facade, because the bump points sit on OBJECTS and an object has no back-pointer to the state
that owns it. That is the free-function form P2 brief D4 allows, and it costs a global load on
a path that has just written object state.
- A SIXTH aggregate, VertexAttribDefault on GLContext, which D4 does not list. Its bit
(NEW_VERTEX_ATTRIB_DEFAULTS) is specified there with a ContentHash over all 32
CurrentVertexAttributeValues, and hashing 768 bytes on every draw does not fit inside the T1
ceiling the same brief pins. The hash still decides whether to EMIT (D11's set-hash
suppressor); the generation decides whether to hash at all.
- 30 bump points: 3 VertexArrayObject config-version sites, 3 FramebufferObject object-version
sites (one of them inside MOBILEGL_DEFINE_FRAMEBUFFER_DEFAULT_SETTER, so the statement
carries its own line continuation), 5 texture content-version sites, 12 texture
params-version sites, SamplerObject::BumpVersion as the sampler choke point, 7 BufferObject
change-serial sites and the 3 glVertexAttrib* defaults.
- Every counter is deliberately COARSER than the state it guards: over-firing costs one extra
push, under-firing renders stale, and under-firing is the direction ARCHITECTURE.md 13.2
names as the dangerous one and the P1 verify comparator cannot see for object-class state.
- TrackerTest: each bump point moves ITS aggregate and no other, plus a null-context note.
- G1: the pull build is 0 added / 0 removed / 0 renamed and the four resized symbols are the
contract commit's own, unchanged by this commit.
- A mode change on its own, so it does not ride inside a code commit. The other three of the
four files created with 0755 were already corrected; this is the last one.
- Coverage.def's emitted list named GetPixelStoreParameters, but the field is
PipeInputs::m_pixelStore[2] - pack AND unpack - and set_pixel_pack_state carries the pack half
only, deliberately and permanently. An emitted row is a licence for the residual fill loop to
skip the field, so the moment the render-state bitmask has its bit set the unpack half would
be written by nothing while its poison stamp said it was published, invisible to the poison
and to the verify comparator alike. The row is gone and the reason is in the file; the pack
half is simply written twice until the field is split.
- kMGPipeRenderStateChunkTableVersion was a promise nobody enforced: a boundary could move,
the two byte-count assertions be updated, and every persisted key stay valid. The hash is now
seeded with the version XOR a compile-time checksum of the boundary table, so a moved boundary
invalidates the keys whether or not anyone remembered - and without a static_assert on the
boundaries, which would turn G7's negative control into a build break instead of a red test.
- MGPipeApplierState gains ScatteredChunkBits: the global chunk bits this applier has itself
scattered into PipeInputs::m_renderState, set by bind_render_state (the whole pipeline half)
and set_dynamic_state (the chunks it names), cleared by MGPipeApplierReset. set_patch_state's
own write is deliberately NOT in it - that is the other carrier, and a wire comparing against
bytes it had just written would be a tautology.
- The residual trip wire now compares capability i only once every chunk that capability's
answer is read out of is in the ledger. Both of the previous form's contracts were undeclared
and one of them was wrong: with the render-state subsystem off (MOBILEGL_PIPE_PUSH=0x10 is a
legal per-subsystem A/B, D14) the working block is the per-verb fill loop's, published per
verb CLASS, and FillPoints.def does not publish GetRenderStateParameters at kDispatch or
kTextureOp - so at a dispatch after a draw the block held the draw's bytes and a correct
context could abort. With the ledger empty the wire now says nothing there, and with the
subsystem on the applier is the block's only writer and its bytes are current at every class.
- The per-capability grain is not decoration: a bind alone owns the pipeline half, and the
eight ClipDistances are answered from ClipDistanceEnabledMask in dynamic chunk D7, so between
a bind and the first set_dynamic_state exactly those eight are unanswerable. The source
chunks come from the same MGP_PLAIN_CAPABILITY_LIST and the same boundary table
DeriveCapability reads, so the two cannot drift.
- The patch-carrier wire arms the same way, which replaces its "some CSO is bound" condition -
a process-global that stayed set from the first bind onward - and covers the verb-class
contract as well as the ordering one.
- Both wires now run in the shipped push build too, counting and logging where a poison or
verify build aborts (one MGP_TRIP_WIRE_REPORT/TAG pair, so only the fatal arm writes the
"Fatal{...}" marker G4 greps for). A wire compiled out of every build a device runs is not a
wire, and the counters are what let a unit case see it fire in every build.
- create_render_state no longer leaves a dead BaseCso to MOBILEGL_ASSERT, which is inert at
INFO - the level every gate and every shipped build uses. A recycled slot's record still
holds the previous occupant's 396 bytes, so inheriting nothing and scattering the delta on
top handed out a record that was half one CSO and half another. Both arms now start from a
defined base and report. The same for a brand-new CSO that does not name every chunk.
- set_vertex_attrib_defaults walks the mask's 32 bits rather than the slot array, consumes a
tail entry for every named location so a named-but-unstorable attribute cannot desynchronise
the rest, and reports all three consistency faults in every build.
- MGPipeDeriveRenderStateFieldsForChunks: the applier no longer recomputes all 29
fields on every scatter. The four wide walks - the 8-wide blend/colour-mask loop,
the 16-wide viewport loop, the 16-wide depth-range loop and the 35-arm capability
switch - are guarded by the chunks whose bytes they read, so a per-frame glViewport
(the D8 case that sends dynamic chunk D0 alone) pays for one 16-entry copy instead
of ~170 stores and 35 switch dispatches. That cost sat on the per-draw path and the
gate it threatened is G11's pinned ns/draw.
- Every guard is MGPipeRenderStateChunkBitsCovering(offsetof(member), sizeof(member))
over the members the guarded block reads, computed from the boundary table: there
is no second, hand-maintained member-to-chunk mapping to go stale when a boundary
moves. The scalar copies stay unguarded on purpose - they cannot go stale, which
keeps the risk of the scoping confined to four guards.
- MGP_PLAIN_CAPABILITY_LIST is written once and used twice, for DeriveCapability's
switch arms and for the capability guard's chunk set, so the two cannot drift.
- set_patch_state now asserts under poison/verify that the trio agrees with what
pipeline chunk P0 delivered, which D6 and D10 both ask for and which was missing:
a stale set_patch_state silently clobbered the CSO-delivered levels. Compared
bitwise, because a NaN outer level is legal and must equal itself; armed only once
a CSO has been bound, which states the ordering contract rather than assuming it.
- set_residual_value_state's trip wire compares the carried bits against the WORKING
BLOCK instead of PipeInputs::m_capability. Only the derivation writes m_capability,
so a residual block emitted before the first bind of a context - what "once per
context" means - compared against all-false storage while Dither and Multisample
default to true, and aborted under poison. The check is unchanged in strength and
now has no ordering contract at all.
- PipeApply.h no longer implies the verify comparator is this package's oracle: it
arms off MG_Config::Features.PipeVerify, which a unit-test process never sets, so
the unit oracle is named for what it is and the comparator is credited to the
retrace and integration-verify lanes.
- 29 of the 47 kDraw PipeInputs fields are pure functions of RenderStateParameters. Once
bind_render_state and set_dynamic_state have assembled the working block - which IS
PipeInputs::m_renderState - copying those fields out of GLContext a second time is the
per-verb pull P2 exists to remove. The applier now derives them after any scatter.
- Each line is a transcription of the RenderState getter of the same name; GLContext's
accessors are one-line forwards to those, so the derivation and the pull path answer the
same question from the same bytes. Two are not field copies and are transcribed exactly:
GetViewport (viewport 0 rounded with std::lround, because glGetIntegerv on float state
rounds to nearest and truncating a 63.5-wide viewport would hand the backends a rectangle
one pixel short), and IsCapabilityEnabled (the 35-way switch, including Blend ->
BlendStates[0].Enabled, ScissorTest -> ScissorTestEnabledMask & 1 and the ClipDistance
run). The indexed twin is the same for Blend[8] and ScissorTest[16].
- IsCapabilityEnabled could not have been written before the contract commit: DepthClamp,
FramebufferSrgb and TextureCubeMapSeamless fell to `default: return false`, so three of
the 35 answers were a compile-time constant rather than state.
- This departs from P1 brief D4's "no derivation logic is re-implemented in PipeInputs",
deliberately: the alternative is to keep pulling those 29 fields per verb. The guard is
the oracle P1 built - MOBILEGL_PIPE_VERIFY's compare-at-read re-reads every one of them
from the live context at EVERY backend read and compares field-wise, so a transcription
error is caught on the first draw that reads it, across 79 retraces and the
integration-verify entries.
- The body sits in MGPipeApplyAccess, the struct PipeInputs already names as its friend, so
no new friend and no new accessor per field. Pull build untouched: PipeApply.cpp compiles
only under MOBILEGL_PIPE_PUSH.
- FramebufferSrgb, DepthClamp and TextureCubeMapSeamless get real storage. All three fell
to SetCapability's "not supported currently" arm and IsCapabilityEnabled's default:
glEnable was swallowed and glIsEnabled lied, so DirectGLES' sRGB block and the
DirectVulkan read points consumed a constant. The three Bools land in the three
alignment bytes at [581, 584) between ColorMasks and ClearColor, so
sizeof(RenderStateParameters) stays 1168 and NO existing offset moves - Espryt's
kBlendSpanBegin/kBlendSpanEnd (312/536) and the whole chunk table depend on that.
- MGPipeRenderStateSpans.{h,cpp}: the pipeline/dynamic split, written in exactly one place.
The rule is the only rule - a byte is pipeline state iff a public RenderState setter that
calls BumpVersions() writes it - which makes G7's "the subset hash moves iff
m_pipelineStateVersion moves" true by construction. 16 boundaries, all offsetof or
sizeof, alternating dynamic/pipeline: 8 dynamic chunks / 772 bytes and 7 pipeline chunks
/ 396 bytes, partitioning [0, 1168) exactly, asserted at compile time.
MGPipeComputePipelineSubsetHash is XXH64 over the seven pipeline chunks, seeded with a
table version so a chunk-table change invalidates every persisted key.
- The pipeline subset is now a strict SUPERSET of the 24 members ComputePipelineStateHash
hashed: 44 members, adding sample coverage, the front face, the provoking vertex, the
scissor-test mask, the back polygon mode, eleven capability bools the hash never read and
the three above. Demoting those setters to ++m_version instead would have changed
MG_State semantics in the PULL build for the push path's sake. The hash runs only when
m_pipelineStateVersion moves, which is exactly when Magma re-hashed before.
- PipeApply.{h,cpp}: the in-process applier, the server half of the P2 calls. The server's
working RenderStateParameters IS PipeInputs::m_renderState, which is why DirectGLES'
SyncRenderState is not one line changed and why the verify comparator stops being a
tautology. Per-context CSO store indexed by slot, gen-validated; the residual block's
capability bits are compared against the assembled block, so a capability a later call
takes over and forgets to carry is Fatal{PipeResidualDiverged}.
MGPipeDeriveRenderStateFields is a declared STUB - its 29 derivations are commit c1.
- SlotAllocator.{h,cpp}: the client's per-kind {slot, gen} allocator, free list plus
high-water, first allocatable slot 1, gen bumping only on slot REUSE, a debug assert on
gen wrap, the composite ShaderCso band held back, and a lifetimeId -> slot map per kind so
a GL name never enters a key. In the contract because both Track H slices need it.
- ResidualValueBlock 1248 -> 8 bytes, one Uint64 of capability bits.
RenderStateParameters retired to create/bind_render_state and set_dynamic_state, Pack to
set_pixel_pack_state, the patch quintet to set_patch_state. gen_pipe.py now emits the
member-by-member offsetof assertions the ratchet comment always promised.
- gen_pipe.py: PIPELINE_STATE_MEMBERS grows to the 44-member set in declaration order and
PipeSpanTable.inc's "deliberately absent" block records the answers instead of the
questions; Coverage.def gains MGP_COVERAGE_EMITTED_LIST (34 rows) and PipeFilled.inc
gains kMGPipeFieldEmittedBy[], which is what lets the residual fill loop skip a field a
P2 call now supplies. One more --self-test negative control covers the new list.
- MOBILEGL_PIPE_PUSH becomes a per-subsystem bitmask with named bits (0..6 migrated at P2,
bit 63 the CSO-content-addressing negative control), defaulting to 0x7f in a push build
and staying 0 in a pull build. New CMake option MOBILEGL_PIPE_LEGACY_MEMOS, ON, forced ON
when MOBILEGL_PIPE_PUSH=OFF where it is the only arm. New Features.PipeHandleAbaControl
under MOBILEGL_PIPE_PUSH, negative control C for HandleRecycleScenario.
- PipeStats gains CallClass::{RenderStateCsoMints, RenderStateCsoBinds} (csom / csob on the
summary line), and they are PUSH-ONLY: growing the enum in the pull build would resize
the counter arrays, the name table and FormatWindowLine for two counters that could never
leave zero, and G1 admits no such resize.
- Four MG_Test/Pipe stubs plus their CMake registration, so the packages that own their
contents never touch MG_Test/Pipe/CMakeLists.txt.
G1, pull build, symbol_report --threshold 0: 0 added, 0 removed, 0 renamed, 4 resized, and
every resize is attributed:
RenderState::RenderState() 1700 -> 1848 (+148) the three {}
RenderState::SetCapability(CapabilityInput,bool) 850 -> 927 (+77) three switch arms
RenderState::IsCapabilityEnabled(CapabilityInput) 239 -> 268 (+29) three switch arms
_GLOBAL__sub_I_DirectGLES.cpp 1340 -> 1331 (-9) the static
initialiser of DirectGLES.cpp's `static RenderStateParameters
g_syncedRenderStateParameters` re-scheduling around the three new default-initialised
members. A shrink, and the only unforeseen entry; it is a direct consequence of the
struct gaining members and touches no interface.
- The verify lane aborted eight integration entries and two retrace cases with
Fatal{PipeVerifyDiffer, "GetSamplingResolutionGeneration@DrawArrays",
where=read}, always one line after "ResolveSamplerDescriptor: using fallback
texture for unbound sampler". The backends write into frontend objects during
their own verb - Magma synthesises a fallback texture for an unbound sampler
and gives it a shape, materialises a queued clear, overrides a unit's sampler
filter - and every one of those writes moves a counter MGP_FILL already
copied, so the pushed block stops equalling the live context for the rest of
the verb. That is a real divergence, not a harness artefact: the pull build
reads the moved value and the push build reads the boundary one.
- Takes the findings' preferred option, push on mutation, over the volatile-in-
verb class: it keeps the comparator's invariant ("the pushed block equals the
live context at every read") literally true, keeps push semantics equal to
pull, and is the shape P2's tracker needs. The fallback would have had to skip
compare-at-read for the field, which is the one comparator arm that is real in
P1 - it would have blinded the gate on the very field that found the bug.
- MG_Pipe/PipeMutation.h declares MGP_NOTE_MUTATION(Field), a no-op that
includes nothing in the pull build; MG_Impl/Pipe/PipeFill.cpp defines the
notice next to the filler it shares CopyField with. The notice refreshes one
field's value when a context is live, a verb has been filled, and the field is
in that verb class's may-read mask; it never touches the poison stamp, so a
stamp MOBILEGL_PIPE_POISON_OMIT withheld stays withheld and a field the verb
never filled stays Fatal{UnmigratedPipeInput} rather than being healed.
- The enumeration behind the three hook sites: of the ~40 backend->frontend
write sites, only the texture family reaches a pushed value. Every path
through them funnels into TextureState::BumpSamplingResolutionGeneration
(SamplerObject::BumpVersion for the sampler setters,
TextureObjectBase::BumpShapeVersion for AllocateStorage / SetInternalFormat /
TruncateMipmapLevels / SetSamples / SetFixedSampleLocations),
BumpTextureBindGeneration (a default texture becoming defined, delete-unbind,
a unit's sampler object changing) or NoteUnitTouched (which also moves the
touched-unit high-water mark), so the notice sits on the counters rather than
on each writer and covers the whole family including writers added later.
The buffer, program and VAO writes reach no pushed field: their objects are
read back through O-class live references, not copied values.
- The verify retrace aborted four cases with Fatal{UnmigratedPipeInput,
"IsCapabilityEnabled@GenerateMipmap"} (x3) and "@DispatchCompute" (x1):
Magma materialises a texture's queued clear inside both verbs
(GenerateMipmap -> MaterializePendingClearForTexture,
DispatchCompute -> PrepareStorageImageTextures -> the same), and the clear
pre-compensates its colour against GL_FRAMEBUFFER_SRGB in
VkClearManager::PreCompensateSrgbClearColor. Neither class named the field.
- Audited every class the same way rather than stopping at those two rows. Two
more helper-program draws sit inside verbs whose class did not name what they
read: GenerateMipmap takes GenerateDepthMipmapWithShader for a depth texture
and BlitFramebuffer takes TryBlitToDefaultFramebufferWithShader for the
default framebuffer. Both bind their helper's descriptors through
BindProgramUniformBuffers, whose sampler resolver reads the draw framebuffer
for its feedback-loop check and whose buffer-block resolvers read the frontend
binding points; the blit additionally sets the dynamic viewport through
ApplyGLViewportState -> ComputeGLViewport and picks its pipeline's provoking
vertex through GetOrCreateBlitPipeline -> SelectProvokingVertexMode.
- kTextureOp gains IsCapabilityEnabled, GetFramebufferBindingSlot and
GetBufferBindingPoint; kDispatch gains IsCapabilityEnabled; kBlitOrCopy gains
GetViewportIndexed, GetDepthRangeIndexed, GetProvokingVertexMode and
GetBufferBindingPoint. Every row carries the path it was derived from.
- Also unfolds the kReadback transform-feedback rows 9087f133 landed on one
1200-column line back into the file's one-row-per-line shape; no row changes.
- the depth/stencil read emulation opens a ScopedEmulationDrawState that pauses an active
capture around its own draw, so ReadPixels/GetTexImage read IsTransformFeedbackActive and
IsTransformFeedbackPaused; without the two kReadback rows every emulated readback aborts
with Fatal{UnmigratedPipeInput, "IsTransformFeedbackActive@ReadPixels"} once the Espryt
sites are converted
- MGPipeInputFieldIsFresh (gen_pipe.py gen_filled, emitted into PipeFilled.inc) compared FilledGen == CurrentVerbSerial for a non-sticky field; before the first MGPipeFillForVerb both are 0, so 55 of the 63 fields read as fresh and served their default-constructed storage silently, with no log line and no abort. Only the four raw-pointer O-class accessors tripped, through their null-base checks.
- D6 says the serial starts at 1 so that FilledGen == 0 means never filled, and names the window "<Field>@<none>"; the predicate now refuses a stamp of 0 before consulting the sticky branch or the serial, so the window is the poison's case as documented. This is the window E's risk table expects the verify lane to find (an init-time read, the first link, anything reached from eglMakeCurrent).
- Reproduced with the reviewer's pre-fill program (a live context with line width 7, no fill, gPipeInputs.GetLineWidth()): stored=0, rc=0 before; rc=134 with exactly Fatal{UnmigratedPipeInput, "GetLineWidth@<none>"} after, with and without Features.PipeVerify set first.
- The compare-at-read hook arms at the first fill and cannot see this window either; a static_assert next to it pins that a verify build always carries the poison, which is what covers the reads before arming.
- PipeFields.def gains MGP_FIELDS_RenderStateParameters (65 members), PixelStoreParameters (8), PerBufferBlendState (7), StencilFaceState (7), DynamicBackendParameters (85) and MGHostSpan (Ptr, Seg, Size, Offset), all appended to MGP_VERIFY_PAYLOAD_LIST: kMGPipeVerifiedPayloadCount 63 -> 69, and ResidualValueBlock / MGPPixelPackState / MGPCaps are now compared field by field all the way down.
- gen_verify: MEMCMP_FALLBACK_TYPES is empty and the generic MGPipeFieldEqual's last branch is static_assert(sizeof(T) == 0) - a struct without a field list is a compile error, not a padding false positive; Array<T, N> gets an element-wise overload, and a VecBase-derived vector (FloatVec4, IntVec4, BoolVec4...) is detected by a probe and compared bitwise over its data, because VecBase::operator== is IEEE == and a derived-to-base overload would lose resolution to the exact-match generic template.
- check_field_lists_cover_struct_members(): for every payload in MGP_VERIFY_PAYLOAD_LIST, parse `struct <Name> {` out of MGPipeTypes.h / MGPipeValueTypes.h / MGPipeHostSpan.h / BackendObject.h (comments and strings masked, statics, functions, nested types and Pad<n> members excluded) and refuse a member without an F(...) or an F(...) that is not a member; runs in both modes, so it is a pipe-gates gate.
- scan_live_accessors(): every MGB_CTX-> / pGLContext-> read under MG_Backend must have a Coverage.def row (rows nobody reads are printed: today only the dead GetBoundTransformFeedbackName).
- --self-test: six negative controls (struct member without F, F without member, payload without struct, verb missing from FillPoints.def, verb outside GLFunctionsTable, field row naming a non-accessor) that must each trip, plus a positive control; zero trips is itself an error.
- MG_Pipe/PipeInputsSwitch.h is the strangler switch (ARCHITECTURE.md 9.2): MGB_CTX is the
live GLContext in the pull build and &gPipeInputs under MOBILEGL_PIPE_PUSH, so the pull
arm's pGLContext spelling stays outside MG_Backend/ and purity gate C's grep.
- MG_Backend/MGPipe/PipeInputs.h holds one struct with every accessor a backend reads (63:
the 61 Coverage.def rows plus GetBoundTransformFeedbackLifetimeId and
HasOpenTransformFeedbackSpan), each keeping its GLContext name, parameters and return
type so the site conversion is type-neutral; V fields are copied values, O fields are
SharedPtr copies or pointers into the context, the seven F fields forward to the live
context from MG_Impl/Pipe/PipeFill.cpp and are the only sticky ones (Coverage.def's
MGP_COVERAGE_STICKY_LIST argues each: argument-keyed lookups and reverse-channel writes,
never a version or generation counter).
- MG_Pipe/FillPoints.def is the verb table: one row per GLFunctionsTable function pointer in
declaration order (69), nine classes and the may-read field rows; gen_pipe.py parses the
struct and refuses a row set that is not exactly its member set, then emits
generated/PipeFillPoints.inc (verb enum, class tables, per-class field masks with the
sticky fields OR'ed in). MGP_FILL(Verb) in MG_Impl/Pipe/PipeFill.h is the fill point;
MGPipeFillForVerb only bumps the serial, records the verb and stamps the sticky fields
here - the per-class copies land in the next commit, the fill points in MG_Impl after.
- MOBILEGL_PIPE_POISON is derived once in PipeInputs.h from MOBILEGL_PIPE_PUSH and the DEBUG
level, MOBILEGL_BUILD_DISAGGREGATED or MOBILEGL_PIPE_VERIFY (the tree has no
MOBILEGL_DEBUG); under it every accessor is a read-side freshness check that aborts with
Fatal{UnmigratedPipeInput, "Field@Verb"}.
- CMake: MOBILEGL_PIPE_PUSH and MOBILEGL_PIPE_VERIFY options (VERIFY forces PUSH on), the two
new sources appended only under PUSH, the compile definitions; Config.h/ConfigLoader.cpp
gain PipeVerifyFatal / PipeVerifyCorrupt / PipePoisonOmit under #if MOBILEGL_PIPE_PUSH so
the pull build's FeaturesTable does not change size.
- Pull build proof: symbol_report.py against the 087685d1 baseline reports 0 added / 0
removed / 0 resized / 0 renamed and a .text delta of 0; ctest -N names unchanged; gen_pipe
--check clean; unit tests green in the pull, push and verify builds.
- check_include_closure.py keeps the directory of a two-token -isystem, makes --require-all
fail on a missing required header even when --probe narrowed the run, and removes its
temp dir at exit
- ProgramArtifactsTest follows whichever STL branch the header pinned (#ifdef the size
macro) instead of re-spelling the libstdc++ condition, and loses its stray executable bit
- MGPipeTypes.h's debt comment says what its closure still reaches (TextureEnum.h via
BackendObject.h), which is why gate A asserts MGPipeValueTypes.h and not this header
- ROADMAP P0.5 / ARCHITECTURE.md value-header manifest: MGPipeTypes.h embedded
RenderStateParameters and PixelStoreParameters through RenderState.h, which drags
FramebufferObject.h and the whole texture/renderbuffer/sampler chain into MG_Pipe;
purity gate A (no MG_State/MG_Impl/MG_Backend/MG_Remote in the closure) could not
be armed for anything in MG_Pipe while that include existed.
- MGPipeValueTypes.h is a verbatim cut, comments included: the eleven RenderState.h
enums (all of them - a split would be the maintenance trap), PixelStoreParameters,
PerBufferBlendState, StencilFaceState, RenderStateParameters (member order
untouched: DirectGLES' offsetof spans and PipeSpanTable.inc name the members),
the six SamplerObject.h enums and SamplerParameters (BorderColorForm stays Uint8,
it sets the tail padding), and VertexAttribute / VertexBufferBindingPoint /
VertexAttributeVersion, which keep namespace MobileGL::MG_State::GLState with a
forward-declared BufferObject so no mangled name changes.
- MAX_DRAW_BUFFERS becomes inline constexpr kMGMaxDrawBuffers in namespace MobileGL
and FramebufferObject::MAX_DRAW_BUFFERS is defined from it, so the eighty existing
spellings keep working and the two cannot drift. No other constant is added.
- The four MG_State headers become forwarders (include the value header, keep their
class definitions); RenderState.cpp gains a direct FramebufferObject.h include
because it spells FramebufferObject::MAX_DRAW_BUFFERS and only ever got that
header transitively. No other TU lost a transitive include: the full build
(Release, clang, tests + integration tests) passed without touching anything
under MG_Backend, MG_Impl or MG_Util.
- DynamicBackendParameters does NOT move (SizeT members and TextureTarget-taking
member functions make that a type change, not a move); MGPipeTypes.h keeps its
BackendObject.h include and the debt comment now says so, which is why gate A
asserts MGPipeValueTypes.h rather than MGPipeTypes.h.
- New trip wires in the header: trivially-copyable + exact sizeof for
PixelStoreParameters/PerBufferBlendState/StencilFaceState (28), SamplerParameters
(100), VertexAttributeVersion (6), RenderStateParameters (1168, standard layout,
BlendStates before LogicOp, BlendStates sized by kMGMaxDrawBuffers). Their runtime
twins ValueTypeLayoutsArePinned and the carrier check
ResidualBlockIsExactlyItsTwoValueStructsPlusPatchTail (Pack at 1168,
CapabilityBits at 1200) are added to PipeCatalogueTest without a new include.
- gen_pipe.py's "field lists of their own in P0.5" comment now says P1 (the
comparator needs std::array<struct> support first); PipeVerify.inc regenerated.
- Verified: ctest -L unit 1460/1460 and -L integration-gpu green; ctest -N names
a superset of feat/disaggregated@6672778b (two added, none lost); one definition
per moved type; the -H closure of the new header contains no MG_State/MG_Backend/
MG_Impl/MG_Remote header and the header compiles alone; nm --defined-only -S
against the base libMobileGL.so: 0 added / 0 removed / 0 resized, .text
byte-identical, the only differing bytes are the build-id and two stamp strings.
- S-1 PipeStats::OnPresent read each frame accumulator and then store(0)'d it; a Bump from a staging thread landing in between was lost from the Tracy plot and from every frame. Each accumulator is now exchange(0, relaxed) and the exchanged value is what is plotted, so every add lands in exactly one frame.
- T-3 FdPassing without MSG_CMSG_CLOEXEC (macOS, BSD) handed back descriptors that survived exec; every received fd now gets FD_CLOEXEC by hand under !MSG_CMSG_CLOEXEC. MSG_NOSIGNAL is defined to 0 where the platform lacks it (FdPassing.cpp, Doorbell.cpp) and SO_NOSIGPIPE is set on the socketpair and on a SocketDoorbell's descriptor where it exists, so a write to a hung-up peer is EPIPE rather than a fatal signal.
- T-4 the missing-flatbuffers fallback wrote OFF into the cache with FORCE, so a plain re-configure after `git submodule update` stayed OFF silently. It is a normal-variable set now, shadowing the cache for that configure only; verified by hiding flatbuffers.h, configuring with ON (warning, transport off, cache still ON) and re-configuring plainly with the header back (transport ON).
- P-2 three LIVE GLFunctionsTable entries had no carrier: GetGpuTimestampNs (glGetInteger64v(GL_TIMESTAMP), a synchronous server answer), QueryCounterTimestamp (glQueryCounter, a one-shot stamp, not a begin/end pair) and WaitSync (the GPU-side wait FenceWait's client wait does not express). QueryTimestamp (MGPTimestampRequest, kCtxQuery, kReplySlot), QueryCounter (MGPQueryDesc with Kind = GL_TIMESTAMP, kCtxQuery) and FenceWaitServer (MGPFenceWait, kScreen) are APPENDED at the end of PipeCalls.def because the opcode is the position: SetSwapInterval stays 68, the three take 69-71, and PipeCatalogue.LateArrivalsAreAppendedWithoutRenumbering pins that. Header counts 71 (screen 11, query 8); the seven generators regenerated.
- P-3 MGPBufferRange inlined a 32-byte MGHostSpan into every range of every class - dead space on every SSBO, atomic-counter and XFB range, and D-B8 says not to freeze the named-UBO payload before the stage-ubo-named numbers exist. The range is 24 bytes now; the host spans are an optional second var-tail behind the ranges, announced by MGPShaderBuffers::HostSpanCount (0 or Count), with set_shader_buffers keeping its kVarTail|kHostSpan flags. PipeCatalogue.BufferRangeCarriesNoInlineHostSpan pins the sizes, the flags and the comparator's view of the count.
- P-4 QueryEnvUint64 parsed with base 0 (a leading zero meant octal: MOBILEGL_PIPE_PUSH=010 read as 8) and accepted -1 as every bit set; it is decimal or explicit 0x now and a '-' anywhere is refused with the warning (smoke through the integration binary: -1 and 12abc warn, 010 and 0x10 parse). The CI stdio gate's alternation now also catches fprintf(stdout, puts( and std::cout/cerr; it is green over MG_Backend and MG_State. MGPSubData states how the buffer half expresses [offset, size): UnionBox.X / UnionBox.W with Target == Buffer, Y = Z = 0, H = D = 1, one record bounded at a 2^31-1 offset and 2^32-1 size beyond which the emitter splits (the same rule the ring's half-capacity bound already imposes); MGPipeSetSubDataBufferRange / MGPipeSubDataBufferOffset / Size are the only spelling and PipeCatalogue.SubDataBufferRangeRidesInTheUnionBox pins the encoding and its bounds. gen_pipe.py now refuses, in both modes, a call payload named in PipeCalls.def with no field list in PipeFields.def (the four memcmp-fallback member types are the documented exception); shown by dropping P(MGPSwapInterval), which exits 1 naming the payload.
- The MGPPixelPackState size assertion compared sizeof against itself; it asserts the literal 28 PixelStoreParameters measures.
- Verified: ctest -L unit green in both the default and the split configuration, gen_pipe.py --check clean with the generated files committed, nm --defined-only of the default libMobileGL.so has no MG_Remote symbol, and the full integration-gpu suite passes (the *IsActuallyArmedWhenTheEnvironmentPinsItOn family trips under -j 8 as documented and passes serially).
- P-1: MGPCaps is DynamicBackendParameters by inclusion (plan B section 4.4.1), but that struct carried MaxComputeWorkGroupInvocations and no per-axis GL_MAX_COMPUTE_WORK_GROUP_COUNT / GL_MAX_COMPUTE_WORK_GROUP_SIZE - the six numbers that ARE the backend-owned indexed answers surviving the getter retirement (GL_Getter.cpp and CompileEnv.cpp ask GLFunctionsTable::GetIntegeri_v for exactly these, DirectVulkan answers them from VkPhysicalDeviceLimits), so the interface had a hole where its only genuine indexed carrier should be. DynamicBackendParameters now has MaxComputeWorkGroupCount[3] / MaxComputeWorkGroupSize[3] with the GL 4.3 minimums as the no-backend defaults; DirectGLES fills them from glGetIntegeri_v inside the loader's bracketed probe run (GLESCapabilities carries them, logged with the other limits) and DirectVulkan from maxComputeWorkGroupCount / maxComputeWorkGroupSize through the loader's SaturateToInt like every other limit. Raw driver answers, as the invocations limit is: the frontend floors them at the shared MIN_COMPUTE_WORK_GROUP_* minimums itself.
- The GetIntegeri_v table path is untouched, as is GL_Getter and CompileEnv behaviour: retiring the getter in favour of the caps is P0.5, and this only makes sure the caps have what P0.5 needs.
- PipeCalls.def's footer no longer claims that "only GL_COMPUTE_WORK_GROUP_SIZE is a real backend answer and it lives in MGPCaps": the six limits live in MGPCaps, and GL_COMPUTE_WORK_GROUP_SIZE is a frontend link artifact (ProgramObject::GetComputeLocalSize, what GL_Program.cpp answers from), which AdvertisedLimitsScenario.ComputeLocalSizeComesFromTheLinkedProgram already pins. The MGPCaps size assertion is a composition of sizeof(DynamicBackendParameters) and follows the struct.
- AdvertisedLimitsScenario.ComputeWorkGroupLimitsAreTheCapsBlocksAnswer pins, on both lanes: answerability, the GL 4.3 floors, vector/indexed agreement, INVALID_VALUE past axis 2, and - through the new Harness/BackendCapsPeek translation unit, which is the one place the module looks past the GL API - that max(caps, minimum) equals the live glGetIntegeri_v answer axis by axis. Shown live by halving each backend's caps copy: both lanes fail with "MGPCaps carries 512 but glGetIntegeri_v answers 1024". On Android the module links the shipping .so (hidden visibility), so the peek returns false there and only the GL-visible half runs. ComputeWorkGroupCapabilities.TakesEveryAxisFromTheIndexedQuery in BackendLoaderTest pins the DirectGLES loader half against the fake driver, per axis and above the initialisers.
- Verified: AdvertisedLimitsScenario 20/20 on DirectGLES and DirectVulkan (llvmpipe / lavapipe), BackendLoaderTest green.
- T-1: InProcessChannel::Close rang each CondVarDoorbell once and claimed that unparks a peer mid-frame. It does not. Doorbell::Wait consumes the one ring, re-tests a condition nothing published, finds the bell alive (CondVarDoorbell never overrode Dead(); Doorbell.cpp had no death state at all) and with kWaitForever parks again for good - so Shutdown could never join a server thread sitting in the design's own steady state (spun, set consumerParked, blocked; plan section 8.1 inheriting the earlier plan's 6.2a). CondVarDoorbell now carries an atomic death latch: Kill() sets it under the mutex and notify_all's, Dead() reports it, Park returns false at once on a dead bell (and the wait predicate includes it, so a Kill cannot slip between the test and the wait), and Close kills both bells instead of ringing them. Same shape as SocketDoorbell's EOF latch; Notify stays the ordinary wakeup.
- T-2: RingProducer::Reserve refused only total > capacity, but a record with capacity/2 < total <= capacity is unplaceable at every head offset where neither the space to the wrap boundary nor the space before it holds it - even in an EMPTY ring, because a wrap pad costs spaceToEnd bytes on top of the record. Concretely: head offset 16 of an empty 256-byte ring, a 248-byte record; FreeBytes() says 256, Reserve says nullptr, forever, and a producer waiting for FreeBytes() >= 248 stalls with nothing logged. The bound is now capacity/2, which is exact rather than conservative (worst case 2*total-8 <= capacity-8), exposed as MaxRecordBytes() for the emitter to chunk against; the minimum ring is two headers so the smallest record still fits the bound. Ring.h states Capacity()/2 as the chunking bound and the G3 header comment in gen_pipe.py now states the chunking rule plan section 8.2 asks G3 to define (PipeWire.inc regenerated).
- Tests, each shown red with only the fix site reverted and green with it: InProcessTransportTest.ShutdownUnparksAWaiterWithNoDeadline (bounded join through a shared_ptr-owned waiter: 5 s red instead of a hung job; reverted it hangs and fails at 5051 ms), RingTest.RecordLargerThanHalfTheRingIsRefused (reverted, the 248-byte record is accepted), RingTest.RecordPlaceabilityDoesNotDependOnTheHeadOffset (the offset-0 vs offset-16 negative control), RingTest.HalfCapacityRecordFitsAtEveryHeadOffset (the positive half: the maximal record at all 32 head offsets of a 256-byte ring) and RingTest.RejectsARingTooSmallForTheSmallestRecord.
- Plan B section 4 makes the frontend/backend boundary explicit and gives the backend its
own state machine. This is the P0 deliverable of section 11: the whole catalogue exists
from day one, placeholders included, because the wire opcode is a call's position in
PipeCalls.def and record numbering must never churn.
- MG_Pipe/PipeCalls.def is the single source of truth: 68 unique calls as
X(Name, Payload, Class, Flags). Its header reconciles that number with the plan's
headline counts (section 4.4, appendix A), which double count - "CSO 15" names
bind_sampler_states and set_sampler_views that the "set_* 17" list also names, "screen
14" tabulates the query family that section 4.3 assigns to the context, and the
"transfer 12" row enumerates 11 calls. Each reconciliation is written down next to the
count rather than resolved silently.
- MGPipeHandles.h: the 8-byte {slot, gen} pair, dense per-kind slots, the reserved null
and default-framebuffer handles, and the ShaderCso composite band (sections 4.2, 5.6.3).
The two generations are documented as strictly separate, with the interface rule that no
call may require the client to know MGGen.
- MGPipeTypes.h: every payload of section 4.5 as a flat POD with explicit padding, a
trivial-copyability assertion and an exact sizeof assertion, because the wire records are
memcpy'd and a field silently changing width is a protocol break no test would see.
MGPCaps embeds DynamicBackendParameters by inclusion so a caps field added there needs no
second edit here; its assertion is stated as a composition because that struct still
carries SizeT. ResidualValueBlock is pinned at MGL_RESIDUAL_BLOCK_SIZE 1248, the ratchet
that only ever goes down and reaches static_assert(... == 0) in P13 (section 6.3).
- MGPipeHostSpan.h keeps the one shape that changes with the transport isolated behind one
predictable branch, with the kFromServerIndexMirror sentinel D-B7 needs.
- MGPipeCallbacks.h names the reverse channel as ten callbacks plus the forward terminator
in the context table, replacing 95 poke sites across 17 methods (section 7.1).
- scripts/gen_pipe.py runs G1-G7 off those .def files. G1 asserts each table is EXACTLY its
call count of function pointers; G3 pads every wire record to the stream's 8-byte
granularity and checks size >= sizeof && size <= remaining && size % 8 == 0 before
dispatch, fatally; G4 compares field by field (padding excluded, floats by bits) because
a comparator with false positives is one nobody reads - DirectGLES.cpp says the same
thing about its own memcmp of RenderStateParameters; G5 turns the accessor list into
per-verb poison generations rather than a written-once bitmap, which is the only version
that can see a field left over from the previous draw (section 6.2.2); G6 joins the 477
read points of the vendored backend_read_inventory.md against Coverage.def and reports
0 UNMAPPED (299 to a call, 167 signatures that become handle parameters, 6 reverse
channel, 5 client-resolved); G7 pins the pipeline subset BY MEMBER NAME from what
VulkanRenderer::ComputePipelineStateHash hashes today, computing no offsets in python.
- The generated files are committed so the build never depends on python; CI regenerates
and diffs them.