mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
b9c137e14648face0f55efd835651fc58bafa583
2729
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b9c137e146 |
[Feat] (Bench, Pipe): run the blend-toggle case in CI, give G7 a negative control, and record the two campaign devices
- DriverBenchStateToggle runs mc_state_toggle as its own ctest entry. The case has been in kBenchCases since P0 and nothing executed it, so nothing would have noticed it rotting - and it is the exact enable/draw/disable/draw shape the microbenchmark P2 owes the GO/NO-GO measures. About 1.2 s inside an existing three-minute job. - scripts/g7_negative_control.sh breaks the pipeline/dynamic split on purpose: it inserts two boundaries so ColorMasks becomes a dynamic chunk of its own, which keeps the partition sorted, non-overlapping and complete - so it still COMPILES - while making glColorMask bump m_pipelineStateVersion without moving the pipeline-subset hash. A non-zero ctest is the pass. - Everything that could make that control lie is refused rather than reported: a missing SetterConsistency test exits 2 instead of reading "no tests matched" as a failure; a tree that is already red or already broken exits 2; a patched table that does not compile exits 2, since a build break would prove the static_asserts work rather than that the test still checks; and the restore is from byte-for-byte copies (never from git, so a dirty tree is given back intact), followed by a rebuild and a re-run that must be green. --verify-patch-only exercises the mechanism where the test does not exist yet and says explicitly that it is not a pass. - Profiles for the two campaign devices, and the guard that stops them being trusted early. Both carry PROFILE_VERIFIED=0 and every device-specific field is TODO_VERIFY_ON_DEVICE rather than a guess: the harness pins through MediaTek nodes and 35d0befa is a Qualcomm part, where `su -c 'echo ... > /proc/ppm/...'` fails with a zero exit and the run would report numbers it believes were pinned. bench.sh and session.sh now refuse an unverified profile unless --allow-unverified-profile is passed, which warns that the run is not comparable with a pinned one. The README records what earns PROFILE_VERIFIED=1. |
||
|
|
e9499d38bd |
[Feat] (Trace): record per-frame thread CPU time beside wall time so a paired A/B can be read as CPU cost
- The metric the disaggregation GO/NO-GO hangs on is per-thread CPU p50/p99, and the tree had no first-party collector for it: no CLOCK_THREAD_CPUTIME_ID, no getrusage, no RUSAGE_THREAD, no /proc/self/task anywhere under MobileGL/, tools/, android-plugin/ or scripts/. - The retrace loop is the cheapest honest place to take it. Retrace runs --singlethread and trace_benchmark states that Begin/OnFrameBoundary/End are only ever reached from that one thread, so that thread's CPU time IS the client-side CPU cost: one extra clock_gettime per frame, no root, no profiler, no debuggable build, no sampling - and no instrumentation committed to a hot path inside the library. - The CPU reading is taken before the wall reading at each boundary, so the syscall lands in the wall delta rather than hiding inside the CPU delta. An inflated wall number is visible; a deflated CPU number would not be. - Report::frameCpuMs is the same length as frameMs or it is EMPTY. A clock that started failing mid-run would otherwise be silently re-indexed and put frame N next to frame N+k, and an empty series and a series of zeroes are different claims about the platform. - SummarizeBenchmark is split into SummarizeSeries and reused verbatim for the CPU series rather than duplicated: same tail window, same even-count median rule, same nearest-rank p95, or the delta between the two series stops meaning anything. - benchmark.json gains meanFrameCpuMs / medianFrameCpuMs / p95FrameCpuMs and the WHOLE frameCpuTimesMs[] array beside frameTimesMs[]; result.json and the completion line gain the three headline numbers. p99 therefore needs no device change - it is a host-side reduction over an artefact that already exists, and run_android_retrace_local.py prints p50/p95/p99 off the same trailing window the device summarised. - Pre-flighted on the desktop CLI, which shares the same core: 2-frame run, cpu series aligned with the wall series and strictly below it (wall 1290.787/20.225 ms, cpu 535.271/8.261 ms). |
||
|
|
f1780b9000 |
[Test] (Pipe): reproduce the handle ABA through public GL and pin the CSO content-addressing switch
- HandleRecycleScenario builds the ABA the Track H re-key has to survive: an object is drawn
for three frames so every per-object memo is armed against it, unbound so its last SharedPtr
drops, deleted, and replaced immediately by one with a byte-identical configuration and
different contents. Three kinds - a vertex array whose buffer is recycled with it, a texture,
a framebuffer - and the readback must come from the replacement.
- The reproducer is asserted, not assumed. TheReproducerRecyclesEveryName pins that the name
allocators hand every deleted name straight back, and a case whose names were not recycled
SKIPS as "inconclusive, not proven" rather than passing - the shape ObjectLifetimeIdTest
already uses.
- Three always-on arms, one ctest lane each, named by the harness marker MGITEST_HANDLE_ARM:
Handles ({slot, gen} only), Legacy (today's lifetimeId + weak_ptr guards) and AbaControl
(MOBILEGL_PIPE_HANDLE_ABA_CONTROL=1, which expects the CORRUPTION so that a reproducer that
stopped reproducing is a red rather than a quieter green). AbaControl is DirectVulkan only:
the knob reverts two DirectVulkan guards and steers nothing on DirectGLES.
- CsoContentAddressingScenario is the G12 control. A Blaze3D blend toggle - enable/draw/
disable/draw x 8 inside one frame - must mint a BOUNDED number of CSOs with content
addressing on and exactly one per bind with bit 63 of MOBILEGL_PIPE_PUSH set, while the
pixels do not move at all. csom == csob is the reading a dead switch cannot produce.
- The counters are read from the library's own "MGPipe stats:" line, because PipeStats is
internal and this module links the shipping library on Android. Each arm therefore gets
MOBILEGL_PIPE_STATS_PERIOD=1 and a private MOBILEGL_LOG_FILE_PATH, the same per-lane rule
the arming lane already follows, and the workload is bracketed by two swaps so the window
covers itself and nothing else.
- Both scenarios skip in the ambient entries, which configure none of the knobs their arms are
about, and both name what is missing when the package they depend on has not landed. What
decides that is the BUILD, not a hand-written guard: CMakeLists looks for
DirectGLES/SlotTables.h, MG_Impl/Pipe/Tracker.cpp and the two markers inside
VertexInputStateFactory.cpp, prints each verdict, and re-evaluates through CONFIGURE_DEPENDS -
so the arms arm themselves when packages B, C and D land.
|
||
|
|
e5c032c89e |
[Fix] (Magma): log the mint's high-water at a level a shipped build keeps
- The live-object high-water mark is the number review v2's MAJOR 1 wants measured on minecraft-1.21.4-in-world and ...-sodium-in-world, and no desktop gate can produce it. It was emitted at MGLOG_D, which is compiled out of every build that ships and of every build P2 measures, so the line existed only in a configuration nobody runs. - MGLOG_I instead, still only on the allocate-a-new-slot branch and still only at powers of two from 1024 up: at most a handful of lines for a whole session, never one on a draw (ROADMAP.md:7). Declared as a narrow deviation from D20's "MGLOG_D for anything non-critical" in the comment beside it. |
||
|
|
a174a06c79 |
[Fix] (Magma): size the {slot, gen} mint by the live working set instead of by a capacity, and give it to the renderer that uses it
- MagmaPipeIdentityTable was a FIXED 2048/8192-entry, 2-way set-associative LRU. Above capacity it evicted LIVE objects, and every memo keyed on the handle died with them: a verbatim transcription of the previous Acquire lost 54% of uses' handles at 2500 live VAOs against 2048 entries, and 20% at 1024 live VAOs once the lifetime ids are sparse (an app that creates and destroys VAOs - the Minecraft chunk shape this exists for). - Two of the three memos it fed had NO capacity before this package: the content-hash memo and the resolved-state memo were unbounded mutable fields on VertexArrayObject. Eviction there turns one ComputeHash per VAO reconfiguration into one per DRAW; once the buffer table thrashes too, the vertex-input content hash becomes a per-draw value that inserts a fresh heap-allocated BackendVertexInputState into an unbounded map on every draw, swept only every 256 frame boundaries. That is a worse leak than the one the fixed table was introduced to avoid. - So the mint grows on demand and reclaims by AGE: a lifetime-id map with a one-entry front memo, a free list, and an OnFrameBoundary sweep on the same cadence and retirement age as the cache entries those slots key. Footprint tracks the live DRAWN working set instead of objects ever created, which is the property MG_Impl/Pipe/SlotAllocator cannot have here (nothing in P2 can call its Free). Re-run of the same workloads: handle churn is 0.0% at 512, 1024, 2048, 2500, 3000, 4000, 8192, 10000 and 16000 live objects, consecutive and sparse ids alike, at 1 and 5 acquisitions per use. - VertexInputStateFactory::m_vaoMemos follows the mint with no capacity of its own, through a chunked table whose entry addresses never move - which is what the fixed table's only real guarantee was, and D12.4's grow-on-demand ask without a relocating Vector. - VulkanRenderer::m_vaoDrawMemoTable deliberately keeps the base ref's 2048 entries and the base ref's older-frameSerial victim rule, and changes only its KEY. It is the one memo of the three that had a capacity before P2, a VaoDrawMemo is ~450 B, and losing one costs one vertex-binding re-resolve. Measured steady-state miss rate against the base ref's address-hashed table: 0.0% vs 6.4% at 512 live VAOs, 0.0% vs 24.0% at 1024, 0.0% vs 60.0% at 2048, 36.2% vs 69.5% at 2500, 63.5% vs 79.1% at 3000; both are ~100% at 4096 (2x capacity), where an LRU on a cyclic pattern cannot win. - The two tables are now a MagmaPipeIdentityTables member of VulkanRenderer, handed to its VertexInputStateFactory, instead of two function-local statics that outlived every context and shared one reclamation clock across two. - A Gen that reaches 2^32-1 retires its slot for good rather than wrapping. MOBILEGL_ASSERT is compiled out of every build P2 runs, and a DEBUG-level build of this tree does not compile at all (MG_Util/Types.h uses MOBILEGL_ASSERT before MGLOG_F is declared - untouched since the base ref, and not this package's file), so the defence has to be on the release path to exist. - The no-CSO pipeline-memo fallback stops using MGLOG_W_ONCE. MOBILEGL_LOG_ONCE_INTERNAL is an unconditional std::atomic_flag::test_and_set - a locked xchg per evaluation, not "one static bool test" - and this site is on the per-draw path in exactly the configuration that reaches it. A plain per-renderer bool replaces it, and the comment now says what the warning's absence does and does not prove (nothing at all while bit 0 is clear). - MOBILEGL_PIPE_LEGACY_MEMOS=0 with kMGPipeSubsystemRenderState clear still runs the pre-handle state hash - there is a correct answer there and bit 0 is not Track H, so it is not fatal - but it is no longer silent: the startup gate names the combination. - The D12.3 static_assert block now names D19's DynamicChunksCoverMagmasDynamicTailKey, whose ctest entry lives in package A's file, so the integrator can see which half is missing. |
||
|
|
46841ac706 |
[Fix] (Magma): assert rather than assume that a handle indexed into a per-slot table is non-null
- MagmaPipeSlotIndex subtracted kMGPipeFirstAllocatableSlot unconditionally, so a null handle would have indexed a per-slot table at ~0u. It is unreachable - both lifetime-id sources start at 1, which is why Acquire's zero guard never fires - but the consequence of being wrong about that is an out-of-range write, not a wrong answer, so it is asserted and the index is pinned to 0 in a release build. |
||
|
|
01179c54d2 |
[Fix] (Magma): make the legacy-memo lever a startup gate for Magma's own bit, bound the {slot, gen} mint, and keep the all-pull arm free of push-only cost
- MOBILEGL_PIPE_LEGACY_MEMOS=0 no longer aborts a draw. D14 spends that lever at STARTUP and only on a Track-H subsystem, so MagmaPipeValidateSubsystemConfiguration runs once from VulkanRenderer::Initialize and checks bit 6 alone: an Espryt-side bitmask cannot kill a Magma run, and bit 0 - which is not Track H and not a memo re-key - is out of the lever's scope entirely. MOBILEGL_PIPE_PUSH=0x60 with the lever off went from 9/9 aborted to 432/432. - A pipeline memo with no render-state CSO bound falls back instead of aborting. delete_render_state clears the binding, so the null handle is reachable on any tree; the fallback is the pre-handle state hash where one is compiled, and the client's own MGPipeComputePipelineSubsetHash over the same 396 pipeline bytes where it is not - which is what makes -DMOBILEGL_PIPE_LEGACY_MEMOS=OFF a runnable configuration (180/432 aborted before, 432/432 now) instead of a build that dies on its first draw. The fallback warns once, so a run that never keys on a CSO handle says so in its log instead of passing silently. - The handles are minted by a fixed-capacity, self-recycling identity table in the backend, not by MG_Impl's client allocator. Nothing in P2 frees a VertexElementsCso or Buffer slot - the frontend has no death notification Magma can hook - so the allocator's live Allocate and dead Free grew one SlotState plus one hash-map node per object ever created, for the life of the process. The table is 2-way set-associative with an LRU victim and a Gen bump on reuse: bounded (32 KB for VAOs, 128 KB for buffers), exactly as ABA-proof, and it takes MG_Backend's only include of MG_Impl back out. - Both per-slot memo tables are now a BIJECTION with that mint rather than a masked direct map, so two live VAOs cannot share an entry and the eviction decision lives once, in the identity table, instead of once per consumer table. The density claim the masked tables rested on was false while slots grew monotonically, and the masked form had also dropped the second candidate and the frame-serial victim choice the address-hashed table used to have. - snap.vaoHandle is stamped only when bit 6 is on. It was guarded by the compile switch alone, so MOBILEGL_PIPE_PUSH=0 - the all-pull control D14 defines as reproducing P1 exactly, and the arm D.4.3's T2 is measured on - paid a mint per new VAO and a compare per draw for a field that arm never reads. - Every re-keyed Track-H site now asks the same MagmaPipeTrackHArmIsHandles helper, including VertexInputStateFactory::ComputeHash, which decided for itself before and could key on the pre-handle identity while its neighbours keyed on the handle. - The pull build's two pipeline-memo sites keep the base ref's text statement for statement: G1 is back to the contract's four resized symbols, 0 added/removed. |
||
|
|
3594f03c4e |
[Refactor] (State, Magma): take the backend's raw pointers out of the frontend VAO - the hash and state memos become the factory's own per-slot fields
- P2 D12.5 (ARCHITECTURE.md 9.5). VertexArrayObject carried three `mutable` memos for the
backend: a content hash, a raw pointer into VertexInputStateFactory's heap-allocated
cache entry plus that cache's eviction epoch, and two aux words. A frontend state object
holding the backend's pointer is what P2 retires - under split the backend is in another
process and its cache entry has no address a client could store.
- The hash and state memos move into a slot-indexed table the FACTORY owns, keyed on the
VAO's {slot, gen} and guarded by exactly the same config version, so nothing is
recomputed more often than it was. Fixed and direct-mapped for the same reason m3's
VaoDrawMemo table is: nothing frees a VertexElementsCso slot in P2, so a grow-on-demand
table would keep one entry per VAO ever created. 2048 x 48 B is 96 KB.
- The AUX memo is deleted rather than moved, as the brief says: its two words already live
in VulkanRenderer::VaoDrawMemo (layoutHash / layoutAuxMasks) and GetBackendAuxMemo has no
live reader anywhere in the tree - the only writer was the line this commit stops
executing.
- The eviction-epoch dance shrinks with them. The PROCESS-WIDE s_evictionEpochSource exists
because the memos live on frontend VAOs and therefore outlive the factory; the handle
arm's table dies with the factory, so a per-instance counter is enough there. The epoch
itself stays - it guards the POINTEE, which is still a cache entry a frame boundary can
erase, and moving the memo does not change that. (The brief reads as if a slot-indexed
table removes the need for an epoch; it removes the need for a process-wide one.)
- The three draw-path readers that asked the VAO "is your content hash already memoized?"
now ask whichever side owns the memo, through a force-inlined wrapper so the PULL build's
two loads stay two loads.
- All three accessors and their storage are kept under MOBILEGL_PIPE_LEGACY_MEMOS rather
than deleted from the file, because that is the arm the pre-handle A/B runs (D14) and
because a pull build forces the option ON, where G1 admits no change at all. Configuring
with -DMOBILEGL_PIPE_LEGACY_MEMOS=OFF is what makes the deletion real, and that build
compiles clean - which is the check that nothing else still reaches for them.
- Verification: pull symbol_report --threshold 0 is 0 added / 0 removed / 0 renamed with
the contract's four resizes and no fifth; ctest -L unit 1489/1489 in both the pull and
the push build; ctest -L integration-gpu -R DirectVulkan 432/432 under the default
bitmask and 432/432 under MOBILEGL_PIPE_PUSH=0. The LEGACY_MEMOS=OFF build compiles but
cannot RUN on this tree, and that is the D14 gate working rather than a defect: no
tracker binds a render-state CSO here, so the handle arm has no key and
Fatal{PipeLegacyMemosDisabled} fires at the first draw instead of the memo quietly
aliasing every render state onto one entry. Re-run it once p2/tracker has landed.
|
||
|
|
43f8b47088 |
[Refactor] (Magma): key the vertex-input cache and the VAO draw memo on {slot, gen} instead of a lifetime id and a heap address
- Track H subsystem 4 (P2 brief D12.4, ARCHITECTURE.md 9.5), behind
kMGPipeSubsystemMagmaVertexInput.
- VertexInputStateFactory::ComputeHash's buffer identity component becomes the buffer's
{slot, gen} - "lifetimeId -> gen mixed into every server-side content hash". Both are
equally ABA-proof (the allocator maps one onto the other and bumps Gen only on slot
REUSE); what changes is that the hash now carries the identity the SERVER will be handed
once buffers travel as handles, instead of a number only the client can mint.
- LookupVaoDrawMemo becomes a direct slot index: the slot IS the index, and the whole
validation is one handle compare. Gone with the re-key are the Fibonacci mix of the VAO's
address, the two-way probe, the frame-serial eviction choice and the (pointer, lifetime
id) pair - slots are dense by construction, so consecutive VAOs land in consecutive
entries and the collision the address hash existed to spread does not arise below the
table size.
- The table stays FIXED at 2048 entries and the slot index wraps, where the brief calls
for a grow-on-demand vector. Reason, and it is a tree fact the brief does not carry:
nothing in P2 frees a VertexElementsCso slot. The frontend death notification is Espryt
0b's e2 and it covers Espryt's six kinds; buffers are the only kind with an OnDestroy
hook today. A grow-on-demand table would therefore hold one ~1 KB VaoDrawMemo per VAO
EVER created, which on a chunk-cycling Minecraft frame is tens of megabytes. Above the
table size this degrades to a direct-mapped cache validated by the full {slot, gen}:
never wrong, only colder, and strictly better than the address hash it replaces.
Revisit when object deletion reaches the client allocator.
- SetupDrawSnapshot's VAO identity collapses to the same handle - one compare instead of
(address, lifetime id) - so the snapshot and the draw memo cannot disagree about whether
the VAO moved. The config version stays: it answers a different question.
- Handle acquisition sits behind a one-entry memo in the renderer. Acquiring is a hash
probe into the allocator's lifetimeId -> slot map and LookupVaoDrawMemo runs per draw, so
without it the arm would have swapped the address hash it deletes for another probe; a
run of draws over one VAO now pays a single Uint64 compare. Magma acquires the handles
itself because the tracker does not emit object-class state in P2 (it emits for dirty
bits 0-4 only); when it does, these become reads of what the client already sent.
- Negative control C (MOBILEGL_PIPE_HANDLE_ABA_CONTROL, brief D18) is implemented here
because the two guards it defeats live here: it makes ComputeHash hash the raw
BufferObject* and makes LookupVaoDrawMemo skip the lifetime-id compare - the exact state
the table was in before the ABA fix. It applies to the PRE-HANDLE arm, which is what
HandleRecycleScenario.AbaControl runs (MOBILEGL_PIPE_PUSH=0), and it is what proves that
scenario's reproducer still reproduces instead of passing for the wrong reason.
- Verification on this tree: ctest -L integration-gpu -R DirectVulkan is 432/432 under the
default bitmask and 432/432 under MOBILEGL_PIPE_PUSH=0, and -L unit is green. Pull build
symbol_report --threshold 0: 0 added / 0 removed / 0 renamed, 4 resized, all four the
contract commit's.
|
||
|
|
c74c4819fb |
[Refactor] (Magma): drive the dynamic tail from the pushed dynamic version and make the chunk table check DynamicTailKey's inventory
- P2 D12.3. ApplyDynamicDrawStateTail keeps reading GetRenderStateParametersVersion, and under MOBILEGL_PIPE_PUSH that accessor is RE-SOURCED: it returns PipeInputs::m_renderStateParametersVersion, which the applier publishes from MGPDynamicState::Version and MGPBindRenderState::Version. The gate now reads what the client pushed rather than what the backend pulled. - The brief expects the same change to stop a PIPELINE-only change invalidating the tail. It does not, and the tree is right against the brief: the applier publishes bind_render_state's Version into the same counter, and it has to - Espryt's SyncRenderState uses that counter as its all-state change detector and G5 forbids touching one line of it, so a bind that rewrote the pipeline half while leaving the counter still would make Espryt skip re-syncing the state it just changed. Getting the finer gate needs a second, dynamic-only version on the wire, which is a CONTRACT change; recorded for the integrator rather than smuggled in here. The second-level DynamicTailKey compare already absorbs a pipeline-only change at the cost of one key build and no vkCmd*, exactly as it did before P2. - The coverage check D12.3 asks for, as static_asserts rather than a unit test: every RenderStateParameters member DynamicTailKey reads is checked against the P2 chunk table (MGPipeRenderStateSpans.h), including the three stencil members PER FACE, since D6 splits StencilFaceState through the middle. The tail's hand-written input inventory and the offsetof-derived chunk table were written for different reasons, so making them check each other is free evidence, and a chunk edit that demoted one of these is a build break here instead of a tail that stops being re-run when its input moves. A ctest entry would have had to live in MG_Test/Pipe/RenderStateSpansTest.cpp, which the ownership table gives to package A; a static_assert in the file that owns the reader is both in-scope and stricter. - ScissorTestEnabledMask is the one input that is NOT dynamic, and the brief says it should be. The tree wins: the split's only rule is "pipeline iff a setter that calls BumpVersions writes it", and SetCapability(ScissorTest) does, so it sits in pipeline chunk P6 with the other capability bools. It is pinned with the assertion INVERTED, so demoting it - which would be a real G7 violation - is also a build break. Reading it in the tail stays harmless because BumpVersions moves both counters together. - Push-only: the whole block is inside MOBILEGL_PIPE_PUSH and the pull build is unchanged (symbol_report --threshold 0: 0 added / 0 removed / 0 renamed, 4 resized, all four the contract commit's). |
||
|
|
96c544514e |
[Refactor] (Magma): key the pipeline memo on the render-state CSO handle and stop recomputing a hash the client already computed
- P2 D12.1. GetOrCreatePipeline's memo compared a VALUE hash of the pipeline-relevant
fixed-function state that Magma recomputed itself. After P2 the CLIENT hashes exactly
those bytes when it mints a content-addressed render-state CSO
(MGPipeComputePipelineSubsetHash over the seven pipeline chunks), so the bound CSO
handle IS that key and ComputePipelineStateHash was doing the boundary's work twice.
The client's pipeline subset is a strict SUPERSET of the 24 members the hash read, so
the handle discriminates at least as finely as the hash it replaces.
- renderPassHash STAYS in the key, and that is load-bearing rather than conservative:
ComputePipelineStateHash was never a pure function of RenderStateParameters - its
signature took colorAttachmentCount and rasterizationSamples, and
ResolveEffectiveSampleMask reads the latter - so those two render-pass facts have to
stay separated by something. entry.renderPassHash already separates them (the pass
hash folds each attachment's sample count and the attachment set), which is why
collapsing the state half onto a handle loses no discrimination.
ResolveEffectiveSampleMask is NOT deleted with the hash: it is a payload computation,
and it keeps reading Multisample / SampleMask / SampleMaskValue out of the working
block.
- Both memo probes are re-keyed, not just the full path's: TrySetupDrawFastPath carries
its own copy of the probe, and a fast path that keyed differently from the full path
would hand back a pipeline the full path would not have matched.
- The arm is chosen at runtime, per D14: kMGPipeSubsystemRenderState in the
MOBILEGL_PIPE_PUSH bitmask AND a non-null bound CSO. The second half is not belt and
braces - a tree whose tracker does not emit create/bind_render_state yet has no handle
to key on, and keying every draw on the null handle would alias every render state onto
one memo entry. Falling into the pre-handle arm with Features.PipeLegacyMemos=0 is
Fatal{PipeLegacyMemosDisabled}, so HandleRecycleScenario.Handles cannot go green by
quietly running the old code.
- ComputePipelineStateHash and its five cached-hash members (m_pipelineStateHash{,Valid,
Version,ColorCount,SampleCount}) survive only under MOBILEGL_PIPE_LEGACY_MEMOS, which
a pull build forces ON: they exist purely to avoid re-hashing, and the handle arm never
hashes. InvalidatePipelineMemo loses them on the same condition.
- New MagmaPipeArms.h holds the two-switch arm selector shared by the P2 Magma re-keys.
- G1, pull build, symbol_report --threshold 0 against ~/w7/p2-before-libMobileGL.so:
0 added, 0 removed, 0 renamed, 4 resized - and all four are the CONTRACT commit's
(RenderState::{RenderState,SetCapability,IsCapabilityEnabled} and
_GLOBAL__sub_I_DirectGLES.cpp). This commit adds none: every edit is inside a
MOBILEGL_PIPE_PUSH arm and the pre-handle statements are left where they stood, which
is why ResolveBoundRenderStateCso is push-only rather than a shared helper - an earlier
shared-helper shape moved 104 bytes of GetOrCreatePipeline around for no behaviour
change and the gate saw it.
|
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
8f66c374aa |
[Fix] (Pipe): re-arm the residual block on any render-state move and republish every vertex-attribute default on a fresh context
- the residual value block armed on NEW_PIPELINE_STATE, so glEnable(GL_CLIP_DISTANCE0)
never re-armed it: SetCapability's ClipDistance0..7 arms are deliberately not
BumpVersions() and those eight are 8 of the 35 CapabilityInputs the block carries, so
the D9/G10 trip wire was disarmed for them for an unbounded window - and invisibly, a
block that is never emitted cannot diverge. It now arms on either render-state counter,
the same answer DirtySurface.def derives for SetCapability, and the comment that
asserted the opposite ("every SET_CAPABILITY arm calls BumpVersions") is corrected.
- the arming moved outside the residual subsystem gate: whether the capability set may
have moved is a fact about the frontend, not about which subsystems this build pushes.
- set_vertex_attrib_defaults published NOTHING across a context change. Tracker::Reset()
sets the staging mirror to the GL defaults and a fresh GLContext holds the same, so the
per-attribute diff was empty on the one walk that must publish a COMPLETE state, while
MGPipeApplierReset() leaves gPipeInputs.m_currentVertexAttribute holding the previous
context's values - which cancelled, two lines later, the InvalidateAll() written for
exactly that case. It now sends all 32 when the tracker is freshly primed, the arm
EmitRenderState already had.
- the fresh-context reset of the CSO cache and the applier moved out of EmitRenderState,
which runs only when bit 0 of MOBILEGL_PIPE_PUSH is set: the per-subsystem A/B D14
invites gave a fresh context a never-reset applier while every suppressor slot was
invalidated.
- MGPipeVertexAttribDefaultsLastHeader() is the observable for both properties of that
call that cannot be read back without a poisoned read of m_currentVertexAttribute.
- the repair case now asserts the invariant (the call named exactly what moved, at most
one repair) instead of repairs == before + 1, which pinned today's applier and would
have gone red the day package A honours MGPAttribValue::ValueClass.
- a static_assert that no MGPipeDirty bit owns kMGPipeSubsystemResidualValues, which is
what makes the residual block's direct subsystem test the one safe exception to
MGPipeSubsystemForDirty, and a note that the NEW_PIPELINE_STATE/NEW_RENDER_STATE
MOBILEGL_ASSERT is a debug/verify alarm over behaviour that is safe in every build.
|
||
|
|
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 |
||
|
|
5d4d91fe7e |
[Fix] (Pipe): carry the class a vertex-attribute default was written through, and stop the applier's lossy write from being observable
- set_vertex_attrib_defaults hard-coded MGPAttribValue::ValueClass to 0 for every attribute and always sent the FLOAT view's four words. A CurrentVertexAttributeValue is one value in three views and GLContext converts numerically between them, so those bytes cannot reproduce the frontend value: glVertexAttrib4f(loc, 1.5f, ..) leaves 1 in intValue and 0x3FC00000 in floatValue, and every glVertexAttribI4i/ui default was wrong too - GLContext now records which view each glVertexAttrib* write filled directly (GetCurrentVertexAttributeClass, push-only) and the payload carries that class and THAT class's own words. It is kept beside the value rather than inside it because CurrentVertexAttributeValue is mirrored into PipeInputs and compared there by a memcmp whose size assertion lives in a file this package does not own - MGPipeFillAttribValue is the flattening, in one named place, so TrackerAttribPayload can pin it: the old defect turns three of its four cases red - the applier (package A's) still memcpys the four words into all three views regardless of ValueClass, so the emitter now CHECKS: it compares the mirror the applier wrote against the frontend's value and, when they differ, copies the field itself and says so once. That closes the window the old code left wrong - a glVertexAttrib* write followed by a verb whose class does not read the field, where the residual fill does not run for it - and it stops repairing by itself the day A's applier honours the class - MGPipeVertexAttribDefaultRepairCount() makes that repair observable to a test without reading storage the fill table forbids that verb to read - the emission gate now goes through MGPipeSubsystemForDirty, the one bit-to-subsystem map, instead of a second copy of it written out by hand at the validate point; five static_asserts tie that map to the field-emitter map it has to agree with - the staging mirror is advanced only by the branch that sent dynamic bytes, with the invariant it used to rely on (BumpVersions moves both counters, RenderState.h) asserted here rather than assumed of another package's file - TrackerShippedEmitter drives MGPipeValidateForVerb itself and reads the real singletons back, so the blend-toggle and viewport shapes are pinned on the shipped emitter and not only on the unit tests' local re-implementation; the fixtures reset the applier, the cache and the tracker together, which is the only consistent state of the three - comments: the derivation probe is a one-field sample, the residual block's trip wire is half a tautology until package A's c1 lands, and the widened counter cannot see a change of exactly 65536 - all three recorded where the code is, not only in a review |
||
|
|
bb781df527 |
[Fix] (Pipe): count the render-state CSO binds the cache has always declared and never incremented
- Counters::Binds was declared, documented as one of the three numbers P13's capacity retune reads, and incremented nowhere: the retune would have read a permanent zero, and the unit tests counted binds in a local of their own - counted in Acquire, which has exactly one caller and is followed by a bind_render_state every time, so the count cannot drift from the emitter forgetting to tick it |
||
|
|
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 |
||
|
|
3ab394e2b8 |
[Test] (Pipe): give the pull build the same ctest names as the push build so a push-only case skips instead of vanishing
- G2 requires the pull and push ctest name sets to be identical, name for name. The two new suites had a single hand-written "SkippedInAPullBuild" placeholder each, which made the pull build 30 names short - a diff G2 exists to catch. - Each file now carries an X-macro list of its push-only suite.name pairs, expanded in the pull branch into cases that GTEST_SKIP. A case added on one side and forgotten on the other is a visible ctest-name diff rather than a test that silently is not there. - Measured with a CORRECTED gate command. The brief's G2/G14 grep is '^\s+Test #', which only matches a four-digit test number: ctest right-aligns the number, so tests 1..999 print as "Test #7:" with more than one space, and on this tree that silently dropped 999 of 2368 names - i.e. the gate as written passes while looking at 58% of the list. The pattern that works is '^ +Test +#[0-9]+: '. Both gates are green under it: 2396 names in the pull build and 2396 in the push build with a zero-line diff, and zero of the 2363 baseline names gone. |
||
|
|
43bf97cc87 |
[Test] (Pipe): pin the tracker's shutters and the CSO cache's content addressing, including the collision the memcmp exists to stop
- TrackerWalk drives the tracker and the cache DIRECTLY rather than through MGPipeValidateForVerb: the validate point reaches the library's one process-wide tracker, and a unit test that asserts on a shared singleton fails the moment ctest runs the suite in parallel. The three lines of emission logic it reproduces are the same three lines. - BlendToggleReusesTwoCsos is the Blaze3D shape the whole "push at validate, not in the setter" decision was made for: 32 enable/draw/disable/draw pairs mint exactly TWO CSOs, bind 64 times and hit 62. A per-setter design would show up here as 64 mints. - ViewportDoesNotMintACso is the regression RenderState.h records: 16 glViewports mint nothing, never move the pipeline version, and each sends exactly chunk D0 - not the other seven. - WrapAroundRePushesButNeverMisses drives m_version past 65535 and asserts every one of 70000 changes fired. The alternating value deliberately never touches the default: a setter that early-outs would otherwise make the first iteration a false miss and hide a real one. - AggregateGenerationCatchesABoundTextureMoving is the first test of the direction the P1 verify comparator cannot see - it compares object-class fields by identity only, so a bound texture whose content moved looks unchanged to it. The bit fires and then settles, so it is a shutter and not a stuck flag. - ANaNPatchLevelEqualsItselfAndDoesNotFireForever: a NaN outer level is a legal glPatchParameterfv value, float equality says it differs from itself and a byte compare says it does not. That is why the shutter is a memcmp. - ThePixelPackShutterIsAByteCompareOfThePackHalfOnly asserts an UNPACK write does not move the pack shutter, which is the half that deliberately has no carrier. - HashCollisionDoesNotAliasTwoStates needed a seam and got one: MGPipeCsoCache::s_hashForTest, null in every real build, one never-taken branch on a path that runs only when the pipeline version moved. Without it the memcmp confirm is unreachable code that nothing can prove is doing anything, and what it stops - two different render states on one CSO - is silent wrong pixels with no gate that can see it. - ContentAddressingOffMintsEveryTime pins that bit 63 really changes mint/reuse behaviour, so the negative control cannot rot into a dead switch. - The set-hash suppressor is exercised on all seven slots even though P2 wires one, including the reserved-zero contract: a computed hash of 0 is remapped to 1 so it is never confused with "never emitted". |
||
|
|
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.
|
||
|
|
7dec32a574 |
[Feat] (Pipe): carry what has no call of its own in the residual value block and abort when it disagrees with the assembled state
- set_residual_value_state emits the 35 capability bits, read from the FRONTEND's own
IsCapabilityEnabled rather than from the assembled mirror. That direction is the whole
design: the applier then compares the carried answer against the assembled one, so the block
is an independent oracle instead of a tautology - which is the failure the P1 entry compare
had and P2 is paying to remove.
- It goes out AFTER the residual fill, not with the other emissions: the mirror the trip wire
compares against is written either by the applier's derivation or by that fill, so before it
the block would be compared against the previous verb's answer.
- It is HELD, not dropped, when the verb's class does not carry IsCapabilityEnabled. kQuery and
kXfbSpan do not read it, so at those verbs the mirror is stale by construction; a capability
that moved between two queries would silently disarm the wire if the emission were skipped
instead of deferred.
- ByteClass::ResidualValueBlock has been a placeholder that "stays at 0 until P2" since P0.
This makes it non-zero, which is half of G10.
- The wire is not theoretical: the first version of this commit fired it for real -
Fatal{PipeResidualDiverged, "Dither"} carried=1 assembled=0, on every verb whose class does
not read the capability mirror - and that is what the holding latch above is for. GL_DITHER
defaults to enabled, so the very first mismatch the block could have found is the one it
found.
- integration-verify 818 green, integration-gpu 878 green under the default bitmask and again
under MOBILEGL_PIPE_PUSH=0, unit 1499 green.
|
||
|
|
dcfa5ad311 |
[Feat] (Pipe): push pixel-pack, patch and vertex-attribute-default state as their own calls, the last one behind the set-hash suppressor
- set_pixel_pack_state on NEW_PIXEL_PACK, set_patch_state on NEW_PATCH_STATE, set_vertex_attrib_defaults on NEW_VERTEX_ATTRIB_DEFAULTS, each gated on its own runtime subsystem bit so the bitmask stays a per-subsystem A/B. - MG_Impl/Pipe/SetHashSuppressor.h: seven slots, one per kVarTail set_*, with SetVertexAttribDefaults the one P2 wires. 0 is reserved for "never emitted" and a computed 0 is remapped to 1, so the first emission always goes out. The other six are the carrier for the ~175 lines of backend debounce that move in P3b/P4b; landing the mechanism now means the shape is pinned by a test rather than by a plan. - The var-tail carries only the attributes that differ from the tracker's mirror, underneath the set-hash suppression of the whole resolved set - the two suppressors answer different questions and both are cheap. Two rows of Coverage.def's emitted list CANNOT yet retire their pull, and each says why in the code rather than being silently absent: - GetPixelStoreParameters is BOTH halves of the pixel store and set_pixel_pack_state deliberately carries only PACK, so the unpack half has no carrier at all. The field keeps being pulled and the verify comparator keeps proving it. - GetCurrentVertexAttribute's three views are not bit-identical - GLContext CONVERTS between them - while MGPipeApplySetVertexAttribDefaults memcpys one Data[4] into all three and ignores MGPAttribValue::ValueClass, which the wire type carries precisely so it does not have to. Until that applier reads ValueClass the carrier cannot reproduce the frontend value. The call is still emitted, so the wire shape, the payload bytes and the suppressor are all real; the residual fill runs after emission, so the mirror ends up correct either way. Both are contract-side defects in files this package does not own; they are reported to the integrator with the exact fix rather than worked around here. |
||
|
|
aa64c91052 |
[Feat] (Pipe): mint render-state CSOs on the pipeline subset and send only the dynamic chunks that moved - the steady-state cost of the whole render-state family is now two Uint16 compares
- MG_Impl/Pipe/CsoCache.h: 64 entries, LRU, hash -> probe -> MEMCMP -> handle. The memcmp is not optional: a bare 64-bit hash equality would let a collision alias two different render states onto one CSO, which is silent wrong pixels with no gate that can see it, and Mesa's cso_cache memcmps for exactly that reason. It runs only when the pipeline version moved, so never in the steady state. Eviction emits delete_render_state and frees the client slot. - kMGPipeBehaviourNoCsoContentAddressing (bit 63) turns off the PROBE and the handle reuse, not the records: every pipeline-version change then mints, binds and evicts, which is the whole-block content addressing the design is measured against. - The validate point's step 3: bind_render_state when the pipeline version moved (12 bytes, no hashing, no blob when the cache hits), set_dynamic_state when m_version moved, carrying only the dynamic chunks that differ from the tracker's staging mirror. An EMPTY chunk mask still sends the 32-byte header, because the version is what Magma's dynamic tail gates on and it moved. - The residual fill now skips a field a P2 call supplies, driven by the generated kMGPipeFieldEmittedBy[] and gated per subsystem on the runtime MOBILEGL_PIPE_PUSH bitmask, so the bitmask is a true per-subsystem A/B. THE STAMP IS UNCHANGED: a stamp says "this verb published this field", which is as true of an emitted field as of a copied one, and withholding it would abort every backend read of the fields the migration just took over. - PipeStats::RecordDrawPayloadBytes has been implemented, unit-tested and called by nothing since P0. This is its first emitter. - A field that reaches PipeInputs only through MGPipeDeriveRenderStateFields is skipped only when that derivation is really there. It is package A's and is a declared stub on the p2/contract tag this branch starts from, so rather than hard-code which branch this is, the filler probes once: a sentinel in a scratch block, the mirror cleared, the derivation run, the answer latched. It stays useful after A lands - if the derivation is ever deleted the filler degrades to PULLING those fields rather than rendering a default. - integration-verify, 818 entries, green: the comparator re-reads every field from the live context at every backend read, so "the assembled block equals the live context" is now proven rather than asserted, and the entry compare has stopped being a tautology. |
||
|
|
fcd4ad3799 |
[Feat] (Pipe): compute the per-verb dirty mask at the validate point and count how often each bit fires
- MGPipeFillForVerb becomes MGPipeValidateForVerb and MGP_FILL expands to the new name. The
macro spelling, the 83 call sites and the verb enum do not change: the dispatch is
kMGPipeVerbClass's nine classes, which is the same code as nine named ValidateFor* entry
points with one call site per verb instead of nine (P2 brief D1, against ARCHITECTURE.md's
eight - FillPoints.def argues in its own comment for splitting kProgramOp out, and the landed
table is what runs).
- MG_Impl/Pipe/Tracker.h: MGPipeDirty's 18 bits, the widened Uint16 shutters, the per-verb walk
and the per-bit-per-verb-class fire tallies. The widening happens in the TRACKER and MG_State
is not changed for it; a wrap costs one extra re-push and never a missed one.
- NOTHING IS EMITTED YET. The mask is computed, latched and counted, and the full P1 residual
fill runs after it unchanged. That is the point of this step: it says the walk is
semantically free before any field stops being pulled, so a regression in the next commit
cannot be blamed on the walk.
- Every shutter over-fires on purpose. Bits 2 and 3 are BYTE compares, not value compares,
because a NaN patch level is a legal glPatchParameterfv value and has to equal itself; bits
5..17 are composed with a mixing hash, which can in principle collide, and that is stated in
the file and is acceptable only because nothing consumes those bits in P2.
- The shutter for bits 6..8 reads the current program's version counters WITHOUT
GetProgramForDraw, so the walk never joins a pending link to answer "did the shader move".
- Header-only rather than Tracker.{h,cpp}: the root CMakeLists.txt that would have to name a
new .cpp belongs to package A and is frozen behind the p2/contract tag. One translation unit
in the library includes it, so inline costs nothing, and splitting it out is one list(APPEND)
line whenever the ownership allows.
- MG_Test/ScopedPipeVerb.h and MG_Test/Pipe/PipeInputsTest.cpp follow the rename. Five comment
references to the old name live in package A's files (MGPipe.h, the two generated .inc, and
gen_pipe.py) and are deliberately left for their owner.
|
||
|
|
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. |
||
|
|
3d1a866e82 |
[Test] (Espryt): pin that the armless cases put the operator's log path and MG_Config::Features back as they found them
- TheArmlessCasesLeaveTheLogPathAndTheConfigAsTheyFoundThem drives ScopedLogFileRedirect and ScopedArmlessKnobPair with and without a pre-set MOBILEGL_LOG_FILE_PATH and asserts the restore byte for byte - in a build without the legacy arm the verdict is Handles whatever the knobs say, so the case pins the knob values rather than the NoArm verdict there |
||
|
|
df784c6752 |
[Fix, Test] (Espryt): deliver a death notice to every holder of a kind, retire the twin table's last sweep, and pin the bring-up call site under the armless knob pair
- BackendSlotTable links every instance into a per-type holder list; OnFrontendObjectDestroyed resolves the handle once, drops the twin in each holder by handle and frees the slot once, last - a notice delivered to one registry left the fixture's saved copy holding a dead twin, and its driver storage, for the life of the process (review v4 minor 2) - ReclaimDeadSlots and CollectGarbageNow leave the slot table: nothing called them, so the header's "backstop" was a claim without a caller; the registry's pre-P2 CollectGarbageNow is a no-op on the handle arm (minor 1) - HandleOf no longer memoises a null answer, which a second holder's acquire could never refresh (minor 8) - EnsureProcessTeardownSentinel is armed by the slot table's first insertion, as D13 says; the registry arms it only on the legacy arm (minor 9) - new SanityTest cases: OneDeathNoticeDropsTheTwinInEveryHolderOfTheKind, ASavedCopyOfARealRegistryDropsTheTwinOnTheSameNotice, ANegativeLookupIsNotCachedAcrossAnotherHoldersAcquire, and EglBringUpUnderTheArmlessKnobPairReturnsInsteadOfStopping, which runs InitPbufferSurface under the pair in a forked child and fails naming both knobs if InitDisplayAndContext ever stops there again (minor 4) - AnArmlessKnobCombinationStopsInsteadOfSkippingTheLane writes a per-process, per-case log path and restores MOBILEGL_LOG_FILE_PATH and MG_Config::Features through RAII guards on every exit path (minor 5) |
||
|
|
c9dd173201 |
[Fix] (Espryt): close four review minors on the twin table - a walk that can outlive its vector, a null call that is not arm-equivalent, and two comments that claimed more than the code does
- ForEachLive walked with a range-for and handed fn a reference INTO m_slots, so a callee that reached GetOrCreate on the same table would resize the vector under both. Index loop and a copied twin, the shape ReclaimDeadSlots already uses. The one caller today happens not to insert; that is not a property the walk should depend on. - GetOrCreate(nullptr) reset the parking twin on EVERY call, so a second null call destroyed what the first was handed. The map arm kept its null-keyed entry until a sweep, so this was an arm difference in the one path (SyncTextureObjectToBackend) that documents relying on the tolerance. It now keeps the parked twin, and the case makes a second call. - The one-entry memo's comment claimed the three per-draw resolution paths ask for the same object every draw. Two of them do not: BindCurrentFBO resolves both targets in a frame and ResolveUnitSamplerBackend asks per texture unit, so both thrash a single-entry memo and pay a probe P1 did not. The comment now says so and names the fix (per-unit / per-target) and the gate that would price it (G11, device-side, owed). - HandleOf caches a NULL answer too - deliberate, because a bound-but-never-synced object would otherwise re-probe every draw - and what makes it safe is that GetOrCreate refreshes the memo. Nothing pinned that; RepeatedLookupsOfALiveObjectKeepOneHandle now does. - Removed the dead #if MOBILEGL_PIPE_PUSH nested inside #if MOBILEGL_PIPE_PUSH in ScopedDetachedTextureFramebufferAttachments. |
||
|
|
f5bd1a0412 |
[Test] (Espryt): drive the acquire / look up / delete / re-acquire walk through the real registry for all six re-keyed kinds
- The D13 "must not break" pins made ZERO slot acquisitions: the scratch-FBO scrub and the
three context-generation guards build their twins with MakeShared directly or under
ScopedStateGuardMocks, so they never reach StateBackendObjectRegistry and pass
identically on both arms. The four acquisitions in the binary were all kind Texture, and
the eleven DirectGLESSlotTable cases drive BackendSlotTable directly on the throwaway
kinds Query and Fence - so they would pass had the six registries never been re-keyed.
Framebuffer, Renderbuffer, SamplerCso, ShaderCso and VertexElementsCso had no case that
could go red for the switch-over.
- EverySwitchedOverKindResolvesItsTwinThroughTheHandleArm walks all six through the real
registry global the shipping paths call: GetOrCreate mints a non-null {slot, gen}, Find
and FindByHandle name the same twin storage, the object's own destructor notice frees the
slot with no sweep, and the successor lands on the freed slot with a moved Gen while the
predecessor's handle resolves to nothing. A kind still on the legacy arm answers the null
handle and fails the assertion by name.
- Evidence: gdb breakpoint on MGPipeSlotAllocator::Acquire counts 12 hits for this case
alone in build-push - six kinds times the two objects each - against 4 for the whole rest
of the binary. It skips visibly on the legacy arm and in the pull build (G2 parity).
|
||
|
|
6cb7d1b83b |
[Fix] (Espryt, State): let the last four object classes announce their own death and delete the twin table's garbage collector
- e2 was landed for two of six kinds, so Texture, Framebuffer, SamplerCso and VertexElementsCso still discovered death in a sweep and ROADMAP.md:18's "delete the GC" was undelivered. TextureObjectBase (the one base every concrete texture derives from), FramebufferObject, SamplerObject and VertexArrayObject now raise NotifyStateObjectDestroyed from their destructor, on RenderbufferObject's pattern: out of line, declared only under MOBILEGL_PIPE_PUSH, so the pull build keeps its implicit destructor and its symbol set (G1 still 0 added / 0 removed / 0 renamed and 0 resized against p2/contract). - With all six announcing, BackendSlotTable loses BOTH sweep drivers: no draw tick, no creation tick, no kGCInterval / kCreationGCInterval / m_gcTick / m_creationTick. CollectGarbageIfNeeded() is empty on this arm; CollectGarbageNow() stays as an EXPLICIT collection and is the backstop for a notice that InProcessTeardown() drops. - The seven CollectGarbageIfNeeded call sites in DirectGLES.cpp keep their spelling because they are the legacy registry's driver and that arm is still compiled beside this one; the registry's body is now guarded on MOBILEGL_PIPE_LEGACY_MEMOS, so a build without the legacy arm has no collector at all. On the handle arm each site is a predicted branch. - The weak_ptr per entry stays for exactly two jobs it is honest about: ForEachLive()'s strong hand-over to ScopedDetachedTextureFramebufferAttachments, and the explicit collection. It is never an identity test; Gen is. - Tests: AProgramAndARenderbufferAnnounceTheirOwnDeath becomes EveryReKeyedObjectClassAnnouncesItsOwnDeath and drives all six classes, by membership rather than count because every texture owns a private sampler that also announces; ObjectChurnAloneDrivesTheSweep becomes AnnouncedDeathKeepsObjectChurnFromAccumulatingWithoutASweep and pins that 256 churned objects hold one live twin at a time with no CollectGarbage* call anywhere. |
||
|
|
7c97fcfee3 |
[Fix] (Espryt): stop the armless knob pair inside the test that needs an arm, not inside the EGL bring-up a forked pre-flight swallows
- Fatal{PipeLegacyMemosDisabled} was raised from InitDisplayAndContext(), i.e. from inside
eglMakeCurrent. The integration harness pre-flights that exact sequence in a forked child
(MG_IntegrationTest/Harness/HeadlessGL.cpp) and reports a child that dies on a signal as
"no usable GPU/display/ICD", so every scenario SKIPPED and ctest called the lane 100%
passed while running nothing - on the very pair of env vars the D14/D18 A/B is driven
with. ROADMAP.md:7 forbids a gate that cannot go red for the reason it exists.
- The arm decision becomes a pure function of the two knobs, ClassifyEsprytSlotArm(), with
three verdicts. Bring-up now calls DiagnoseEsprytSlotArm(), which names both knobs at
ERROR and RETURNS; the stop stays in ResolveEsprytSlotTablesArm(), which the inline latch
reaches at the first twin lookup - a scenario body, where a crash is a test failure.
- A process that never looks a twin up never needs an arm and is no longer stopped by one
it would not have used. That is the only behaviour this moves.
- SanityTest gains an always-on case: the four knob combinations of the pure classifier,
that the diagnosis does not stop, and that the stop is SIGABRT whose log line names
PipeLegacyMemosDisabled, MOBILEGL_PIPE_PUSH, MOBILEGL_PIPE_LEGACY_MEMOS=0 and the bit -
the message and not merely the signal, because "Subprocess aborted" alone tells an
operator nothing. It skips visibly in the pull and no-legacy builds (G2 name parity).
|
||
|
|
caa0a7221b |
[Fix] (Espryt): pick the unit-bindings debounce by the runtime arm, and stop two slot cases sharing one kind
- UnitBindingsSnapshot was split by #if MOBILEGL_PIPE_PUSH, so a push build ran P2's lifetime-id debounce on the MOBILEGL_PIPE_PUSH=0 arm too. That arm has to reproduce P1 (ConfigLoader.cpp), or the integrator's A/B measures this slice's mechanism on both sides and attributes it to neither - the same complaint g_fbSlotCache was already fixed for. The snapshot now carries P1's WeakPtr fields beside the lifetime ids whenever the legacy arm is compiled, and Capture/Unchanged pick by EsprytSlotTablesEnabled(). The two answers are equivalent (OwnerEquals on two empty pointers is true and LifetimeIdOf(nullptr) == 0 == 0; a live-versus-expired control block and two distinct lifetime ids both compare unequal), so this is A/B fidelity, not a behaviour change, and a build with no legacy arm carries neither the fields nor the branch. - TwoTablesOfTheSameKindShareOneSlotAndKeepTheirOwnTwin deliberately never swept, so it left a live MGPipeKind::Query slot behind for good, and ObjectChurnAloneDrivesTheSweep reads HighWater/LiveCount of that same process-global kind. Deltas made them pass today, but --gtest_shuffle or a third case on kind Query would have made them interact. The two-holder case now has a kind to itself and returns its slot at the end. - Its comment claimed two live tables of one kind "cannot arise outside this case". They can and do: ScopedDirectGLESTextureBindings holds a second live table of kind Texture, and package D's subsystem 4 re-keys VaoDrawMemo out of the same per-kind allocator. The comment now states the real hazard (whichever holder frees first orphans the other's entry; safe, because Free is generation-guarded and FindByHandle compares Gen, but not free) and flags it for the integrator. |
||
|
|
eb81705130 |
[Fix] (Espryt): tell the backend when a frontend object dies instead of discovering it in a garbage sweep
- P2 step e2, as far as the file-ownership table lets one package take it. New frontend
header MG_State/GLState/StateObjectDeathNotice.h carries BufferBackendOps' shape for the
other six kinds: an ops table the backend fills in, and one entry point that takes
{kind, lifetimeId} rather than the object, because by the time the last SharedPtr has
dropped there is no object left to pass and the lifetime id is exactly what the client
slot allocator resolves a handle from. Declared only under MOBILEGL_PIPE_PUSH, so the
pull build's symbol set is untouched.
- BackendSlotTable::DestroyByLifetimeId drops the twin and returns the slot at the moment
the object goes, instead of at the next sweep - which for a renderbuffer or a texture
atlas is the difference between freeing the driver allocation now and freeing it 64
creations from now. It returns the slot only when THIS table holds it: two holders of one
kind already exist (the ScopedDirectGLESTextureBindings fixture; Magma's subsystem-4
table shares the VertexElementsCso kind), and a table that never twinned the object must
not free a slot the other one still names. The legacy arm keys on the frontend heap
address, cannot answer a notice at all, and keeps the sweep - which is the announced-
versus-discovered half of the A/B the compile-time arm exists for.
- Managers.cpp registers one dispatcher for all six kinds from ResolveEsprytSlotTablesArm(),
i.e. exactly when the arm that can answer a notice is the arm that runs, and drops a
notice that arrives after exit() has begun.
- FIRING it needs a destructor per class, and the P2 ownership table gives
{Texture,Framebuffer,Sampler,VertexArray}State/* to other packages, so only ProgramObject
and RenderbufferObject raise it here. The other four still rely on the sweep; their four
one-line calls retire it entirely.
- Three cases pin the three halves: the slot comes back with no sweep and the notice is
idempotent and does not free another holder's slot; a program and a renderbuffer announce
their own death when the last SharedPtr drops and not before; and the handle arm actually
installs a consumer, rather than the two halves each being fine on their own.
|
||
|
|
e10f5d6750 |
[Test] (Espryt): run the sanity binary on the twin arm it was compiled for, and pin that it does
- SanityTest never calls MG_ConfigLoader::Init, so MG_Config::Features.PipePush kept its static default of 0 - the value a PULL build ships - and every case in the binary took StateBackendObjectRegistry's legacy UnorderedMap arm. In build-linux, build-push AND build-verify alike, the whole D13 "must not break" list (the scratch-FBO scrub, the three context-generation guards on the texture/framebuffer/renderbuffer twins, the sampled-set staleness walk and the whole-registry ScopedDirectGLESTextureBindings fixture) was therefore evidence about code this package did not change; a gdb breakpoint on MGPipeSlotAllocator::Acquire was the only way to see it. - A gtest global Environment now seeds Features.PipePush with ConfigLoader's own push-build default, so the binary runs the arm that SHIPS in the build it was compiled for: legacy in build-linux (where the handle arm is not compiled and every DirectGLESSlotTable case skips visibly), handles in build-push and build-verify. MOBILEGL_PIPE_PUSH in the environment overrides it with ConfigLoader's decimal/0x contract, so the legacy-arm run of the same binary is one env var. It is an Environment and not a static initializer because MG_Config::Features has a String member and is dynamically initialised. - TheTwinRegistryCasesInThisBinaryRunOnTheHandleArm is that gdb probe made falsifiable: delete the environment and it goes red naming the arm rather than a symptom. |
||
|
|
5a3c0616b7 |
[Test] (Espryt): pin the churn-driven sweep cadence, the null tolerance, and the one slot per object the two Track H slices share
- ObjectChurnAloneDrivesTheSweep churns 256 objects through GetOrCreate and never calls
CollectGarbage{IfNeeded,Now}: nothing but the creation tick can collect them. Verified
red - it is the only case that fails - when the creation-driven sweep is neutered, and
green again on restore. The churn count is a fixed constant rather than a multiple of the
interval so that a negative control which pushes the interval out of reach makes the case
FAIL instead of running for 2^32 iterations.
- TwoTablesOfTheSameKindAgreeOnOneObjectsHandle could not fail: HandleOf never reads the
table, so any two tables agree for any implementation. Replaced by a case that asserts
what is actually load-bearing - ONE slot of the kind is consumed however many tables hold
a twin of the object (which is what makes Magma's subsystem-4 table resolve the same
handle for the same VAO), and the shared handle still addresses each table's own twin.
- GetOrCreateToleratesANullStateObject pins the release-build behaviour the map arm had.
- The three names are registered as visible GTEST_SKIPs in the pull build, like the five
before them, so the pull and push builds keep listing the same ctest entries.
|
||
|
|
9a8369296e |
[Fix] (Espryt): sweep the twin table on object churn again, refuse to run an arm the operator disabled, and give the hot lookups back their array probe
- The handle arm took only ONE of the registry's two sweep drivers. The map arm sweeps
every 64 first-time insertions BECAUSE object churn, not draw count, is what makes the
sweep urgent: a CTS-shaped case runs ~10 per-draw ticks, so the 1024-tick draw-path
driver alone spans ~100 cases' worth of dead, gigabyte-sized twins. BackendSlotTable now
carries the same kCreationGCInterval = 64 creation tick, swept before the entry reference
exists for the same reason the map arm sweeps there. Without this the slice REGRESSED the
memory it was supposed to leave unchanged.
- Fatal{PipeLegacyMemosDisabled} now aborts. It logged and then returned false, which fell
straight into the legacy arm the operator had just made unreachable: a green run measured
on the wrong arm, and the exact lever HandleRecycleScenario's arms are selected with. It
is also resolved at backend context creation now, not on the first twin lookup, so a
process that twins nothing still learns its knobs leave it with no arm at all.
- EsprytSlotTablesEnabled() becomes an inline latch over an out-of-line resolver. It is
consulted on every Find/GetOrCreate/HandleOf/ForEachLive/CollectGarbage*, i.e. several
times per draw, and as a cross-TU call with no LTO that was a PLT call per lookup.
- HandleOf keeps a one-entry lifetimeId -> handle memo, so the three per-draw resolution
paths whose TwinLookupMemos this slice deleted go back to an integer compare plus an
array index instead of the allocator's ByLifetimeId hash - which is the "direct slot
indexing" the memo removal was traded for. It cannot serve a stale answer: a lifetime id
is never handed out twice, and FindByHandle compares Gen anyway.
- GetOrCreate(nullptr) returns a parking slot instead of dereferencing null in a release
build; the map arm inserted a null key and SyncTextureObjectToBackend documents relying
on that tolerance.
- ReclaimDeadSlots moves the twin out before it writes the entry, so a twin destructor that
re-entered GetOrCreate and grew m_slots could not make the writes land in freed memory.
- The framebuffer binding-slot cache is gated on kMGPipeSubsystemEsprytSlots rather than on
the compile-time MOBILEGL_PIPE_PUSH, so MOBILEGL_PIPE_PUSH=0 stays the faithful
all-subsystems-pull control ConfigLoader.cpp documents. The poison bypass stays closed on
the arm that ships.
- MGB_TWIN_KIND_PARAM/ARG stop leaking into every TU that includes Managers.h: the twelve
declaration and definition sites name a TwinRegistry alias template that swallows the
kind in the pull build, and the one remaining macro is #undef'd after the class.
- The twin lookup inside BindCurrentFBO stops shadowing the framebuffer binding slot in a
function whose whole subject is which "slot" is meant.
|
||
|
|
149e26a79a |
[Fix] (Espryt): do not return a reference through a null slot pointer, and stop a comment claiming a memo that is no longer there
- SyncTextureObjectToBackend re-resolves its slot after the nested glTextureView sync. The assert that it is still there is right - the caller holds the frontend object, so nothing can reclaim its slot - but the function returns a REFERENCE, and in a release build the assert is gone and the deref is not. It now falls back to GetOrCreate and puts the twin the caller is about to use back. - ResolveVaoTwin documents why its raw twin pointer survives the whole draw on both arms instead of pointing at TwinLookupMemo, which the handle arm does not compile. |
||
|
|
3160c4b85b |
[Test] (Espryt): pin the twin table identity contract - a reclaimed slot is a new handle and the stale one resolves to nothing
- Five cases against BackendSlotTable directly, through a stand-in state object that carries
only GetLifetimeId(), so none of them needs a GLContext, a driver or ES entry points.
- The load-bearing one is the ABA: an object dies, the sweep returns its slot, the next
object takes the same slot with a moved generation, and the predecessor handle answers
null instead of the successor twin. That is the property the address key could only paper
over with a weak_ptr.
- Gen moves on reuse and only on reuse; Find never mutates the table (which is what lets
SyncTextureObjectToBackend drop its second Find); a whole table saves, resets with = {} and
restores, which is the shape ScopedDirectGLESTextureBindings needs; and two tables of one
kind agree on a single object handle, because the identity comes from the client allocator
rather than from either table.
|
||
|
|
bd2092f4eb |
[Refactor] (Espryt): key every backend twin on {slot, gen} instead of the frontend object heap address
- ResolveVaoTwin, SyncCurrentProgram and BindCurrentFBO stop consulting the three
TwinLookupMemos on the handle arm. The memo existed to replace the registry hash probe
with an array index, and the slot table Find already IS that array index; its safety
argument - owner-equality of a weak snapshot against a recycled heap address - is
answered by the generation instead of re-derived per lookup. OwnerEquals, the memo
template and its three instances are now compiled only under MOBILEGL_PIPE_LEGACY_MEMOS.
- UnitSamplerLookupMemo compares {slot, gen} instead of owner-equality, and keeps its "a
miss is never cached" contract verbatim: the sampler twin is created later in the same
draw by the program pass.
- UnitBindingsSnapshot holds lifetime ids rather than weak_ptrs under push. It cannot hold
handles: a bound-but-never-synced texture has no twin and so no handle, and two of those
would read as equal. A lifetime id exists before the twin does and is never handed out
twice, which is the property the weak_ptr was there for.
- SyncTextureObjectToBackend keeps its by-value copy only to hold the twin alive across the
nested glTextureView sync; the second Find-or-create and the put-the-twin-back repair are
gone on the handle arm, because nothing there erases a live entry.
- The one direct-iteration site walks ForEachLive, which hands over a strong reference to
the framebuffer instead of the map key - the raw frontend address it had to null- and
expiry-check by hand before dereferencing.
- GetFramebufferBindingSlotFast becomes GetFramebufferBindingSlotChecked and, under push,
reads MGB_CTX->GetFramebufferBindingSlot(target) every time. This closes the P1 accessor
bypass: the cached raw pointer ran the checked accessor once per context change and then
handed out the pointee forever, so the per-verb poison stamp and the verify read-hook
were skipped at all five call sites.
- Every one of these is a push-build arm; the pull build compiles the pre-P2 text and its
symbol report stays 0 added / 0 removed / 0 renamed with no new resize.
|
||
|
|
f4dbea2300 |
[Feat] (Espryt): give the backend a dense {slot, gen} twin table beside the address-keyed registry
- SlotTables.h: BackendSlotTable<StateObject, BackendObject, kKind>, indexed by
MGPipeHandle::Slot and validated by Gen, with the handle minted by the client
MGPipeSlotAllocator off the frontend object GetLifetimeId(). A lookup is one bounds
check plus one array index, and unlike the registry Find it never mutates the table,
so a returned BackendPtr* is not invalidated by the next call on it.
- StateBackendObjectRegistry keeps its name, its signature and all ~40 call sites, and
becomes the two-arm facade ARCHITECTURE.md 9.6 asks for: the pre-handle map under
MOBILEGL_PIPE_LEGACY_MEMOS, the slot table under MOBILEGL_PIPE_PUSH, chosen once per
process by EsprytSlotTablesEnabled() off kMGPipeSubsystemEsprytSlots. A clear bit with
MOBILEGL_PIPE_LEGACY_MEMOS=0 leaves no arm at all and is Fatal{PipeLegacyMemosDisabled}.
- The kind is a template parameter only in the push build (MGB_TWIN_KIND_ARG): a third
template argument would rename every instantiation and G1 wants the pull build byte
identical. Pull-build symbol report is 0 added / 0 removed / 0 renamed and adds no
resize beyond the three RenderState symbols the contract commit already moved.
- ForEachLive replaces begin()/end() under push and hands the callee a strong reference
to the state object instead of the map key, which was the raw frontend address.
- Nothing switches over yet: the tables are built and reachable, and the twins still go
through whichever arm the bit selects.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
ce370a3e84 |
[Test] (Pipe): drive both redundancy trip wires and the four apply entry points nothing in any build reached, and finish the setter walk's stencil faces and hint targets
- Five of the applier's eight entry points had no caller in any build, including both wires the redundancy of the patch trio and of the residual block exists to arm. ROADMAP.md asks that every gate be able to go red for the reason it exists; these could not change colour at all. - Each wire is now driven in three states: DISARMED (the applier has not scattered the bytes it would compare, which is the MOBILEGL_PIPE_PUSH=0x10 shape), ARMED AND AGREEING, and ARMED AND DIVERGING. The diverging state is asserted in the form the build gives it - a poison or verify build aborts and the parent reads SIGABRT and the Fatal line out of the log, a shipped push build counts and logs - so neither case is skipped anywhere. - ResidualTripWireHoldsAcrossAVerbClassThatDoesNotPublishTheBlock is the draw-then-dispatch sequence itself: it aborts on the pre-ledger form of the wire and passes on this one. - The patch case carries a NaN outer level, a legal glPatchParameterfv value that must compare equal to itself, which is why the wire memcmps rather than compares. - set_pixel_pack_state, set_vertex_attrib_defaults and delete_render_state are driven and read back through the accessor a backend would use, each under the verb class that publishes it. MGPipeDeriveRenderStateFields - D-7's kept whole-block form, which production does not call - is checked against the chunk-scoped one. - The suite gains its own main() (and links gtest, not gtest_main) for PipeInputsTest's reason: the diverging cases read the wire's line back out of a log file this process names before anything logs. - SetterConsistency drives SetStencilOp and SetStencilFunc and SetStencilMask on BOTH faces and SetHint on all four targets. Face 0's Func ends chunk P2 and its three ops open P3, which face 1's Func closes, so the previous one-face-each walk never wrote P3 at all. |
||
|
|
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.
|
||
|
|
7c2c1456f8 |
[Test] (Pipe): make the slot allocator's ABA case prove its claim without waiting for the heap, and narrow the DEBUG skip to the arm that needs it
- LifetimeIdSurvivesARecycledAddress could not distinguish what its name claimed: Acquire never sees an address, and each round freed its handle, so the next handle differed whether or not the heap repeated the address - a restatement of GenMovesOnlyOnSlotReuse - and the case could silently GTEST_SKIP on a machine that never repeats one. The reuse count is now recorded rather than depended on, and a deterministic arm proves the strictly stronger form: re-acquiring the SAME LIFETIME ID after a free - the key the map is actually built on, which MG_State never reissues - still cannot reproduce the handle, because the reused slot carries a new generation. If that cannot reproduce a handle, no recycled address can. - CompositeShaderBandIsNeverHandedOut skipped the whole case in a DEBUG build, including the two arms that trip no assert. The eight low-slot handouts and the "every other kind is unaffected" arm now run in every build; only the exhaustion walk, which trips the allocator's own "slot space is exhausted" MOBILEGL_ASSERT on purpose, is behind the skip. |
||
|
|
ad1238bd6f |
[Test] (Pipe): require a named pipeline member to be WHOLLY pipeline, drive every indexed setter at index 0, and walk the incremental chunk path
- ChunkTablePartitionsTheBlock asserted only that a named pipeline member is TOUCHED
by the pipeline half. A boundary that demotes part of one - four bytes at the head
of BlendStates, i.e. the glEnablei(GL_BLEND, 0) bit - left the case green except
for the two byte-count constants, which P3 will legitimately move; after that a
partial demotion would have been invisible, and its consequence is one CSO handle
serving two different pipeline states. Now IsWhollyPipeline for every named member,
with StencilStates the one documented straddler.
- Every indexed setter is driven at index 0 as well as at a middle index. Index 0 is
the element both backends consume and the one a head-of-array boundary demotes
first; with it, the demotion above also fails SetterConsistency, naming the setter.
- The round trip D2 rests on is asserted: the assembled block is memcmp-equal to the
live one. That is the only cover for the ~25 members with no derived field at all -
SampleCoverage*, SampleMaskValue, PolygonModeBack, the hints, ScissorBoxes[1..15],
ClipDistanceEnabledMask and the raw capability bools - which is exactly the set
Espryt's SyncRenderState reads through its span memcmp.
- IncrementalChunksKeepEveryDerivedFieldInStep: the shape the tracker actually emits.
A create_render_state naming only the pipeline chunks that moved against a BaseCso
(D7 step 2's miss path) and a set_dynamic_state naming only the dynamic chunks that
moved (D8's suppressor), ten steps plus all 35 capabilities one at a time, each
followed by the full derived-field comparison. It is the first caller of the
base-inherit branch, of MGPipeScatterPipelineChunks, MGPipePipelineChunkBlobBytes,
MGPipe{Dynamic,Pipeline}ChunksThatMoved and MGPipeHashPipelineBytes, and it is the
oracle for the applier's chunk-scoped derivation - a whole-block apply asks for
every chunk and so cannot tell a correct guard from one that is too narrow.
- The 25 kDraw comparisons move into ExpectDerivedDrawFieldsMatch, shared by the
whole-block walk and the incremental one.
|
||
|
|
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. |
||
|
|
02b970e9c1 |
[Test] (Pipe): pin the slot allocator's identity contract - gen moves only on reuse and a recycled address never reproduces a handle
- Everything Track H keys off is only as sound as these statements, so each case is the
answer to a bug the {slot, gen} pair exists to close rather than a coverage exercise.
- GenMovesOnlyOnSlotReuse: the first handout of a slot is generation 0; nothing in the
interface can move a live handle's generation, which is "never on a respecify" stated as
an absence; the bump lands on the NEXT handout rather than on the free, so a double free
cannot skip a generation; and the stale handle then fails IsLive and cannot free the slot
its successor owns. Kinds are independent slot spaces.
- FreedSlotComesBackBeforeHighWaterGrows: two freed slots are both handed back before a
ninth is minted. Density is not a nicety - it is what lets the server's object table be
an array indexed by slot rather than a hash map.
- SlotZeroIsNeverHandedOut, over every kind and across a free/allocate churn: {0, 0} is
null for every kind and {0, 1} is the default framebuffer, so neither may be minted.
- LifetimeIdSurvivesARecycledAddress drives 64 construct/destroy rounds of a real
VertexArrayObject through a volatile address sink (ObjectLifetimeIdTest's trick, so the
new/delete pairs are not elided) and asserts that when the heap hands the same address
back, the handle is still a different one. It also asserts Acquire is an identity - the
same live object always answers the same handle - and that a freed lifetime id stops
resolving. When the allocator refuses to repeat an address the case SKIPS with
"inconclusive, not proven" rather than passing for the wrong reason.
- CompositeShaderBandIsNeverHandedOut walks the ShaderCso space to the composite base and
asserts the last ordinary slot is base - 1 and that the next call REFUSES rather than
stepping in. It skips in a DEBUG build, where reaching the edge trips the allocator's
own exhaustion assert on purpose; the INFO builds the gates run are where it is checked.
- Replaces the contract commit's placeholder, whose one live claim survives as
ReservedHandlesAreWhatMGPipeHandlesSaysTheyAre - the only case that is not push-only.
|