mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
874d1ee77dcc38f74f23febf7f9bfcdf8a13f9c9
2760
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
874d1ee77d | [Feat] (Pipe, State): mint a {slot, gen} handle for every buffer object and publish its create, respecify, sub-data, flush, readback and destroy as pipe calls | ||
|
|
b9eaa47480 | [Build] (Config): bump CoreVersion to 26.9 so GL_VERSION, the POST stamp and the Magma pipeline-cache key follow the 26.09 plugin version | ||
|
|
42d43af25b | [Build] (Android): bump the plugin version to 26.09 - versionCode 26090000 + this month's revision count, versionName 26.09.<hash> | ||
|
|
12e6bfcf14 | [Fix, Test] (Pipe): a make-current is not a teardown - the applier keeps its object records across one, counts every call refused on a record it does not have, advances the two vertex-input serials instead of restarting them at 0, and bounds the slot it grows a record table on | ||
|
|
e6452ce948 | [Fix] (Pipe): give both blob-carrying record families one Blob rule, and name the wire views the vertex-elements blob actually carries | ||
|
|
56366331dc | [Fix] (Pipe): account for DispatchIndirect and Query in Coverage.def's target-by-target split, re-sort the new emitted row, and name the P3a default mask in ConfigLoader's include comment | ||
|
|
355c60b901 | [Test] (Pipe): pin the resource record's lifecycle and the buffer sub-data range encoding at both of its bounds | ||
|
|
6eb0e675ad | [Feat] (Pipe): apply vertex elements, vertex buffers and the index buffer into the server's own working state | ||
|
|
45c8f1a8be | [Feat] (Pipe): apply the resource calls into a per-context slot-indexed record and dispatch them to the backend by handle | ||
|
|
5cb826b01e |
[Merge] (dev): bring dev@9eae9858 into feat/disaggregated - the adopted-store respecify VAO rebind fix (d7655247) and the persistent-buffer ordering POST probe
|
||
|
|
e01c0ccc53 | [Feat] (Pipe): land the P3a contract - the handle-shaped resource op table, the applier's resource and vertex-elements records, the two vertex wire views, an explicit baseInstance on set_vertex_buffers, and the first kNeedsAck | ||
|
|
c036900d72 | [Docs] (Disaggregated): add the Oppo Mali half of the P2 paired A/B - 16 rows per device, Espryt +8-18% and Magma +10-11% per-thread CPU over the pull baseline, plus the harness single-sample pidof kill recorded as a dev-side follow-up | ||
|
|
44c2b5cf3a | [Docs] (Disaggregated): record the P2 landing - the five-part gate results, the two-device paired A/B protocol with all 16 Xiaomi rows, DriverBench T1/T2 with the CSO control, the push-side counters, and the 2026-09-08 rule that performance is recorded against the pull baseline rather than gated | ||
|
|
738b289df8 |
[Fix] (Magma, MG_IntegrationTest): drop the fourth ABA-control consumer, which was unreachable, and the scenario comment its own body contradicts
- VulkanRenderer::LookupVaoDrawMemo's legacy arm read MG_Config::Features.PipeHandleAbaControl
a fourth time, bypassing the one-question accessor MagmaPipeAbaControlDefeatsIdentity that
55d2af9b's own message says all the control's sites go through. It could never be false
there: the control's early return fires ahead of BOTH arms, so a run that reaches the legacy
compare has the knob off. The compare is unconditional again, with a note saying where the
question is answered and to route through the accessor if that early return is ever narrowed
- which is the trap the dead line was, since narrowing it would have silently restored D18's
retired semantics at this one site.
- HandleRecycleScenario's vertex-array case still said "BOTH the VAO and the buffer are
recycled here so that a key built out of raw addresses matches", which the body has
contradicted since
|
||
|
|
a9778eaabe |
[Fix, Test] (Magma, MG_Test): cover the {slot, gen} generation in a unit test that forces a real slot reuse, and stop claiming the ABA lanes do
-
|
||
|
|
c73ae7d443 |
[Test] (MG_IntegrationTest): pin MOBILEGL_PIPE_PUSH in the handle-arm lanes
- an ENVIRONMENT property overrides only the variables it names, so the gate's all-pull control arm (MOBILEGL_PIPE_PUSH=0 over the whole integration-gpu label) leaked into the Handles and AbaControlHandles lanes, turned their LEGACY_MEMOS=0 into D14's armless combination and made the bring-up abort by design; the lanes now pin the P2 default bitmask themselves |
||
|
|
55d2af9bd1 |
[Fix, Test] (Magma, MG_IntegrationTest): make the handle-ABA negative control construct its own collision and defeat the {slot, gen} generation
- MOBILEGL_PIPE_HANDLE_ABA_CONTROL asserted the corruption and saw correct pixels, so
DirectVulkan.HandleRecycle.AbaControl.*AVertexArray* was RED in an always-on
integration-gpu lane while every guard it was supposed to be defeating stood. Two
measured reasons, neither of them the {slot, gen} re-key: (1) D18 spelled the control as
"hash the raw BufferObject* instead of its lifetime id, and skip the vaoLifetimeId
compare", which only collides if the allocator hands the freed block back - it does not.
glGen* recycles the NAME, but a VertexArrayObject is 3920 bytes, past glibc's tcache, so
its chunk goes to the unsorted bin and is split by the next allocation the replacement
path makes; four create/delete cycles in one run gave four addresses ~1 MiB apart, and
the BufferObject behaves the same. (2) The reproducer put a frame boundary between the
arming draw and the recycled draw, and the only memo that carries a GPU slice rather
than a layout - ResolvedVertexBindings - declines across frames by design, so no key
collision whatsoever could have shown up in pixels.
- The control no longer asks the allocator for the collision: on both arms it replaces the
object identity in DirectVulkan's vertex-input keys with a constant, which is the
strongest form of "the block came back" and is deterministic. Three sites, all behind
one question (MagmaPipeAbaControlDefeatsIdentity): the buffer identity leaves
VertexInputStateFactory::ComputeHash, VertexInputStateFactory::MemosFor claims one entry
without its Owner compare, and VulkanRenderer::LookupVaoDrawMemo hands one entry back
uncleared ahead of both arms.
- That is what makes the control cover the key P2 SHIPS. Under MOBILEGL_PIPE_PUSH=0 the
handle arm is not executed at all, so the old control said nothing about the generation
in {slot, gen} - the whole of what makes the re-keyed memos ABA-safe. A second lane,
DirectVulkan.HandleRecycle.AbaControlHandles., runs the handle arm with the knob and
asserts the same corruption; D18's lane is kept verbatim beside it for the pre-handle arm.
- The reproducer's two draws now share a frame, and both buffers are realised before the
window, so a moved slice epoch cannot mask the ABA behind a gate that is not about
identity. Nothing else is relaxed: the frame serial, the slice epochs and the host-map
check stay in force, so a green arm still means "a replacement object was handed its
predecessor's resolved vertex bindings because the identity halves of the keys were
defeated".
- ExpectPixelsFor now prints what it OBSERVED (STALE/FRESH/NEITHER) next to what the arm
expected, on every arm and whether or not the case passes.
- Knob-off is unchanged and the pull build is untouched: every new branch is
#if MOBILEGL_PIPE_PUSH, and symbol_report.py --threshold 0 against the pre-P2 baseline
still reports 0 added / 0 removed / 0 renamed and the same four resized symbols
(RenderState::RenderState, SetCapability, IsCapabilityEnabled, _GLOBAL__sub_I_DirectGLES.cpp).
|
||
|
|
2d690754dd |
[Tooling] (DeviceBench): verify the two campaign devices' pin profiles and add a pin helper that works on them
- bench.sh's pin_freqs writes /proc/ppm and /proc/gpufreq, which exist on neither 35d0befa (SM8750) nor 3B159D009VZ00000 (MT6993 dropped both for /proc/gpufreqv2); the guard the PROFILE_VERIFIED key exists for was about to be defeated on the device its own comment guessed was safe - pin_device.sh pins big/little/GPU through the nodes each device actually has (Xiaomi: policy6 1958400 / policy0 1555200 / kgsl pwrlevel 0; Oppo: policy4 2000000 + policy7 2000000 / policy0 1600000 / gpufreqv2 fix_target_opp_index 0), checks only against its own pins because ColorOS moves stock maxima by itself, and refuses to call a zero-key read a verdict - both profiles are PROFILE_VERIFIED=1 on 30 s pinned windows with zero drift; the evidence and the two things not verified (Oppo GPU held at DVFS-request level only, no full bench window) are in docs/Disaggregated/devices/pin-verification-2026-09-07.md |
||
|
|
7a2e256133 |
[Build] (Android): let the trace APK be built in the push shape for the paired A/B
- -Pmobilegl.pipePush=ON (or MOBILEGL_PIPE_PUSH_APK=ON) passes -DMOBILEGL_PIPE_PUSH=ON to the NDK build the way the spike opt-in already does; OFF by default so nothing shipped moves |
||
|
|
bb2a236d5f |
[Fix] (Pipe): drop the emitter arm for set_pixel_pack_state that spans retired
- spans (
|
||
|
|
13d7e32b7b |
[Fix] (Tooling): restore the executable bit on the two device_bench entry points
- bench.sh and session.sh are invoked as ./tools/device_bench/bench.sh in their own README and are the harness for the paired two-device A/B the GO/NO-GO asks for; a previous commit in this package dropped the bit and every check invoked them through bash, so nothing saw it |
||
|
|
b1c37699b1 |
[Fix] (Bench, Trace, CI): let only the profile answer for itself, name an unreadable profile, and describe the CI step by the mechanism the tree has
- The verified-profile guard read the process environment as well as the profile: the test ran after the source, so PROFILE_VERIFIED=1 exported in an operator's shell re-opened the fail-open hole for every profile that says nothing. Both scripts now set PROFILE_VERIFIED=0 immediately before sourcing, so the file is the only thing that can answer. - A --device path that cannot be sourced was diagnosed as an unverified profile, because both scripts cd to their own directory first and neither checked readability. The path is now also tried relative to the directory the script was invoked from (which is what a repo-root-relative --device means), and an unreadable one is reported as unreadable, naming both places tried. - Verified: exported PROFILE_VERIFIED=1 + an unverified profile -> rc 2; exported 1 + a profile with no key -> rc 2; a repo-root-relative path -> resolved, then refused for its own reason; a missing file -> "cannot read the device profile"; odinlite.env -> past the guard; --allow-unverified-profile -> the three warnings, then proceeds. - test.yml's new step described a mechanism the tree does not have. G6's and G10's entries are registered in the pull build too - they must be, for G2's name-for-name comparison - and skip inside their bodies. The step's value is unchanged and its comment now says the true thing: the `test` job runs those names as a column of skips, and this is the first CI job that unpacks a build which compiled the assertions. - trace_benchmark takes the wall baseline before the CPU baseline, the order OnFrameBoundary already reads them in, so frame 0 stops reporting a CPU delta biased upward against its own wall delta; and it includes <time.h> rather than <ctime> for the POSIX names it uses. |
||
|
|
e5603f9a46 |
[Fix] (Test, Pipe): probe every arm by content, and let the G7 control's exit status carry what it already knows
- All four capability markers are now content probes over the directory the owning package owns,
through one helper. The magma pair still read a single hard-coded VertexInputStateFactory.cpp
while package D already keeps one of its two Features.PipeHandleAbaControl consumers in
Renderer/VulkanRenderer.cpp, so one file move on D's side was a permanent AbaControl skip - the
same defect the CSO probe was rewritten for. The DirectGLES probe stops asking whether
SlotTables.h exists and asks for kMGPipeSubsystemEsprytSlots, the bit the arm is actually gated
on. Every globbed file stays in CMAKE_CONFIGURE_DEPENDS, and the glob is CONFIGURE_DEPENDS.
- Verified: with one throwaway header naming each symbol, build-push configures to "keyed on
{slot, gen}" / "has an emitter" / "has a consumer" and all four MGITEST_* markers appear 24
times in the generated ctest environments; with the headers gone, all four are back to 0 and
the four "will SKIP" verdicts return. The magma sim sat in Renderer/, not in the path the old
probe hard-coded.
- g7_negative_control.sh no longer exits 0 when the control trips for the wrong reason. A
SetterConsistency that had gone red for an unrelated reason satisfied "ctest failed" and never
named SetColorMask, and the integrator's D.3 reads this script's rc. The verdict is now taken
after the restore and the rebuild - a broken build directory is worse than any exit status -
and reported as rc 1 with the output kept, alongside the existing "did not trip" rc 1.
- HandleRecycleScenario writes down what the name-recycle proxy costs: the AbaControl arm asserts
corruption that needs the heap BLOCK back, sees only the NAME, and so can red an always-on
integration-gpu lane for an allocator reason. That trade is deliberate - the alternative is an
arm that is green on the day the reproducer stops reproducing - and the consequence is now
written both in the header and at the skip that is the last thing standing between the two.
|
||
|
|
08d14d85ef |
[Fix] (Bench): make the blend-toggle gate go red when the case it names stops running
- DriverBenchStateToggle was an entry that could not fail for the reason it was added. A case name matching nothing in kBenchCases selected nothing, run_case is void, and main returned 0 unconditionally, so renaming or dropping mc_state_toggle left the entry green while measuring nothing - the exact state it was landed to end (ROADMAP.md:7). - DriverBench now refuses an unknown case name before any GL work (exit 2, listing the cases it does have), so a caller that names a case - run_driver_bench.sh included - learns the case is gone instead of getting an empty CSV. - Both ctest entries additionally require the case's own output row via PASS_REGULAR_EXPRESSION, so the gate stands on the evidence rather than on that check staying in the binary. The toggle entry pins the ops-per-frame column to 46, because the mc_* cases are deliberately excluded from the DRIVERBENCH_DRAWS scaling and 46 toggles per frame is part of what "this case still runs" means. A PASS_REGULAR_EXPRESSION makes ctest ignore the exit code, which is why the row is what is checked; the comment says so. - Verified: renaming mc_state_toggle in kBenchCases -> DriverBenchStateToggle FAILS; setting its ops-per-frame to 45 -> FAILS; restored -> both entries pass again. |
||
|
|
af20dba6db |
[Fix] (Trace, Bench, CI): compute p50 by the device's own median rule, fail the profile guard closed, and give the new control step its sibling's environment
- format_benchmark printed a p50 taken with the nearest-rank rule beside a medianFrameCpuMs the device computes as the average of the two middle frames, and documented the two as one rule; on an even window they differ (the pre-flight printed p50=8.261ms next to medianCpuMs=271.766). p50 now goes through series_median, which is SummarizeSeries' rule transcribed; p95 and p99 stay nearest rank, which is the device's rule for p95 and the honest extension of it for the p99 the device does not compute at all - require_verified_profile treated a profile that simply omits PROFILE_VERIFIED as verified, which is the fail-open default a profile written by copying another one inherits - exactly the case the guard exists for. It defaults to unverified now, odinlite.env carries PROFILE_VERIFIED=1 explicitly (it is the one profile that earned it), and the refusal says "says 0, or says nothing" - the two new profiles claimed profile.sh refuses an unverified profile; it has no such check and needs none - it records a simpleperf profile and pins nothing. The claim is corrected in both profiles and in the README rather than a guard added where there is nothing to guard - the handle-ABA / CSO control step in test.yml set only MOBILEGL_ITEST_REQUIRE_GPU while its sibling verify step sets the three MOBILEGL_MAGMA_* fixes and arms core dumps. It runs the same DirectVulkan binary on the same runner, so a crash there left no core; it now carries both |
||
|
|
a5d1136c02 |
[Fix] (Test, Pipe): ask the BUILD, not the source tree, whether a control's arm exists, and probe the CSO emitter by content
- the three capability markers were decided from source-tree file existence / file text alone, so
after packages C and D land they would have armed the PULL build too, where every arm they name
is compiled out: the AbaControl lane would have gone hard red on `ctest -L integration-gpu`
(gate G2 requires it green in both builds) and the Handles lane green against a library with no
{slot, gen} key at all. The whole block now sits under the same `if (MOBILEGL_PIPE_PUSH)` as
MGITEST_PIPE_PUSH_BUILD, and HandleRecycleScenario re-checks that marker before either push arm
asserts, so a hand-forced environment cannot arm an arm this build does not have either
- the two push-only knobs of those lanes (MOBILEGL_PIPE_LEGACY_MEMOS=0, MOBILEGL_PIPE_HANDLE_ABA_CONTROL=1)
are set only in a push build. In a pull build the legacy arm is the only arm and every subsystem
bit is clear, which is D14's startup Fatal{PipeLegacyMemosDisabled} - the process would abort
before the scenario could report its skip. Test NAMES are unaffected, so G2 still compares equal
- the CSO control armed itself off `MG_Impl/Pipe/Tracker.cpp`, a file the owning package does not
create: it implements the tracker and the cache header-only, so all four CsoContentAddressing
entries would have kept skipping after it landed, with a reason that had become false. The probe
now greps every source under MG_Impl/Pipe/ for the two counters the control actually reads
(RenderStateCsoMints / RenderStateCsoBinds), watching the directory and each file, so the owning
package keeps control of its file layout
- an unrecognised MGITEST_HANDLE_ARM is a FAIL in SetUp instead of a silent downgrade to the
Legacy arm, which would have passed while claiming to be the lane it was not
|
||
|
|
d704401a56 |
[Test] (Pipe): one case per CSO lane, because two of them would race on the lane's log
- CsoContentAddressingScenario reads the library's own summary line, and a log is a per-LANE resource: the library opens it fopen(path, "w"), so every process in a lane truncates it. The file had TWO cases in each lane, which under `ctest -j` is a race whose failure mode is an empty read - indistinguishable from "the counters were never emitted", which is precisely the thing the case exists to report on. - The separate plumbing case is folded into the control as its first ASSERT, keeping its own message, so nothing is lost but the flake. Splitting it out bought a clearer failure message and paid for it with a flake in the mechanism that message is about. - This is the same hazard the file's existing comments describe for the arming lane; it is worth saying out loud that the rule is "a log-reading case owns its lane", not "a log-reading case owns its log path". - Verified at -j 4: 44/44 on build-verify and 24/24 on build-push, and the pull/push ctest name lists are still identical (1402 entries each; 0 names removed against the contract tree, 34 added). |
||
|
|
ce9f44a24c |
[CI] (Pipe): make the dirty-surface report a gate and run the push-only unit tests on the verify runtime
- pipe-gates stops printing gen_pipe_dirty_surface.py --summary and runs --check && --self-test. --check fails both directions - a scanned mutator with no row in MG_Pipe/DirtySurface.def, and a row naming a mutator the scan no longer finds - so a deleted mutator cannot leave a stale row behind claiming coverage. --self-test is what keeps --check honest: a completeness check that silently stopped checking is indistinguishable from a complete mapping, so two canned negative controls must both trip. Same shape as gen_pipe.py --self-test next to it. - integration-verify gains `ctest -L unit`. G6's chunk-table walk and G10's residual assertions live in MG_Test/Pipe, compiled only under MOBILEGL_PIPE_PUSH, and the `test` job builds the PULL library - so before this those tests ran in no CI job at all. The artifact already carries them (the packaging step tars MobileGL/MG_Test whole), so the whole cost is the run: ~14 s for ~1490 entries, measured locally on this tree. - integration-verify also runs the two always-on negative controls by name. They are labelled integration-gpu rather than integration-verify - they are about the handle key and the CSO switch, not the comparator - and this is the only CI job that unpacks a push build, which CsoContentAddressingScenario needs because both counters and the cso[] bracket are #if MOBILEGL_PIPE_PUSH. - build-linux-verify's arming check accepts the per-verb entry point under either of its two names. P2 renames MGPipeFillForVerb to MGPipeValidateForVerb, and a check that named only the old one would go red on the rename for a reason unrelated to what it tests. What it tests is unchanged: the artifact has a per-verb entry point, and it still fails when there is none. |
||
|
|
1a012f2820 |
[Test] (Pipe): register the CSO control in the pull build too, so pull and push name the same tests
- G2 requires `ctest -L integration-gpu` to be name-for-name IDENTICAL between the pull build and the push build, and the four CsoContentAddressing lanes were registered inside `if (MOBILEGL_PIPE_PUSH)`. That is four entries the push build has and the pull build does not, which breaks the comparison for this package and for every package that lands after it. - They now register unconditionally. What the pull build lacks is not the entry but the thing the entry is about, so the build passes MGITEST_PIPE_PUSH_BUILD in and the scenario skips saying exactly that: no render-state CSO exists, no cso[] bracket is compiled into the summary line, and the content-addressing bit steers nothing. - The marker also sharpens the plumbing assertion it guards. Past that skip the process is known to be a push build, and the cso[] bracket is unconditional inside that same #if - so a missing bracket can no longer mean "wrong build configuration" and the failure message stops offering that as an explanation. - Verified: build-linux and build-push now differ by zero ctest names (diff empty over 1408 entries each), and the pull lanes skip with the push-build reason while the push lanes skip with the tracker-not-landed reason. |
||
|
|
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". |