Commit Graph
42 Commits
Author SHA1 Message Date
swung0x48 7408bad9fd [Feat] (Pipe): apply the framebuffer record per bound target and the texture and renderbuffer resource calls into their own slot-indexed records 2026-09-08 15:07:23 -04:00
swung0x48 e8502a6100 [Fix] (Pipe): key the framebuffer record to the object it names - a fourth Target value for every DSA entry point that hands a framebuffer over by name, and the metadata-only respecify the sticky bind mask needs 2026-09-08 14:50:30 -04:00
swung0x48 17db759891 [Fix] (Pipe): state the four seam encodings the packages were each inventing - the sub-data target packing, the depth-stencil aspect numbers, the surface kind constants and the texture target the surface record grew where its padding was 2026-09-08 13:51:39 -04:00
swung0x48 32033d6993 [Fix] (Pipe): land the birth half of the P4a client API behind a publication latch both halves read, gate every emitter on its family's wired constant, raise the sampler view's death notice and count the shader composite band apart from the ordinary slots 2026-09-08 13:13:21 -04:00
swung0x48 08192d7266 [Feat] (Pipe): land the P4a contract - the resource target enum, the framebuffer target byte, the texture params' builtin sampler, the sampler-parameter field table, four subsystem bits, seven dirty arms and the program archive codec 2026-09-08 12:18:29 -04:00
Swung0x48 d54ec57a5d [Fix] (MG_Impl, MG_Pipe): outlive the exit handlers - a frontend object destroyed by __run_exit_handlers reached the slot allocator, the resource tracker and the applier after their own destructors had run 2026-09-08 07:33:16 -04:00
swung0x48 707bced438 [Fix] (MG_Impl, MG_Pipe, MG_IntegrationTest): close the four cross-package rows nobody claimed - symmetric vertex-input pairing asserts, the counter G10 reads, and the buffer ABA's Magma-only evidence 2026-09-08 07:04:19 -04:00
swung0x48 78ff014547 [Fix] (MG_Impl, MG_State, DirectGLES): give the vertex-elements CSO a backend-neutral death path - the client minted every VAO's slot and only Espryt ever freed one, so under Magma each VAO leaked a slot and a 1.3 KB applier record for the life of the process 2026-09-08 06:49:59 -04:00
swung0x48 0c55560510 [Fix] (Pipe, State): look the resource handle up on the content paths instead of minting it, publish the bind mask from the two draw-time emitters, pair the destroy with the create's own latch, and state the record-lifetime rule the applier now holds 2026-09-08 04:47:51 -04:00
swung0x48 b11bb9650a [Feat] (Pipe): widen the dirty-surface scan to MG_State so the MGP_NOTE_MUTATION sites stop being outside the gate 2026-09-08 04:47:51 -04:00
swung0x48 874d1ee77d [Feat] (Pipe, State): mint a {slot, gen} handle for every buffer object and publish its create, respecify, sub-data, flush, readback and destroy as pipe calls 2026-09-08 04:47:51 -04:00
swung0x48 12e6bfcf14 [Fix, Test] (Pipe): a make-current is not a teardown - the applier keeps its object records across one, counts every call refused on a record it does not have, advances the two vertex-input serials instead of restarting them at 0, and bounds the slot it grows a record table on 2026-09-08 03:51:21 -04:00
swung0x48 e6452ce948 [Fix] (Pipe): give both blob-carrying record families one Blob rule, and name the wire views the vertex-elements blob actually carries 2026-09-08 03:51:21 -04:00
swung0x48 56366331dc [Fix] (Pipe): account for DispatchIndirect and Query in Coverage.def's target-by-target split, re-sort the new emitted row, and name the P3a default mask in ConfigLoader's include comment 2026-09-08 03:51:21 -04:00
swung0x48 6eb0e675ad [Feat] (Pipe): apply vertex elements, vertex buffers and the index buffer into the server's own working state 2026-09-08 03:51:21 -04:00
swung0x48 45c8f1a8be [Feat] (Pipe): apply the resource calls into a per-context slot-indexed record and dispatch them to the backend by handle 2026-09-08 03:51:21 -04:00
swung0x48 e01c0ccc53 [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 2026-09-08 02:43:19 -04:00
swung0x48 59191cd296 [Fix] (Pipe, DirtySurface): resolve both sides of the dirty-surface derivation to member+field, follow reference and pointer aliases, and turn every write the analysis cannot place into an UNDECIDED answer instead of a verdict
- 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.
2026-09-07 23:18:09 -04:00
swung0x48 8d0ed5b82c [Fix] (Pipe, DirtySurface): read the writes that go through a member's field and the ones the preprocessor pastes together, and decline the rows the write analysis cannot answer - its "UNDER-FIRING" verdicts were an absence proof it did not have, and one of them put a false answer in the map for a bit P2 already ships
- 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
2026-09-07 23:18:09 -04:00
swung0x48 f15b0fdf4b [Fix] (Pipe): derive the dirty-surface map's object-class and value-class answers too, and correct the two rows that named a shutter their mutator never moves
- 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.
2026-09-07 23:18:09 -04:00
swung0x48 067b186677 [Docs] (Pipe): say what actually fails to publish a program, a pipeline and a shader dying, instead of naming a scope
- 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
2026-09-07 23:18:09 -04:00
swung0x48 c574043c13 [Fix] (Pipe): derive the render-state answers of the dirty-surface map from RenderState.cpp instead of believing them, and correct the two rows that named a publisher which does not always fire
- 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
2026-09-07 23:18:09 -04:00
swung0x48 3302ee82b5 [Feat] (Pipe): map every frontend mutator onto the aggregate generation that publishes it, and make the dirty-surface scanner a gate
- 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.
2026-09-07 23:18:09 -04:00
swung0x48 de532f55a9 [Feat] (State): give the frontend six aggregate generations so a tracker can answer "did any bound texture, buffer, attachment or attribute move" with one Uint64 compare
- 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.
2026-09-07 23:18:09 -04:00
swung0x48 842af23331 [Chore] (Pipe): drop the executable bit from MGPipeRenderStateSpans.cpp
- 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.
2026-09-06 09:51:14 -04:00
swung0x48 d1a7c5f159 [Fix] (Pipe): stop claiming set_pixel_pack_state supplies a field it only half writes, and fold the chunk boundaries into the subset hash's seed
- 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.
2026-09-06 09:51:14 -04:00
swung0x48 bee07c3273 [Fix] (Pipe): arm both applier trip wires off the applier's own scatter ledger instead of off a bound handle, and stop leaving a mixed CSO or a malformed attribute tail to a DEBUG-only assertion
- 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.
2026-09-06 09:51:14 -04:00
swung0x48 a9bb99a46a [Fix] (Pipe): scope the derivation to the chunks a scatter moved, and give the patch trio and the residual block trip wires that do not depend on call order
- 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.
2026-09-06 09:00:04 -04:00
swung0x48 810850b13a [Feat] (Pipe): derive every render-state PipeInputs field from the assembled working block instead of pulling it again from GLContext
- 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.
2026-09-06 07:58:53 -04:00
swung0x48 9c6a8a25d8 [Feat] (Pipe): land the P2 contract - real storage for the three swallowed capabilities, the render-state chunk table and its subset hash, the in-process applier, the slot allocator, the subsystem bitmask and the residual ratchet down to 8
- 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.
2026-09-06 07:45:07 -04:00
swung0x48 6b681c4a63 [Fix] (Pipe, State): refresh a pushed PipeInputs field when the frontend moves it inside a verb
- 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.
2026-09-06 05:32:13 -04:00
swung0x48 80a6b39003 [Fix] (Pipe): fill the capability set on a texture op and a dispatch, and the shader blit's viewport, provoking vertex and buffer bindings
- 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.
2026-09-06 05:31:50 -04:00
swung0x48 9087f13308 [Fix] (Pipe): let a readback fill the transform-feedback state its emulation reads
- 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
2026-09-06 04:41:15 -04:00
swung0x48 30d72c5b4e [Fix] (Pipe): refuse a stamp of 0 as fresh on both branches - a read before the first fill is Fatal{UnmigratedPipeInput, "<Field>@<none>"}, not default storage
- 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.
2026-09-06 02:55:17 -04:00
swung0x48 83b16561c2 [Feat] (Pipe): give the six value structs G4 field lists and assert the lists cover their members - the memcmp fallback is now a compile error, floats in vector types compare bitwise
- 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.
2026-09-06 02:02:07 -04:00
swung0x48 bf86b1ede6 [Feat] (Pipe): land the P1 contract - PipeInputsSwitch.h with MGB_CTX, the 63-field PipeInputs block with type-identical accessors, FillPoints.def and its G5b generator, the MOBILEGL_PIPE_PUSH/VERIFY options and the three verify knobs; pull build unchanged
- 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.
2026-09-06 01:35:45 -04:00
swung0x48 b566bf4db9 [Fix] (Purity, Program, Pipe): close the review minors of the three P0.5 packages
- 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
2026-09-05 23:14:30 -04:00
swung0x48 8566a288f8 [Refactor] (Pipe, State): extract MGPipeValueTypes.h - move the render-state, sampler and vertex value types and their enums out of MG_State/GLState so MG_Pipe no longer reaches RenderState.h; pure move, member order and namespaces unchanged
- 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.
2026-09-05 23:11:00 -04:00
swung0x48 901d48a678 [Fix] (MGPipe, Metrics, Config, CI): exchange the per-frame stats instead of racing a store, carry the three uncarried table entries, drop the inline host span from a buffer range, spell the buffer subdata range, and close the small gate holes
- 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).
2026-09-05 21:19:00 -04:00
swung0x48 e8ee7b1a88 [Feat] (Backend, MGPipe): carry the six per-axis compute limits in DynamicBackendParameters so MGPCaps has every backend-owned indexed answer, and pin them against glGetIntegeri_v on both backends
- 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.
2026-09-05 21:19:00 -04:00
swung0x48 1154f9a00d [Fix] (MG_Remote, Transport): give the inproc doorbell a death state so Shutdown can join a parked waiter, and bound a ring record at half the capacity so a refusal can never look like backpressure
- 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.
2026-09-05 21:19:00 -04:00
swung0x48 9c773182bb [Feat] (MGPipe): land the interface skeleton - the complete call catalogue, the payload PODs and the seven generators
- 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.
2026-09-05 20:16:49 -04:00