Compare commits

..
Author SHA1 Message Date
swung0x48 c20e2f2b67 [Fix] (scripts): attribute G7 trip over the failing assertion BLOCKS - gtest prints the field name among the compared expressions, so a per-line match calls a real trip inconclusive 2026-09-08 07:21:19 -04:00
swung0x48 680ea63360 [Test] (MG_IntegrationTest): baseline the CSO slot leak case after two warm-up rounds so it measures growth with the churn, not the first draws one-off slots 2026-09-08 07:09:25 -04:00
swung0x48 959ca30810 [Test] (MG_Test): declare the new respecify-repair case in ResourceEmitTest pull-skip list so the pull and push ctest name sets stay identical (G2) 2026-09-08 07:09:25 -04:00
swung0x48 0bff6875b3 [Fix] (MG_Impl, DirectGLES, scripts): take the cheap review minors - the stale validate-point comment, G7's whole-log trip grep and missing INT/TERM traps, G5's CRLF stdout, and the dangling-twin invariant at its member 2026-09-08 07:04:20 -04:00
swung0x48 707bced438 [Fix] (MG_Impl, MG_Pipe, MG_IntegrationTest): close the four cross-package rows nobody claimed - symmetric vertex-input pairing asserts, the counter G10 reads, and the buffer ABA's Magma-only evidence 2026-09-08 07:04:19 -04:00
swung0x48 83c8101a9b [Fix] (DirectGLES): record the fp64 narrowing's dropped SyncGpuWrites as a ruled P3a deviation at the site, with its blast radius 2026-09-08 07:00:28 -04:00
swung0x48 2a5e0195b8 [Fix] (DirectGLES): stop on an armless P3a subsystem verdict now that every LEGACY_MEMOS=0 lane pins an explicit 0x1ff arm - warning and continuing drew through an unconfigured driver VAO 2026-09-08 07:00:28 -04:00
swung0x48 afda613bd9 [Fix] (DirectGLES): publish GLESBufferResource::hostBytes under pendingMutex - it was the one member the off-thread SubData/FlushRange arm wrote with neither a lock nor an atomic 2026-09-08 07:00:28 -04:00
swung0x48 433f51a065 [Fix] (MG_Impl, MG_Test): publish the create a respecify's handle never got - a buffer born while the resource op table was unregistered had every later respecify refused and drew through id 0 with no diagnostic 2026-09-08 07:00:06 -04:00
swung0x48 9ba5d7ba1e [CI] (scripts, test.yml, DirectGLES): hash FlushPendingRangesFrom as G5's eleventh row - a push build compiles no FlushPendingRangesNow, so the tenth row protected text the shipping build never sees 2026-09-08 06:54:25 -04:00
swung0x48 f0beefa85c [Test] (MG_Test, MG_IntegrationTest): assert a destroyed vertex array returns its CSO slot, on the client allocator and on the DirectVulkan lane where it leaked 2026-09-08 06:49:59 -04:00
swung0x48 78ff014547 [Fix] (MG_Impl, MG_State, DirectGLES): give the vertex-elements CSO a backend-neutral death path - the client minted every VAO's slot and only Espryt ever freed one, so under Magma each VAO leaked a slot and a 1.3 KB applier record for the life of the process 2026-09-08 06:49:59 -04:00
swung0x48 3e298c9ad1 [CI] (Pipe): add FlushPendingRangesNow's three-tier drain to G5's byte-identical set 2026-09-08 06:02:36 -04:00
swung0x48 fd6b5bdbf2 [Fix] (Pipe): restore and rebuild on every exit path of the vertex-input negative control, and keep its logs out of the repository 2026-09-08 06:02:36 -04:00
swung0x48 8d3cd67b94 [CI] (Pipe): select the P3a subsystem and map-persistent lanes in the only job that unpacks a push build 2026-09-08 06:02:36 -04:00
swung0x48 f6c7dcd1b6 [Fix] (Pipe): raise the four CSO stats lanes to the P3a subsystem mask so they stop asserting on a configuration nothing ships 2026-09-08 06:02:36 -04:00
swung0x48 4ad88214cd [Test] (Pipe): count the map-persistent round trips over a re-specified adopted arena in both arms, now that the VAO rebind fix is in the branch 2026-09-08 06:02:36 -04:00
swung0x48 4412cef00c [CI] (Pipe): gate that the buffer pool, the deferred-release drain and the three rings did not move 2026-09-08 06:02:36 -04:00
swung0x48 6e9943b36e [Test] (Pipe): assert a regrown adopted store costs one map-persistent round trip per storage definition, not one per draw 2026-09-08 06:02:36 -04:00
swung0x48 0cb46fe2bd [Test] (Pipe): reproduce the buffer handle ABA through public GL and prove the pre-rekey guards are what stops it 2026-09-08 06:02:36 -04:00
swung0x48 83302ca247 [Test] (MG_Test): scope the pipe resource op table in BufferTest's fixture the way it scopes BufferBackendOps - a push build installs both at bring-up, and these 86 cases are the BufferBackendOps arm's 2026-09-08 06:00:46 -04:00
swung0x48 3c55e02797 [Fix] (Espryt): republish the live shadow base on the ensure path so a lazily twinned store is not left with none for the readers that hold no frontend object 2026-09-08 05:27:19 -04:00
swung0x48 75ea7ee2df [Fix] (Espryt): ask the frontend object whether the shadow the ensure path is about to upload holds bytes, instead of a descriptor no content call refreshes 2026-09-08 05:27:03 -04:00
swung0x48 13b380feda [Fix] (Espryt): resolve an attribute's pushed vertex buffer by its attribute index, not by the GL binding point the wire attribute carries 2026-09-08 05:26:48 -04:00
swung0x48 ed83424c75 [Fix, Test] (Espryt): pin that a live host map keeps the handle arm's draw probe dirty and that a backwards generation is refused 2026-09-08 04:52:15 -04:00
swung0x48 df055eab13 [Fix] (Espryt): treat an unbound vertex-elements record as a memo miss and record the applier-reset serial rule the twin's gate depends on 2026-09-08 04:52:15 -04:00
swung0x48 511b3752c0 [Fix] (Espryt): make the vertex-input bit require the resource bit and give both new subsystems a named verdict when the knobs leave no arm 2026-09-08 04:52:15 -04:00
swung0x48 b28058f37c [Fix] (Espryt): walk all 32 attribute slots so a vertex-elements record that shrinks still disables the arrays it dropped 2026-09-08 04:52:15 -04:00
swung0x48 14137bc9a6 [Fix] (Espryt): refuse a backwards generation and an out-of-range slot in the handle-keyed GetOrCreate instead of adopting them 2026-09-08 04:52:15 -04:00
swung0x48 39933613ae [Fix] (Espryt): re-read the client's shadow base at every use and forget it at the two events that free the allocation it names 2026-09-08 04:52:15 -04:00
swung0x48 816373ffd9 [Fix] (Espryt): keep the live-map question in the handle arm's draw-clean probe instead of a record field P3a pins false 2026-09-08 04:52:15 -04:00
swung0x48 7047331a41 [Fix] (Espryt): clamp the handle readback to the backend store past its offset, memoise the buffer handle lookup, and write down the two contracts the ops owed their callers 2026-09-08 04:52:15 -04:00
swung0x48 7ef8cb93fc [Fix] (Espryt): leave FlushPendingRangesNow as this file's only definition and call the one shared range ladder from both arms 2026-09-08 04:52:15 -04:00
swung0x48 b97a228cea [Fix] (Espryt): carry the adopted-store respecify's buffer-id generation bump into the handle arm as well 2026-09-08 04:52:15 -04:00
swung0x48 3840cf734b [Test] (Espryt): extend the slot-table suite to the buffer kind and pin that its death crosses as resource_destroy 2026-09-08 04:52:15 -04:00
swung0x48 68db6db7f8 [Refactor] (Espryt): key the narrowed fp64 vertex stream on the buffer's handle instead of its lifetime id 2026-09-08 04:52:15 -04:00
swung0x48 cd05de504e [Refactor] (Espryt): drive the driver VAO from the pushed vertex-elements record and retire the wrapping index-slot version and its identity patch 2026-09-08 04:52:15 -04:00
swung0x48 ce24e2a734 [Refactor] (Espryt): answer a readback through the reverse channel and bump the mutation epoch after it, never before 2026-09-08 04:52:15 -04:00
swung0x48 4e44650199 [Refactor] (Espryt): take the buffer ops by handle and payload instead of by frontend object reference 2026-09-08 04:52:15 -04:00
swung0x48 9951961d9c [Refactor] (Espryt): give the buffer resource its own {slot, gen} table instead of hanging it off the frontend object 2026-09-08 04:52:15 -04:00
swung0x48 31e370bed5 [Test] (Pipe): pin the base instance across a make-current and both of the validate point's exits, the record surviving one, the draw-time bind mask, the create-destroy pairing latch, and the bind-mask table against literals 2026-09-08 04:47:51 -04:00
swung0x48 0c55560510 [Fix] (Pipe, State): look the resource handle up on the content paths instead of minting it, publish the bind mask from the two draw-time emitters, pair the destroy with the create's own latch, and state the record-lifetime rule the applier now holds 2026-09-08 04:47:51 -04:00
swung0x48 f11e78b0a4 [Feat] (Pipe, GLImpl): set the pending base instance from the three glDraw*BaseInstance entry points, through a pull-safe macro beside MGP_FILL 2026-09-08 04:47:51 -04:00
swung0x48 d9bde13127 [Fix] (Pipe): stop the tracker's context Reset eating the pending base instance the same call is about to read, and clear it on the validate point's no-context exit too 2026-09-08 04:47:51 -04:00
swung0x48 cc427ec4de [Test] (Pipe): pin the buffer and vertex-input emitters - every attribute field survives the wire, a bare baseInstance change still emits, and the index-buffer bit ignores unrelated writes 2026-09-08 04:47:51 -04:00
swung0x48 b11bb9650a [Feat] (Pipe): widen the dirty-surface scan to MG_State so the MGP_NOTE_MUTATION sites stop being outside the gate 2026-09-08 04:47:51 -04:00
swung0x48 14efd6eb24 [Feat] (Pipe): push the bound VAO's format, its vertex buffers with an explicit baseInstance, and its index binding as their own calls 2026-09-08 04:47:51 -04:00
swung0x48 874d1ee77d [Feat] (Pipe, State): mint a {slot, gen} handle for every buffer object and publish its create, respecify, sub-data, flush, readback and destroy as pipe calls 2026-09-08 04:47:51 -04:00
swung0x48 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 2026-09-08 04:42:02 -04:00
swung0x48 42d43af25b [Build] (Android): bump the plugin version to 26.09 - versionCode 26090000 + this month's revision count, versionName 26.09.<hash> 2026-09-08 04:41:03 -04:00
swung0x48 12e6bfcf14 [Fix, Test] (Pipe): a make-current is not a teardown - the applier keeps its object records across one, counts every call refused on a record it does not have, advances the two vertex-input serials instead of restarting them at 0, and bounds the slot it grows a record table on 2026-09-08 03:51:21 -04:00
swung0x48 e6452ce948 [Fix] (Pipe): give both blob-carrying record families one Blob rule, and name the wire views the vertex-elements blob actually carries 2026-09-08 03:51:21 -04:00
swung0x48 56366331dc [Fix] (Pipe): account for DispatchIndirect and Query in Coverage.def's target-by-target split, re-sort the new emitted row, and name the P3a default mask in ConfigLoader's include comment 2026-09-08 03:51:21 -04:00
swung0x48 355c60b901 [Test] (Pipe): pin the resource record's lifecycle and the buffer sub-data range encoding at both of its bounds 2026-09-08 03:51:21 -04:00
swung0x48 6eb0e675ad [Feat] (Pipe): apply vertex elements, vertex buffers and the index buffer into the server's own working state 2026-09-08 03:51:21 -04:00
swung0x48 45c8f1a8be [Feat] (Pipe): apply the resource calls into a per-context slot-indexed record and dispatch them to the backend by handle 2026-09-08 03:51:21 -04:00
swung0x48 5cb826b01e [Merge] (dev): bring dev@9eae9858 into feat/disaggregated - the adopted-store respecify VAO rebind fix (d7655247) and the persistent-buffer ordering POST probe 2026-09-08 03:15:31 -04:00
swung0x48 e01c0ccc53 [Feat] (Pipe): land the P3a contract - the handle-shaped resource op table, the applier's resource and vertex-elements records, the two vertex wire views, an explicit baseInstance on set_vertex_buffers, and the first kNeedsAck 2026-09-08 02:43:19 -04:00
swung0x48 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 2026-09-08 02:20:24 -04:00
swung0x48 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 2026-09-08 01:40:51 -04:00
swung0x48 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 55d2af9b: both buffers are created before the ABA window and neither is
  deleted inside it, deliberately, so that no buffer traffic moves VkBufferManager's
  slice-epoch counter inside the window. Only the VAO is recycled, and only its GL name - the
  heap block is not handed back, which is why the knob rather than the allocator constructs the
  AbaControl arms' collision.
2026-09-08 01:08:51 -04:00
swung0x48 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
- 55d2af9b claimed - in its message, in MagmaPipeArms.h, in VertexInputStateFactory.cpp and
  in MG_IntegrationTest/CMakeLists.txt - that the AbaControlHandles lane defeats the
  GENERATION in {slot, gen}. It does not, and no lane of that shape can. Magma's mint has no
  death notification (nothing in MG_Backend/DirectVulkan consumes NotifyStateObjectDestroyed)
  and returns a slot only through OnFrameBoundary's age sweep, kSweepInterval 256 /
  kRetireAgeBoundaries 1024; HandleRecycleScenario issues five frame boundaries, so the
  replacement VAO acquires against an empty free list and gets a BRAND-NEW slot at Gen 1
  (measured: redVao slot=2 gen=1, greenVao slot=3 gen=1). The knob-off FRESH verdict there is
  decided by the SLOT alone, and deleting ++m_entries[index].Gen leaves all 32 HandleRecycle
  entries green - re-measured this round.
- What the lane does defeat is the object identity that SELECTS the slot, which IS the key the
  handle arm ships, and that is what the three code sites now say. The two requirements are
  mutually exclusive for the pixel-visible memo: a genuine slot reuse needs >= 1024 idle
  boundaries after the dead object's last draw, which necessarily puts the two draws in
  different frames, and ResolvedVertexBindings - the only memo carrying a GPU slice rather
  than a layout - declines across frames by design.
- So the generation is covered where it IS expressible. MG_Test/Pipe/MagmaPipeIdentityTest.cpp
  drives the mint's real retire -> reuse (1280 boundaries, with a keep-alive object holding the
  first allocatable slot so the reuse is not the slot the control aliases onto) and asserts
  four things: the retired slot comes back with Gen+1; with the knob OFF a memo stamped at
  {slot, gen=N} is NOT served at {slot, gen=N+1}; with the knob ON it IS, out of one uncleared
  and unclaimed entry; and a live object keeps its slot, its generation and its memo across two
  sweeps, so the generation cannot be "fixed" by bumping it on every acquisition.
- The claim rule itself moves into MagmaPipeArms.h as MagmaPipeClaimSlotMemos so the suite
  exercises production code rather than a copy of it. VertexInputStateFactory::MemosFor is now
  one call to it and is otherwise unchanged, on both the knob-on and the knob-off path.
- Load-bearing, measured: with ++m_entries[index].Gen commented out, ctest -L unit in
  build-push goes 1563/1566 - three of the four new cases red, one of them naming the inherited
  0xDEAD payload out of the same slot - while ctest -R HandleRecycle stays 32/32. Restored, all
  four pass in build-push and build-verify and skip visibly in the pull build, so the ctest name
  sets stay identical (G2).
2026-09-08 01:08:51 -04:00
swung0x48 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
2026-09-08 00:40:47 -04:00
swung0x48 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).
2026-09-08 00:19:39 -04:00
swung0x48 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
2026-09-07 23:43:11 -04:00
swung0x48 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
2026-09-07 23:38:49 -04:00
swung0x48 bb2a236d5f [Fix] (Pipe): drop the emitter arm for set_pixel_pack_state that spans retired
- spans (d1a7c5f1) removed GetPixelStoreParameters from the emitted list because the call
  carries the pack half of a two-element field, so the generated MGPipeFieldEmitter no longer
  has a SetPixelPackState enumerator; the tracker's subsystem map and its static_assert, written
  against the contract, still named it and the push and verify builds did not compile on the
  integrated tree
- the NEW_PIXEL_PACK dirty bit still names the pixel-pack subsystem, which is what the
  emission gate consults; the field itself stays in the residual fill loop by design
2026-09-07 23:38:10 -04:00
swung0x48 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
2026-09-07 23:18:10 -04:00
swung0x48 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.
2026-09-07 23:18:10 -04:00
swung0x48 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.
2026-09-07 23:18:10 -04:00
swung0x48 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.
2026-09-07 23:18:10 -04:00
swung0x48 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
2026-09-07 23:18:09 -04:00
swung0x48 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
2026-09-07 23:18:09 -04:00
swung0x48 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).
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 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).
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 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).
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 59191cd296 [Fix] (Pipe, DirtySurface): resolve both sides of the dirty-surface derivation to member+field, follow reference and pointer aliases, and turn every write the analysis cannot place into an UNDECIDED answer instead of a verdict
- The writer side recorded a member-rooted write bound to a reference (`for (auto& blendState : m_parameters.BlendStates)`) as the field alone, so seven RenderState setters read as writing nothing; the reader side resolved `render.PatchVertices` to the whole of m_parameters, so every setter that touched any byte of it "supported" NEW_PATCH_STATE and a row saying glClearColor publishes the patch state was green. Both were the same defect: the two sides did not resolve to the same token.
- Both sides now carry MEM:<member> and FIELD:<member>.<leaf>; a whole-member write or read is every field. A reference, pointer or range-for alias bound to a member-rooted lvalue is followed (rebinds and aliases of aliases included), a write through a call-result lvalue and a mutating call on a member-rooted lvalue count as whole writes, a const alias cannot be written through with `.`.
- A write, or a non-read-only method call, whose root the analysis cannot place - a reference parameter, a call result, a member without the m_ prefix, an unattributable assignment operator - taints the function; the taint rides the call-graph fixed point and every (row, bit) that depends on a tainted function is UNDECIDED, never a verdict. Nothing is trusted by name.
- Match rule: a writer supports a bit iff the two sides share a member and, both field-resolved, their field sets intersect; a member in common with no field information on one side is COARSE, reported and never counted; UNDER-FIRING only when both sides are resolved and disjoint for every member the shutter reads.
- --check counts only supported answers as derived, prints the COARSE and UNDECIDED tallies, and fails on an UNDECIDED row unless MGP_DIRTY_SURFACE_UNDECIDED_LIST in DirtySurface.def marks it; a mark on a row the derivation decides is a red gate too. The list is empty: all 8 non-render bit answers are supported at field level, and NEW_PATCH_STATE has exactly three legal carriers again.
- --self-test grows from 10 to 21 negative controls, including the synthetic bodies of every shape above through the real extractor, the NEW_PATCH_STATE analogue of the value-class control, the taint, COARSE and stale-mark paths, and positive controls for SetPixelStoreParam's pasted writes and the seven alias setters.
2026-09-07 23:18:09 -04:00
swung0x48 8d0ed5b82c [Fix] (Pipe, DirtySurface): read the writes that go through a member's field and the ones the preprocessor pastes together, and decline the rows the write analysis cannot answer - its "UNDER-FIRING" verdicts were an absence proof it did not have, and one of them put a false answer in the map for a bit P2 already ships
- the write analysis under-approximated in the exact direction its own claim forbids:
  written_tokens recorded a write through a member's field (m_foo.bar = v) as FIELD:bar
  and never as MEM:m_foo, while resolve_reader resolves a shutter's accessor to
  MEM:m_foo, so for any struct-valued member the two halves could not meet; a new
  MEMBER_ROOTED_WRITE_RE records both, for m_x.f, m_x[i].f, m_x->f and nested
- it could not see RenderState::SetPixelStoreParam's sixteen writes twice over, because
  they are spelled with the token-pasting operator and the file was read raw - the
  "field" it recorded was the macro parameter name, paramNameTail. The derivation now
  expands the function-like macros defined under its two roots (directives blanked,
  parameters substituted, ## pasted), which is also what makes SET_CAPABILITY's
  m_parameters.capability##Enabled writes visible
- and it now DECLINES rather than answers wherever it cannot say it read every writer:
  a body carrying a construct it does not model (an unexpandable token paste), anything
  that reaches such a body through the call-graph fixed point the writes already travel,
  and any shutter member with a write-shaped occurrence outside the analysed roots.
  --check prints every decline with its site, plus how many bodies and files the absence
  claim rests on and the one place it stays coarse
- the BitwiseEqual bits' shutter window now also starts at the last `}` before the
  `dirty |=`, so the pack block's trailing `m_pack = pack;` no longer leaks the pixel
  store into NEW_PATCH_STATE's reader set
- consequence in the map: X(SetPixelStoreParam, NEW_PIXEL_PACK) went from a verdict the
  gate could not support - no function name in the tree could carry that bit - to an
  accepted, checked answer, and the row it forced (kPulledEveryVerb, documented as "no
  shutter exists, and none is needed yet") said that of the only mutator behind the
  shipped set_pixel_pack_state. The row is now kPulledPartialShutter|NEW_PIXEL_PACK: the
  pull is what holds on every mutating path, the bit moves on the eight Pack arms, and
  both facts are machine-readable for the P3a reader D16 writes this file for
- --self-test grows from 7 negative controls to 10 - kPulledPartialShutter naming no
  bit, a mutator whose write analysis is incomplete, and a shutter member written
  outside the roots, the last two asserting a DECLINE and no verdict - and gains a
  positive control that fails if SetPixelStoreParam's pasted writes ever go unread again
2026-09-07 23:18:09 -04:00
swung0x48 f15b0fdf4b [Fix] (Pipe): derive the dirty-surface map's object-class and value-class answers too, and correct the two rows that named a shutter their mutator never moves
- X(SetNamedTransformFeedbackBinding, NEW_SO_TARGETS) was false on EVERY path: that
  mutator binds a BufferState binding point or writes a saved-bindings entry, while the
  bit's shutter mixes the buffer-CONTENT aggregate with the transform-feedback
  generation, and a binding moves neither. It answers kPulledEveryVerb, which is what
  reaches the backend today (GetBufferBindingPoint, in the class's may-read mask).
- X(SetPixelStoreParam, NEW_PIXEL_PACK) was false on the eight Unpack arms: the setter
  writes both halves and the tracker's bit 2 is a byte compare of the PACK half alone,
  because set_pixel_pack_state deliberately has no unpack counterpart. It answers
  kPulledEveryVerb, the one publisher every arm has.
- --check no longer rubber-stamps the 28 rows the RenderState derivation cannot reach.
  It reads Tracker.h's Update() for what each bit's shutter READS, resolves those
  accessors through MG_State's getters to the members behind them, computes what every
  mutator transitively WRITES as a fixed point over MG_State/GLState and MG_Impl/Pipe
  (expanding MGP_NOTE_AGGREGATE through MGPipeNoteAggregate's own switch rather than
  assuming the hop), and fails a row naming a bit whose shutter its mutator moves on no
  path. One-directional by construction: the write analysis over-approximates, so it can
  prove absence and not presence, and absence is the under-firing direction.
- the enumerator spelling and the NEW_* spelling are paired BY POSITION out of Tracker.h,
  so the enum and kMGPipeDirtyNames drifting apart is itself a gate failure.
- two more self-test negative controls, one per family, both built from the defect that
  was really in the file; 7 controls now, all tripping.
- --check prints what it did NOT check: how many rows carry a prose answer, and every row
  the derivation declined, so "all mapped" cannot be read as "all verified".
- render_state_publishers() folds the bodies of one name with INTERSECTION, so two
  overloads - one BumpVersions, one bare ++m_version - can no longer derive as "both
  always fire" and bless an under-firing row.
- the header states what "every path" means: every path that MUTATES, so a redundant-write
  guard does not make its publisher conditional, while a publisher reached on only some
  mutating paths must not be named.
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 067b186677 [Docs] (Pipe): say what actually fails to publish a program, a pipeline and a shader dying, instead of naming a scope
- the kUnpublishedDestroy note said the three kinds were outside the six D13 scopes explicit
  destroy to; the tree's reason is sharper and does not depend on reading D13's prose one way
  or the other: those three have no per-object handle on the wire at all, so their DirectGLES
  twins are still reclaimed by the backend's own registry teardown
- records the brief/tree disagreement in place: D13 says "six kinds" while the Core.cpp line
  ranges it cites also cover MarkProgram/MarkShaderForDeletion
2026-09-07 23:18:09 -04:00
swung0x48 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
2026-09-07 23:18:09 -04:00
swung0x48 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
2026-09-07 23:18:09 -04:00
swung0x48 c574043c13 [Fix] (Pipe): derive the render-state answers of the dirty-surface map from RenderState.cpp instead of believing them, and correct the two rows that named a publisher which does not always fire
- SetCapability named NEW_PIPELINE_STATE, but its ClipDistance0..7 arms write
  ClipDistanceEnabledMask (dynamic chunk D7) and deliberately do not BumpVersions, so that
  publisher does not fire at all for glEnable(GL_CLIP_DISTANCE0); SetStencilFunc named it
  too, while ++m_pipelineStateVersion there is conditional on Func moving, so a ref-only
  glStencilFunc does not move it either. Both are now NEW_RENDER_STATE, the answer that
  holds on every path
- a row may now carry several publishers joined with '|', which is what lets the 18 setters
  that call BumpVersions on every path state both counters, and the patch trio state its own
  bit and the two render counters it also moves
- --check no longer validates only row existence and answer vocabulary: it reads
  RenderState.cpp, derives per setter which of the two counters moves on EVERY path
  (BumpVersions moves both, a bare ++m_version only the first, a setter with both kinds of
  path only the first, a delegating setter inherits its callee's) and fails when a row
  claims a publisher that under-fires or omits one that always fires
- two new self-test negative controls, one per direction, both built from the defects that
  were actually in the file
- MarkProgram/MarkProgramPipeline/MarkShaderForDeletion answered kExplicitDestroy, a
  mechanism D13 does not build for them: Espryt 0b's explicit destroy is scoped to six
  object kinds that exclude programs, pipelines and shaders. They answer kUnpublishedDestroy
  now - a recorded hole rather than a mechanism that does not exist
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 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".
2026-09-07 23:18:09 -04:00
swung0x48 3302ee82b5 [Feat] (Pipe): map every frontend mutator onto the aggregate generation that publishes it, and make the dirty-surface scanner a gate
- MG_Pipe/DirtySurface.def: 73 rows, one per distinct mutator the scanner finds, each answering
  "what publishes this". The answer vocabulary is a MGPipeDirty bit name or one of five
  non-bit answers, and each of the five is documented in the file's header rather than left to
  be inferred: kImmediate, kReverseChannel, kNoBackendRead, kExplicitDestroy and
  kPulledEveryVerb. Where a mutator has more than one true answer the row carries the COARSER
  one - the one that cannot under-fire.
- gen_pipe_dirty_surface.py --check is the gate and it fails in BOTH directions: an unmapped
  mutator renders stale, and a row naming a mutator the scan no longer finds keeps a real hole
  looking covered. It also rejects an answer that is neither a documented non-bit answer nor a
  bit name read out of Tracker.h's own kMGPipeDirtyNames, so a renamed bit cannot leave a row
  silently pointing at nothing.
- --self-test runs three canned negative controls - a withheld mutator, a stale row, a bad
  answer - and each must trip; trips == 0 is itself an error, the shape
  check_include_closure.py and gen_pipe.py --self-test already use. ROADMAP.md's rule is that
  every gate must be able to go red for the reason it exists.
- --summary keeps working unchanged, because the CI file that still calls it belongs to
  another package until it lands.
- The human report prints the mapped answer where it printed UNMAPPED.
- FillPoints.def: the verdict on the eight statically over-approximated rows, recorded per
  group in the def's own comment. All eight are KEPT and the reason is the same in all three
  groups - each row names a concrete backend path (the depth/stencil read emulation's paused
  capture, VkClearManager::PreCompensateSrgbClearColor's GL_FRAMEBUFFER_SRGB read, the shader
  blit's viewport / provoking vertex / binding-point reads), and the only evidence that could
  retire one is dynamic. A corpus that never reaches a path proves nothing about it, and a row
  dropped on that basis turns a rare path into Fatal{UnmigratedPipeInput} in a shipped build.
  The contract's new FramebufferSrgb storage in fact makes one of the eight MORE load-bearing
  than it was, not less: it used to read a compile-time constant.
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 de532f55a9 [Feat] (State): give the frontend six aggregate generations so a tracker can answer "did any bound texture, buffer, attachment or attribute move" with one Uint64 compare
- MGP_NOTE_AGGREGATE(Aggregate) next to MGP_NOTE_MUTATION in MG_Pipe/PipeMutation.h, ((void)0)
  in the pull build for the same reason and with the same shape. It answers a DIFFERENT
  question from MGP_NOTE_MUTATION - "did any object of this class move since the tracker last
  looked", not "did a backend move a frontend value inside its own verb" - which is why it is a
  second macro rather than an overload.
- The counters are members of the owning MG_State container (VertexArrayState,
  FramebufferState, TextureState x2, BufferState) and are reached through a push-only GLContext
  facade, because the bump points sit on OBJECTS and an object has no back-pointer to the state
  that owns it. That is the free-function form P2 brief D4 allows, and it costs a global load on
  a path that has just written object state.
- A SIXTH aggregate, VertexAttribDefault on GLContext, which D4 does not list. Its bit
  (NEW_VERTEX_ATTRIB_DEFAULTS) is specified there with a ContentHash over all 32
  CurrentVertexAttributeValues, and hashing 768 bytes on every draw does not fit inside the T1
  ceiling the same brief pins. The hash still decides whether to EMIT (D11's set-hash
  suppressor); the generation decides whether to hash at all.
- 30 bump points: 3 VertexArrayObject config-version sites, 3 FramebufferObject object-version
  sites (one of them inside MOBILEGL_DEFINE_FRAMEBUFFER_DEFAULT_SETTER, so the statement
  carries its own line continuation), 5 texture content-version sites, 12 texture
  params-version sites, SamplerObject::BumpVersion as the sampler choke point, 7 BufferObject
  change-serial sites and the 3 glVertexAttrib* defaults.
- Every counter is deliberately COARSER than the state it guards: over-firing costs one extra
  push, under-firing renders stale, and under-firing is the direction ARCHITECTURE.md 13.2
  names as the dangerous one and the P1 verify comparator cannot see for object-class state.
- TrackerTest: each bump point moves ITS aggregate and no other, plus a null-context note.
- G1: the pull build is 0 added / 0 removed / 0 renamed and the four resized symbols are the
  contract commit's own, unchanged by this commit.
2026-09-07 23:18:09 -04:00
swung0x48 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
2026-09-07 23:18:09 -04:00
swung0x48 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)
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 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).
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 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).
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 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.
2026-09-07 23:18:09 -04:00
Swung0x48 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.
2026-09-07 23:18:09 -04:00
Swung0x48 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.
2026-09-07 23:18:09 -04:00
Swung0x48 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.
2026-09-07 23:18:09 -04:00
Swung0x48 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.
2026-09-07 23:18:09 -04:00
swung0x48 9eae98581f [Test] (SelfTest, POST): probe upload/draw ordering in persistently mapped vertex arenas - native GLES controls distinguish mapped destination corruption from staging and synchronization failures
Add the persistent buffer ordering probe to the GLES POST's known-driver-bug inventory. Queue updates and draws into independent FBOs before reading them back, covering SubData and copies from both coherent persistent and ordinary staging buffers. Retry fresh mapped allocations for intermittent corruption.

Require passing never-mapped and fully serialized controls before reporting a finding. Include finish-before, map-then-unmap and barrier diagnostics, preserve caller GL state, and treat setup, allocation and GL errors as inconclusive. This adds detection and reporting only; no rendering workaround is enabled.

Validation: all 46 DriverBugProbesTest tests pass, including 11 new ordering, control, collector and cleanup cases. The Android API 26 / NDK 27 native probe detects all three upload paths on Mali-G1-Ultra r54p1 with clean controls and no GL errors. llvmpipe reports no finding after 240 mapped FBO checks per upload path.
2026-09-07 00:44:08 -04:00
swung0x48 d7655247f7 [Fix, Test] (DirectGLES, Integration): rebind VAOs when an adopted buffer is respecified - the immediate retire path forgot the buffer-id generation, so cached vertex and element bindings kept the deleted store
Advance the buffer-id generation when Ops_Respecify retires immutable storage on the context thread, matching the existing adoption and deferred-retirement paths.

Add pixel regression coverage for unchanged VBO/IBO attachments across same-size redefinition, growth and shrinkage, including bound and unbound VAOs sharing a vertex arena and an index arena returning to shadow storage.
2026-09-06 22:55:06 -04:00
swung0x48 842af23331 [Chore] (Pipe): drop the executable bit from MGPipeRenderStateSpans.cpp
- A mode change on its own, so it does not ride inside a code commit. The other three of the
  four files created with 0755 were already corrected; this is the last one.
2026-09-06 09:51:14 -04:00
swung0x48 d1a7c5f159 [Fix] (Pipe): stop claiming set_pixel_pack_state supplies a field it only half writes, and fold the chunk boundaries into the subset hash's seed
- Coverage.def's emitted list named GetPixelStoreParameters, but the field is
  PipeInputs::m_pixelStore[2] - pack AND unpack - and set_pixel_pack_state carries the pack half
  only, deliberately and permanently. An emitted row is a licence for the residual fill loop to
  skip the field, so the moment the render-state bitmask has its bit set the unpack half would
  be written by nothing while its poison stamp said it was published, invisible to the poison
  and to the verify comparator alike. The row is gone and the reason is in the file; the pack
  half is simply written twice until the field is split.
- kMGPipeRenderStateChunkTableVersion was a promise nobody enforced: a boundary could move,
  the two byte-count assertions be updated, and every persisted key stay valid. The hash is now
  seeded with the version XOR a compile-time checksum of the boundary table, so a moved boundary
  invalidates the keys whether or not anyone remembered - and without a static_assert on the
  boundaries, which would turn G7's negative control into a build break instead of a red test.
2026-09-06 09:51:14 -04:00
swung0x48 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.
2026-09-06 09:51:14 -04:00
swung0x48 bee07c3273 [Fix] (Pipe): arm both applier trip wires off the applier's own scatter ledger instead of off a bound handle, and stop leaving a mixed CSO or a malformed attribute tail to a DEBUG-only assertion
- MGPipeApplierState gains ScatteredChunkBits: the global chunk bits this applier has itself
  scattered into PipeInputs::m_renderState, set by bind_render_state (the whole pipeline half)
  and set_dynamic_state (the chunks it names), cleared by MGPipeApplierReset. set_patch_state's
  own write is deliberately NOT in it - that is the other carrier, and a wire comparing against
  bytes it had just written would be a tautology.
- The residual trip wire now compares capability i only once every chunk that capability's
  answer is read out of is in the ledger. Both of the previous form's contracts were undeclared
  and one of them was wrong: with the render-state subsystem off (MOBILEGL_PIPE_PUSH=0x10 is a
  legal per-subsystem A/B, D14) the working block is the per-verb fill loop's, published per
  verb CLASS, and FillPoints.def does not publish GetRenderStateParameters at kDispatch or
  kTextureOp - so at a dispatch after a draw the block held the draw's bytes and a correct
  context could abort. With the ledger empty the wire now says nothing there, and with the
  subsystem on the applier is the block's only writer and its bytes are current at every class.
- The per-capability grain is not decoration: a bind alone owns the pipeline half, and the
  eight ClipDistances are answered from ClipDistanceEnabledMask in dynamic chunk D7, so between
  a bind and the first set_dynamic_state exactly those eight are unanswerable. The source
  chunks come from the same MGP_PLAIN_CAPABILITY_LIST and the same boundary table
  DeriveCapability reads, so the two cannot drift.
- The patch-carrier wire arms the same way, which replaces its "some CSO is bound" condition -
  a process-global that stayed set from the first bind onward - and covers the verb-class
  contract as well as the ordering one.
- Both wires now run in the shipped push build too, counting and logging where a poison or
  verify build aborts (one MGP_TRIP_WIRE_REPORT/TAG pair, so only the fatal arm writes the
  "Fatal{...}" marker G4 greps for). A wire compiled out of every build a device runs is not a
  wire, and the counters are what let a unit case see it fire in every build.
- create_render_state no longer leaves a dead BaseCso to MOBILEGL_ASSERT, which is inert at
  INFO - the level every gate and every shipped build uses. A recycled slot's record still
  holds the previous occupant's 396 bytes, so inheriting nothing and scattering the delta on
  top handed out a record that was half one CSO and half another. Both arms now start from a
  defined base and report. The same for a brand-new CSO that does not name every chunk.
- set_vertex_attrib_defaults walks the mask's 32 bits rather than the slot array, consumes a
  tail entry for every named location so a named-but-unstorable attribute cannot desynchronise
  the rest, and reports all three consistency faults in every build.
2026-09-06 09:51:14 -04:00
swung0x48 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.
2026-09-06 09:00:04 -04:00
swung0x48 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.
2026-09-06 09:00:04 -04:00
swung0x48 a9bb99a46a [Fix] (Pipe): scope the derivation to the chunks a scatter moved, and give the patch trio and the residual block trip wires that do not depend on call order
- MGPipeDeriveRenderStateFieldsForChunks: the applier no longer recomputes all 29
  fields on every scatter. The four wide walks - the 8-wide blend/colour-mask loop,
  the 16-wide viewport loop, the 16-wide depth-range loop and the 35-arm capability
  switch - are guarded by the chunks whose bytes they read, so a per-frame glViewport
  (the D8 case that sends dynamic chunk D0 alone) pays for one 16-entry copy instead
  of ~170 stores and 35 switch dispatches. That cost sat on the per-draw path and the
  gate it threatened is G11's pinned ns/draw.
- Every guard is MGPipeRenderStateChunkBitsCovering(offsetof(member), sizeof(member))
  over the members the guarded block reads, computed from the boundary table: there
  is no second, hand-maintained member-to-chunk mapping to go stale when a boundary
  moves. The scalar copies stay unguarded on purpose - they cannot go stale, which
  keeps the risk of the scoping confined to four guards.
- MGP_PLAIN_CAPABILITY_LIST is written once and used twice, for DeriveCapability's
  switch arms and for the capability guard's chunk set, so the two cannot drift.
- set_patch_state now asserts under poison/verify that the trio agrees with what
  pipeline chunk P0 delivered, which D6 and D10 both ask for and which was missing:
  a stale set_patch_state silently clobbered the CSO-delivered levels. Compared
  bitwise, because a NaN outer level is legal and must equal itself; armed only once
  a CSO has been bound, which states the ordering contract rather than assuming it.
- set_residual_value_state's trip wire compares the carried bits against the WORKING
  BLOCK instead of PipeInputs::m_capability. Only the derivation writes m_capability,
  so a residual block emitted before the first bind of a context - what "once per
  context" means - compared against all-false storage while Dither and Multisample
  default to true, and aborted under poison. The check is unchanged in strength and
  now has no ordering contract at all.
- PipeApply.h no longer implies the verify comparator is this package's oracle: it
  arms off MG_Config::Features.PipeVerify, which a unit-test process never sets, so
  the unit oracle is named for what it is and the comparator is credited to the
  retrace and integration-verify lanes.
2026-09-06 09:00:04 -04:00
swung0x48 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.
2026-09-06 08:16:53 -04:00
swung0x48 eec92cd221 [Test] (Pipe): walk every RenderState setter and assert the pipeline-subset hash moves exactly when the pipeline version does
- SetterConsistency is G7. It drives every public RenderState setter with a value that
  differs from the one stored, and asserts the pipeline-subset hash moves IF AND ONLY IF
  m_pipelineStateVersion moves. Every case also asserts m_version moved, which is the
  vacuity guard: a setter handed the value it already holds satisfies "neither moved"
  trivially and proves nothing.
- The cases that are not just a list: SetStencilFunc twice (a reference-only change must
  move the version and NOT the hash, because Ref/ValueMask are dynamic and Func is
  pipeline, and RenderState.cpp's pipeline bump is conditional on Func); SetPolygonMode
  with only the back face moving (PolygonModeBack is one of the members P2's subset added
  over the 24 ComputePipelineStateHash hashed); the eight ClipDistance capabilities (the
  one family that moves m_version alone, so the hash must hold); SetScissorBox across the
  ScissorBoxWrittenMask transition; all 25 SET_CAPABILITY names including the three that
  had no storage before the contract commit; and SetPixelStoreParam, which must move
  neither counter and touch no byte of the block.
- Verified red for the right reason: moving the P1/D2 boundary so ColorMasks falls in the
  dynamic half - the partition stays complete, so it still compiles - makes
  SetterConsistency fail naming SetColorMask and SetColorMaskIndexed, and nothing else.
- ChunkTablePartitionsTheBlock re-states the header's static_assert at run time and adds
  the half the hash cannot check for itself: membership. It walks PipeFields.def's
  MGP_FIELDS_RenderStateParameters - the same list gen_pipe.py checks against the struct -
  and asserts a member is covered by a pipeline chunk exactly when
  kMGPipePipelineStateMembers names it, that every other member is wholly dynamic, and
  that StencilStates straddles at exactly the sub-member granularity the table intends.
- DerivationMatchesTheFrontendGetters drives 30-odd setters on a live GLContext AFTER the
  filler has run, assembles the working block through the real create/bind/set_dynamic_state
  path, and compares the derived fields against the frontend getters. The stale fill is the
  point: an ASSERT_NE before each apply proves the block disagrees first, so nothing here
  can pass by comparing the filler with itself. It runs in THREE verb phases, because the
  fill table is the only thing that says what a verb may read: 25 of these fields are
  kDraw's, the three clear values are kClear's and GetClampReadColor is kReadback's alone,
  and reading a clear value under DrawArrays is Fatal{UnmigratedPipeInput} - correctly, and
  the poison caught exactly that in the verify build before this shape.
- GetViewport's rounding is exercised on (1.5, 2.5, 63.5, 32.25) and the expected literal is
  std::lround's answer - half away from zero - not the banker's rounding nearbyint gives.
- DynamicChunksCoverMagmasDynamicTailKey checks every GL-state input of DirectVulkan's
  ApplyDynamicDrawStateTail against the dynamic half, and records the one exception the
  design implies but no document states: ScissorTestEnabledMask is read by DynamicTailKey's
  scissorEnabled yet is PIPELINE state, because SetCapability(ScissorTest) calls
  BumpVersions(). Harmless - BumpVersions moves m_version too, so MGPDynamicState::Version
  still moves and the tail still re-runs - and asserted the other way round so a later
  table edit that demotes the mask is loud here.
- Replaces the contract commit's placeholder case, which existed only so the target had a
  test before this package filled it in. The four names are the same four in every build:
  in a pull build each is a visible SKIP, never a vanishing test.
2026-09-06 08:16:53 -04:00
swung0x48 810850b13a [Feat] (Pipe): derive every render-state PipeInputs field from the assembled working block instead of pulling it again from GLContext
- 29 of the 47 kDraw PipeInputs fields are pure functions of RenderStateParameters. Once
  bind_render_state and set_dynamic_state have assembled the working block - which IS
  PipeInputs::m_renderState - copying those fields out of GLContext a second time is the
  per-verb pull P2 exists to remove. The applier now derives them after any scatter.
- Each line is a transcription of the RenderState getter of the same name; GLContext's
  accessors are one-line forwards to those, so the derivation and the pull path answer the
  same question from the same bytes. Two are not field copies and are transcribed exactly:
  GetViewport (viewport 0 rounded with std::lround, because glGetIntegerv on float state
  rounds to nearest and truncating a 63.5-wide viewport would hand the backends a rectangle
  one pixel short), and IsCapabilityEnabled (the 35-way switch, including Blend ->
  BlendStates[0].Enabled, ScissorTest -> ScissorTestEnabledMask & 1 and the ClipDistance
  run). The indexed twin is the same for Blend[8] and ScissorTest[16].
- IsCapabilityEnabled could not have been written before the contract commit: DepthClamp,
  FramebufferSrgb and TextureCubeMapSeamless fell to `default: return false`, so three of
  the 35 answers were a compile-time constant rather than state.
- This departs from P1 brief D4's "no derivation logic is re-implemented in PipeInputs",
  deliberately: the alternative is to keep pulling those 29 fields per verb. The guard is
  the oracle P1 built - MOBILEGL_PIPE_VERIFY's compare-at-read re-reads every one of them
  from the live context at EVERY backend read and compares field-wise, so a transcription
  error is caught on the first draw that reads it, across 79 retraces and the
  integration-verify entries.
- The body sits in MGPipeApplyAccess, the struct PipeInputs already names as its friend, so
  no new friend and no new accessor per field. Pull build untouched: PipeApply.cpp compiles
  only under MOBILEGL_PIPE_PUSH.
2026-09-06 07:58:53 -04:00
swung0x48 9c6a8a25d8 [Feat] (Pipe): land the P2 contract - real storage for the three swallowed capabilities, the render-state chunk table and its subset hash, the in-process applier, the slot allocator, the subsystem bitmask and the residual ratchet down to 8
- FramebufferSrgb, DepthClamp and TextureCubeMapSeamless get real storage. All three fell
  to SetCapability's "not supported currently" arm and IsCapabilityEnabled's default:
  glEnable was swallowed and glIsEnabled lied, so DirectGLES' sRGB block and the
  DirectVulkan read points consumed a constant. The three Bools land in the three
  alignment bytes at [581, 584) between ColorMasks and ClearColor, so
  sizeof(RenderStateParameters) stays 1168 and NO existing offset moves - Espryt's
  kBlendSpanBegin/kBlendSpanEnd (312/536) and the whole chunk table depend on that.
- MGPipeRenderStateSpans.{h,cpp}: the pipeline/dynamic split, written in exactly one place.
  The rule is the only rule - a byte is pipeline state iff a public RenderState setter that
  calls BumpVersions() writes it - which makes G7's "the subset hash moves iff
  m_pipelineStateVersion moves" true by construction. 16 boundaries, all offsetof or
  sizeof, alternating dynamic/pipeline: 8 dynamic chunks / 772 bytes and 7 pipeline chunks
  / 396 bytes, partitioning [0, 1168) exactly, asserted at compile time.
  MGPipeComputePipelineSubsetHash is XXH64 over the seven pipeline chunks, seeded with a
  table version so a chunk-table change invalidates every persisted key.
- The pipeline subset is now a strict SUPERSET of the 24 members ComputePipelineStateHash
  hashed: 44 members, adding sample coverage, the front face, the provoking vertex, the
  scissor-test mask, the back polygon mode, eleven capability bools the hash never read and
  the three above. Demoting those setters to ++m_version instead would have changed
  MG_State semantics in the PULL build for the push path's sake. The hash runs only when
  m_pipelineStateVersion moves, which is exactly when Magma re-hashed before.
- PipeApply.{h,cpp}: the in-process applier, the server half of the P2 calls. The server's
  working RenderStateParameters IS PipeInputs::m_renderState, which is why DirectGLES'
  SyncRenderState is not one line changed and why the verify comparator stops being a
  tautology. Per-context CSO store indexed by slot, gen-validated; the residual block's
  capability bits are compared against the assembled block, so a capability a later call
  takes over and forgets to carry is Fatal{PipeResidualDiverged}.
  MGPipeDeriveRenderStateFields is a declared STUB - its 29 derivations are commit c1.
- SlotAllocator.{h,cpp}: the client's per-kind {slot, gen} allocator, free list plus
  high-water, first allocatable slot 1, gen bumping only on slot REUSE, a debug assert on
  gen wrap, the composite ShaderCso band held back, and a lifetimeId -> slot map per kind so
  a GL name never enters a key. In the contract because both Track H slices need it.
- ResidualValueBlock 1248 -> 8 bytes, one Uint64 of capability bits.
  RenderStateParameters retired to create/bind_render_state and set_dynamic_state, Pack to
  set_pixel_pack_state, the patch quintet to set_patch_state. gen_pipe.py now emits the
  member-by-member offsetof assertions the ratchet comment always promised.
- gen_pipe.py: PIPELINE_STATE_MEMBERS grows to the 44-member set in declaration order and
  PipeSpanTable.inc's "deliberately absent" block records the answers instead of the
  questions; Coverage.def gains MGP_COVERAGE_EMITTED_LIST (34 rows) and PipeFilled.inc
  gains kMGPipeFieldEmittedBy[], which is what lets the residual fill loop skip a field a
  P2 call now supplies. One more --self-test negative control covers the new list.
- MOBILEGL_PIPE_PUSH becomes a per-subsystem bitmask with named bits (0..6 migrated at P2,
  bit 63 the CSO-content-addressing negative control), defaulting to 0x7f in a push build
  and staying 0 in a pull build. New CMake option MOBILEGL_PIPE_LEGACY_MEMOS, ON, forced ON
  when MOBILEGL_PIPE_PUSH=OFF where it is the only arm. New Features.PipeHandleAbaControl
  under MOBILEGL_PIPE_PUSH, negative control C for HandleRecycleScenario.
- PipeStats gains CallClass::{RenderStateCsoMints, RenderStateCsoBinds} (csom / csob on the
  summary line), and they are PUSH-ONLY: growing the enum in the pull build would resize
  the counter arrays, the name table and FormatWindowLine for two counters that could never
  leave zero, and G1 admits no such resize.
- Four MG_Test/Pipe stubs plus their CMake registration, so the packages that own their
  contents never touch MG_Test/Pipe/CMakeLists.txt.

G1, pull build, symbol_report --threshold 0: 0 added, 0 removed, 0 renamed, 4 resized, and
every resize is attributed:
  RenderState::RenderState()                       1700 -> 1848 (+148)  the three {}
  RenderState::SetCapability(CapabilityInput,bool)   850 ->  927  (+77)  three switch arms
  RenderState::IsCapabilityEnabled(CapabilityInput)  239 ->  268  (+29)  three switch arms
  _GLOBAL__sub_I_DirectGLES.cpp                     1340 -> 1331   (-9)  the static
    initialiser of DirectGLES.cpp's `static RenderStateParameters
    g_syncedRenderStateParameters` re-scheduling around the three new default-initialised
    members. A shrink, and the only unforeseen entry; it is a direct consequence of the
    struct gaining members and touches no interface.
2026-09-06 07:45:07 -04:00
swung0x48 4826806881 [CI] (Workflows): run the test and apk lanes on every push to feat/disaggregated
- the MGPipe phases land as a series of pushes and each one needs the full lane; dispatching
  by hand after every merge is a step that gets forgotten
- both entries carry a remove-before-merging-to-dev note: dev's trigger set is what ships
2026-09-06 06:32:37 -04:00
swung0x48 e7a6a72f6a [Docs] (Disaggregated): record the P1 landing and what its verify lane found
- README status, the ROADMAP P1 row with the measured site and accessor counts, and the
  ARCHITECTURE correction from 293 to the 277 arrow sites the tree actually has
- MEASUREMENTS gains the P1 scale table, the two classes of finding the verify lane
  produced (nine missing fill rows, three fields a backend moves inside its own verb) with
  the push-on-mutation decision and its three hooks, and the acceptance numbers
2026-09-06 06:17:12 -04:00
swung0x48 62a7786184 [Fix] (Pipe, Purity): end a declared verb honestly, and gate the header MG_State now includes
- MGPipeLeaveVerb() bumps the serial and puts the current verb back to none, so a test that
  drives a backend helper directly stops declaring where it says it stops and a later
  unguarded read aborts as "<Field>@<none>" instead of naming an unrelated verb
- check_include_closure.py gains a fourth probe: F2 put MGP_NOTE_MUTATION into frontend
  mutators, so MG_State includes MG_Pipe/PipeMutation.h and that header must never reach
  back into MG_State, MG_Impl or a backend
2026-09-06 06:15:56 -04:00
swung0x48 ef6227e19b [Test] (Pipe): let a test that drives a backend helper directly declare the verb it stands in
- ScopedPipeVerb (MG_Test/ScopedPipeVerb.h): an RAII "as if we were inside verb X" object
  that runs the real MGPipeFillForVerb for the verb it names, so the eleven unit entries
  that construct a GLContext by hand and call a backend helper with no GL entry point in
  between stop reading an empty, unstamped PipeInputs block
- it weakens nothing: it fills exactly that verb class's may-read mask, so a read outside
  it is still Fatal{UnmigratedPipeInput} naming the field and the declared verb; leaving
  the scope re-arms the poison with a one-field kQuery fill, so one case's declaration
  cannot cover a later one when the binary runs as a single process
- placed the way MGP_FILL is placed in production: immediately before the backend call,
  after every frontend mutation it is meant to see; a second call after the test moved
  state is a second verb (Renew()), a helper of another class gets a nested scope
- no-op in the pull build, no test renamed, no test added, no production source touched
2026-09-06 06:01:41 -04:00
swung0x48 9bd6d39403 [Test] (Pipe): pin the push-on-mutation shape - a frontend write inside a verb refreshes the pushed field, and only its value
- AFrontendMutationInsideAVerbRefreshesThePushedField: fills DrawArrays, then
  does what UniformManager's fallback path does mid-draw (a SamplerObject filter
  change) and what a bind reached from inside a verb does
  (NoteTextureUnitTouched), and asserts the block still equals the live context
  for GetSamplingResolutionGeneration, GetTextureBindGeneration and
  GetMaxTouchedTextureUnit.
- AFrontendMutationInsideAVerbDoesNotDivergeAtRead: the lane failure end to end,
  under the armed comparator in a forked child - the read after the mutation
  must complete and the log must carry no Fatal{.
- TheMutationNoticeRefreshesTheValueButNotTheStamp: the notice must not restamp
  a field whose stamp the fill withheld (negative control B), and must not stamp
  a field the verb class never fills (the generation under a kQuery verb).
- Falsified: with the notice's body short-circuited, the first two fail (the
  read-side one by SIGABRT on Fatal{PipeVerifyDiffer,
  "GetSamplingResolutionGeneration@DrawArrays", where=read}) and the third
  stays green, which is what a guard case should do.
- The three names also exist as visible GTEST_SKIPs in the pull build, as every
  other case in this file does.
2026-09-06 05:32:25 -04:00
swung0x48 6b681c4a63 [Fix] (Pipe, State): refresh a pushed PipeInputs field when the frontend moves it inside a verb
- The verify lane aborted eight integration entries and two retrace cases with
  Fatal{PipeVerifyDiffer, "GetSamplingResolutionGeneration@DrawArrays",
  where=read}, always one line after "ResolveSamplerDescriptor: using fallback
  texture for unbound sampler". The backends write into frontend objects during
  their own verb - Magma synthesises a fallback texture for an unbound sampler
  and gives it a shape, materialises a queued clear, overrides a unit's sampler
  filter - and every one of those writes moves a counter MGP_FILL already
  copied, so the pushed block stops equalling the live context for the rest of
  the verb. That is a real divergence, not a harness artefact: the pull build
  reads the moved value and the push build reads the boundary one.
- Takes the findings' preferred option, push on mutation, over the volatile-in-
  verb class: it keeps the comparator's invariant ("the pushed block equals the
  live context at every read") literally true, keeps push semantics equal to
  pull, and is the shape P2's tracker needs. The fallback would have had to skip
  compare-at-read for the field, which is the one comparator arm that is real in
  P1 - it would have blinded the gate on the very field that found the bug.
- MG_Pipe/PipeMutation.h declares MGP_NOTE_MUTATION(Field), a no-op that
  includes nothing in the pull build; MG_Impl/Pipe/PipeFill.cpp defines the
  notice next to the filler it shares CopyField with. The notice refreshes one
  field's value when a context is live, a verb has been filled, and the field is
  in that verb class's may-read mask; it never touches the poison stamp, so a
  stamp MOBILEGL_PIPE_POISON_OMIT withheld stays withheld and a field the verb
  never filled stays Fatal{UnmigratedPipeInput} rather than being healed.
- The enumeration behind the three hook sites: of the ~40 backend->frontend
  write sites, only the texture family reaches a pushed value. Every path
  through them funnels into TextureState::BumpSamplingResolutionGeneration
  (SamplerObject::BumpVersion for the sampler setters,
  TextureObjectBase::BumpShapeVersion for AllocateStorage / SetInternalFormat /
  TruncateMipmapLevels / SetSamples / SetFixedSampleLocations),
  BumpTextureBindGeneration (a default texture becoming defined, delete-unbind,
  a unit's sampler object changing) or NoteUnitTouched (which also moves the
  touched-unit high-water mark), so the notice sits on the counters rather than
  on each writer and covers the whole family including writers added later.
  The buffer, program and VAO writes reach no pushed field: their objects are
  read back through O-class live references, not copied values.
2026-09-06 05:32:13 -04:00
swung0x48 80a6b39003 [Fix] (Pipe): fill the capability set on a texture op and a dispatch, and the shader blit's viewport, provoking vertex and buffer bindings
- The verify retrace aborted four cases with Fatal{UnmigratedPipeInput,
  "IsCapabilityEnabled@GenerateMipmap"} (x3) and "@DispatchCompute" (x1):
  Magma materialises a texture's queued clear inside both verbs
  (GenerateMipmap -> MaterializePendingClearForTexture,
  DispatchCompute -> PrepareStorageImageTextures -> the same), and the clear
  pre-compensates its colour against GL_FRAMEBUFFER_SRGB in
  VkClearManager::PreCompensateSrgbClearColor. Neither class named the field.
- Audited every class the same way rather than stopping at those two rows. Two
  more helper-program draws sit inside verbs whose class did not name what they
  read: GenerateMipmap takes GenerateDepthMipmapWithShader for a depth texture
  and BlitFramebuffer takes TryBlitToDefaultFramebufferWithShader for the
  default framebuffer. Both bind their helper's descriptors through
  BindProgramUniformBuffers, whose sampler resolver reads the draw framebuffer
  for its feedback-loop check and whose buffer-block resolvers read the frontend
  binding points; the blit additionally sets the dynamic viewport through
  ApplyGLViewportState -> ComputeGLViewport and picks its pipeline's provoking
  vertex through GetOrCreateBlitPipeline -> SelectProvokingVertexMode.
- kTextureOp gains IsCapabilityEnabled, GetFramebufferBindingSlot and
  GetBufferBindingPoint; kDispatch gains IsCapabilityEnabled; kBlitOrCopy gains
  GetViewportIndexed, GetDepthRangeIndexed, GetProvokingVertexMode and
  GetBufferBindingPoint. Every row carries the path it was derived from.
- Also unfolds the kReadback transform-feedback rows 9087f133 landed on one
  1200-column line back into the file's one-row-per-line shape; no row changes.
2026-09-06 05:31:50 -04:00
swung0x48 97b997d5da [CI] (Pipe): read the verify library's static symtab, grep only the arming lane's log, and stop the two retrace lanes overwriting each other's evidence
- both `nm -D --defined-only ... | grep -q MGPipe...` gates could never pass: the library
  is built CXX_VISIBILITY_PRESET hidden in every non-Debug configuration and the MGPipe
  entry points carry no export attribute, so the dynamic table holds none of them (0 of
  11930 exported symbols on this tree, while `nm --defined-only` finds MGPipeFillForVerb
  as a local `t`). build-linux-verify, and with it integration-verify and every
  retrace-verify entry, would have been red forever for a reason unrelated to the
  comparator. Both now read the static table, name what a miss means, and refuse a
  stripped artifact instead of reporting its silence as a missing symbol
- "Every verify process really armed" grepped one shared per-lane log that holds only the
  LAST process of 406 - and the last ambient entry is the poison control's parent, which
  forks, execve()s and waits without ever issuing a verb, so the step would have red a
  healthy lane while proving nothing about the other 405. It now greps the two
  VerifyArming. lanes' own logs, requires one per backend, and says what that establishes
- remove-artifact-clutter kept fixtures for failed `retrace (` jobs only, so a failed
  `retrace verify (` case lost the fixture needed to reproduce it; both prefixes now count
- the retrace negative control replayed OpenRA into the same case directory, so
  "Upload actual image" shipped the deliberately corrupted run's output under the good
  run's name. The verified output is put aside and restored before the verdict
- build-linux-verify regains build-linux's "Show installed toolchain" step, and its
  deliberate divergence (Release even under ACTIONS_STEP_DEBUG - a Debug build would flip
  visibility and arm the poison through a different #if arm) is written down
- the lane's scope is stated where it is run: every integration ENTRY under the
  comparator, not every configuration - `integration`'s second
  MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1 pass (186 entries here) is not affordable at
  the 5-10x the comparator costs, so that tier stays covered unverified by `integration`
2026-09-06 04:41:28 -04:00
swung0x48 7d80c9678e [Test] (Retrace): scan a verify retrace's log for the third MGPipe Fatal too
- the block looked for Fatal{PipeVerifyDiffer and Fatal{UnmigratedPipeInput only. A
  misspelt MOBILEGL_PIPE_VERIFY_CORRUPT / MOBILEGL_PIPE_POISON_OMIT reports
  Fatal{PipeVerifyBadKnob, and it was caught only because D2 makes that one abort the
  process - which is precisely what MOBILEGL_PIPE_VERIFY_FATAL=0, the supported triage
  configuration, takes away. A typo'd knob would have left a negative-control run
  looking healthy
- the FATAL_ERROR text now says what each of the three means and where the two
  vocabularies live, because that message is the whole diagnosis a `cmake -P` step gets
2026-09-06 04:41:28 -04:00
swung0x48 72aa9191b4 [Fix] (Tooling): gate symbol_report on all four buckets and on a .text that moved in either direction
- G1 is spelled "added == removed == resized == renamed == 0, .text delta 0", but
  gate_failures() took only added and removed and fired on text_delta > N: a pull build
  whose .text SHRANK, or whose functions were resized with zero net delta - the exact
  shape a null-guard or ternary rewrite produces - walked through
  `--threshold 0 --fail-on-symbol-set-change --fail-on-added-bytes 0` green
- --fail-on-symbol-set-change now covers the four buckets the report prints, and
  --fail-on-added-bytes 0 means byte-identical rather than "did not grow" (a positive
  budget keeps the one-sided meaning). --fail-on-text-delta is the explicit spelling for
  a run that wants no byte budget at all
- the gates read the threshold-0 buckets whatever --threshold says: --threshold is a
  report control, and a gate that read the thresholded resize list would have quietly
  weakened itself the day someone raised it. The run says so in its output
- the self-test drives the three shapes that used to pass (a shrunk .text at budget 0, a
  resize and a rename at zero net delta) from the same canned transcripts
2026-09-06 04:41:28 -04:00
swung0x48 1e3a74686f [Test] (Pipe): give the arming assertion a lane and a log of its own, and stop the poison child calling a sequence it never ran a success
- the arming case read the ambient lane's MOBILEGL_LOG_FILE_PATH, and that log is
  opened fopen(path, "w") by every process in the lane: with 406 entries per backend
  and CI running them -j 4, a whole-file read races a neighbour's bring-up, and the
  file that survives the lane holds only the LAST writer. Every other log-reading
  scenario in this suite (UnlocatedIoBlocks, the primgen reroute, the point-size
  demotion) is registered in a filtered lane with its own log for exactly that reason;
  PipeVerifyArmingScenario.Armed now follows them, in DirectGLES.VerifyArming. /
  DirectVulkan.VerifyArming., and skips anywhere MGITEST_PIPE_ARMING_LANE is unset
- what that can prove is written down where it is asserted: arming is a property of
  (this library, this environment) and these two processes share both with their ~400
  ambient siblings. A per-process census is not available through a shared log, and a
  comment that claimed one was the reason CI grepped a file that could not answer
- the re-exec'd poison child ran RunSequence() and then _exit(0) unconditionally, so a
  fatal assertion inside it - the shader failing to compile, say - returned before the
  draw and the glGenerateMipmap and still reported success: WithoutOmissionCompletes,
  the one green entry negative control B turns red, passed on a child that ran none of
  the sequence. It now exits HasFailure() ? 1 : 0, and checks glGetError() after the
  mipmap so a rejected sequence is part of the answer rather than stderr nobody reads
2026-09-06 04:41:28 -04:00
swung0x48 0fe7bf82d2 [CI] (Pipe): the third CI mode - a verify build, its integration and retrace lanes, and the two negative controls as always-on steps
- build-linux-verify is a second Release/INFO build with -DMOBILEGL_PIPE_VERIFY=ON,
  because the comparator is a compile-time option and does not exist in the shipped
  library. It refuses to ship an artifact whose libMobileGL.so does not export
  MGPipeVerifyInputs and MGPipeFillForVerb: a typo'd -D is not an error in CMake,
  and every lane below would then be green having compared nothing.
- integration-verify runs the suite with the comparator armed and then proves it
  armed twice over: --no-tests=error reds a build whose verify entries were never
  registered, and a step greps every pipe-verify-*.log for the arming line.
- The two negative controls are steps of that job, not a manual exercise: a gate
  that can only be shown to work by someone remembering to break it has already
  stopped working. Each passes when ctest FAILS, and each first counts its own
  selection - an empty selection also exits non-zero under --no-tests=error, and a
  control that passed because it ran nothing would be worse than no control.
- Control B targets PoisonOmissionScenario.WithoutOmissionCompletes, the only
  integration entry in the tree that calls glGenerateMipmap at all; the case no
  longer skips itself when the omission knob is set, precisely so that the control
  has a green entry to turn red.
- retrace-verify replays the eight "verify": true cases against the verify library,
  copied over build-linux/libMobileGL.so because build-retrace freezes that absolute
  path into every case, with an nm check that the swap happened and an inverted
  OpenRA step that must go red under MOBILEGL_PIPE_VERIFY_CORRUPT.
  remove-artifact-clutter now waits for it: it deletes the trace fixtures these jobs
  download.
- monolith-symbol-report is G1 as a job: two pull builds with identical flags and
  LTO off, the baseline named by a workflow_dispatch input, symbol_report.py with
  both hard gates, and a refusal of any MG_Remote symbol in the monolith. It is
  dispatch-only because its answer is about a baseline, not about this push.
- pipe-gates gains the two --self-test steps. Regenerating and diffing cannot see a
  structural check that silently stopped checking; a broken gate and a clean tree
  produce the same green. Its dirty-surface comment now says P2, which is where
  ROADMAP.md:18 puts the first mapping round.
2026-09-06 04:41:28 -04:00
swung0x48 416cd23c28 [Feat] (Tooling): give symbol_report.py the two hard gates G1 needs, with the report written first
- --fail-on-added-bytes was a reserved no-op that printed "this run stays
  informational"; it now exits non-zero when .text grew past the budget, and
  --fail-on-symbol-set-change joins it for the added/removed buckets. Together they
  are the spelling of P1's G1 ("the pull build is byte-identical"): --threshold 0
  --fail-on-symbol-set-change --fail-on-added-bytes 0.
- Default behaviour is unchanged: with no gate flag the tool prints its report and
  exits 0, which is what every existing caller and the informational
  monolith-symbol-report job expect.
- A gate fires AFTER the Markdown and JSON are written, never before: the report is
  the diagnosis, and a CI job that failed before uploading its artifact is one
  nobody can act on.
- The decision lives in a pure gate_failures(), so --self-test drives it from the
  same two canned transcripts as the buckets: each flag fires on the canned
  add/remove/+100 delta, each stays quiet when it was not asked for, and a
  tolerated budget is tolerated. A gate whose only test is a real build is a gate
  nobody re-tests.
2026-09-06 04:41:28 -04:00
swung0x48 5f8e8db1b9 [Test] (Retrace): make a retrace under MOBILEGL_PIPE_VERIFY prove it armed, and give the lane a label
- A retrace that exports MOBILEGL_PIPE_VERIFY=1 at a library configured without
  -DMOBILEGL_PIPE_VERIFY=ON is a no-op: the frames still match their goldens and the
  case reports green having compared nothing. run_trace_case.cmake now demands the
  evidence whenever the variable is set to anything but 0/false - mobilegl.log must
  exist, must carry "MGPipe: verify armed", and must carry neither
  Fatal{PipeVerifyDiffer nor Fatal{UnmigratedPipeInput. With the variable unset the
  script is byte-for-byte the old one.
- The Fatal scan is not redundant with the replay's exit status:
  MOBILEGL_PIPE_VERIFY_FATAL=0 is the supported triage configuration, and there a
  divergence is logged and counted rather than aborted, so the run would finish 0
  with its own report sitting unread in the log.
- LABELS retrace on both registrations: the lane was selectable only by regex, so
  `ctest -L retrace --no-tests=error` - the spelling that reds a lane which
  registered nothing - could not be written at all.
- "verify": true on eight cases (the five 180s cases plus minecraft-1.21.4-in-world,
  minecraft-1.21.4-fabric-sodium-in-world and improved-transparency-minecraft-26.3),
  and --format github-verify-matrix over that subset: 16 entries, against the full
  lane's 77. The verify build compares at every verb boundary and again at every
  accessor read, which the design budgets at 5-10x, so the per-push job runs the
  subset and the full sweep is a phase-exit / workflow_dispatch run.
- The flag is validated in the manifest loader, not at the matrix, so a non-boolean
  or a verify case excluded from CI is a loud error in every consumer instead of a
  subset that is quietly one case short.
2026-09-06 04:41:28 -04:00
swung0x48 bdf05514c3 [Test] (Pipe): the integration-verify lanes and their two always-on negative controls
- ARCHITECTURE.md 13.2-(2) asks for a third CI mode, and a third mode whose only
  evidence is "ctest was green" proves nothing: MOBILEGL_PIPE_VERIFY=1 against a
  library that never compiled the comparator in is a silent no-op that looks
  exactly like a clean pass. Six registrations, all under if (MOBILEGL_PIPE_VERIFY)
  and all labelled integration-verify, make both halves falsifiable - a
  mis-configured build registers nothing and --no-tests=error reds the lane, and
  PipeVerifyArmingScenario.Armed fails a lane whose library never printed its
  arming line.
- PipeVerifyArmingScenario.CorruptedFieldIsReported is negative control A (G4):
  its lane pins MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters with
  MOBILEGL_PIPE_VERIFY_FATAL=0 so the process survives its own divergence and can
  read the report back; the CI step that exports the same knob against the ambient
  lane, where FATAL keeps its default, asserts the other half - the abort.
- PoisonOmissionScenario is negative control B (G5), in two cases that cannot share
  a process because the knob is process-wide: the omitted (verb, field) pair must
  abort the glGenerateMipmap and NOT the draw before it, and the same sequence with
  the knob unset must complete with no Fatal at all.
- The sequence runs in a fork()+execve() of this same binary rather than a bare
  fork(): the fixture has already brought a context up, and a bare fork of a
  process holding a live Vulkan device inherits the driver's mutexes with no
  threads to release them - measured here as a 120s wedge on DirectVulkan against a
  clean pass on DirectGLES. The child gets its own MOBILEGL_LOG_FILE_PATH because
  the library opens its log with fopen(path, "w") and would otherwise truncate the
  file the parent is about to read.
- No ambient Verify. entry names MOBILEGL_PIPE_VERIFY_CORRUPT or
  MOBILEGL_PIPE_POISON_OMIT in its ENVIRONMENT property, because a property entry
  overrides the job environment for the names it lists: the two CI negative-control
  steps export those knobs into the job environment and must reach the processes.
  Every list appends MGL_ITEST_COMMON_ENV / MGL_ITEST_VULKAN_ENV for the same
  reason, so the vendor and ICD pinning survives.
2026-09-06 04:41:28 -04:00
swung0x48 bf8b39a867 [Refactor] (Magma): route every frontend read through MGB_CTX - 164 arrow sites sed'd, 49 non-arrow lines converted (43 asserts keep their meaning as MGB_CTX_LIVE); pull build byte-identical
- Every MG_State::pGLContext-> in the seven DirectVulkan TUs becomes MGB_CTX->
  (DirectVulkan.cpp 12, BackendObject_DirectVulkan.cpp 2, UniformManager.cpp 14,
  VkClearManager.cpp 1, VkRenderPassManager.cpp 3, VkTextureManager.cpp 2,
  VulkanRenderer.cpp 130 occurrences on 127 lines); each TU includes
  <MG_Pipe/PipeInputsSwitch.h> right after its MG_State/GLState/Core.h include
  (VkRenderPassManager.cpp after its include block, it never included Core.h).
- The 43 MOBILEGL_ASSERT(pGLContext) / (pGLContext != nullptr) lines become
  MOBILEGL_ASSERT(MGB_CTX_LIVE, ...): identical in every INFO build (the macro is
  empty there) and still a null-context assert in a DEBUG build.
- The six code-bearing guards are like-for-like pointer tests in the pull arm:
  if (MGB_CTX_LIVE) at the two InvalidateCompileEnv sites, MGB_CTX_LIVE in the
  XFB query counter condition and the ternary that snapshots the paused-primitive
  counter, !MGB_CTX_LIVE in BeginXfbCaptureForDraw, MGB_CTX_LIVE && in the
  provoking-vertex resolve. No semantic rewrite: under push MGB_CTX_LIVE is simply
  "a context exists", the real meaning of these guards is P2's business.
- VertexInputStateFactory.h's comment stops naming pGLContext so purity gate C
  (grep -rc pGLContext MobileGL/MG_Backend/DirectVulkan) reads 0 in every file.
- The 12 SyncPersistentMappedRange and 3 SyncGpuWrites sites of the D10 table are
  untouched; PipeStats AddCalls literals unchanged.
- Proof on the pull build (Release/INFO, LTO off, vs feat/disaggregated@087685d1):
  symbol_report --threshold 0 -> 27799 symbols, 0 added / 0 removed / 0 resized /
  0 renamed, .text 10792579 -> 10792579 (+0); nm --defined-only name set identical;
  ctest -N name set identical (2334); unit 1466/1466; DirectVulkan integration lane
  427/427 on lavapipe (two ArmedWhenTheEnvironmentPinsItOn entries flake under -j 4
  exactly as on the baseline and pass serially). The push build compiles and links.
2026-09-06 04:41:28 -04:00
swung0x48 d4504e30f8 [Refactor] (Espryt): route every frontend read through MGB_CTX - 113 arrow sites sed'd, 9 non-arrow lines converted (the fb-slot cache keys on MGB_CTX_IDENTITY); pull build byte-identical
- P1 package B (BRIEF-P1 C.2): the four DirectGLES TUs include <MG_Pipe/PipeInputsSwitch.h> right after
  their MG_State/GLState/Core.h include (Managers.cpp after its Managers.h include) and spell every
  frontend read MGB_CTX->Accessor(...). In the pull build MGB_CTX is MG_State::pGLContext, so the
  code is the tree before this commit token for token; in the push build it is &gPipeInputs, the block
  the frontend fills at every verb boundary.
- 113 arrow occurrences on 113 lines went through the mechanical sed (DirectGLES.cpp 91, Managers.cpp 15,
  MultiDraw.cpp 5, Utils.cpp 2). The 9 non-arrow lines follow D9: Managers.cpp's five bare/compound
  guards and three ternary conditions become MGB_CTX_LIVE (UniquePtr::operator bool spelled out, so the
  pull build does not move); DirectGLES.cpp's GetFramebufferBindingSlotFast keys its static cache on
  MGB_CTX_IDENTITY (a const void* compare) instead of pGLContext.get().
- The cache refill loop dereferences MGB_CTX once into a local reference and reads the slots through it,
  instead of &MGB_CTX->GetFramebufferBindingSlot(i) per iteration as D9 spells it: a store into
  g_fbSlotCache (a pointer) may alias the unique_ptr's own pointer under clang's TBAA, so the per-iteration
  spelling re-reads pGLContext inside the loop and grows the three functions the loop is inlined into
  (SyncCurrentProgram +16, ForceBindCurrentFBO +9, BlitNamedFramebuffer +1; .text +32). Hoisting the
  dereference restores the single load and a zero .text delta.
- grep -rc pGLContext MobileGL/MG_Backend/DirectGLES reports 0 in every file; symbol_report --threshold 0
  against the 087685d1 baseline: 0 added / 0 removed / 0 resized / 0 renamed, .text +0, nm --defined-only
  set identical. The only bytes that move are __LINE__ immediates in RecordError sites after the inserted
  include line. The 8 SyncPersistentMappedRange and 3 SyncGpuWrites sites and the PipeStats AddCalls
  literals are untouched.
2026-09-06 04:41:28 -04:00
swung0x48 9087f13308 [Fix] (Pipe): let a readback fill the transform-feedback state its emulation reads
- the depth/stencil read emulation opens a ScopedEmulationDrawState that pauses an active
  capture around its own draw, so ReadPixels/GetTexImage read IsTransformFeedbackActive and
  IsTransformFeedbackPaused; without the two kReadback rows every emulated readback aborts
  with Fatal{UnmigratedPipeInput, "IsTransformFeedbackActive@ReadPixels"} once the Espryt
  sites are converted
2026-09-06 04:41:15 -04:00
swung0x48 44ffafb2dc [Test] (Pipe): pin the pre-fill window - every stamp of 0 is stale at serial 0, a read before any fill aborts naming "<none>", and the FATAL=0 child leaves through std::exit so the teardown summary is observed
- PipeCatalogue.PipeInputFieldsStartUnfilled asserted "unfilled" only after setting the serial to 1 by hand, stepping around the serial-0 window; it now asserts every field stale on a value-initialised state first (sticky fields included).
- PipeInputsTest.ReadingBeforeAnyFillAbortsNamingNoVerb: a live context, no fill, gPipeInputs.GetLineWidth() in a forked child; the parent expects SIGABRT, exactly Fatal{UnmigratedPipeInput, "GetLineWidth@<none>"}, no PipeVerifyDiffer and no arming line. In a verify build the child sets Features.PipeVerify first, the lane's shape.
- VerifyFatalOffLogsTheDivergenceAndContinues: the child _exit(0)ed, so VerifyState's destructor never ran and the "verify summary" line was covered only by a lane run; std::exit(0) runs it, and the parent now asserts "2 divergence(s) survived MOBILEGL_PIPE_VERIFY_FATAL=0".
2026-09-06 02:55:17 -04:00
swung0x48 12b57055b7 [Docs] (Pipe): record why the forwarders carry no poison check, the InHook re-entry guard, and the Index slot no FillPoints.def row can fill
- The seven F-class forwarders are the declared exception to D4's "every accessor body": a forward is a live call, not a stored value, and InvalidateCompileEnv is reached from backend initialisation before any verb has filled, where a check would be Fatal{...@<none>} on every start. Their sticky stamp is consulted by no accessor; the tests pin it through MGPipeInputFieldIsFresh directly. Written down so P2's tracker does not "fix" the missing check.
- MGPipeVerifyReadHook: InHook is what keeps a GetProgramForDraw-triggered backend re-entry from recursing into a second hook, and the whole-field re-read is a per-read cost to keep in mind when reading the verify lane's wall time.
- GetBufferBindingSlot(Index) is polymorphic on GLContext (the bound VAO's element-buffer slot) and null in the block by design: a push Fatal there is not a missing row. No backend reads it today.
2026-09-06 02:55:17 -04:00
swung0x48 d0ff647581 [Fix] (Pipe): re-arm the verify comparator when any of its three knobs changes, not only when PipeVerify does
- ArmVerify latched PipeVerifyFatal and PipeVerifyCorrupt at the moment PipeVerify changed; a later change to either without a PipeVerify toggle was not seen, so 878db2c4's "re-parse when their value changes" held for one knob of three.
- The latch now keys on all three Features values. A lane loads Features before its first fill, so it still arms once per process; cost is two Bool compares and one String compare per fill, verify builds only.
2026-09-06 02:55:17 -04:00
swung0x48 30d72c5b4e [Fix] (Pipe): refuse a stamp of 0 as fresh on both branches - a read before the first fill is Fatal{UnmigratedPipeInput, "<Field>@<none>"}, not default storage
- MGPipeInputFieldIsFresh (gen_pipe.py gen_filled, emitted into PipeFilled.inc) compared FilledGen == CurrentVerbSerial for a non-sticky field; before the first MGPipeFillForVerb both are 0, so 55 of the 63 fields read as fresh and served their default-constructed storage silently, with no log line and no abort. Only the four raw-pointer O-class accessors tripped, through their null-base checks.
- D6 says the serial starts at 1 so that FilledGen == 0 means never filled, and names the window "<Field>@<none>"; the predicate now refuses a stamp of 0 before consulting the sticky branch or the serial, so the window is the poison's case as documented. This is the window E's risk table expects the verify lane to find (an init-time read, the first link, anything reached from eglMakeCurrent).
- Reproduced with the reviewer's pre-fill program (a live context with line width 7, no fill, gPipeInputs.GetLineWidth()): stored=0, rc=0 before; rc=134 with exactly Fatal{UnmigratedPipeInput, "GetLineWidth@<none>"} after, with and without Features.PipeVerify set first.
- The compare-at-read hook arms at the first fill and cannot see this window either; a static_assert next to it pins that a verify build always carries the poison, which is what covers the reads before arming.
2026-09-06 02:55:17 -04:00
swung0x48 d9f4698d98 [Test] (Pipe): give every PipeInputsTest process its own log file, and cover the compare-at-read arm, the three knob parsers and VERIFY_FATAL=0 with forked children
- gtest_discover_tests runs each case as its own process; under ctest -j the five shared one fixed log path, each main() unlinked it and each fork parent re-read it by path, so a sibling's Fatal line or unlink landed in another case's assertion (red 40/40 at -j 8, for a reason unrelated to the poison). The name now carries the pid and the file is removed on the way out; a forked child inherits the path on purpose.
- MutatedFieldIsNamedAtRead: the first falsifier of MGPipeVerifyReadHook - the child arms verify, fills DrawArrays, reads GetLineWidth (completes), mutates the live context's line width and reads again, and the parent expects SIGABRT with Fatal{PipeVerifyDiffer, "GetLineWidth@DrawArrays", verb=<serial>, where=read} and no where=entry.
- PoisonOmitKnobArmsTheOmission / BadPoisonOmitKnobIsFatalNamingTheKnob / VerifyCorruptKnobNamesTheFieldAtEntry / BadVerifyCorruptKnobIsFatalNamingTheKnob / VerifyFatalOffLogsTheDivergenceAndContinues: the knob parsers through MG_Config::Features, their arming lines, the exact Fatal{PipeVerifyBadKnob} text, and a FATAL=0 run that logs two divergences at consecutive serials and exits 0.
- OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale now loops every field: fresh iff in kTextureOp's mask and not the omitted one, so the name is true.
- The fork/waitpid/log-delta shape is one RunInChild helper; every case is still a visible skip where its switch is off.
2026-09-06 02:31:56 -04:00
swung0x48 878db2c405 [Fix] (Pipe): re-parse the POISON_OMIT and VERIFY knobs when their Features value changes, not once per process
- The two parsers (ParsePoisonOmissionKnob, ArmVerify) latched on the first fill, so the only way to reach them was a lane that loads MG_Config::Features before any fill; no unit test could exercise the parse, the arming lines or Fatal{PipeVerifyBadKnob}.
- The latch now keys on the value: a lane still parses once (Features is loaded before the first fill), while a forked test child that sets Features after its parent filled gets its own parse. An empty omission value never clears an omission armed through MGPipeSetPoisonOmission.
- Re-arming resets the CORRUPT field so a stale corruption cannot outlive the knob that named it.
2026-09-06 02:31:56 -04:00
swung0x48 77ecde1524 [Fix] (Pipe): refuse an eighth sticky row at compile time - the forwarded count equals the generated sticky count
- kMGPipeForwardedFieldCount = 7 was a hand-written twin of the generated kMGPipeInputStickyFieldCount, tied only by PipeCatalogue.StickyFieldsAreExactlyTheSeven; a static_assert in the header makes a PipeFields.def sticky row without a forwarder a build error instead of a test failure.
2026-09-06 02:31:56 -04:00
swung0x48 510ecd9293 [Fix] (Impl): move the DeleteSync fill of the orphan sweep inside its null-entry guard
- D7 says a verb whose table entry is null never bumps the serial; the sweep's fill sat before `if (backendDeleteSync && syncObject->backendHandle)`, so a backend without DeleteSync, or a sync without a backend handle, bumped once per orphan with nothing reading the fill.
- The sibling sweep in GL_Query.cpp (DeleteBackendQuery) already fills inside its guard; this makes the two the same shape and leaves the declared list of guarded-expression sites at nine.
- Pull build unchanged: MGP_FILL is ((void)0) there.
2026-09-06 02:31:56 -04:00
swung0x48 440d3c5253 [Test] (Pipe): pin the P1 poison and verify shapes - 63 fields, 69 verbs, the seven sticky fields, an omitted GenerateMipmap field leaves exactly that field stale, a corrupted snapshot names its field, reading an unfilled field aborts with SIGABRT
- PipeCatalogueTest (header-only, every build): VerbTableIsTheFunctionTable (69 verbs, 9 non-empty classes, the seven sticky bits in every class mask, D7's edges), StickyFieldsAreExactlyTheSeven, FloatVectorsCompareBitwise (a NaN FloatVec4 equals itself, -0.0f differs from 0.0f, a differing BlendStates[3] names RenderState then BlendStates), SixValueStructsHaveFieldLists (69 payloads, PixelStoreParameters and MGHostSpan compared member-wise with Pad0 ignored).
- PipeInputsTest, a new unit target linking the static library with its own main() that points MOBILEGL_LOG_FILE_PATH at a temp file: a fake GLContext, the real filler and accessors. OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale (G5 layer 1), ReadingAnOmittedFieldAbortsNamingTheVerb (G5 layer 2: fork, the child reads the omitted field after a draw and a sibling read that must not abort, the parent expects SIGABRT and the exact Fatal line and no @DrawArrays), ReadingAFilledFieldCompletes (the sibling without the omission: _exit(0), no Fatal), CorruptedSnapshotFieldIsNamedWithItsSerial (G4 at block level: clean compare true, a corrupted GetRenderStateParameters is named, its stamp is the fill serial, a corruption outside the mask is not seen), EveryVerbFillsItsClassAndNothingElse (after each of the 69 fills a field is fresh iff its class bit is set).
- Every PipeInputsTest case is a visible GTEST_SKIP in a pull build and the poison/verify cases skip in a push build without them; the ctest name set is the same in all three configurations (additions only: 9 names).
2026-09-06 02:06:37 -04:00
swung0x48 83b16561c2 [Feat] (Pipe): give the six value structs G4 field lists and assert the lists cover their members - the memcmp fallback is now a compile error, floats in vector types compare bitwise
- PipeFields.def gains MGP_FIELDS_RenderStateParameters (65 members), PixelStoreParameters (8), PerBufferBlendState (7), StencilFaceState (7), DynamicBackendParameters (85) and MGHostSpan (Ptr, Seg, Size, Offset), all appended to MGP_VERIFY_PAYLOAD_LIST: kMGPipeVerifiedPayloadCount 63 -> 69, and ResidualValueBlock / MGPPixelPackState / MGPCaps are now compared field by field all the way down.
- gen_verify: MEMCMP_FALLBACK_TYPES is empty and the generic MGPipeFieldEqual's last branch is static_assert(sizeof(T) == 0) - a struct without a field list is a compile error, not a padding false positive; Array<T, N> gets an element-wise overload, and a VecBase-derived vector (FloatVec4, IntVec4, BoolVec4...) is detected by a probe and compared bitwise over its data, because VecBase::operator== is IEEE == and a derived-to-base overload would lose resolution to the exact-match generic template.
- check_field_lists_cover_struct_members(): for every payload in MGP_VERIFY_PAYLOAD_LIST, parse `struct <Name> {` out of MGPipeTypes.h / MGPipeValueTypes.h / MGPipeHostSpan.h / BackendObject.h (comments and strings masked, statics, functions, nested types and Pad<n> members excluded) and refuse a member without an F(...) or an F(...) that is not a member; runs in both modes, so it is a pipe-gates gate.
- scan_live_accessors(): every MGB_CTX-> / pGLContext-> read under MG_Backend must have a Coverage.def row (rows nobody reads are printed: today only the dead GetBoundTransformFeedbackName).
- --self-test: six negative controls (struct member without F, F without member, payload without struct, verb missing from FillPoints.def, verb outside GLFunctionsTable, field row naming a non-accessor) that must each trip, plus a positive control; zero trips is itself an error.
2026-09-06 02:02:07 -04:00
swung0x48 275dd3edb4 [Feat] (Pipe): the MOBILEGL_PIPE_VERIFY shadow comparator - a per-verb entry compare over the fill set and a compare-at-read in every accessor, first differing field and verb serial, fatal by default
- Entry compare: at the end of MGPipeFillForVerb a file-static second PipeInputs is filled by SnapshotFromGLContext (the branch that survives P13) over the same class mask, MOBILEGL_PIPE_VERIFY_CORRUPT perturbs one field of that snapshot arm, and MGPipeVerifyInputs compares every field in the mask through MGPipeInputsFieldEqual (V by G4's MGPipeFieldEqual, O by identity, F equal by definition). Both arms come from the same context at the same instant, so this arm is tautological until P2 - the CORRUPT knob keeps it falsifiable.
- Compare-at-read: MGP_INPUT_VERIFY_READ now calls MGPipeVerifyReadHook(*this, field, i0, i1), which re-reads the whole field from the live context into a scratch block and compares it against the stored value on the live block only - a superset of "the same indices"; the indices decorate the report. This is the arm that is real in P1.
- Reporting per D8: MGLOG_F("MGPipe: Fatal{PipeVerifyDiffer, \"Field@Verb\", verb=<serial>, where=entry|read}") then abort unless MOBILEGL_PIPE_VERIFY_FATAL=0, which counts and summarises at teardown with MGLOG_E; arming logs "MGPipe: verify armed - 63 fields, 69 verbs, fatal=N" once, the knobs acknowledge themselves, an unknown field name is Fatal{PipeVerifyBadKnob}; a push build without the comparator answers MOBILEGL_PIPE_VERIFY=1 with one MGLOG_W_ONCE.
- MGPipeVerifyInputs carries default visibility so the retrace-verify job's nm -D probe can prove the verify library was the one swapped in; pointer corruption flips low bits instead of nulling (a null pointer already null was invisible to the compare), a SharedPtr becomes an aliasing pointer with no control block; CurrentVertexAttributeValue gets its own bitwise equality.
2026-09-06 01:56:44 -04:00
swung0x48 a196ada4c1 [Feat] (Impl): call MGP_FILL before every GLFunctionsTable entry - 83 statements over 69 verbs, a no-op in the pull build
- Every call through gBackendFunctionsTable.GL in the seven MG_Impl TUs (Drawing 36, Framebuffer 11, Texture 8, Getter 3, Program 1, Query 18, Sync 6) is preceded by MGP_FILL(<Verb>); placed after every early return the call sits behind - the conditional-render check, the null-entry guards, the loop bodies - so a verb whose entry is null on this backend never bumps the serial.
- Seven calls sit on a continuation line of a guarded expression (GetQueryResult64 x2, BeginXfbPrimitivesQuery, BeginTimeElapsedQuery, QueryCounterTimestamp, IsTimerQuerySupported, GetSyncStatus) and two more fold the null guard into the same expression (IsQueryResultAvailable, ClientWaitSync's guarded return); there the fill precedes the statement, so a null entry bumps the serial once with nothing to read it - harmless for the poison, recorded for the record.
- Each TU includes <MG_Impl/Pipe/PipeFill.h> after its last MG_State/MG_Backend include; under MOBILEGL_PIPE_PUSH=OFF the macro is ((void)0) and the pull library is symbol-identical with a .text delta of zero.
2026-09-06 01:51:16 -04:00
swung0x48 3aa4d8af1f [Feat] (Pipe): fill PipeInputs per verb class from GLContext and stamp per-verb generations - a read of a field the verb did not fill is Fatal{UnmigratedPipeInput}
- MGPipeFillForVerb now walks kMGPipeClassFieldMask[kMGPipeVerbClass[verb]] and copies every field in it by calling the GLContext accessor of the same name (MGPipeFillAccess::CopyField, one switch over the 56 stored fields; the seven forwarded fields copy nothing), stamping each with the new serial; the sticky seven get FilledGen = 1 on the first live fill through the same mask walk, since every class mask carries them.
- MOBILEGL_PIPE_POISON_OMIT (<Verb>:<FieldName>) is parsed once on the first fill and MGPipeSetPoisonOmission is the programmatic form for the unit tests; the omitted pair keeps its value copy and loses only its stamp, so the omission is indistinguishable from a forgotten FillPoints.def row. An unknown name is Fatal{PipeVerifyBadKnob}; a non-poison push build acknowledges the knob with one warning because no stamp exists to omit.
- PipeInputs names one friend, struct MGPipeFillAccess, instead of two friend functions, and VisitStorage gains a const overload for the comparator that follows.
2026-09-06 01:48:23 -04:00
swung0x48 bf86b1ede6 [Feat] (Pipe): land the P1 contract - PipeInputsSwitch.h with MGB_CTX, the 63-field PipeInputs block with type-identical accessors, FillPoints.def and its G5b generator, the MOBILEGL_PIPE_PUSH/VERIFY options and the three verify knobs; pull build unchanged
- MG_Pipe/PipeInputsSwitch.h is the strangler switch (ARCHITECTURE.md 9.2): MGB_CTX is the
  live GLContext in the pull build and &gPipeInputs under MOBILEGL_PIPE_PUSH, so the pull
  arm's pGLContext spelling stays outside MG_Backend/ and purity gate C's grep.
- MG_Backend/MGPipe/PipeInputs.h holds one struct with every accessor a backend reads (63:
  the 61 Coverage.def rows plus GetBoundTransformFeedbackLifetimeId and
  HasOpenTransformFeedbackSpan), each keeping its GLContext name, parameters and return
  type so the site conversion is type-neutral; V fields are copied values, O fields are
  SharedPtr copies or pointers into the context, the seven F fields forward to the live
  context from MG_Impl/Pipe/PipeFill.cpp and are the only sticky ones (Coverage.def's
  MGP_COVERAGE_STICKY_LIST argues each: argument-keyed lookups and reverse-channel writes,
  never a version or generation counter).
- MG_Pipe/FillPoints.def is the verb table: one row per GLFunctionsTable function pointer in
  declaration order (69), nine classes and the may-read field rows; gen_pipe.py parses the
  struct and refuses a row set that is not exactly its member set, then emits
  generated/PipeFillPoints.inc (verb enum, class tables, per-class field masks with the
  sticky fields OR'ed in). MGP_FILL(Verb) in MG_Impl/Pipe/PipeFill.h is the fill point;
  MGPipeFillForVerb only bumps the serial, records the verb and stamps the sticky fields
  here - the per-class copies land in the next commit, the fill points in MG_Impl after.
- MOBILEGL_PIPE_POISON is derived once in PipeInputs.h from MOBILEGL_PIPE_PUSH and the DEBUG
  level, MOBILEGL_BUILD_DISAGGREGATED or MOBILEGL_PIPE_VERIFY (the tree has no
  MOBILEGL_DEBUG); under it every accessor is a read-side freshness check that aborts with
  Fatal{UnmigratedPipeInput, "Field@Verb"}.
- CMake: MOBILEGL_PIPE_PUSH and MOBILEGL_PIPE_VERIFY options (VERIFY forces PUSH on), the two
  new sources appended only under PUSH, the compile definitions; Config.h/ConfigLoader.cpp
  gain PipeVerifyFatal / PipeVerifyCorrupt / PipePoisonOmit under #if MOBILEGL_PIPE_PUSH so
  the pull build's FeaturesTable does not change size.
- Pull build proof: symbol_report.py against the 087685d1 baseline reports 0 added / 0
  removed / 0 resized / 0 renamed and a .text delta of 0; ctest -N names unchanged; gen_pipe
  --check clean; unit tests green in the pull, push and verify builds.
2026-09-06 01:35:45 -04:00
swung0x48 087685d19b [CI] (Purity): install libx11-dev for the include-graph-check job
- Includes.h defines VK_USE_PLATFORM_XLIB_KHR before vulkan.h, so the clang-mode probes and
  the negative controls need X11/Xlib.h on the runner; run 34008829271 failed on exactly that
2026-09-05 23:27:10 -04:00
swung0x48 5635e33ffe [Docs] (Disaggregated): record the P0.5 landing
- README status, the ROADMAP P0.5 row (measured outcome, the 8-includer correction, the
  DynamicBackendParameters exception) and the ARCHITECTURE note on which header gate A asserts
2026-09-05 23:22:15 -04:00
swung0x48 5d99ee435f [CI] (Purity): require both P0.5 closure probes now that the headers exist
- the ctest and the include-graph-check job pass --require-all, so a SKIP on
  MGPipeValueTypes.h or ProgramArtifacts.h is red from here on
2026-09-05 23:14:30 -04:00
swung0x48 b566bf4db9 [Fix] (Purity, Program, Pipe): close the review minors of the three P0.5 packages
- check_include_closure.py keeps the directory of a two-token -isystem, makes --require-all
  fail on a missing required header even when --probe narrowed the run, and removes its
  temp dir at exit
- ProgramArtifactsTest follows whichever STL branch the header pinned (#ifdef the size
  macro) instead of re-spelling the libstdc++ condition, and loses its stray executable bit
- MGPipeTypes.h's debt comment says what its closure still reaches (TextureEnum.h via
  BackendObject.h), which is why gate A asserts MGPipeValueTypes.h and not this header
2026-09-05 23:14:30 -04:00
swung0x48 09d2bb11b7 [Feat] (Program): add the reflection-archive field tables and sizeof trip wires to ProgramArtifacts.h
- One VisitFields table per type (ARCHITECTURE.md:259), a free constrained template so
  the moved struct bodies stay verbatim and one table serves both the const (serialize)
  and non-const (deserialize) direction. LinkArtifacts::program is deliberately absent:
  it is null for every archived instance and must never be serialized.
- Trip wires: TypeFacts is pinned at 44 bytes on every ABI; the container-bearing four
  are pinned per standard library - libstdc++ 64-bit here (128/128/1056/88, measured on
  this build), the libc++ branch left inert for the integrator to pin from the NDK
  build, MSVC unasserted. A member added without a table entry changes the size and
  the assertion message sends the author to the table.
- ProgramArtifactsTest counts the tables (20/14/11/57/8; const and non-const walks
  agree, names distinct), proves constness passes through, and records every sizeof as
  a ctest property so a new toolchain's numbers are readable from any `ctest -V` log.
2026-09-05 23:11:00 -04:00
swung0x48 fee3902472 [Refactor] (Program): stop ProgramObject.h including SpvcSession.h - it names no spirv-cross or SPIRV-Reflect symbol
- The include line was the header's only match for spvc_*/SpvReflect*/SpvcMetadata/
  SpvcSession; it only ever forwarded <spirv_reflect.h> and the session type to the
  eight includers, every one of which builds without it (no consumer needed a direct
  include added).
- One less transpiler header behind ProgramObject.h, on the way to a ProgramArtifacts.h
  closure that stays clear of MG_Util/ShaderTranspiler/ (P0.5 gate A).
2026-09-05 23:11:00 -04:00
swung0x48 da249f30e2 [Refactor] (Program): extract ProgramArtifacts.h - move TypeFacts, ResourceReflection, XfbVarying, LinkArtifacts and SpirvArtifacts to namespace scope with in-class aliases so every existing spelling compiles unchanged; pure move
- P0.5 of the MGPipe disaggregation (ROADMAP P0.5, ARCHITECTURE.md:260): the five
  reflection types a link produces now live in a header that includes only
  <Includes.h> and <set>, so a future server-side consumer can name them without
  dragging ShaderObject.h / SpvcSession.h / the transpiler behind it.
- Struct bodies move verbatim, comments included, re-indented one level; no field is
  added, removed, reordered or re-typed. The two glslang-typed members
  (LinkArtifacts::program, uniformInitialValues) stay as they are (B.0 D5); the
  comment mentions of glslang types are respelled without the scope token so the
  include-closure gate's "glslang:: exactly twice" limit holds.
- kInvalidUniformOffset moves to namespace scope (SpirvArtifacts defaults to it);
  ProgramObject::kInvalidUniformOffset is defined from it, so the two cannot drift.
- ProgramObject keeps in-class aliases (fully qualified on the right-hand side) for all
  nine names, so none of the 8 includers nor any ProgramObject::X spelling changes.
- ProgramArtifactsTest pins the aliases as the same types (is_same_v) and TypeFacts as
  a 44-byte POD; its first include is the new header, so it is also the proof that the
  header is self-contained.
2026-09-05 23:11:00 -04:00
swung0x48 8566a288f8 [Refactor] (Pipe, State): extract MGPipeValueTypes.h - move the render-state, sampler and vertex value types and their enums out of MG_State/GLState so MG_Pipe no longer reaches RenderState.h; pure move, member order and namespaces unchanged
- ROADMAP P0.5 / ARCHITECTURE.md value-header manifest: MGPipeTypes.h embedded
  RenderStateParameters and PixelStoreParameters through RenderState.h, which drags
  FramebufferObject.h and the whole texture/renderbuffer/sampler chain into MG_Pipe;
  purity gate A (no MG_State/MG_Impl/MG_Backend/MG_Remote in the closure) could not
  be armed for anything in MG_Pipe while that include existed.
- MGPipeValueTypes.h is a verbatim cut, comments included: the eleven RenderState.h
  enums (all of them - a split would be the maintenance trap), PixelStoreParameters,
  PerBufferBlendState, StencilFaceState, RenderStateParameters (member order
  untouched: DirectGLES' offsetof spans and PipeSpanTable.inc name the members),
  the six SamplerObject.h enums and SamplerParameters (BorderColorForm stays Uint8,
  it sets the tail padding), and VertexAttribute / VertexBufferBindingPoint /
  VertexAttributeVersion, which keep namespace MobileGL::MG_State::GLState with a
  forward-declared BufferObject so no mangled name changes.
- MAX_DRAW_BUFFERS becomes inline constexpr kMGMaxDrawBuffers in namespace MobileGL
  and FramebufferObject::MAX_DRAW_BUFFERS is defined from it, so the eighty existing
  spellings keep working and the two cannot drift. No other constant is added.
- The four MG_State headers become forwarders (include the value header, keep their
  class definitions); RenderState.cpp gains a direct FramebufferObject.h include
  because it spells FramebufferObject::MAX_DRAW_BUFFERS and only ever got that
  header transitively. No other TU lost a transitive include: the full build
  (Release, clang, tests + integration tests) passed without touching anything
  under MG_Backend, MG_Impl or MG_Util.
- DynamicBackendParameters does NOT move (SizeT members and TextureTarget-taking
  member functions make that a type change, not a move); MGPipeTypes.h keeps its
  BackendObject.h include and the debt comment now says so, which is why gate A
  asserts MGPipeValueTypes.h rather than MGPipeTypes.h.
- New trip wires in the header: trivially-copyable + exact sizeof for
  PixelStoreParameters/PerBufferBlendState/StencilFaceState (28), SamplerParameters
  (100), VertexAttributeVersion (6), RenderStateParameters (1168, standard layout,
  BlendStates before LogicOp, BlendStates sized by kMGMaxDrawBuffers). Their runtime
  twins ValueTypeLayoutsArePinned and the carrier check
  ResidualBlockIsExactlyItsTwoValueStructsPlusPatchTail (Pack at 1168,
  CapabilityBits at 1200) are added to PipeCatalogueTest without a new include.
- gen_pipe.py's "field lists of their own in P0.5" comment now says P1 (the
  comparator needs std::array<struct> support first); PipeVerify.inc regenerated.
- Verified: ctest -L unit 1460/1460 and -L integration-gpu green; ctest -N names
  a superset of feat/disaggregated@6672778b (two added, none lost); one definition
  per moved type; the -H closure of the new header contains no MG_State/MG_Backend/
  MG_Impl/MG_Remote header and the header compiles alone; nm --defined-only -S
  against the base libMobileGL.so: 0 added / 0 removed / 0 resized, .text
  byte-identical, the only differing bytes are the build-id and two stamp strings.
2026-09-05 23:11:00 -04:00
swung0x48 2318f6ae44 [Tooling] (Purity): add scripts/symbol_report.py - per-symbol nm/.text attribution between two libMobileGL.so builds
- P0.5's acceptance gate requires every `nm --defined-only -S` delta to be explainable
  per symbol, and nothing in the tree reads nm or size today.
- The problem the tool exists to solve: de-nesting a type renames every mangled name
  that mentions it, including inside template arguments, so a raw nm diff of a pure move
  looks catastrophic. --strip-scope 'A::B::C::' rewrites 'A::B::C::X' to 'A::B::X' on the
  demangled name before comparing, which folds those into a renamed-only bucket - same
  normalised name, byte-identical size - and leaves the real churn visible.
- Buckets sorted by |delta|: removed, added, resized, renamed-only, unchanged, plus
  .text/.data/.bss/Total from `size --format=sysv`; --only-names narrows the listing,
  --markdown/--json write the report the integrator pastes into the merge commit.
- Always exits 0 (this is informational, ARCHITECTURE.md:507); --fail-on-added-bytes is
  accepted and documented as reserved for the day it becomes a hard gate.
- The docstring carries the guard rails a reader would otherwise supply by assumption:
  same CMAKE_BUILD_TYPE (the visibility presets differ per configuration), LTO off on
  both sides, same compiler - and every report prints both paths and their byte sizes.
- --self-test runs two canned nm/size transcripts through the same parser and bucketer
  and pins all five buckets, including that a de-nested member folds to renamed rather
  than to an added+removed pair.
2026-09-05 22:51:32 -04:00
swung0x48 fe3dc1dde8 [CI] (Purity): add scripts/check_include_closure.py - the -H include-closure gate for the P0.5 headers with an always-on negative control, wired as a unit ctest and the include-graph-check job
- ROADMAP P0.5 asks for a CI assertion on the include closure of the two headers the
  phase extracts, and ROADMAP.md:7 asks every gate to be able to fail for the reason it
  exists. `nm --undefined-only` cannot express either: a header that is included but
  whose types are never named leaves no symbol behind, and "included at all" is exactly
  the coupling P1 and P7 have to sever. The preprocessor's own `-H` transcript can.
- Three probes, coded against the fixed path contract of the P0.5 brief so this package
  lands before the headers do: value-header (MG_Pipe/MGPipeValueTypes.h: no MG_State/,
  MG_Impl/, MG_Backend/, MG_Remote/), artifacts-header (ProgramArtifacts.h: no
  ShaderObject.h, ShaderTranspiler/, Config.h, MG_Backend/, BufferState/, ProgramState/
  Shader*, plus a budget of two `glslang::` tokens for the two members D5 keeps
  verbatim) and wire-header (ITransport.h must not reach Includes.h - green today, so
  the gate has a live probe from its first commit).
- The forbidden sets say nothing about glslang, spirv-cross or vulkan on purpose:
  Includes.h pulls all three unconditionally and both new headers are allowed
  <Includes.h>, so a textually glslang-free closure is unsatisfiable by construction.
  P7 measures that with `nm -D | grep glslang` on the server binary instead.
- Two modes because they check different things. Text mode walks literal #include lines,
  needs no compiler and no submodules, and is what the ctest runs (the CI `test` job's
  runner has neither); clang mode is the arbiter, and adds a -fsyntax-only pass proving
  the header is self-contained. `--mode both` additionally fails on a disagreement
  between the two violation sets, so text mode's blindness to `#if` cannot hide a hit.
- -H parsing normalises before matching (today's transcripts contain
  TextureState/../SamplerState/SamplerObject.h) and accepts only `^\.+ ` lines, which
  discards the "Multiple include guards may be useful for:" paragraph g++ appends.
  Both are pinned by a canned-transcript check inside --self-test.
- D9 skip semantics: a probe whose header does not exist prints SKIP and is counted, and
  --require-all turns every SKIP into a failure. That is what lets the gate land first
  and still stops an all-SKIP run from passing for free once the headers exist; the
  integrator flips --require-all on after all three P0.5 packages land.
- --self-test is always on in both the ctest and the CI job: it synthesizes its TUs in a
  tempdir (it never touches a tracked file) and requires a negative control that does
  not depend on P0.5 at all - MGPipeHandles.h plus RenderState.h checked against the
  value-header list - to report RenderState.h as a depth-1 violation in every enabled
  mode. Zero trips anywhere is an ::error:: and exit 1, because a gate that cannot go
  red is not a gate. Controls 2 and 3 arm themselves as the two headers appear.
- Registered as MobileGLPurity.IncludeClosure with LABELS unit so `ctest -L unit` runs
  it, and with no ENVIRONMENT property, which would replace the job env wholesale.
2026-09-05 22:51:32 -04:00
swung0x48 6672778b80 [Merge] (dev): merge dev@50fb1343 into feat/disaggregated
- brings the write-map landing into GPU-resident stores, the open-ended fp64 storage block flattening, the idle-based CTS chunk timeout and the MSVC /WHOLEARCHIVE test link
- verified on the merged tree: unit 1458, integration 868, Wire 54, retrace 79/79
2026-09-05 22:13:21 -04:00
swung0x48 6e0e3df372 [Docs] (Disaggregated): point the history note at commit hashes only
- the retired drafts stay in git history; the README no longer names or characterises them
2026-09-05 21:58:28 -04:00
swung0x48 bee22f9d26 [Docs] (Disaggregated): separate the dirty-surface totals from the immediate-publish subset in MEASUREMENTS
- the sentence read as if 836 of the 92 immediate calls were RecordError; 836 is RecordError's share of all 926 calls, and most of the 92 immediate ones are RecordError
2026-09-05 21:57:11 -04:00
swung0x48 8952b14024 [Docs] (Disaggregated): rewrite the MGPipe plan into a design and architecture set - README, ARCHITECTURE, ROADMAP, MEASUREMENTS - and retire PLAN.md and REVIEW.md to git history
- README.md: what MGPipe is, the one-paragraph architecture, the P0 status line, the file and code map, and the commit range where the design competition and the three adversarial review rounds live
- ARCHITECTURE.md: the design as decided, one reason per decision - handles and generations, the 71-call catalogue by class with flags and cap bits, record and payload conventions, the tracker, texture subdata and dirty ownership, shader state and the P0.5 header extraction, the reverse channel, the backend strangler, the server side and the index host mirror, the transport as landed in MG_Remote, the persistent-map tiers as measured, roundtrips, present and threads, process and platform delivery, build shapes and purity gates, the five-part verification gate, and the knob tables marked landed versus planned
- ROADMAP.md: P0..P13 as one table of what lands, the gate and the dependency, the two tracks and milestones, the day-43 GO/NO-GO checklist with both exits, the re-baseline checkpoints, and the questions still open after P0
- MEASUREMENTS.md: spike A on both devices, the spike B tier matrix, the four-trace boundary-counter baselines on both backends, the desktop and corpus facts, and the harness traps with the exact commands
- every file:line kept is verified at 458ccde1 and the citation lint is clean; everything else cites a symbol plus a file
- dropped on purpose: the v1/v2 revision archaeology, rejected alternatives, per-subsystem day estimates, the Feat/CS-Delta-IPC reuse audit and the reviewer back-and-forth
2026-09-05 21:56:31 -04:00
swung0x48 458ccde176 [Feat] (Metrics, Config): make the boundary-counter summary cadence a knob, because the device harness never reaches the teardown dump
- MOBILEGL_PIPE_STATS_PERIOD (default 120, clamped to [1, 1000000]) sets the frames per
  summary line; Init() latches it and a zero falls back to the default
- the trace APK's replay never tears MobileGL down, so MOBILEGL_PIPE_STATS_FILE never
  fires on device and a fixture shorter than the period (create-indirect) reported nothing
- PipeStatsTest pins the latch and the zero fallback
2026-09-05 21:21:54 -04:00
swung0x48 901d48a678 [Fix] (MGPipe, Metrics, Config, CI): exchange the per-frame stats instead of racing a store, carry the three uncarried table entries, drop the inline host span from a buffer range, spell the buffer subdata range, and close the small gate holes
- S-1 PipeStats::OnPresent read each frame accumulator and then store(0)'d it; a Bump from a staging thread landing in between was lost from the Tracy plot and from every frame. Each accumulator is now exchange(0, relaxed) and the exchanged value is what is plotted, so every add lands in exactly one frame.
- T-3 FdPassing without MSG_CMSG_CLOEXEC (macOS, BSD) handed back descriptors that survived exec; every received fd now gets FD_CLOEXEC by hand under !MSG_CMSG_CLOEXEC. MSG_NOSIGNAL is defined to 0 where the platform lacks it (FdPassing.cpp, Doorbell.cpp) and SO_NOSIGPIPE is set on the socketpair and on a SocketDoorbell's descriptor where it exists, so a write to a hung-up peer is EPIPE rather than a fatal signal.
- T-4 the missing-flatbuffers fallback wrote OFF into the cache with FORCE, so a plain re-configure after `git submodule update` stayed OFF silently. It is a normal-variable set now, shadowing the cache for that configure only; verified by hiding flatbuffers.h, configuring with ON (warning, transport off, cache still ON) and re-configuring plainly with the header back (transport ON).
- P-2 three LIVE GLFunctionsTable entries had no carrier: GetGpuTimestampNs (glGetInteger64v(GL_TIMESTAMP), a synchronous server answer), QueryCounterTimestamp (glQueryCounter, a one-shot stamp, not a begin/end pair) and WaitSync (the GPU-side wait FenceWait's client wait does not express). QueryTimestamp (MGPTimestampRequest, kCtxQuery, kReplySlot), QueryCounter (MGPQueryDesc with Kind = GL_TIMESTAMP, kCtxQuery) and FenceWaitServer (MGPFenceWait, kScreen) are APPENDED at the end of PipeCalls.def because the opcode is the position: SetSwapInterval stays 68, the three take 69-71, and PipeCatalogue.LateArrivalsAreAppendedWithoutRenumbering pins that. Header counts 71 (screen 11, query 8); the seven generators regenerated.
- P-3 MGPBufferRange inlined a 32-byte MGHostSpan into every range of every class - dead space on every SSBO, atomic-counter and XFB range, and D-B8 says not to freeze the named-UBO payload before the stage-ubo-named numbers exist. The range is 24 bytes now; the host spans are an optional second var-tail behind the ranges, announced by MGPShaderBuffers::HostSpanCount (0 or Count), with set_shader_buffers keeping its kVarTail|kHostSpan flags. PipeCatalogue.BufferRangeCarriesNoInlineHostSpan pins the sizes, the flags and the comparator's view of the count.
- P-4 QueryEnvUint64 parsed with base 0 (a leading zero meant octal: MOBILEGL_PIPE_PUSH=010 read as 8) and accepted -1 as every bit set; it is decimal or explicit 0x now and a '-' anywhere is refused with the warning (smoke through the integration binary: -1 and 12abc warn, 010 and 0x10 parse). The CI stdio gate's alternation now also catches fprintf(stdout, puts( and std::cout/cerr; it is green over MG_Backend and MG_State. MGPSubData states how the buffer half expresses [offset, size): UnionBox.X / UnionBox.W with Target == Buffer, Y = Z = 0, H = D = 1, one record bounded at a 2^31-1 offset and 2^32-1 size beyond which the emitter splits (the same rule the ring's half-capacity bound already imposes); MGPipeSetSubDataBufferRange / MGPipeSubDataBufferOffset / Size are the only spelling and PipeCatalogue.SubDataBufferRangeRidesInTheUnionBox pins the encoding and its bounds. gen_pipe.py now refuses, in both modes, a call payload named in PipeCalls.def with no field list in PipeFields.def (the four memcmp-fallback member types are the documented exception); shown by dropping P(MGPSwapInterval), which exits 1 naming the payload.
- The MGPPixelPackState size assertion compared sizeof against itself; it asserts the literal 28 PixelStoreParameters measures.
- Verified: ctest -L unit green in both the default and the split configuration, gen_pipe.py --check clean with the generated files committed, nm --defined-only of the default libMobileGL.so has no MG_Remote symbol, and the full integration-gpu suite passes (the *IsActuallyArmedWhenTheEnvironmentPinsItOn family trips under -j 8 as documented and passes serially).
2026-09-05 21:19:00 -04:00
swung0x48 e8ee7b1a88 [Feat] (Backend, MGPipe): carry the six per-axis compute limits in DynamicBackendParameters so MGPCaps has every backend-owned indexed answer, and pin them against glGetIntegeri_v on both backends
- P-1: MGPCaps is DynamicBackendParameters by inclusion (plan B section 4.4.1), but that struct carried MaxComputeWorkGroupInvocations and no per-axis GL_MAX_COMPUTE_WORK_GROUP_COUNT / GL_MAX_COMPUTE_WORK_GROUP_SIZE - the six numbers that ARE the backend-owned indexed answers surviving the getter retirement (GL_Getter.cpp and CompileEnv.cpp ask GLFunctionsTable::GetIntegeri_v for exactly these, DirectVulkan answers them from VkPhysicalDeviceLimits), so the interface had a hole where its only genuine indexed carrier should be. DynamicBackendParameters now has MaxComputeWorkGroupCount[3] / MaxComputeWorkGroupSize[3] with the GL 4.3 minimums as the no-backend defaults; DirectGLES fills them from glGetIntegeri_v inside the loader's bracketed probe run (GLESCapabilities carries them, logged with the other limits) and DirectVulkan from maxComputeWorkGroupCount / maxComputeWorkGroupSize through the loader's SaturateToInt like every other limit. Raw driver answers, as the invocations limit is: the frontend floors them at the shared MIN_COMPUTE_WORK_GROUP_* minimums itself.
- The GetIntegeri_v table path is untouched, as is GL_Getter and CompileEnv behaviour: retiring the getter in favour of the caps is P0.5, and this only makes sure the caps have what P0.5 needs.
- PipeCalls.def's footer no longer claims that "only GL_COMPUTE_WORK_GROUP_SIZE is a real backend answer and it lives in MGPCaps": the six limits live in MGPCaps, and GL_COMPUTE_WORK_GROUP_SIZE is a frontend link artifact (ProgramObject::GetComputeLocalSize, what GL_Program.cpp answers from), which AdvertisedLimitsScenario.ComputeLocalSizeComesFromTheLinkedProgram already pins. The MGPCaps size assertion is a composition of sizeof(DynamicBackendParameters) and follows the struct.
- AdvertisedLimitsScenario.ComputeWorkGroupLimitsAreTheCapsBlocksAnswer pins, on both lanes: answerability, the GL 4.3 floors, vector/indexed agreement, INVALID_VALUE past axis 2, and - through the new Harness/BackendCapsPeek translation unit, which is the one place the module looks past the GL API - that max(caps, minimum) equals the live glGetIntegeri_v answer axis by axis. Shown live by halving each backend's caps copy: both lanes fail with "MGPCaps carries 512 but glGetIntegeri_v answers 1024". On Android the module links the shipping .so (hidden visibility), so the peek returns false there and only the GL-visible half runs. ComputeWorkGroupCapabilities.TakesEveryAxisFromTheIndexedQuery in BackendLoaderTest pins the DirectGLES loader half against the fake driver, per axis and above the initialisers.
- Verified: AdvertisedLimitsScenario 20/20 on DirectGLES and DirectVulkan (llvmpipe / lavapipe), BackendLoaderTest green.
2026-09-05 21:19:00 -04:00
swung0x48 1154f9a00d [Fix] (MG_Remote, Transport): give the inproc doorbell a death state so Shutdown can join a parked waiter, and bound a ring record at half the capacity so a refusal can never look like backpressure
- T-1: InProcessChannel::Close rang each CondVarDoorbell once and claimed that unparks a peer mid-frame. It does not. Doorbell::Wait consumes the one ring, re-tests a condition nothing published, finds the bell alive (CondVarDoorbell never overrode Dead(); Doorbell.cpp had no death state at all) and with kWaitForever parks again for good - so Shutdown could never join a server thread sitting in the design's own steady state (spun, set consumerParked, blocked; plan section 8.1 inheriting the earlier plan's 6.2a). CondVarDoorbell now carries an atomic death latch: Kill() sets it under the mutex and notify_all's, Dead() reports it, Park returns false at once on a dead bell (and the wait predicate includes it, so a Kill cannot slip between the test and the wait), and Close kills both bells instead of ringing them. Same shape as SocketDoorbell's EOF latch; Notify stays the ordinary wakeup.
- T-2: RingProducer::Reserve refused only total > capacity, but a record with capacity/2 < total <= capacity is unplaceable at every head offset where neither the space to the wrap boundary nor the space before it holds it - even in an EMPTY ring, because a wrap pad costs spaceToEnd bytes on top of the record. Concretely: head offset 16 of an empty 256-byte ring, a 248-byte record; FreeBytes() says 256, Reserve says nullptr, forever, and a producer waiting for FreeBytes() >= 248 stalls with nothing logged. The bound is now capacity/2, which is exact rather than conservative (worst case 2*total-8 <= capacity-8), exposed as MaxRecordBytes() for the emitter to chunk against; the minimum ring is two headers so the smallest record still fits the bound. Ring.h states Capacity()/2 as the chunking bound and the G3 header comment in gen_pipe.py now states the chunking rule plan section 8.2 asks G3 to define (PipeWire.inc regenerated).
- Tests, each shown red with only the fix site reverted and green with it: InProcessTransportTest.ShutdownUnparksAWaiterWithNoDeadline (bounded join through a shared_ptr-owned waiter: 5 s red instead of a hung job; reverted it hangs and fails at 5051 ms), RingTest.RecordLargerThanHalfTheRingIsRefused (reverted, the 248-byte record is accepted), RingTest.RecordPlaceabilityDoesNotDependOnTheHeadOffset (the offset-0 vs offset-16 negative control), RingTest.HalfCapacityRecordFitsAtEveryHeadOffset (the positive half: the maximal record at all 32 head offsets of a 256-byte ring) and RingTest.RejectsARingTooSmallForTheSmallestRecord.
2026-09-05 21:19:00 -04:00
swung0x48 7ef7c7e543 [Fix] (Spikes): answer the tier question for DirectGLES too, make an OK mean bytes round-tripped through a real GPU access, and exercise T3 in the direction that makes it a tier
- plan-B §8.3 asks which tier `AcquirePersistentMap` lands in, but the probe only
  asked Vulkan. DirectGLES ("Espryt") reaches a persistent map through
  `glBufferStorageEXT` + `glMapBufferRange(PERSISTENT|COHERENT)`, not a
  `VkDeviceMemory` map, so a Vulkan-only answer decides nothing for that backend.
  Add a GLES leg to T1: the exported fd imported with `glCreateMemoryObjectsEXT`
  + `glImportMemoryFdEXT` + `glBufferStorageMemEXT`, then mapped
  PERSISTENT|COHERENT -- in-process first (isolates "GL can import this fd" from
  "the fd survives a process boundary"), then cross-process (new `t1gl` child).
  Drivers disagree about how the import must be phrased, so each attempt walks a
  ladder over {dedicated flag} x {import size = memory requirement or the fd's
  own size} x {buffer size} and reports the rung the driver accepted plus every
  rejected rung with its GL error -- a driver *preference* must never be reported
  as a missing capability. A driver that backs the storage but refuses
  PERSISTENT|COHERENT is reported separately from one that refuses the storage:
  that distinction is exactly T1 vs T2 for DirectGLES. The T0 GLES leg
  (`EGL_ANDROID_get_native_client_buffer` + `glBufferStorageExternalEXT` +
  persistent map, verified by `AHardwareBuffer_lock` on the client side) now
  reports every step's GL enum and requires the persistent flags for OK.
- the verdict was unfalsifiable: T1 reported PARTIAL when neither leg had moved a
  byte. Replace it with an explicit decisive-leg model -- OK only when every
  decisive leg round-tripped in both directions, PARTIAL when at least one did,
  FAIL otherwise with the failing step and its driver error named in `why:`.
  Every row now opens with a per-leg trace (`vkimport[D]=rt gpu[D]=rt`). The raw
  `mmap` leg is informational for opaque-fd (Vulkan forbids interpreting that
  payload outside the driver, so a refusal is conformant) and decisive for
  dma-buf, where a CPU mapping is the point of the handle type.
- T3 never ran the direction that would make it a tier: both ends were the
  importing process. Add `T3-client-memfd-server-import` (new `t3c` child) -- the
  client creates and writes the memfd, the server mmaps the received fd, imports
  the client's host pointer into a `VkDeviceMemory`, reads what the client wrote,
  writes back, and takes a GPU access on the client's memory, which the client
  then verifies through its own mapping.
- no route touched the GPU, so an OK proved only that a map call returned a
  pointer. Every tier row now takes a real GPU access before it can be OK:
  `vkCmdCopyBuffer` out of the shared allocation into private staging (mismatch =
  the GPU could not read what the peer wrote) plus `vkCmdFillBuffer` into it,
  queue-idle and an explicit host-read barrier, with the peer checking the filled
  region through its own mapping. VkCtx grows a queue and command pool for it.
- the device run executes in the `shell` SELinux domain, not the `untrusted_app`
  domain MobileGL runs in, and the two do not share dmabuf/gralloc rules. Print
  uid/pid/`/proc/self/attr/current` in a run-context header, repeat the caveat in
  the summary, and document in README.md how to answer it for the real domain
  later (exec the same binary from the trace app's spike hook, spike-A package)
  without implementing that here.
- `vkStr()` returned a pointer into one static buffer while several results
  routinely appear in one format call, so all of them showed the last one; it
  returns std::string now, `memFlagStr` likewise, and `fmt`/`pr` carry
  `format(printf)` so a missed `.c_str()` is a compile error rather than UB.
- `advertisedExportable` decided the status at the allocate site but not at the
  `vkGetMemoryFdKHR` site. One rule at every export failure now
  (`exportFailStatus`): advertised EXPORTABLE and then declining is FAIL, never
  advertised is UNSUPPORTED. Export + map + fd is factored into `exportHostVisible`.
- `T0-ahb-blob-transfer` was recorded OK on the socket handoff alone. The handoff
  keeps its own informational row; the tier row is now composed at the end from
  the full import+map+compare+writeback chain over the Vulkan, GL and GPU legs.
- `mmapErrno` kept the first attempt's errno after the second-chance mmap
  succeeded, so a working mapping carried a failure code; it is cleared on
  success and the first errno moves into the note.
- a failed `glImportMemoryFdEXT` no longer closes the fd: EXT_memory_object_fd
  does not say whether ownership still transfers on failure and Mesa closes it
  either way, so closing risks a double close landing on the socket. Leaking a
  handful of dups in a short-lived probe is the safe side of that trade.
- validated end to end on the host harness (lavapipe + llvmpipe,
  `VK_DRIVER_FILES=lvp_icd.json EGL_PLATFORM=surfaceless`): T1-opaque-fd OK,
  T3-external-memory-host OK, T3-memfd-cross-process OK,
  T3-client-memfd-server-import OK, T1-dma-buf UNSUPPORTED (not advertised
  exportable). The two T1-gles rows FAIL there with GL_OUT_OF_MEMORY on every
  ladder rung although GL_DEVICE_UUID_EXT matches the Vulkan deviceUUID --
  llvmpipe's GL does not implement importing a lavapipe opaque-fd allocation,
  a Mesa interop gap recorded in README.md so a device FAIL stays attributable.
  Rebuilt for arm64-v8a with NDK r27d (PIE, android-30); the device run is
  pending, both device locks are held by another campaign.
2026-09-05 20:50:05 -04:00
swung0x48 6c7ad0a1bf [Feat] (Spikes): add the standalone external-memory probe that decides the persistent-map tier
- Plan B §11 P0 requires spike B ("external memory 导出,两台设备") to run before the
  persistent-map decision of §8.3 can be taken: T0 (server imports a client
  allocation), T1 (server exports its own HOST_VISIBLE|HOST_COHERENT allocation)
  or T2 (AcquirePersistentMap returns nullptr, making the §5.10 client-side block
  push mandatory). §8.3 says the answer must be measured on the two campaign
  devices, and that a platform unknown must not block the interface work.
- tools/spikes/extmem_probe/ is a self-contained NDK command-line program: it
  links vulkan/EGL/GLESv3/android/log and nothing from MobileGL, is configured by
  its own CMakeLists with the android toolchain file, and is deliberately absent
  from the project's build graph (the root CMakeLists only pulls in
  tools/trace_replay), so the default ALL target is untouched.
- Phase A enumerates VK_KHR_external_memory_fd, VK_EXT_external_memory_dma_buf,
  VK_EXT_external_memory_host, VK_ANDROID_external_memory_android_hardware_buffer
  and, through a headless EGL pbuffer context, GL_EXT_memory_object{,_fd},
  GL_EXT_external_buffer, GL_EXT_buffer_storage, GL_OES_EGL_image_external{,_essl3}
  and EGL_ANDROID_get_native_client_buffer, plus the memory-type table and the
  vkGetPhysicalDeviceExternalBufferProperties verdict per handle type for the exact
  buffer usage set MobileGL needs.
- Route T1 allocates a HOST_VISIBLE|HOST_COHERENT buffer memory with
  VkExportMemoryAllocateInfo, writes a pattern through vkMapMemory, exports an fd
  with vkGetMemoryFdKHR (opaque-fd and, where advertised, dma-buf), hands it to a
  second process over SCM_RIGHTS, and has that process both mmap() the fd and
  import it into its own VkDeviceMemory + vkMapMemory. Both sides write and both
  sides compare, so a copy-on-import or one-directional mapping is reported as
  PARTIAL rather than as success.
- Route T0 has the second process allocate an AHardwareBuffer BLOB
  (CPU_READ_OFTEN|CPU_WRITE_OFTEN|GPU_DATA_BUFFER), send it with
  AHardwareBuffer_sendHandleToUnixSocket, and the first process import it three
  ways -- AHardwareBuffer_lock, VkDeviceMemory via
  VK_ANDROID_external_memory_android_hardware_buffer, and a GL buffer via
  eglGetNativeClientBufferANDROID + glBufferStorageExternalEXT mapped
  persistent/coherent -- with a write-back leg the allocating process verifies.
- Route T3 covers VK_EXT_external_memory_host: a memfd-backed mmap region aligned
  to minImportedHostPointerAlignment, imported through
  VkImportMemoryHostPointerInfoEXT, plus the same memfd handed to another process.
- The second process is /proc/self/exe re-exec'd with --child=<route> and one end
  of a socketpair on fd 3. A bare fork() is not usable: neither side's Vulkan
  driver survives fork, and both sides need live Vulkan. It is also the topology
  the transport actually has (§8.1, inheriting PLAN.md §11.1-§11.6: the client
  spawns the server), so the probe measures the arrangement that would ship.
- The probe also builds for the host with T0 compiled out. That is not scope
  creep: a negative device result is only worth something if the harness is known
  to report a working route as working. Running it on lavapipe did that, and paid
  for itself immediately by exposing two harness bugs that would have produced
  false negatives on the devices -- (a) the child wrote through its plain mmap
  before reading through the Vulkan import, so on a driver whose exported fd maps
  at an offset the probe overwrote the very payload the second read compares
  (lavapipe reports payloadAt=4096); reads through both mappings now precede
  writes through either, and the offset is searched for and reported; (b) an
  export failure on a handle type the driver never advertised as EXPORTABLE was
  classified FAIL instead of UNSUPPORTED (lavapipe's dma-buf answer).
- Output is a RESULT/summary table carrying the raw driver verdicts (VkResult
  names, errno, GL enums) because those codes -- not a pass/fail bit -- are what
  §8.3 needs in order to pick the tier.
- Built with NDK 27.3.13750724 for arm64-v8a / android-30, RelWithDebInfo, PIE,
  warning-clean; host build clang/RelWithDebInfo, warning-clean.
2026-09-05 20:50:04 -04:00
swung0x48 38d4c2372c [Feat] (TraceApp, CI): pass arbitrary env vars through the retrace lane
- PLAN-B.md §8.2 and appendix B add a batch of new runtime switches
  (MOBILEGL_PIPE_PUSH / _VERIFY / _STATS / _LEGACY_MEMOS / _TEXEL_RETAIN_MB /
  _INDEX_MIRROR_MB, plus MOBILEGL_IPC_* later), and §11 P0 wants them parsed beside
  the existing ones. Today every knob that has to reach an Android replay costs an
  edit in five files - run_android_retrace_local.py, trace-replay-ci.sh,
  TraceReplayActivity's request record, the JNI marshalling, and the setenv block in
  trace_replay_core.cpp. That per-knob tax is what this replaces: one extra,
  `--es mobilegl_env "K=V;K=V"`, carries all of them.
- Applied last, immediately before dlopen(libMobileGL.so), so it can also override
  the dedicated fields above it - MobileGL's config is read during the load, and an
  escape hatch that cannot beat the defaults is not one. An entry with no '=' unsets
  the variable, which is the only way to clear a default the marshalling sets.
- The existing per-knob flags stay: they carry semantics beyond a setenv (use_angle
  also selects a variant, the dump lists are joined, DirectVulkan forces the
  R11G11B10F fallback), and rewriting them as env strings would move that logic into
  the callers.
- Surface: --env / MOBILEGL_TRACE_ENV in trace-replay-ci.sh, repeatable --env
  KEY=VALUE in run_android_retrace_local.py, `mobilegl_env` intent extra,
  Request::envOverrides.
- The two-level parse now lives in trace_env_overrides.hpp, beside the semicolon
  splitter it shares with the texture and FBO dump lists, and
  tools/trace_replay/trace_env_overrides_test.cpp pins it: the empty entries a
  trailing ';' leaves behind must not become unsetenv(""), `K=` must stay a Set of
  the empty string rather than an Unset (a knob read with getenv() != nullptr sees
  those as opposite answers), and only the FIRST '=' may separate, or a value
  carrying '=' is truncated without a word of warning. The whole MOBILEGL_PIPE_*
  batch rides on this parse, and the only lane that exercised it end to end was an
  on-device retrace, which would have reported a splitting bug as "the knob had no
  effect".
- The check is built and RUN at build time and mobilegl_trace_replay depends on it,
  so `cmake --build ... --target mobilegl_trace_replay` - the exact command of
  test.yml's "Build trace replay" job, which never invokes ctest - runs it. It is
  assert-free on purpose: that lane configures Release, and <cassert> under NDEBUG
  would compile every check into a green run that checked nothing. Negative control:
  swapping find('=') for rfind('=') fails 2 checks, and keeping the splitter's empty
  entries fails 2 more.
2026-09-05 20:49:57 -04:00
swung0x48 8a239177ac [Feat] (Build, TraceApp): ship and exec a second native binary on android
- PLAN-B.md §11 P0 lists spike A (the Android delivery chain) as a P0 deliverable,
  inherited verbatim from PLAN.md §15 P0; §8.1 inherits PLAN.md §11.1-§11.6, whose
  Android path needs a second process. Android gives an application no writable
  exec-able directory, so the only supported route is to name the binary lib*.so, let
  the packager put it in lib/<abi>/, and exec it out of
  getApplicationInfo().nativeLibraryDir. This builds that route end to end so the
  spike can be answered with evidence instead of folklore.
- New root option MOBILEGL_BUILD_SERVER_SPIKE (OFF, ANDROID-only) adds the
  MobileGLServer target from tools/spikes/server_stub/main.cpp with PREFIX "lib" /
  SUFFIX ".so" and -fPIE/-pie: an .so name does not exempt the file from Android's
  PIE requirement. Its RUNTIME_OUTPUT_DIRECTORY is pointed at
  CMAKE_LIBRARY_OUTPUT_DIRECTORY, because AGP packages what lands in the per-ABI
  library output directory and CMake would otherwise put an executable elsewhere.
- The option is opt-in on both sides. The plugin flavour cannot turn it on at all,
  and the trace flavour builds it only when asked, with
  `-Pmobilegl.buildServerSpike=ON` or MOBILEGL_BUILD_SERVER_SPIKE=ON in the
  environment; a flavour that silently carries an executable nothing loads is the
  kind of thing nobody notices until it ships. Verified both ways:
  assembleTraceDebug -Pmobilegl.buildServerSpike=ON packages
  lib/arm64-v8a/libMobileGLServer.so and `file` reports "ELF 64-bit LSB pie
  executable, ARM aarch64 ... interpreter /system/bin/linker64, for Android 26";
  the same task with no property packages only libMobileGL.so and
  libtrace_replay_runner.so.
- The stub prints one line to stdout and writes the same line to the file named by
  argv[1], then exits 0. The line carries pid/ppid/uid/gid and, decisively, the
  child's own /proc/self/attr/current: only `u:r:untrusted_app:...` proves an
  ordinary app process did the exec. An `adb run-as` shell runs in a different
  SELinux domain, so a success there would prove nothing.
- RunSpawnSpike() starts the stub with argv [serverPath, markerPath], redirects the
  child's stdout/stderr into a captured file (an app process has stdout on
  /dev/null, so a printed line would otherwise vanish), waits for it, and reports
  exit status, signal, the exec errno, the parent's own SELinux context, the marker
  content and the captured stdout - to logcat, to the returned string, and to a
  <marker>.report file, because the Activity finishes immediately afterwards.
- The child reports the errno of a REFUSED execve through a close-on-exec pipe.
  Without it the one datum the spike exists to produce is lost: the parent only ever
  sees a wait status, in which every reason has already been flattened into one exit
  code, and EACCES (SELinux, or a noexec mount) versus ENOEXEC (a packager that
  mangled the file) are opposite verdicts for the design. A successful exec closes
  the write end for free, so the parent reads EOF and reports execErrno=0.
- fork/execve only. The earlier draft also carried a posix_spawn arm behind
  `__ANDROID_API__ >= 28`, which was dead code in every configuration this repo can
  build - bionic declares posix_spawn from API 28 and the root CMakeLists.txt pins
  MOBILEGL_ANDROID_API_LEVEL to 26 and refuses to configure lower - and would have
  silently become the production path, untested, on a minSdk bump. Keeping the arm
  that actually ships means the spike measures the code the server would really use.
  Nothing happens between fork and execve except open/dup2/execve/write/_exit, all
  async-signal-safe, because the parent is a multi-threaded JVM process.
- The spike lives in its own TU, spawn_spike.cpp/.hpp, listed only by the trace
  APK's CMakeLists. Its sibling trace_replay_core.cpp is compiled verbatim by the
  DESKTOP mobilegl_trace_replay runner (tools/trace_replay/CMakeLists.txt names the
  same file), where <android/log.h> does not exist, so nothing Android-only may live
  there; spawn_spike.cpp carries an #error for anyone who adds it to that list.
- The Activity runs the spike, and nothing else, when launched with the
  `mobilegl_spike_spawn` intent extra; that mode needs no trace, no golden and no
  render surface. It is a separate JNI entry point rather than another parameter on
  the 30-argument replay call, which it shares nothing with.
- Not yet run on a device: both device locks are held by another campaign. The
  on-device verdict is the coordinator's step.
2026-09-05 20:49:57 -04:00
swung0x48 87ee17c68c [Docs] (Disaggregated): fold the P0 measurements and corrections into the plan
- GL_COMPUTE_WORK_GROUP_SIZE is answered by MG_Impl from ProgramObject::GetComputeLocalSize, not by a backend; only the compute limits are caps, and the two dead table entries (GetInteger64i_v, GetProgramiv) were retired in P0
- the glRenderbufferStorage OOM-probe idiom appears in 0 of 41 fixtures, so kNeedsAck is carried by glBufferStorage only
- FramebufferSrgb/DepthClamp: six readers of a constant false and zero readers respectively, no fixture enables either; recorded as a decision to take before the render-state chunk table freezes
- the call catalogue is 68 unique records (screen 10, ctx-query 6, CSO 13, kCtxState 17, kCtxObject 9, kCtxVerb 13); PipeCalls.def is the single source of truth and the wire opcode is a line's position
- measured layouts (MGPDrawInfo head 56 B, RenderStateParameters 1168 B, ResidualValueBlock 1248 B, MGPipeContext 464 B ...), the 926/73 MG_Impl mutator surface, the first per-draw accessor numbers on lavapipe (Espryt 20.65, Magma 15.54), the EndTransformFeedback null-as-capability trap, and the host-side spike results
2026-09-05 20:29:30 -04:00
swung0x48 aa005720d0 [Test] (MG_Remote, Wire): pin the hung-up doorbell, the wakeup that must not be eaten, the ring's capacity ceiling and the publish-then-ring handoff
- FdPassingTest.SocketDoorbellStopsParkingWhenThePeerHangsUp builds a
  SOCK_STREAM socketpair - deliberately not FdPassing::CreateSocketPair's
  datagram pair, because only a stream end reports the hangup at all - closes
  the notifier, and asserts Park returns false, latches Dead(), stays latched,
  and that a Wait with kWaitForever gives up in under a second instead of
  spinning.

- FdPassingTest.SocketDoorbellStillDeliversTheLastRingBeforeAHangup rings and
  then closes: detecting death must not swallow the wakeup already sitting in
  the socket buffer, since the peer's last publish is the one a waiter is most
  likely to be blocked on.

- InProcessTransportTest.AFrameWakeupIsNotEatenByAWaiterOnDescriptors blocks
  two readers on one endpoint with two different predicates and requires the
  frame to arrive within 2s rather than "eventually, when a receive timed out".

- RingTest.RejectsACapacityTheRecordHeaderCannotDescribe refuses 4 GiB from
  both roles without mapping anything (the constructor rejects before it
  touches the base pointer) and keeps 2 GiB accepted as the positive control.

- RingTest.DoorbellHandoffWakesBothSidesOnEveryPublish runs 2000 records
  through the ring with real parking in both directions, in the publish-then-
  NotifyIfParked order the fences assume. It cannot prove the fence pairing -
  no test can, since x86 has to actually hold the release store in the store
  buffer across the flag read - but it exercises the exact call order, and a
  lost wakeup surfaces as a Wait that times out with work available (a red
  test) rather than as a hung CI job.

- Result: 1429 unit tests pass with MOBILEGL_BUILD_DISAGGREGATED=ON (1424
  before this commit; the wire subset is 47), 1382 pass with it OFF, and the
  same three pre-existing skips appear in both. Every new case was verified to
  fail with its fix reverted and to pass again with it restored.
2026-09-05 20:16:50 -04:00
swung0x48 c1a7ffac94 [Fix] (MG_Remote, Transport): give descriptor offers their own condition variable, cap the ring at what a 32-bit size field can describe, and keep the frontend umbrella out of Framing.h
- InProcessChannel::Direction served two different predicates from one
  condition_variable signalled with notify_one, so a SendFrame wakeup could be
  delivered to a thread blocked in ReceiveFd, which re-tests its own predicate
  and goes back to sleep - leaving a queued message undelivered until some
  unrelated later event. ITransport narrows the contract to one dedicated
  reader thread, but that is a comment, not a mechanism, and the first caller
  that splits its reader should not have to discover this. Offers now have
  their own fdCv, and Close() notifies both.

- RingProducer/RingConsumer accepted any power-of-two capacity while
  RingRecordHeader::size is 32-bit by wire contract (plan section 8.1 ->
  PLAN.md section 6.3: 8-byte RecHeader). At 4 GiB or more a record's size -
  or a wrap filler's, which is sized by the distance to the boundary - would be
  truncated on the way in, and the consumer would then bounds-check the
  truncated value against the real one. kMaxRingCapacity rejects that at
  construction, the same class of guard as the power-of-two and
  smaller-than-a-header checks beside it. Unreachable today (SEG_CMD 8 MiB,
  SEG_STAGE 32 MiB), which is the point of catching it now.

- Framing.h included MG_Util/Debug/Log.h, which includes Includes.h, the GL
  frontend's umbrella header: 661 headers by `clang++ -H`. It is the one header
  under Transport/ that broke the rule ITransport.h states for this layer
  ("nothing about a byte pipe needs the GL frontend's umbrella header"), which
  matters when the server-side binary links this and when the include-graph
  purity gate of plan section 10.3 (gate A, asserted on -H output rather than
  on symbols) lands. Its three error paths now call WireLogError, declared in a
  new dependency-free WireLog.h whose .cpp owns the umbrella. Framing.h is down
  to 134 headers, none of them MG_State, Includes.h or Log.h.

- Two documentation corrections. ITransport::Shutdown documented a one-sided
  "releases the endpoint" while InProcessTransport::Shutdown closes both
  directions - which is what closing a socket does, so the spawn transport will
  behave the same way; the interface now says whole-connection teardown, and
  keeps the promise that queued messages stay readable until drained.
  InProcessTransport.h cited a CMake option
  MOBILEGL_BUILD_DISAGGREGATED_INPROC that grep finds nowhere: plan appendix B
  reserves it for the role-isolation shim, this skeleton does not add it, and
  the delivery mode is a runtime choice (MOBILEGL_TRANSPORT), not a build one.

- Evidence: both configurations reconfigured and rebuilt; nm --defined-only on
  the OFF build still reports 0 MG_Remote symbols and the ldd dependency set is
  byte-identical to the OFF link (plan section 10.3, the two surviving
  byte-level equalities). Negative controls: removing the capacity ceiling
  makes RingTest.RejectsACapacityTheRecordHeaderCannotDescribe fail on both
  roles; collapsing fdCv back into cv makes
  InProcessTransportTest.AFrameWakeupIsNotEatenByAWaiterOnDescriptors fail at
  3950ms against its 2000ms bound.
2026-09-05 20:16:50 -04:00
swung0x48 bdd4bed431 [Fix] (MG_Remote, Transport): close the doorbell's lost-wakeup window with two seq_cst fences and stop a hung-up peer turning every park into a spin
- The header claimed the park flag's own seq_cst store and load closed the
  lost-wakeup window. They cannot: that Dekker argument needs all FOUR accesses
  in the seq_cst total order, and the other two are not in it - the watermark
  publish is a release store (RingProducer::Publish) and the condition re-test
  is an acquire load. There was no atomic_thread_fence anywhere under
  MG_Remote. On x86 a release store and a seq_cst load are both plain MOVs, so
  the notifier can read parked==0 while its head store still sits in the store
  buffer, and the waiter then parks on a stale watermark forever; ARMv8
  survived only because STLR->LDAR is RCsc, which is luck, not the design.
  Doorbell::Wait now fences after announcing and NotifyIfParked fences before
  reading the flag - the pairing the standard actually guarantees
  ([atomics.order]) - and the header says so, including the other half of the
  contract: publish the watermark BEFORE ringing, because a fence only orders
  what precedes it. This is the claim the whole P5/P6 wait discipline (present
  credit, kNeedsAck blocking requests, full-ring escalation) will be built on,
  and its failure mode is a silent cross-process hang.
  Inherited design, plan section 8.1 (PLAN.md section 6.2/6.2a: bidirectional
  doorbell, MOBILEGL_IPC_SPIN_US default 50us, condvar for inproc).

- SocketDoorbell::Park treated any poll() return > 0 as a wakeup and never
  looked at revents. Measured on this machine: an AF_UNIX SOCK_STREAM
  socketpair whose peer has closed returns revents=POLLIN|POLLHUP with
  recv()==0 immediately and forever. Park therefore returned true, Wait stored
  parked=0, found its condition still false and re-parked - so a waiter with
  kWaitForever burned a big core at full clock with no bound. That is exactly
  the pathology the bidirectional doorbell exists to prevent (a whole 16.6ms
  frame of a big core on a phone, competing with the GPU and the game's JVM),
  reached from the other side. Park now branches on revents, a new Drain()
  latches death on EOF (and on ECONNRESET/EPIPE from Notify), and the new
  Doorbell::Dead() lets Wait give up instead of re-parking on a descriptor that
  can never deliver another wakeup.

- Same commit, same defect class: `fd` is documented as one end of an AF_UNIX
  socket pair, not "a socket or pipe end". Notify uses send(MSG_DONTWAIT|
  MSG_NOSIGNAL) and Park uses poll()+recv(), which a pipe end refuses with
  ENOTSOCK, and a SOCK_DGRAM pair reports no readiness at all when the peer
  closes (measured), so the spawn transport wants SOCK_STREAM.

- Evidence: build-linux-split rebuilt clean; the wire suite is green (47/47).
  Negative control - reinstating the revents-blind Park makes
  FdPassingTest.SocketDoorbellStopsParkingWhenThePeerHangsUp fail on both Park
  assertions, and restoring this code turns it green again.
2026-09-05 20:16:49 -04:00
swung0x48 10315e71f3 [Test] (MG_Remote, CI): cover the wire layer with five suites and gate the generated header with flatc-check
- MobileGL/MG_Test/Wire, 42 gtest cases in five binaries, ctest label `unit`, registered only when MOBILEGL_BUILD_DISAGGREGATED is ON so the default configuration is untouched.
- FdPassingTest is the one the earlier branch could not have had: it forks a child that creates a shared segment, fills 64 KiB with a pattern and hands the descriptor over SCM_RIGHTS; the parent adopts it, maps it read-only and compares every byte. Everything in Feat/CS-Delta-IPC ran in one process, which is why `out->fd = -1` survived unnoticed. It also pins that a too-small sideband buffer is refused before the datagram is consumed (so no descriptor is dropped), that an empty sideband still carries its fd, and that a receive with no offer times out. The `spawn` SocketDoorbell is covered in the same file because it rides the same kind of socket: a parked waiter woken through NotifyIfParked, a clean timeout, and a wakeup that arrives before anyone parks being remembered and then consumed exactly once.
- RingTest: control-page size/alignment/cache-line layout, a non-power-of-two capacity refused, payload alignment, 200 records driven through a 256-byte ring so the wrap filler path runs repeatedly and no record ever straddles the boundary, backpressure (full ring refuses, applied alone frees nothing, retired frees), a record larger than the ring refused, the generation bump refused while records are in flight and accepted once quiesced, a corrupt header refused instead of dispatched, and a 20000-record two-thread producer/consumer run checking order, content and the final cursor equality.
- FramingTest: byte-at-a-time reassembly of two frames, the magic reading "MGLF" on the wire, empty payloads, a bad magic and an oversized length each latching the reader dead (the old code hung silently instead), the send-side cap, and the buffer-too-small contract keeping the message so the retry still finds it.
- InProcessTransportTest: both directions, ordering, buffer-too-small, poll and timeout, shutdown draining queued messages before it closes, a blocked receiver woken by a send and by a shutdown, the frame cap, descriptor hand-off with its sideband, and three condvar doorbell cases - a parked waiter woken through NotifyIfParked, an already-true condition that must never park, and a clean timeout.
- ProtocolSmokeTest: a Hello built and read back through the committed generated header, a Welcome carrying the four segment announcements, the same buffer travelling across the transport unchanged, a truncated buffer failing verification rather than being read, and the CtrlMsg / SegmentKind / LogLevel / FatalCode tag values frozen - they are wire numbers, so if that test has to be edited the schema change was a wire break.
- CI: a `flatc-check` job in test.yml that checks out only the flatbuffers submodule, builds the pinned flatc through scripts/gen_protocol.py into the runner temp directory, regenerates and runs `git diff --exit-code` on protocol_generated.h. It needs no MobileGL build, so it does not depend on build-linux (plan B section 8.1 / earlier section 7.1: the committed header plus a CI regeneration check, and no flatc in the default build graph).
- Verified: `ctest -L unit` is green in both configurations - 1382 cases with the option OFF and 1424 with it ON (the same 1382 plus these 42); the nm gate is 0 matches OFF and 93 ON; regeneration of protocol_generated.h is byte-identical, and the flatc-check gate goes red on a hand-edited header and green again after a clean regeneration.
2026-09-05 20:16:49 -04:00
swung0x48 bfa087d0f7 [Feat] (MG_Remote, Transport): land the transport skeleton behind MOBILEGL_BUILD_DISAGGREGATED - framing, the SPSC ring, doorbells, shm segments and SCM_RIGHTS
- New CMake option MOBILEGL_BUILD_DISAGGREGATED (default OFF, plan B appendix B). ON appends the MG_Remote sources to SOURCE_FILES, puts 3rdparty/flatbuffers/include on the include path and defines MOBILEGL_BUILD_DISAGGREGATED=1. OFF compiles nothing from MG_Remote and adds no include path and no library, which is one of the two byte-level equalities plan B section 10.3 keeps: measured `nm --defined-only build-linux/libMobileGL.so | grep -ic MG_Remote` = 0 with the option OFF and 93 with it ON, with an identical ldd set in both configurations.
- The option forces itself OFF with message(WARNING) when 3rdparty/flatbuffers/include/flatbuffers/flatbuffers.h is missing, so a checkout without the submodule still configures and builds rather than failing a hundred lines later on a missing header.
- ITransport.h: SendFrame / ReceiveFrame / PeekFrameSize / ShareFd / ReceiveFd / Shutdown. ReceiveFrame's contract is the fix for a defect of the earlier branch: a destination buffer smaller than the pending message returns MOBILEGL_ERR_BUFFER_TOO_SMALL with the required size and KEEPS the message queued. The earlier transport failed the call and popped the message anyway, which wedges the stream permanently the first time a reader guesses a size wrong.
- Framing.h: [u32 'MGLF'][u32 len][payload], 64 MiB cap, validated on read. A bad magic or an oversized length latches the reader dead, is logged at ERROR and makes every later call return MOBILEGL_ERR_PROTOCOL_MISMATCH. This is the second inherited defect: Feat/CS-Delta-IPC's Framing.h:41-45 Feed() always returned OK and its header peek merely returned false, so a desynchronized stream became a silent permanent hang; its LocalSocketTransport.cpp:232-236 then allocated on the peer-supplied length with no cap. The magic is also byte-ordered so it reads "MGLF" on the wire, where the earlier constant spelled "FLGM".
- Ring.h/.cpp: RingControl exactly as the inherited design (plan B section 8.1 -> earlier section 6.2): two independent cursor triples (cmd and stage, each {head, appliedTail, retiredTail}), appliedSeq / submittedSeq / retiredSeq / completedFrameSerial / presentAckSerial, serverEpoch, ringGeneration, consumerParked, producerParked, eventRingFull, eventDropped. One 4096-byte page with each contended group on its own cache line, pinned by static_assert on size and alignment and by an offset test.
- The SPSC pair uses monotonic byte cursors and a power-of-two mask, so a torn read can never look like a valid earlier position. A record never straddles the wrap: the producer emits a kPad filler to the boundary, which is always a multiple of 8 and therefore always has room for a header. The producer reclaims against retiredTail rather than appliedTail, so the day the server borrows a ring slot into the GPU timeline (kRecBorrowSlot) it does not silently degrade to early recycling. HardDrainRing bumps ringGeneration only when the ring is quiesced, and leaves the cursors monotonic so cached offsets are recognisably stale.
- The consumer bounds-checks every record header before dispatch (8-aligned, at least a header, no larger than what the producer published, contiguous inside the mapping) and reports corruption to the caller instead of dispatching into undefined behaviour. That is the runtime half of the earlier section 6.3 discipline: SEG_CMD is written by another process, so a compile-time static_assert on record sizes proves nothing about what is in the mapping.
- Doorbell.h/.cpp: spin then park, both directions (earlier section 6.2a). CondVarDoorbell for inproc, SocketDoorbell for spawn (one byte, codes 0x01 ring-advanced and 0x02 watermark-advanced) - no futex, eventfd or named event anywhere. The lost-wakeup window is closed by ordering: the waiter stores its park flag seq_cst and then re-tests the condition, NotifyIfParked loads the same flag seq_cst after the watermark is published, so one of the two always sees the other. kDefaultSpinUs is 50, the MOBILEGL_IPC_SPIN_US default.
- ShmSegment.{h,cpp} + ShmSegmentPosix.cpp: memfd_create by raw syscall on desktop Linux (the glibc wrapper is too recent to rely on), ASharedMemory_create on Android (API 26; libc's memfd_create wrapper is API 30, above MobileGL's floor), shm_open + immediate shm_unlink as the fallback. Adopt() refuses a descriptor whose fstat size is smaller than the size the peer announced, so a short segment cannot turn every later offset into an out-of-bounds map. ShmSegmentWin32.cpp (CreateFileMappingW in Local\) is compile-guarded and untested - this project's Windows machine is not a correctness gate.
- The Android path is compile-verified, not just written: an arm64-v8a NDK build with the option ON links, ShmSegmentPosix.cpp.o carries an undefined ASharedMemory_create, and ShmSegmentWin32.cpp.o is empty there. That build is also what caught the missing <cstddef> in ShmSegment.h and Framing.h, where std::size_t / std::ptrdiff_t only resolved through a transitive include on the host sysroot.
- FdPassing.{h,cpp}: SCM_RIGHTS in the FIRST transport commit, as plan B section 8.1 demands, over a dedicated AF_UNIX SOCK_DGRAM socketpair rather than the control byte stream (message boundaries survive on every POSIX - SOCK_SEQPACKET does not exist on macOS - and ancillary data can never be split from its payload). The third inherited defect this replaces: Feat/CS-Delta-IPC deferred fd passing and hardcoded `out->fd = -1` in LocalSocketTransport.cpp:296, so on the only platform that matters its data plane could not move one byte between processes. MSG_CTRUNC, an unexpected descriptor count and a malformed sideband header all close every descriptor received before failing, and a too-small sideband buffer is refused before the recvmsg so a datagram is never half-consumed.
- InProcessTransport.{h,cpp}: two in-memory queues plus the condvar doorbell pair. It keeps the frame size cap so nothing that passes in inproc becomes illegal after the switch to spawn, hands descriptors over with dup() under the same ownership rule as SCM_RIGHTS, and lets a peer's queued messages be drained after Shutdown - usually the last one says why it is going away.
- Not done here on purpose: the MOBILEGL_IPC_* environment variables are parsed in ConfigLoader.cpp, which belongs to another P0 work package running in parallel; this commit only exposes the constants (kDefaultSpinUs, kMaxFramePayloadSize, FdPassing::kMaxSidebandBytes) so that plumbing has something to set.
2026-09-05 20:16:49 -04:00
swung0x48 a1e22c26ab [Build] (MG_Remote, Protocol): pin the flatbuffers submodule, add the control-plane schema and commit its generated header
- 3rdparty/flatbuffers submodule pinned to the latest release tag v25.12.19 (7e163021). The runtime is header-only, so only 3rdparty/flatbuffers/include is ever used and no library is linked; CMake never calls add_subdirectory on it and flatc is not in the build graph (plan B section 8.1, inheriting the earlier plan's section 7.1).
- MobileGL/MG_Remote/Protocol/protocol.fbs carries the CONTROL PLANE only: SegmentRef, Hello, Welcome, CapsSnapshot, DefaultFramebufferInfo, SurfaceOp, SurfaceReply, ResyncRequest, ResyncDone, AuxRequest, Fatal, LogLine, union CtrlMsg and the CtrlEnvelope root with a file_identifier. Hot-path records are FlatBuffers structs generated from MG_Pipe/PipeCalls.def in a later package and are deliberately absent here, so record numbering never churns.
- Two deviations from the earlier plan's section 7.1 sketch, both deliberate: (a) ProgramReflection is not a union member - plan B ships program artifacts inside the create_shader_state CSO blob (section 8.2), and union tags are wire values that may only ever be appended, so reserving a tag for a message that may never exist is worse than appending one later; (b) maxComputeWorkGroupCount/Size are vectors, not [int:3] - fixed-size arrays are legal only in FlatBuffers structs, never in tables.
- scripts/gen_protocol.py resolves flatc as MOBILEGL_FLATC_EXECUTABLE, otherwise builds the pinned flatc ONCE into <repo>/../flatc-build (override with MOBILEGL_FLATC_BUILD_DIR), outside the project build graph. A flatc found on PATH is deliberately refused and a version mismatch against the pinned runtime is a hard error: the generated header static_asserts FLATBUFFERS_VERSION, so a stray flatc either fails to compile or churns the committed file on every machine. The earlier branch did the opposite - Protocol/CMakeLists.txt:22-38 add_subdirectory'd the FlatBuffers tree with FLATBUFFERS_BUILD_FLATC=ON whenever MOBILEGL_FLATC_EXECUTABLE was unset, which is exactly the NDK trap it claimed to avoid (cross-compile an arm64 flatc, then run it on the host).
- protocol_generated.h is committed with the project source header prepended by the generator, so regeneration is byte-identical: verified by running gen_protocol.py twice and by perturbing the file and regenerating it back.
- Reuse from Feat/CS-Delta-IPC: MobileGL/Protocol/mg_protocol_base.h, kept as the shared C vocabulary (result codes, byte spans, shm region, id typedefs) and keeping the structSize-first versioning discipline that section 14.2 calls the answer to risk B-R10. Dropped from it: MobileGLObjectKind / MobileGLObjectScope / MobileGLObjectHandle - plan B never puts GL object identity on the wire (the frontend allocates {slot, generation} handles in MG_Pipe, section 4.2.1), so a second identity vocabulary would be a drift surface with no reader. Added MOBILEGL_ERR_BUFFER_TOO_SMALL as an append-only code for the receive contract.
2026-09-05 20:16:49 -04:00
swung0x48 bd2b4158e0 [Fix] (DirectVulkan, State): key the transform-feedback counter slots on the object's identity, not on its recycled GL name
- VulkanRenderer::CurrentXfbCounterSlot keyed m_xfbCounterSlotByObject on
  GetBoundTransformFeedbackName(). glGenTransformFeedbacks hands a deleted name
  straight back (IndexGenerator is LIFO) and nothing ever removed a map entry, so
  a transform feedback object created on a recycled name was served the DEAD
  object's counter group - and with it that group's m_xfbCountersValid and
  m_xfbLastSeenGeneration entries, which are the resume/fresh decision for
  vkCmdBeginTransformFeedbackEXT. This is D21 in plan B v2 4.7.3, the one entry
  in that table whose today-key "guards nothing", and 10.4-5 asks for it to land
  on dev on its own - hence this separate commit, kept in files no other commit
  on this branch touches so the cherry-pick applies unaided.
- Frontend: TransformFeedbackObjectState gains a never-reused `lifetimeId`
  through a default member initialiser, so every route into existence
  (operator[] materialisation, `= {}` in GenTransformFeedbackNames and
  CreateTransformFeedbackObject) mints a fresh one and a recycled name cannot
  carry the dead object's id back. The allocator is the same shape as
  BufferObject::AllocateLifetimeId (atomic, starts at 1 so a zeroed backend slot
  is never a live object).
- The bound object's id is mirrored in m_boundTransformFeedbackLifetimeId,
  refreshed by RestoreBoundTransformFeedbackState - which every bind, and the
  revert that deleting the bound object performs, goes through - and seeded for
  the default object by the GLContext constructor. GetBoundTransformFeedback
  LifetimeId is therefore a const load. Reading it through operator[] instead
  would have been an INSERT on the per-draw path, and UnorderedMap is
  ska::flat_hash_map, whose rehash invalidates every reference into the
  container, not just its iterators.
- Backend: the UnorderedMap is replaced by a fixed 16-entry owner table, which
  fixes the second half of the same defect - the map was keyed on a value that
  recycles yet was never pruned, so it grew for the life of the context. With
  lifetime ids as keys a map would have grown without bound instead, so the
  bounded table is required, not cosmetic.
- Slot exhaustion: past sixteen owners a group has to be taken over, and the
  victim is chosen among owners with NO OPEN SPAN, which
  GLContext::HasOpenTransformFeedbackSpan answers; an identity no live object
  carries any more answers false, and that is what lets a dead owner's group
  come back. Least-recently-used ALONE would have been exactly the wrong rule:
  GL only permits another object to capture while this one is PAUSED, so the
  paused span these groups exist to protect is by construction the least
  recently used entry, and an LRU takeover would reset the one resume offset
  that still matters. LRU is now only the tie-break among reclaimable groups.
  Sixteen genuinely open spans at once is reported (MGLOG_E_ONCE) rather than
  resolved silently, because whatever is taken then restarts at offset 0.
- Not done, and why: the natural place to hand a group back is
  glEndTransformFeedback, but registering DirectVulkan's EndTransformFeedback
  table entry would flip the test FixupGsStripCaptureOrder makes of that same
  pointer (GL_Drawing.cpp:1255) to decide whether the backend already captured
  in GL's vertex order, silently disabling the geometry-stage strip fixup for
  DirectVulkan. Giving that discriminator a name of its own is a separate
  change; until then the no-open-span rule is what keeps the table honest.
- CurrentXfbCounterSlot asserts the identity is never 0. Zero is the free-slot
  sentinel, so an identity of 0 would match every free slot as "mine" without
  ever claiming one - this bug reintroduced, with no symptom at the call site.
- TransformFeedbackLifetimeIdTest, in its own translation unit, pins the
  frontend halves: an object created on a recycled name must not report the dead
  object's id, the default object has an identity before anything binds it, and
  a PAUSED span still reads as open while another object is bound and capturing
  - which is the whole correctness argument for the eviction rule. The name
  reuse is not simulated: the test asks the real generator and skips (loudly) if
  it never recycled. Still untested: the >16-owners path itself, which needs a
  backend scenario with seventeen capturing objects and there is none.
- Negative controls, each applied then reverted: making a non-bound object's
  span read as closed reddens APausedSpanStaysOpenWhileAnotherObjectCaptures;
  making a vanished identity read as open reddens the same case on its delete
  assertion; dropping the constructor's seeding reddens
  AnObjectAtARecycledNameCarriesAFreshLifetimeId.
- Tested: cmake --build build-linux -j 24 (clean, 166 targets); ctest -L unit
  -j 12 -> 1386/1386 passed; ctest -L integration-gpu -> 866/866 passed
  serially, and 866/866 on one of two -j 8 runs. The other -j 8 run failed
  DirectGLES.PointSizeDemotionScenario.TheDemotionIsActuallyArmedWhenTheEnviron
  mentPinsItOn, a member of the pre-existing parallel-ctest flake family: it
  passes in isolation here, and the unmodified parent tree
  (~/w7/p0-noop-wins-base) reproduces the same family under -j 8.
2026-09-05 20:16:49 -04:00
swung0x48 9c7339b214 [Feat] (State): give RenderbufferObject the never-reused lifetime id every other cache-keyable state object already has
- RenderbufferObject was the last state object a backend twin registry keys on
  that could only be identified by its heap address or its GL name - both of
  which recycle. BufferObject, VertexArrayObject and ProgramObject all carry a
  process-wide, never-reused id for exactly this; the renderbuffer's absence is
  named in plan B §10.4-5 as one of the two latent problems P0 closes.
- Mirrors BufferObject.h:202-208 / BufferObject.cpp:19-24 verbatim in shape: a
  private static AllocateLifetimeId() over a namespace-scope
  std::atomic<Uint64> starting at 1 (so a zero-initialised memo slot can never
  carry a live object's id), a const member initialised from it at construction,
  and an inline const getter. The doc comment is the buffer one restated for the
  renderbuffer's own recycling sources.
- Deliberately NO GetVersion(): plan B §11 P0 says the id only. A mutation
  counter would be a second, independent invalidation surface to keep correct,
  and nothing needs one yet - the renderbuffer's mutable content already reaches
  the backends through AllocateStorage / SetInternalFormat / SetSamples.
- No caller yet, by design: the id exists so §4.7.3's D1 rekey (and the
  DirectGLES renderbuffer twin registry at Managers.h:1858) has something to key
  on. It is a pure addition - no existing field, signature or answer changes.
- ObjectLifetimeIdTest gains the two cases the other two object types already
  have, so the renderbuffer is covered by the same allocator-reuse probe: an
  object rebuilt at a freed address must not answer to the dead one's id, and
  two live ones must differ.
- Tested: cmake --build build-linux -j 24 (clean); ctest -R ObjectLifetimeId ->
  6/6 passed (4 pre-existing + 2 new).
2026-09-05 20:16:49 -04:00
swung0x48 50815a232e [Fix] (Backend, Getter): retire the two frontend queries that were never asked and strip the unreachable frontend arms from the third
- GLFunctionsTable had three entries that are frontend queries wearing a
  backend interface (plan B v2 §2.1(a)): GetIntegeri_v, GetInteger64i_v and
  GetProgramiv. Two of them have NO caller at all - `grep -o
  'gBackendFunctionsTable\.GL\.[A-Za-z_0-9]*'` outside MG_Backend/ lists 69
  distinct entries and neither GetInteger64i_v nor GetProgramiv is among them -
  so both table slots, both backends' implementations and both registrations are
  deleted here. This is plan B §11 P0's first "strictly no-op free win".
- Nothing was moved into MG_Impl, because MG_Impl already answers all of it.
  glGetInteger64i_v is served by GL_Getter.cpp:1240-1314, which handles the
  indexed buffer queries itself and derives every other pname from its own
  GetIntegeri_v ("Handing the leftovers straight to the backend instead made
  glGetInteger64i_v disagree with glGetIntegeri_v on the very same pname").
  glGetProgramiv is served by GL_Program.cpp, whose GL_COMPUTE_WORK_GROUP_SIZE
  arm (:928-946) reads ProgramObject::GetComputeLocalSize - a link artifact of
  the program the APPLICATION wrote, which is the only program in the
  application's namespace. §4.7.1 class B.
- GetIntegeri_v stays, but only for what a backend genuinely owns. Its pure
  frontend arms were unreachable: GL_Getter::GetIntegeri_v answers
  GL_SHADER_STORAGE_BUFFER_{BINDING,START,SIZE} through
  TryDecodeIndexedBufferQuery (:991-1029) and the six GL_IMAGE_BINDING_* pnames
  at :1115-1153, and returns before touching the table. That left 9 dead cases
  in DirectGLES.cpp and 9 in DirectVulkan.cpp - the plan's "15" undercounts the
  two files separately. What still arrives is GL_MAX_COMPUTE_WORK_GROUP_COUNT /
  _SIZE (GL_Getter.cpp:1161-1177, and MG_Util/ShaderTranspiler/CompileEnv.cpp
  :134-138 asks the table directly), so DirectVulkan keeps exactly those two and
  DirectGLES becomes a plain driver passthrough.
- The dead arms were also WRONG, which is why deleting rather than reconciling
  them is the strict no-op: they clamped a bound range's size to the buffer's
  current storage, while the frontend reports the size glBindBufferRange was
  asked for verbatim (GL 4.6 core tables 23.4/23.5 - the clamp answered 0 for
  KHR-GL43.shader_storage_buffer_object.basic-binding's shape). Had a later
  refactor made the table the answer, the regression would have been silent.
- ProgramResourceCache::computeWorkGroupSize and the spirv-reflect entry-point
  loop that filled it go with DirectVulkan's GetProgramiv; nothing else read it.
- Three cases added to AdvertisedLimitsScenario pin what the frontend answers,
  on both lanes: the indexed SSBO binding/start/size on the 32- and 64-bit
  widths INCLUDING a shrink of the store underneath the binding (the arm that
  actually separates verbatim from clamped), the six image-unit pnames on both
  widths, and the compute local size plus the INVALID_OPERATION a program with
  no compute stage must give.
- Both new gates were shown to go red for their reason: making
  GL_COMPUTE_WORK_GROUP_SIZE answer a defaulted (1,1,1) fails
  ComputeLocalSizeComesFromTheLinkedProgram on both lanes, and re-introducing
  the deleted store clamp in GL_Getter fails
  IndexedBufferBindingsAreReportedVerbatimOnBothWidths on both lanes.
- Tested: cmake --build build-linux -j 24 (clean); ctest -L unit -j 12 ->
  1382/1382 passed; ctest -R AdvertisedLimits -> 18/18 passed (6 pre-existing +
  3 new, x DirectGLES and DirectVulkan).
2026-09-05 20:16:49 -04:00
swung0x48 d380a01f32 [Fix] (Metrics, DirectGLES, DirectVulkan): stop the summary line printing window totals under a per-frame label, and wire the six staging paths the site inventory claimed were covered or absent
- A window with no Present divided by a faked 1 and printed the window TOTALS under
  "bytes/f[...]": the *MultiDraw* slice (47 draws, no present) reported 1,404,550 bytes as a
  PER-FRAME figure, a 47x overstatement of exactly the SEG_STAGE sizing input plan B section
  8.2 / section 11 P0 asks this package to produce. FormatWindowLine now relabels the bracket
  to "bytes[...]" and prints draws/f=n/a when the window holds no frame; acc/draw=n/a follows
  the same rule, because "0.00" beside a non-zero acc= is the same lie. Pinned by
  PipeStatsTest.SummaryLineSurvivesZeroFrames and the reworked SummaryLineSurvivesZeroDraws.
- Every per-frame and per-draw field now goes through one FormatFixed2 helper. draws/f read 1
  for 26 draws over 14 frames (1.86) and buf read 97 for 1360 bytes (97.14): a systematic
  downward truncation of up to a whole unit on the figures the package exists to produce.
  Pinned by PipeStatsTest.PerFrameFieldsKeepTwoDecimals.
- FormatSummaryLine rewrote the window bases as a side effect of formatting, so any second
  reader silently zeroed the next window. Split into a pure FormatWindowLine() and an explicit
  AdvanceSummaryWindow(); EmitSummaryLine calls both. Pinned by
  PipeStatsTest.FormattingTwiceDoesNotConsumeTheWindow.
- Init() called ResetForTesting(), against the header's own "not used by any shipping path".
  Both now forward to an internal ResetCounters().
- TracyPlot published only the MISS half of each gate under the gate's unqualified name, so
  the headline output channel of section 11 P0 carried no denominator. Two series per gate now
  ("...-hit" / "...-miss") from static literal arrays. The payload histogram stays unplotted
  and says why: it is a run-total distribution over draws (section 4.5.7), not a per-frame
  scalar, and it reaches the operator through the JSON dump.
- GetOrCreatePipeline's 15-call tally sat ABOVE the list-topology primitive-restart refusal,
  so a declined draw added ten reads it never made - an OVER-count, which breaks the lower
  bound contract every other tally keeps. Moved to immediately before the payload build, and
  the enumeration reconciled with the constant: the excluded read is the sample-shading
  capability, short-circuited by m_sampleRateShadingFeatureEnabled.
- Six real staging paths were uncounted while the inventory claimed coverage. The inventory
  claim "DirectVulkan's own buffer staging ... has no second copy to count" was simply false.
  Now wired: MultiDraw.cpp's UploadScratch/UploadScratchRing (indirect commands, the compute
  tier's draw-info array, the rebased index stream - the class is passed in, so a new tier
  cannot forget it); Managers.cpp's pool-recycle reseed and the VBO-backed Float64 narrowing;
  VkBufferManager's eight host->device copies of buffer contents plus UploadTransient, the
  single chokepoint for Magma's per-draw vertex/index/indirect staging; VkTextureManager's
  packed staging slice, with the same box/rect SHAPE split Espryt already reported.
- New byte class stage-indirect-cmd, for draw PARAMETER bytes a backend synthesises and
  stages. Kept out of stage-index-client because these are the population that becomes MGPipe
  command-record payload (section 4.5.7), not resource bytes. A name addition, not a rename:
  no recorded baseline is invalidated.
- stage-ubo-named moved past the zero-copy direct-bind decision in ResolveUniformBufferPayload
  (a direct bind repacks nothing, so counting it there reported a copy that never happened),
  and Magma's default-uniform-block image now feeds stage-ubo-global the way Espryt's does,
  counted after the per-frame slice memo.
- The site inventory in PipeStats.cpp is rewritten to name every unwired path by file and
  function. An inventory that overstates coverage is worse than a missing counter, because
  the zero is then read as an answer.
- Evidence, lavapipe/llvmpipe, MOBILEGL_PIPE_STATS=1: the MultiDraw slice now prints
  "window=0 draws/f=n/a bytes[buf=1404550 ...]"; the indirect tiers
  (MOBILEGL_ESPRYT_MULTIDRAW_MODE=indirect|multiindirect) move icmd 0 -> 660; Magma's GuiBatch
  line moves from buf=0 tex=0 ubog=0 to buf=685.71 tex=41.14 ubog=157.71 with tex[emit=9
  box=9 rect=0 jobs=9]. Off-path A/B against the base tree with the env unset, 9 runs each of
  a 40-scenario draw slice, sorted totals in ms: Espryt base 389..794 (median 399) vs branch
  384..403 (median 394); Magma base 406..472 (median 410) vs branch 408..761 (median 414) -
  the always-compiled guard is below this harness's noise on both backends.
- 1410 unit tests green (15 PipeStatsTest). integration-gpu 860/860, 860/860, 859/860; the one
  failure is DirectVulkan.PointSizeDemotion...TheDemotionIsActuallyArmedWhenTheEnvironmentPins-
  ItOn, a member of the load-dependent *IsActuallyArmed* flake family already present in the
  untouched base tree, and it passes 8/8 standalone here. stdio gate and gen_pipe check green.
2026-09-05 20:16:49 -04:00
swung0x48 7566a0b002 [Feat] (DirectGLES, DirectVulkan): instrument the six memo gates, the staging byte paths and both Present hooks with the MGPipe counters
- The sites of plan B section 2.3.1, verified against dev@81b17c0b (the plan's own line
  numbers for DirectGLES drift by 4-9 lines; the DirectVulkan ones are exact):
  SyncRenderState is DirectGLES.cpp:1994 with the version read at :1998 and the
  early-out at :2007-2010 (plan says 2003 / 2007 / 2016-2018); SyncNeccessaryTextures
  at :1511 (plan :1520); CurrentUnitBindingsEpoch at :1412-1435 (plan :1418-1436);
  PrepareForDraw at :2907-2968 (plan :2916-2976); the global-UBO upload at :3355-3397
  (plan :3369-3392); TrySetupDrawFastPath :5994, GetOrCreatePipeline :4948-4993,
  ApplyDynamicDrawStateTail :5871-5893 and UniformManager ResolveUniformBufferPayload
  :2022/:2052 all as cited.
- Accessor counting is STATIC TALLIES at ten hot entry points, not a wrapper around the
  293 pGLContext-> sites: each instrumented function adds the number of accessor calls
  its own body made on the path taken. Reads inside callees, and every conditional read
  (the sRGB capability in SyncRenderState, the XFB probe and the version-gated parameter
  fetch in TrySetupDrawFastPath, the cull-mode/logic-op/tessellation reads in the
  pipeline payload builder) are excluded, so the number is a consistent LOWER bound. The
  full inventory of what is and is not counted is the header comment of PipeStats.cpp.
- The Magma fast-path gate is counted from SetupDraw, not from inside
  TrySetupDrawFastPath: that function has 27 decline returns and one success return, and
  counting at the caller is the only shape that cannot miss one.
- The texture upload counts the SHAPE (union box vs N-rect list) separately from the
  bytes, because SSIM is blind to the shape and the +6 ms/frame Mali regression of
  section 7.3 was a shape regression, not a byte one.
- Frame boundary: DirectGLES::Present after the ring upkeep, and DirectVulkan's backend
  Present rather than VulkanRenderer::Present - the latter has an early return for the
  no-usable-swapchain case, and a suspended frame is still a frame the counters close.
- Measured on lavapipe/llvmpipe with MOBILEGL_PIPE_STATS=1, GuiBatchScenario, 14 frames
  and 26 draws: Espryt 20.65 accessor calls per draw (gates ers 22/18, etl 71/9, eub
  71/9), Magma 15.54 (mfp 12/14, mpm 0/14, mdt 12/14). Both land inside the 10-25 band
  section 2.3.1 predicted and far below the 124/169 static counts, which is the
  correction that section was written to force.
2026-09-05 20:16:49 -04:00
swung0x48 42e0f47ebb [Feat] (Metrics): land the MGPipe boundary counters - bytes, dynamic accessor calls and the six memo gates, off unless MOBILEGL_PIPE_STATS is set
- Plan B section 11 P0 asks for TracyPlot per-frame counters on both sides of the
  boundary, and section 2.3.1 adds the deliverable v1 did not have: DYNAMIC call
  counters. The static call-site counts everyone quotes (Espryt 124 / Magma 169) are
  not the per-draw cost, because every one of those paths is memo-gated; without a
  dynamic counter P2's verdict stays a guess. The tree had no per-frame byte or call
  measurement at all - MG_Util/Metrics is format arithmetic, and Tracy has zones but
  no plots.
- Cost when off: g_pipeStatsEnabled is a plain global Bool latched once in
  Initialize() right after MG_ConfigLoader::Init(), and every counting site is
  `if (PipeStats::Enabled()) ...` - one load of a hot global and one never-taken
  branch. The counters are relaxed atomics because buffer and texture staging are
  reachable from more than one thread; the off path never touches them.
- Eight byte classes (stage-buffer, stage-texture, stage-ubo-global, stage-ubo-named,
  stage-vertex-client, stage-index-client, persistent-map-push and the
  residual-value-block placeholder of section 6.3), six call classes and the six memo
  gates of section 2.3.1, each as a hit/miss pair. Names are minted now, including the
  two that stay 0 in P0, so no recorded baseline is invalidated by a later rename -
  which is why the name set is pinned by a test.
- Reporting: TracyPlot per counter per frame when TRACY_ENABLE; with
  MOBILEGL_PIPE_STATS=1 one MGLOG_I summary line every 120 frames and at teardown, plus
  a JSON dump to MOBILEGL_PIPE_STATS_FILE when that is set. MGLOG_I against the usual
  "MGLOG_D for non-critical" rule on purpose: the line has to survive an INFO build -
  the only build a device runs - it is at most one line per 120 frames, and it exists
  only when the operator asked for it.
- Summary lines report disjoint WINDOWS, not run totals: a run total over a workload
  that changes shape (load, then steady state) averages away the very number section
  2.3.1 wants an absolute value for.
- MOBILEGL_PIPE_STATS_FILE joins the six switches parsed in e48a3582; like them it
  needs no allow-list entry because InitializeAcceptedEnvVariables accepts every
  MOBILEGL_-prefixed variable by construction.
2026-09-05 20:16:49 -04:00
swung0x48 9bbf71990c [Build] (CI): add the pipe-gen check, the stdio-instrumentation gate and the citation lint
- Three P0 gates from plan B section 11, in one job that needs no build: a broken build
  must not be able to hide a drifted interface.
- pipe-gen-check regenerates G1-G7 and runs git diff --exit-code over
  MobileGL/MG_Pipe/generated. Since all seven generators read the same .def files, this is
  what makes drift between them impossible rather than merely unlikely.
- The stdio gate refuses fprintf(stderr and printf( under MG_Backend and MG_State. Nothing
  there matches today, so it lands with NO whitelist - verified with a negative control
  that fprintf(stderr and std::printf both trip it while snprintf does not. Per-draw
  instrumentation has been committed by accident before, once inside a mutex critical
  section, and MGLOG_D is the channel these trees are allowed to use because it compiles
  out in INFO builds.
- scripts/gen_pipe_dirty_surface.py reports the frontend mutation surface corollary 4
  needs covered: 926 mutator calls under MG_Impl/GLImpl over 73 distinct mutators, of
  which only 92 sit in a function that also reaches the backend. The other 834 are
  published by the NEXT verb, which is precisely the population that needs an aggregate
  generation. Informational in P0; it becomes a gate in P1 when there is a mapping file to
  diff against.
- scripts/check_doc_citations.py resolves every `path:line` citation against a git
  revision. It reproduces the failure that motivated it - the plan's first draft cited
  SamplerObject.h:468-492 in a 160-line file - and reports 84 unresolved citations out of
  1028 in docs/Disaggregated today, which is why CI runs it warning-only until those
  documents settle. --strict exits 1, verified in both directions.
2026-09-05 20:16:49 -04:00
swung0x48 2f8d0f0d51 [Feat] (Config): parse the six MOBILEGL_PIPE_* switches
- Appendix B of plan B. All six default to today's behaviour: PipePush 0 is "pull
  everything", verify and stats off, legacy memos ON so the first handle waves keep a real
  old-versus-new arm (B-R16), texel retain 0 because MipmapStorage already holds a complete
  CPU shadow so that cache buys latency and never correctness (section 7.5c), index mirror
  64 MiB.
- No allow-list edit is needed and the header says why: InitializeAcceptedEnvVariables
  accepts every MOBILEGL_ / LIBGL_ prefixed variable found in the environment, so a name
  with that prefix is visible to these queries by construction - the failure mode of a
  hand-maintained accepted list does not exist here.
- PipeLegacyMemos defaults ON, so it is read as a tri-state (QueryEnvQuirkOverride !=
  ForceOff) rather than as a plain truthy flag: unset must keep the memos and only an
  explicitly falsy value may drop them. Reading it with QueryEnvFlag would have inverted
  the default silently.
- QueryEnvUint64 is added alongside QueryEnvUint32 for the subsystem bitmask, accepting a
  0x prefix - a bitmask written in decimal is unreadable - and warning and falling back to
  the default on anything unparseable, exactly like its 32-bit sibling.
2026-09-05 20:16:49 -04:00
swung0x48 3363258908 [Test] (MGPipe): pin the catalogue arithmetic, the wire opcodes and the comparator
- Thirteen cases that need no GL context and no driver, so the fact that PipeCalls.def and
  the seven generated files agree is checked on every unit run rather than at review time.
- The catalogue count is one fact stated three times - the macro expansion,
  MGP_CALL_LIST_DOCUMENTED_COUNT and the two generated tables - and the per-class counts
  documented in the .def header are asserted individually, which is what caught kCtxState
  being 17 rather than 18 (set_texture_params carries kCtxObject, per the plan's own
  example in section 4.1).
- An uninstalled pipe must be all-null: that is what "this subsystem has not been migrated,
  keep pulling" means (section 4.1), so it is asserted rather than assumed.
- The comparator test pins the two properties the verify harness depends on: the FIRST
  differing field is named, and padding is not a field - two payloads that differ only in
  padding compare equal, while a nested array element does not.
- The poison test pins the per-verb behaviour a bitmap cannot express: a field filled for
  verb N is stale at verb N+1.
2026-09-05 20:16:49 -04:00
swung0x48 9c773182bb [Feat] (MGPipe): land the interface skeleton - the complete call catalogue, the payload PODs and the seven generators
- Plan B section 4 makes the frontend/backend boundary explicit and gives the backend its
  own state machine. This is the P0 deliverable of section 11: the whole catalogue exists
  from day one, placeholders included, because the wire opcode is a call's position in
  PipeCalls.def and record numbering must never churn.
- MG_Pipe/PipeCalls.def is the single source of truth: 68 unique calls as
  X(Name, Payload, Class, Flags). Its header reconciles that number with the plan's
  headline counts (section 4.4, appendix A), which double count - "CSO 15" names
  bind_sampler_states and set_sampler_views that the "set_* 17" list also names, "screen
  14" tabulates the query family that section 4.3 assigns to the context, and the
  "transfer 12" row enumerates 11 calls. Each reconciliation is written down next to the
  count rather than resolved silently.
- MGPipeHandles.h: the 8-byte {slot, gen} pair, dense per-kind slots, the reserved null
  and default-framebuffer handles, and the ShaderCso composite band (sections 4.2, 5.6.3).
  The two generations are documented as strictly separate, with the interface rule that no
  call may require the client to know MGGen.
- MGPipeTypes.h: every payload of section 4.5 as a flat POD with explicit padding, a
  trivial-copyability assertion and an exact sizeof assertion, because the wire records are
  memcpy'd and a field silently changing width is a protocol break no test would see.
  MGPCaps embeds DynamicBackendParameters by inclusion so a caps field added there needs no
  second edit here; its assertion is stated as a composition because that struct still
  carries SizeT. ResidualValueBlock is pinned at MGL_RESIDUAL_BLOCK_SIZE 1248, the ratchet
  that only ever goes down and reaches static_assert(... == 0) in P13 (section 6.3).
- MGPipeHostSpan.h keeps the one shape that changes with the transport isolated behind one
  predictable branch, with the kFromServerIndexMirror sentinel D-B7 needs.
- MGPipeCallbacks.h names the reverse channel as ten callbacks plus the forward terminator
  in the context table, replacing 95 poke sites across 17 methods (section 7.1).
- scripts/gen_pipe.py runs G1-G7 off those .def files. G1 asserts each table is EXACTLY its
  call count of function pointers; G3 pads every wire record to the stream's 8-byte
  granularity and checks size >= sizeof && size <= remaining && size % 8 == 0 before
  dispatch, fatally; G4 compares field by field (padding excluded, floats by bits) because
  a comparator with false positives is one nobody reads - DirectGLES.cpp says the same
  thing about its own memcmp of RenderStateParameters; G5 turns the accessor list into
  per-verb poison generations rather than a written-once bitmap, which is the only version
  that can see a field left over from the previous draw (section 6.2.2); G6 joins the 477
  read points of the vendored backend_read_inventory.md against Coverage.def and reports
  0 UNMAPPED (299 to a call, 167 signatures that become handle parameters, 6 reverse
  channel, 5 client-resolved); G7 pins the pipeline subset BY MEMBER NAME from what
  VulkanRenderer::ComputePipelineStateHash hashes today, computing no offsets in python.
- The generated files are committed so the build never depends on python; CI regenerates
  and diffs them.
2026-09-05 20:16:49 -04:00
swung0x48 8349babe90 [Docs] (Disaggregated): consolidate into a single MGPipe plan and drop the replica plan
- docs/Disaggregated/PLAN.md is now the one plan: the MGPipe design with the transport, control-plane, present, threading, EGL/process, monolith/build chapters inlined as real chapters (7-13) instead of references, sections renumbered 0-17 + appendices, every internal citation updated
- the replica-GLContext plan and its review record are removed at the user's request; REVIEW.md is the MGPipe design-competition and adversarial-review record only, with the comparison verdicts dropped and the remaining finding text reworded to the new section numbers
- day-43 GO/NO-GO now names its two outcomes (continue / shrink to headless tooling or re-evaluate) without any rollback path
2026-09-05 14:06:24 -04:00
swung0x48 1794ac94b1 [Docs] (Disaggregated): land plan B - MGPipe, a gallium-style explicit frontend/backend interface with a backend-owned state machine (client-allocated {slot,gen} handles, CSOs keyed on the pipeline subset with dynamic state pushed separately, set_* catalogue derived from the twins the two backends already keep, per-verb validate with aggregate generations, reverse channel as ten named callbacks plus a pull terminator, strangler migration behind PipeInputs with per-verb poison and a field-wise verify harness, three purity gates replacing byte identity), the item-by-item comparison against the replica plan A (memory, copies, drift surface, monolith benefit vs 4x effort and 7x later first frame) with a day-43 GO/NO-GO hedge, and the round-2 design-competition and adversarial-review record; mark plan A as superseded except for its inherited transport sections 2026-09-05 13:05:43 -04:00
swung0x48 8b31de2f8d [Docs] (Disaggregated): land the two-process split plan - server runs the unchanged backends against a replica GLContext driven by mutator replay, the client is the sole originator of every implicit-publish semantic (persistent-map push, MarkGpuWritten, texture dirty clear, XFB accounting, generated-mip allocation), SPSC shm ring with FlatBuffers structs on the hot path and tables on the control socket, two-way doorbells, present credit 1, spawn-only shipping build with an inproc CI variant, and a P0-P9 schedule gated on the existing unit/integration/retrace/CTS suites; plus the design-competition and adversarial-review record 2026-09-05 09:21:58 -04:00
swung0x48 50fb13430f [Merge] (MG_State, ShaderTranspiler, tools/cts): land the write-map landing and open-ended fp64 storage block fixes that take KHR-Single-GL45.subgroups to 100% on Magma 2026-09-05 05:43:34 -04:00
swung0x48 d4f8adcf6d [Tools, Test] (tools/cts, MG_Test): make the CTS runner's chunk timeout idle-based so a healthy 20-minute invocation is no longer killed and its in-flight case mis-recorded as a crash, and link MSVC test executables with /WHOLEARCHIVE so the dllimport gl* references in GetProcAddress.cpp resolve 2026-09-05 05:36:50 -04:00
swung0x48 795e08f7e6 [Fix, Test] (ShaderTranspiler): flatten fp64 storage blocks whose last member is a runtime array - the pass declined them, so the fp64 demotion re-derived ArrayStride 4 over the application's 8/16/32-byte double buffer and every double/dvecN data[] SSBO read raw words 2026-09-05 05:36:49 -04:00
swung0x48 1e7ecab4db [Fix, Test] (MG_State, BufferObject): land a non-persistent write map's staged bytes into a GPU-resident store at unmap and explicit flush instead of dropping them - SSBO binding and large-store adoption make resident stores reachable through glMapBufferRange, so every per-draw re-initialisation was silently lost 2026-09-05 05:36:48 -04:00
swung0x48 81b17c0b75 [Test] (ShaderTranspiler, Integration): pin the five reworked shapes - the read-only capture stage through the real link, the seeded carrier's ESSL declaration, the metadata-driven block redeclaration and its decline, the control-stage-less evaluation decline, and a carrier clearing an i64vec4 2026-08-28 06:22:12 -04:00
swung0x48 d1edf765f5 [Fix] (Link, Async): carry the resolved gl_PointSize capture request into the SPIR-V handoff and let a deferred verdict name its own severity - the demotion read the request off a reflection slice phase A never fills it into, so its forced carrier was dead code, and its decline reason replayed at a level no shipped build keeps 2026-08-28 06:22:12 -04:00
swung0x48 97e07190ac [Fix] (ShaderTranspiler): seed a forced point-size carrier nothing writes and decline a control stage whose live clip/cull distance would still print gl_PointSize - an ES front end deletes a never-written output, and SPIRV-Cross redeclares that block from member decorations rather than access 2026-08-28 06:22:11 -04:00
swung0x48 a4dcdf989e [Fix] (ShaderTranspiler): stop the point-size carrier landing on a location a live varying owns, and decline the evaluation stage a synthesized pass-through control stage cannot feed - an i64vec4 counted as one location, and a located input carrier trips both backends' pass-through guard 2026-08-28 06:22:11 -04:00
swung0x48 1c113e4b26 [Fix] (ShaderTranspiler): count a 64-bit integer vector as two locations in the XFB flattener's LocationSpan - it read 64-bitness off the element's float type alone, so an i64vec3/4 member packed the members after it onto locations it already owns 2026-08-28 06:22:11 -04:00
swung0x48 bf9cfb3079 [Test] (Integration): PointSizeDemotionScenario - the demoted value chain is client-invisible in both configurations, with pinned per-backend lanes and a log-latch arming guard 2026-08-28 06:22:10 -04:00
swung0x48 e1818d497a [Test] (ShaderTranspiler): pin the point-size demotion's shapes - capability stripped, carriers named and located past the program's varyings, byte-identical no-ops, whole-struct-copy decline, and the two new L1 key bits 2026-08-28 06:21:31 -04:00
swung0x48 d7f66722d1 [Fix] (ShaderTranspiler, Link, DirectGLES, DirectVulkan): demote tessellation/geometry gl_PointSize to an ordinary varying where the device cannot host the built-in - the value survives for gl_in reads and by-name capture, both backends' declines stay for shapes the pass refuses, and the verdict rides the L1 key 2026-08-28 06:21:30 -04:00
swung0x48 92dc41ebf9 [Test] (DirectVulkan, SelfTest): pin the probe's fence-timeout teardown against a fake driver, the never-worse arming refusal, and paused-span patch/instanced counting against an unpaused control 2026-08-28 06:19:41 -04:00
swung0x48 feea131d8b [Fix] (DirectVulkan, SelfTest): honour the primitives-generated probe's leak-on-timeout contract in both of its callers, count paused-span draws the frontend cannot price, refuse a substitute measured worse than the stream query, and derive the POST row's failure clause from the measurement 2026-08-28 06:19:40 -04:00
swung0x48 19f4402fbf [Test] (DirectVulkan): pin the primitives-generated probe's verdict and override mapping, and hold the reroute's GL answers and arming observable on the integration lane 2026-08-28 06:19:40 -04:00
swung0x48 1350031368 [Fix] (DirectVulkan): count GL_PRIMITIVES_GENERATED for transform-feedback-inactive draws - a bring-up probe measures the silent stream query and reroutes such draws through the dedicated primitives-generated query or a clipping-statistics pool, reported from the Vulkan POST 2026-08-28 06:19:39 -04:00
swung0x48 0ee3384b22 [Fix] (Espryt): land a SubData into an adopted store as a GPU-ordered copy - the in-place coherent write tore the frames still reading the old section bytes 2026-08-28 04:50:07 -04:00
swung0x48 ba3f8d6774 [Test] (Integration): pin the adopted mesh-arena store - cross-frame SubData visibility, readback identity, and the GPU-written readback 2026-08-28 04:19:11 -04:00
swung0x48 3327784fd0 [Fix] (Espryt): adopt mesh-arena-sized stores into coherent persistent maps at definition, and stop the flush tiers from re-synchronizing them 2026-08-28 04:19:11 -04:00
swung0x48 ff426da3a9 [Fix, Test] (MG_State, DirectVulkan): order host writes to an adopted store after recorded GPU work - a SubData issued after a dispatch landed in coherent memory before the deferred dispatch executed, so its increments overwrote the newer bytes; un-skip the DirectVulkan half of the SubData-after-dispatch scenario 2026-08-28 03:01:15 -04:00
swung0x48 08419a1fe6 [Chore] (Config): point the two stale comments at the renamed feature fields 2026-08-28 00:47:35 -04:00
swung0x48 5d51372c44 [Chore] (Config): triage backend-scoped env toggles under MOBILEGL_ESPRYT_ and MOBILEGL_MAGMA_ prefixes 2026-08-28 00:38:17 -04:00
swung0x48 7fd4550968 [Fix] (Espryt): copy before any FBO attach in the packed16 probe - attaching the array relayouts it to the plain order and was neutralizing the subject 2026-08-28 00:22:16 -04:00
swung0x48 971537058e [Fix] (Espryt): probe every allocation recipe of the packed16 shape - the Mali layout heuristic inverts between contexts, so one recipe cannot speak for the storage 2026-08-28 00:22:16 -04:00
swung0x48 dd98c450ad [Test] (SelfTest): model the params-before-upload escape so a params-first probe regression reads as a red test 2026-08-28 00:22:15 -04:00
swung0x48 5dbbbbd7eb [Fix] (Espryt): allocate the packed16 probe's textures uploads-first, the order a minted backend texture performs and the one the Mali layout choice keys on 2026-08-28 00:22:15 -04:00
swung0x48 5d140a41ce [Fix] (Espryt): re-arm the packed16 probe on the allocation-scoped mirror the device actually has and let any mirrored level trigger the widening 2026-08-28 00:22:15 -04:00
swung0x48 bf376b230f [CI] (Integration): rerun the buffer scenarios with the map flush disabled so the upload-ring tier keeps coverage 2026-08-28 00:15:15 -04:00
swung0x48 29599dcf90 [Fix] (DirectGLES): flush queued buffer ranges through a range-invalidating map first, so a partial write into a busy store is priced by the range 2026-08-27 23:54:19 -04:00
swung0x48 734fab9f90 [Test] (Integration): scope the SubData-after-dispatch readback case to DirectGLES, naming the DirectVulkan upload-ordering gap it exposed 2026-08-27 23:04:52 -04:00
swung0x48 2cd1809c29 [Fix] (Review): rebuild the packed16 probe on the CTS's three-level chains and let the POST row answer for the widening knob actually in force 2026-08-27 22:46:13 -04:00
swung0x48 faed498476 [Test] (IntegrationTest): shield the packed16 renderbuffer clear from an inherited scissor 2026-08-27 22:46:13 -04:00
swung0x48 8282eecbfa [Fix] (Espryt): store RGB565/RGB5_A1/RGBA4 as 8-bit channels where a POST probe measures the Mali packed16 array-mip field-order mirror 2026-08-27 22:46:12 -04:00
swung0x48 f4f3afb0b6 [Test] (Integration): pin that a queued SubData survives an immediate readback of a GPU-written buffer 2026-08-27 22:19:45 -04:00
swung0x48 a28da07641 [Fix] (DirectGLES): queue app buffer updates and flush them through a staged-copy upload ring instead of stalling in glBufferSubData 2026-08-27 22:19:44 -04:00
swung0x48 200c21336f [Fix] (Readback): size a cube-face pack buffer by one face, and give a cube view's face its owner layer 2026-08-27 22:05:23 -04:00
swung0x48 2c3fc583d5 [Test] (Readback): pin the one-face pixel pack buffer and a cube view's per-face layers 2026-08-27 21:58:51 -04:00
swung0x48 645a12d8bc [Fix] (Readback): read the cube face a readback names, and let glGetTextureSubImage name one 2026-08-27 21:54:47 -04:00
swung0x48 8e6acc5528 [Test] (Readback): pin that a cube map's readback answers the face it was asked for 2026-08-27 21:54:46 -04:00
swung0x48 525ffe0f14 [Test] (Review): pin the inverted override mapping, the probe's controls, and that the pinned-on lane is really armed 2026-08-27 21:06:01 -04:00
swung0x48 ad28d2b744 [Fix, Test] (Review): strip a block's member-level locations too, restore the probe's colour mask, and let the POST verdict follow the override 2026-08-27 20:15:31 -04:00
swung0x48 eab622388f [Test] (TranslationCache): pin the two interface-block location-strip flags as L2 key material 2026-08-27 20:07:49 -04:00
swung0x48 75e573c923 [Fix] (SelfTest): give the located-interface-block probe its own entry-point gate instead of the storage probe's 2026-08-27 19:56:26 -04:00
swung0x48 0d0ef13619 [Fix] (DirectGLES): run the interface-block location strip last, where its deliberately Vulkan-invalid module reaches no validator 2026-08-27 19:44:18 -04:00
swung0x48 23565fcacd [Fix, Test] (DirectGLES, SelfTest, MG_Test): probe the located-interface-block defect with its controls and cover the strip in both gates 2026-08-27 19:38:39 -04:00
swung0x48 5dc26e3e2c [Fix] (DirectGLES, ShaderTranspiler): drop the location qualifier from inter-stage interface blocks on a driver that loses their payload 2026-08-27 19:18:35 -04:00
swung0x48 90564aa82e [Test] (Review): give the geometry point-size case its own capability probe 2026-08-27 15:42:04 -04:00
swung0x48 7520607d47 [Fix] (Review): decline a point-size program the device cannot run, bound the compile-failure source dump, and correct the pass-through rationale 2026-08-27 15:42:04 -04:00
swung0x48 57635a9198 [Test] (Tessellation): unit and headless-GPU coverage for capturing a patch draw's per-vertex payload 2026-08-27 15:06:02 -04:00
swung0x48 c19d0f0b75 [Fix] (DirectVulkan): mirror gl_PointSize for capture and enable the feature its stages need 2026-08-27 15:06:02 -04:00
swung0x48 e42e7d00f5 [Fix] (DirectGLES): request the point-size extension a tessellation or geometry stage's ESSL needs 2026-08-27 15:06:01 -04:00
swung0x48 62695ee3c2 [Test] (Review): pin the single-sample sample mask, the completeness-flip sampled set, and the integer multisample placeholders 2026-08-27 13:17:56 -04:00
swung0x48 02cc0ce83c [Fix] (Review): scope the sample mask to a multisample target, version the sampled-set memo on completeness, clamp the mask word count, give the multisample placeholder every numeric domain, unwind the fixup revert one pass at a time, and bound the validator log 2026-08-27 13:03:51 -04:00
swung0x48 9e52a0b23e [Test] (DirectVulkan): pin the incomplete-default-texture draw, the unbound multisample sampler, and the unwritten redeclared gl_Position 2026-08-27 12:52:19 -04:00
swung0x48 9dee53337f [Fix] (DirectVulkan): give an unbound multisample sampler a placeholder, clear a multisample texture through a load-op pass, and plumb glSampleMaski into the pipeline 2026-08-27 12:52:19 -04:00
swung0x48 28c5badf8f [Fix] (SPIR-V): keep the entry-point interface consistent when the clip fixup and the XFB position mirror inject through gl_Position 2026-08-27 12:52:18 -04:00
swung0x48 01116f7b41 [Fix] (DirectVulkan): treat an incomplete sampled texture as unbound in the collect path too, and stop dereferencing a declined texture sync 2026-08-27 12:52:18 -04:00
swung0x48 05d627ba2d [Fix] (Xfb): never issue a transform feedback capture-point bind the application did not ask for, and error-check the driver span instead of losing it silently 2026-08-27 12:28:32 -04:00
swung0x48 ea5d52f126 [Test] (Tessellation): assert the patch-array cross-stage link outright now that the fork pin carries the guard 2026-08-27 10:52:00 -04:00
swung0x48 3c70b4fc0f [Fix] (Program): read an API colour index of zero as no override, matching the IO resolver and ProgramInterface 2026-08-27 10:52:00 -04:00
swung0x48 b6d6316333 [Fix] (Blend): decline a GL_SRC1_* factor on an incapable GLES driver even when blending is off 2026-08-27 10:51:59 -04:00
swung0x48 3c9ab5a68f [Test] (Blend): make the dual-source scenario's capability gate skip the case, not just the helper 2026-08-27 10:48:22 -04:00
swung0x48 532b5e9cc5 [Fix] (Program): let two fragment outputs share a colour number when their colour index differs 2026-08-27 10:48:22 -04:00
swung0x48 06605ed0ea [Test] (Tessellation): include the standard headers the link test uses directly 2026-08-27 10:48:21 -04:00
swung0x48 e1d5bdc4a5 [Fix] (Blend): decline an unsupported dual-source blend instead of throwing through the GL ABI 2026-08-27 10:48:21 -04:00
swung0x48 66867a41ba [Fix] (Program): count the tessellation control stage as a transform-feedback capture stage 2026-08-27 10:48:20 -04:00
swung0x48 ebff4b21f7 [Fix] (Query): tie the two tessellation pipeline-statistics targets to the GL_ARB_tessellation_shader advertisement 2026-08-27 10:48:20 -04:00
swung0x48 d4e7378868 [Chore] (ShaderTranspiler): pin glslang fork at d89cf443 (patch-array io-resize guard for the evaluation stage) 2026-08-27 10:22:37 -04:00
swung0x48 01d20e5c96 [Test] (Review): border-colour clamping, a readback across a mip gap, and blending on renderbuffer formats with no exact VkFormat 2026-08-27 08:35:06 -04:00
swung0x48 b04c67d9a8 [Test] (Review): pin the sampler getter matrix, the shadow-readback refusals, and the proxy and by-name parameter targets 2026-08-27 08:35:05 -04:00
swung0x48 685d83e3ec [Fix] (Review): bound a texture readback by the image's mip count, refuse cross-format depth/stencil copies, and probe the renderbuffer's real VkFormat 2026-08-27 08:35:05 -04:00
swung0x48 009b140691 [Fix] (Review): clamp integer and floating-point border colours to the sampled format's representable range 2026-08-27 08:35:05 -04:00
swung0x48 1f44e5bc1d [Fix] (Review): convert every scalar sampler query to the type the caller asked for instead of the other type's bit pattern 2026-08-27 08:35:04 -04:00
swung0x48 d9aebcba26 [Fix] (Review): refuse a CPU-shadow readback whose layout the pack path cannot produce, and apply the parameter-target rule to proxy and by-name spellings 2026-08-27 08:35:04 -04:00
swung0x48 0db666897e [Fix] (DirectGLES): key the border-colour sync memo on the authoritative representation, not just the float one 2026-08-27 08:35:03 -04:00
swung0x48 5f445e499f [Test] (Integration): integer border-colour sampling and clear-tex-image on a texture with no level 0 2026-08-27 08:35:03 -04:00
swung0x48 c52ebd5bf6 [Test] (Texture): pin the border-colour forms, the GL 4.6 conversion pair and the texparameter validation gaps 2026-08-27 08:35:02 -04:00
swung0x48 8e072bc793 [Fix] (DirectVulkan): answer a texture-image query from the CPU shadow when the texture has no VkImage 2026-08-27 08:35:02 -04:00
swung0x48 88ee75be0e [Fix] (DirectVulkan): resolve renderbuffer VkFormats through the shared texture table and police copy size-compatibility 2026-08-27 08:35:01 -04:00
swung0x48 0dbb4ceba8 [Fix] (DirectVulkan): resolve arbitrary and integer border colours through VK_EXT_custom_border_color, clamped to the sampled format 2026-08-27 08:35:01 -04:00
swung0x48 a94b3e0bd5 [Fix] (DirectGLES): forward an integer border colour through glTexParameterIiv/glSamplerParameterIiv instead of flattening it to float 2026-08-27 08:35:01 -04:00
swung0x48 764b6e044d [Fix] (Texture): reject illegal texparameter targets and sampler enum values, invert the integer border-colour read, and report multisample sampler state as INVALID_ENUM 2026-08-27 08:35:00 -04:00
swung0x48 c1d89de729 [Fix] (Sampler): carry GL_TEXTURE_BORDER_COLOR with its form, convert per GL 4.6 eq 2.2/2.3, and unify the name and scalar-pname error classes 2026-08-27 08:35:00 -04:00
swung0x48 c136384f97 [CI] (TraceReplay): keep the rd12-odinlite perf fixture out of the CI matrices until its archive is published 2026-08-27 08:23:04 -04:00
swung0x48 1920a3d16f [Test] (Review): pin the composite's linked capture list, the capture-stage rule, and the matrix uniform forms' link check 2026-08-27 06:00:54 -04:00
swung0x48 11f4b4bd3b [Fix] (Review): honour a SPIR-V module's transform-feedback decorations, take the composite's capture list from the linked snapshot, and reorder every glProgramUniformMatrix* link check 2026-08-27 05:56:45 -04:00
swung0x48 9ef33f4274 [Fix] (Review): bound copies by the requested level, reach every cube face, keep array layer counts, and give glSpecializeShader its spec error surface 2026-08-27 05:51:58 -04:00
swung0x48 e430e1b3be [Fix] (Texture): give glTexBuffer the sized-format check its TODO deferred and the target-taking forms their own INVALID_ENUM 2026-08-27 05:37:18 -04:00
swung0x48 e315d9e798 [Test] (GlSpirv): unit and headless-GPU coverage for glShaderBinary, glSpecializeShader and the SPIR_V_BINARY state 2026-08-27 05:37:18 -04:00
swung0x48 6f299372c6 [Feat] (Program): implement GL_ARB_gl_spirv - glShaderBinary, glSpecializeShader and the SPIR_V_BINARY state, feeding the module into the ordinary compile pipeline 2026-08-27 05:37:17 -04:00
swung0x48 be7bf21eb8 [Fix] (ShaderTranspiler): enforce the layout(binding) range rule for samplers, images and uniform/atomic-counter blocks, not only for SSBOs 2026-08-27 05:37:17 -04:00
swung0x48 0e4302b399 [Feat] (RenderState): implement glClipControl, glPolygonOffsetClamp and glTextureBarrier instead of stubbing them 2026-08-27 05:37:16 -04:00
swung0x48 c9c2dcb42a [Feat] (Query): accept the ARB_pipeline_statistics_query targets and report zero counter bits for them 2026-08-27 05:37:15 -04:00
swung0x48 2d938971b9 [Fix] (Getter): saturate combined uniform components in 64-bit and answer the missing GL4 state tokens 2026-08-27 05:37:15 -04:00
swung0x48 f7e23d5d83 [Fix] (DirectVulkan): generate mipmaps for cube-map-array and 1D-array targets, and stop shrinking an array's layer count down the chain 2026-08-27 05:37:14 -04:00
swung0x48 02fbb816e9 [Fix] (Texture): cube-map-array shape rules on glTexImage3D, GL_TEXTURE_SHARED_SIZE, and the unimplemented glCopyTexSubImage1D/3D 2026-08-27 05:37:14 -04:00
swung0x48 0e0882cfc6 [Test] (IntegrationTest): read cube faces back through a per-face FBO, which glGetTexImage's face token does not distinguish on DirectVulkan 2026-08-27 05:07:38 -04:00
swung0x48 de09646d5e [Fix] (DirectVulkan): one shared attachment layer count for the render pass and the clear key, and direct VkResult checks in the render-pass builder 2026-08-27 05:04:44 -04:00
swung0x48 747864777e [Test] (IntegrationTest): pin the layered 3D and cube-map-array attachment shapes and their per-layer routing 2026-08-27 04:37:29 -04:00
swung0x48 6fc3504bd9 [Fix] (Framebuffer): carry the layered flag through the GL_DEPTH_STENCIL_ATTACHMENT split 2026-08-27 04:37:28 -04:00
swung0x48 df1bcdba09 [Fix] (DirectVulkan): legal view types for layered 3D/cube attachments, and a real failure channel for the render-pass builder 2026-08-27 04:19:06 -04:00
swung0x48 843c61dee1 [Chore] (ShaderTranspiler): pin glslang fork at 7e255451 (GL_ARB_cull_distance registration and version-gated availability) 2026-08-27 04:01:43 -04:00
swung0x48 c0a3f4cc50 [Docs] (Getter): restate the uniform-binding airtightness argument against the per-stage sum, not the combined limit 2026-08-27 03:54:17 -04:00
swung0x48 06744fde7f [Fix] (Getter): close the review findings - six-stage combined uniform blocks, bounded uniform-block bindings, honest vertex-stream count, per-format sample ceilings 2026-08-27 03:54:16 -04:00
swung0x48 6cc9faf772 [Feat] (Program): report the geometry and tessellation link properties glGetProgramiv had no source for 2026-08-27 03:49:38 -04:00
swung0x48 7168f2ef77 [Fix] (Getter): answer the GL 4.6 limit surface honestly - tess/cull/subroutine pnames, TBuiltInResource drift, 84 UBO binding points, 64-bit GL_MAX_ELEMENT_INDEX, per-category sample truth 2026-08-27 03:47:16 -04:00
swung0x48 07669aacd4 [Fix] (ShaderTranspiler): fan #extension implications out to both new gates and bound the mid-line #version probe to its line 2026-08-27 03:35:58 -04:00
swung0x48 d52a3b2196 [Test] (IntegrationTest): sample-shading state reaches both backends without disturbing the draw 2026-08-27 03:35:58 -04:00
swung0x48 9e23016dd4 [Perf] (ShaderTranspiler): memchr the mid-line #version scan and gate it on an accepted directive 2026-08-27 03:35:57 -04:00
swung0x48 f1354dc25e [Fix] (DirectVulkan): hash the sample-shading state into the pipeline memo key 2026-08-27 03:35:56 -04:00
swung0x48 b7a694711a [Test] (IntegrationTest): gl_NumSamples scenario - the value must follow the draw framebuffer, not the link 2026-08-27 03:35:56 -04:00
swung0x48 2ae848ca19 [Fix] (ShaderTranspiler): route restored ES extension macros through glslang's custom preamble and detect a mid-line repeated #version 2026-08-27 03:35:55 -04:00
swung0x48 acf86d1fb6 [Feat] (RenderState): implement glMinSampleShading and the GL_SAMPLE_SHADING enable on both backends 2026-08-27 03:35:55 -04:00
swung0x48 07a0408a28 [Fix] (ShaderTranspiler): lower gl_NumSamples onto a reserved global-UBO uniform, restore ES preamble extension macros, tolerate a repeated #version 2026-08-27 03:35:47 -04:00
Swung0x48 8cf2e2aea9 [Fix] (Getter): answer GL_PATCH_DEFAULT_*_LEVEL from the float state in glGetBooleanv and write every component in glGetInteger64v 2026-08-27 03:18:15 -04:00
Swung0x48 31252cf0da [Fix] (Tessellation): bound the pass-through control-stage cache and stop baking "draw nothing" for levels GL clamps 2026-08-27 03:18:14 -04:00
Swung0x48 2635fe84b6 [Fix] (Tessellation): compare the default patch levels by bit pattern, so a NaN level stops re-linking the program on every draw 2026-08-27 03:18:13 -04:00
Swung0x48 e3163233a5 [Fix] (Framebuffer): apply the glFramebufferTexture error conditions to the 2D/3D/Layer attach paths and bound a view by its own level count 2026-08-27 03:18:12 -04:00
Swung0x48 eb9e4fdac1 [Fix] (DirectVulkan): report the index the arbitrary-restart rewrite cannot represent instead of silently drawing a different vertex 2026-08-27 03:18:10 -04:00
Swung0x48 90b7a689c5 [Fix] (Backend): resolve primitive restart per draw - never for a non-indexed one, and never on an index the type cannot hold 2026-08-27 03:18:09 -04:00
Swung0x48 e69e939d1a [Fix] (ProgramLink): fail the link when a tessellation control stage declares more output vertices than GL_MAX_PATCH_VERTICES 2026-08-27 02:11:23 -04:00
Swung0x48 b675e2a0b0 [Fix] (Drawing): refuse a draw whose program runs a geometry or tessellation stage with no vertex shader 2026-08-27 02:11:21 -04:00
Swung0x48 6979926a6f [Fix] (Framebuffer): the four glFramebufferTexture error conditions the DSA sibling already implemented 2026-08-27 02:11:20 -04:00
Swung0x48 d5286e69b6 [Feat] (Tessellation): implement glPatchParameterfv and bake the default levels into both pass-through control stages 2026-08-27 02:11:19 -04:00
Swung0x48 0f2fcbc469 [Fix] (Backend): honour desktop GL_PRIMITIVE_RESTART with an arbitrary index instead of throwing through the C GL ABI 2026-08-27 02:11:18 -04:00
swung0x48 18fccdd796 [Feat] (Backend): advertise OpenGL 4.6 (target version + OpenGL44/45/46 tokens) on both backends 2026-08-26 23:48:36 -04:00
swung0x48 0d2fceab0e [Docs] (DeviceBench): note session.sh leaves frequency pins active by design 2026-08-26 05:52:34 -04:00
swung0x48 82decded58 [Test] (TraceReplay): minecraft-1.21.4-rd12-odinlite-in-world perf fixture (campaign benchmark scene) 2026-08-26 05:52:33 -04:00
swung0x48 d04a3394de [Feat] (DeviceBench): launch retry loop (signal-34 JVM flake), session.sh runner, MSYS path-conversion guards 2026-08-26 05:52:32 -04:00
swung0x48 33eabfc2ff [Perf] (DirectGLES): upload the union dirty box instead of scatter rects when staging through the unpack ring (each PBO-sourced glTexSubImage is a GPU copy job on Mali; ~100 sprite rects per tick cost +6ms/frame of GPU time) 2026-08-26 05:52:31 -04:00
swung0x48 56e5d13dc9 [Perf] (DirectGLES): stage glTexSubImage uploads through a persistent-mapped unpack PBO ring (Mali blocks the render thread on in-flight destination textures) 2026-08-26 05:52:30 -04:00
swung0x48 34b7cc772f [Fix] (Build): compile fordebug native code as RelWithDebInfo (debuggable APK was shipping an -O0 libMobileGL.so, invalidating all performance measurements on this flavor) 2026-08-26 02:18:47 -04:00
swung0x48 c1d6a3c908 [Feat] (TraceReplay): benchmark mode with per-frame timing for device fixtures 2026-08-26 02:17:35 -04:00
swung0x48 386bd7e461 [Feat] (DeviceBench): scripted in-game FPS benchmark harness (FCLFPS logcat sampling, MTK ppm frequency pinning, thermal gate, GPU busy telemetry) 2026-08-26 01:51:04 -04:00
swung0x48 12df061e0b [Test] (SelfTest): model the vertex-input-location and layered-blit defects in the fake driver and pin both probes' controls 2026-08-25 05:57:58 -04:00
swung0x48 d83a48da5c [Fix] (SelfTest): read the probe-saved texture bindings off the same unit the restore puts them back on 2026-08-25 05:40:08 -04:00
swung0x48 b3100b0de5 [Fix] (DirectGLES): take the layered-blit substitute's colour destination from the draw buffer that is actually enabled, and leave self-overlapping copies alone 2026-08-25 05:38:12 -04:00
swung0x48 1cde801a01 [Fix] (DirectGLES): blit onto a non-zero array layer with glCopyImageSubData where the driver ignores the destination layer 2026-08-25 05:27:46 -04:00
swung0x48 52c050131e [Fix] (DirectGLES): advertise the vertex attributes whose layout(location) the driver's compiler will actually accept 2026-08-25 05:21:50 -04:00
swung0x48 ebb8a4cebf [Fix] (DirectGLES, DirectVulkan): answer GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT with the storage limit, not the uniform one 2026-08-25 05:21:42 -04:00
swung0x48 5398fb4289 [Fix] (IntegrationTest): report the backend that actually comes up when MOBILEGL_BACKEND_TYPE is unset
HeadlessGL reported the literal "<unset>" as the backend name when the variable was
not set, while MG_ConfigLoader::InitBackendType defaults it to DirectGLES and brought
DirectGLES up. Every ctest entry sets the variable through its ENVIRONMENT property,
which is why nothing ever noticed - but run straight from an adb shell, where nothing
sets it, the name matched neither backend and every case gated on DirectGLES skipped
as though DirectGLES were not running. On an Adreno 830 that silently disabled the
gl_ViewportIndex emulation control and the DirectGLES-only attachment scenarios.
2026-08-25 04:52:21 -04:00
swung0x48 7f2ca68615 [Test] (IntegrationTest): take the shader-compiler and viewport quirks from the environment instead of internal symbols
Android links this module against the SHIPPING libMobileGL.so on purpose, so the
on-device run validates the real artifact - and that library is -fvisibility=hidden.
Six symbols the scenarios reached for were therefore undefined and the executable
could not be linked at all: MG_Config::Features, SetAsyncShaderCompileSuspended,
ShaderCompilePool::Get/GetThreadCount/SetMaxConcurrency and
AsyncShaderCompileEnabled. All six are gone rather than exported.

CompilerThreadScope now restores the pool with glMaxShaderCompilerThreadsKHR
(0xFFFFFFFF), which MaxShaderCompilerThreadsKHR_State defines as exactly the two
steps it used to perform by hand, and AsyncAndSyncProgramsRenderIdenticalFrames
picks its two modes with the same entry point - a zero count compiles inline, a
nonzero one lifts that - so the switching is now itself under test.

ExtensionStringMatchesTheConfiguration stops deriving its expectation from
AsyncShaderCompileEnabled(), the very function the backends gate the extension
string on: it was asserting the implementation against itself and would have passed
however wrong both halves were. The expectation is now MOBILEGL_ASYNC_SHADER_COMPILE
as the process inherited it, and the case skips where that is unset because the
built-in default is a value only the implementation knows.

Both-mode coverage in one ctest run is preserved by registration rather than by
in-process forcing, and is wider than before. New entries, each APPENDING to the
common/Vulkan environment so the EGL-vendor and ICD pinning is not lost - a ctest
ENVIRONMENT property replaces the job environment rather than adding to it:
DirectGLES./DirectVulkan.AsyncOn. run all of AsyncCompileScenario with
MOBILEGL_ASYNC_SHADER_COMPILE=1; .AsyncOff. run the extension case with =0, which
asserts the withdrawn side that nothing covered before;
.OptimisticShaderStatus. run the Iris-shaped case with
MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1 (its own entries because the quirk is not
neutral for the rest of the scenario); DirectGLES.NoViewportArrayEmulation. runs the
emulation control with MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0. Run from a device
shell with nothing set, the ambient configuration runs and the rest skip cleanly.

1836 -> 1853 ctest entries, all green.
2026-08-25 04:52:14 -04:00
swung0x48 b12ef4d717 [Test] (DirectVulkan): ask each unbound-descriptor case only for the limit its own resource kind needs 2026-08-25 03:44:36 -04:00
swung0x48 724755d9df [Test] (DirectVulkan): pin the image-uniform numeric domain the unbound placeholder is chosen from 2026-08-25 03:44:36 -04:00
swung0x48 59f7059bf4 [Fix, Test] (DirectVulkan): keep the draw when a program declares an image-backed resource the application left unbound 2026-08-24 05:31:15 -04:00
swung0x48 4446c861be [Fix, Test] (DirectVulkan): keep the draw when a program declares a storage block the application left unbound 2026-08-24 04:57:05 -04:00
swung0x48 602da1d131 [Merge] (GLImpl, DirectGLES, DirectVulkan): complete direct state access and the compressed-texture family 2026-08-22 22:47:08 -04:00
swung0x48 06bbaf32b1 [Merge] (GLImpl, DirectGLES, DirectVulkan): take the extension advertisements under the DSA completion 2026-08-22 22:42:47 -04:00
swung0x48 9bcf0a15a0 [Docs] (DirectGLES, DirectVulkan): correct what advertising cube map arrays actually unlocks 2026-08-22 22:03:26 -04:00
swung0x48 26e5a946ac [Test] (GLImpl): pin the DSA name rule on the compressed 3D by-name entry point 2026-08-22 21:56:51 -04:00
swung0x48 dc543fa905 [Feat, Test] (DirectGLES, DirectVulkan, SelfTest): advertise the implemented extensions that were never named 2026-08-22 21:54:17 -04:00
swung0x48 f559d68728 [Feat, Test] (GLImpl): store and patch compressed 3D texture images, and wire the 1D/3D named entry points 2026-08-22 21:49:33 -04:00
swung0x48 48968a663f [Fix, Test] (GLImpl, Util): let a buffer clear take a GL_INT pattern into a normalized format 2026-08-22 21:49:26 -04:00
swung0x48 0fdcb5d6c4 [Feat] (DirectGLES, DirectVulkan): advertise GL_ARB_sync and GL_ARB_shader_atomic_counters 2026-08-22 21:45:13 -04:00
swung0x48 6b25e7a7e3 [Fix] (GLState): report the storage flags glBufferData implies 2026-08-22 21:45:13 -04:00
swung0x48 50d260c840 [Fix] (GLImpl): reject memory-barrier bits the spec does not define 2026-08-22 21:45:13 -04:00
swung0x48 6a8bf4c03c [Fix, Test] (DirectVulkan): resolve a lowered atomic-counter block from the atomic-counter binding points 2026-08-22 21:45:13 -04:00
swung0x48 01098e9dd7 [Fix] (GLImpl): raise the errors ARB_sync specifies for ClientWaitSync, WaitSync and GetSynciv 2026-08-22 21:45:12 -04:00
swung0x48 82244e9048 [Merge] (GLState, GLImpl, DirectGLES, DirectVulkan, ShaderTranspiler): land texture views, the 4.3 version claim and the extension unlocks 2026-08-22 14:28:37 -04:00
swung0x48 7bd2f08313 [Fix] (DirectGLES): stop advertising GL_ARB_texture_view, whose functional half fails on a host driver that has EXT_texture_view 2026-08-22 13:40:35 -04:00
swung0x48 121b99f8c3 [Fix, Test] (DirectVulkan, GLImpl): size a 1D-array texture's mip chain and layered attachment by the axis that carries its layers 2026-08-22 12:52:10 -04:00
swung0x48 e8d79344d6 [Fix] (DirectVulkan): resolve a cube-map-array sampler to the cube-map-array texture target 2026-08-22 12:47:13 -04:00
swung0x48 88be35c9ba [Fix, Test] (Util): widen a three-channel 16-bit texture to its same-width four-channel sibling 2026-08-22 12:41:18 -04:00
swung0x48 60808b6cf2 [Feat] (DirectGLES, DirectVulkan): advertise base-vertex draws, instanced arrays, KHR_debug and the 4.1-4.3 version tokens 2026-08-22 12:41:18 -04:00
swung0x48 eb2f14e55a [Fix, Test] (GLImpl): validate count, instancecount, drawcount, type and range on the base-vertex draw family 2026-08-22 12:41:18 -04:00
swung0x48 0eb5d54bb8 [Fix, Test] (GLState, ShaderTranspiler): report GL's default binding of zero for an unqualified uniform block 2026-08-22 12:41:18 -04:00
swung0x48 6a2e9dc791 Merge branch 'target-gl43' of /mnt/c/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL into gl43-claim 2026-08-22 11:42:58 -04:00
swung0x48 d5aceebd7b Merge branch 'dev' of /mnt/c/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL into gl43-claim 2026-08-22 11:42:50 -04:00
swung0x48 473d9951b7 [Fix, Test] (DirectVulkan, GLState): apply a texture view level and layer window at every subresource boundary 2026-08-22 11:17:09 -04:00
swung0x48 13783e3aec [Feat] (DirectGLES, DirectVulkan): raise the advertised OpenGL target version to 4.3 2026-08-22 10:53:45 -04:00
swung0x48 6bb844b1c1 [Feature, Test] (GLImpl): give KHR_debug a real group stack and object labels 2026-08-22 10:42:49 -04:00
swung0x48 6162603072 [Feature, Fix, Test] (GLState, GLImpl, DirectVulkan, DirectGLES): implement glTextureView over shared texture storage 2026-08-22 10:41:52 -04:00
swung0x48 666f150202 [Fix] (SelfTest): stop optional-capability failures from declaring the whole backend unsupported, and log the POST report in chunks 2026-08-22 09:08:13 -04:00
swung0x48 2f62970dd5 [Refactor] (SelfTest): give every POST capability row a PASS/WARN/FAIL verdict and keep INFO for identity 2026-08-22 08:59:03 -04:00
swung0x48 dcb568d445 [Feature, Test] (SelfTest): probe the four remaining known driver bugs from the POST 2026-08-22 08:45:05 -04:00
swung0x48 a5f36c8f8d [Feature, Test] (SelfTest): add a Known Driver Bugs POST section and probe the geometry write-after-emit drop 2026-08-22 07:49:17 -04:00
swung0x48 1ebe9d11c5 [Fix, Test] (GLState, DirectGLES): stop calling a multisample texture filter-incomplete so it still binds 2026-08-22 07:18:11 -04:00
swung0x48 a8bb63950d [Fix, Test] (ShaderTranspiler): relocate a late length constant so an offset atomic-counter block still flattens 2026-08-22 07:18:11 -04:00
swung0x48 7d68a17774 [Fix, Test] (DirectGLES): give a split buffer image its own view so the sampler still sees whole texels 2026-08-22 06:16:52 -04:00
swung0x48 415645ccdd [Merge] (ShaderTranspiler, DirectGLES): land the normalized-format carriers and the buffer-image split 2026-08-22 04:47:39 -04:00
swung0x48 e6d03eb2a1 [Merge] (DirectGLES): take the image-alias rename under the format carriers 2026-08-22 04:44:41 -04:00
swung0x48 5e29e7d266 [Feat, Test] (ShaderTranspiler, DirectGLES): split a non-core buffer image by its subscript instead of losing the stage 2026-08-22 04:30:06 -04:00
swung0x48 7eac33d17b [Feat, Fix, Test] (ShaderTranspiler, DirectGLES): carry the seven normalized image formats as their own codes in rgba16ui 2026-08-22 04:17:46 -04:00
Swung0x48 8f19ce6fa7 [Fix, Test] (DirectGLES): rename an image SPIRV-Cross already qualified so two stages cannot merge it 2026-08-22 03:46:35 -04:00
swung0x48 d0f7fb99db [Feat, Test] (ShaderTranspiler, DirectGLES): carry rgb10_a2ui storage images in rgba16ui and split its packed upload 2026-08-22 03:21:48 -04:00
swung0x48 d4247db6c3 [Fix, Test] (ShaderTranspiler, GLImpl, ProgramState, DirectVulkan): keep fp64 where the backend consumes it natively 2026-08-22 00:57:06 -04:00
swung0x48 e4f41e0fd3 [Merge] (ShaderTranspiler, DirectGLES): land the fp64 block layout, colour-index and readonly-writeonly repairs 2026-08-21 22:24:38 -04:00
swung0x48 9cf340cbef [Merge] (DirectGLES, ShaderTranspiler): take the viewport routing and image repairs under the fp64 and qualifier fixes 2026-08-21 22:21:17 -04:00
swung0x48 348a30a816 [Fix, Test] (ShaderTranspiler): keep a storage block with doubles at the byte layout it was bound with 2026-08-21 22:15:05 -04:00
swung0x48 b5e0ada97e [Merge] (DirectGLES, ShaderTranspiler, Config): land the viewport-array routing emulation 2026-08-21 22:13:50 -04:00
swung0x48 cb27ac7761 [Merge] (DirectGLES): take the image-widening repairs under the viewport routing 2026-08-21 22:10:53 -04:00
swung0x48 38c56a3d38 [Test] (DirectGLES, IntegrationTest): run the viewport-array scenarios on Espryt and pin the routing rewrites against a negative control 2026-08-21 22:08:33 -04:00
swung0x48 908172ba0f [Feat] (DirectGLES, Config, ShaderTranspiler): route gl_ViewportIndex on Espryt by replaying a draw per distinct viewport state 2026-08-21 22:08:25 -04:00
swung0x48 e18bac8cb2 [Fix, Test] (ShaderTranspiler, DirectGLES): never widen a buffer image - its texels are the application buffer, not storage we can reallocate 2026-08-21 22:02:23 -04:00
swung0x48 7b0f443d3a [Fix, Test] (ShaderTranspiler, DirectGLES): drop the inert readonly+writeonly pair a storage block cannot carry in ESSL 2026-08-21 21:43:42 -04:00
swung0x48 f1b4a5e07f [Fix, Test] (ShaderTranspiler, DirectGLES): stop printing the default fragment-output colour index into ESSL 2026-08-21 21:42:31 -04:00
swung0x48 f3cd4091bf [Fix, Test] (DirectGLES): decode the packed r11f_g11f_b10f shadow into the float level its rgba16f carrier is uploaded as 2026-08-21 21:38:45 -04:00
swung0x48 529d26f38f [Fix] (DirectGLES): arm the image-format widening for r11f_g11f_b10f in the reflection gate too 2026-08-21 21:27:14 -04:00
swung0x48 9bd125aeec [Fix, Test] (ShaderTranspiler): carry r11f_g11f_b10f storage images in rgba16f instead of losing the stage 2026-08-21 21:21:54 -04:00
swung0x48 ece9491d4b [Test] (ShaderTranspiler): pin the two capture holes the side-by-side corpus run found 2026-08-21 13:55:09 -04:00
swung0x48 51b4abd801 [Docs] (ShaderTranspiler, GLState): retire the comments that still describe the lexical side channels 2026-08-21 13:53:17 -04:00
swung0x48 cbb616093b [Refactor, Test] (ShaderTranspiler, GLState): take what the relaxed parse destroys from glslang instead of scanning the source 2026-08-21 13:51:22 -04:00
swung0x48 e5846569ca [Refactor] (ShaderTranspiler): bump the vendored glslang for the uniform-location snapshot and the atomic-counter offset check 2026-08-21 13:51:21 -04:00
swung0x48 194c2f189b [Merge] (ShaderTranspiler): land the macro-spelled storage-block binding repair 2026-08-21 12:16:52 -04:00
swung0x48 7de7cfc6eb [Fix, Test] (ShaderTranspiler): read a storage block's macro-spelled binding as declared, not as absent 2026-08-21 12:09:03 -04:00
swung0x48 03e69fc9ef [Merge] (ShaderTranspiler): land the single-implementation subroutine lowering and the imageSize select ladder 2026-08-21 11:59:49 -04:00
swung0x48 ee74c8ea3a [Merge] (DirectGLES): land the post-relink rebind repair 2026-08-21 11:59:49 -04:00
swung0x48 54bbe805e5 [Merge] (GLState): land the GL block and uniform enumeration repair 2026-08-21 11:59:49 -04:00
swung0x48 6e2a3b3496 [Fix, Test] (GLState): stop enumerating buffer variables as GL uniforms 2026-08-21 11:55:29 -04:00
swung0x48 f5a0779385 [Fix, Test] (GLState): keep the atomic-counter and storage blocks out of the GL uniform-block list 2026-08-21 11:54:45 -04:00
swung0x48 86fdc68efa [Fix, Test] (ShaderTranspiler): let the image-array select ladder carry an imageSize query, not just a read 2026-08-21 11:49:30 -04:00
swung0x48 1185265e22 [Fix, Test] (DirectGLES): re-bind the driver program after a relink so a stage the relink added reaches the draw 2026-08-21 11:47:03 -04:00
swung0x48 77c05b151a [Fix, Test] (ShaderTranspiler): lower a single-implementation GLSL subroutine to a forwarding call 2026-08-21 11:45:49 -04:00
swung0x48 fdbe0b3117 [Fix, Test] (DirectGLES, DirectVulkan, GLImpl, GLState): ask the last link, not the live attach list, what stages a program has 2026-08-21 08:01:48 -04:00
swung0x48 9c9739e1c3 [Merge] (DirectGLES, GLState, GLImpl, ShaderTranspiler): land GL43 wave6 and wave7 2026-08-21 07:15:13 -04:00
swung0x48 87548ae78a [Merge] (DirectGLES, GLState, GLImpl, ShaderTranspiler): land GL43 wave6 and wave7 with the image-uniform naming repair 2026-08-21 07:11:00 -04:00
swung0x48 2a902ff58c [Fix, Test] (ShaderTranspiler): bound the whole loop nest a fragment-output index marks for unrolling 2026-08-21 07:04:19 -04:00
swung0x48 a79eadd724 [Fix, Test] (ShaderTranspiler): bound the whole loop nest a resource-array index marks for unrolling 2026-08-21 07:04:19 -04:00
swung0x48 518e9c7796 [Fix, Test] (DirectGLES): read an image array subscript unsigned literal as the element index it is 2026-08-21 06:57:59 -04:00
swung0x48 a4c11f2603 [Fix, Test] (DirectGLES): arm the image-format widening for a baked format on every driver 2026-08-21 06:54:39 -04:00
swung0x48 37a656dedc [Fix, Test] (DirectGLES): name a repaired image uniform after its repair, not after its stage 2026-08-21 06:51:12 -04:00
swung0x48 3cc6b88767 [Fix, Test] (GLState): keep named-block members out of the GL uniform location pool 2026-08-21 06:36:47 -04:00
swung0x48 b32c35a113 [Fix, Test] (GLImpl): ask the stage, not GL_NONE, whether a geometry shader is active 2026-08-21 06:20:54 -04:00
swung0x48 8587b83be3 [Fix, Test] (ShaderTranspiler, DirectGLES): make every emitted image-array subscript a compile-time constant 2026-08-21 06:11:04 -04:00
swung0x48 0cef345d61 [Fix, Test] (GLState): give an atomic counter array its packed stride of four 2026-08-21 06:10:47 -04:00
swung0x48 31367de628 [Test] (MG_IntegrationTest): cover a storage block's default binding where another resource competes for it 2026-08-21 05:27:45 -04:00
swung0x48 bc4b62026a [Docs] (ShaderTranspiler): record the byte-exact fp64 block-layout evidence and its one misleading artifact 2026-08-21 05:24:49 -04:00
swung0x48 50da7de737 [Fix, Test] (DirectGLES, ShaderTranspiler): synthesize the pass-through tessellation control stage ES requires 2026-08-21 05:23:57 -04:00
swung0x48 d9def5c1bb [Fix, Test] (GLImpl, GLState): give a storage block with no binding qualifier GL's default binding of zero 2026-08-21 05:13:58 -04:00
swung0x48 21a4c8aa95 [Fix, Test] (ShaderTranspiler, DirectGLES): widen the offset and gradients of a 1D sampler lookup for ESSL 2026-08-21 04:57:52 -04:00
swung0x48 6317066add [Fix, Test] (DirectGLES): reach an image array's non-consecutive units by widening the array over their span 2026-08-21 04:38:59 -04:00
swung0x48 02b59bef80 [Fix, Test] (DirectGLES): give every repaired image uniform a per-stage name so no linker can merge two stages' qualifiers 2026-08-21 04:28:44 -04:00
swung0x48 668f3e90c9 [Fix] (DirectGLES, ShaderTranspiler): widen the formats SPIRV-Cross refuses to print even where GL_NV_image_formats exists 2026-08-21 04:13:01 -04:00
swung0x48 07d6277f87 [Test] (MG_IntegrationTest): cover GL's missing-channel semantics for a non-core image format 2026-08-21 03:54:16 -04:00
swung0x48 b164692387 [Test] (ShaderTranspiler): retarget the bake-decline pin at the formats no core carrier rescues 2026-08-21 03:48:59 -04:00
swung0x48 80ea44573e [Test] (ShaderTranspiler): pin the ESSL a widened image module emits, on both SPIRV-Cross failure modes 2026-08-21 03:34:27 -04:00
swung0x48 2a7d6f2e16 [Perf] (DirectGLES, ShaderTranspiler): fold the image-widening gate into the shared SPIR-V probe 2026-08-21 03:24:09 -04:00
swung0x48 3a12f6d4f3 [Fix, Test] (DirectGLES, ShaderTranspiler): emulate the 17 exactly-carriable non-core image formats by channel widening 2026-08-21 03:21:09 -04:00
swung0x48 36b9d26b9d [Fix] (DirectVulkan): match the 2_10_10_10 storage image view format to the texture's own 2026-08-21 02:54:47 -04:00
swung0x48 4b41f01b68 [Merge] (DirectGLES, GLState, ShaderTranspiler): land GL43 wave5 with its two new passes inside the L2 boundary 2026-08-21 00:43:39 -04:00
swung0x48 e81e938bb8 [Docs] (GLImpl): name the right copy_image conformance case in the 1D-array bounds note 2026-08-21 00:28:22 -04:00
swung0x48 5fa849674e [Fix] (DirectVulkan): gate the GL_DOUBLE vertex narrowing on the same fp64 flag the shader demotion uses 2026-08-21 00:28:03 -04:00
swung0x48 aed10f65a6 [Fix, Test] (GLImpl, MG_IntegrationTest): enforce the tessellation draw-mode rules and waive the XFB mode match for it 2026-08-21 00:23:15 -04:00
swung0x48 a687fc4577 [Fix] (ProgramState): stop the uniform-location grow path minting locations past GL_MAX_UNIFORM_LOCATIONS 2026-08-21 00:19:28 -04:00
swung0x48 ef66aea73b [Fix, Test] (GLImpl, DirectGLES, DirectVulkan): address a 1D array's copy-image layers on Z, not Y 2026-08-21 00:15:01 -04:00
swung0x48 c129cdec2d [Fix, Test] (ShaderTranspiler, ProgramState): reject an out-of-range atomic-counter offset at compile 2026-08-21 00:10:53 -04:00
swung0x48 7a7340ebe2 [Fix, Test] (GLImpl, ProgramState): answer the classic uniform queries for atomic counters at GL level 2026-08-21 00:07:07 -04:00
swung0x48 325ba07776 [Fix, Test] (ShaderTranspiler): route every sub-array of an array-of-arrays uniform to its own UBO offset 2026-08-21 00:02:30 -04:00
swung0x48 7aa91e8024 [Fix, Test] (DirectGLES, DirectVulkan): narrow GL_DOUBLE vertex arrays to float32 instead of dropping them 2026-08-20 23:54:49 -04:00
swung0x48 1b5a39473e [Fix, Test] (ShaderTranspiler, DirectGLES): flatten the atomic-counter block's declared offsets for ESSL 2026-08-20 23:39:26 -04:00
swung0x48 0e5f591cfa [Docs] (ShaderTranspiler): name the conformance cases fp64 block demotion costs 2026-08-20 23:26:18 -04:00
swung0x48 79336c5ccc [Fix, Test] (DirectGLES): re-issue the indexed binding of a storage buffer whose store was regrown 2026-08-20 23:16:08 -04:00
swung0x48 be45dbcf54 [Test] (MG_IntegrationTest): pin a non-constant index into an array of storage blocks 2026-08-20 23:15:48 -04:00
swung0x48 f7dfa01c18 [Fix, Test] (ShaderTranspiler, DirectGLES): make every array-of-storage-blocks index a constant for ESSL 2026-08-20 23:08:38 -04:00
swung0x48 50efa4410a [Test] (MG_IntegrationTest): dispatch an imageAtomicAdd against the two 1D image targets 2026-08-20 22:55:51 -04:00
swung0x48 bea3086b41 [Diagnostic, Test] (DirectGLES): name the image-uniform split as a cause when the backend link fails 2026-08-20 22:53:25 -04:00
swung0x48 8ae93c837d [Fix, Test] (DirectGLES): order the split image pair's store before its load with memoryBarrierImage 2026-08-20 22:49:05 -04:00
swung0x48 4154f2e941 [Fix, Test] (ShaderTranspiler, DirectGLES): widen a non-arrayed 1D storage image's atomic coordinate 2026-08-20 22:46:32 -04:00
swung0x48 cd07d42a47 [Fix, Test] (RenderState, DirectGLES, MG_IntegrationTest): tell a deliberately empty scissor box apart from one that was never written 2026-08-20 22:27:14 -04:00
swung0x48 ae0373eb48 [Merge] (DirectGLES, ShaderTranspiler): land GL43 wave4 with the interface-block rename inside the L2 boundary 2026-08-20 21:13:55 -04:00
swung0x48 7480bf4490 [Perf] (CTS-Harness): add a --cpu-mask switch and pin glcts to the big cluster by default 2026-08-20 21:03:54 -04:00
swung0x48 54b206d90c [Test, Bench] (ShaderTranspiler): pin the parse-verdict memo and measure the deferred parse 2026-08-20 18:53:57 -04:00
swung0x48 5daf7bf093 [Perf] (ShaderTranspiler, ProgramState): memoize the glslang parse verdict so a repeated compile skips the parse 2026-08-20 18:53:57 -04:00
swung0x48 a8228ca287 [Merge] (ShaderTranspiler, GLState, DirectGLES): land dev GL43 wave2/wave3 under the translation cache 2026-08-20 18:03:06 -04:00
swung0x48 8b827bd2ce [Fix, Test] (TextureFormatProcessor, DirectGLES, MG_IntegrationTest): give every unrenderable signed-normalized colour attachment an exact float substitute 2026-08-20 17:17:31 -04:00
swung0x48 6aa161fee7 [Fix, Test] (DirectGLES, ShaderTranspiler, MG_IntegrationTest): spell an interface block declared in both directions once per producing stage 2026-08-20 16:56:58 -04:00
swung0x48 48a70fea81 [Fix, Test] (DirectGLES, PixelStoreProcessor, MG_IntegrationTest): read a packed level's stored words instead of trusting the shadow 2026-08-20 16:15:52 -04:00
swung0x48 6ea4f32635 [Fix, Test] (TextureFormatProcessor): store the desktop-only low-bit formats without a driver requantization 2026-08-20 16:04:38 -04:00
swung0x48 dc1fffb041 [Fix, Test] (GLImpl): bound glCopyImageSubData's region against both images 2026-08-20 16:02:08 -04:00
swung0x48 d24d5b5ccd [Fix, Test] (BackendLoader, DirectGLES, DirectVulkan, GLImpl): answer the layer and viewport-index provoking-vertex conventions from the backend 2026-08-20 15:40:25 -04:00
swung0x48 51883cf1a3 [Fix, Test] (GLState): deliver the GL_MIN_MAP_BUFFER_ALIGNMENT that glGetIntegerv advertises 2026-08-20 15:35:02 -04:00
swung0x48 6dfadeb7d2 [Fix, Test] (BackendLoader): drain and gate every capability probe whose pname is not ES core 2026-08-20 15:30:11 -04:00
swung0x48 4fc3531d0d [Fix, Test] (BackendLoader, DirectVulkan, ShaderTranspiler): report GL_MAX_CLIP_DISTANCES from the backend's real clip-distance capability 2026-08-20 15:25:29 -04:00
swung0x48 9bde0e500f [Merge] (CTS): land the GL43 wave-3 fixes and the DirectVulkan texture-shape repairs 2026-08-20 14:17:20 -04:00
swung0x48 3477d87b50 [Fix] (DirectVulkan): back a 1D array with its layers in arrayLayers, not in the image height 2026-08-20 14:14:21 -04:00
swung0x48 c2a081fa75 [Fix] (GLState, DirectVulkan): bust the texture-sync skip when a re-spec moved only the shape 2026-08-20 14:02:05 -04:00
swung0x48 685fd750c9 [Test] (MG_Test): expect buffer-texture level queries to answer, not to error 2026-08-20 13:50:45 -04:00
swung0x48 02c9b8a32d [Fix, Test] (GLImpl, DirectVulkan, MG_IntegrationTest): record glVertexAttribLFormat's state and drop the array at draw 2026-08-20 13:44:44 -04:00
swung0x48 26f02567d7 [Fix, Test] (GLState, GLImpl): reserve an inactive uniform's explicit location and pin the link to GL_MAX_UNIFORM_LOCATIONS 2026-08-20 13:39:38 -04:00
swung0x48 a3dbe234d7 [Fix, Test] (GLImpl, MG_IntegrationTest): answer glGetTexLevelParameter for buffer textures instead of erroring 2026-08-20 13:27:50 -04:00
swung0x48 de8e7a4606 [Fix, Test] (ShaderTranspiler): parse layout literals in every GLSL base and key array-of-arrays uniforms per element 2026-08-20 13:24:04 -04:00
swung0x48 31a5da6190 [Test] (BackendLoader, DirectGLES): cover the per-stage storage block limits and the mg_IndirectParams injection gate 2026-08-20 13:16:29 -04:00
swung0x48 8899f065f4 [Fix] (DirectGLES): gate the mg_IndirectParams vertex-stage injection on the driver having a vertex storage block 2026-08-20 13:16:29 -04:00
swung0x48 a991f63899 [Fix] (GLImpl): answer the per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS queries from the backend instead of a fixed 16 2026-08-20 13:16:29 -04:00
swung0x48 3ff9cfe5c2 [Fix] (BackendLoader, DirectGLES, DirectVulkan): derive the per-stage shader storage block limits from the backend 2026-08-20 13:16:29 -04:00
swung0x48 6359fba455 [Test] (MG_Test): compile the compute-limit probe against the captured env, not the null-env fallback 2026-08-20 13:09:38 -04:00
swung0x48 872876961d [Fix] (GLState): pin the storage-binding ceiling's min/max to Int so no platform can widen either argument 2026-08-20 13:08:22 -04:00
swung0x48 e2923a239f [Fix, Test] (ShaderTranspiler): size a non-final unsized storage-block member so the members after it stop aliasing it 2026-08-20 13:05:50 -04:00
swung0x48 1740a8a41a [Feat, Test] (GLImpl, GLState): implement glBeginConditionalRender and discard the commands GL 4.6 10.9 names 2026-08-20 12:59:22 -04:00
swung0x48 6b1d89f279 [Fix, Test] (DirectGLES, MG_IntegrationTest): re-sync image-unit bindings when a draw's image texture was re-specified 2026-08-20 12:52:13 -04:00
swung0x48 01fbe0b4b0 [Fix, Test] (GLState, ShaderTranspiler): reject a storage-block binding at or past GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS 2026-08-20 12:45:56 -04:00
swung0x48 cb155c5b94 [Fix, Test] (GLImpl, ShaderTranspiler): reconcile the compute work-group limits glGetIntegeri_v and glslang advertise 2026-08-20 12:41:01 -04:00
swung0x48 04a06438c5 [Fix] (GLState): count an image-uniform array once however reflection spelled it 2026-08-20 12:16:57 -04:00
swung0x48 db00774224 [Fix] (DirectGLES): report the image formats GLSL ES cannot spell instead of losing the program silently 2026-08-20 12:16:56 -04:00
swung0x48 f378c1a064 [Fix, Test] (DirectGLES): read 1D-array and cube-map-array levels back layer by layer in glGetTexImage 2026-08-20 12:16:55 -04:00
swung0x48 421ccd08c6 [Fix, Test] (DirectGLES): make both halves of a split read+write image coherent 2026-08-20 12:04:43 -04:00
swung0x48 039af520bf [Fix, Test] (GLState): fail the link when a stage exceeds GL_MAX_*_IMAGE_UNIFORMS 2026-08-20 12:02:22 -04:00
Swung0x48 cdba7bed2e [Test, Bench] (ShaderTranspiler): pin L1 backend-agnosticism and measure the whole-front-end hit 2026-08-20 12:00:01 -04:00
Swung0x48 1eeeb44d94 [Perf] (ProgramState): serve a whole linked program from translation cache L1, skipping the link entirely 2026-08-20 12:00:01 -04:00
swung0x48 fa2e15c27e [Fix, Test] (GLImpl): answer GL_IMAGE_FORMAT_COMPATIBILITY_TYPE from glGetTexParameterfv 2026-08-20 11:56:24 -04:00
Swung0x48 14744f117c [Refactor] (ProgramInterface): build the program-resource model from the reflection snapshot, retiring GetReflection 2026-08-20 11:47:57 -04:00
Swung0x48 8329ab4264 [Refactor] (ProgramState): answer the GL query surface from an owned reflection snapshot, not the live TProgram 2026-08-20 11:43:42 -04:00
swung0x48 ee98c453ed [Fix, Test] (GLImpl): enforce GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS on the bind and indexed-query paths 2026-08-20 11:39:28 -04:00
swung0x48 f88322ce84 [Feat, Test] (DirectGLES, ShaderTranspiler): bind atomic counter buffers end-to-end on the ES backend 2026-08-20 11:36:52 -04:00
Swung0x48 93f1106ba4 [Fix] (ShaderTranspiler): key translation cache L1 on the front-end environment only, not backend identity 2026-08-20 11:30:31 -04:00
swung0x48 31b5b563d6 [Fix, Test] (GLState): fail the link when two atomic counters share a binding and an offset 2026-08-20 11:27:13 -04:00
swung0x48 a9fb7ef0af [Fix, Test] (GLImpl, GLState): answer GL_ACTIVE_ATOMIC_COUNTER_BUFFERS and implement glGetActiveAtomicCounterBufferiv 2026-08-20 11:23:33 -04:00
swung0x48 6159166d38 [Fix, Test] (GLImpl, ShaderTranspiler): reconcile the atomic-counter limits glGetIntegerv and glslang advertise 2026-08-20 11:20:21 -04:00
swung0x48 c6d1b29407 [Merge] (CTS): land the GL43 copy_image and clear_tex_image fixes 2026-08-20 11:16:59 -04:00
Swung0x48 5fecfa42f6 [Bench] (ShaderTranspiler): bracket the translation-cache win with a CTS-sized and a heavy stage 2026-08-20 11:11:37 -04:00
Swung0x48 d48e5d0053 [Fix] (ShaderTranspiler): leak the translation caches so no worker inserts into a destroyed one at exit 2026-08-20 11:09:01 -04:00
swung0x48 0995dfea35 [Test] (MG_IntegrationTest): force the iterationRP repairs on when the pinned ICD is lavapipe 2026-08-20 11:06:48 -04:00
Swung0x48 7a0182b58f [Bench] (ShaderTranspiler): measure the translation cache on a repeated-compile loop 2026-08-20 10:59:46 -04:00
Swung0x48 0f523db14d [Test] (ShaderTranspiler): cover both translation-cache key inventories, eviction and the concurrent path 2026-08-20 10:59:46 -04:00
Swung0x48 442cec1a15 [Perf] (DirectGLES): memoize the SPIR-V to ESSL transpile per stage (translation cache L2) 2026-08-20 10:51:05 -04:00
Swung0x48 246a438138 [Perf] (ShaderTranspiler): memoize a linked program's sanitized SPIR-V (translation cache L1) 2026-08-20 10:51:05 -04:00
swung0x48 042c61fb75 [Fix, Test] (TextureUtil, GLImpl): accept GL_STENCIL_INDEX as a stencil-only texture internal format 2026-08-20 10:49:57 -04:00
swung0x48 a8bebe1a3c [Fix, Test] (GLImpl, GLState): refuse a compressed texture in glClearTexImage/glClearTexSubImage 2026-08-20 10:44:52 -04:00
swung0x48 85cd6913b3 [Fix] (DirectGLES): sync a texture whose mip chain only defines the upper levels 2026-08-20 10:38:52 -04:00
swung0x48 afebf38e90 [Fix, Test] (GLImpl): require only the requested level to exist in glGetTexImage 2026-08-20 10:38:29 -04:00
swung0x48 898c39f1de [Fix] (DirectGLES, DirectVulkan): decline a null copy-image endpoint and settle a renderbuffer on its attachment layout 2026-08-20 10:23:03 -04:00
swung0x48 b1774e80be [Fix, Test] (GLImpl): make copy-image completeness mipmap-aware per GL 4.6 core 8.17 2026-08-20 10:20:34 -04:00
swung0x48 a9b4c47fea [Fix, Test] (GLImpl): record the specific compressed internalformat in TexImage3D and TexStorage3D 2026-08-20 10:17:55 -04:00
swung0x48 1c0be3e715 [Fix, Test] (DirectGLES, TextureUtil): return RGB9_E5 glGetTexImage from the stored words 2026-08-20 10:14:48 -04:00
swung0x48 27ec3d3438 [Merge] (CTS): land the Adreno CTS wave-1 conformance fixes 2026-08-20 10:11:43 -04:00
swung0x48 52718ecf84 [Fix, Test] (GLImpl, DirectGLES, DirectVulkan): accept GL_RENDERBUFFER endpoints in glCopyImageSubData 2026-08-20 10:11:38 -04:00
swung0x48 baeb2fa1bc [Perf] (ShaderTranspiler, Benchmark): add a per-stage stopwatch for the DirectGLES program-build chain 2026-08-20 10:08:02 -04:00
RISC-1145 54a88ef1e2 Merge pull request #15 from MobileGL-Dev/asio-include-fix-bug
fixed Asio include bug and added the ignored dir item .gradle
2026-08-20 21:48:47 +08:00
RISC-1145 ee124018a2 [Fix] (git) Added the ignored item .gradle 2026-08-20 21:46:01 +08:00
RISC-1145 d7ce0c48ef [Fix] (Cmake) Fixed the issue of including the header files of the ASIO library 2026-08-20 21:44:12 +08:00
swung0x48 0b3101bf6b [Perf, Test] (ShaderTranspiler, DirectGLES): answer both pass-gate probes from one SPIR-V parse 2026-08-20 07:53:07 -04:00
swung0x48 a4fda520ed [Fix, Test] (ShaderTranspiler, DirectGLES): clamp multisample fetches to the backend's real sample count 2026-08-20 06:15:39 -04:00
swung0x48 f0fd6407ae [Fix] (ShaderTranspiler): keep the demoted viewport-index variable after its private pointer type 2026-08-20 05:37:53 -04:00
swung0x48 bde14cae29 [Fix, Test] (DirectGLES, MG_Test): request GL_OES_viewport_array in the emitted ESSL, or lower the builtin away 2026-08-20 05:29:18 -04:00
swung0x48 f2f6430e34 [Feat] (Loader): detect GL_OES_viewport_array in the GLES capability scan 2026-08-20 05:29:12 -04:00
swung0x48 15e36ad1e9 [Feat, Test] (ShaderTranspiler, MG_Test): demote gl_ViewportIndex to a plain global for ESSL targets 2026-08-20 05:29:08 -04:00
swung0x48 7940a09491 [Fix, Test] (GLImpl, MG_Test): validate glBlitFramebuffer's mask bits, filter enum and LINEAR depth rule 2026-08-20 05:14:11 -04:00
swung0x48 1e8d4661e6 [Fix, Test] (GLImpl, MG_Test): raise a draw's mode INVALID_ENUM before the no-current-program guard 2026-08-20 05:10:38 -04:00
swung0x48 916702629e [Fix, Test] (GLImpl, MG_Test): validate glFenceSync's condition/flags and glWaitSync's flags/timeout 2026-08-20 05:08:11 -04:00
swung0x48 9b37c77ae2 [Test] (MG_IntegrationTest): pin an overflowing vertex-only transform feedback capture's written and generated counts 2026-08-20 04:59:00 -04:00
swung0x48 085eb5835b [Fix, Test] (GLImpl, MG_State, DirectGLES, MG_Test): separate the transform feedback query counters and prefer the exact CPU count on DirectGLES 2026-08-20 04:58:54 -04:00
swung0x48 56377d2025 [Fix, Test] (DirectGLES, MG_IntegrationTest): drop the image-binding layer for targets that have none 2026-08-20 04:36:01 -04:00
swung0x48 7d2c16a90e [Fix] (DirectGLES): bound every driver error drain so a lost context cannot spin forever 2026-08-20 04:32:14 -04:00
swung0x48 261cfd1591 [Fix] (DirectGLES): retry a failed blit's colour and depth/stencil aspects independently 2026-08-20 04:31:22 -04:00
swung0x48 bbc7b9ca84 [Fix] (DirectGLES): report refused renderbuffer storage and collect dead backend twins on object churn 2026-08-20 04:21:10 -04:00
swung0x48 c1b3b16cab [Fix] (DirectGLES): drain the ES error queue in ErrorLopper's non-debug arm too 2026-08-20 04:21:09 -04:00
swung0x48 f17cb23ea3 [Fix, Test] (GLImpl, DirectGLES): deallocate zero-sized multisample images instead of defining them 2026-08-20 04:15:46 -04:00
swung0x48 f297af7d2b [Fix] (GLImpl, DirectGLES, DirectVulkan): floor every advertised sample cap and clamp the realised count in the backends 2026-08-20 04:10:45 -04:00
swung0x48 d9abf1c2c1 [Fix] (DirectGLES): probe the real multisample texture sample counts instead of hardcoding one 2026-08-20 04:07:38 -04:00
swung0x48 392736fb6b [Fix, Test] (ShaderTranspiler): rewrite float-equals-zero exactly instead of within a 1e-4 epsilon 2026-08-20 03:59:29 -04:00
swung0x48 0944925679 [Fix] (CTS-Harness): pin device glcts surface to 256^2 rgba8888d24s8, sync qpa, classify no-log reboots as hangs 2026-08-20 03:48:53 -04:00
swung0x48 eadf7bc474 [Fix] (TraceReplay): import iterationRP repair flags in the desktop CLI
Initialize the desktop replay Request from the three iterationRP environment flags before loading MobileGL. Without this, the Request defaults caused the replay core to unset CI's exported flags, leaving all repairs disabled despite the workflow configuration.
2026-08-20 00:42:34 -04:00
swung0x48 00a326ef78 [CI] (MG_IntegrationTest): enable iterationRP repairs in the integration gate
Run the lavapipe Program 203 golden test with the same subgroup scratch, derived topology, and missing-barrier repairs as the iterationRP retrace matrix. This keeps the prerequisite integration job from failing before retrace jobs can start.
2026-08-20 00:03:52 -04:00
swung0x48 c09045fe59 [Fix, Test] (DirectVulkan, ShaderTranspiler, TraceReplay): repair iterationRP's missing reduction barrier
Program 203 reuses prefixSumCache for a second subgroup reduction before every workgroup invocation has consumed the first result. Add a fingerprint-gated SPIR-V pass that inserts the missing Workgroup acquire-release barrier while preserving native subgroup operations.

Keep the repair opt-in behind MOBILEGL_ITERATIONRP_FIX_BARRIER, cover insertion, pass-through, and idempotence, and enable it together with the existing iterationRP subgroup repairs for the matching Linux and Android CI retraces.
2026-08-19 23:42:17 -04:00
swung0x48 5bd8ef01e5 [Test] (MG_IntegrationTest): pin Program 203's complete golden output across desktop and Android
Add a deterministic iterationRP Program 203 fixture that dispatches the original shader and compares every RG16F texel against fixed half-float golden bits. This catches both a wrong exposure result and collateral writes without retaining a serial reference shader.

Make MobileGLIntegrationTest runnable as a standalone Android executable by linking the shared MobileGL library and backing EGL with an AImageReader window; desktop keeps its static-library pbuffer path.

Validation: Adreno 830 passes with 0/262656 mismatches; lavapipe reproduces the current reduction defect with 1/262656 mismatches at the exposure texel.
2026-08-19 22:30:30 -04:00
swung0x48 3181ed2c5a [Fix] (DirectGLES): repair the emulation-guard mask restore and scissor the resolve fallback's staging blit correctly
Final audit round over the DirectGLES scratch/shadow mechanisms; three verified
defects fixed:

- ~ScopedEmulationDrawState restored the APPLICATION's per-buffer colour masks,
  not what SyncRenderState actually pushed: a widened attachment's alpha-off
  doctoring (g_syncedColorMaskAlphaWidenMask) was dropped while the memo still
  claimed it applied, so the next sync early-outed and draws wrote fragment
  alpha into the widened buffer - breaking the stored-alpha==1.0 invariant the
  widen discipline exists to protect. The restore now re-applies the doctoring.
- The same restore loop gated on the core glColorMaski name only, while the sync
  push falls back to glColorMaskiEXT/OES: EXT/OES-only devices were left holding
  buffer 0's mask broadcast across every draw buffer with the shadow recording
  the divergent set (never repaired). The restore now uses the same three-way
  pointer fallback.
- ResolveThenBlit ran its resolve-into-scratch staging blit under the
  application's scissor: a box not covering the scratch-origin rect clipped the
  resolve silently (no GL error), and the second blit then copied stale scratch
  renderbuffer texels into the destination. The staging blit now runs scissor-off
  (shadow-tracked, like ScopedScissorDisable); the caller-visible blit keeps its
  native scissor semantics.
2026-08-19 16:41:31 -04:00
swung0x48 6aed3b08f3 [Fix] (DirectVulkan, GLImpl, MG_State, ShaderTranspiler): second audit round over the remaining memo sites
Six more verified defects from the residual memo/cache mechanisms:

- Program resource cache (DirectVulkan reflection): glShaderStorageBlockBinding
  deliberately does not bump the backend state version, and the SSO pipeline
  composite is unnamed so the by-name in-place patch can never reach its slot -
  the composite kept serving pre-rebind SSBO bindings. The cache now keys on the
  program's block-binding version; a binding-only change re-applies the overrides
  by name instead of re-running spirv-reflect. SetShaderStorageBlockBinding also
  gains the equality bail-out its uniform-block sibling has, so the composite
  mirror's replay stops churning the version every draw.
- LinkProgram's allowVSOnlyPrograms function-static latch never set its own
  initialized flag (dead memo, re-read every call) - and completing it would have
  frozen a per-backend capability across re-initialization. Replaced with a fresh
  per-link read from the null-checked active backend.
- Query object registry: drained at full library teardown (DestroyAllQueryObjects,
  mirroring DestroyAllSyncObjects) - undeleted queries and their backend wrappers
  leaked across Destroy/Initialize cycles, stale ids stayed IsQuery == GL_TRUE in
  the re-initialized library, and a later delete could hand the old backend's
  wrapper to a different backend's DeleteBackendQuery.
- Converted vertex streams and the host-side EBO max-index scan now SyncGpuWrites
  before reading the coherent mapping: XFB/SSBO/image writes are merely recorded
  at that point, so the conversion read pre-write bytes (the restart-index
  rewrite already synced; these two host reads did not).
- Zero-stride converted bindings: both converters rejected stride 0, making the
  factory's documented single-element conversion unreachable and silently
  dropping every draw using such a binding; the stride is substituted with the
  element size for the one-element case.
- DemoteFloat64Pass block relayout: measurement queued into the module eagerly,
  so a mid-struct failure left a half-relaid-out block (compacted offsets before
  the failing member, 64-bit offsets after) while claiming the block was left
  alone. Decoration writes are now collected and committed only when the whole
  block measures successfully.
2026-08-19 16:41:28 -04:00
swung0x48 281467a345 [Fix] (DirectGLES, DirectVulkan, MG_State): close stale-cache, A-B-A and state-leak holes across the memo layers
Audit of every memoization implementation; sixteen verified defects fixed:

DirectGLES backend:
- Broadcast draw-buffer memo: cleared at MakeCurrent/DestroyEGLContext like its
  sibling shadows; its identity+version key is only monotonic within one GLContext,
  so a library teardown + re-init could false-hit on a recycled FBO address.
- Backend texture id re-mint (RecreateBackendTexture) now bumps an attachment
  generation that the SyncCurrentFBO gate and every FBO twin compare, so driver
  FBOs re-attach instead of keeping the deleted texture name; the attachment walk
  re-enters until the generation is quiescent (a walk itself can re-mint).
- Buffer id re-mint (persistent-map adoption, immutable-store retire) now bumps a
  generation the VAO twin sync compares, forcing a full re-emit of the baked
  glVertexAttribPointer / element-array bindings that frontend versions cannot see.
- VAO element-array sync memo: bound-object identity joins the wrapping Uint16
  slot version (same pairing the ResolvedDrawBuffers IBO memo already uses).

DirectVulkan backend:
- EBO slice memo gains the mapped-buffer guard its vertex-binding sibling has: a
  shadow-backed persistent map mutates with no epoch bump, so a hit must decline.
- VkClearManager::MergeClearPayload keeps colorEncoding/colorInt/colorUint with
  the color, so deferred glClearBufferiv/uiv no longer degrade to all-zero float.
- GetOrCreateComputePipeline no longer memoizes a failed creation (same contract
  as PipelineFactory): a transient driver failure was permanently disabling every
  dispatch of that program.
- Explicit-LOD-0 verdict memo keys on the sampling-resolution generation; sampler
  filter/aniso/LOD setters bump only that counter, so the old key served a stale
  verdict (wrong SPIR-V variant) after glTexParameter/glSamplerParameter changes.
- SetupDraw fast path declines instead of re-arming on a moved sampling-resolution
  generation (the snapshot bakes the LOD verdict into its pipeline), and
  recomputes the XfbCapture bit so the first draw after glBeginTransformFeedback
  cannot bind the undecorated variant and silently capture nothing.
- VertexInputStateFactory eviction epoch is drawn from a process-wide source: VAO
  state-pointer memos outlive the factory across renderer recreation, and a fresh
  factory restarting at epoch 1 would dereference a dead factory's entry.
- Cached render passes re-read the live renderbuffer clear payload at begin (the
  clear VALUE is not in the pass hash; the entry's inline snapshot replayed the
  creation-time color and dropped the newly queued one).
- FramebufferObject gains a never-reused lifetime id, keyed into the render-pass
  fast-path memo and the SetupDraw snapshot beside the raw pointer + Uint16
  version pair, which address reuse plus fresh version counts could equal.
- SyncTextureResource's preserved-content image goes through the deferred-release
  ring on both failure paths instead of a synchronous destructor under the GPU.

MG_State frontend:
- Layer-1 compile memo is env-disciplined like layers 2/3: a node computed against
  a dead CompileEnv (e.g. pre-capability fallback limits) no longer answers
  glCompileShader forever once the environment's content changes.
- Pipeline composite cache rebuilds from each stage program's last-link shader
  snapshot (new LinkedShaderRef list + pinned link inputs) instead of the live
  attach list and current compile nodes: post-link glAttachShader/glCompileShader
  must not leak into the composite while the (lifetimeId, linkVersion) signature
  still hits - GL's "as last linked" rule.
2026-08-19 16:41:26 -04:00
swung0x48 c7e36986e7 [Fix] (ShaderTranspiler, DirectVulkan, MG_IntegrationTest): patch both of iterationRP's under-declared subgroup scratch arrays
The previous commit's fingerprint was pinned to one array's incidental
dimensions - workgroup exactly 32x16x1, element exactly vec2, length
exactly 32 - which is the auto-exposure reduction and nothing else. The
pack ships the same idiom twice:

  - auto-exposure:  32x16 (512 invocations), shared vec2 prefixSumCache[32]
  - RTW warp:       1024 invocations,        shared float prefixSumCache[64]

so the warp kept writing 128 subgroups into 64 entries on an 8-lane
device and the retrace stayed bit-identically wrong (ssim 0.027902).

Key the fingerprint on the pack's idiom instead of one array's shape: a
workgroup array of 32-bit floats indexed by gl_SubgroupID, fed by a
subgroup scan, whose declared length is below ceil(invocations / native
width). Three properties keep that a targeted repair rather than a
general array resizer:

  - the index must BE gl_SubgroupID (through OpCopyObject, a signedness
    OpBitcast, or a spill whose every store is that id), so an index
    masked or clamped into range is left alone;
  - the >= 16-lane early-out is retained, so every module on the devices
    the pack was written for passes through byte-identical;
  - growth is certified against maxComputeSharedMemorySize using a
    natural-alignment layout model, and declined outright when a
    declaration cannot be sized, so a patched module can never fail
    pipeline creation where the original would not have.

Verified against the shaders the CI trace actually contains: of the 14
compute modules in the fixture exactly these two change, the other
twelve are byte-identical, and all fourteen pass spirv-val. The
integration scenario grows a second case for the 1024-invocation shape;
both abort with heap corruption when the patch is disabled.
2026-08-19 16:36:06 -04:00
swung0x48 d8576a2ed3 [Fix] (DirectVulkan, ShaderTranspiler, MG_IntegrationTest, SelfTest, TraceReplay): use native subgroups and patch iterationRP's under-declared scratch
iterationRP's Program 203 declares shared vec2 prefixSumCache[32] for a
512-invocation workgroup indexed by gl_SubgroupID; any device narrower
than 16 lanes partitions into more than 32 subgroups and the pack writes
shared memory out of bounds (heap corruption on lavapipe's CPU
rasterizer, ssim 0.028 on the CI retrace). Fix it where the fault lies -
in the fixture - and keep the GL contract sound everywhere else:

- FixIterationRPSubgroupScratchPass: fingerprint-gated SPIR-V pass that
  grows exactly that array to ceil(invocations/width) entries on sub-16-lane devices; every other module passes through byte-identical.
- DeriveNumSubgroupsPass stays default-on for the Adreno topology bug
  and is made spec-sound: pipelines request REQUIRE_FULL_SUBGROUPS
  whenever the workgroup shape makes the flag legal (computeFullSubgroups
  enabled, local_size_x a multiple of the native width, subgroup count
  within maxComputeWorkgroupSubgroups).
- EmulateSubgroupsPass: 32-lane virtual-subgroup lowering kept in-tree
  as a last resort, enabled only by MOBILEGL_MAGMA_EMULATE_SUBGROUP=1 on
  devices with no native subgroup support; fails closed on extended
  subgroup instructions and on modules whose added scratch would exceed
  maxComputeSharedMemorySize.
- IterationRPFirstReductionScenario skips gracefully outside the pack's
  16..256-lane source domain; the new IterationRPScratchFixScenario runs
  the fixture-shaped reduction on any width and asserts the exact
  width-independent total. DriverPost keeps reporting FAIL on
  out-of-domain devices.
- Program203 -> IterationRP rename throughout; the per-trace
  num_subgroups_quirk plumbing is removed from the trace replayer, JNI
  chain, and CI workflows.
2026-08-19 09:48:11 -04:00
swung0x48 2b6c2b561c [Fix, Test] (DirectVulkan, ShaderTranspiler, TraceReplay): derive NumSubgroups behind opt-in quirk 2026-08-18 22:31:57 -04:00
swung0x48andClaude Fable 5 12c94111b5 [Fix, Test] (DirectVulkan, SelfTest, MG_IntegrationTest, TraceReplay): snapshot sampler/image feedback and add Program 203 diagnostics
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 03:04:09 -04:00
swung0x48 7769156cfc [Fix, Test] (ShaderTranspiler, WGL, TraceReplay): remove subgroup pack quirks and tune iterationRP 2026-08-17 01:55:17 -04:00
swung0x48 0ecfdff4e7 [Fix, Test] (MG_State, MG_Util, DirectVulkan, MG_Test): replay narrow-subgroup reductions correctly 2026-08-16 13:33:01 -04:00
swung0x48 6df5a6137f [CI] (trace-replay): run iterationRP only on DirectVulkan 2026-08-16 11:09:56 -04:00
swung0x48 b3794f4e6a [Feat] (MG_Impl, MG_State, MG_Util, DirectGLES, DirectVulkan, MG_Test): implement ARB_clear_buffer_object correctly 2026-08-16 01:12:28 -04:00
swung0x48 14d3901d30 [Chore, Test] (MG_State, MG_Util, DirectGLES, DirectVulkan): make SPIR-V validation task-local 2026-08-15 22:58:28 -04:00
swung0x48 d4766513e4 [Fix, Test] (DirectVulkan): replay Photon descriptor pressure correctly 2026-08-15 22:35:39 -04:00
swung0x48 72dc7aa6aa [Chore] (MG_Config, MG_State, MG_Util): gate SPIR-V validation at startup 2026-08-15 22:35:39 -04:00
swung0x48 9d1b280375 [Fix, Test] (MG_Util, MG_Test): rename Photon-conflicting MSL identifiers 2026-08-15 09:40:13 -04:00
swung0x48 8acd885594 [Fix, Test] (DirectVulkan): preserve viewport-index program metadata 2026-08-15 09:40:13 -04:00
swung0x48 10ff5e2b18 [Fix, Test] (DirectGLES, DirectVulkan): advertise indirect draw capabilities accurately
- advertise GL_ARB_draw_indirect when supported
- gate GL_ARB_base_instance on complete non-zero firstInstance semantics
- synchronize Driver POST reporting
- add capability and extension-advertisement regression tests
2026-08-15 06:30:44 -04:00
swung0x48 a6e52476f3 [Chore] (CMake): skip embedded SPIRV-Tools executables 2026-08-15 05:48:41 -04:00
swung0x48 0deff52a1b [Fix, Test] (DirectVulkan, trace-replay): replay quarter-turn surfaces correctly 2026-08-13 06:45:28 -04:00
swung0x48 50fefca959 Merge branch "feat/cts-viewport-array" into dev 2026-08-13 04:57:40 -04:00
swung0x48 42ad62b54c [Fix, Test] (MG_Util, MG_IntegrationTest): advertise the GL 4.3 VIEWPORT_BOUNDS_RANGE floor on a GLES driver that has no such query, instead of a range admitting no origin 2026-08-13 04:44:23 -04:00
swung0x48 f41403e227 [Feat, Test] (MG_Backend/DirectVulkan, MG_Test, MG_IntegrationTest): rasterize the viewport gl_ViewportIndex selects, instead of collapsing all sixteen onto viewport 0 2026-08-13 04:44:23 -04:00
swung0x48 5fbb17f6b9 [Feat, Fix, Test] (MG_State, MG_Impl, MG_Backend, MG_Test): give ARB_viewport_array real 16-element indexed state instead of eight stubs and a viewport-0 echo 2026-08-13 04:44:23 -04:00
swung0x48 92d8f7269b [Fix] (MG_IntegrationTest): drop the executable bit a copied-in scenario file carried 2026-08-13 04:29:23 -04:00
swung0x48 822e405c77 Merge branch "feat/vk-barrier-layer-ranges" into dev 2026-08-13 04:28:57 -04:00
swung0x48 b8233f9c4e [Fix, Test] (MG_Backend/DirectVulkan, MG_IntegrationTest): the barrier before every attachment transfer only ever moved layer 0, so a copy off a non-zero layer read a layout nothing had transitioned 2026-08-13 04:01:20 -04:00
Swung0x48 91475a7b6f Merge branch feat/cts-copyimage-frontend into dev 2026-08-13 03:59:16 -04:00
Swung0x48 cee17025a0 [Test] (MG_Test): pin the plain mutable-texture copy every new validator rule is a new way to reject 2026-08-13 03:17:27 -04:00
Swung0x48 9642ae4d20 [Fix, Test] (MG_Impl/Texture, MG_Backend, MG_Test): copy_image compatibility is texel-block size, and neither backend was told which slice to copy 2026-08-13 03:17:27 -04:00
swung0x48 595d140036 Merge branch "feat/cts-copyimage-magma-layers" into dev 2026-08-13 03:11:21 -04:00
swung0x48 b62d1f2078 [Fix, Test] (MG_Backend/DirectVulkan, MG_IntegrationTest): map a layered CopyImageSubData onto the axis each endpoint keeps its slices on, instead of copying slice 0 and calling it done 2026-08-13 02:49:57 -04:00
swung0x48 5e82ff968a Merge branch "feat/cts-rgb9e5-raw-transfer" into dev 2026-08-13 02:47:18 -04:00
swung0x48 6a80a82dd3 [Fix, Test] (MG_Util, MG_Backend/DirectGLES, MG_Backend/DirectVulkan, MG_Test): a packed texel whose client type already spells its storage word must cross glGetTexImage unencoded, or the RGB9_E5 shared exponent gets canonicalized 2026-08-13 02:41:37 -04:00
swung0x48 f9182a5ca3 Merge branch "fix/cts-copyimage-level-validation" into dev 2026-08-13 02:38:43 -04:00
swung0x48 f37b511fca [Fix, Test] (MG_Impl/GLImpl, MG_Backend/DirectVulkan, MG_Backend/DirectGLES, MG_Test, MG_IntegrationTest): a glCopyImageSubData to a mipmap level the texture never had is INVALID_VALUE, not a subresource handed to the driver 2026-08-13 02:28:55 -04:00
swung0x48 38027d21f8 Merge branch "feat/log-level-semantics" into dev 2026-08-13 02:19:26 -04:00
Swung0x48 dd2a62228f [Fix, Feat, Test] (MG_Util, MG_Backend, MG_Impl, MG_State): correct the log severity ordering and unwind the diagnostics it silenced 2026-08-13 02:02:49 -04:00
swung0x48 373aa44dd7 Merge branch "fix/cts-tess-ssbo-crash" into dev 2026-08-13 01:38:58 -04:00
swung0x48 96646df12e [Fix] (MG_Backend/DirectVulkan): review round - failure diagnostics take MGLOG_E, the once-per-patch-size success note takes MGLOG_D, and the per-draw refusal stays latched 2026-08-13 01:34:57 -04:00
swung0x48 f20b20e643 [Fix, Feat, Test] (MG_Backend/DirectVulkan, MG_Test): synthesize the pass-through tessellation control stage GL gives an evaluation-only program, and refuse the half-tessellated pipeline Mali dereferences null inside 2026-08-13 01:28:35 -04:00
swung0x48 2587814970 Merge branch "feat/cts-image-format-qualifier" into dev 2026-08-12 19:23:51 -04:00
swung0x48 43398e33e8 [Fix, Test] (MG_Util, MG_IntegrationTest): review round - anchor a clone only behind a real definition, decline the atomic-image shape, and say what the software lanes cannot falsify 2026-08-12 19:12:39 -04:00
swung0x48 b7557d6615 [Feat, Fix, Test] (MG_Util, MG_Backend/DirectGLES, MG_IntegrationTest): bake the bound image format into the ESSL a format-less image declaration needs 2026-08-12 18:56:31 -04:00
swung0x48 bce34d7fac Merge branch "feat/cts-followup-fp64-ds-imgbuf" into dev 2026-08-12 17:56:13 -04:00
swung0x48 f91857266f [Fix, Test] (MG_Backend/DirectGLES, MG_Test): the tessellation stages never got the flat qualifier an ES linker compares across both sides of every integer interface 2026-08-12 17:50:31 -04:00
swung0x48 49cb1be0fd [Fix, Test] (MG_Backend/DirectGLES, MG_IntegrationTest): a shader writing a buffer texture through an image unit left the CPU shadow stale, so every map and readback after it saw the old bytes 2026-08-12 17:50:31 -04:00
swung0x48 a51c68bb2c [Fix, Test] (MG_Util, MG_IntegrationTest): a 64-bit float constant was read as 32 bits, so every comparison against a round double became an epsilon test against zero 2026-08-12 17:50:25 -04:00
swung0x48 1c723a6cfc Merge branch "feat/cts-image-targets" into dev 2026-08-12 16:52:34 -04:00
swung0x48 44805bfa07 [Fix, Test] (MG_Util, MG_Backend/DirectVulkan, MG_IntegrationTest): adversarial review - the rewritten 1D-array image collided with a module's own 2D-array one and left an invalid duplicate type; pin the component order, keep the subject kinds in a truncated matrix 2026-08-12 16:49:59 -04:00
swung0x48 257fcbfd0b [Fix, Test] (MG_Util, MG_Backend/DirectVulkan, MG_IntegrationTest): review round - one module parse for the shaders with no 1D-array image, per-kind values the combined case can name, and the invariants two shared buffers rest on 2026-08-12 16:43:29 -04:00
swung0x48 2b46a3db96 [Fix, Test] (MG_Util, MG_Backend/DirectGLES, MG_Backend/DirectVulkan): the two image target kinds a compute dispatch could not read - 1D-array on ES, imageBuffer on Vulkan 2026-08-12 16:11:10 -04:00
swung0x48 0b36621069 Merge branch "feat/cts-diag-wave" into dev 2026-08-12 15:33:43 -04:00
swung0x48 9ee2e0a1db [Fix] (MG_Backend/DirectGLES): re-push the texture parameters a regenerated driver texture has lost, instead of trusting caches that outlived it 2026-08-12 15:22:39 -04:00
swung0x48 6b6623ae72 [Fix, Test] (MG_Backend, MG_Impl, MG_State): advertise the uniform-block and stencil-texturing strings a 4.0 context hides behind, forward DEPTH_STENCIL_TEXTURE_MODE to both backends, and stop a reserved transform feedback name passing for an object 2026-08-12 15:12:01 -04:00
swung0x48 a6e029734b [Test, Doc] (MG_IntegrationTest, MG_Backend/DirectGLES): review wave - stop the skip paths leaking their GL objects, normalise the clip enables a neighbouring scenario leaves behind, and state what the software lanes cannot falsify 2026-08-12 13:57:32 -04:00
swung0x48 e7d6bfddac [Fix, Test] (MG_Backend/DirectGLES, MG_State, MG_Util): publish the real default framebuffer depth/stencil format, forward the clip distance enables, and stop trusting a stale format probe for non-2D depth attachments 2026-08-12 13:34:39 -04:00
swung0x48 f2c879528f Merge branch "feat/es-ds-readback" into dev 2026-08-12 12:12:49 -04:00
Swung0x48 eaba4ac1dc [Fix, Docs] (MG_Backend/DirectGLES): drain the attachment-format probe, and document the depth/stencil readback emulation switch 2026-08-12 12:09:45 -04:00
Swung0x48 ed6578954e [Feat, Fix, Test] (MG_Backend/DirectGLES, MG_IntegrationTest): emulate ES depth/stencil readback by shader sampling, and stop the replicate blit leaking its borrowed texture unit 2026-08-12 11:38:34 -04:00
swung0x48 e005c8b6cb [Test] (tools/trace_replay): drop the minecraft-26.2-main-menu fixture 2026-08-12 11:07:38 -04:00
swung0x48 535b5e3095 Merge branch "feat/cts-xfb-respec-compressed" into dev 2026-08-12 10:35:09 -04:00
swung0x48 1b05a84928 [Fix, Test] (MG_State, MG_Backend, MG_Impl, MG_Test): review wave - hand the mapping back instead of renewing it, retire an immutable ES store on respecify, and tag glTexStorage2D levels too 2026-08-12 10:32:39 -04:00
swung0x48 7ccb762936 [Fix, Test] (MG_Impl, MG_Test): a persistently mapped unpack buffer is a legal source for the compressed uploads too 2026-08-12 09:24:57 -04:00
swung0x48 442e7eec1c [Docs] (MG_IntegrationTest): the capture-buffer note described a bug that is fixed and pinned elsewhere now 2026-08-12 09:22:17 -04:00
swung0x48 2787d15706 [Fix] (MG_Impl): the compressed sub-image bounds check added two application-supplied ints 2026-08-12 09:19:50 -04:00
swung0x48 74ce58a6c7 [Fix] (MG_Backend/DirectGLES): a declined map renewal left the respecify pointing at immutable storage 2026-08-12 09:17:54 -04:00
swung0x48 bb122ebd4f [Test] (MG_IntegrationTest): the DSA enable-disable-attributes case, reassembled where it can be stepped through 2026-08-12 08:56:13 -04:00
swung0x48 9b0ed5b3af [Fix, Feat, Test] (MG_Impl, MG_Test): a specific compressed internalformat now tags the level it defines, and the compressed sub-image entry points stopped being stubs 2026-08-12 08:52:01 -04:00
swung0x48 0e31c1481b [Fix, Test] (MG_State, MG_Backend/DirectVulkan, MG_Backend/DirectGLES, MG_IntegrationTest): a respecified capture buffer left the transform feedback writing one store and the readback reading another 2026-08-12 08:42:45 -04:00
swung0x48 811f32760e [CI] (test): gate the retrace pipeline on the integration lane - build-retrace and trace-cases now need it 2026-08-12 08:41:19 -04:00
swung0x48 01381a0404 Merge branch "feat/cts-baseinstance-dsa-bufstorage" into dev 2026-08-12 08:23:57 -04:00
swung0x48 0f02b0fdb1 [Fix] (MG_Util): the POST base-instance row described an emulation that no longer exists 2026-08-12 08:22:26 -04:00
swung0x48 3be02abf47 [Fix] (MG_Impl): multisample renderbuffers ignored the integer formats own sample limit 2026-08-12 08:17:11 -04:00
swung0x48 f91d6b676c [Fix] (MG_Impl): the per-stage atomic-counter-buffer limits answered GL_INVALID_ENUM instead of their zero 2026-08-12 08:12:32 -04:00
swung0x48 1ebf191f94 [Fix, Test] (MG_Backend/DirectGLES, MG_Util): baseInstance reached the shader but never the vertex fetch 2026-08-12 08:12:27 -04:00
swung0x48 ccad803023 Merge branch "feat/cts-vab-current-attrib" into dev 2026-08-12 07:40:47 -04:00
swung0x48 21159caf31 [Fix, Test] (MG_Util, MG_Backend/DirectGLES): transform feedback captured nothing at all from an interface block 2026-08-12 07:38:50 -04:00
swung0x48 ea4819a21d [Fix, Test] (MG_State, MG_IntegrationTest): an array vertex input occupies one location per element, not one location in total 2026-08-12 07:36:12 -04:00
swung0x48 6b882b3ccf Merge branch "feat/fp64-demote-to-fp32" into dev 2026-08-12 06:42:23 -04:00
swung0x48 96bd36c50b [Docs] (README): the MOBILEGL_ADVERTISE_FP64 switch 2026-08-12 06:41:54 -04:00
swung0x48 46fbd837b3 [Fix, Test] (MG_State, MG_Impl, MG_IntegrationTest): a double uniform initializer no longer reads zero 2026-08-12 06:41:54 -04:00
swung0x48 796a57a115 [Feat, Test] (MG_Backend, MG_Util, MG_Impl): report the fp64 tier at startup, decline 64-bit vertex formats everywhere, advertise GL_ARB_gpu_shader_fp64 only on request 2026-08-12 06:25:35 -04:00
swung0x48 62a2dae5ba [Fix, Test] (MG_Impl, MG_State, MG_IntegrationTest): glUniform*d stores what the demoted shader reads 2026-08-12 06:20:11 -04:00
swung0x48 532836c058 [Feat, Test] (MG_Util): demote every 64-bit float in a shader to 32 bits, with the block layout re-derived 2026-08-12 06:11:50 -04:00
swung0x48 2fced2241b [Fix, Test] (MG_Util, MG_State, MG_Backend): the GL 4.3 vertex binding model - array vertex inputs, zero binding strides, instance divisors, and formats ES refuses 2026-08-12 05:36:00 -04:00
swung0x48 21b5fc2d92 Merge branch 'feat/cts-draw-parameters' into dev 2026-08-12 04:24:54 -04:00
swung0x48 1f753ab5fa [Test] (MG_IntegrationTest): the draw-parameter builtins, read back out of the shader the draw produced 2026-08-12 04:18:08 -04:00
swung0x48 3ed9501be5 [Fix, Feat, Test] (MG_Backend/DirectGLES, MG_Test): the draw-parameter builtins never reached the draws that carry them, and glMultiDrawArraysIndirectCount had no backend at all 2026-08-12 04:18:08 -04:00
swung0x48 7311251f30 [Fix] (MG_Util, MG_Backend/DirectVulkan): GL reads gl_BaseVertex as zero on a non-indexed draw where Vulkan's builtin hands over firstVertex 2026-08-12 04:18:07 -04:00
swung0x48 7625cf450d [Fix, Test] (MG_Impl, MG_IntegrationTest): a block count may not exceed its binding points, and an atomic-counter buffer size must survive division by them 2026-08-12 02:38:04 -04:00
swung0x48 450eb209b6 [Fix, Test] (MG_State, MG_Impl): the program interface of a separable program is its own first and last stage, not vertex and fragment 2026-08-12 02:38:04 -04:00
swung0x48 8c5c39b3c3 [Feat, Test] (MG_Impl): glBindTextures and glBindImageTextures were no-op stubs while GL_ARB_multi_bind was advertised 2026-08-12 02:38:03 -04:00
swung0x48 64a0ea397c [Perf] (CI): rewrite one rolling ccache entry per job on the default branch instead of saving one per run 2026-08-12 01:56:17 -04:00
swung0x48 205d837942 [Fix, Test] (MG_Backend/DirectGLES, MG_Util, CI): keep an explicit LOD constant under a new avoid flag - folding the bias uniform into it kills the ANGLE llvmpipe JIT 2026-08-12 01:42:55 -04:00
swung0x48 b5e9339c66 [Fix] (MG_Util): the unsupported-buffer-textures log tells a too-old driver apart from one whose entry point failed to resolve 2026-08-12 01:24:41 -04:00
swung0x48 e310e3e9ff [Fix, Test] (MG_Util, MG_Backend/DirectGLES, MG_Test): resolve buffer textures through the entry point the tier ships, not the ES 3.2 core name; bound the OES retarget to an exact extension name 2026-08-12 01:24:40 -04:00
swung0x48 a0bf4a83bc [Fix, Test] (MG_Util, MG_Backend/DirectGLES, MG_Test, MG_IntegrationTest): detect buffer-texture support, emit the directive the driver advertises, and name the capability when it is missing 2026-08-12 01:24:40 -04:00
swung0x48 97facf777b [Fix] (MG_Backend/DirectVulkan): act on a failed binding remap, bound the storage-block array count, and correct the decline rationale review found describing the reverted mechanism 2026-08-12 01:09:56 -04:00
swung0x48 5cfbb716c0 Merge remote-tracking branch 'origin/dev' into feat/sampler-array-descriptors 2026-08-12 00:58:20 -04:00
swung0x48 64c3411d70 [Perf] (CI): restore unchanged trace fixtures from the Actions cache keyed on their Git LFS pointer oid, downloading only on a miss 2026-08-12 00:49:47 -04:00
swung0x48 f3a0d9e0a3 [Refactor] (CI): move the trace fixture LFS pointer and verification helpers into a shared library 2026-08-12 00:49:47 -04:00
swung0x48 068786e812 [Fix, Test] (MG_Backend/DirectVulkan, MG_IntegrationTest): a declined descriptor must refuse the draw, not just leave the layout - a multi-dimensional sampler array otherwise faulted in the shader 2026-08-12 00:45:36 -04:00
swung0x48 8e7cc62c24 [Test] (MG_Benchmark): a standing UnorderedMap observatory - the container's own shapes, measured through the alias so a future swap re-runs the same suite 2026-08-12 00:40:57 -04:00
swung0x48 d2a36d65a3 [Fix, Test] (MG_Backend/DirectVulkan, MG_IntegrationTest): a sampler uniform array is one binding with many descriptors too - Magma wrote only element zero, and declines the multi-dimensional shape loudly 2026-08-12 00:31:37 -04:00
swung0x48 a020de76e3 Merge remote-tracking branch 'origin/dev' into feat/sampler-array-descriptors 2026-08-12 00:18:51 -04:00
swung0x48 574634adfa [Fix, Test] (MG_Backend/DirectVulkan, MG_IntegrationTest): let the binding remap accept the descriptor arrays that now have per-element paths, and reserve image-info scratch for them 2026-08-12 00:14:18 -04:00
swung0x48 e71d715e1a [Fix, Test] (MG_Backend/DirectVulkan, MG_Util, MG_Test): VkProgramObject move dropped stageSpirvDigests - a robin-hood swap would pair one program's modules with another's digests; sanity tests assert key identity, not iteration counts 2026-08-12 00:11:56 -04:00
swung0x48 7b946fd527 [Fix] (MG_Backend/DirectVulkan, MG_Util): RenderPassEntry needs move assignment - robin-hood probing swaps entries, so mapped types must be swappable 2026-08-12 00:11:55 -04:00
swung0x48 8f3ce5f5b7 [Fix] (MG_Backend/DirectGLES, MG_State, Docs): CopyImageSubData held a registry reference across a re-entrant sync; retarget container rationales at the new erase contract 2026-08-12 00:11:54 -04:00
swung0x48 21ec744ef2 [Refactor] (MG_Util, MG_Test): retire FastSTL for ska::flat_hash_map, the table MobileGlues settled on 2026-08-12 00:11:54 -04:00
swung0x48 faa7b17da3 [Test] (tools/trace_replay): drop the minecraft-26.2-in-world fixture 2026-08-12 00:03:03 -04:00
swung0x48 f6849fc0b3 [Fix, Test] (MG_Backend/DirectVulkan, MG_IntegrationTest): an image uniform array is one binding with many descriptors, not one - Magma wrote only element zero and left the rest undefined 2026-08-11 23:49:31 -04:00
swung0x48 4d1d4f6225 [Fix] (DirectGLES): report a rejected shader and a failed link at MGLOG_I - MGLOG_E is compiled out of every INFO build, which is why the retrace artifact could not say which shader the driver refused 2026-08-11 23:39:48 -04:00
swung0x48 cef81df73f [Fix, Test] (ShaderTranspiler, DirectGLES): fold or lower dynamically indexed fragment outputs before ESSL emission - GLSL ES requires constant integral indices, so the OIT coefficient shader linked nothing on ANGLE and every translucent draw was a silent no-op 2026-08-11 23:39:03 -04:00
swung0x48 c4e6ea1f23 [Docs] (tools/trace_replay): strip investigation logs from the README - usage and procedures only 2026-08-11 23:25:20 -04:00
swung0x48 b4e07ce651 [Fix, Test] (MG_State, MG_Backend/DirectGLES, MG_Test): close three holes review found in the composite uniform mirror - unrecorded stage programs, byte-identical writes, and Espryt's baked image units 2026-08-11 23:19:22 -04:00
swung0x48 ff324057ad [Fix, Perf, Test] (MG_State, MG_Impl/GLImpl, MG_Test, MG_IntegrationTest): a pipeline composite mirrors only the uniforms a stage was written to, and survives a sampler or block rebinding 2026-08-11 23:11:13 -04:00
swung0x48 ef4c6dbe0a Merge branch 'feat/cts-420pack-wave' into dev 2026-08-11 23:07:45 -04:00
swung0x48 19fc7346c5 [Fix] (MG_Backend/DirectVulkan): a re-entrant cache lookup dangled the reference glShaderStorageBlockBinding then wrote through 2026-08-11 23:05:21 -04:00
swung0x48 bcb0e894ef [Fix, Test] (MG_State, MG_Backend/DirectGLES, MG_Util, MG_IntegrationTest, MG_Test): array layout(binding=N) elements bind consecutively, a read+write image reaches ESSL legally, compute local_size comes from the linked intermediate, and glShaderStorageBlockBinding is baked into the generated source 2026-08-11 23:05:21 -04:00
swung0x48 794c10e56c [Fix] (3rdparty/glslang): bump the fork to the invariant-placement and atomic-counter-lowering patches 2026-08-11 23:05:20 -04:00
swung0x48 28390667d7 [Docs] (tools/trace_replay): sundial-lite's sampler state is identical on both stacks - the crash is a driver defect, not a dangling binding 2026-08-11 23:04:25 -04:00
swung0x48 90dd9bec77 [Docs] (tools/trace_replay): sundial-lite crashes in llvmpipe's JIT on a NULL per-texture-unit descriptor 2026-08-11 22:54:50 -04:00
swung0x48 7994ca31d3 [Docs] (tools/trace_replay): sundial-lite has no output-array indexing - separate cause from improved-transparency, and it reproduces locally 2026-08-11 22:48:51 -04:00
swung0x48 5705e05156 [Docs] (tools/trace_replay): sundial-lite kills the emulator via a host llvmpipe segfault, not OOM 2026-08-11 22:38:14 -04:00
swung0x48 ab62f81545 [Docs] (tools/trace_replay): the local farm ANGLE is not the CI ANGLE - re-attribute the improved-transparency causes 2026-08-11 22:32:14 -04:00
swung0x48 f6cf04d6d7 [Docs] (tools/trace_replay): name the two generated-ESSL defects that empty the improved-transparency lane on ANGLE 2026-08-11 22:25:57 -04:00
swung0x48 38eb9589f9 [Test] (tools/trace_replay): dump every live framebuffer attachment at a call boundary 2026-08-11 22:25:57 -04:00
swung0x48 99ebf67a3d [Fix] (CMake): the Android API guard holds a floor of 26, not an exact pin - higher platforms may configure, the minSdk-26 gradle build enforces API usage 2026-08-11 22:08:54 -04:00
swung0x48 2292e99476 Merge remote-tracking branch 'origin/dev' into dev 2026-08-11 21:59:25 -04:00
swung0x48 4c5afecc71 Merge branch 'fix/drop-android-api-pin' into dev 2026-08-11 21:59:05 -04:00
swung0x48 1c5744f2be [Fix] (MobileGL, Build): drop the __ANDROID_API__ source pin; hold API 26 in CMake instead 2026-08-11 21:58:30 -04:00
swung0x48 22859b0958 [Docs] (tools/trace_replay): localise the improved-transparency ANGLE divergence to the OIT composite 2026-08-11 21:55:40 -04:00
swung0x48 7aa958fbc9 [Docs] (tools/trace_replay): reproduce the Android DirectGLES lane on Linux, and pin improved-transparency below MobileGL 2026-08-11 21:43:56 -04:00
swung0x48 a4bd4e04a1 Merge branch 'fix/ssbo-rc13' into dev 2026-08-11 21:11:38 -04:00
swung0x48 09459edb6b [Fix] (CI, android-plugin): an ANGLE window surface lost before init finishes is infrastructure, not a trace failure 2026-08-11 21:00:31 -04:00
swung0x48 8af6ebc174 [Fix] (MG_State): seed uniform initializers through the phase-A gate, not off the raw artifact block 2026-08-11 20:59:25 -04:00
swung0x48 577cd8c670 [Fix] (3rdparty/glslang): bump the fork to the uniform-initializer preservation patch 2026-08-11 20:56:59 -04:00
swung0x48 5267243404 [Test] (MG_IntegrationTest): pin declared uniform initializers end to end, on both backends 2026-08-11 20:56:07 -04:00
swung0x48 33c2715912 [Fix] (MG_State, MG_Util): a default-block uniform starts at its declared initializer instead of zero 2026-08-11 20:54:14 -04:00
swung0x48 3068cdadf8 [Test] (CI): keep the emulator log of the attempt that lost the emulator, and record host memory pressure 2026-08-11 20:51:54 -04:00
swung0x48 6dd0201bf2 [Fix, Test] (MG_Impl/GLImpl): an indexed buffer SIZE query reports the range that was asked for, not what the buffer currently holds 2026-08-11 20:43:28 -04:00
swung0x48 05bef7118b [Fix, Test] (MG_State): a pipeline draw uses the block bindings its stage programs were given, not the ones their shaders declared 2026-08-11 20:23:06 -04:00
swung0x48 94e75fef79 [Fix, Test] (MG_State, MG_Impl): glIsProgramPipeline answers for the first bind, not for the materialization every pipeline command now does 2026-08-11 20:10:18 -04:00
swung0x48 43bcd03dca [Fix] (MG_Backend/DirectVulkan, MG_Impl): a malformed graphics pipeline shape skips the draw instead of faulting inside the driver 2026-08-11 20:10:18 -04:00
swung0x48 2ce0595fab [Fix, Test] (MG_State, MG_Impl, MG_Backend): a program pipeline's compute stage is dispatched on its own, and the graphics composite draws its stage programs' uniform values 2026-08-11 20:10:17 -04:00
swung0x48 ba9af18033 [Test] (MG_IntegrationTest): park the two program-pipeline cases whose mechanisms are still open 2026-08-11 20:10:17 -04:00
swung0x48 f7d63f88fa [Fix, Test] (MG_State, MG_Impl): a reserved program-pipeline name takes state from UseProgramStages and its siblings instead of rejecting them 2026-08-11 20:10:16 -04:00
swung0x48 6cf5a7744e [Test] (MG_IntegrationTest): separable programs drawn through a program pipeline object 2026-08-11 20:10:16 -04:00
swung0x48 ca3d24f5ea [Fix] (CI, android-plugin): keep a failed Android retrace's result artifact, and echo the logs a failed replay already wrote 2026-08-11 19:30:38 -04:00
swung0x48 cc34d34706 [Fix, Test] (MG_Backend/DirectGLES): hand every compiled shader's lifetime to its program - glDeleteShader was never called, so each program build leaked its driver shader objects 2026-08-11 11:11:28 -04:00
swung0x48 757b31592d [Fix, Test] (MG_Backend/DirectVulkan): a multisample resolve that must also change orientation resolves through a pooled scratch image, then blits 2026-08-11 10:49:58 -04:00
swung0x48 dbae4eda10 [Fix, Test] (MG_Backend/DirectVulkan): depth blits to or from the default framebuffer convert their rect out of GL's bottom-origin space, like the colour blit does 2026-08-11 10:24:59 -04:00
swung0x48 fa5ff5d168 [Test] (MG_IntegrationTest): every texture access routine must read the same texel out of a usampler2DArray 2026-08-11 10:21:38 -04:00
swung0x48 994ae372f8 [Fix, Test] (MG_Backend/DirectVulkan): an SSBO block instance array is one binding of N descriptors, so bind every element from its own GL binding point 2026-08-11 10:11:23 -04:00
swung0x48 7ba012adf9 [Test] (MG_IntegrationTest): SSBO runtime-array length across preambles, block arrays and bound ranges 2026-08-11 10:11:23 -04:00
swung0x48 b1fdffd767 [Fix, Test] (MG_Backend/DirectVulkan): read the default framebuffer's depth and stencil back instead of leaving the caller's buffer untouched 2026-08-11 10:11:22 -04:00
swung0x48 18c17ae5ca [Fix, Test] (MG_Backend/DirectVulkan): a blit into the default framebuffer must execute the clear parked before it, not leave it for the readback 2026-08-11 10:08:25 -04:00
swung0x48 16c010985f [Fix, Test] (MG_Impl): multi-bind name rejection is per element, and only transform feedback constrains the range size to a multiple of four 2026-08-11 09:17:51 -04:00
swung0x48 6f64ec0f51 [Fix, Test] (MG_Impl): validate indirect-dispatch and indirect-count arguments before the backend-availability check 2026-08-11 09:12:49 -04:00
swung0x48 5d47698349 [Fix, Test] (MG_Backend/DirectVulkan): materialize the default framebuffer's pending clear before a readback, alpha included 2026-08-11 09:09:31 -04:00
swung0x48 7b593e39ef [Fix, Test] (MG_Impl, MG_State): negative-path GL errors for multi_bind, indirect_parameters, texture_storage, compute dispatch/link and buffer-range alignment; indexed getters answer the full pname table 2026-08-11 09:02:22 -04:00
swung0x48 d83b4dbbb5 [Fix, Test] (MG_State, MG_Impl): ARB_vertex_attrib_binding state model - spec stride default, legacy stride/pointer shadows, divisor re-binds, core-profile VAO-0 rejection 2026-08-11 08:49:37 -04:00
swung0x48 964a7fcc92 [Fix, Test] (MG_State, MG_Backend/DirectVulkan): resolve transform-feedback captures that name a member of an output interface block 2026-08-11 08:39:39 -04:00
swung0x48 5a7bd9942d [Fix] (MG_Backend/DirectVulkan): print VkShaderModule as a 64-bit value - the const void* cast is ill-formed on 32-bit ABIs where the handle is a plain uint64_t 2026-08-11 08:19:31 -04:00
swung0x48 21a43bf6a4 [Fix, Test] (MG_Backend/DirectVulkan): re-land the gl_FragCoord default-framebuffer origin fix - the suspected slowdown was a mismeasurement, paired timings are within 1% 2026-08-11 07:54:24 -04:00
swung0x48 5b6dec2d81 [Revert] (MG_Backend/DirectVulkan): back out the gl_FragCoord default-framebuffer origin fix - correct, but it costs DirectVulkan a large order-dependent slowdown that is not yet root-caused 2026-08-11 06:30:30 -04:00
swung0x48 543c29bf86 [Fix, Test] (MG_Backend/DirectVulkan): gl_FragCoord on the default framebuffer reports GL's window origin - the stored row is not the window row once the viewport rect is converted 2026-08-11 05:25:32 -04:00
swung0x48 ef562ee9b5 [Fix, Test] (MG_Backend/DirectGLES): backend framebuffer, renderbuffer and sampler twins release their driver ids - a framebuffer per readback leaked the driver into stale pixels 2026-08-11 03:54:05 -04:00
swung0x48 fa0f6693d0 [Fix, Test] (MG_State, MG_Impl): reflection-backed glGetProgramiv queries answer zero instead of dereferencing a null TProgram 2026-08-11 02:59:10 -04:00
swung0x48 a6c362c6ce [Fix, Test] (MG_Backend/DirectVulkan): convert every default-framebuffer rectangle between GL and display Y origins - viewport, scissor, ReadPixels offset, rect-capable readback remap, blit source 2026-08-11 02:51:22 -04:00
swung0x48 921504eccf [Fix, Test] (MG_Backend/DirectVulkan, MG_Util): clamp Vulkan-derived GL buffer limits and saturate the uint32 to Int casts 2026-08-11 02:38:51 -04:00
swung0x48 7c5fc03b26 [Fix, Test] (MG_Backend/DirectVulkan): never bind or cache a null pipeline, and name the modules a failed vkCreateGraphicsPipelines rejected 2026-08-11 02:33:32 -04:00
swung0x48 ed29e63543 [Fix, Test] (MG_Impl): glGetProgramResourceiv reports a written length on every exit path 2026-08-11 02:27:26 -04:00
swung0x48 5c8a9c41d6 [Fix, Test] (MG_Impl, MG_State): GL entry points record errors instead of throwing through the C ABI - CopyTexImage superset rule, TEXTURE_BUFFER level queries, indexed cap toggles 2026-08-11 02:24:41 -04:00
swung0x48 efa0345c36 [Fix] (MG_Backend/DirectVulkan): offscreen surfaces never touch a window system - delete the hidden-Xlib fallback, make surface and device-enumeration failures loud, guard zero devices 2026-08-11 00:58:24 -04:00
swung0x48 ce0f18969c [Fix] (MG_Util): dlopen the versioned libEGL.so.1 first - the unversioned name is a dev-package symlink absent on runtime-only systems, and its silent failure nulled every EGL entry point 2026-08-11 00:58:23 -04:00
swung0x48 7ce0966e7d [Fix, Test] (MG_IntegrationTest): the harness is surfaceless by construction - never binds a window system, hardware demand split out of REQUIRE_GPU, pre-flight crashes keep their cores 2026-08-11 00:58:22 -04:00
swung0x48 1c6ca2753f [Test] (CI): wire the integration-gpu lane into the Test workflow - lavapipe ICD pinned at configure time, REQUIRE_GPU armed, failure-only core-dump artifacts in every native lane 2026-08-11 00:58:21 -04:00
swung0x48 61b0532865 [Fix] (MG_Backend/DirectVulkan): replay client-memory multi-draw through the single-draw path - the batched shared index view cannot express per-draw client pointers and dropped the whole batch 2026-08-10 21:58:06 -04:00
swung0x48 f5b8a505ed [Fix] (MG_Backend/DirectVulkan): keep the transient arena's outgrown buffers alive until frame rewind - the transient drain freed them while frame-serial memos still bound them 2026-08-10 21:47:28 -04:00
swung0x48 8371365db5 [Fix] (MG_Backend/DirectVulkan): include the texture enum converter the WARN-level log line needs - VulkanRenderer.cpp did not compile below INFO log level 2026-08-10 21:47:27 -04:00
swung0x48 b219992ee3 [Feat, Perf, Test] (MG_Util, MG_Backend, MG_Test): test-lane SPIR-V validation latch; entry-local Private rewrite, in-chain rect lowering and uniform-Location strip 2026-08-10 13:00:20 -04:00
swung0x48 94233ef928 [Fix] (MG_Backend/DirectVulkan): drop draws and dispatches when a program has no optimized SPIR-V - a phase-B failure left raw glslang words in GetGeneratedSpirv 2026-08-10 13:00:11 -04:00
swung0x48 0827d7a539 [Fix] (MobileGL): define the numeric log levels in Defines.h - the MOBILEGL_ASSERT gate compared undefined tokens in TUs that never include Log.h 2026-08-10 13:00:10 -04:00
swung0x48 5248b8b746 [Test] (MG_Test): replay a real Iris shader pair through the whole async frontend, both phases and both quirk states 2026-08-10 10:06:26 -04:00
swung0x48 d868e1c476 [Test] (MG_Util, MG_State): remove the TEMP stage-timing probes - the async-compile measurement campaign is done 2026-08-10 09:56:05 -04:00
swung0x48 c3412ca394 [Fix, Test] (MG_Util): give inactive vertex inputs a Location - undecorated inputs are invalid SPIR-V and Adreno rejects the whole pipeline 2026-08-10 09:27:01 -04:00
swung0x48 5ccaff37af [Fix] (MG_State): revert the Cut A-prime reorder - device dumps prove the ordering is not the pipeline failure, and record the third measurement 2026-08-10 08:58:38 -04:00
swung0x48 71e29f9d58 [Fix] (MG_State): keep GlslangToSpv ahead of buildReflection - reflection-first drops a vertex input's Location decoration and Adreno rejects the pipeline 2026-08-10 08:33:47 -04:00
swung0x48 8ad07c222c [Test] (MG_Test): cover the cancelled-SPIR-V program, the buffered-write valve and drop, the delete-does-not-block bound and the zero-thread join 2026-08-10 07:31:58 -04:00
swung0x48 5722094d6f [Docs] (MG_State): the early AST drop frees only the re-parsed shaders - the compile node co-owns the rest 2026-08-10 07:31:58 -04:00
swung0x48 4831387cf0 [Fix] (MG_State, MG_Backend/DirectVulkan): a program whose SPIR-V job was cancelled faulted on the first glUniform - bounds-check the shadow lookup 2026-08-10 07:31:57 -04:00
swung0x48 d03b72267a [Perf] (MG_State): drop the parsed ASTs as soon as the SPIR-V job has generated its modules 2026-08-10 06:37:49 -04:00
swung0x48 847ec74f48 [Test] (MG_Test): AsyncSpirvPhaseTest - phase-A completeness, buffered-uniform replay, version bumps and the cancel matrix 2026-08-10 06:12:39 -04:00
swung0x48 e02e5caa17 [Fix] (MG_State, MG_Backend/DirectGLES): join both link phases on the draw and drain paths, and refuse to bind a program whose SPIR-V never arrived 2026-08-10 06:08:30 -04:00
swung0x48 1958934594 [Feat] (MG_State, MG_Impl): buffer non-opaque glUniform writes across the SPIR-V window and replay them at its publish 2026-08-10 06:05:37 -04:00
swung0x48 dec0c5eaff [Refactor, Test] (MG_State, MG_Util): split the link into ProgramLinkTask (query surface) and a chained ProgramSpirvTask behind its own join gate 2026-08-10 06:02:52 -04:00
swung0x48 b6a44cd1e2 [Refactor] (MG_State): run reflection and link validation before SPIR-V generation - the ordering constraint retested byte-identical 2026-08-10 05:50:20 -04:00
swung0x48 85f45d0e44 [Test] (MG_Util, MG_State): TEMP spirv-null plumbing probe for the allocator-pathology discriminator 2026-08-10 05:42:23 -04:00
swung0x48 404236d337 [Test] (MG_Util, MG_State): TEMP stage-timing probes for the async-compile campaign - remove before merge 2026-08-10 05:16:24 -04:00
swung0x48 6ea948779e [Feat, Test] (MG_Impl, MG_State, MG_Util): opt-in MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS - shader compile status/log answer without joining, latched per compile 2026-08-10 02:06:17 -04:00
560 changed files with 186588 additions and 8265 deletions
+4 -51
View File
@@ -1,6 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=trace-fixture-lib.sh
. "${script_dir}/trace-fixture-lib.sh"
if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then
echo "usage: $0 <trace-case> [fixture-dir]" >&2 echo "usage: $0 <trace-case> [fixture-dir]" >&2
exit 2 exit 2
@@ -62,57 +66,6 @@ if [ "${case_name}" = "OpenRA" ]; then
exit 0 exit 0
fi fi
get_lfs_metadata() {
local file="$1"
local pointer
local expected_oid
local expected_size
if ! pointer="$(git show "HEAD:${file}" 2>/dev/null)"; then
echo "failed to read tracked fixture metadata: ${file}" >&2
return 1
fi
if ! grep -q '^version https://git-lfs.github.com/spec/v1$' <<< "${pointer}"; then
echo "tracked fixture is not a Git LFS pointer: ${file}" >&2
return 1
fi
expected_oid="$(awk '$1 == "oid" && $2 ~ /^sha256:/ { sub(/^sha256:/, "", $2); print $2 }' <<< "${pointer}")"
expected_size="$(awk '$1 == "size" { print $2 }' <<< "${pointer}")"
if ! [[ "${expected_oid}" =~ ^[0-9a-f]{64}$ ]] || ! [[ "${expected_size}" =~ ^[0-9]+$ ]]; then
echo "invalid Git LFS pointer metadata: ${file}" >&2
return 1
fi
printf '%s %s\n' "${expected_oid}" "${expected_size}"
}
verify_fixture_file() {
local downloaded_file="$1"
local display_name="$2"
local expected_oid="$3"
local expected_size="$4"
local actual_oid
local actual_size
if [ ! -f "${downloaded_file}" ]; then
echo "fixture file is missing: ${display_name}" >&2
return 1
fi
actual_size="$(wc -c < "${downloaded_file}" | tr -d '[:space:]')"
if [ "${actual_size}" != "${expected_size}" ]; then
echo "fixture size mismatch for ${display_name}: expected ${expected_size}, got ${actual_size}" >&2
return 1
fi
actual_oid="$(sha256sum "${downloaded_file}" | awk '{ print $1 }')"
if [ "${actual_oid}" != "${expected_oid}" ]; then
echo "fixture SHA-256 mismatch for ${display_name}: expected ${expected_oid}, got ${actual_oid}" >&2
return 1
fi
}
fetch_file_from_mirror() { fetch_file_from_mirror() {
local file="$1" local file="$1"
local url="$2" local url="$2"
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env bash
# Cache-side helper for trace fixtures.
#
# key <case> [fixture-dir] derive the actions/cache key and path list
# verify <case> [fixture-dir] check restored fixtures against their pointers
# reset <case> [fixture-dir] drop restored fixtures, leaving the pointers
#
# The cache key is content-addressed on the Git LFS pointer oids tracked at
# HEAD, which are readable from a plain checkout without smudging. Fixture
# content therefore maps 1:1 onto a key: unchanged content hits, changed
# content is a new key and thus a miss, and the download path handles it. The
# key deliberately carries no restore-keys prefix in the workflow - a fixture
# that does not match the pointer exactly must never be restored.
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=trace-fixture-lib.sh
. "${script_dir}/trace-fixture-lib.sh"
# Bump when the key derivation changes in a way that must invalidate old
# entries; the content digest alone would not notice a format change.
key_schema="v1"
if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then
echo "usage: $0 <key|verify|reset> <trace-case> [fixture-dir]" >&2
exit 2
fi
command_name="$1"
case_name="$2"
fixture_dir="${3:-tools/trace_replay/fixtures}"
python_bin="${PYTHON:-python3}"
if ! command -v "${python_bin}" >/dev/null 2>&1 && command -v python >/dev/null 2>&1; then
python_bin=python
fi
mapfile -t files < <(trace_fixture_files "${case_name}" "${fixture_dir}" "${python_bin}")
if [ "${#files[@]}" -eq 0 ]; then
echo "no fixture files declared for trace case: ${case_name}" >&2
exit 1
fi
# Writes "name=value" to $GITHUB_OUTPUT when running under Actions, and to
# stdout otherwise so the script stays runnable (and testable) off-CI.
emit_output() {
local name="$1"
local value="$2"
if [ -n "${GITHUB_OUTPUT:-}" ]; then
if [[ "${value}" == *$'\n'* ]]; then
local delimiter="ghadelim_$(date +%s%N)_$$"
{
printf '%s<<%s\n' "${name}" "${delimiter}"
printf '%s\n' "${value}"
printf '%s\n' "${delimiter}"
} >> "${GITHUB_OUTPUT}"
else
printf '%s=%s\n' "${name}" "${value}" >> "${GITHUB_OUTPUT}"
fi
fi
printf '%s=%s\n' "${name}" "${value}"
}
sanitize_case() {
printf '%s' "$1" | sed 's/[^A-Za-z0-9._-]/_/g'
}
case "${command_name}" in
key)
manifest=""
for file in "${files[@]}"; do
# A case whose fixtures are committed directly rather than through Git LFS
# (OpenRA) has no pointer oid to key on, and nothing to download either.
# Report it as uncacheable so the workflow skips the cache entirely.
if ! metadata="$(get_lfs_metadata "${file}" 2>/dev/null)"; then
echo "trace case ${case_name} is not stored in Git LFS; skipping fixture cache" >&2
emit_output "cacheable" "false"
emit_output "key" ""
exit 0
fi
read -r expected_oid expected_size <<< "${metadata}"
manifest+="$(basename "${file}") ${expected_oid} ${expected_size}"$'\n'
done
digest="$(printf '%s' "${manifest}" | sha256sum | awk '{ print substr($1, 1, 16) }')"
safe_case="$(sanitize_case "${case_name}")"
emit_output "cacheable" "true"
emit_output "key" "trace-fixture-${key_schema}-${safe_case}-${digest}"
emit_output "paths" "$(printf '%s\n' "${files[@]}")"
;;
verify)
for file in "${files[@]}"; do
metadata="$(get_lfs_metadata "${file}")"
read -r expected_oid expected_size <<< "${metadata}"
verify_fixture_file "${file}" "${file}" "${expected_oid}" "${expected_size}"
done
echo "Verified ${#files[@]} fixture file(s) for ${case_name} against the tracked Git LFS pointers."
;;
reset)
# Put the working tree back to the pointer files a fresh checkout would
# have, so that a rejected cache entry falls through to exactly the same
# download path a cache miss takes.
for file in "${files[@]}"; do
rm -f "${file}" "${file}.tmp"
done
git checkout -- "${files[@]}"
echo "Reset ${#files[@]} fixture file(s) for ${case_name} to their tracked Git LFS pointers."
;;
*)
echo "unknown command: ${command_name}" >&2
exit 2
;;
esac
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
# Shared helpers for trace-fixture handling: reading the in-tree Git LFS pointer
# metadata and verifying a fixture file against it. Sourced by
# fetch-trace-fixture-lfs.sh (verify after download) and by
# trace-fixture-cache.sh (cache key derivation and verify after cache restore),
# so both paths agree on what a valid fixture is.
# Reads the Git LFS pointer tracked at HEAD for a fixture path and prints
# "<oid> <size>". Fails if the tracked blob is not a well-formed LFS pointer.
get_lfs_metadata() {
local file="$1"
local pointer
local expected_oid
local expected_size
if ! pointer="$(git show "HEAD:${file}" 2>/dev/null)"; then
echo "failed to read tracked fixture metadata: ${file}" >&2
return 1
fi
if ! grep -q '^version https://git-lfs.github.com/spec/v1$' <<< "${pointer}"; then
echo "tracked fixture is not a Git LFS pointer: ${file}" >&2
return 1
fi
expected_oid="$(awk '$1 == "oid" && $2 ~ /^sha256:/ { sub(/^sha256:/, "", $2); print $2 }' <<< "${pointer}")"
expected_size="$(awk '$1 == "size" { print $2 }' <<< "${pointer}")"
if ! [[ "${expected_oid}" =~ ^[0-9a-f]{64}$ ]] || ! [[ "${expected_size}" =~ ^[0-9]+$ ]]; then
echo "invalid Git LFS pointer metadata: ${file}" >&2
return 1
fi
printf '%s %s\n' "${expected_oid}" "${expected_size}"
}
# Checks an on-disk fixture against the size and SHA-256 from its LFS pointer.
verify_fixture_file() {
local downloaded_file="$1"
local display_name="$2"
local expected_oid="$3"
local expected_size="$4"
local actual_oid
local actual_size
if [ ! -f "${downloaded_file}" ]; then
echo "fixture file is missing: ${display_name}" >&2
return 1
fi
actual_size="$(wc -c < "${downloaded_file}" | tr -d '[:space:]')"
if [ "${actual_size}" != "${expected_size}" ]; then
echo "fixture size mismatch for ${display_name}: expected ${expected_size}, got ${actual_size}" >&2
return 1
fi
actual_oid="$(sha256sum "${downloaded_file}" | awk '{ print $1 }')"
if [ "${actual_oid}" != "${expected_oid}" ]; then
echo "fixture SHA-256 mismatch for ${display_name}: expected ${expected_oid}, got ${actual_oid}" >&2
return 1
fi
}
# Prints the fixture file paths of a trace case, one per line. Strips CR so the
# result is usable when python emits CRLF (Git Bash on Windows).
trace_fixture_files() {
local case_name="$1"
local fixture_dir="$2"
local python_bin="${3:-python3}"
"${python_bin}" tools/trace_replay/trace_cases.py \
--format fixture-files \
--case "${case_name}" \
--fixture-root "${fixture_dir}" | tr -d '\r'
}
+3 -3
View File
@@ -44,12 +44,12 @@ require 'key:MOBILEGL_BACKEND_TYPE' "$plugin_resource_text" 'V2 backend variable
require 'defaultValue:DirectGLES' "$plugin_resource_text" 'V2 DirectGLES default' require 'defaultValue:DirectGLES' "$plugin_resource_text" 'V2 DirectGLES default'
require 'DirectVulkan' "$plugin_resource_text" 'V2 DirectVulkan option' require 'DirectVulkan' "$plugin_resource_text" 'V2 DirectVulkan option'
require 'key:MOBILEGL_DISABLE_TIMERQUERY' "$plugin_resource_text" 'V2 timer-query toggle' require 'key:MOBILEGL_DISABLE_TIMERQUERY' "$plugin_resource_text" 'V2 timer-query toggle'
require 'key:MOBILEGL_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle' require 'key:MOBILEGL_MAGMA_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
require 'key:MOBILEGL_MAGMA_R11G11B10F_FALLBACK' "$plugin_resource_text" 'V2 Magma format fallback toggle' require 'key:MOBILEGL_MAGMA_R11G11B10F_FALLBACK' "$plugin_resource_text" 'V2 Magma format fallback toggle'
require 'key:MOBILEGL_MAGMA_FRAMESINFLIGHT' "$plugin_resource_text" 'V2 Magma frames-in-flight setting' require 'key:MOBILEGL_MAGMA_FRAMESINFLIGHT' "$plugin_resource_text" 'V2 Magma frames-in-flight setting'
require 'key:MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle' require 'key:MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
require 'key:MOBILEGL_COHERENT_AS_FLUSH' "$plugin_resource_text" 'V2 coherent-as-flush toggle' require 'key:MOBILEGL_COHERENT_AS_FLUSH' "$plugin_resource_text" 'V2 coherent-as-flush toggle'
require 'key:MOBILEGL_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle' require 'key:MOBILEGL_ESPRYT_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
if [[ $(grep -Fc 'fclPlugin_V2' <<<"$plugin_manifest") -ne 1 ]]; then if [[ $(grep -Fc 'fclPlugin_V2' <<<"$plugin_manifest") -ne 1 ]]; then
echo '::error::Plugin manifest must expose exactly one V2 descriptor' >&2 echo '::error::Plugin manifest must expose exactly one V2 descriptor' >&2
+121 -17
View File
@@ -6,11 +6,18 @@ on:
- dev - dev
- Feat/Backend-Direct-GLES - Feat/Backend-Direct-GLES
- Feat/Backend-Direct-Vulkan - Feat/Backend-Direct-Vulkan
# TEMPORARY, remove before merging the MGPipe work into dev: the disaggregation
# branch runs the full lane on every push so a phase's landing is not gated on
# someone remembering to dispatch the workflow by hand.
- feat/disaggregated
workflow_dispatch: workflow_dispatch:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
actions: write
contents: read
env: env:
CCACHE_BASEDIR: ${{ github.workspace }} CCACHE_BASEDIR: ${{ github.workspace }}
CCACHE_COMPRESS: "true" CCACHE_COMPRESS: "true"
@@ -41,12 +48,11 @@ jobs:
gradle-version: 8.10.2 gradle-version: 8.10.2
- name: Restore ccache - name: Restore ccache
uses: actions/cache@v5 uses: actions/cache/restore@v5
with: with:
path: .ccache path: .ccache
key: ${{ runner.os }}-apk-${{ github.job }}-ccache-${{ github.ref_name }}-${{ github.run_id }} key: ${{ runner.os }}-apk-${{ github.job }}-ccache-v1
restore-keys: | restore-keys: |
${{ runner.os }}-apk-${{ github.job }}-ccache-${{ github.ref_name }}-
${{ runner.os }}-apk-${{ github.job }}-ccache- ${{ runner.os }}-apk-${{ github.job }}-ccache-
- name: Install ccache - name: Install ccache
@@ -125,6 +131,28 @@ jobs:
if: always() if: always()
run: ccache --show-stats run: ccache --show-stats
# Rewrite one rolling entry per job on the default branch. The upload stays
# cumulative - it carries every object restored at the top of this run plus
# the few TUs that actually changed - but Actions cache keys are immutable,
# so the superseded blob has to be released before the same key can be
# re-uploaded. Running after the build means a failed build leaves the
# existing entry untouched. The other trigger branches restore this entry
# rather than each writing a ~4 GB one of their own.
- name: Release superseded ccache entry
if: github.ref_name == github.event.repository.default_branch
env:
GH_TOKEN: ${{ github.token }}
CACHE_KEY: ${{ runner.os }}-apk-${{ github.job }}-ccache-v1
run: gh cache delete "${CACHE_KEY}" || true
- name: Save ccache
if: github.ref_name == github.event.repository.default_branch
continue-on-error: true
uses: actions/cache/save@v5
with:
path: .ccache
key: ${{ runner.os }}-apk-${{ github.job }}-ccache-v1
- name: Verify APK metadata and packaging - name: Verify APK metadata and packaging
run: | run: |
AAPT2="$(find "$ANDROID_HOME/build-tools" -name aapt2 -type f | sort -V | tail -n 1)" AAPT2="$(find "$ANDROID_HOME/build-tools" -name aapt2 -type f | sort -V | tail -n 1)"
@@ -185,7 +213,7 @@ jobs:
- name: Load trace cases - name: Load trace cases
id: trace-cases id: trace-cases
run: | run: |
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk)" >> "$GITHUB_OUTPUT" echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk-matrix)" >> "$GITHUB_OUTPUT"
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT" echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
trace-fixtures: trace-fixtures:
@@ -201,9 +229,41 @@ jobs:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Derive trace fixture cache key
id: fixture-key
run: bash .github/scripts/trace-fixture-cache.sh key '${{ matrix.case }}'
- name: Restore trace fixture cache
id: fixture-cache
if: steps.fixture-key.outputs.cacheable == 'true'
uses: actions/cache/restore@v5
with:
path: ${{ steps.fixture-key.outputs.paths }}
key: ${{ steps.fixture-key.outputs.key }}
- name: Verify restored trace fixture
id: fixture-verify
if: steps.fixture-cache.outputs.cache-hit == 'true'
run: |
if bash .github/scripts/trace-fixture-cache.sh verify '${{ matrix.case }}'; then
echo "ok=true" >> "$GITHUB_OUTPUT"
else
echo "ok=false" >> "$GITHUB_OUTPUT"
echo "::warning::Cached fixture for ${{ matrix.case }} failed verification; falling back to the download path"
bash .github/scripts/trace-fixture-cache.sh reset '${{ matrix.case }}'
fi
- name: Fetch trace fixture - name: Fetch trace fixture
if: steps.fixture-verify.outputs.ok != 'true'
run: bash .github/scripts/fetch-trace-fixture-lfs.sh '${{ matrix.case }}' run: bash .github/scripts/fetch-trace-fixture-lfs.sh '${{ matrix.case }}'
- name: Save trace fixture cache
if: steps.fixture-key.outputs.cacheable == 'true' && steps.fixture-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: ${{ steps.fixture-key.outputs.paths }}
key: ${{ steps.fixture-key.outputs.key }}
- name: Stage trace fixture - name: Stage trace fixture
run: | run: |
safe_case="$(printf '%s' '${{ matrix.case }}' | sed 's/[^A-Za-z0-9._-]/_/g')" safe_case="$(printf '%s' '${{ matrix.case }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
@@ -281,13 +341,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: 4 max-parallel: 4
matrix: matrix: ${{ fromJSON(needs.trace-cases.outputs.android) }}
backend:
- name: DirectGLES
gpu: software
- name: DirectVulkan
gpu: lavapipe
case: ${{ fromJSON(needs.trace-cases.outputs.android) }}
steps: steps:
- name: Set Swap Space - name: Set Swap Space
uses: pierotofy/set-swap-space@v1.0 uses: pierotofy/set-swap-space@v1.0
@@ -367,9 +421,12 @@ jobs:
- name: Retrace and validate - name: Retrace and validate
env: env:
MOBILEGL_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }} MOBILEGL_ESPRYT_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }} MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }} MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }}
MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
run: | run: |
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk" apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
test -f "${apk_file}" test -f "${apk_file}"
@@ -379,6 +436,9 @@ jobs:
if [ "${{ matrix.backend.name }}" = "DirectGLES" ] && [ "${{ matrix.case.name }}" = "minecraft-1.21.4-fabric-iris-bliss-in-world" ]; then if [ "${{ matrix.backend.name }}" = "DirectGLES" ] && [ "${{ matrix.case.name }}" = "minecraft-1.21.4-fabric-iris-bliss-in-world" ]; then
extra_retrace_args+=(--avoid-angle-llvmpipe-sampler-mipmap-min-filter) extra_retrace_args+=(--avoid-angle-llvmpipe-sampler-mipmap-min-filter)
fi fi
if [ "${{ matrix.backend.name }}" = "DirectGLES" ] && [ "${{ matrix.case.avoid_angle_llvmpipe_explicit_lod_bias || false }}" = "true" ]; then
extra_retrace_args+=(--avoid-angle-llvmpipe-explicit-lod-bias)
fi
if [ "${{ matrix.case.coherent_as_flush || false }}" = "true" ]; then if [ "${{ matrix.case.coherent_as_flush || false }}" = "true" ]; then
extra_retrace_args+=(--coherent-as-flush) extra_retrace_args+=(--coherent-as-flush)
fi fi
@@ -411,6 +471,24 @@ jobs:
run_retrace || retrace_status=$? run_retrace || retrace_status=$?
if [ "${retrace_status}" -eq 75 ]; then if [ "${retrace_status}" -eq 75 ]; then
echo "::warning::Android emulator infrastructure failed; restarting it and retrying this retrace once." echo "::warning::Android emulator infrastructure failed; restarting it and retrying this retrace once."
# Surface-lost is retried rather than failed, so it would otherwise
# be invisible. Report it per job - a healthy run prints nothing and
# a rate spike shows up as a row per affected case.
reason_file="android-retrace-result/infrastructure-failure-reason.txt"
surface_lost_retries=0
if [ -f "${reason_file}" ]; then
surface_lost_retries="$(grep -c 'angle-surface-lost' "${reason_file}" || true)"
fi
if [ "${surface_lost_retries}" -gt 0 ]; then
echo "surface-lost retries: ${surface_lost_retries} (${{ matrix.backend.name }}, ${{ matrix.case.name }})" \
>> "${GITHUB_STEP_SUMMARY}"
fi
# The restart truncates EMULATOR_LOG, and the attempt that lost the
# emulator is the one worth reading - the retry usually only shows
# the wreckage. Keep the first attempt's log before it is clobbered.
if [ -f "${EMULATOR_LOG}" ]; then
cp "${EMULATOR_LOG}" "${EMULATOR_LOG}.first-attempt" || true
fi
sh android-plugin/run-avd-ci.sh stop \ sh android-plugin/run-avd-ci.sh stop \
--avd-name "${AVD_NAME}" \ --avd-name "${AVD_NAME}" \
--emulator-log "${EMULATOR_LOG}" \ --emulator-log "${EMULATOR_LOG}" \
@@ -450,6 +528,13 @@ jobs:
if [ -f "${EMULATOR_LOG}" ]; then if [ -f "${EMULATOR_LOG}" ]; then
cp "${EMULATOR_LOG}" android-retrace-result/diagnostics/emulator.log cp "${EMULATOR_LOG}" android-retrace-result/diagnostics/emulator.log
fi fi
if [ -f "${EMULATOR_LOG}.first-attempt" ]; then
cp "${EMULATOR_LOG}.first-attempt" android-retrace-result/diagnostics/emulator-first-attempt.log
fi
# A vanished emulator looks identical whether the host OOM killer took
# qemu or the renderer faulted. These two say which.
free -h > android-retrace-result/diagnostics/host-memory.txt 2>&1 || true
sudo dmesg -T 2>/dev/null | tail -300 > android-retrace-result/diagnostics/host-dmesg.txt || true
- name: Stop Emulator - name: Stop Emulator
if: always() if: always()
@@ -531,22 +616,41 @@ jobs:
) )
if ((${#failed_cases[@]})); then if ((${#failed_cases[@]})); then
echo "Retaining fixtures for failed retrace case(s):" echo "Retaining fixtures and results for failed retrace case(s):"
printf ' %s\n' "${!failed_cases[@]}" printf ' %s\n' "${!failed_cases[@]}"
else else
echo "All retrace jobs succeeded; no fixtures need to be retained." echo "All retrace jobs succeeded; nothing needs to be retained."
fi fi
deleted=0 deleted=0
retained=0 retained=0
while IFS=$'\t' read -r artifact_id artifact_name; do while IFS=$'\t' read -r artifact_id artifact_name; do
keep=0
if [[ "${artifact_name}" == MobileGL-trace-fixture-* ]]; then if [[ "${artifact_name}" == MobileGL-trace-fixture-* ]]; then
case_name="${artifact_name#MobileGL-trace-fixture-}" case_name="${artifact_name#MobileGL-trace-fixture-}"
if [[ -v "failed_cases[${case_name}]" ]]; then if [[ -v "failed_cases[${case_name}]" ]]; then
echo "Retaining ${artifact_name} (${artifact_id}) for failed retrace." keep=1
((retained += 1))
continue
fi fi
elif [[ "${artifact_name}" == MobileGL-android-retrace-result-* ]]; then
# The result artifact carries mobilegl.log, retrace.log, logcat,
# the emulator log and the actual/diff images - the only record of
# why a retrace failed. Its name ends in -<backend>-<case>, so a
# suffix match on the case name keeps both backends' results for a
# case that failed on either of them, which is what a comparison
# needs. The match is anchored at the end, so a case name that is a
# prefix of a longer one does not retain the longer one's results.
for case_name in "${!failed_cases[@]}"; do
if [[ "${artifact_name}" == *-"${case_name}" ]]; then
keep=1
break
fi
done
fi
if ((keep)); then
echo "Retaining ${artifact_name} (${artifact_id}) for failed retrace."
((retained += 1))
continue
fi fi
echo "Deleting ${artifact_name} (${artifact_id})" echo "Deleting ${artifact_name} (${artifact_id})"
+1122 -17
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -27,3 +27,4 @@ MobileGL/MG*/cmake-build*
tools/trace_replay/work/ tools/trace_replay/work/
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
/.gradle
+6 -6
View File
@@ -7,9 +7,6 @@
[submodule "3rdparty/SPIRV-Cross"] [submodule "3rdparty/SPIRV-Cross"]
path = 3rdparty/SPIRV-Cross path = 3rdparty/SPIRV-Cross
url = https://github.com/KhronosGroup/SPIRV-Cross.git url = https://github.com/KhronosGroup/SPIRV-Cross.git
[submodule "include/FastSTL"]
path = include/FastSTL
url = https://github.com/MobileGL-Dev/FastSTL.git
[submodule "3rdparty/tracy"] [submodule "3rdparty/tracy"]
path = 3rdparty/tracy path = 3rdparty/tracy
url = https://github.com/wolfpld/tracy.git url = https://github.com/wolfpld/tracy.git
@@ -34,6 +31,9 @@
[submodule "3rdparty/asio"] [submodule "3rdparty/asio"]
path = 3rdparty/asio path = 3rdparty/asio
url = https://github.com/chriskohlhoff/asio.git url = https://github.com/chriskohlhoff/asio.git
[submodule "3rdparty/libfork"] [submodule "include/ska"]
path = 3rdparty/libfork path = include/ska
url = https://github.com/ConorWilliams/libfork.git url = https://github.com/MobileGL-Dev/flat_hash_map.git
[submodule "3rdparty/flatbuffers"]
path = 3rdparty/flatbuffers
url = https://github.com/google/flatbuffers.git
Vendored Submodule
+1
Submodule 3rdparty/flatbuffers added at 7e163021e5
-1
Submodule 3rdparty/libfork deleted from 9b2b844a5f
+276 -8
View File
@@ -14,12 +14,107 @@ option(MOBILEGL_ENABLE_TRACY "Enable tracy for profiling"
option(MOBILEGL_BUILD_TRACE_REPLAY "Build desktop apitrace replay runner" OFF) option(MOBILEGL_BUILD_TRACE_REPLAY "Build desktop apitrace replay runner" OFF)
option(MOBILEGL_TRACE_ANGLE_VARIANTS "Enable signed trace-APK ANGLE variant loading" OFF) option(MOBILEGL_TRACE_ANGLE_VARIANTS "Enable signed trace-APK ANGLE variant loading" OFF)
option(MOBILEGL_IOS "Build MobileGL for iOS instead of macOS when APPLE is set" OFF) option(MOBILEGL_IOS "Build MobileGL for iOS instead of macOS when APPLE is set" OFF)
# The disaggregated (two-process) shape. OFF is the shipping default and OFF
# must stay byte-comparable to a tree without MG_Remote at all: nothing under
# MobileGL/MG_Remote/ is compiled, no include path is added, and no library is
# linked, so `nm --defined-only libMobileGL.so | grep -i MG_Remote` is empty.
# That emptiness is one of the two byte-level equalities the plan's validation
# gates keep (section 10.3).
option(MOBILEGL_BUILD_DISAGGREGATED "Build the MG_Remote transport layer (two-process shape)" OFF)
option(MOBILEGL_BUILD_SERVER_SPIKE "Build the P0 spike-A MobileGLServer delivery-chain executable (Android only)" OFF)
# The PipeInputs strangler (ARCHITECTURE.md 9.2). OFF is the pull build and must stay
# byte-identical to a tree without either option: MGB_CTX is the live GLContext, no
# MGPipe/PipeInputs source is compiled, every MGP_FILL is ((void)0).
option(MOBILEGL_PIPE_PUSH "Backends read frontend state through the MGPipe PipeInputs block instead of MG_State::pGLContext (ARCHITECTURE.md 9.2 phase A)" OFF)
option(MOBILEGL_PIPE_VERIFY "Compile SnapshotFromGLContext() and the G4 per-verb shadow comparator; implies MOBILEGL_PIPE_PUSH; never shipped" OFF)
# Track H's old-versus-new arm (ARCHITECTURE.md 9.6). With a MOBILEGL_PIPE_PUSH bit clear
# the backend would still run the RE-KEYED memo code, so the bitmask alone stops being a
# valid A/B the moment a handle wave lands: this option compiles the pre-handle arm - the
# registries, OwnerEquals, the TwinLookupMemos, g_fbSlotCache, ComputePipelineStateHash,
# the address-keyed VaoDrawMemo - beside it, behind the same PipeInputs interface. ON for
# the whole migration window; it retires with the pull path itself at P13.
option(MOBILEGL_PIPE_LEGACY_MEMOS "Compile the pre-handle memo arm beside the {slot, gen} arm so Track H has a real A/B (ARCHITECTURE.md 9.6)" ON)
set(MOBILEGL_LOG_ACTIVE_LEVEL "MOBILEGL_LOG_LEVEL_INFO" CACHE STRING "MobileGL active log level macro") set(MOBILEGL_LOG_ACTIVE_LEVEL "MOBILEGL_LOG_LEVEL_INFO" CACHE STRING "MobileGL active log level macro")
set(MOBILEGL_VULKAN_LIBRARY "" CACHE FILEPATH "Vulkan loader/MoltenVK library to link for iOS builds") set(MOBILEGL_VULKAN_LIBRARY "" CACHE FILEPATH "Vulkan loader/MoltenVK library to link for iOS builds")
if (ANDROID) if (ANDROID)
set(MOBILEGL_BUILD_TEST OFF CACHE BOOL "Build MobileGL tests" FORCE) set(MOBILEGL_BUILD_TEST OFF CACHE BOOL "Build MobileGL tests" FORCE)
set(MOBILEGL_BUILD_BENCHMARK OFF CACHE BOOL "Build MobileGL benchmarks" FORCE) set(MOBILEGL_BUILD_BENCHMARK OFF CACHE BOOL "Build MobileGL benchmarks" FORCE)
# ------- Android API level policy: minimum 26, decided here and only here -------
# MobileGL ships against API 26: the codebase must not use any API introduced
# after 26. That usage constraint is enforced where it is real - the shipping
# gradle build compiles at minSdk 26, where a newer API is simply undeclared
# and fails to compile. Configuring at a HIGHER level is therefore allowed
# (nothing in the tree may rely on it), but a LOWER level would change the
# libc contract underneath the shipped library and is refused.
#
# This has to live at configure time because the level cannot be corrected
# from a source header. A `#define __ANDROID_API__ 26` in a common header
# only rewrites the macro for the bionic headers that happen to be included
# after it; any libc++ header pulled in earlier has already latched its
# feature macros at the real configure-time level. libc++ and bionic then
# disagree about which symbols exist - libc++ calls e.g.
# pthread_cond_clockwait while bionic, re-read at the lowered level, has
# hidden its declaration. MobileGL/Defines.h carried exactly that pin from
# the first commit until it was removed; this guard is what replaces it.
#
# Read the level back from the compiler target triple first. Its trailing
# number (aarch64-none-linux-android26) is precisely what clang turns into
# __ANDROID_API__, so it cannot disagree with the compile itself, and it is
# already past every NDK normalisation step - codename aliases, "latest",
# and per-ABI minimum pull-ups. ANDROID_PLATFORM_LEVEL is the fallback for
# generators/languages where the triple variable is not populated.
#
# Note CMAKE_SYSTEM_VERSION is deliberately NOT consulted: it holds the API
# level only under the NDK's newer toolchain path, and is a meaningless 1
# when ANDROID_USE_LEGACY_TOOLCHAIN_FILE is on (which is what AGP has been
# defaulting to). Reading it would fail every legacy-mode build.
set(MOBILEGL_ANDROID_API_LEVEL 26)
set(_mobilegl_android_api "")
foreach (_mobilegl_api_triple "${CMAKE_CXX_COMPILER_TARGET}"
"${CMAKE_C_COMPILER_TARGET}")
if (NOT _mobilegl_android_api AND
_mobilegl_api_triple MATCHES "-android([0-9]+)$")
set(_mobilegl_android_api "${CMAKE_MATCH_1}")
endif()
endforeach()
foreach (_mobilegl_api_var ANDROID_PLATFORM_LEVEL ANDROID_NATIVE_API_LEVEL
ANDROID_PLATFORM)
if (NOT _mobilegl_android_api AND ${_mobilegl_api_var})
string(REGEX REPLACE "^android-" ""
_mobilegl_android_api "${${_mobilegl_api_var}}")
endif()
endforeach()
if (NOT _mobilegl_android_api MATCHES "^[0-9]+$")
message(FATAL_ERROR
"MobileGL: could not determine the Android API level (got "
"\"${_mobilegl_android_api}\"). Configure with the NDK toolchain "
"file and -DANDROID_PLATFORM=android-${MOBILEGL_ANDROID_API_LEVEL}.")
elseif (_mobilegl_android_api LESS MOBILEGL_ANDROID_API_LEVEL)
message(FATAL_ERROR
"MobileGL requires at least Android API ${MOBILEGL_ANDROID_API_LEVEL}, "
"but this build resolved to API ${_mobilegl_android_api}.\n"
"Configure with -DANDROID_PLATFORM=android-${MOBILEGL_ANDROID_API_LEVEL} "
"(gradle builds get this from minSdk ${MOBILEGL_ANDROID_API_LEVEL}, so "
"check that minSdk instead of adding an override).")
elseif (_mobilegl_android_api GREATER MOBILEGL_ANDROID_API_LEVEL)
message(STATUS
"MobileGL: configuring at Android API ${_mobilegl_android_api} "
"(> shipping minimum ${MOBILEGL_ANDROID_API_LEVEL}). Allowed, but the "
"tree must not use post-${MOBILEGL_ANDROID_API_LEVEL} APIs - the "
"minSdk-${MOBILEGL_ANDROID_API_LEVEL} gradle build is the enforcing "
"compile.")
endif()
message(STATUS "MobileGL: Android API level ${_mobilegl_android_api}")
unset(_mobilegl_android_api)
unset(_mobilegl_api_var)
unset(_mobilegl_api_triple)
endif() endif()
option(MOBILEGL_ENABLE_LTO "Build with ThinLTO/IPO" OFF) option(MOBILEGL_ENABLE_LTO "Build with ThinLTO/IPO" OFF)
@@ -107,6 +202,7 @@ set(ENABLE_SPVREMAPPER OFF CACHE BOOL "Enable SPVRemapper" FORCE)
set(ENABLE_OPT ON CACHE BOOL "Enable SPIRV-Tools opt usage in glslang" FORCE) set(ENABLE_OPT ON CACHE BOOL "Enable SPIRV-Tools opt usage in glslang" FORCE)
set(BUILD_EXTERNAL ON CACHE BOOL "Build external deps in External/" FORCE) set(BUILD_EXTERNAL ON CACHE BOOL "Build external deps in External/" FORCE)
set(ENABLE_GLSLANG_INSTALL OFF CACHE BOOL "Install glslang targets" FORCE) set(ENABLE_GLSLANG_INSTALL OFF CACHE BOOL "Install glslang targets" FORCE)
set(SPIRV_SKIP_EXECUTABLES ON CACHE BOOL "Skip building SPIRV-Tools executables" FORCE)
set(SPIRV_CROSS_C_API ON CACHE BOOL "Enable C API" FORCE) set(SPIRV_CROSS_C_API ON CACHE BOOL "Enable C API" FORCE)
set(SPIRV_CROSS_ENABLE_GLSL ON CACHE BOOL "Enable GLSL backend" FORCE) set(SPIRV_CROSS_ENABLE_GLSL ON CACHE BOOL "Enable GLSL backend" FORCE)
@@ -162,6 +258,8 @@ set(SOURCE_FILES
MobileGL/MG_Util/Metrics/BufferMetrics.cpp MobileGL/MG_Util/Metrics/BufferMetrics.cpp
MobileGL/MG_Util/Metrics/PipeStats.cpp
MobileGL/MG_Util/Converters/GLToStr/GLEnumConverter.cpp MobileGL/MG_Util/Converters/GLToStr/GLEnumConverter.cpp
MobileGL/MG_Util/Converters/EGLToStr/EGLEnumConverter.cpp MobileGL/MG_Util/Converters/EGLToStr/EGLEnumConverter.cpp
MobileGL/MG_Util/Converters/MGToStr/DataTypeConverter.cpp MobileGL/MG_Util/Converters/MGToStr/DataTypeConverter.cpp
@@ -194,6 +292,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp
MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp
MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp
MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp
MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
@@ -201,18 +300,45 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameBuiltinShadowingFunctionsPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameBuiltinShadowingFunctionsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenFloat64StorageBlockPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/UniquifyIoBlockNamesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripIoBlockLocationsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DSampledImagesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeResourceArrayIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenAtomicCounterBlockPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemotePointSizePass.cpp
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.cpp
MobileGL/MG_Util/SelfTest/DriverPost.cpp MobileGL/MG_Util/SelfTest/DriverPost.cpp
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
@@ -235,6 +361,7 @@ set(SOURCE_FILES
MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp
MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp
MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp
MobileGL/MG_Impl/GLImpl/Debug/GL_Debug.cpp
MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp
MobileGL/MG_Impl/GLImpl/Texture/ProxyTexture.cpp MobileGL/MG_Impl/GLImpl/Texture/ProxyTexture.cpp
MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp
@@ -292,10 +419,13 @@ set(SOURCE_FILES
MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp
MobileGL/MG_State/GLState/TextureState/TextureObject3D.cpp MobileGL/MG_State/GLState/TextureState/TextureObject3D.cpp
MobileGL/MG_State/GLState/TextureState/TextureObjectBuffer.cpp MobileGL/MG_State/GLState/TextureState/TextureObjectBuffer.cpp
MobileGL/MG_State/GLState/TextureState/TextureObjectView.cpp
MobileGL/MG_State/GLState/TextureState/TextureUnit.cpp MobileGL/MG_State/GLState/TextureState/TextureUnit.cpp
MobileGL/MG_State/GLState/TextureState/TextureState.cpp MobileGL/MG_State/GLState/TextureState/TextureState.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramTranslationCache.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp
MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.cpp MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.cpp
@@ -310,6 +440,79 @@ set(SOURCE_FILES
MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp
) )
# ---------------------------------------------------------------------------
# MG_Remote (disaggregated transport). Everything below is gated: with the
# option OFF not one file here is compiled and no include path is added.
# ---------------------------------------------------------------------------
# FlatBuffers is a submodule and its runtime is header-only. Guard both ways:
# a checkout without the submodule must configure and build, just without the
# disaggregated shape, rather than fail with a missing-header error a hundred
# lines later. Note this only checks for the RUNTIME headers - flatc is never
# built here (see scripts/gen_protocol.py).
if (MOBILEGL_BUILD_DISAGGREGATED AND
NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/flatbuffers/include/flatbuffers/flatbuffers.h")
message(WARNING
"MOBILEGL_BUILD_DISAGGREGATED=ON but 3rdparty/flatbuffers/include is missing. "
"Run `git submodule update --init 3rdparty/flatbuffers`. Building without the "
"disaggregated shape for this configure; the cached ON takes effect once the "
"submodule is present.")
# A NORMAL variable, deliberately not `CACHE BOOL ... FORCE`: forcing OFF into the cache
# made the plain re-configure after `git submodule update` stay OFF with no message at
# all. Shadowing the cache entry for this configure only keeps the operator's ON where it
# was, so the next configure - with the submodule there - honours it.
set(MOBILEGL_BUILD_DISAGGREGATED OFF)
endif()
# MOBILEGL_PIPE_VERIFY implies MOBILEGL_PIPE_PUSH: the comparator compares the pushed block
# against a snapshot, so there has to be a pushed block. A normal variable, not a forced
# cache write, for the same reason as the disaggregated fallback above.
if (MOBILEGL_PIPE_VERIFY AND NOT MOBILEGL_PIPE_PUSH)
message(STATUS "MobileGL: MOBILEGL_PIPE_VERIFY=ON forces MOBILEGL_PIPE_PUSH ON for this configure")
set(MOBILEGL_PIPE_PUSH ON)
endif()
# In a pull build the legacy arm is the ONLY arm, so the option cannot be off there.
# A normal variable, not a forced cache write, for the same reason as the two above.
if (NOT MOBILEGL_PIPE_PUSH AND NOT MOBILEGL_PIPE_LEGACY_MEMOS)
message(STATUS "MobileGL: MOBILEGL_PIPE_PUSH=OFF forces MOBILEGL_PIPE_LEGACY_MEMOS ON for this "
"configure: with nothing pushed it is the only arm there is")
set(MOBILEGL_PIPE_LEGACY_MEMOS ON)
endif()
if (MOBILEGL_PIPE_PUSH)
message(STATUS "MobileGL: PipeInputs push ON, appending the MGPipe fill sources")
list(APPEND SOURCE_FILES
MobileGL/MG_Backend/MGPipe/PipeInputs.cpp
MobileGL/MG_Impl/Pipe/PipeFill.cpp
# P2's contract: the chunk table and its subset hash, the in-process applier, and
# the client's {slot, gen} allocator. All three are push-only, which is how the
# pull build gains no symbol from P2 (G1) - a declaration emits nothing.
MobileGL/MG_Pipe/MGPipeRenderStateSpans.cpp
MobileGL/MG_Pipe/PipeApply.cpp
MobileGL/MG_Impl/Pipe/SlotAllocator.cpp
)
endif()
if (MOBILEGL_BUILD_DISAGGREGATED)
message(STATUS "MobileGL: disaggregated transport ON, appending MG_Remote sources")
list(APPEND SOURCE_FILES
MobileGL/MG_Remote/Transport/Ring.cpp
MobileGL/MG_Remote/Transport/Doorbell.cpp
MobileGL/MG_Remote/Transport/ShmSegment.cpp
# Both platform halves are listed unconditionally and each is empty on
# the other OS, so neither can rot behind an `if (WIN32)` nobody
# configures.
MobileGL/MG_Remote/Transport/ShmSegmentPosix.cpp
MobileGL/MG_Remote/Transport/ShmSegmentWin32.cpp
MobileGL/MG_Remote/Transport/FdPassing.cpp
MobileGL/MG_Remote/Transport/InProcessTransport.cpp
# Keeps MG_Util/Debug/Log.h - and through it the GL frontend's
# umbrella header - out of the header-only wire code (WireLog.h).
MobileGL/MG_Remote/Transport/WireLog.cpp
)
endif()
if (APPLE AND NOT MOBILEGL_IOS) if (APPLE AND NOT MOBILEGL_IOS)
list(APPEND SOURCE_FILES list(APPEND SOURCE_FILES
MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp
@@ -360,11 +563,29 @@ set(MOBILEGL_COMPILE_DEF
-DASIO_NO_DEPRECATED -DASIO_NO_DEPRECATED
) )
if (MOBILEGL_BUILD_DISAGGREGATED)
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_BUILD_DISAGGREGATED=1)
endif()
if (MOBILEGL_PIPE_PUSH)
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_PUSH=1)
endif()
if (MOBILEGL_PIPE_VERIFY)
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_VERIFY=1)
endif()
if (MOBILEGL_PIPE_LEGACY_MEMOS)
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_LEGACY_MEMOS=1)
endif()
message(STATUS "MOBILEGL_COMPILE_DEF=${MOBILEGL_COMPILE_DEF}") message(STATUS "MOBILEGL_COMPILE_DEF=${MOBILEGL_COMPILE_DEF}")
set(MOBILEGL_INCLUDE_DIR set(MOBILEGL_INCLUDE_DIR
${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/MobileGL ${CMAKE_SOURCE_DIR}/MobileGL
# The MGPipe boundary headers. They are reachable as <MG_Pipe/MGPipe.h> through the
# line above too; this entry lets the client, the backends and MG_Remote spell them
# as <MGPipe.h> once MG_Pipe stops being a leaf of the frontend tree.
${CMAKE_SOURCE_DIR}/MobileGL/MG_Pipe
${spirv-tools_SOURCE_DIR} ${spirv-tools_SOURCE_DIR}
${spirv-tools_SOURCE_DIR}/include ${spirv-tools_SOURCE_DIR}/include
${spirv-tools_BINARY_DIR} ${spirv-tools_BINARY_DIR}
@@ -372,16 +593,16 @@ set(MOBILEGL_INCLUDE_DIR
# Header-only submodule: no add_subdirectory, no link target. Only # Header-only submodule: no add_subdirectory, no link target. Only
# MG_Util/Async/ShaderCompilePool.cpp includes it, and it stays behind that file's # MG_Util/Async/ShaderCompilePool.cpp includes it, and it stays behind that file's
# pimpl so no consumer target needs this path. # pimpl so no consumer target needs this path.
${CMAKE_SOURCE_DIR}/3rdparty/asio/asio/include ${CMAKE_SOURCE_DIR}/3rdparty/asio/include
# The second shader-compile execution engine (MOBILEGL_ASYNC_POOL=libfork), on the
# same terms as Asio above: header-only, no add_subdirectory (its CMakeLists only
# declares an INTERFACE target plus install/test scaffolding we do not want), no link
# target, and reachable from exactly one translation unit. libfork's own
# target_compile_features asks for cxx_std_23, which this project already sets
# globally, so its C++20 coroutines need no per-source standard override.
${CMAKE_SOURCE_DIR}/3rdparty/libfork/include
) )
if (MOBILEGL_BUILD_DISAGGREGATED)
# Header-only runtime: an include path, no add_subdirectory, no link
# target, and above all no flatc in the build graph. protocol_generated.h
# is committed and regenerated by scripts/gen_protocol.py.
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/flatbuffers/include)
endif()
add_library(${CMAKE_PROJECT_NAME} SHARED add_library(${CMAKE_PROJECT_NAME} SHARED
${SOURCE_FILES} ${SOURCE_FILES}
) )
@@ -591,3 +812,50 @@ if (NOT ANDROID)
add_subdirectory(tools/trace_replay) add_subdirectory(tools/trace_replay)
endif() endif()
endif() endif()
# The integration binary is also useful as a standalone adb-shell executable.
# Android cannot use the desktop-only MobileGL_s target, so its CMake module
# links libMobileGL.so and creates an AImageReader-backed window instead.
if (ANDROID AND MOBILEGL_BUILD_INTEGRATION_TEST)
add_subdirectory(MobileGL/MG_IntegrationTest)
endif()
# ---------------------------------------------------------------------------
# P0 spike A: the Android delivery chain for a second native executable.
#
# The disaggregated design needs a server process on Android (PLAN-B.md §8.1,
# inheriting PLAN.md §11.1-§11.6). An APK's only exec-able install location is
# lib/<abi>/, and the packager only puts a file there if it is named lib*.so -
# so a second executable has to be built with an .so name and exec'd out of
# getApplicationInfo().nativeLibraryDir. This target is the stub that proves the
# chain end to end: it is packaged like a library, exec'd from the app's own
# untrusted_app process, and writes a marker the parent reads back.
#
# Off by default and ANDROID-only, so no shipping configuration builds it. The
# trace flavour of the plugin APK turns it on (android-plugin/build.gradle).
# ---------------------------------------------------------------------------
if (ANDROID AND MOBILEGL_BUILD_SERVER_SPIKE)
add_executable(MobileGLServer
${CMAKE_CURRENT_SOURCE_DIR}/tools/spikes/server_stub/main.cpp)
# An executable that is named like a shared library still has to be a real
# PIE executable: Android has refused non-PIE executables since API 21, and
# the name alone does not change what the loader demands of the file.
set_target_properties(MobileGLServer PROPERTIES
PREFIX "lib"
SUFFIX ".so"
OUTPUT_NAME "MobileGLServer"
POSITION_INDEPENDENT_CODE ON)
target_compile_options(MobileGLServer PRIVATE -fPIE)
target_link_options(MobileGLServer PRIVATE -pie)
# AGP packages what the external native build drops into the per-ABI output
# directory, and it selects by the .so extension. CMake puts executables in
# CMAKE_RUNTIME_OUTPUT_DIRECTORY, which is not the directory AGP hands to
# CMAKE_LIBRARY_OUTPUT_DIRECTORY, so point this target's runtime output at
# the library directory when the generator gave us one.
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
set_target_properties(MobileGLServer PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
endif()
endif()
+279 -21
View File
@@ -14,7 +14,7 @@ namespace MobileGL::MG_Config {
inline const String ProjectName = "MobileGL"; inline const String ProjectName = "MobileGL";
inline const String CoreName = "MobileGL Core"; inline const String CoreName = "MobileGL Core";
inline const String CoreVendor = "MobileGL-Dev (BZLZHH, Swung0x48, Tungsten)"; inline const String CoreVendor = "MobileGL-Dev (BZLZHH, Swung0x48, Tungsten)";
inline const Version CoreVersion = {26, 8, 0, "-dev", VersionType::Development}; inline const Version CoreVersion = {26, 9, 0, "-dev", VersionType::Development};
inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true}; inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true};
inline const Uint64 CacheVersion = 0; inline const Uint64 CacheVersion = 0;
@@ -66,30 +66,105 @@ namespace MobileGL::MG_Config {
// - DISPLAY: X11 session variable, not MobileGL configuration. // - DISPLAY: X11 session variable, not MobileGL configuration.
// - MOBILEGL_LOG_FILE_PATH: log-file init runs before MG_ConfigLoader::Init // - MOBILEGL_LOG_FILE_PATH: log-file init runs before MG_ConfigLoader::Init
// (see MG_Util/Debug/Log.cpp). // (see MG_Util/Debug/Log.cpp).
// - MOBILEGL_ASYNC_POOL: a ShaderCompilePool is constructed by binaries that never call
// MobileGL::Initialize() and so never run MG_ConfigLoader::Init - MG_Test's
// JobNodeTest builds pools directly, and it is the suite that runs the whole async
// matrix against both execution engines. Mirroring it here would resolve to the
// default in exactly the tests that exist to tell the engines apart (see
// MG_Util/Async/ShaderCompilePool.cpp, DetectAsyncPoolEngine).
struct FeaturesTable { struct FeaturesTable {
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries. // MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
Bool DisableTimerQuery = false; Bool DisableTimerQuery = false;
// MOBILEGL_USE_ANGLE: load ANGLE EGL/GLES libraries. // MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW: advertise GL_ARB_texture_view on DirectGLES when
Bool UseAngle = false; // the host ES driver has EXT/OES_texture_view. Off by default: the host extension is
// present on Adreno 830 and the functional half of KHR-GL4{2,3}.texture_view still fails
// there, because the view's ES internalformat is normalized independently of the storage
// it aliases (see BackendObject_DirectGLES::BuildAdvertisedExtensions). The flag exists
// so that work can be done without editing the gate.
Bool EsprytEnableTextureView = false;
// MOBILEGL_ENABLE_SPIRV_VALIDATION: validate generated and transformed SPIR-V.
// Disabled by default because validation is a diagnostics-only cost.
Bool EnableSpirvValidation = false;
// MOBILEGL_ESPRYT_USE_ANGLE: load ANGLE EGL/GLES libraries.
Bool EsprytUseAngle = false;
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS) #if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
// MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash. // MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash.
String TraceAngleVariant; String TraceAngleVariant;
#endif #endif
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support. // MOBILEGL_MAGMA_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support,
Bool DisableSubgroup = false; // including the opt-in emulated compute path below.
Bool MagmaDisableSubgroup = false;
// MOBILEGL_MAGMA_EMULATE_SUBGROUP: implement GL_KHR_shader_subgroup's compute
// stage on a 32-lane VIRTUAL subgroup lowered to workgroup-shared memory
// (ShaderTranspiler::EmulateSubgroupsPass). Strictly a last resort: it only ever
// engages when this flag is set AND the device has no native subgroup support at
// all - a device with real subgroup operations always uses them natively,
// whatever their width (the known iterationRP defect is patched by
// MagmaFixIterationRPSubgroupScratch below instead). Off by default.
Bool MagmaEmulateSubgroup = false;
// MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: patch iterationRP's own bug - the
// pack declares `shared vec2 prefixSumCache[32]` for a 512-invocation exposure
// reduction and indexes it by gl_SubgroupID, so any device with sub-16-lane
// subgroups (8-lane lavapipe -> 64 subgroups) writes shared memory out of
// bounds. The pass grows that one array to what the device's topology needs and
// touches nothing else; it only rewrites modules positively matching the pack's
// reduction fingerprint (ShaderTranspiler::FixIterationRPSubgroupScratchPass),
// so every other shader passes through byte-identical - as does iterationRP
// itself on >= 16-lane devices. Auto is ON; ForceOff replays the pack's bug
// verbatim.
QuirkOverride MagmaFixIterationRPSubgroupScratch = QuirkOverride::Auto;
// MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup
// rendezvous between its two reductions over prefixSumCache. Off by default and
// fingerprint-gated by FixIterationRPBarrierPass when enabled.
Bool MagmaIterationRPFixBarrier = false;
// MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
// ceil(workgroup invocations / gl_SubgroupSize) on the NATIVE subgroup path
// (ShaderTranspiler::DeriveNumSubgroupsPass). Auto is ON: GL requires
// gl_SubgroupID < gl_NumSubgroups, Adreno's builtin reports 1 while the same
// dispatch emits IDs 0..7, and the derived value is the one Vulkan guarantees
// whenever the pipeline can request REQUIRE_FULL_SUBGROUPS (which the renderer
// does whenever local_size_x is a multiple of the native width). ForceOff returns
// to the raw driver builtin.
QuirkOverride MagmaDeriveNumSubgroups = QuirkOverride::Auto;
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
// promises 64-bit precision, and that is the one thing the narrowing cannot deliver.
// Off by default so an application that checks the string before using doubles keeps
// its float path; on for measuring what the conformance suite makes of the demoted
// precision. See the DemoteFloat64Pass header and the "fp64" POST row.
Bool AdvertiseFp64 = false;
// MOBILEGL_MAGMA_R11G11B10F_FALLBACK: use fallback format for R11G11B10F on Vulkan. // MOBILEGL_MAGMA_R11G11B10F_FALLBACK: use fallback format for R11G11B10F on Vulkan.
Bool MagmaR11G11B10FFallback = false; Bool MagmaR11G11B10FFallback = false;
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3. // MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
Uint32 MagmaFramesInFlight = 3; Uint32 MagmaFramesInFlight = 3;
// MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers, // MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
// resolves certain rendering bugs on ANGLE + llvmpipe. // resolves certain rendering bugs on ANGLE + llvmpipe.
Bool AvoidSamplerMipmapMinFilter = false; Bool EsprytAvoidSamplerMipmapMinFilter = false;
// MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS: leave an already-explicit LOD argument alone when
// emulating GL_TEXTURE_LOD_BIAS, instead of adding the bias uniform to it. Injecting
// the uniform turns a compile-time-constant LOD into a runtime expression, which
// sends ANGLE + llvmpipe down a mip-selection path that dereferences a NULL
// descriptor and kills the process. Deviates from spec (Vulkan adds the bias to
// OpImageSampleExplicitLod), so it is an avoidance for that stack only.
Bool EsprytAvoidExplicitLodBias = false;
// MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS: emit a tessellation/geometry program's
// inter-stage interface blocks WITHOUT their layout(location=) qualifier, letting ES
// match them by block name and member sequence instead. The Mali ES driver delivers
// nothing at all through a located block once a tessellation or geometry stage is in
// the pipeline; the driver POST measures that and turns this on by itself, so Auto is
// the right setting everywhere. ForceOn exists so the emulation can be exercised on a
// healthy driver - which is what the integration lane does, since llvmpipe and
// lavapipe carry a located block correctly and would otherwise never run this code -
// and ForceOff is the negative control. See StripIoBlockLocationsPass.
QuirkOverride EsprytUnlocatedIoBlocks = QuirkOverride::Auto;
// MOBILEGL_POINT_SIZE_DEMOTION: demote gl_PointSize out of tessellation/geometry
// stages into an ordinary varying (ShaderCompiler::
// DemoteTessellationGeometryPointSizeForProgram) instead of declining such programs
// on a device that advertises neither EXT/OES_tessellation_point_size /
// geometry_point_size (DirectGLES) nor shaderTessellationAndGeometryPointSize
// (DirectVulkan). Auto arms it exactly where the detection says the capability is
// absent, which is the right setting everywhere. ForceOn exists so the demotion can
// be exercised on a healthy driver - llvmpipe and lavapipe host the built-in
// natively and would otherwise never run this code, which is what the pinned
// integration lane uses - and ForceOff restores the plain declines (escape hatch /
// negative control). Cross-backend by design: the demotion runs in the shared
// phase-B chain, so one switch covers both. See DemotePointSizePass.
QuirkOverride PointSizeDemotion = QuirkOverride::Auto;
// MOBILEGL_COHERENT_AS_FLUSH: app-compat for engines (e.g. Flywheel) that write // MOBILEGL_COHERENT_AS_FLUSH: app-compat for engines (e.g. Flywheel) that write
// GPU-read data through persistent GL_MAP_FLUSH_EXPLICIT_BIT maps they never // GPU-read data through persistent GL_MAP_FLUSH_EXPLICIT_BIT maps they never
// flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent // flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent
@@ -99,20 +174,51 @@ namespace MobileGL::MG_Config {
Bool CoherentAsFlush = false; Bool CoherentAsFlush = false;
// MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp). // MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp).
Bool TraceSkipAutodestroy = false; Bool TraceSkipAutodestroy = false;
// MOBILEGL_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the // MOBILEGL_ESPRYT_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator // per-draw glBufferSubData path instead of the persistent-mapped ring allocator
// (negative control / driver-bug escape hatch). // (negative control / driver-bug escape hatch).
Bool DisableUboRing = false; Bool EsprytDisableUboRing = false;
// MOBILEGL_ESPRYT_DISABLE_UNPACK_RING: force DirectGLES texture uploads back to
// glTexSubImage from the client pointer instead of staging them through the
// persistent-mapped unpack-PBO ring (negative control / driver-bug escape
// hatch).
Bool EsprytDisableUnpackRing = false;
// MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING: force DirectGLES app buffer updates
// (glBufferSubData / map flushes) back to the immediate driver upload instead
// of queueing them for the staged-copy flush through the persistent-mapped
// upload ring (negative control / driver-bug escape hatch; the immediate
// upload stalls on drivers that resolve the WAR hazard on the CPU, e.g. Mali).
Bool EsprytDisableUploadRing = false;
// MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH: skip the glMapBufferRange(WRITE |
// INVALIDATE_RANGE) tier of the DirectGLES pending-range flush and go straight
// to the upload ring's staged glCopyBufferSubData (negative control / escape
// hatch for a driver whose range-invalidating map misbehaves). The map tier is
// what keeps a partial write into a large in-flight buffer priced by the RANGE:
// on Mali both the immediate glBufferSubData and a staged copy into a busy
// mutable store ghost the whole destination on the CPU.
Bool EsprytDisableInvalidateFlush = false;
// MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION: keep mesh-arena-sized buffer stores
// (>= 16MiB) on the CPU-shadow model instead of backing them with the backend's
// persistently+coherently mapped storage at definition time (negative control /
// escape hatch). Frontend-scoped: it engages only where the active backend
// provides AcquirePersistentMap. With adoption on, an app SubData into a busy
// 128MB arena is a plain memcpy into GPU-visible memory; every driver-mediated
// route for the same write stalls the thread or ghost-copies the whole arena on
// this class of Mali driver, and the arena stops costing its size again in RAM.
Bool DisableLargeBufferAdoption = false;
// MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
// depth/stencil reads and always go through the shader-sampling emulation. Core GL
// ES has no depth or stencil readback, but some drivers accept it anyway (Mesa does,
// Adreno does not), which means the emulation is dead code on exactly the stack the
// headless suite runs on. This forces it live so the scenarios and the CTS can
// exercise the path, and gives the device an A/B lever over the same choice.
Bool EsprytForceDepthStencilReadbackEmulation = false;
// MOBILEGL_RELAXED_SEMANTICS: relax strict core-profile rules (e.g. VAO-0 draws, // MOBILEGL_RELAXED_SEMANTICS: relax strict core-profile rules (e.g. VAO-0 draws,
// texture-name reuse after delete) even on contexts that explicitly requested a core // texture-name reuse after delete) even on contexts that explicitly requested a core
// profile. Without it, relaxed semantics still apply to every context that did not // profile. Without it, relaxed semantics still apply to every context that did not
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1 // explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
// version request. // version request.
Bool RelaxedSemantics = false; Bool RelaxedSemantics = false;
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN: overrides the shader-source quirk that
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that // MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive // strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
// ONE+ONE - the multi-pass depth-equality signature) on drivers without // ONE+ONE - the multi-pass depth-equality signature) on drivers without
@@ -120,10 +226,10 @@ namespace MobileGL::MG_Config {
// gl_FragDepth writers, and fully color-masked attachments are exempt (see // gl_FragDepth writers, and fully color-masked attachments are exempt (see
// PipelineFactory::ShouldSuppressDepthWrite). Auto detects Qualcomm. // PipelineFactory::ShouldSuppressDepthWrite). Auto detects Qualcomm.
QuirkOverride MagmaDisableBlendedDepthWriteQuirk = QuirkOverride::Auto; QuirkOverride MagmaDisableBlendedDepthWriteQuirk = QuirkOverride::Auto;
// MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device // MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
// feature off. It is enabled by default to match GL's defined out-of-range fetch // feature off. It is enabled by default to match GL's defined out-of-range fetch
// behavior; this escape hatch exists to measure or dodge its GPU cost on a device. // behavior; this escape hatch exists to measure or dodge its GPU cost on a device.
Bool DisableRobustBufferAccess = false; Bool MagmaDisableRobustBufferAccess = false;
// MOBILEGL_MAGMA_MULTIDRAW_MODE: preferred DirectVulkan multi-draw dispatch tier // MOBILEGL_MAGMA_MULTIDRAW_MODE: preferred DirectVulkan multi-draw dispatch tier
// ("ext" | "indirect" | "unroll", see MultiDrawMode). Clamped to device support; // ("ext" | "indirect" | "unroll", see MultiDrawMode). Clamped to device support;
// unset picks the best supported tier. // unset picks the best supported tier.
@@ -142,6 +248,158 @@ namespace MobileGL::MG_Config {
// MOBILEGL_ASYNC_SHADER_COMPILE_THREADS: shader-compile worker count. 0 (unset) means // MOBILEGL_ASYNC_SHADER_COMPILE_THREADS: shader-compile worker count. 0 (unset) means
// auto, which is min(4, big cores); an explicit value is honoured as given. // auto, which is min(4, big cores); an explicit value is honoured as given.
Uint32 AsyncShaderCompileThreads = 0; Uint32 AsyncShaderCompileThreads = 0;
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS: while a compile job is still in flight,
// glGetShaderiv(GL_COMPILE_STATUS) answers GL_TRUE and the shader info log reads
// empty, WITHOUT joining the job (latched per compile - see
// ShaderObject::TakeOptimisticCompileAnswer). A deliberate, bounded spec violation:
// a real failure still fails the program link with the compile log quoted. It
// exists for applications that compile hundreds of shaders serially and read the
// status right after each glCompileShader - Iris's shader-pack load - where those
// per-shader joins are what serializes the batch on its main path (Iris's gbuffer
// phase issues no program-level query between programs; program-level LINK_STATUS
// and the program info log still join truthfully, so paths that check each link
// immediately stay serial by their own construction). Off by default; never
// advertise it.
QuirkOverride AsyncOptimisticShaderStatus = QuirkOverride::Auto;
// MOBILEGL_SHADER_CACHE: the three-level, in-memory shader translation memo
// (MG_Util/ShaderTranspiler/TranslationCache.h). The levels follow the GL
// entry points - L1c memoizes one glCompileShader's PARSE VERDICT, L1 a
// linked program's whole front end, L2 DirectGLES's emitted ESSL. Auto is
// ON; ForceOff turns ALL THREE off and makes every translation run from
// scratch. The escape hatch exists because a wrong cache hit is a silently
// miscompiled shader: if a device ever renders differently with the cache
// on, one run with this falsy says so.
QuirkOverride ShaderTranslationCache = QuirkOverride::Auto;
// MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION: DirectGLES' gl_ViewportIndex routing
// emulation - the builtin becomes a flat varying, the fragment stage gets a
// per-pass gate, and a routed draw is REPLAYED once per distinct viewport state
// with the real glViewport/glScissor/glDepthRangef set for it. Auto is ON, and
// it is ON even where the driver advertises GL_OES_viewport_array, because that
// extension only ever gave the SHADER a compilable name: MobileGL has never
// programmed a driver's INDEXED viewport state (SyncRenderState pushes index 0
// and nothing else), so on an extension-capable driver every index rasterized as
// index 0 exactly as it did without one. ForceOff returns to that behaviour -
// the pre-emulation path, extension passthrough where it exists and
// LowerViewportIndexPass' demote-to-a-plain-global where it does not - and is
// the negative control the emulation is measured against.
QuirkOverride EsprytViewportArrayEmulation = QuirkOverride::Auto;
// MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE: DirectGLES stores GL_RGB565/GL_RGB5(A1)/GL_RGBA4
// images as 8-bit-per-channel ES storage (GL_RGB8/GL_RGBA8) instead of the driver's
// native 16-bit packed formats. Auto defers to a POST driver-bug probe
// (SelfTest::CopyImageMirrorsPacked16FieldOrder): some Mali drivers store SOME
// packed16 allocations with a MIRRORED field order (allocation-scoped and
// shape/context dependent - the failing 30x30x12 GL_TEXTURE_2D_ARRAYs are mirrored
// at every level), so glCopyImageSubData - a raw texel-block move - lands R/G/B/A
// reversed whenever exactly one endpoint sits in a mirrored allocation
// (KHR-GL4x.copy_image.functional rgb5/rgb5_a1/rgba4 x every *2d_array* pair).
// With no 16-bit packed ES image left there is no field order to disagree about; the
// client word still round-trips exactly, because the canonical shadow is already
// UNorm8 and an n-bit field encodes to UNorm8 and back losslessly for n <= 8.
// ForceOn widens on any driver (the llvmpipe suites use it to exercise the widened
// path); ForceOff keeps the native narrow storage even where the probe fires - the
// negative control that replays the corruption. Costs 2x the memory of the affected
// formats where it engages, which is why Auto is probe-gated rather than always-on.
QuirkOverride EsprytWidenPacked16Storage = QuirkOverride::Auto;
// MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE: DirectVulkan's GL_PRIMITIVES_GENERATED
// reroute for draws made while transform feedback is INACTIVE. The stream query
// (VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT primitivesNeeded) is defined to count
// them, but a Mali driver - and Mesa lavapipe - answers 0 unless a capture span is
// open, which is exactly the shape the CTS uses to measure the tessellator, so ~29
// tessellation tests per tree size a capture buffer from the 0 and die on the
// zero-length map. Auto defers to a device probe at renderer bring-up
// (SelfTest::RunPrimitivesGeneratedNoXfbProbe), which measures two substitutes on
// the same capture-less draws and arms the best proven one: the dedicated
// VK_EXT_primitives_generated_query (exact semantics by definition; lavapipe passes
// it, rasterizer discard included), else a clipping-invocations pipeline-statistics
// pool (see the verdict vocabulary for its rasterizer-discard split). ForceOn pins
// the reroute structurally wherever a pool can exist (the arming-observable lane,
// immune to the probe's verdict moving), and ForceOff is the negative control that
// replays the driver's silence.
QuirkOverride MagmaPrimGenQueryReroute = QuirkOverride::Auto;
// --- MGPipe (the disaggregation plan's explicit frontend/backend boundary) ---
// MOBILEGL_PIPE_PUSH: per-subsystem bitmask selecting which state the frontend
// PUSHES over MGPipe instead of leaving the backend to pull it out of GLContext.
// 0 - the only shipped value until the migration lands - is "pull everything",
// i.e. exactly today's behaviour, and is the default of a PULL build, where the
// knob is meaningless anyway. A PUSH build defaults to every subsystem migrated so
// far (MG_Pipe::kMGPipeSubsystemsMigratedAtP3a), so MOBILEGL_PIPE_PUSH=0 in the
// environment is the all-pull control and 0x7f (kMGPipeSubsystemsMigratedAtP2) is
// the "P2 only" control P3a's A/B is run against. Accepts decimal or 0x-prefixed
// hex, and operators pass it as hex, so the bits are listed here (MG_Pipe/MGPipe.h
// owns them):
// 0x01 render state (create/bind_render_state + set_dynamic_state)
// 0x02 pixel pack 0x04 patch state 0x08 vertex attrib defaults
// 0x10 residual values 0x20 Espryt slots 0x40 Magma vertex input
// 0x80 resources (the resource_* family: the seven BufferBackendOps hooks)
// 0x100 vertex input (vertex elements / vertex buffers / index buffer)
// 1<<63 NOT a subsystem, a BEHAVIOUR: turn OFF client-side content addressing of
// CSOs, so every pipeline-version change mints a fresh CSO and the map is
// never probed. The negative control the CSO design is measured against.
Uint64 PipePush = 0;
// MOBILEGL_PIPE_VERIFY: per-draw, per-FIELD shadow comparison of the pushed state
// against a snapshot taken from GLContext the old way, printing the first field
// that differs and the draw serial. Roughly 5-10x slower and never shipped; it is
// the semantic gate that replaces byte identity, and it catches the dangerous
// direction - a dirty bit that fires too RARELY - which no purity gate can see.
Bool PipeVerify = false;
#if MOBILEGL_PIPE_PUSH
// The three knobs of the MOBILEGL_PIPE_VERIFY build (P1 brief D2). Compiled only
// under MOBILEGL_PIPE_PUSH so the pull build's FeaturesTable does not change size.
// MOBILEGL_PIPE_VERIFY_FATAL: the first divergence aborts (default). 0 logs and
// counts instead, for triage and for the lane that must survive to read its own
// log. Tri-state parse like PipeLegacyMemos: only an explicit falsy value turns it
// off.
Bool PipeVerifyFatal = true;
// MOBILEGL_PIPE_VERIFY_CORRUPT: a field name from kMGPipeInputFieldNames[]; the
// comparator perturbs that field in the SNAPSHOT arm before the entry compare, so a
// green verify run goes red naming it (negative control A). Unknown name is
// Fatal{PipeVerifyBadKnob}.
String PipeVerifyCorrupt;
// MOBILEGL_PIPE_POISON_OMIT: <Verb>:<FieldName>; the filler skips the STAMP (not
// the value) of that field for that verb, an omission indistinguishable from a
// forgotten FillPoints.def row, so that verb's read of it is
// Fatal{UnmigratedPipeInput} (negative control B). Unknown name is
// Fatal{PipeVerifyBadKnob}.
String PipePoisonOmit;
// MOBILEGL_PIPE_HANDLE_ABA_CONTROL (negative control C, P2 brief D18): replace the
// OBJECT IDENTITY in every DirectVulkan vertex-input memo key with a constant, on
// whichever arm the run is on - the pre-handle (address, lifetime id) pair AND the
// handle arm's {slot, gen} generation - so a replacement object inherits its dead
// predecessor's resolved vertex bindings and HandleRecycleScenario.AbaControl asserts
// the WRONG pixels. That is what proves the reproducer still reproduces. D18 wrote
// this as "hash the raw BufferObject* instead of its lifetime id"; measured, the heap
// block is never handed back, so that spelling collided with nothing and the control
// went vacuous - see MagmaPipeArms.h's MagmaPipeAbaControlDefeatsIdentity for the
// measurement and for what the control still leaves standing. Under
// MOBILEGL_PIPE_PUSH only, so it cannot exist in a shipping pull build.
Bool PipeHandleAbaControl = false;
#endif
// MOBILEGL_PIPE_STATS: dump the boundary counters (bytes, calls, roundtrips,
// texture pulls, upload shapes, residual-block bytes, index mirror bytes).
Bool PipeStats = false;
// MOBILEGL_PIPE_LEGACY_MEMOS: keep the pre-handle registries and TwinLookupMemos
// alive so the first handle waves have a real old-versus-new arm to be compared
// against. ON by default for the whole migration window, deleted with the pull
// path itself.
Bool PipeLegacyMemos = true;
// MOBILEGL_PIPE_TEXEL_RETAIN_MB: LRU budget for texels retained against a
// server-initiated texture re-send. Default 0, i.e. OFF: MipmapStorage already
// holds a complete CPU shadow, so this cache buys latency, never correctness.
Uint32 PipeTexelRetainMb = 0;
// MOBILEGL_PIPE_INDEX_MIRROR_MB: budget for the server-side index host mirror,
// which is what lets primitive-restart rewriting and multi-draw flattening stay on
// the server without shipping index bytes per draw. Over budget it degrades to
// per-draw staging, counted separately in the stats.
Uint32 PipeIndexMirrorMb = 64;
// MOBILEGL_PIPE_STATS_PERIOD: frames per boundary-counter summary line. 120 is the
// steady-state cadence; the device retrace harness never reaches the teardown dump
// and a trimmed fixture (create-indirect) is shorter than 120 frames, so a run that
// needs its numbers at all sets this low enough to land at least one window.
Uint32 PipeStatsPeriod = 120;
// MOBILEGL_PIPE_STATS_FILE: where the boundary counters' teardown JSON dump goes.
// Empty (the default) means no dump; the per-120-frame summary line still goes to
// the log whenever PipeStats is on, so a device run needs no writable path.
String PipeStatsFile;
}; };
extern FeaturesTable Features; extern FeaturesTable Features;
} // namespace MobileGL::MG_Config } // namespace MobileGL::MG_Config
+103 -7
View File
@@ -7,6 +7,12 @@
// End of Source File Header // End of Source File Header
#include "Config.h" #include "Config.h"
#if MOBILEGL_PIPE_PUSH
// For kMGPipeSubsystemsMigratedAtP3a, the push build's PipePush default (the P2 constant
// beside it is the phase-by-phase control, not the default). Push-only, so the pull
// build's translation unit is unchanged.
#include <MG_Pipe/MGPipe.h>
#endif
#include <cerrno> #include <cerrno>
#include <cstdlib> #include <cstdlib>
@@ -159,30 +165,120 @@ namespace MobileGL::MG_ConfigLoader {
return static_cast<Uint32>(parsedValue); return static_cast<Uint32>(parsedValue);
} }
// Same contract as QueryEnvUint32, over 64 bits and accepting an explicit 0x prefix: the
// one consumer is a subsystem BITMASK, and a bitmask written in decimal is unreadable.
// Decimal otherwise - never strtoull's base 0, whose "leading zero means octal" rule
// silently read MOBILEGL_PIPE_PUSH=010 as 8 - and a '-' anywhere is rejected rather than
// wrapped, which strtoull would otherwise do without complaint (-1 -> every bit set).
inline Uint64 QueryEnvUint64(const String& key, Uint64 defaultValue) {
auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) {
return defaultValue;
}
const String& value = it->second;
const char* text = value.c_str();
int base = 10;
if (value.size() > 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) {
text += 2;
base = 16;
}
char* parseEnd = nullptr;
errno = 0;
const bool negative = value.find('-') != String::npos;
const unsigned long long parsedValue = negative ? 0 : std::strtoull(text, &parseEnd, base);
if (negative || parseEnd == text || *parseEnd != '\0' || errno == ERANGE) {
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected a non-negative integer "
"(decimal, or 0x-prefixed hexadecimal), using default %llu",
key.c_str(), value.c_str(), static_cast<unsigned long long>(defaultValue));
return defaultValue;
}
return static_cast<Uint64>(parsedValue);
}
inline void InitFeatures() { inline void InitFeatures() {
auto& features = MG_Config::Features; auto& features = MG_Config::Features;
features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY"); features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
features.UseAngle = QueryEnvFlag("MOBILEGL_USE_ANGLE"); features.EsprytEnableTextureView = QueryEnvFlag("MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW");
features.EnableSpirvValidation = QueryEnvFlag("MOBILEGL_ENABLE_SPIRV_VALIDATION");
features.EsprytUseAngle = QueryEnvFlag("MOBILEGL_ESPRYT_USE_ANGLE");
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS) #if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, ""); QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
#endif #endif
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP"); features.MagmaDisableSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_SUBGROUP");
features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
features.MagmaFixIterationRPSubgroupScratch =
QueryEnvQuirkOverride("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
features.MagmaIterationRPFixBarrier = QueryEnvFlag("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER");
features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK"); features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64); features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
features.AvoidSamplerMipmapMinFilter = features.EsprytAvoidSamplerMipmapMinFilter =
QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER"); QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
features.EsprytUnlocatedIoBlocks = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS");
features.PointSizeDemotion = QueryEnvQuirkOverride("MOBILEGL_POINT_SIZE_DEMOTION");
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH"); features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY"); features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING"); features.EsprytDisableUboRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UBO_RING");
features.EsprytDisableUnpackRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UNPACK_RING");
features.EsprytDisableUploadRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING");
features.EsprytDisableInvalidateFlush = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH");
features.DisableLargeBufferAdoption = QueryEnvFlag("MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION");
features.EsprytForceDepthStencilReadbackEmulation =
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS"); features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
features.MagmaDisableBlendedDepthWriteQuirk = features.MagmaDisableBlendedDepthWriteQuirk =
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE"); QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS"); features.MagmaDisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS");
features.MagmaMultiDrawMode = QueryEnvMultiDrawMode("MOBILEGL_MAGMA_MULTIDRAW_MODE"); features.MagmaMultiDrawMode = QueryEnvMultiDrawMode("MOBILEGL_MAGMA_MULTIDRAW_MODE");
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE"); features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE"); features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64); features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
features.AsyncOptimisticShaderStatus =
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE");
features.EsprytViewportArrayEmulation =
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION");
features.EsprytWidenPacked16Storage =
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE");
features.MagmaPrimGenQueryReroute = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE");
// MGPipe. Nothing here needs adding to an allow-list: InitializeAcceptedEnvVariables
// accepts every MOBILEGL_ / LIBGL_ prefixed variable in the environment, so a name
// that starts with MOBILEGL_ is visible to these queries by construction.
#if MOBILEGL_PIPE_PUSH
// A push build with the knob unset runs every subsystem migrated so far, so the
// shipped path is the one the gates measure; MOBILEGL_PIPE_PUSH=0 in the
// environment is the all-subsystems-pull control that reproduces P1 exactly, and
// kMGPipeSubsystemsMigratedAtP2 (0x7f) is the phase-by-phase control - P3a's two
// subsystems off, everything P2 landed still on.
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP3a);
#else
// Meaningless in a pull build: there is nothing to push. Config.h documents 0 as
// "pull everything" and that stays literally true.
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", 0);
#endif
features.PipeVerify = QueryEnvFlag("MOBILEGL_PIPE_VERIFY");
#if MOBILEGL_PIPE_PUSH
// Defaults ON: read as a tri-state so only an explicitly falsy value turns it off.
features.PipeVerifyFatal =
QueryEnvQuirkOverride("MOBILEGL_PIPE_VERIFY_FATAL") != MG_Config::QuirkOverride::ForceOff;
QueryEnvVariable("MOBILEGL_PIPE_VERIFY_CORRUPT", features.PipeVerifyCorrupt, "");
QueryEnvVariable("MOBILEGL_PIPE_POISON_OMIT", features.PipePoisonOmit, "");
features.PipeHandleAbaControl = QueryEnvFlag("MOBILEGL_PIPE_HANDLE_ABA_CONTROL");
#endif
features.PipeStats = QueryEnvFlag("MOBILEGL_PIPE_STATS");
// Defaults ON, so the flag has to be read as a tri-state rather than as a plain
// truthy check: unset must keep the memos, and only an explicitly falsy value may
// drop them.
features.PipeLegacyMemos =
QueryEnvQuirkOverride("MOBILEGL_PIPE_LEGACY_MEMOS") != MG_Config::QuirkOverride::ForceOff;
features.PipeTexelRetainMb = QueryEnvUint32("MOBILEGL_PIPE_TEXEL_RETAIN_MB", 0, 0, 4096);
features.PipeIndexMirrorMb = QueryEnvUint32("MOBILEGL_PIPE_INDEX_MIRROR_MB", 64, 0, 4096);
features.PipeStatsPeriod = QueryEnvUint32("MOBILEGL_PIPE_STATS_PERIOD", 120, 1, 1000000);
QueryEnvVariable("MOBILEGL_PIPE_STATS_FILE", features.PipeStatsFile, "");
} }
inline void InitBackendType() { inline void InitBackendType() {
+37 -4
View File
@@ -9,10 +9,20 @@
#pragma once #pragma once
// ============== Platform-specific definitions and macros ============== // // ============== Platform-specific definitions and macros ============== //
#ifdef __ANDROID__ // No __ANDROID_API__ pin here on purpose. The effective API level is owned by
#undef __ANDROID_API__ // the build system (gradle minSdk 26 -> -DANDROID_PLATFORM=android-26, enforced
#define __ANDROID_API__ 26 // force Android API level to 26 for compatibility // by the configure-time guard in CMakeLists.txt), not by a macro.
#endif //
// History: this used to `#define __ANDROID_API__ 26` to *raise* the level back
// when the build configured something lower, so that pthread_getname_np (which
// bionic guards with __INTRODUCED_IN(26)) would be declared. Once a later
// change added an `#undef` in front of it, the same line started *lowering* the
// level whenever the build configured higher than 26 - and that is an
// include-order split-brain, not a compatibility knob: a TU that includes any
// libc++ header before Includes.h latches libc++'s feature macros at the
// configure-time level, and only the bionic headers pulled in afterwards see
// the lowered value. The two halves then disagree (e.g. libc++ believes
// pthread_cond_clockwait exists while bionic has since hidden its declaration).
#ifdef _WIN32 #ifdef _WIN32
#ifndef NOMINMAX #ifndef NOMINMAX
@@ -37,6 +47,23 @@
#define MOBILEGL_WGL_API MOBILEGL_API #define MOBILEGL_WGL_API MOBILEGL_API
// ====================== MobileGL configurations ======================= // // ====================== MobileGL configurations ======================= //
// The numeric log levels live here, not only in Log.h: MOBILEGL_ASSERT below compares
// MOBILEGL_LOG_ACTIVE_LEVEL against MOBILEGL_LOG_LEVEL_DEBUG, and in a translation unit
// that includes Defines.h without Log.h both tokens would silently evaluate to 0 in the
// preprocessor conditional - enabling the assert in exactly the INFO-level builds it is
// documented to be compiled out of. Log.h redefines them identically, which is legal.
//
// Severity order, ascending: DEBUG < INFO < WARN < ERROR < FATAL. MOBILEGL_LOG_ACTIVE_LEVEL
// names the lowest severity compiled in, so the production default INFO keeps I/W/E/F and
// drops only D. Any edit here must be mirrored in Log.h.
#ifndef MOBILEGL_LOG_LEVEL_DEBUG
#define MOBILEGL_LOG_LEVEL_DEBUG 0
#define MOBILEGL_LOG_LEVEL_INFO 1
#define MOBILEGL_LOG_LEVEL_WARN 2
#define MOBILEGL_LOG_LEVEL_ERROR 3
#define MOBILEGL_LOG_LEVEL_FATAL 4
#endif
#ifndef MOBILEGL_LOG_ACTIVE_LEVEL #ifndef MOBILEGL_LOG_ACTIVE_LEVEL
#define MOBILEGL_LOG_ACTIVE_LEVEL MOBILEGL_LOG_LEVEL_INFO #define MOBILEGL_LOG_ACTIVE_LEVEL MOBILEGL_LOG_LEVEL_INFO
#endif #endif
@@ -68,6 +95,12 @@
#endif #endif
// =============================== Utils ================================ // // =============================== Utils ================================ //
// Asserts are live in exactly the builds where MGLOG_D is live, i.e. DEBUG builds only;
// an INFO build (the production default) compiles them out. DEBUG is the lowest severity
// in the ordering above, so "ACTIVE <= DEBUG" is true only for ACTIVE == DEBUG - the same
// gate MGLOG_D uses in Log.h. That equivalence is what makes this gate survive the
// 2026-08-13 renumbering unchanged; the contract is and stays
// "INFO builds: asserts OFF; DEBUG builds: asserts ON".
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
#define MOBILEGL_ASSERT(condition, ...) \ #define MOBILEGL_ASSERT(condition, ...) \
do { \ do { \
+2 -2
View File
@@ -49,8 +49,8 @@
#include <stacktrace> #include <stacktrace>
#endif #endif
// Include FastSTL // Include ska::flat_hash_map
#include <FastSTL/UnorderedMap.h> #include <ska/flat_hash_map.hpp>
// Include xxHash // Include xxHash
#include <xxhash.h> #include <xxhash.h>
+26
View File
@@ -15,8 +15,12 @@
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h> #include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h> #include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <MG_Impl/GLImpl/Sync/GL_Sync.h> #include <MG_Impl/GLImpl/Sync/GL_Sync.h>
#include <MG_Impl/GLImpl/Query/GL_Query.h>
#include <MG_Util/Async/ShaderCompilePool.h> #include <MG_Util/Async/ShaderCompilePool.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h> #include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
#include <atomic> #include <atomic>
#include <mutex> #include <mutex>
@@ -39,6 +43,11 @@ namespace MobileGL {
if (logLifecycle) { if (logLifecycle) {
MGLOG_I("MobileGL closing..."); MGLOG_I("MobileGL closing...");
} }
// Before any subsystem the counters name goes away, and before the last frame's
// numbers can be lost: emits the final summary line and, when
// MOBILEGL_PIPE_STATS_FILE is set, the JSON dump. A no-op when the counters are
// off, and idempotent.
MG_Util::PipeStats::Shutdown();
// First, before anything else is torn down. In-flight compile/link jobs own // First, before anything else is torn down. In-flight compile/link jobs own
// their own inputs and are safe against everything below EXCEPT glslang's // their own inputs and are safe against everything below EXCEPT glslang's
// process globals and the TShader/TProgram objects hanging off pGLContext, // process globals and the TShader/TProgram objects hanging off pGLContext,
@@ -51,6 +60,11 @@ namespace MobileGL {
// before a re-initialized library could pair them with the wrong // before a re-initialized library could pair them with the wrong
// backend's DeleteSync). // backend's DeleteSync).
MG_Impl::GLImpl::DestroyAllSyncObjects(); MG_Impl::GLImpl::DestroyAllSyncObjects();
// Queries die with their contexts for the same reason, and their registry
// is the same shape of process-global map: drain it here too, while the
// function table can still pair each backend handle with the backend that
// minted it.
MG_Impl::GLImpl::DestroyAllQueryObjects();
MG_Backend::pActiveBackendObject.reset(); MG_Backend::pActiveBackendObject.reset();
MG_State::pGLContext.reset(); MG_State::pGLContext.reset();
MG_State::pEGLContext.reset(); MG_State::pEGLContext.reset();
@@ -66,6 +80,14 @@ namespace MobileGL {
// built-in symbol tables the prewarm latch stands for, so leaving it set would // built-in symbol tables the prewarm latch stands for, so leaving it set would
// make the next Initialize() skip a prewarm it genuinely needs. // make the next Initialize() skip a prewarm it genuinely needs.
MG_Util::ShaderTranspiler::ShaderCompiler::ResetPrewarmLatch(); MG_Util::ShaderTranspiler::ShaderCompiler::ResetPrewarmLatch();
// The two-level translation memo. Nothing in it references a glslang object -
// both levels hold plain bytes - so this is RSS hygiene rather than a lifetime
// requirement, and it is safe either side of FinalizeProcess. Stats first: an
// fordebug build gets one line per level saying how the run went.
MG_Util::ShaderTranspiler::LogShaderTranslationCacheStats();
MG_Util::ShaderTranspiler::ClearShaderTranslationCaches();
MG_State::GLState::LogProgramTranslationCacheStats();
MG_State::GLState::ClearProgramTranslationCache();
MG_Backend::gBackendFunctionsTable = {}; MG_Backend::gBackendFunctionsTable = {};
g_isInitialized = false; g_isInitialized = false;
if (logLifecycle) { if (logLifecycle) {
@@ -86,6 +108,10 @@ namespace MobileGL {
MGLOG_I("Initializing MobileGL..."); MGLOG_I("Initializing MobileGL...");
MG_ConfigLoader::Init(); MG_ConfigLoader::Init();
MGLOG_I("Config loaded"); MGLOG_I("Config loaded");
// Immediately after the config load and before anything can count: the MGPipe
// boundary counters latch their enable flag here, so every counting site in the
// two backends is a load of an already-settled global for the rest of the run.
MG_Util::PipeStats::Init();
MG_State::Init(); MG_State::Init();
MGLOG_D("MG_State initialized"); MGLOG_D("MG_State initialized");
MG_Backend::Init(); MG_Backend::Init();
+154 -4
View File
@@ -14,6 +14,7 @@ namespace MobileGL {
namespace MG_State::GLState { namespace MG_State::GLState {
class FramebufferObject; class FramebufferObject;
class ITextureObject; class ITextureObject;
class RenderbufferObject;
} }
enum class BackendType { enum class BackendType {
@@ -24,6 +25,19 @@ namespace MobileGL {
}; };
namespace MG_Backend { namespace MG_Backend {
// One endpoint of a glCopyImageSubData. GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER
// alongside the ten whole-image texture targets, and a renderbuffer name lives in a
// namespace of its own - so an endpoint is a sum type, not an ITextureObject. At most
// one of the two pointers is set; neither is set when the name named nothing, which is
// the INVALID_VALUE the frontend validator reports.
struct CopyImageEndpoint {
SharedPtr<MG_State::GLState::ITextureObject> Texture;
SharedPtr<MG_State::GLState::RenderbufferObject> Renderbuffer;
Bool IsRenderbuffer() const { return Renderbuffer != nullptr; }
Bool Exists() const { return Texture != nullptr || Renderbuffer != nullptr; }
};
enum class FormatCapability : Uint64 { enum class FormatCapability : Uint64 {
Creatable = 1ull << 0, Creatable = 1ull << 0,
@@ -160,9 +174,9 @@ namespace MobileGL {
GLsizei height, GLint border); GLsizei height, GLint border);
void (*CopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, void (*CopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height); GLsizei width, GLsizei height);
void (*CopyImageSubData)(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, void (*CopyImageSubData)(const CopyImageEndpoint& src,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, const CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth); GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void (*GenerateMipmap)(GLenum target); void (*GenerateMipmap)(GLenum target);
@@ -178,9 +192,23 @@ namespace MobileGL {
void (*MemoryBarrierByRegion)(GLbitfield barriers); void (*MemoryBarrierByRegion)(GLbitfield barriers);
void (*BindImageTexture)(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, void (*BindImageTexture)(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer,
GLenum access, GLenum format); GLenum access, GLenum format);
// The ONLY indexed query that is genuinely a backend one, and only for the pnames
// MG_Impl/GLImpl/Getter/GL_Getter.cpp does not already own. Every indexed pname that
// names FRONTEND state - the indexed buffer bindings, the per-unit texture/sampler
// bindings, the image-unit bindings, the viewport rectangles, the indexed capabilities
// - is answered in GL_Getter::GetIntegeri_v and never reaches this entry; the
// 64-bit and float/double widths are derived there from the same answer, which is why
// no GetInteger64i_v/GetFloati_v/GetDoublei_v table entry exists. In practice this
// leaves GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE (also asked directly by
// MG_Util/ShaderTranspiler/CompileEnv.cpp) plus whatever pname the frontend has no
// case for at all.
void (*GetIntegeri_v)(GLenum target, GLuint index, GLint* data); void (*GetIntegeri_v)(GLenum target, GLuint index, GLint* data);
void (*GetInteger64i_v)(GLenum target, GLuint index, GLint64* data); // There is deliberately NO GetProgramiv entry: glGetProgramiv describes the program
void (*GetProgramiv)(GLuint program, GLenum pname, GLint* params); // the APPLICATION wrote - link status, the transform-feedback mode, the compute local
// size - all of which are frontend link artifacts on ProgramObject, and
// MG_Impl/GLImpl/Program/GL_Program.cpp answers every one of them from there. Asking a
// backend would mean asking about a DIFFERENT program (a SPIRV-Cross-generated ESSL
// one, or a SPIR-V module), in a namespace the application never sees.
// The GL program interface (glGetProgramInterfaceiv / glGetProgramResource*) is NOT // The GL program interface (glGetProgramInterfaceiv / glGetProgramResource*) is NOT
// a backend query: it describes the program the application wrote, in the // a backend query: it describes the program the application wrote, in the
// application's namespace, which neither backend program is in. It is answered // application's namespace, which neither backend program is in. It is answered
@@ -236,6 +264,14 @@ namespace MobileGL {
// (optional; null = frontend falls back to CPU accounting). // (optional; null = frontend falls back to CPU accounting).
BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated); BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated);
void (*EndXfbPrimitivesQuery)(BackendQueryHandle query); void (*EndXfbPrimitivesQuery)(BackendQueryHandle query);
// Whether GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN should be answered from the
// frontend's own accounting wherever that accounting is exact - a capture with no
// geometry stage - instead of from the query above. Set by DirectGLES, whose result
// is whatever the ES driver's PRIMITIVES_WRITTEN counter says: Adreno reports twice
// the written count for a vertex-only capture that follows a large render pass,
// where the desktop-exact answer is the one the frontend already computed. Defaults
// to false, so a backend that never sets it keeps using its GPU result.
Bool PrefersCpuXfbPrimitiveAccounting = false;
// Transform feedback capture spans, for backends whose own GL/ES driver // Transform feedback capture spans, for backends whose own GL/ES driver
// performs the capture (DirectGLES). Both optional; null means the backend // performs the capture (DirectGLES). Both optional; null means the backend
// drives capture from its draw recording instead (DirectVulkan). End is // drives capture from its draw recording instead (DirectVulkan). End is
@@ -279,6 +315,12 @@ namespace MobileGL {
struct DynamicBackendParameters { struct DynamicBackendParameters {
SizeT UniformBufferOffsetAlignment = 256; SizeT UniformBufferOffsetAlignment = 256;
// GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, which is a SEPARATE limit from the
// uniform one and is routinely larger: Adreno 830 reports 32 for uniform buffers and
// 64 for storage buffers. Answering the storage query with the uniform value let an
// application bind a storage range at an offset the driver cannot address, which it
// accepted without error and then wrote somewhere else entirely.
SizeT ShaderStorageBufferOffsetAlignment = 256;
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT. 1.0 means the backend cannot filter anisotropically, // GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT. 1.0 means the backend cannot filter anisotropically,
// which is also why the extension is not advertised in that case. // which is also why the extension is not advertised in that case.
Float MaxTextureMaxAnisotropy = 1.0f; Float MaxTextureMaxAnisotropy = 1.0f;
@@ -318,8 +360,37 @@ namespace MobileGL {
Int MaxVertexAttribs = 16; Int MaxVertexAttribs = 16;
Int MaxComputeShaderStorageBlocks = 8; Int MaxComputeShaderStorageBlocks = 8;
Int MaxCombinedShaderStorageBlocks = 32; Int MaxCombinedShaderStorageBlocks = 32;
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. Zero is a legal answer for the four
// non-compute, non-fragment stages and these defaults are the spec minimums, not
// placeholders: GL 4.6 table 23.64 and ES 3.2 table 21.44 both set the minimum for
// vertex, tessellation control, tessellation evaluation and geometry at 0, and only
// fragment (8 in GL, 4 in ES) and compute are guaranteed to have any. Every real ARM
// GLES driver takes that allowance - a Mali-G925 reports 0 for all four - so a
// backend that cannot honour a graphics-stage storage block MUST report 0 here
// rather than a hopeful number. Advertising a non-zero count the driver will refuse
// does not make the block work; it only moves the failure from an honest
// "unsupported" at query time to a backend link error the frontend never surfaces,
// after which every draw with that program silently renders nothing.
Int MaxVertexShaderStorageBlocks = 0;
Int MaxTessControlShaderStorageBlocks = 0;
Int MaxTessEvaluationShaderStorageBlocks = 0;
Int MaxGeometryShaderStorageBlocks = 0;
Int MaxFragmentShaderStorageBlocks = 8;
Int MaxComputeUniformBlocks = 12; Int MaxComputeUniformBlocks = 12;
Int MaxComputeWorkGroupInvocations = 128; Int MaxComputeWorkGroupInvocations = 128;
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / GL_MAX_COMPUTE_WORK_GROUP_SIZE, one value per
// axis. These six, with the invocations limit above, are the only indexed limits a
// backend genuinely OWNS - the device answers them (glGetIntegeri_v on DirectGLES,
// VkPhysicalDeviceLimits::maxComputeWorkGroupCount/Size on DirectVulkan) - and so
// the only ones that survive the retirement of the GetIntegeri_v table entry: they
// cross the MGPipe boundary inside MGPCaps, by inclusion of this struct (plan B
// section 4.4.1). Every other indexed pname names frontend state. RAW driver
// answers, like the invocations limit: GL_Getter and the compile environment floor
// them at the shared MIN_COMPUTE_WORK_GROUP_* minimums themselves. The defaults are
// the GL 4.3 core minimums (table 23.60) and describe the no-backend case, as
// MaxClipDistances' does.
Int MaxComputeWorkGroupCount[3] = {65535, 65535, 65535};
Int MaxComputeWorkGroupSize[3] = {1024, 1024, 64};
Int MaxShaderStorageBufferBindings = 8; Int MaxShaderStorageBufferBindings = 8;
Int MaxTextureBufferSize = 65536; Int MaxTextureBufferSize = 65536;
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained. // GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
@@ -334,8 +405,45 @@ namespace MobileGL {
Int MaxComputeImageUniforms = 8; Int MaxComputeImageUniforms = 8;
Int MaxDrawBuffers = 8; Int MaxDrawBuffers = 8;
Int MaxColorAttachments = 8; Int MaxColorAttachments = 8;
// GL_MAX_CLIP_DISTANCES. Zero is a legal answer here, not a placeholder, and a
// backend that cannot host a clip distance MUST report it: advertising eight the
// backend will refuse does not make gl_ClipDistance work, it only moves the failure
// from an honest "unsupported" at query time to a backend shader-compile error the
// frontend never surfaces, after which every draw with that program silently renders
// nothing. DirectGLES fills it from GL_EXT_clip_cull_distance, DirectVulkan from the
// shaderClipDistance device feature. The DEFAULT stays at the GL 4.3 core minimum
// because it describes the no-backend case (standalone shader compiles, unit tests),
// where there is no device to be honest about and BuildTBuiltInResource still has to
// hand glslang a workable gl_MaxClipDistances.
Int MaxClipDistances = 8; Int MaxClipDistances = 8;
// GL_MAX_CULL_DISTANCES and GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, under exactly
// the contract stated for MaxClipDistances above: ZERO IS A LEGAL ANSWER and a
// backend that cannot host a cull distance MUST report it. The failure this prevents
// is worse than the clip one, because cull distance discards the whole primitive:
// glslang bounds gl_CullDistance[i] against maxCullDistances and expands
// gl_MaxCullDistances from it, SPIRV-Cross then emits
// `#extension GL_EXT_clip_cull_distance : require` into the ESSL, and a host driver
// without that extension rejects the program in an info log nobody surfaces. These
// used to be bare 8s inside BuildTBuiltInResource with no backend consulted at all.
// The DEFAULTS are the GL 4.5 core minimums for the same reason MaxClipDistances'
// is: they describe the no-backend case (standalone compiles, unit tests).
Int MaxCullDistances = 8;
Int MaxCombinedClipAndCullDistances = 8;
Int MaxViewports = 16; Int MaxViewports = 16;
// GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX: which vertex of a
// primitive supplies gl_Layer and gl_ViewportIndex. GL 4.6 table 23.65 makes
// GL_UNDEFINED_VERTEX a legal answer for both, and it is the honest default - naming
// a convention is a statement about behaviour, so a backend that does not pin one
// must not claim it does. DirectGLES fills the layer one from the ES 3.2 query and
// the viewport one from GL_OES_viewport_array, and leaves UNDEFINED where the
// capability is absent: without the viewport array extension only viewport 0 is ever
// rasterized, so no convention selects anything. DirectVulkan keeps UNDEFINED for
// both - which vertex provokes is decided per pipeline by
// VulkanRenderer::SelectProvokingVertexMode out of VK_EXT_provoking_vertex,
// provokingVertexModePerPipeline and the topology, so no single convention is true
// of the backend.
GLenum LayerProvokingVertex = GL_UNDEFINED_VERTEX;
GLenum ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
Int MaxViewportWidth = 16384; Int MaxViewportWidth = 16384;
Int MaxViewportHeight = 16384; Int MaxViewportHeight = 16384;
Float ViewportBoundsRangeMin = 0.0f; Float ViewportBoundsRangeMin = 0.0f;
@@ -383,13 +491,55 @@ namespace MobileGL {
const Uint32 bit = PerLayerFramebufferAttachmentBit(target); const Uint32 bit = PerLayerFramebufferAttachmentBit(target);
return bit != 0 && (PerLayerFramebufferAttachmentTargets & bit) != 0; return bit != 0 && (PerLayerFramebufferAttachmentTargets & bit) != 0;
} }
// Whether this backend can CONSUME a shader module that still declares 64-bit floats,
// i.e. whether `double` survives the transpile instead of being narrowed to `float`
// (ShaderTranspiler::DemoteFloat64Pass). Detected, never assumed:
// * DirectVulkan sets it from VkPhysicalDeviceFeatures::shaderFloat64, the feature
// VUID-VkShaderModuleCreateInfo-pCode-08740 requires before a module declaring
// OpCapability Float64 may be created at all. lavapipe has it; Adreno and Mali
// both report VK_FALSE, so no real mobile device does.
// * DirectGLES can NEVER have it. GLSL ES has no 64-bit float type in any version
// or extension, so SPIRV-Cross cannot emit one ("FP64 not supported in ES
// profile") and the demotion there is mathematically mandatory, always.
// Defaults to false so a backend that never sets it - and the no-backend case, which
// is what standalone shader compiles and the unit tests run under - keeps the
// demotion, which is the behaviour that works everywhere.
Bool SupportsShaderFloat64 = false;
// Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e. // Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e.
// whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected, // whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected,
// never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (the // never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (the
// attribute travels as its 32-bit word pair, so no VK_FORMAT_R64* is required, but the // attribute travels as its 32-bit word pair, so no VK_FORMAT_R64* is required, but the
// bitcast result is Float64); DirectGLES can never have it, ESSL having no fp64 type at // bitcast result is Float64); DirectGLES can never have it, ESSL having no fp64 type at
// all. Defaults to false so a backend that never sets it gets the conservative answer. // all. Defaults to false so a backend that never sets it gets the conservative answer.
//
// INDEPENDENT of SupportsShaderFloat64, and it has to be: this flag decides a VkFormat
// from the VAO ATTRIBUTE alone, which does not know what type the shader declared, and
// glVertexAttribFormat(GL_DOUBLE) feeding a plain `in vec4` is both legal and common
// (KHR-GL43.vertex_attrib_binding.basic-input-case4/5, advanced-bindingUpdate). A
// backend with native fp64 that still cannot FETCH 64 bits keeps this false and relies
// on the per-MODULE rule in ShaderCompiler::SanitizeAndOptimizeBinary instead: a vertex
// module that declares a 64-bit float INPUT is demoted whole, so the two shader-side
// halves (PackDoubleVertexInputsPass and VertexInputStateFactory::ToVkVertexFormat)
// still see one consistent world.
Bool SupportsFloat64VertexAttributes = false; Bool SupportsFloat64VertexAttributes = false;
// Whether a TESSELLATION stage of this backend may access gl_PointSize - i.e.
// whether a module declaring OpCapability TessellationPointSize can reach the
// driver at all. DirectVulkan sets both this and the geometry twin from the one
// shaderTessellationAndGeometryPointSize feature; DirectGLES sets them
// independently from the EXT/OES_tessellation_point_size /
// geometry_point_size extension pairs (PointSizeTier), which really do come
// separately. When absent, ProgramSpirvTask demotes the built-in to an ordinary
// varying program-wide (ShaderCompiler::
// DemoteTessellationGeometryPointSizeForProgram); MOBILEGL_POINT_SIZE_DEMOTION
// overrides the detection in either direction at backend init.
//
// Defaults TRUE, deliberately against the house "assume absent" rule: false
// ARMS a rewrite, so the conservative no-backend answer (standalone compiles,
// unit tests) is the one that leaves modules untouched. A backend that never
// sets it gets standard modules and, at worst, the old honest declines.
Bool SupportsTessellationPointSize = true;
// The geometry-stage twin (OpCapability GeometryPointSize).
Bool SupportsGeometryPointSize = true;
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024; SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
Uint32 SubgroupSize = 0; Uint32 SubgroupSize = 0;
Uint32 SubgroupSupportedStages = 0; Uint32 SubgroupSupportedStages = 0;
@@ -8,6 +8,7 @@
#include "BackendObject_DirectGLES.h" #include "BackendObject_DirectGLES.h"
#include "MG_Backend/BackendObject.h" #include "MG_Backend/BackendObject.h"
#include "MG_Backend/BackendObjects.h"
#include <MG_Backend/DirectGLES/DirectGLES.h> #include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Backend/DirectGLES/Managers.h> #include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectGLES/Utils.h> #include <MG_Backend/DirectGLES/Utils.h>
@@ -212,7 +213,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) { if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
reasons.push_back("no colour-renderable three-channel format on OpenGL ES"); reasons.push_back("no colour-renderable three-channel format on OpenGL ES");
} }
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) { // A format is either 8- or 16-bit signed normalized, so at most one of the two ever
// survives GetApplicablePixelFormatNormalizeOptions and the reason is not duplicated.
if ((options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) ||
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
reasons.push_back("EXT_render_snorm not supported"); reasons.push_back("EXT_render_snorm not supported");
} }
@@ -303,6 +307,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
return capabilities.MaxColorTextureSamples; return capabilities.MaxColorTextureSamples;
} }
// The RENDERBUFFER twin, and it is a different set of pnames on purpose.
// GL_MAX_{COLOR,DEPTH}_TEXTURE_SAMPLES bound multisample TEXTURES; a renderbuffer is
// bounded by GL_MAX_SAMPLES (GL 4.6 core 9.2.4), with GL_MAX_INTEGER_SAMPLES for the
// integer formats. Using the texture ceilings here - which is what the renderbuffer probe
// did - is not merely untidy: the two texture pnames are ES 3.1 state, so on an ES 3.0
// context the loader's rejected-probe clamp leaves them at 1 (see the multisample clamps
// in the GLES loader) and the walk below would never run past one sample, recording {1}
// for EVERY colour format while GL_MAX_SAMPLES - ES 3.0 core, so genuinely answered -
// reports 4. Once the frontend validates against this list, that would reject every
// multisample renderbuffer on such a context.
Int GetGLESRenderbufferFormatMaxSamples(const MG_External::GLESCapabilities& capabilities,
GLenum imageFormat) {
const Bool isInteger = imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER ||
imageFormat == GL_RGB_INTEGER || imageFormat == GL_RGBA_INTEGER;
return isInteger ? capabilities.MaxIntegerSamples : capabilities.MaxSamples;
}
Bool ProbeFramebufferCompletenessForTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target, Bool ProbeFramebufferCompletenessForTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target,
GLuint texture, TextureInternalFormat format) { GLuint texture, TextureInternalFormat format) {
GLuint framebuffer = 0; GLuint framebuffer = 0;
@@ -406,9 +427,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
return complete; return complete;
} }
// `samples` only reaches the multisample targets; every other target ignores it. The
// descending sample walk (ProbeTextureSampleCounts) reuses this whole routine rather than
// repeating the gen/bind/completeness/delete dance.
Bool ProbeTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target, GLenum internalFormat, Bool ProbeTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target, GLenum internalFormat,
GLenum imageFormat, GLenum imageType, TextureInternalFormat logicalFormat, GLenum imageFormat, GLenum imageType, TextureInternalFormat logicalFormat,
Bool* outRenderable) { Bool* outRenderable, Int samples = 1) {
if (!IsGLESProbeTextureTarget(target) || !gl.glGenTextures || !gl.glBindTexture || !gl.glDeleteTextures) { if (!IsGLESProbeTextureTarget(target) || !gl.glGenTextures || !gl.glBindTexture || !gl.glDeleteTextures) {
return false; return false;
} }
@@ -428,10 +452,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Bool isMultisample = IsGLESProbeMultisampleTarget(target); const Bool isMultisample = IsGLESProbeMultisampleTarget(target);
if (isMultisample) { if (isMultisample) {
const auto probeSamples = static_cast<GLsizei>(std::max(samples, 1));
if (target == TextureTarget::Texture2DMultisample && gl.glTexStorage2DMultisample) { if (target == TextureTarget::Texture2DMultisample && gl.glTexStorage2DMultisample) {
gl.glTexStorage2DMultisample(glTarget, 1, internalFormat, 1, 1, GL_TRUE); gl.glTexStorage2DMultisample(glTarget, probeSamples, internalFormat, 1, 1, GL_TRUE);
} else if (target == TextureTarget::Texture2DMultisampleArray && gl.glTexStorage3DMultisample) { } else if (target == TextureTarget::Texture2DMultisampleArray && gl.glTexStorage3DMultisample) {
gl.glTexStorage3DMultisample(glTarget, 1, internalFormat, 1, 1, 1, GL_TRUE); gl.glTexStorage3DMultisample(glTarget, probeSamples, internalFormat, 1, 1, 1, GL_TRUE);
} else { } else {
gl.glBindTexture(glTarget, static_cast<GLuint>(previousBinding)); gl.glBindTexture(glTarget, static_cast<GLuint>(previousBinding));
gl.glDeleteTextures(1, &texture); gl.glDeleteTextures(1, &texture);
@@ -527,6 +552,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
return sampleCounts; return sampleCounts;
} }
// The multisample TEXTURE twin of ProbeRenderbufferSampleCounts. It used to be a
// hardcoded {1}, which made glGetInternalformativ(GL_SAMPLES) claim a one-sample maximum
// for every format on the multisample targets even where glTexImage2DMultisample happily
// accepts four - GL 4.6 core 8.8 makes that query the definition of the maximum, so the
// two answers cannot both be right. Completeness is required at every count, exactly as
// the renderbuffer walk requires it; the caller only reaches here once the one-sample
// probe has already succeeded, so 1 terminates the list without being re-probed.
Vector<Int> ProbeTextureSampleCounts(const MG_External::GLESFunctionsTable& gl, TextureTarget target,
GLenum internalFormat, GLenum imageFormat, GLenum imageType,
TextureInternalFormat logicalFormat, Int maxSamples) {
Vector<Int> sampleCounts;
for (Int samples = std::max(maxSamples, 1); samples > 1; samples >>= 1) {
Bool renderable = false;
const Bool created = ProbeTexture(gl, target, internalFormat, imageFormat, imageType, logicalFormat,
&renderable, samples);
if (created && renderable) {
sampleCounts.push_back(samples);
}
}
sampleCounts.push_back(1);
return sampleCounts;
}
void PopulateFormatCapabilitiesImpl(const MG_External::GLESFunctionsTable& gl, void PopulateFormatCapabilitiesImpl(const MG_External::GLESFunctionsTable& gl,
const MG_External::GLESCapabilities& capabilities, const MG_External::GLESCapabilities& capabilities,
FormatCapabilityCache& cache) { FormatCapabilityCache& cache) {
@@ -627,7 +675,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
AddFullFormatCaps(cache, targetIndex, formatIndex, AddFullFormatCaps(cache, targetIndex, formatIndex,
BuildTextureCapsFromProbe(logicalFormat, target, nativeRenderable)); BuildTextureCapsFromProbe(logicalFormat, target, nativeRenderable));
if (IsGLESProbeMultisampleTarget(target)) { if (IsGLESProbeMultisampleTarget(target)) {
cache.SampleCounts[targetIndex][formatIndex] = {1}; const Int maxSamples =
GetGLESFormatMaxSamples(capabilities, logicalFormat, nativeInfo.ImageFormat);
cache.SampleCounts[targetIndex][formatIndex] = ProbeTextureSampleCounts(
gl, probeTarget, nativeInfo.InternalFormat, nativeInfo.ImageFormat,
nativeInfo.ImageType, logicalFormat, maxSamples);
} }
} }
shouldProbeFallback = !nativeCreated || !nativeRenderable; shouldProbeFallback = !nativeCreated || !nativeRenderable;
@@ -645,7 +697,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
LogGLESFormatCaveat(logicalFormat, targetIndex, fallbackInfo); LogGLESFormatCaveat(logicalFormat, targetIndex, fallbackInfo);
} }
if (IsGLESProbeMultisampleTarget(target)) { if (IsGLESProbeMultisampleTarget(target)) {
cache.SampleCounts[targetIndex][formatIndex] = {1}; const Int maxSamples =
GetGLESFormatMaxSamples(capabilities, logicalFormat, fallbackInfo.ImageFormat);
cache.SampleCounts[targetIndex][formatIndex] = ProbeTextureSampleCounts(
gl, probeTarget, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat,
fallbackInfo.ImageType, logicalFormat, maxSamples);
} }
} }
} }
@@ -678,7 +734,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
AddFullFormatCaps(cache, renderbufferTargetIndex, formatIndex, AddFullFormatCaps(cache, renderbufferTargetIndex, formatIndex,
GetRenderbufferFeatureCaps(logicalFormat)); GetRenderbufferFeatureCaps(logicalFormat));
const Int maxSamples = const Int maxSamples =
GetGLESFormatMaxSamples(capabilities, logicalFormat, nativeInfo.ImageFormat); GetGLESRenderbufferFormatMaxSamples(capabilities, nativeInfo.ImageFormat);
cache.SampleCounts[renderbufferTargetIndex][formatIndex] = cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
ProbeRenderbufferSampleCounts(gl, nativeInfo.InternalFormat, logicalFormat, maxSamples); ProbeRenderbufferSampleCounts(gl, nativeInfo.InternalFormat, logicalFormat, maxSamples);
} else { } else {
@@ -692,7 +748,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, renderbufferFallbackInfo); LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, renderbufferFallbackInfo);
} }
const Int maxSamples = const Int maxSamples =
GetGLESFormatMaxSamples(capabilities, logicalFormat, renderbufferFallbackInfo.ImageFormat); GetGLESRenderbufferFormatMaxSamples(capabilities, renderbufferFallbackInfo.ImageFormat);
cache.SampleCounts[renderbufferTargetIndex][formatIndex] = ProbeRenderbufferSampleCounts( cache.SampleCounts[renderbufferTargetIndex][formatIndex] = ProbeRenderbufferSampleCounts(
gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, maxSamples); gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, maxSamples);
} }
@@ -710,11 +766,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
.ExtraVendor = Nullopt, // Extra vendor .ExtraVendor = Nullopt, // Extra vendor
.RendererGLInfo = .RendererGLInfo =
{ {
.TargetGLVersion = {4, 0, 0}, // GL target version .TargetGLVersion = {4, 6, 0}, // GL target version
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version .TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
// Baseline advertisement (no timer queries / anisotropy yet); reconciled // Baseline advertisement (no runtime capabilities yet); reconciled once
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions. // the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
.Extensions = BuildAdvertisedExtensions(false, false), .Extensions = BuildAdvertisedExtensions(false, false, false, false, false, false),
.IsCompatibilityProfile = false // Is Compatibility Profile .IsCompatibilityProfile = false // Is Compatibility Profile
}, },
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability .StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
@@ -734,9 +790,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// thread can only observe the extension string after the // thread can only observe the extension string after the
// advertisement for its context has settled; rebuilding the whole // advertisement for its context has settled; rebuilding the whole
// list keeps the re-run after a context recreation idempotent. // list keeps the re-run after a context recreation idempotent.
void UpdateAdvertisedCapabilityExtensions(Bool anisotropicFilteringSupported) { void UpdateAdvertisedCapabilityExtensions(const MG_External::GLESCapabilities& capabilities) {
MutableRendererInfo().RendererGLInfo.Extensions = MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(
BuildAdvertisedExtensions(AreTimerQueriesSupported(), anisotropicFilteringSupported); AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy,
capabilities.SupportsDrawIndirect,
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance,
capabilities.SupportsTextureView, capabilities.SupportsTextureCubeMapArray);
} }
} // namespace } // namespace
@@ -745,6 +804,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
PopulateFormatCapabilitiesImpl(gl, capabilities, cache); PopulateFormatCapabilitiesImpl(gl, capabilities, cache);
} }
Int ClampSamplesToBackendSupport(SizeT targetIndex, TextureInternalFormat logicalFormat, GLenum imageFormat,
Int samples) {
if (samples <= 1) {
return samples;
}
Int maxSamples = 0;
const SizeT formatIndex = static_cast<SizeT>(logicalFormat);
if (pActiveBackendObject && targetIndex < kFormatCapabilityTargetCount &&
formatIndex < kFormatCapabilityFormatCount) {
// Descending, so the head is the largest count this device actually allocated.
const Vector<Int>& probedCounts =
pActiveBackendObject->GetFormatCapabilities().SampleCounts[targetIndex][formatIndex];
if (!probedCounts.empty()) {
maxSamples = probedCounts.front();
}
}
if (maxSamples <= 0) {
maxSamples = GetGLESFormatMaxSamples(g_GLESCapabilities, logicalFormat, imageFormat);
}
return std::min(samples, std::max(maxSamples, 1));
}
BackendObject_DirectGLES::~BackendObject_DirectGLES() { BackendObject_DirectGLES::~BackendObject_DirectGLES() {
DestroyEGLContext(); DestroyEGLContext();
} }
@@ -779,11 +861,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false; return false;
} }
DirectGLES::SetGLESCapabilities(m_GLESCapabilities); DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query and // Now that g_GLESCapabilities knows the host extensions, entry points, and ES version,
// GL_EXT_texture_filter_anisotropic, reconcile the advertisement (see the comment on // reconcile every runtime-gated advertisement (see the comment on
// UpdateAdvertisedCapabilityExtensions for why it cannot happen when the extension // UpdateAdvertisedCapabilityExtensions for why this cannot happen when the list is first
// list is first built). // built).
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities.SupportsTextureFilterAnisotropy); UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities);
UpdateDynamicBackendParameters(); UpdateDynamicBackendParameters();
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities()); PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
PrintFormatCapabilities(GetFormatCapabilities()); PrintFormatCapabilities(GetFormatCapabilities());
@@ -924,11 +1006,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
return MutableRendererInfo(); return MutableRendererInfo();
} }
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported) { Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
Bool drawIndirectSupported,
Bool nonZeroIndirectBaseInstanceSupported,
Bool textureViewSupported, Bool cubeMapArraySupported) {
Vector<GLExtension> extensions = { Vector<GLExtension> extensions = {
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend, // The version tokens have to reach the version the backend actually claims:
// TargetGLVersion is {4,6,0}, and a list that stopped at OpenGL40 told an
// application feature-detecting off these tokens the opposite of what
// GL_MAJOR_VERSION / GL_MINOR_VERSION told it.
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, V_OpenGL41, V_OpenGL42, V_OpenGL43,
V_OpenGL44, V_OpenGL45, V_OpenGL46,
E_GL_ARB_draw_buffers_blend,
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store, E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object, E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_texture_storage, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_texture_storage,
E_GL_ARB_texture_storage_multisample, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access, E_GL_ARB_texture_storage_multisample, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters, E_GL_ARB_shader_draw_parameters, E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters, E_GL_ARB_shader_draw_parameters,
@@ -940,10 +1031,105 @@ namespace MobileGL::MG_Backend::DirectGLES {
// picks a whole different shader for draw_buffers without // picks a whole different shader for draw_buffers without
// explicit_attrib_location. DirectVulkan advertises both. // explicit_attrib_location. DirectVulkan advertises both.
E_GL_ARB_explicit_attrib_location, E_GL_ARB_texture_multisample, E_GL_ARB_shader_image_size, E_GL_ARB_explicit_attrib_location, E_GL_ARB_texture_multisample, E_GL_ARB_shader_image_size,
// Core since GL 3.1 and implemented for every version advertised here. The string
// matters because applications gate the ENTRY POINTS on it rather than on the
// version: a caller that finds the extension missing never resolves
// glGetUniformBlockIndex / glUniformBlockBinding, and one that then uses uniform
// blocks anyway calls through a null pointer.
E_GL_ARB_uniform_buffer_object,
// Sampling the stencil aspect through DEPTH_STENCIL_TEXTURE_MODE. Core from 4.3,
// so on a 4.0 context the string is the only way to reach it. The host ES driver
// has had the same texture parameter since ES 3.1, which every device MobileGL
// runs on provides.
E_GL_ARB_stencil_texturing,
// Core since 3.2 and implemented here on both backends - glDrawElementsBaseVertex,
// glDrawRangeElementsBaseVertex, glDrawElementsInstancedBaseVertex and
// glMultiDrawElementsBaseVertex all reach real per-draw vertex rebasing. The string
// was simply never emitted, which left KHR-GL4*.draw_elements_base_vertex_tests
// NotSupported on a feature that works.
E_GL_ARB_draw_elements_base_vertex,
// The whole sync-object family is real and core since 3.2: glFenceSync, glIsSync,
// glDeleteSync, glClientWaitSync, glWaitSync and glGetSynciv all live in GLImpl over a
// backend fence (a host GLsync here, a VkFence on DirectVulkan), and glGetInteger64v
// answers GL_MAX_SERVER_WAIT_TIMEOUT. The string matters for the same reason
// ARB_uniform_buffer_object's does: LWJGL builds GLCapabilities from the extension
// list, and a caller that finds GL_ARB_sync missing never resolves the entry points -
// then calls through null if it uses fences anyway. Nothing in the CTS gates on this
// string, so it is advertised on the strength of the implementation, not a test unlock.
E_GL_ARB_sync,
// Atomic counters, core since 4.2. glGetActiveAtomicCounterBufferiv and the whole
// GL_ATOMIC_COUNTER_BUFFER_* query family are real in GLImpl, and SyncAtomicCounterBuffers
// re-issues the counter buffer as an SSBO binding in the range reserved at the top of
// the ES driver's shader-storage points, so a counter dispatch reads and writes the
// buffer the application bound. DirectVulkan reaches the same place through its own
// descriptor resolution, so the string is symmetric.
E_GL_ARB_shader_atomic_counters,
// glVertexAttribDivisor, core since 3.3 and real on both backends. Applications
// (Better Clouds' GLCompat among them) accept the extension string as an
// ALTERNATIVE to a 3.3 context when deciding whether instanced rendering is
// available, so withholding it makes MobileGL look less capable than it is.
E_GL_ARB_instanced_arrays,
// The whole of KHR_debug lives in GLImpl - the message log, the group stack and the
// object-label table are MobileGL's own state, not the host driver's - so it is as
// available here as it is on DirectVulkan, which has advertised it all along.
E_GL_KHR_debug,
// Core GL 3.0-4.3 plumbing that has been real here for as long as the backend has
// existed, and that was simply never named. None of these unlocks a single CTS case -
// the conformance suite reaches all of them through the version - so they are
// advertised for the OTHER consumer of this list: LWJGL builds GLCapabilities from the
// string set, and an application that gates its ENTRY POINTS on the string rather than
// on the version never resolves them and then calls through null. Each is backed by
// the entry points named beside it.
//
// glBindVertexArray / glGenVertexArrays / glDeleteVertexArrays / glIsVertexArray.
E_GL_ARB_vertex_array_object,
// The 14 glSamplerParameter* / glGetSamplerParameter* entry points, including the
// integer-valued Iiv/Iuiv forms.
E_GL_ARB_sampler_objects,
// glMapBufferRange + glFlushMappedBufferRange, which ARB_buffer_storage's persistent
// maps are already built on top of.
E_GL_ARB_map_buffer_range,
// glCopyBufferSubData plus the GL_COPY_READ_BUFFER / GL_COPY_WRITE_BUFFER targets.
E_GL_ARB_copy_buffer,
// glCopyImageSubData, wired to a real backend hook on both backends.
E_GL_ARB_copy_image,
// GL_TEXTURE_SWIZZLE_{R,G,B,A,RGBA}, which this backend syncs through to the ES
// driver's identical parameters.
E_GL_ARB_texture_swizzle,
// GL_INT_2_10_10_10_REV / GL_UNSIGNED_INT_2_10_10_10_REV on glVertexAttribPointer plus
// the eight glVertexAttribP* entry points.
E_GL_ARB_vertex_type_2_10_10_10_rev,
// The R/RG internal formats. Named separately from the float ones because an
// application may check either.
E_GL_ARB_texture_rg,
// GL_DEPTH_COMPONENT32F and GL_DEPTH32F_STENCIL8.
E_GL_ARB_depth_buffer_float,
// The floating-point colour formats. Unlike the rest of this block this string DOES
// gate CTS cases - KHR-GL4*.internalformat.texture2d.*{16f,32f} is keyed on it with no
// core-version fallback, so eight cases per version list were NotSupported on formats
// the backend has always had.
E_GL_ARB_texture_float,
// glViewportArrayv / glViewportIndexedf{,v} / glScissorArrayv / glScissorIndexed{,v} /
// glDepthRangeArrayv / glDepthRangeIndexed / glGetFloati_v / glGetDoublei_v, over the
// 16 viewports GL_MAX_VIEWPORTS reports and the per-viewport routing emulation.
E_GL_ARB_viewport_array,
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the // Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
// extension explicitly permits. It is also the only thing that // extension explicitly permits. It is also the only thing that
// exposes glProgramParameteri before GL 4.1. // exposes glProgramParameteri before GL 4.1.
E_GL_ARB_get_program_binary}; E_GL_ARB_get_program_binary};
// Minecraft 26.3 checks this prerequisite before it even considers
// GL_ARB_multi_draw_indirect. ES 3.1 supplies both single-draw entry points; the loader
// folds the version and pointer checks into SupportsDrawIndirect.
if (drawIndirectSupported) {
extensions.push_back(E_GL_ARB_draw_indirect);
}
// ARB_base_instance also defines the last word of an indirect command. Direct calls are
// emulated on every Espryt device, but without host GL_EXT_base_instance a native indirect
// draw cannot shift divisor attributes by a GPU-authored non-zero value, so do not promise
// that incomplete case.
if (drawIndirectSupported && nonZeroIndirectBaseInstanceSupported) {
extensions.push_back(E_GL_ARB_base_instance);
}
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the host ES // GL_KHR_parallel_shader_compile is MobileGL's own capability, not the host ES
// driver's: the compiler threads are MobileGL's, and glCompileShader/glLinkProgram // driver's: the compiler threads are MobileGL's, and glCompileShader/glLinkProgram
// are serviced entirely inside the frontend. Whether the device driver advertises // are serviced entirely inside the frontend. Whether the device driver advertises
@@ -960,12 +1146,62 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (MG_Util::Async::AsyncShaderCompileEnabled()) { if (MG_Util::Async::AsyncShaderCompileEnabled()) {
extensions.push_back(E_GL_KHR_parallel_shader_compile); extensions.push_back(E_GL_KHR_parallel_shader_compile);
} }
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64). Every `double` in a
// shader compiles and runs already - it is narrowed to 32 bits before the module
// reaches this backend - so an application that simply uses doubles needs nothing
// advertised. What the extension additionally promises is 64-bit PRECISION, which no
// mobile GPU has and the narrowing cannot fake, so advertising it by default would
// make an application that checks the string take a path MobileGL cannot honour.
if (MG_Config::Features.AdvertiseFp64) {
extensions.push_back(E_GL_ARB_gpu_shader_fp64);
}
// Only advertised when the device driver actually has usable timer queries // Only advertised when the device driver actually has usable timer queries
// (GL_EXT_disjoint_timer_query plus its entry points) and the // (GL_EXT_disjoint_timer_query plus its entry points) and the
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off. // MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) { if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
extensions.push_back(E_GL_ARB_timer_query); extensions.push_back(E_GL_ARB_timer_query);
} }
// Cube map arrays are core from GL 4.0 and from ES 3.2, but on a pre-ES-3.2 driver without
// EXT/OES_texture_cube_map_array there is nothing underneath: the texture gets no storage
// and a samplerCubeArray shader does not even compile, which is exactly what the POST
// reports. So the string follows the host capability rather than the version.
//
// Named for the application's benefit rather than the suite's: measured on Adreno 830,
// KHR-GL43.texture_gather.plain-gather-*-cube-array already passed without the string, so
// this unlocks no conformance case. It is advertised because the feature is real and
// because an application that feature-detects cube map arrays off the string (rather than
// off the 4.0 version) would otherwise decline a path this backend serves.
if (cubeMapArraySupported) {
extensions.push_back(E_GL_ARB_texture_cube_map_array);
}
// Only advertised when the host ES driver has EXT/OES_texture_view. ES has no core
// texture views at any version and no honest emulation exists: a view is a SECOND NAME
// over the SAME storage, so that writes through either are visible through the other and
// the two carry independent per-texture parameters at the same time - which is exactly
// what applications use it for (Better Clouds samples one D24S8 through its own name with
// DEPTH_STENCIL_TEXTURE_MODE = STENCIL_INDEX and through a view with DEPTH_COMPONENT, in
// a single shading pass). A copy-based fallback satisfies neither half, and fails
// silently; withholding the string and answering glTextureView with INVALID_OPERATION is
// the only behaviour that cannot be mistaken for success.
//
// The host extension is necessary and NOT sufficient, which is why this second gate
// exists. Adreno 830 has EXT_texture_view, and on it the whole functional half of
// KHR-GL4{2,3}.texture_view fails: base_and_max_levels, reference_counting and
// view_sampling Fail and view_classes crashes, while only the two pure-API cases
// (errors, gettexparameter - neither of which touches the host view) pass. The cause is
// known and is MobileGL's, not the driver's: SyncTextureViewToBackend normalizes the
// VIEW's ES internalformat independently of the storage it aliases, so whenever the two
// land on different renderability carriers the host rejects the pair, the error is
// swallowed, and the view is left as a storage-less name that samples as zeros.
// DirectVulkan builds the view as a second VkImageView over one VkImage and has no such
// seam - it passes 5 of the 7 cases on the same device - so the string stays there.
//
// Until that reconciliation exists, advertising here would be the same lie the comment
// above refuses to tell, just with an extra prerequisite met. Set
// MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1 to re-enable it for that work.
if (textureViewSupported && MG_Config::Features.EsprytEnableTextureView) {
extensions.push_back(E_GL_ARB_texture_view);
}
// Only advertised when the host ES driver actually filters anisotropically: the sampler // Only advertised when the host ES driver actually filters anisotropically: the sampler
// state is accepted regardless, but forwarding it would be a no-op without the extension, // state is accepted regardless, but forwarding it would be a no-op without the extension,
// and an app that trusts the string (LWJGL builds GLCapabilities from it) would silently // and an app that trusts the string (LWJGL builds GLCapabilities from it) would silently
@@ -1002,6 +1238,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect; funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
funcsTable.GL.MultiDrawElementsIndirectCount = MultiDrawElementsIndirectCount; funcsTable.GL.MultiDrawElementsIndirectCount = MultiDrawElementsIndirectCount;
funcsTable.GL.MultiDrawArraysIndirect = MultiDrawArraysIndirect; funcsTable.GL.MultiDrawArraysIndirect = MultiDrawArraysIndirect;
funcsTable.GL.MultiDrawArraysIndirectCount = MultiDrawArraysIndirectCount;
funcsTable.GL.DrawRangeElementsBaseVertex = DrawRangeElementsBaseVertex; funcsTable.GL.DrawRangeElementsBaseVertex = DrawRangeElementsBaseVertex;
funcsTable.GL.DrawRangeElements = DrawRangeElements; funcsTable.GL.DrawRangeElements = DrawRangeElements;
funcsTable.GL.DrawElementsInstancedBaseVertexBaseInstance = DrawElementsInstancedBaseVertexBaseInstance; funcsTable.GL.DrawElementsInstancedBaseVertexBaseInstance = DrawElementsInstancedBaseVertexBaseInstance;
@@ -1018,8 +1255,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
funcsTable.GL.MemoryBarrierByRegion = MemoryBarrierByRegion; funcsTable.GL.MemoryBarrierByRegion = MemoryBarrierByRegion;
funcsTable.GL.BindImageTexture = BindImageTexture; funcsTable.GL.BindImageTexture = BindImageTexture;
funcsTable.GL.GetIntegeri_v = GetIntegeri_v; funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
funcsTable.GL.GetProgramiv = GetProgramiv;
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding; funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
funcsTable.GL.Clear = Clear; funcsTable.GL.Clear = Clear;
funcsTable.GL.ClearBufferfi = ClearBufferfi; funcsTable.GL.ClearBufferfi = ClearBufferfi;
@@ -1069,6 +1304,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// geometry shader's amplification. // geometry shader's amplification.
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery; funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery; funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
// ...but where it CAN see the whole capture - no geometry stage - the frontend's
// own count is the desktop-exact one and the ES driver's is only as good as the
// vendor made it (Adreno doubles PRIMITIVES_WRITTEN for a vertex-only capture that
// follows a large render pass). The query above stays installed: it is still what
// answers an amplifying span, and PRIMITIVES_GENERATED always.
funcsTable.GL.PrefersCpuXfbPrimitiveAccounting = true;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable; funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64; funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery; funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
@@ -1098,6 +1339,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BackendObject_DirectGLES::UpdateDynamicBackendParameters() { void BackendObject_DirectGLES::UpdateDynamicBackendParameters() {
m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment; m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment;
m_dynamicParameters.ShaderStorageBufferOffsetAlignment =
m_GLESCapabilities.ShaderStorageBufferOffsetAlignment;
m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy; m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy;
m_dynamicParameters.AliasedLineWidthRangeMin = m_GLESCapabilities.AliasedLineWidthRangeMin; m_dynamicParameters.AliasedLineWidthRangeMin = m_GLESCapabilities.AliasedLineWidthRangeMin;
m_dynamicParameters.AliasedLineWidthRangeMax = m_GLESCapabilities.AliasedLineWidthRangeMax; m_dynamicParameters.AliasedLineWidthRangeMax = m_GLESCapabilities.AliasedLineWidthRangeMax;
@@ -1148,9 +1391,48 @@ namespace MobileGL::MG_Backend::DirectGLES {
static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS)); static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_GLESCapabilities.MaxComputeShaderStorageBlocks; m_dynamicParameters.MaxComputeShaderStorageBlocks = m_GLESCapabilities.MaxComputeShaderStorageBlocks;
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_GLESCapabilities.MaxCombinedShaderStorageBlocks; m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_GLESCapabilities.MaxCombinedShaderStorageBlocks;
// Per-stage storage-block counts, forwarded from the host driver rather than invented.
// A stage the driver cannot serve reports 0, which is a legal answer everywhere these
// limits appear (GL 4.6 table 23.64, ES 3.2 table 21.44 - the minimum is 0 for every
// graphics stage except fragment) and is the only answer that lets an application take
// its own fallback instead of building a program the driver will refuse to link. The
// stage limit cannot exceed the combined limit or the number of binding points there
// are to bind buffers to, so clamp to both.
const auto clampStageStorageBlocks = [this](Int stageLimit) {
return std::min({std::max(stageLimit, 0), std::max(m_dynamicParameters.MaxCombinedShaderStorageBlocks, 0),
std::max(m_dynamicParameters.MaxShaderStorageBufferBindings, 0)});
};
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings;
m_dynamicParameters.MaxVertexShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxVertexShaderStorageBlocks);
m_dynamicParameters.MaxTessControlShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxTessControlShaderStorageBlocks);
m_dynamicParameters.MaxTessEvaluationShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxTessEvaluationShaderStorageBlocks);
m_dynamicParameters.MaxGeometryShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxGeometryShaderStorageBlocks);
m_dynamicParameters.MaxFragmentShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxFragmentShaderStorageBlocks);
m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks; m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks;
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations; m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings; // The six per-axis compute limits: the driver's raw glGetIntegeri_v answers, the same
// numbers GLFunctionsTable::GetIntegeri_v forwards live. Carried here so that MGPCaps has
// them once the table entry retires (plan B section 4.4.1); GL_Getter floors them.
for (SizeT axis = 0; axis < 3; ++axis) {
m_dynamicParameters.MaxComputeWorkGroupCount[axis] = m_GLESCapabilities.MaxComputeWorkGroupCount[axis];
m_dynamicParameters.MaxComputeWorkGroupSize[axis] = m_GLESCapabilities.MaxComputeWorkGroupSize[axis];
}
// (MaxShaderStorageBufferBindings is assigned above, before the per-stage clamp reads it.)
// This is the number glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE) hands the application, and
// on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather
// than a driver answer (m_GLESCapabilities.MaxTextureBufferSizeIsDriverReported says
// which). Reporting 0 instead was considered and rejected: MobileGL advertises an OpenGL
// 4.x context, where buffer textures are core and the limit has a spec minimum of 65536,
// so 0 is not a legal answer and applications are not written to survive it. GL offers no
// way to say "this core feature is missing", so the honesty is carried outside the limit:
// FillInGLESCapabilities logs the tier, glTexBuffer and the program build each name the
// missing capability at MGLOG_I, and the driver POST carries a "Buffer textures" row that
// FAILs on this tier.
m_dynamicParameters.MaxTextureBufferSize = m_GLESCapabilities.MaxTextureBufferSize; m_dynamicParameters.MaxTextureBufferSize = m_GLESCapabilities.MaxTextureBufferSize;
m_dynamicParameters.TextureBufferOffsetAlignment = m_GLESCapabilities.TextureBufferOffsetAlignment; m_dynamicParameters.TextureBufferOffsetAlignment = m_GLESCapabilities.TextureBufferOffsetAlignment;
m_dynamicParameters.MaxUniformBufferBindings = m_GLESCapabilities.MaxUniformBufferBindings; m_dynamicParameters.MaxUniformBufferBindings = m_GLESCapabilities.MaxUniformBufferBindings;
@@ -1193,14 +1475,61 @@ namespace MobileGL::MG_Backend::DirectGLES {
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray); DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
} }
} }
// Not a driver question and never will be: OpenGL ES has no double-precision vertex format // Not a driver question and never will be: GLSL ES has no 64-bit float type in ANY version
// and ESSL has no fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to // or extension, so SPIRV-Cross cannot emit one ("FP64 not supported in ES profile") and a
// land on this backend regardless of what the driver underneath happens to support. // module that still declared Float64 would never reach the driver at all. The demotion is
// mathematically mandatory here, on every device, forever - which is why this stays false
// regardless of what the driver underneath happens to support.
m_dynamicParameters.SupportsShaderFloat64 = false;
// Follows the line above, and must: OpenGL ES has no double-precision vertex format and no
// fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to land here.
m_dynamicParameters.SupportsFloat64VertexAttributes = false; m_dynamicParameters.SupportsFloat64VertexAttributes = false;
// Whether a tessellation / geometry stage's ESSL may name gl_PointSize at all: the two
// extension pairs the loader probed, independently, because they really do come
// separately. False arms the shared phase-B demotion
// (ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram), whose ESSL then
// never names the built-in in those stages and needs no extension.
// MOBILEGL_POINT_SIZE_DEMOTION=1 pretends both are absent so the demotion can be
// exercised on a healthy driver (the pinned integration lane); =0 restores the
// detected answer's declines.
m_dynamicParameters.SupportsTessellationPointSize =
m_GLESCapabilities.TessellationPointSizeSupport !=
MG_External::GLESCapabilities::PointSizeTier::None;
m_dynamicParameters.SupportsGeometryPointSize =
m_GLESCapabilities.GeometryPointSizeSupport !=
MG_External::GLESCapabilities::PointSizeTier::None;
switch (MG_Config::Features.PointSizeDemotion) {
case MG_Config::QuirkOverride::ForceOn:
MGLOG_I("DirectGLES: MOBILEGL_POINT_SIZE_DEMOTION=1 - treating tessellation/geometry "
"gl_PointSize as unhosted so the demotion runs on this driver");
m_dynamicParameters.SupportsTessellationPointSize = false;
m_dynamicParameters.SupportsGeometryPointSize = false;
break;
case MG_Config::QuirkOverride::ForceOff:
MGLOG_I("DirectGLES: MOBILEGL_POINT_SIZE_DEMOTION=0 - keeping the built-in and the "
"plain declines regardless of the driver's extensions");
m_dynamicParameters.SupportsTessellationPointSize = true;
m_dynamicParameters.SupportsGeometryPointSize = true;
break;
case MG_Config::QuirkOverride::Auto:
break;
}
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers; m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments; m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances; m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
// The loader already gated both on GL_EXT_clip_cull_distance and left 0 without it, which
// is the answer that keeps glslang from accepting a gl_CullDistance the ESSL compiler
// would reject.
m_dynamicParameters.MaxCullDistances = m_GLESCapabilities.MaxCullDistances;
m_dynamicParameters.MaxCombinedClipAndCullDistances = m_GLESCapabilities.MaxCombinedClipAndCullDistances;
m_dynamicParameters.MaxViewports = m_GLESCapabilities.MaxViewports; m_dynamicParameters.MaxViewports = m_GLESCapabilities.MaxViewports;
// Whatever the driver said about which vertex supplies gl_Layer, and GL_UNDEFINED_VERTEX
// for gl_ViewportIndex on every driver without GL_OES_viewport_array - which is both test
// devices. That is not a shortfall being hidden: without the extension only viewport 0 is
// ever rasterized, so no vertex "selects" a viewport index and naming a convention would
// describe behaviour this backend does not implement.
m_dynamicParameters.LayerProvokingVertex = m_GLESCapabilities.LayerProvokingVertex;
m_dynamicParameters.ViewportIndexProvokingVertex = m_GLESCapabilities.ViewportIndexProvokingVertex;
m_dynamicParameters.MaxViewportWidth = m_GLESCapabilities.MaxViewportWidth; m_dynamicParameters.MaxViewportWidth = m_GLESCapabilities.MaxViewportWidth;
m_dynamicParameters.MaxViewportHeight = m_GLESCapabilities.MaxViewportHeight; m_dynamicParameters.MaxViewportHeight = m_GLESCapabilities.MaxViewportHeight;
m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin; m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
@@ -18,6 +18,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
const MG_External::GLESCapabilities& capabilities, const MG_External::GLESCapabilities& capabilities,
FormatCapabilityCache& cache); FormatCapabilityCache& cache);
// Clamps a requested sample count down to what the ES driver can really deliver for this
// format on this format-capability target: the probed per-format list when there is one, the
// driver's per-class GL_MAX_*_SAMPLES otherwise. The frontend deliberately validates against
// the count MobileGL advertises instead (GL_Getter's GetAdvertisedMaxSamples), which on a
// driver reporting GL_MAX_INTEGER_SAMPLES 1 is higher than the driver accepts, so every ES
// allocation call has to come through here. The shadow state keeps the requested count, so
// GL_TEXTURE_SAMPLES and framebuffer completeness still answer what the application asked for.
Int ClampSamplesToBackendSupport(SizeT targetIndex, TextureInternalFormat logicalFormat, GLenum imageFormat,
Int samples);
class BackendObject_DirectGLES : public BackendObject { class BackendObject_DirectGLES : public BackendObject {
public: public:
~BackendObject_DirectGLES() override; ~BackendObject_DirectGLES() override;
@@ -67,9 +77,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
const RendererInfo& GetRendererIdentity(); const RendererInfo& GetRendererIdentity();
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS)) // The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
// for a device whose timer queries / anisotropic filtering are (or are not) usable. // for a device whose timer queries / anisotropic filtering / native indirect draws /
// non-zero indirect baseInstance semantics / EXT-OES texture views are (or are not) usable.
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside. // The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported); Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
Bool drawIndirectSupported,
Bool nonZeroIndirectBaseInstanceSupported,
Bool textureViewSupported, Bool cubeMapArraySupported);
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an // Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
// initialized backend returns from GetBackendAPIVersionString (and that ends up // initialized backend returns from GetBackendAPIVersionString (and that ends up
File diff suppressed because it is too large Load Diff
+22 -4
View File
@@ -40,6 +40,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride); GLsizei maxdrawcount, GLsizei stride);
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride); void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount,
GLsizei stride);
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices, GLint basevertex); const void* indices, GLint basevertex);
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices); void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices);
@@ -74,9 +76,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLsizei height, GLint border); GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height); GLsizei height);
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, void CopyImageSubData(const CopyImageEndpoint& src,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, const CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth); GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target); void GenerateMipmap(GLenum target);
@@ -90,8 +92,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
GLenum format); GLenum format);
void GetIntegeri_v(GLenum target, GLuint index, GLint* data); void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding); void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
Bool InitWindowSurface(NativeWindowType window); Bool InitWindowSurface(NativeWindowType window);
Bool InitPbufferSurface(EGLint width, EGLint height); Bool InitPbufferSurface(EGLint width, EGLint height);
@@ -117,6 +117,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
// capability read needs no current ES context, and it stays false until // capability read needs no current ES context, and it stays false until
// the ES capabilities have been filled in. // the ES capabilities have been filled in.
Bool AreTimerQueriesSupported(); Bool AreTimerQueriesSupported();
// True when the host ES driver can back a GL_TEXTURE_BUFFER at all - ES 3.2 core, or
// EXT/OES_texture_buffer, with glTexBuffer resolved. Desktop GL has had buffer textures as
// core since 3.1, so the frontend advertises them unconditionally and an app may call
// glTexBuffer whenever it likes; this is the only thing standing between that call and a
// null entry point. False also means every shader declaring a samplerBuffer is
// uncompilable on this driver, which the program build reports by name.
Bool AreBufferTexturesSupported();
// Human-readable name of the buffer-texture tier for diagnostics and the driver POST:
// "core (ES 3.2)", "GL_EXT_texture_buffer", "GL_OES_texture_buffer" or "unsupported".
const char* GetBufferTextureTierName();
// glTexBuffer / glTexBufferRange through whichever spelling this driver's buffer-texture
// support actually ships: the unsuffixed names are ES 3.2 core, while an EXT/OES driver
// exports glTexBuffer{,Range}EXT / OES. Callers must have checked
// AreBufferTexturesSupported() first. CallTexBufferRange reports whether it could honour
// the range - no tier is required to expose the range form, and the whole-buffer form is
// the documented fallback.
void CallTexBuffer(GLenum target, GLenum internalFormat, GLuint buffer);
Bool CallTexBufferRange(GLenum target, GLenum internalFormat, GLuint buffer, GLintptr offset, GLsizeiptr size);
// GL timer-query objects, backed by GL_EXT_disjoint_timer_query. The // GL timer-query objects, backed by GL_EXT_disjoint_timer_query. The
// creators return null (the frontend then falls back to an immediately // creators return null (the frontend then falls back to an immediately
// available zero result) when the calling thread does not own the ES // available zero result) when the calling thread does not own the ES
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+150 -52
View File
@@ -9,6 +9,8 @@
#include "MultiDraw.h" #include "MultiDraw.h"
#include "Managers.h" #include "Managers.h"
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_Pipe/PipeInputsSwitch.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <cstring> #include <cstring>
#include <limits> #include <limits>
@@ -29,21 +31,26 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
} }
} }
// The all-ones value of an index type, which is what GL restarts on once // The index value this batch restarts on, compared at 32 bits against the zero-extended
// primitive restart is in play. CheckPrimitiveRestartSupported has already // source index. Normally the all-ones value of the source type, which is what
// rejected the arbitrary-index form of GL_PRIMITIVE_RESTART, so an enabled // GL_PRIMITIVE_RESTART_FIXED_INDEX and GLES both restart on; with desktop
// restart always restarts here and nowhere else. // GL_PRIMITIVE_RESTART it is instead whatever glPrimitiveRestartIndex named. The rebased
// tier turns whichever it is into 0xFFFFFFFF in its widened stream, which is what the
// driver restarts on.
//
// No truncation, deliberately, and the same rule ResolveRestartSubstitution applies: a
// restart index the source type cannot hold simply matches nothing, so returning it
// verbatim is already "this batch restarts nowhere".
Uint32 RestartSentinelFor(GLenum type) { Uint32 RestartSentinelFor(GLenum type) {
switch (type) { if (ResolveRestartSubstitution(type) != RestartSubstitutionKind::None) {
case GL_UNSIGNED_BYTE: return 0xFFu; return MGB_CTX->GetPrimitiveRestartIndex();
case GL_UNSIGNED_SHORT: return 0xFFFFu;
default: return 0xFFFFFFFFu;
} }
return MG_Util::FixedRestartIndexForGLType(type);
} }
Bool RestartActive() { Bool RestartActive() {
return MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) || return MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex); MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
} }
// Vertices per primitive for the modes whose sub-draws may be concatenated into a // Vertices per primitive for the modes whose sub-draws may be concatenated into a
@@ -78,7 +85,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
Uint BoundDrawIndirectBufferId() { Uint BoundDrawIndirectBufferId() {
const auto& indirect = const auto& indirect =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (!indirect) return 0; if (!indirect) return 0;
const auto* resource = BufferImpl::EnsureBufferResource(indirect); const auto* resource = BufferImpl::EnsureBufferResource(indirect);
return resource ? resource->id : 0; return resource ? resource->id : 0;
@@ -86,7 +93,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
const SharedPtr<MG_State::GLState::BufferObject>& BoundIndexBuffer() { const SharedPtr<MG_State::GLState::BufferObject>& BoundIndexBuffer() {
static const SharedPtr<MG_State::GLState::BufferObject> none; static const SharedPtr<MG_State::GLState::BufferObject> none;
const auto& vao = MG_State::pGLContext->GetBoundVertexArray(); const auto& vao = MGB_CTX->GetBoundVertexArray();
if (!vao) return none; if (!vao) return none;
return vao->GetIndexBufferBindingSlot().GetBoundObject(); return vao->GetIndexBufferBindingSlot().GetBoundObject();
} }
@@ -151,7 +158,10 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// are bound as storage blocks. Respecifies rather than sub-updates: glBufferData // are bound as storage blocks. Respecifies rather than sub-updates: glBufferData
// orphans the previous store, so the upload never waits on a dispatch still reading // orphans the previous store, so the upload never waits on a dispatch still reading
// the old contents out of the same name. // the old contents out of the same name.
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data) { // statsClass: which MGPipe byte population these bytes belong to. Counted here
// rather than at the four call sites so a new tier cannot forget it.
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data,
MG_Util::PipeStats::ByteClass statsClass) {
if (bytes == 0) return true; if (bytes == 0) return true;
if (!EnsureScratchName(buffer)) return false; if (!EnsureScratchName(buffer)) return false;
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id); BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id);
@@ -164,6 +174,9 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
buffer.cursor = 0; buffer.cursor = 0;
if (data) { if (data) {
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast<GLsizeiptr>(bytes), data); g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast<GLsizeiptr>(bytes), data);
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(statsClass, static_cast<Uint64>(bytes));
}
} }
return true; return true;
} }
@@ -178,7 +191,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
constexpr SizeT kRingAlignment = 16; // >= 4, so both command and uint32-index offsets stay legal constexpr SizeT kRingAlignment = 16; // >= 4, so both command and uint32-index offsets stay legal
constexpr SizeT kMinRingBytes = 1u << 16; constexpr SizeT kMinRingBytes = 1u << 16;
Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data, SizeT& outOffset) { Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data,
MG_Util::PipeStats::ByteClass statsClass, SizeT& outOffset) {
outOffset = 0; outOffset = 0;
if (bytes == 0) return true; if (bytes == 0) return true;
if (!EnsureScratchName(buffer)) return false; if (!EnsureScratchName(buffer)) return false;
@@ -202,6 +216,9 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
if (data) { if (data) {
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, static_cast<GLintptr>(outOffset), g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, static_cast<GLintptr>(outOffset),
static_cast<GLsizeiptr>(bytes), data); static_cast<GLsizeiptr>(bytes), data);
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(statsClass, static_cast<Uint64>(bytes));
}
} }
buffer.cursor += aligned; buffer.cursor += aligned;
return true; return true;
@@ -252,7 +269,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
g_resolvedTier = g_resolvedTier =
ResolveTier(g_GLESCapabilities, g_GLESFuncs, MG_Config::Features.EsprytMultiDrawMode, ResolveTier(g_GLESCapabilities, g_GLESFuncs, MG_Config::Features.EsprytMultiDrawMode,
&g_tierResolution); &g_tierResolution);
MGLOG_I("DirectGLES multi-draw: %s", g_tierResolution.c_str()); MGLOG_D("DirectGLES multi-draw: %s", g_tierResolution.c_str());
} }
// Which tiers have already announced themselves, one bit per GLESMultiDrawMode. // Which tiers have already announced themselves, one bit per GLESMultiDrawMode.
@@ -267,24 +284,39 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
const Uint32 bit = 1u << static_cast<Uint32>(tier); const Uint32 bit = 1u << static_cast<Uint32>(tier);
if (g_announcedTiers & bit) return; if (g_announcedTiers & bit) return;
g_announcedTiers |= bit; g_announcedTiers |= bit;
MGLOG_I("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier)); MGLOG_D("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier));
} }
// The tier this particular batch can actually take. A tier is demoted here when // The tier this particular batch can actually take. A tier is demoted here when
// the batch's own shape - not the driver - rules it out; the compute tier keeps // the batch's own shape - not the driver - rules it out; the compute tier keeps
// its remaining feasibility checks inside its implementation, where the data it // its remaining feasibility checks inside its implementation, where the data it
// has to walk is already in hand. // has to walk is already in hand.
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool hasIndexBuffer) { GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool perSubDrawBaseVertex,
Bool hasIndexBuffer, Bool arbitraryRestart) {
ResolveTierOnce(); ResolveTierOnce();
GLESMultiDrawMode tier = g_resolvedTier; GLESMultiDrawMode tier = g_resolvedTier;
// Desktop GL_PRIMITIVE_RESTART restarts on an application-chosen index; the driver
// only ever restarts on the all-ones value. Every tier but the rebased one hands
// the application's own index data to the driver, which would then see no restarts
// at all and weld the primitives together. The rebased tier is the one that
// REWRITES the stream, and RestartSentinelFor already tells it which value to
// translate, so it is the only tier this batch can take.
if (arbitraryRestart) {
return GLESMultiDrawMode::DrawElements;
}
// Batched tiers issue one driver entry for the whole batch, so the emulated // Batched tiers issue one driver entry for the whole batch, so the emulated
// gl_DrawID uniform can only hold one value across every sub-draw. A program // gl_DrawID uniform can only hold one value across every sub-draw. A program
// that reads gl_DrawID gets an unrolled tier, which feeds each sub-draw its // that reads gl_DrawID gets an unrolled tier, which feeds each sub-draw its
// own index (the spec's value); nothing else observes the difference. // own index (the spec's value); nothing else observes the difference. The
// emulated gl_BaseVertex is one uniform for the same reason, so a batch whose
// sub-draws carry their own base vertices unrolls too - even the Ext tier,
// which hands the driver the whole basevertex array, can only leave ONE value
// in the uniform the shader reads.
const Bool batched = tier == GLESMultiDrawMode::Ext || tier == GLESMultiDrawMode::MultiIndirect || const Bool batched = tier == GLESMultiDrawMode::Ext || tier == GLESMultiDrawMode::MultiIndirect ||
tier == GLESMultiDrawMode::Compute; tier == GLESMultiDrawMode::Compute;
if (batched && programReadsDrawID) { if (batched && (programReadsDrawID || perSubDrawBaseVertex)) {
tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex
: GLESMultiDrawMode::DrawElements; : GLESMultiDrawMode::DrawElements;
} }
@@ -371,7 +403,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
Bool RunIndirect(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, Bool RunIndirect(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex, Bool batched, Bool feedDrawID) { GLsizei drawcount, const GLint* basevertex, Bool batched, Bool feedDrawID,
Bool feedBaseVertex) {
if (!SupportsTier(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect)) return false; if (!SupportsTier(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect)) return false;
const SizeT indexSize = IndexTypeSize(type); const SizeT indexSize = IndexTypeSize(type);
if (indexSize == 0) return false; if (indexSize == 0) return false;
@@ -396,7 +429,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
const SizeT commandBytes = g_commandStaging.size() * sizeof(DrawElementsIndirectCommand); const SizeT commandBytes = g_commandStaging.size() * sizeof(DrawElementsIndirectCommand);
SizeT commandBase = 0; SizeT commandBase = 0;
if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(), commandBase)) { if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(),
MG_Util::PipeStats::ByteClass::StageIndirectCmd, commandBase)) {
return false; return false;
} }
@@ -408,15 +442,21 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
const Uint previousIndirectBinding = BoundDrawIndirectBufferId(); const Uint previousIndirectBinding = BoundDrawIndirectBufferId();
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, g_indirectCommands.id); BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, g_indirectCommands.id);
if (batched) { if (batched) {
g_GLESFuncs.glMultiDrawElementsIndirectEXT(mode, type, reinterpret_cast<const void*>(commandBase), ForEachViewportRoutingPass([&] {
drawcount, 0); g_GLESFuncs.glMultiDrawElementsIndirectEXT(mode, type, reinterpret_cast<const void*>(commandBase),
drawcount, 0);
});
} else { } else {
for (GLsizei i = 0; i < drawcount; ++i) { for (GLsizei i = 0; i < drawcount; ++i) {
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i)); if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
const SizeT commandOffset = commandBase + static_cast<SizeT>(i) * sizeof(DrawElementsIndirectCommand); const SizeT commandOffset = commandBase + static_cast<SizeT>(i) * sizeof(DrawElementsIndirectCommand);
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset)); ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
});
} }
if (feedDrawID) SetCurrentDrawID(0); if (feedDrawID) SetCurrentDrawID(0);
if (feedBaseVertex) SetCurrentBaseVertex(0);
} }
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, previousIndirectBinding); BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, previousIndirectBinding);
NoteTierExecuted(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect); NoteTierExecuted(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect);
@@ -428,15 +468,19 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
Bool RunBaseVertexLoop(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, Bool RunBaseVertexLoop(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID) { GLsizei drawcount, const GLint* basevertex, Bool feedDrawID, Bool feedBaseVertex) {
if (!SupportsTier(GLESMultiDrawMode::BaseVertex)) return false; if (!SupportsTier(GLESMultiDrawMode::BaseVertex)) return false;
for (GLsizei i = 0; i < drawcount; ++i) { for (GLsizei i = 0; i < drawcount; ++i) {
if (count[i] <= 0) continue; if (count[i] <= 0) continue;
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i)); if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i], if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
basevertex ? basevertex[i] : 0); ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
basevertex ? basevertex[i] : 0);
});
} }
if (feedDrawID) SetCurrentDrawID(0); if (feedDrawID) SetCurrentDrawID(0);
if (feedBaseVertex) SetCurrentBaseVertex(0);
NoteTierExecuted(GLESMultiDrawMode::BaseVertex); NoteTierExecuted(GLESMultiDrawMode::BaseVertex);
return true; return true;
} }
@@ -446,7 +490,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
Bool RunRebasedDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, Bool RunRebasedDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID) { GLsizei drawcount, const GLint* basevertex, Bool feedDrawID,
Bool feedBaseVertex) {
const SizeT indexSize = IndexTypeSize(type); const SizeT indexSize = IndexTypeSize(type);
if (indexSize == 0) return false; if (indexSize == 0) return false;
@@ -471,6 +516,16 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
const Bool restartActive = RestartActive(); const Bool restartActive = RestartActive();
const Uint32 restartSentinel = RestartSentinelFor(type); const Uint32 restartSentinel = RestartSentinelFor(type);
// Widening to GL_UNSIGNED_INT gives a UBYTE/USHORT source a sentinel it can never
// spell, so those batches are lossless. A UINT source that already uses 0xFFFFFFFF as
// a real vertex index while restarting on a different one is the one shape 32 bits
// cannot express - the same corner the single-draw substitution reports.
if (restartActive && indexSize == 4 && restartSentinel != 0xFFFFFFFFu) {
MGLOG_E_ONCE("GL_PRIMITIVE_RESTART with restart index %u over GL_UNSIGNED_INT multi-draw indices: "
"any index that is already 0xFFFFFFFF will restart too, because the rewritten stream "
"has no wider sentinel to move to.",
restartSentinel);
}
g_indexStaging.resize(total); g_indexStaging.resize(total);
SizeT cursor = 0; SizeT cursor = 0;
for (GLsizei i = 0; i < drawcount; ++i) { for (GLsizei i = 0; i < drawcount; ++i) {
@@ -479,7 +534,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
const Uint8* source = ResolveSubDrawIndices(indexBuffer, indexBufferBytes, indexBufferSize, indices[i], const Uint8* source = ResolveSubDrawIndices(indexBuffer, indexBufferBytes, indexBufferSize, indices[i],
subDrawCount, indexSize); subDrawCount, indexSize);
if (!source) { if (!source) {
MGLOG_E("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index " MGLOG_E_ONCE("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
"buffer; skipping the batch", "buffer; skipping the batch",
i); i);
return false; return false;
@@ -490,7 +545,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
} }
SizeT indexBase = 0; SizeT indexBase = 0;
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(), indexBase)) { if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(),
MG_Util::PipeStats::ByteClass::StageIndexClient, indexBase)) {
return false; return false;
} }
@@ -500,11 +556,18 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
for (GLsizei i = 0; i < drawcount; ++i) { for (GLsizei i = 0; i < drawcount; ++i) {
if (count[i] <= 0) continue; if (count[i] <= 0) continue;
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i)); if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT, // The base vertex is folded into the rewritten index stream here, so the
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32))); // driver sees none - but gl_BaseVertex still has to report the value the
// application passed for this sub-draw.
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
});
cursor += static_cast<SizeT>(count[i]); cursor += static_cast<SizeT>(count[i]);
} }
if (feedDrawID) SetCurrentDrawID(0); if (feedDrawID) SetCurrentDrawID(0);
if (feedBaseVertex) SetCurrentBaseVertex(0);
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding); BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
NoteTierExecuted(GLESMultiDrawMode::DrawElements); NoteTierExecuted(GLESMultiDrawMode::DrawElements);
return true; return true;
@@ -580,7 +643,7 @@ void main() {
const GLuint shader = g_GLESFuncs.glCreateShader(GL_COMPUTE_SHADER); const GLuint shader = g_GLESFuncs.glCreateShader(GL_COMPUTE_SHADER);
if (shader == 0) { if (shader == 0) {
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed"); MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed");
return false; return false;
} }
const char* source = kFlattenComputeSource; const char* source = kFlattenComputeSource;
@@ -591,14 +654,14 @@ void main() {
if (status != GL_TRUE) { if (status != GL_TRUE) {
char log[1024] = {}; char log[1024] = {};
g_GLESFuncs.glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log); g_GLESFuncs.glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log); MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log);
g_GLESFuncs.glDeleteShader(shader); g_GLESFuncs.glDeleteShader(shader);
return false; return false;
} }
const GLuint program = g_GLESFuncs.glCreateProgram(); const GLuint program = g_GLESFuncs.glCreateProgram();
if (program == 0) { if (program == 0) {
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateProgram failed"); MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): glCreateProgram failed");
g_GLESFuncs.glDeleteShader(shader); g_GLESFuncs.glDeleteShader(shader);
return false; return false;
} }
@@ -609,7 +672,7 @@ void main() {
if (status != GL_TRUE) { if (status != GL_TRUE) {
char log[1024] = {}; char log[1024] = {};
g_GLESFuncs.glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log); g_GLESFuncs.glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log); MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log);
g_GLESFuncs.glDeleteProgram(program); g_GLESFuncs.glDeleteProgram(program);
return false; return false;
} }
@@ -619,7 +682,7 @@ void main() {
g_uDrawCount = g_GLESFuncs.glGetUniformLocation(program, "uDrawCount"); g_uDrawCount = g_GLESFuncs.glGetUniformLocation(program, "uDrawCount");
g_uTotalIndices = g_GLESFuncs.glGetUniformLocation(program, "uTotalIndices"); g_uTotalIndices = g_GLESFuncs.glGetUniformLocation(program, "uTotalIndices");
g_computeProgramFailed = false; g_computeProgramFailed = false;
MGLOG_I("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program); MGLOG_D("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program);
return true; return true;
} }
@@ -688,10 +751,16 @@ void main() {
if (total == 0) return; // nothing to draw; the ordinary tiers no-op just as well if (total == 0) return; // nothing to draw; the ordinary tiers no-op just as well
if (!EnsureComputeProgram()) return; if (!EnsureComputeProgram()) return;
if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data())) { if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data(),
MG_Util::PipeStats::ByteClass::StageIndirectCmd)) {
return;
}
// data == nullptr: pure respecify, the compute pass writes the contents, so no
// host bytes cross here and nothing is counted.
if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr,
MG_Util::PipeStats::ByteClass::StageIndexClient)) {
return; return;
} }
if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr)) return;
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 0, sourceResource->id); BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 0, sourceResource->id);
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 1, g_drawInfo.id); BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 1, g_drawInfo.id);
@@ -828,8 +897,14 @@ void main() {
void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex) { GLsizei drawcount, const GLint* basevertex) {
if (drawcount <= 0 || !count || !indices) return; if (drawcount <= 0 || !count || !indices) return;
// State-independent and possibly throwing, so it runs before any GL work. // Read before any GL work, because it decides the tier below: a desktop restart index
CheckPrimitiveRestartSupported(type); // the driver does not know about can only be honoured by the tier that rewrites the
// index stream (see ResolveTierForBatch). A restart index this index type cannot hold
// needs no rewrite at all - nothing can match it - but it does need the driver's own
// fixed-index restart held off for the batch, which is what the scope below does.
const RestartSubstitutionKind restartKind = ResolveRestartSubstitution(type);
const Bool arbitraryRestart = restartKind == RestartSubstitutionKind::RewriteIndices;
const ScopedSuppressedPrimitiveRestart restartCapOverride(restartKind);
const Bool hasIndexBuffer = BoundIndexBuffer() != nullptr; const Bool hasIndexBuffer = BoundIndexBuffer() != nullptr;
@@ -837,8 +912,15 @@ void main() {
// afterwards would mean unpicking the program, SSBO and index bindings // afterwards would mean unpicking the program, SSBO and index bindings
// PrepareForDraw just made, and a dispatch inside an open transform feedback // PrepareForDraw just made, and a dispatch inside an open transform feedback
// span is not legal at all. On success it hands back a flattened index stream. // span is not legal at all. On success it hands back a flattened index stream.
// A batch whose sub-draws carry their own base vertices cannot be flattened either
// when the program reads gl_BaseVertex: one draw call leaves one uniform value.
// Asked conservatively because this decision precedes PrepareForDraw - see
// CurrentProgramMayNeedPerSubDrawBuiltins. Flattening is the irreversible half:
// once the batch is one draw the values are gone, whereas declining to flatten only
// costs the unrolled tier.
FlattenedStream flattened; FlattenedStream flattened;
if (ResolvedTier() == GLESMultiDrawMode::Compute && !CurrentProgramReadsDrawID()) { if (ResolvedTier() == GLESMultiDrawMode::Compute &&
!CurrentProgramMayNeedPerSubDrawBuiltins(basevertex != nullptr)) {
FlattenWithCompute(mode, count, type, indices, drawcount, basevertex, flattened); FlattenWithCompute(mode, count, type, indices, drawcount, basevertex, flattened);
} }
@@ -847,13 +929,19 @@ void main() {
if (flattened.indexCount != 0) { if (flattened.indexCount != 0) {
const Uint previousIndexBinding = BoundIndexBufferId(); const Uint previousIndexBinding = BoundIndexBufferId();
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, flattened.bufferId); BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, flattened.bufferId);
g_GLESFuncs.glDrawElements(mode, static_cast<GLsizei>(flattened.indexCount), GL_UNSIGNED_INT, nullptr); ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElements(mode, static_cast<GLsizei>(flattened.indexCount), GL_UNSIGNED_INT, nullptr);
});
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding); BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
return; return;
} }
// Now that PrepareForDraw has synced the program, both questions have real answers;
// the tier choice and the per-sub-draw feeds use those, not the guess above.
const Bool feedDrawID = CurrentProgramReadsDrawID(); const Bool feedDrawID = CurrentProgramReadsDrawID();
const GLESMultiDrawMode tier = ResolveTierForBatch(feedDrawID, hasIndexBuffer); const Bool feedBaseVertex = basevertex != nullptr && CurrentProgramReadsBaseVertex();
const GLESMultiDrawMode tier =
ResolveTierForBatch(feedDrawID, feedBaseVertex, hasIndexBuffer, arbitraryRestart);
Bool drawn = false; Bool drawn = false;
switch (tier) { switch (tier) {
@@ -861,16 +949,19 @@ void main() {
drawn = RunExt(mode, count, type, indices, drawcount, basevertex); drawn = RunExt(mode, count, type, indices, drawcount, basevertex);
break; break;
case GLESMultiDrawMode::MultiIndirect: case GLESMultiDrawMode::MultiIndirect:
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/true, feedDrawID); drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/true, feedDrawID,
feedBaseVertex);
break; break;
case GLESMultiDrawMode::Indirect: case GLESMultiDrawMode::Indirect:
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/false, feedDrawID); drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/false, feedDrawID,
feedBaseVertex);
break; break;
case GLESMultiDrawMode::BaseVertex: case GLESMultiDrawMode::BaseVertex:
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID); drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
break; break;
case GLESMultiDrawMode::DrawElements: case GLESMultiDrawMode::DrawElements:
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID); drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID,
feedBaseVertex);
break; break;
case GLESMultiDrawMode::Compute: case GLESMultiDrawMode::Compute:
// Its pre-pass ran above; reaching here means it declined this batch's shape. // Its pre-pass ran above; reaching here means it declined this batch's shape.
@@ -882,11 +973,18 @@ void main() {
// Every tier above may decline a batch whose shape it cannot express. The two // Every tier above may decline a batch whose shape it cannot express. The two
// below are the floor: a base-vertex replay where the driver has one, and the // below are the floor: a base-vertex replay where the driver has one, and the
// rewritten index stream where it does not. Both are safe for any batch these // rewritten index stream where it does not. Both are safe for any batch these
// entry points can receive. // entry points can receive - except that the base-vertex replay hands the
if (!drawn) drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID); // application's own indices to the driver, which cannot restart on a desktop
if (!drawn) drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID); // restart index, so that batch has only the rewriting floor.
if (!drawn && !arbitraryRestart) {
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
}
if (!drawn) { if (!drawn) {
MGLOG_E("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); " drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID,
feedBaseVertex);
}
if (!drawn) {
MGLOG_E_ONCE("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
"the batch was dropped", "the batch was dropped",
drawcount, mode, type); drawcount, mode, type);
} }
+577
View File
@@ -0,0 +1,577 @@
// MobileGL - MobileGL/MG_Backend/DirectGLES/SlotTables.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include <MG_Pipe/MGPipeHandles.h>
#if MOBILEGL_PIPE_PUSH
#include <MG_Impl/Pipe/SlotAllocator.h>
#endif
// Espryt 0b, the first Track H slice: the DENSE, {slot, gen}-keyed twin table that replaces
// StateBackendObjectRegistry's UnorderedMap<StateObject*, Entry>.
//
// What changes, and why each of them is the point:
//
// * The KEY stops being a frontend heap address. It is MGPipeHandle{Slot, Gen}, minted by the
// client's MGPipeSlotAllocator off the frontend object's GetLifetimeId(). A recycled heap
// address cannot reproduce a handle, so the weak_ptr the registry carried per entry purely
// to catch that (its Entry::stateRef, used as an IDENTITY test) stops being an identity
// mechanism, and OwnerEquals / TwinLookupMemo x3 / UnitSamplerLookupMemo's owner compare all
// lose their reason to exist.
// * The lookup stops being a hash probe into an open-addressed map and becomes one bounds
// check plus one array index, so a returned BackendPtr* is NOT invalidated by the next Find
// on the table. That kills the hazard Managers.h documents at length, and with it the
// by-value copy plus second Find that SyncTextureObjectToBackend paid to survive it.
// * Slots are dense per kind, which is what lets the server side (ARCHITECTURE.md 10.1,
// MG_Remote/Server/PipeObjectTables) be an array rather than an object graph.
//
// Death is ANNOUNCED, and that is what lets this table have no garbage collector - the
// deliverable ROADMAP.md:18 spells "GC" in and the one D13 makes a precondition of the switch-
// over. All six re-keyed object classes raise MG_State::GLState::NotifyStateObjectDestroyed()
// from their destructor (BufferBackendOps' shape, one entry point for six kinds), the backend
// consumes it in Managers.cpp, and OnFrontendObjectDestroyed() below drops the twin in EVERY
// table of the kind and returns the slot, at the moment the frontend object's last SharedPtr
// goes. So:
// * there is NO draw-path tick, NO creation tick and NO sweep of any kind on this arm. The
// seven CollectGarbageIfNeeded call sites in DirectGLES.cpp drive the LEGACY registry only;
// * a twin, and the driver storage it owns, is freed when the application lets go of the
// object rather than up to 64 creations or 1024 draw ticks later. That is what
// Managers.h's "dead gigabytes" note asked for.
//
// EVERY HOLDER OF THE KIND, not one. Two live tables of one kind is a real configuration - the
// ScopedDirectGLESTextureBindings fixture keeps a by-value copy of the Texture registry for the
// length of a test, and a context reset does the same in reverse - and the slot allocator
// erases its lifetimeId -> slot mapping on Free, so a notice delivered to one holder and
// resolved again by the next would find nothing to resolve. Every table therefore links itself
// into a per-table-type list at construction and out at destruction, and one notice resolves
// the handle ONCE, drops the twin in each holder BY HANDLE, and frees the slot once, last. No
// holder can be left naming a live entry for a dead object, and there is nothing a sweep could
// still find. (The list is per table TYPE; the kind is the type's template parameter, and each
// of the six kinds has exactly one table type in this backend. Magma's subsystem-4 table mints
// out of its own per-renderer allocator, not MGPipeSlots(), so it is not a holder here.)
//
// The weak_ptr per entry survives for exactly one reason: ForEachLive() hands the callee a
// STRONG reference to the frontend object, which the one direct-iteration site
// (ScopedDetachedTextureFramebufferAttachments) needs. It is never an identity test - that is
// what Gen is for - and it is never read to decide whether an entry is dead: a destructor that
// runs after exit() has begun has its notice dropped by InProcessTeardown(), and that twin is
// then a DELIBERATE leak (the process is exiting, the driver reclaims the object, and a twin
// destructor must not call into a driver that may already be unloaded), not something to be
// collected later.
//
// P3+ DEBT, recorded rather than hidden: this header is under MG_Backend/ and it MINTS
// handles (MGPipeSlots().Acquire below) off a frontend SharedPtr's GetLifetimeId().
// MGPipeHandles.h:13-16 says a handle is minted by the CLIENT and never by the server, and
// under a real split neither the frontend object nor its lifetime id exists on this side of
// the wire. This is monolith glue: the minting and the lifetimeId -> handle resolution both
// belong on the client, and the backend should receive the handle in the verb payload. It is
// NOT part of "Track H done" and check_include_closure.py does not probe MG_Backend headers,
// so nothing catches it automatically.
namespace MobileGL::MG_Backend::DirectGLES {
#if MOBILEGL_PIPE_PUSH
// Declared in Managers.h as well; repeated here because this header is included from it
// before that declaration, and the table below is the arming site on this arm (D13: "the
// arming site moves to the slot table's first insertion").
void EnsureProcessTeardownSentinel();
// What the two knobs add up to. Split out as a PURE function of them so a test can drive
// every combination without needing a process per combination.
enum class EsprytSlotArmVerdict {
Handles, // kMGPipeSubsystemEsprytSlots is set: the {slot, gen} tables run.
Legacy, // the bit is clear and the legacy address-keyed registry is reachable.
NoArm, // the bit is clear AND MOBILEGL_PIPE_LEGACY_MEMOS=0 made the legacy arm
// unreachable, so the operator asked for a configuration with no arm at all.
};
EsprytSlotArmVerdict ClassifyEsprytSlotArm(Bool subsystemBitSet, Bool legacyMemosEnabled);
// This process's verdict, read off MG_Config::Features. Latches nothing and stops nothing.
EsprytSlotArmVerdict CurrentEsprytSlotArmVerdict();
// Says, at backend bring-up, that the knobs leave no arm - and does NOT stop.
//
// The stop cannot live here, and that is the whole point of the split. Backend context
// creation runs inside eglMakeCurrent, and the integration harness pre-flights exactly that
// sequence in a FORKED CHILD (MG_IntegrationTest/Harness/HeadlessGL.cpp): a child that dies
// on a signal is reported as "no usable GPU/display/ICD" and every scenario in the lane is
// SKIPPED - i.e. the lane goes green having run nothing, on the very pair of env vars the
// D14/D18 A/B is driven with, which is what ROADMAP.md:7 forbids. So bring-up only
// DIAGNOSES; the stop is raised by ResolveEsprytSlotTablesArm() at the first twin lookup,
// which happens in the test body where the harness reports it as a failure.
//
// The CALL SITE (InitDisplayAndContext in DirectGLES.cpp) is pinned by
// DirectGLESSlotTable.EglBringUpUnderTheArmlessKnobPairReturnsInsteadOfStopping, which runs
// the real bring-up entry point under the pair in a forked child: edit that site back to
// ResolveEsprytSlotTablesArm() and the case fails naming both knobs.
void DiagnoseEsprytSlotArm();
// Reads the config, logs, installs the death-notice consumer, and STOPS when the operator
// left no arm at all. Cold: called exactly once per process, from the latch below - i.e. at
// the first twin lookup, which is the first moment an arm is actually needed. A process
// that never twins anything needs no arm and is not stopped.
Bool ResolveEsprytSlotTablesArm();
// True when this process runs the {slot, gen} arm. Fixed for the life of the process: the
// two arms hold their twins in different containers, so flipping mid-run would strand them.
//
// INLINE on purpose. Every Find / GetOrCreate / HandleOf / ForEachLive on the twin tables
// consults it, i.e. it is on the per-draw path several times per draw. As an out-of-line
// function in Managers.cpp (no LTO in any shipped configuration) that was a call through
// the PLT per lookup; here the caller sees a guard-variable load and a perfectly-predicted
// branch, and the arm dispatch folds into the caller.
inline Bool EsprytSlotTablesEnabled() {
static const Bool enabled = ResolveEsprytSlotTablesArm();
return enabled;
}
template <typename StateObject, typename BackendObject, MG_Pipe::MGPipeKind kKind>
class BackendSlotTable {
public:
using StatePtr = SharedPtr<StateObject>;
using StateWeakPtr = std::weak_ptr<StateObject>;
using BackendPtr = SharedPtr<BackendObject>;
// The largest slot index this table will grow to for a handle that ARRIVED in a call's
// payload. Slots are dense and allocated per kind, so a million of one kind is already
// far past any application's live object count; the cap is here because the alternative
// is letting a corrupt 32-bit slot decide a vector resize. See GetOrCreate(MGPipeHandle).
static constexpr Uint32 kMaxHandleSlot = 1u << 20;
struct Entry {
BackendPtr backend;
// LIVENESS ONLY, and only for ForEachLive(), which locks it so the callee holds a
// strong ref. Never compared against another object to decide identity - that is
// what Gen is for - never dereferenced for its address, and never read to decide
// whether the slot is dead: death is announced, not discovered.
StateWeakPtr stateRef;
// The generation this entry's twin was built for. An entry whose Gen no longer
// matches the allocator's is a twin of the slot's PREVIOUS owner.
Uint32 Gen = 0;
Bool Live = false;
};
// Every constructor links the table into the per-type holder list and the destructor
// unlinks it, so a by-value copy (the ScopedDirectGLESTextureBindings fixture's saved
// registry) is a holder for exactly as long as it exists. Copy and move carry the
// ENTRIES and the memo; the links are the table's own and are never copied.
BackendSlotTable() { LinkHolder(); }
BackendSlotTable(const BackendSlotTable& other):
m_slots(other.m_slots),
m_nullTwin(other.m_nullTwin),
m_memoLifetimeId(other.m_memoLifetimeId),
m_memoHandle(other.m_memoHandle) {
LinkHolder();
}
BackendSlotTable(BackendSlotTable&& other) noexcept:
m_slots(std::move(other.m_slots)),
m_nullTwin(std::move(other.m_nullTwin)),
m_memoLifetimeId(other.m_memoLifetimeId),
m_memoHandle(other.m_memoHandle) {
other.m_slots.clear();
other.ForgetHandle();
LinkHolder();
}
BackendSlotTable& operator=(const BackendSlotTable& other) {
if (this != &other) {
m_slots = other.m_slots;
m_nullTwin = other.m_nullTwin;
m_memoLifetimeId = other.m_memoLifetimeId;
m_memoHandle = other.m_memoHandle;
}
return *this;
}
BackendSlotTable& operator=(BackendSlotTable&& other) noexcept {
if (this != &other) {
m_slots = std::move(other.m_slots);
m_nullTwin = std::move(other.m_nullTwin);
m_memoLifetimeId = other.m_memoLifetimeId;
m_memoHandle = other.m_memoHandle;
other.m_slots.clear();
other.ForgetHandle();
}
return *this;
}
~BackendSlotTable() { UnlinkHolder(); }
// Resolve-or-create. The handle comes from the client allocator keyed on the frontend
// object's lifetime id, so two calls for the same live object always land on the same
// slot, and a successor object at the same heap address never does.
BackendPtr& GetOrCreate(const StatePtr& stateObj) {
// No assert on null here, unlike the map arm: null is TOLERATED, so a DEBUG build
// must not trap where the release build quietly does the documented thing.
if (stateObj == nullptr) {
// The registry this replaces inserted a null KEY and handed back that entry's
// twin (DirectGLES.cpp's SyncTextureObjectToBackend documents relying on
// exactly that tolerance), so a release build never dereferenced null here.
// Keep the shape exactly, INCLUDING across calls: the map kept its null-keyed
// entry, so a second null call was handed the same twin the first one got.
// Resetting here instead would have destroyed it - an arm difference in the one
// path that documents relying on this. One per-table parking slot, never live,
// never handed a handle, because a null object has no identity and
// therefore cannot have a {slot, gen}.
return m_nullTwin;
}
// D13: the teardown sentinel is armed by the slot table's first insertion. Twin
// creation is the moment a driver-owned id starts needing a guarded destructor;
// this is the cold path, so the once-guard costs nothing per draw. On the legacy
// arm StateBackendObjectRegistry::GetOrCreate arms it itself.
EnsureProcessTeardownSentinel();
const MG_Pipe::MGPipeHandle handle =
MG_Pipe::MGPipeSlots().Acquire(kKind, stateObj->GetLifetimeId());
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
"MGPipe slot space of kind %u is exhausted",
static_cast<Uint32>(kKind));
Entry& entry = EntryAt(handle.Slot);
if (entry.Live && entry.Gen != handle.Gen) {
// The slot was reclaimed and handed to a new object: the twin at it describes
// driver ids the new state object never made.
entry.backend.reset();
}
entry.Gen = handle.Gen;
entry.Live = true;
entry.stateRef = stateObj;
// No creation tick and no sweep here. The registry this replaces needed both,
// because nothing told it a texture or a renderbuffer had been DELETED and object
// CHURN rather than draw count is what made that urgent. Every one of the six kinds
// now announces its own death from its destructor, so a dead twin's slot is already
// back before the next creation asks for one.
RememberHandle(stateObj->GetLifetimeId(), handle);
return entry.backend;
}
// P3a: resolve-or-create BY HANDLE, and it is the shape that discharges the debt this
// header records against itself at the top of the file.
//
// The overload above mints - it calls MGPipeSlots().Acquire off a frontend object's
// lifetime id, from inside MG_Backend - which is monolith glue: a handle is minted by
// the CLIENT, and under a real split neither the object nor its lifetime id exists on
// this side. This overload never touches the allocator at all. The handle ARRIVED, in
// the call's payload, already minted by the side that owns minting; all this does is
// index the slot, notice a generation that no longer matches (the slot was recycled,
// so the twin at it describes driver ids the new resource never made) and hand back
// the twin pointer. FindByHandle beside it is the same shape and already existed.
//
// No StatePtr, therefore no Entry::stateRef: the weak pointer is liveness for
// ForEachLive() and a handle-keyed entry has no frontend object to weakly hold. Such
// an entry is therefore invisible to ForEachLive, which is correct - the one direct
// iteration site walks texture twins, and it is not one of these tables.
//
// Death stays ANNOUNCED, as it is on the other overload: for a handle-keyed kind the
// announcement is the family's own destroy call, not the shared death notice, and the
// slot is freed by the CLIENT after that call returns.
//
// UNUSED AT THE CONTRACT COMMIT, deliberately: it is a member of a class template, so
// an uninstantiated one costs nothing anywhere, and the backend package is what gives
// it its first caller.
BackendPtr& GetOrCreate(MG_Pipe::MGPipeHandle handle) {
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
"GetOrCreate(handle) named the reserved null handle");
if (MG_Pipe::MGPipeHandleIsNull(handle)) return m_nullTwin;
// A slot index that ARRIVED in a payload indexes a vector this call would RESIZE,
// and nothing between the payload and here bounds it: the applier's blob gates sit
// in front of the vertex-input family, not in front of the resource family, which
// dispatches ops->Create(record.Res, ...) straight through. There is no allocator
// constant to check against on this side - the allocator is the client's - so this
// is a sanity cap and is documented as one: kMaxHandleSlot entries of one kind is
// already orders of magnitude past any real GL object count, while a corrupt 32-bit
// slot asks for a four-billion-entry resize.
if (handle.Slot >= kMaxHandleSlot) {
MOBILEGL_ASSERT(false, "GetOrCreate(handle) named slot %u, past this table's %u bound",
handle.Slot, kMaxHandleSlot);
return m_nullTwin;
}
// Same arming as the minting overload, and for the same reason: twin creation is
// the moment a driver-owned id starts needing a guarded destructor.
EnsureProcessTeardownSentinel();
// THE TWO DIRECTIONS ARE NOT SYMMETRIC HERE, where they are on the minting overload.
// There the handle comes straight out of MGPipeSlots().Acquire and can never be
// BEHIND the entry, so a bare `!=` only ever means "the slot was recycled forward".
// Here the handle arrived in a payload, so `handle.Gen < entry.Gen` is a reachable
// input, and adopting it would destroy the INCUMBENT LIVE twin - a driver buffer id,
// a persistent map, a pooled store, released by a defaulted destructor that issues
// no glDeleteBuffers and no pool enrolment - and then stamp the slot back to the
// dead resource's generation, after which the incumbent's own FindByHandle refuses
// it and it is silently handed a fresh, empty twin. That is a leak AND a resource
// that loses its storage with no diagnostic, i.e. the shape commit d7655247 fixed
// and the thing MGPipeHandle::Gen exists to prevent. So: forward is a recycle and
// resets the twin, BACKWARD is refused - which is the same answer FindByHandle
// below already gives the same input.
Entry& entry = EntryAt(handle.Slot);
if (entry.Live && entry.Gen > handle.Gen) {
MOBILEGL_ASSERT(false,
"GetOrCreate(handle) named generation %u at slot %u, which is BEHIND "
"the live entry's %u - refusing rather than destroying the incumbent",
handle.Gen, handle.Slot, entry.Gen);
return m_nullTwin;
}
if (entry.Live && entry.Gen != handle.Gen) entry.backend.reset();
entry.Gen = handle.Gen;
entry.Live = true;
return entry.backend;
}
// The generation of the LIVE entry at this slot, or 0 when the slot is out of range or
// holds no live entry. It exists so a caller can DIAGNOSE - in a release build, where
// MOBILEGL_ASSERT is inert - the refusal GetOrCreate(handle) above performs silently.
Uint32 LiveGenAt(Uint32 slot) const {
if (slot >= m_slots.size()) return 0;
const Entry& entry = m_slots[slot];
return entry.Live ? entry.Gen : 0;
}
// P3a: the death half of the overload above, for a kind whose announcement is its own
// destroy CALL rather than the shared death notice (D-L). Hands the twin OUT rather
// than destroying it in place, because the caller may still have to decide what
// happens to the driver id it owns - Espryt pools it, deletes it, or parks it on the
// deferred-release list when no context is current on this thread - and every one of
// those outcomes has to be reached with the entry already retired, so a re-entrant
// GetOrCreate from a twin destructor cannot resurrect it.
//
// The slot itself is NOT freed here: it belongs to the kind, and for a handle-keyed
// kind the CLIENT frees it after the destroy call returns (SlotAllocator.h:60 - the
// Gen bump rides the next handout, so a double free cannot skip a generation). An
// entry whose Gen no longer matches is a twin of the slot's previous owner and is
// left alone: the successor's own GetOrCreate resets it.
BackendPtr ReleaseByHandle(MG_Pipe::MGPipeHandle handle) {
if (MG_Pipe::MGPipeHandleIsNull(handle)) return BackendPtr{};
if (m_memoHandle.Slot == handle.Slot) ForgetHandle();
if (handle.Slot >= m_slots.size()) return BackendPtr{};
Entry& entry = m_slots[handle.Slot];
if (!entry.Live || entry.Gen != handle.Gen) return BackendPtr{};
BackendPtr dead = std::move(entry.backend);
entry.backend.reset();
entry.stateRef.reset();
entry.Live = false;
return dead;
}
// Null when no live twin of this object exists. Unlike the registry's Find this NEVER
// mutates the table, so the returned pointer survives any later Find on it; only a
// GetOrCreate that grows the vector can move it, and callers that hold one across a
// possible insertion still copy the BackendPtr out.
BackendPtr* Find(StateObject* stateObj) {
if (stateObj == nullptr) return nullptr;
return FindByHandle(HandleOf(stateObj));
}
const BackendPtr* Find(StateObject* stateObj) const {
return const_cast<BackendSlotTable*>(this)->Find(stateObj);
}
BackendPtr* FindByHandle(MG_Pipe::MGPipeHandle handle) {
if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr;
if (handle.Slot >= m_slots.size()) return nullptr;
Entry& entry = m_slots[handle.Slot];
if (!entry.Live || entry.Gen != handle.Gen) return nullptr;
return &entry.backend;
}
// The handle this object's twin is keyed on, or the null handle. This is what a backend
// memo stores instead of a raw pointer, a GL name or a bare lifetime id.
//
// A NULL answer is never memoised. The memo is per table and the allocator is per
// kind, so with two holders of one kind the OTHER table can be the one that acquires;
// a cached "no handle" here would then outlive the twin's creation over there, and
// nothing on this table's own acquire path would ever refresh it. A miss costs the
// allocator probe it always cost; a hit is refreshed the moment anyone acquires.
MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const {
if (stateObj == nullptr) return MG_Pipe::kMGPipeNullHandle;
const Uint64 lifetimeId = stateObj->GetLifetimeId();
if (lifetimeId == m_memoLifetimeId) return m_memoHandle;
const MG_Pipe::MGPipeHandle handle =
MG_Pipe::MGPipeSlots().FindByLifetimeId(kKind, lifetimeId);
if (!MG_Pipe::MGPipeHandleIsNull(handle)) RememberHandle(lifetimeId, handle);
return handle;
}
// P2 step e2's backend half. The frontend object with this lifetime id has just been
// DESTROYED: resolve its handle ONCE, drop its twin in EVERY table of this type, and
// return the slot to the allocator - in that order, because the allocator forgets the
// lifetime id on Free and a holder told second could no longer resolve it.
//
// The slot is returned whether or not any holder still had a twin at it: the lifetime
// id is dead and MG_State never hands one out twice, so nothing can acquire it again,
// and a slot minted for it that no table holds (a table reset with `= {}` drops its
// entries without freeing) would otherwise stay allocated for the life of the process.
//
// STATIC, and deliberately so: a notice is about an object, not about a table, and
// "which table holds it" is exactly the question that produced the two-holder leak.
// Returns whether the object had a slot of this kind, i.e. whether anything was freed;
// a second call for the same id answers false because the allocator no longer maps it.
static Bool OnFrontendObjectDestroyed(Uint64 lifetimeId) {
const MG_Pipe::MGPipeHandle handle =
MG_Pipe::MGPipeSlots().FindByLifetimeId(kKind, lifetimeId);
if (MG_Pipe::MGPipeHandleIsNull(handle)) return false;
for (BackendSlotTable* holder = s_firstHolder; holder != nullptr;) {
// The successor is read BEFORE the release: ReleaseTwinAt runs the twin's
// destructor, which is a driver call, and nothing that outlives it may be a
// reference into this holder.
BackendSlotTable* const next = holder->m_nextHolder;
holder->ReleaseTwinAt(handle);
holder = next;
}
MG_Pipe::MGPipeSlots().Free(kKind, handle);
return true;
}
// How many tables of this type exist right now. For the tests that pin the holder
// list; nothing on a shipping path asks.
static Uint32 HolderCount() {
Uint32 count = 0;
for (const BackendSlotTable* holder = s_firstHolder; holder != nullptr;
holder = holder->m_nextHolder) {
++count;
}
return count;
}
// fn(const StatePtr& state, const BackendPtr& twin) over every live, still-owned entry.
// Replaces the registry's begin()/end(), whose iterator exposed the raw frontend
// address as the map key - the one place the backend read an identity it must not have.
// The state object is handed over as a STRONG reference, so the callee cannot be handed
// a dangling key the way the old iteration could.
template <typename Fn>
void ForEachLive(Fn&& fn) const {
// Index loop and a COPIED twin, not a range-for over references: fn is arbitrary
// backend code, and a nested GetOrCreate on this table would resize m_slots and
// invalidate both the iterator and any reference into the vector that outlives the
// call. The one caller today happens not to insert; that is not a property the
// walk should depend on.
for (SizeT slot = 0; slot < m_slots.size(); ++slot) {
const Entry& entry = m_slots[slot];
if (!entry.Live || !entry.backend) continue;
const StatePtr state = entry.stateRef.lock();
if (!state) continue;
const BackendPtr twin = entry.backend;
fn(state, twin);
}
}
Uint32 LiveCount() const {
Uint32 count = 0;
for (const Entry& entry : m_slots) {
if (entry.Live) ++count;
}
return count;
}
private:
// Drop the twin at `handle` if THIS table holds it. Frees nothing: the slot belongs to
// the kind, not to the table, and OnFrontendObjectDestroyed returns it once, after
// every holder has let go.
Bool ReleaseTwinAt(MG_Pipe::MGPipeHandle handle) {
// Forget the memo whenever it names this slot, even if this table has no entry
// there: a memo can be a handle learned from the allocator for an object another
// holder twinned, and it must not survive the slot's next handout.
if (m_memoHandle.Slot == handle.Slot) ForgetHandle();
if (handle.Slot >= m_slots.size()) return false;
// The twin's destructor is a driver call and could, in principle, re-enter
// GetOrCreate on this table and resize m_slots. So NOTHING that outlives the
// destructor may be a reference into m_slots: the twin is moved out into a local,
// the entry is finished with, and only then is the local released.
BackendPtr dead;
{
Entry& entry = m_slots[handle.Slot];
if (!entry.Live || entry.Gen != handle.Gen) return false;
dead = std::move(entry.backend);
entry.backend.reset();
entry.stateRef.reset();
entry.Live = false;
}
dead.reset();
return true;
}
// Grows the table to hold `slot`. Every caller bounds `slot` first - the minting
// overload because the allocator produced it, the handle overload against
// kMaxHandleSlot - because this is the one place a client-supplied number decides an
// allocation size.
Entry& EntryAt(Uint32 slot) {
if (slot >= m_slots.size()) m_slots.resize(static_cast<SizeT>(slot) + 1);
return m_slots[slot];
}
void RememberHandle(Uint64 lifetimeId, MG_Pipe::MGPipeHandle handle) const {
m_memoLifetimeId = lifetimeId;
m_memoHandle = handle;
}
void ForgetHandle() const {
m_memoLifetimeId = 0;
m_memoHandle = MG_Pipe::kMGPipeNullHandle;
}
// The holder list: intrusive and doubly linked, so registering and unregistering are
// two pointer writes with no allocation, and its head is a constant-initialised
// static - which is what lets the process-lifetime registry globals in Managers.cpp
// link themselves in from their own constructors with no initialisation-order
// question to answer. Single-threaded, like every table it links (the tables live and
// die on the context thread, as the notice they answer does).
void LinkHolder() {
m_prevHolder = nullptr;
m_nextHolder = s_firstHolder;
if (s_firstHolder != nullptr) s_firstHolder->m_prevHolder = this;
s_firstHolder = this;
}
void UnlinkHolder() {
if (m_prevHolder != nullptr) {
m_prevHolder->m_nextHolder = m_nextHolder;
} else {
s_firstHolder = m_nextHolder;
}
if (m_nextHolder != nullptr) m_nextHolder->m_prevHolder = m_prevHolder;
m_prevHolder = nullptr;
m_nextHolder = nullptr;
}
static inline BackendSlotTable* s_firstHolder = nullptr;
BackendSlotTable* m_prevHolder = nullptr;
BackendSlotTable* m_nextHolder = nullptr;
// Indexed by MGPipeHandle::Slot; [0] is the reserved slot and is never live.
Vector<Entry> m_slots;
// Handed back by GetOrCreate for a null state object. Never live, never handed a handle.
BackendPtr m_nullTwin;
// ONE-entry resolution memo, lifetimeId -> handle. It exists because without it every
// resolution goes through the allocator's ByLifetimeId hash, which the deleted
// TwinLookupMemos existed to avoid and which D13 promises to replace with "direct slot
// indexing".
//
// It is one entry and therefore only helps a caller that asks for the SAME object twice
// running - ResolveVaoTwin and SyncCurrentProgram do, once per draw each. Two callers
// it does NOT help, recorded rather than claimed away: BindCurrentFBO resolves BOTH
// targets in a frame, and ResolveUnitSamplerBackend asks for a different sampler per
// texture unit, so both thrash a single-entry memo and pay the probe P1 did not (P1 had
// a per-unit memo and a direct-mapped 6-slot array there). Making the memo per-unit /
// per-target is the fix, and G11 - the device-side gate that would price it - is owed.
//
// It cannot serve a stale answer, by three independent arguments:
// * the key is a lifetime id, which MG_State never hands out twice, so a recycled
// heap address cannot hit this memo the way it could hit an address-keyed one;
// * a null answer is never stored, so another holder's acquire cannot be hidden by
// a "no handle" this table remembered earlier; and
// * even a hit for a slot that has since been freed and re-handed is caught, because
// the caller resolves the handle through FindByHandle, which compares Gen.
// Cleared anyway when a death notice names the memoised slot. 0 is never a live
// lifetime id (MG_State's counters start at 1), so a zeroed memo is a guaranteed miss.
mutable Uint64 m_memoLifetimeId = 0;
mutable MG_Pipe::MGPipeHandle m_memoHandle = MG_Pipe::kMGPipeNullHandle;
};
#endif // MOBILEGL_PIPE_PUSH
} // namespace MobileGL::MG_Backend::DirectGLES
File diff suppressed because it is too large Load Diff
+392 -1
View File
@@ -46,6 +46,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions( Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(
const MG_External::GLESCapabilities& capabilities, SizeT targetIndex); const MG_External::GLESCapabilities& capabilities, SizeT targetIndex);
// Whether this format's ES storage is widened to 8-bit-per-channel because the
// driver stores some packed16 allocations with a mirrored field order
// (PixelFormatNormalizeOptionBit::WidenPacked16Norm). True only for
// GL_RGB565/GL_RGB5(_A1)/GL_RGBA4, and only where the POST probe measured the
// divergence (or MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE forces it). The transfer paths
// consult it too: the packed-norm re-upload leg must stand down when the ES storage
// is no longer 16-bit packed.
Bool UsesWidenedPacked16NormStorage(TextureInternalFormat internalFormat);
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat, void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
GLenum* outFormat, GLenum* outType, GLenum* outFormat, GLenum* outType,
TextureTarget target = TextureTarget::Unknown); TextureTarget target = TextureTarget::Unknown);
@@ -60,6 +69,115 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target); Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat); Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat);
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat); Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
// The CHANNEL WIDENING an image-bindable texture's ES storage takes, so that a format
// GLSL ES cannot spell as an image is carried by one it can.
//
// GL has forty image formats, GLSL ES core has thirteen, and no test device advertises
// GL_NV_image_formats - so a shader declaring one of the other twenty-six has no legal
// ESSL at all and glBindImageTexture rejects the narrow format outright for most of them
// (GL_INVALID_VALUE for nineteen of twenty-six on Adreno, twenty-five on both Malis).
// Seventeen have a core format of the SAME per-channel width and component type,
// differing only in channel count, and in one of those the emulation is EXACT: GL already
// defines an imageLoad from a narrower format as (r, 0, 0, 1) and an imageStore as
// dropping the components the format does not have, so the carrier's surplus channels
// hold values GL has already named. WidenImageFormatsPass pins them in the shader; this
// is the storage half, and DirectGLES::TextureImpl::SyncImageTextureBinding the bind
// half. All three ask WidenedCoreEsslImageFormat, so they cannot pick different carriers.
//
// Reports nothing (InternalFormat == GL_UNKNOWN_MGL) for a format that is core already,
// for the nine with no exact carrier (r11f_g11f_b10f, rgb10_a2, rgb10_a2ui, rgba16, rg16,
// r16, rgba16_snorm, rg16_snorm, r16_snorm - those keep the honest "no GLSL ES spelling"
// diagnostic rather than a silent approximation), and on a driver that HAS
// GL_NV_image_formats, where the shader keeps the declared format and no widening may
// happen behind it.
//
// The widened triple REPLACES what GenerateTextureFormatInfo chose, including any
// renderability substitution: an image that cannot be image-bound is useless whatever its
// attachment behaviour, so the image constraint wins. In practice that only bites
// RG8_SNORM/R8_SNORM on a driver without EXT_render_snorm, where the storage stays
// signed-normalized instead of becoming the half float that fallback would have picked -
// so an image-bound texture in one of those two formats is no longer attachable, and
// glGetTexImage on it falls through to the CPU shadow, which a shader-side imageStore
// does not update. Accepted deliberately: before the widening, an image binding in either
// format was refused outright by every driver tested and the stage that declared it never
// compiled at all, so nothing that works today is being given up.
//
// KNOWN GAP, for the same "all three layers move together" reason: a widened texture that
// is ALSO an FBO colour attachment gains one to three writable channels, and a draw into
// it can leave values in channels GL says are 0 and 1. Sampling and imageLoad are covered
// (the swizzle composition in SyncTextureParamsToBackend and the shader-side mask), but a
// glReadPixels/glGetTexImage that asks for more channels than the frontend format has
// would see them. Closing it needs the per-draw-buffer colour mask the three-channel
// widening already carries (FramebufferImpl::g_alphaWidenedDrawBufferMask) generalized
// from "alpha" to a channel count, which is its own change.
// How the FRONTEND's CPU shadow for a widened format is laid out relative to the carrier's
// transfer, i.e. what the upload has to do to it. Almost every entry is `Components`: the
// shadow already holds SourceChannels components of exactly the carrier's own type, so
// padding it out to four is the whole conversion. The packed entries do not - their shadow
// is ONE 32-bit word per texel - and reading such a word as components of the carrier's
// type takes twelve or sixteen bytes out of four and shears the level.
enum class ImageWidenSourceEncoding : Uint8 {
Components = 0,
// r11f_g11f_b10f: GL_UNSIGNED_INT_10F_11F_11F_REV -> four GL_FLOATs of an rgba16f.
PackedFloat11f11f10f,
// rgb10_a2 and rgb10_a2ui: GL_UNSIGNED_INT_2_10_10_10_REV -> four GL_UNSIGNED_SHORT
// channel CODES of an rgba16ui. The same split serves both: the two formats differ
// only in what the codes MEAN, which is the shader's business and not the transfer's.
PackedInt2101010Rev,
};
struct ImageBindableStorageWidening {
GLenum InternalFormat = GL_UNKNOWN_MGL;
GLenum Format = GL_UNKNOWN_MGL;
GLenum Type = GL_UNKNOWN_MGL;
// Channels the FRONTEND format has, i.e. how many of the carrier's four the client
// data fills. The rest are uploaded as 0, and the fourth as the format's implied 1.
Uint SourceChannels = 0;
// Whether that implied 1 is the integer one or a saturated normalized field - the
// transfer type cannot tell the two apart (GL_UNSIGNED_BYTE serves both RG8 and
// RG8UI), so the carrier decides.
Bool IntegerData = false;
// What the upload has to do to the frontend shadow before it describes the level to
// the driver (PrepareImageWidenedUpload).
ImageWidenSourceEncoding SourceEncoding = ImageWidenSourceEncoding::Components;
// Non-zero when the carrier holds this format's channels as the INTEGER CODES of a
// NORMALIZED value - the seven 16-bit and 10-bit normalized formats, which core ESSL
// has no image format of any width for and which a float carrier would requantise.
// Each entry is the largest code that channel can hold, i.e. the denominator of GL 4.6
// 2.3.5; SignedNormalized picks which of the two conversions it is the denominator of.
//
// Two things depend on it, both because the ES storage no longer shares the frontend
// format's component class: the upload pads a missing alpha with ChannelMax[3] instead
// of the transfer type's own "one" (through a uint carrier the saturated field IS the
// one), and glGetTexImage divides the codes back out into the floats the application
// is still owed.
Uint ChannelMax[4] = {0u, 0u, 0u, 0u};
Bool SignedNormalized = false;
Bool CarriesNormalizedCodes() const { return ChannelMax[0] != 0u; }
explicit operator Bool() const { return InternalFormat != GL_UNKNOWN_MGL; }
};
ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat);
// The single-channel core format an image-bindable BUFFER texture's view is SPLIT into, or
// GL_UNKNOWN_MGL for a format that needs no split (or has no core base).
//
// A buffer texture cannot be widened: its texels are the application's buffer object, at
// the size and layout the application gave it, and it is usually also a vertex, index or
// storage buffer whose bytes are not ours to restride. But an rg32f view of N texels and
// an r32f view of 2N texels describe exactly the SAME bytes, so the split changes only
// how the shader subscripts them - component j of texel i is texel 2i + j of the base
// view - which WidenImageFormatsPass rewrites every access to do. The same rule as the
// widening decides WHETHER: a driver that can spell rg32f for an imageBuffer needs
// nothing.
//
// KNOWN GAP, and the reason this is not applied to a texture that is merely sampled: a
// buffer texture that is BOTH image-bound and read through a samplerBuffer would have its
// sampled view split too, and the sampler side is not rewritten. Accepted for the same
// reason the storage widening's gaps are - on a driver where the split applies at all
// there is no legal ESSL for the image declaration, so such a program did not compile.
GLenum GetImageBindableBufferSplitFormat(TextureInternalFormat internalFormat);
} // namespace TextureImpl } // namespace TextureImpl
namespace FramebufferImpl {} // namespace FramebufferImpl namespace FramebufferImpl {} // namespace FramebufferImpl
@@ -115,6 +233,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight, Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type, GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
void* pixels, Bool applyPackImageParams); void* pixels, Bool applyPackImageParams);
// Stores packed 32-bit source words verbatim, with the same destination addressing, PACK
// parameters and pixel-pack-buffer handling as StoreWideRowsToClient. For the sources whose
// storage word already IS the client word (MG_Util::IsRawPackedPixelTransfer): routing those
// through the wide float intermediate re-encodes them, and the RGB9_E5 encoder canonicalizes
// the shared exponent, so glGetTexImage would answer with different bits than were stored.
// `srcWords` holds sliceHeight * sliceCount tightly stacked rows of `width` 32-bit words.
// False when `type` is not a 4-byte packed type.
Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount,
GLenum type, void* pixels, Bool applyPackImageParams);
} // namespace ReadbackImpl } // namespace ReadbackImpl
namespace PrgramImpl { namespace PrgramImpl {
@@ -130,7 +258,265 @@ namespace MobileGL::MG_Backend::DirectGLES {
// drawBufferCount <= 1, i.e. for everything but a framebuffer that actually // drawBufferCount <= 1, i.e. for everything but a framebuffer that actually
// enables several draw buffers, so the ordinary single-target shader is untouched. // enables several draw buffers, so the ordinary single-target shader is untouched.
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount); String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount);
// SPIRV-Cross emits `#extension GL_EXT_texture_buffer : require` for every buffer-texture
// sampler when it targets ESSL below 320, and offers no way to ask for the OES spelling.
// On a driver that advertises only GL_OES_texture_buffer that directive is a compile
// error, so the name is retargeted in the emitted source. A no-op on every other tier:
// ES 3.2 needs no directive at all and an EXT driver already has the right one.
String RetargetTextureBufferExtension(String glslCode,
MG_External::GLESCapabilities::TextureBufferTier tier);
// Adds `#extension GL_NV_image_formats : require` when the shader carries an image
// format qualifier GLSL ES has no core spelling for. SPIRV-Cross prints the format and
// asks for nothing, so the request has to be made here. `needed` is the caller's answer,
// because only it knows which formats are in play AND whether the driver advertises the
// extension - requesting an unadvertised extension is itself a compile error, so this is
// never emitted speculatively. A no-op when not needed or already present.
String RequestExtendedImageFormats(String glslCode, Bool needed);
// Adds `#extension GL_OES_viewport_array : require` when the emitted ESSL names
// gl_ViewportIndex. SPIRV-Cross prints that identifier and asks for nothing (unlike
// gl_Layer, which it backs with GL_NV_viewport_array2 on ES) and ESSL has no core
// spelling for it at any version, so the request has to be made here or the stage does
// not compile - which loses the whole program, not just the multi-viewport routing.
// `needed` is the caller's answer for the same reason as above: only it knows whether the
// driver advertises the extension, and requesting an unadvertised one is itself a compile
// error, so this is never emitted speculatively. A no-op when not needed or already
// present.
String RequestViewportArrayExtension(String glslCode, Bool needed);
// Adds `#extension <extensionName> : require` when a TESSELLATION or GEOMETRY stage's
// emitted ESSL names gl_PointSize. Desktop GL has that built-in in gl_PerVertex for every
// vertex-processing stage; ESSL does NOT have it in those two at any version - not even
// 320, where the stages themselves are core - until EXT/OES_tessellation_point_size resp.
// EXT/OES_geometry_point_size is requested. SPIRV-Cross prints the identifier bare and
// asks for nothing, exactly as it does for gl_ViewportIndex, so without this the stage
// fails to compile with "`gl_PointSize' undeclared" and the WHOLE program is replaced by
// program 0 - the draw renders nothing and any transform-feedback capture it was carrying
// is rejected outright. `extensionName` is the caller's answer, nullptr when the driver
// advertises neither spelling, because requesting an unadvertised extension is itself a
// compile error. A no-op when nullptr or already present.
String RequestPointSizeExtension(String glslCode, const char* extensionName);
// The extension name RequestPointSizeExtension should be given for `tier`, or nullptr for
// PointSizeTier::None. `tessellation` picks the tessellation spellings over the geometry
// ones; the two extensions are separate and neither implies the other.
const char* PointSizeExtensionName(MG_External::GLESCapabilities::PointSizeTier tier, Bool tessellation);
// Writes a format layout qualifier into the image declarations named in
// `esslFormatByUniformName` that still have none. The completion half of the image-format
// bake, and ONLY that: the SPIR-V pass (BakeImageFormatsPass) is what normally puts the
// format in, but SPIRV-Cross throws rather than printing the formats it calls
// desktop-only when it targets ESSL - r8ui among them, which is what the stencil half of
// KHR-GL4x.packed_depth_stencil.stencil_texturing binds - and a throw loses the whole
// stage. So those formats stay out of the module and are spelled here instead, on the
// emitted text, where nothing can refuse them.
//
// Declarations that already carry a format are left exactly as they are, whoever wrote
// it. Must run before RemoveLayoutBinding, which is where an image's layout qualifier
// stops being safe to edit by hand.
String BakeImageFormatQualifiers(String glslCode, const UnorderedMap<String, String>& esslFormatByUniformName);
String RemoveLayoutBinding(const String& glslCode); String RemoveLayoutBinding(const String& glslCode);
// Prefix of the per-element scalar declarations RemapImageArrayElementUnits splits an
// image array into; the suffix is the array's own name and the element's index.
constexpr const char* IMAGE_ARRAY_ELEMENT_PREFIX = "mg_imageElem_";
// One image ARRAY whose elements the application pointed at units that are not
// consecutive-from-element-zero.
struct ImageArrayUnitPlan {
String name; // the array's name, exactly as the emitted ESSL declares it
Vector<Int> units; // the frontend image unit element k has to reach
};
// Desktop GL lets an application give each element of an image array an ARBITRARY unit
// (glUniform1i per element). ES has no such call at all - "ES image units come
// exclusively from the layout(binding=N) qualifier" - and one declaration carries one
// binding, so ESSL nails an array's elements to the CONSECUTIVE units N, N+1, N+2, ...
// MobileGL used to stamp element [0]'s unit as the binding and let the rest fall where
// they fell: KHR-GL4x.shader_image_load_store.advanced-sso-simple assigns 0,2,4,6 and
// 1,3,5,7, so its two programs actually addressed 0,1,2,3 and 1,2,3,4 - one layer got the
// wrong value and three were never written, with no GL error and no link log. The same
// defect for SAMPLER arrays was fixed API-side (SubscriptUniformNameForElement); an image
// array has no API side to fix, because ES makes glUniform1i on an image uniform an
// INVALID_OPERATION.
//
// Repaired by SPLITTING the array into one SCALAR image uniform per element, each with
// its own layout(binding = N), and rewriting `name[k]` to the scalar declared for
// element k. One declaration carries one binding, so one declaration per unit is the
// only spelling that reaches an arbitrary set of them.
//
// That rewrite needs every k in the emitted text to be a LITERAL, and it is:
// LegalizeResourceArrayIndexingForEssl has already folded or lowered every dynamic
// image-array subscript in the module, because ESSL forbids one outright ("image arrays
// indexed with non-constant expressions are forbidden in GLSL ES", Mesa 26.1.4 at
// ES 3.2, on a raw GLES probe with no MobileGL in the loop). The earlier shape here -
// widening the array to cover the whole span of units and routing each subscript through
// a `const highp int` offset table - was written before that pass covered images, and
// the table lookup was itself one of the non-constant expressions the same probe refuses.
// The split also costs exactly the image uniforms the application declared, where the
// widening cost the whole SPAN (seven for the four elements of
// KHR-GL42.shader_image_load_store.advanced-sso-simple), so there is no budget for it to
// fail to fit in.
//
// Declines - leaving the array exactly as it was, and naming it in `outDeclined` for the
// caller to report - when the emitted extent disagrees with the reflection, when the
// array is reached by anything other than a subscript, or when a subscript is not a
// literal element index. Silence was the whole defect here, so a decline must be audible.
//
// Must run AFTER RebindImageUniformsToFrontendUnits and BakeImageFormatQualifiers (both
// key on the GL uniform name and on a binding already being stamped) and BEFORE
// SplitReadWriteImageUniforms (so each element that is both read and written is split
// with its own binding already on it) and RemoveLayoutBinding (which is what preserves
// image bindings). Like them, it is downstream of the L2 shader-translation memo, so the
// per-program units it reads need no entry in BuildEsslTranslationKey.
String RemapImageArrayElementUnits(const String& glslCode, const Vector<ImageArrayUnitPlan>& plans,
Vector<String>* outDeclined = nullptr);
// The member list of a `gl_PerVertex { ... }` redeclaration in already-emitted ESSL -
// the text between the braces, verbatim - or nullopt when the shader does not redeclare
// the block in that direction. `input` selects the `in gl_PerVertex` form over the
// `out` one.
//
// Exists so BuildPassthroughTessControlEssl can MIRROR the stages it has to sit between
// rather than guess at them. Whether SPIRV-Cross redeclares the built-in block, and with
// which members, depends on what the application's shader touched; a synthesized stage
// that redeclares a different shape than its neighbours is an ES link error against a
// program that has no other problem.
std::optional<String> ExtractPerVertexBlockMembers(const String& essl, Bool input);
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes: "the input
// patch is passed through unmodified", the output patch has PATCH_VERTICES vertices, and
// the levels come from the PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
//
// Desktop GL makes the control stage OPTIONAL. OpenGL ES 3.2 does not: it has no
// PATCH_DEFAULT_*_LEVEL state at all (only glPatchParameteri, for PATCH_VERTICES) and
// rejects a program that has an evaluation stage without a control stage - with an EMPTY
// info log, verified on an Adreno 830 with no MobileGL in the process. MobileGL's own
// frontend link succeeds, so the program reports GL_LINK_STATUS = TRUE, program 0 is
// bound in its place, and every draw silently renders nothing.
//
// `inPerVertexMembers` / `outPerVertexMembers` are the member lists to redeclare gl_in
// and gl_out with - normally taken from the neighbouring stages' own emitted ESSL via
// ExtractPerVertexBlockMembers, and empty to leave the driver's built-in declaration
// alone, which is what matching a neighbour that did not redeclare requires.
//
// All four outer levels and both inner levels are written unconditionally: writing a
// level the evaluation stage's domain does not use is legal and ignored, and it saves
// this from having to know the domain. They are the GL_PATCH_DEFAULT_OUTER_LEVEL /
// GL_PATCH_DEFAULT_INNER_LEVEL state, baked in as literals - ES has no such state and no
// glPatchParameterfv to forward to, so compiling them in is the only way to honour them.
// That makes them part of what a built program is stale against, exactly as PATCH_VERTICES
// is: see the staleness clause in DirectGLES.cpp's SyncCurrentProgram, which compares both.
//
// The same stage, for the same reason, that DirectVulkan synthesizes in
// ProgramFactory::BuildPassthroughTessControlSource - Vulkan likewise requires both
// tessellation stages. Kept as two generators rather than one because the two targets
// disagree on everything but the algorithm: desktop GLSL 450 against ESSL, a fixed
// gl_PerVertex shape that Vulkan matches structurally against a mirrored one, and a
// VkShaderModule against a driver shader object.
String BuildPassthroughTessControlEssl(Uint esslVersion, Uint patchVertices,
const String& inPerVertexMembers,
const String& outPerVertexMembers,
const FloatVec4& defaultOuterLevel,
const FloatVec2& defaultInnerLevel);
// Prefix of the writeonly half a read+write image uniform is split into (see
// SplitReadWriteImageUniforms); the suffix is the image's own (already access-tagged) name.
constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_";
// The three names SplitReadWriteImageUniforms renames a rewritten image declaration
// under, one per REPAIR it can apply. Which one a stage picks is decided by that stage's
// own accesses, so two stages that use an image the same way arrive at the SAME name and
// two that use it differently arrive at different ones - which is exactly the property
// the rename exists for, at no cost to the stages that agree. Exposed for the tests.
constexpr const char* IMAGE_READONLY_ALIAS_PREFIX = "mg_imageRo_";
constexpr const char* IMAGE_WRITEONLY_ALIAS_PREFIX = "mg_imageWo_";
constexpr const char* IMAGE_SPLIT_READ_ALIAS_PREFIX = "mg_imageRw_";
// ESSL refuses an image variable that carries a format qualifier other than r32f /
// r32i / r32ui unless it also carries `readonly` or `writeonly` (GLSL ES 3.10 4.9 /
// 3.20 4.10; glslang enforces it verbatim in ParseHelper.cpp's layoutObjectCheck).
// SPIRV-Cross emits NEITHER for an image the shader both reads and writes: it
// speculatively decorates every storage image NonWritable+NonReadable
// (fixup_image_load_store_access), then OpImageRead clears NonReadable and
// OpImageWrite clears NonWritable, and to_qualifiers_glsl only prints `readonly`
// from NonWritable and `writeonly` from NonReadable. Desktop GLSL is happy with the
// bare declaration, so the frontend raises no error and the illegal ESSL only shows
// up as a device compile failure - and then as a silently no-op draw.
//
// Restores a legal declaration, and RENAMES it after the repair it applied while doing so:
// * loaded only -> add `readonly`, rename under IMAGE_READONLY_ALIAS_PREFIX
// * stored only -> add `writeonly`, rename under IMAGE_WRITEONLY_ALIAS_PREFIX
// * both -> emit TWO declarations on the same binding and of the
// same type, `coherent readonly
// <IMAGE_SPLIT_READ_ALIAS_PREFIX><name>` and `coherent
// writeonly <IMAGE_WRITE_ALIAS_PREFIX><that name>`, point
// every imageStore at the second one, and follow each of
// those stores with `memoryBarrierImage();`. Several image
// variables may share an image unit as long as they have
// the same type and format, which is exactly what the pair
// is.
//
// The rename is the other half of the repair and applies to all three cases. The qualifier
// chosen above is a decision about ONE STAGE's accesses, and GLSL requires a uniform
// declared in two stages to be declared identically - so a shader that stores an image from
// the vertex stage and loads it from the fragment stage came out of here `writeonly` in one
// and `readonly` in the other. Adreno merges the two same-named declarations and silently
// drops the vertex-stage STORES: no GL error, no link log, LINK_STATUS = 1, and the image
// still reads back its initial contents
// (KHR-GL4x.shader_image_load_store.advanced-memory-dependentInvocation; a raw-ES probe
// isolated the trigger to the same-name/mismatched-qualifier pair, and only when both
// carry `coherent`). Renaming leaves no cross-stage variable to merge.
//
// The name is keyed on the REPAIR, not on the stage, and that distinction is the whole
// point: two stages that use an image the same way emit byte-identical declarations, so
// letting them keep one shared name costs nothing and merging them is correct, while two
// stages that use it differently land on different prefixes and cannot be merged at all.
// A per-STAGE tag also satisfied the first requirement but violated the second: it made
// the SAME image a distinct uniform in every stage that named it, and Adreno allocates
// image LOCATIONS per distinct uniform. KHR-GL43.shading_language_420pack.
// binding_images_texture_type_* declares three read+write images in each of its five
// stages; merged that is 6 image uniforms, per-stage-tagged it is 30, and the Adreno 830
// linker answered "Error: Image Image location or component exceeds max allowed. Error:
// Linking failed." - which, the frontend having already published LINK_STATUS = TRUE from
// glslang's link, surfaced only as every draw silently doing nothing and the images
// reading back zero. Mali and Mesa link the same text, so nothing but a device gate
// catches this.
//
// A declaration SPIRV-Cross already tagged `readonly` or `writeonly` needs no qualifier
// repair, but it is NOT stage-independent: that tag is derived from the accesses of the
// stage being emitted, so an image stored in the vertex stage and loaded in the fragment
// stage arrives here as `coherent writeonly g_image` and `coherent readonly g_image` -
// one name, two spellings, which is exactly the pair Adreno merges. Those declarations
// are therefore renamed too, keyed on the qualifier they already carry (readonly ->
// IMAGE_READONLY_ALIAS_PREFIX, writeonly -> IMAGE_WRITEONLY_ALIAS_PREFIX) and with
// nothing but the identifier changed. Stages that agree still reach the same alias and
// stay merged, so this costs no shader an extra image uniform.
//
// The declarations this pass still leaves untouched keep their names: one carrying BOTH
// readonly and writeonly (a spelling no access analysis produces, so it came from the
// application and is identical everywhere), and one carrying NEITHER, which is legal only
// for the r32f/r32i/r32ui formats and is likewise spelled the same in every stage.
//
// The `coherent` on both halves of the pair is load-bearing, not decoration: GLSL only
// guarantees a write through one image variable is visible to a read through a DIFFERENT
// one when both are coherent, and the split is what makes a same-variable
// read-after-write cross-variable. The single-declaration repairs above do not get it -
// nothing aliases them.
//
// The barrier is the other half of the same problem, and coherent alone did not cover it:
// visibility is not ORDER. Within one invocation the ES compiler sees a write to one
// variable and a read of another it has no reason to believe alias, and is free to serve
// the read from before the write - which is what advanced-memory-order's store/load/
// compare loop measured on Adreno. memoryBarrierImage() orders exactly those two, is core
// GLSL ES 3.10 in every stage, and is not an execution barrier, so it is legal in
// non-uniform control flow. It costs something in a shader that stores to a read+write
// image in a loop, which is why it is confined to the split pair.
//
// Budget note: the split DOUBLES the image-uniform count of the stage it fires in, so
// a driver advertising a tight GL_MAX_{FRAGMENT,VERTEX,...}_IMAGE_UNIFORMS can turn a
// shader that used to compile into a link failure. ES only guarantees 4 fragment image
// uniforms, so a shader with more than half the limit in read+write images is the case
// to watch.
//
// Runs on the transpiled ESSL, so it must see the bindings the frontend units were
// already rewritten to and must run before those bindings are stripped - see the call
// site in Managers.cpp. Its output is a function of the emitted text alone - it needs no
// stage and no per-program state - so it adds nothing to BuildEsslTranslationKey either.
//
// `outSplitCount`, when given, receives the number of declarations that were actually
// doubled - i.e. exactly how many image uniforms this stage gained over what the
// application declared. Zero for every shader but a handful, and the only number the
// budget note above can be reported with.
String SplitReadWriteImageUniforms(const String& glslCode, Uint* outSplitCount = nullptr);
// Prefix of the per-sampler float uniform that carries GL_TEXTURE_LOD_BIAS into // Prefix of the per-sampler float uniform that carries GL_TEXTURE_LOD_BIAS into
// the shader (see EmulateTextureLodBias); the suffix is the sampler's own name. // the shader (see EmulateTextureLodBias); the suffix is the sampler's own name.
constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_"; constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_";
@@ -142,7 +528,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the bound texture's (or sampler object's) value into it; a shader whose samplers // the bound texture's (or sampler object's) value into it; a shader whose samplers
// all have a zero bias is therefore unaffected. Returns the source unchanged when // all have a zero bias is therefore unaffected. Returns the source unchanged when
// there is nothing to rewrite. // there is nothing to rewrite.
String EmulateTextureLodBias(const String& glslCode); //
// avoidExplicitLodBias leaves lookups that already carry an explicit LOD untouched,
// so their constant level stays constant; only the implicit-LOD forms take the bias.
// Off by default and only ever set on ANGLE + llvmpipe, where injecting the uniform
// into a constant LOD crashes the driver (MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS).
String EmulateTextureLodBias(const String& glslCode, Bool avoidExplicitLodBias = false);
} // namespace PrgramImpl } // namespace PrgramImpl
namespace Utils { namespace Utils {
@@ -9,7 +9,10 @@
#include "BackendObject_DirectVulkan.h" #include "BackendObject_DirectVulkan.h"
#include "MG_Backend/BackendObject.h" #include "MG_Backend/BackendObject.h"
#include "DirectVulkan.h" #include "DirectVulkan.h"
#include "SubgroupSupportPolicy.h"
#include "MG_State/GLState/FramebufferState/FramebufferObject.h" #include "MG_State/GLState/FramebufferState/FramebufferObject.h"
#include "MG_State/GLState/Core.h"
#include <MG_Pipe/PipeInputsSwitch.h>
#include "MG_State/GLState/TextureState/TextureState.h" #include "MG_State/GLState/TextureState/TextureState.h"
#include "MG_Util/Classifiers/TextureEnumClassifier.h" #include "MG_Util/Classifiers/TextureEnumClassifier.h"
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h" #include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
@@ -383,6 +386,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
UpdateDynamicBackendParameters(); UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions(); UpdateAdvertisedExtensions();
if (MGB_CTX_LIVE) {
MGB_CTX->InvalidateCompileEnv();
}
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps, PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
MutableFormatCapabilities()); MutableFormatCapabilities());
PrintFormatCapabilities(GetFormatCapabilities()); PrintFormatCapabilities(GetFormatCapabilities());
@@ -495,33 +501,131 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.RendererName = "Magma", .RendererName = "Magma",
.BackendName = "Direct (Vulkan)", .BackendName = "Direct (Vulkan)",
.ExtraVendor = Nullopt, .ExtraVendor = Nullopt,
.RendererGLInfo = {.TargetGLVersion = {4, 0, 0}, .RendererGLInfo = {.TargetGLVersion = {4, 6, 0},
.TargetGLSLVersion = {4, 6, 0}, .TargetGLSLVersion = {4, 6, 0},
// Baseline advertisement (no shader subgroup, no timer queries); a // Baseline advertisement (no runtime-gated capabilities); a live
// live backend reconciles its copy in UpdateAdvertisedExtensions. // backend reconciles its copy in UpdateAdvertisedExtensions.
.Extensions = BuildAdvertisedExtensions(false, false, false), .Extensions = BuildAdvertisedExtensions(false, false, false, false, false),
.IsCompatibilityProfile = false}, .IsCompatibilityProfile = false},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}}; .StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
return rendererInfo; return rendererInfo;
} }
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported, Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
Bool anisotropicFilteringSupported) { Bool anisotropicFilteringSupported,
Bool nonZeroIndirectBaseInstanceSupported,
Bool cubeMapArraySupported) {
Vector<GLExtension> extensions = { Vector<GLExtension> extensions = {
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend, // The version tokens have to reach the version the backend actually claims:
// TargetGLVersion is {4,6,0}, and a list that stopped at OpenGL40 told an
// application feature-detecting off these tokens the opposite of what
// GL_MAJOR_VERSION / GL_MINOR_VERSION told it.
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, V_OpenGL41, V_OpenGL42, V_OpenGL43,
V_OpenGL44, V_OpenGL45, V_OpenGL46,
E_GL_ARB_draw_buffers_blend,
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store, E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_multi_draw_indirect, E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_draw_indirect,
E_GL_ARB_multi_draw_indirect,
E_GL_ARB_indirect_parameters, E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_indirect_parameters, E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample, E_GL_ARB_texture_multisample, E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample, E_GL_ARB_texture_multisample,
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access, E_GL_ARB_shader_draw_parameters, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access, E_GL_ARB_shader_draw_parameters,
E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size, E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size,
E_GL_ARB_explicit_attrib_location, E_GL_ARB_explicit_attrib_location,
// Core since GL 3.1 and implemented for every version advertised here. The string
// matters because applications gate the ENTRY POINTS on it rather than on the
// version: a caller that finds the extension missing never resolves
// glGetUniformBlockIndex / glUniformBlockBinding, and one that then uses uniform
// blocks anyway calls through a null pointer.
E_GL_ARB_uniform_buffer_object,
// Sampling the stencil aspect through DEPTH_STENCIL_TEXTURE_MODE. Core from 4.3,
// so on a 4.0 context the string is the only way to reach it.
E_GL_ARB_stencil_texturing,
// Unconditional, unlike DirectGLES: a GL texture view is a second set of VkImageViews
// over the same VkImage with a sub-range and possibly a reinterpreted VkFormat, which
// is core Vulkan on every device MobileGL runs on. Format-reinterpreting views need
// VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT on the image, which SyncTextureResource sets for
// every immutable-storage texture (see the comment there).
E_GL_ARB_texture_view,
// Core since 3.2 and implemented here on both backends - glDrawElementsBaseVertex,
// glDrawRangeElementsBaseVertex, glDrawElementsInstancedBaseVertex and
// glMultiDrawElementsBaseVertex all reach real per-draw vertex rebasing. The string
// was simply never emitted, which left KHR-GL4*.draw_elements_base_vertex_tests
// NotSupported on a feature that works.
E_GL_ARB_draw_elements_base_vertex,
// The whole sync-object family is real and core since 3.2: glFenceSync, glIsSync,
// glDeleteSync, glClientWaitSync, glWaitSync and glGetSynciv all live in GLImpl over a
// backend fence (a VkFence here, an EGLSync/GLsync on DirectGLES), and glGetInteger64v
// answers GL_MAX_SERVER_WAIT_TIMEOUT. The string matters for the same reason
// ARB_uniform_buffer_object's does: LWJGL builds GLCapabilities from the extension
// list, and a caller that finds GL_ARB_sync missing never resolves the entry points -
// then calls through null if it uses fences anyway. Nothing in the CTS gates on this
// string, so it is advertised on the strength of the implementation, not a test unlock.
E_GL_ARB_sync,
// Atomic counters, core since 4.2. glGetActiveAtomicCounterBufferiv and the whole
// GL_ATOMIC_COUNTER_BUFFER_* query family are real in GLImpl, and the counter buffer
// now reaches the shader on BOTH backends - Magma resolves the lowered
// gl_AtomicCounterBlock_<N> from the atomic-counter binding points rather than the
// shader-storage ones (see ResolveStorageBufferDescriptor). Withheld here until that
// landed, because the counter silently read whatever was bound as SSBO N instead.
E_GL_ARB_shader_atomic_counters,
// glVertexAttribDivisor, core since 3.3 and real on both backends. Applications
// (Better Clouds' GLCompat among them) accept the extension string as an
// ALTERNATIVE to a 3.3 context when deciding whether instanced rendering is
// available, so withholding it makes MobileGL look less capable than it is.
E_GL_ARB_instanced_arrays,
// Core GL 3.0-4.3 plumbing that has been real here for as long as the backend has
// existed, and that was simply never named. None of these unlocks a single CTS case -
// the conformance suite reaches all of them through the version - so they are
// advertised for the OTHER consumer of this list: LWJGL builds GLCapabilities from the
// string set, and an application that gates its ENTRY POINTS on the string rather than
// on the version never resolves them and then calls through null. Each is backed by
// the entry points named beside it. Kept identical to the DirectGLES block so the two
// backends do not disagree about what MobileGL is.
//
// glBindVertexArray / glGenVertexArrays / glDeleteVertexArrays / glIsVertexArray.
E_GL_ARB_vertex_array_object,
// The 14 glSamplerParameter* / glGetSamplerParameter* entry points, including the
// integer-valued Iiv/Iuiv forms.
E_GL_ARB_sampler_objects,
// glMapBufferRange + glFlushMappedBufferRange, which ARB_buffer_storage's persistent
// maps are already built on top of.
E_GL_ARB_map_buffer_range,
// glCopyBufferSubData plus the GL_COPY_READ_BUFFER / GL_COPY_WRITE_BUFFER targets.
E_GL_ARB_copy_buffer,
// glCopyImageSubData, wired to a real backend hook on both backends.
E_GL_ARB_copy_image,
// GL_TEXTURE_SWIZZLE_{R,G,B,A,RGBA}, which map onto a VkImageView's component swizzle.
E_GL_ARB_texture_swizzle,
// GL_INT_2_10_10_10_REV / GL_UNSIGNED_INT_2_10_10_10_REV on glVertexAttribPointer plus
// the eight glVertexAttribP* entry points.
E_GL_ARB_vertex_type_2_10_10_10_rev,
// The R/RG internal formats. Named separately from the float ones because an
// application may check either.
E_GL_ARB_texture_rg,
// GL_DEPTH_COMPONENT32F and GL_DEPTH32F_STENCIL8.
E_GL_ARB_depth_buffer_float,
// The floating-point colour formats. Unlike the rest of this block this string DOES
// gate CTS cases - KHR-GL4*.internalformat.texture2d.*{16f,32f} is keyed on it with no
// core-version fallback, so eight cases per version list were NotSupported on formats
// the backend has always had.
E_GL_ARB_texture_float,
// glViewportArrayv / glViewportIndexedf{,v} / glScissorArrayv / glScissorIndexed{,v} /
// glDepthRangeArrayv / glDepthRangeIndexed / glGetFloati_v / glGetDoublei_v, over the
// 16 viewports GL_MAX_VIEWPORTS reports.
E_GL_ARB_viewport_array,
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the // Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
// extension explicitly permits. It is also the only thing that // extension explicitly permits. It is also the only thing that
// exposes glProgramParameteri before GL 4.1. // exposes glProgramParameteri before GL 4.1.
E_GL_ARB_get_program_binary}; E_GL_ARB_get_program_binary};
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) { // Vulkan's drawIndirectFirstInstance feature is optional. Direct base-instance calls work
// without it, but ARB_base_instance also promises non-zero firstInstance in GPU indirect
// commands; the renderer supplies true only when that word is legal and gl_InstanceID can
// be rebased to OpenGL's zero-based semantics.
if (nonZeroIndirectBaseInstanceSupported) {
extensions.push_back(E_GL_ARB_base_instance);
}
if (shaderSubgroupSupported && !MG_Config::Features.MagmaDisableSubgroup) {
extensions.push_back(E_GL_KHR_shader_subgroup); extensions.push_back(E_GL_KHR_shader_subgroup);
} }
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the Vulkan // GL_KHR_parallel_shader_compile is MobileGL's own capability, not the Vulkan
@@ -539,6 +643,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (MG_Util::Async::AsyncShaderCompileEnabled()) { if (MG_Util::Async::AsyncShaderCompileEnabled()) {
extensions.push_back(E_GL_KHR_parallel_shader_compile); extensions.push_back(E_GL_KHR_parallel_shader_compile);
} }
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64), and stays opt-in even on a
// device that HAS shaderFloat64. Every `double` in a shader compiles and runs either way
// - narrowed to 32 bits where the device has no 64-bit floats, kept whole where it does -
// so an application that simply uses doubles needs nothing advertised. What the extension
// additionally promises is the whole GL_ARB_gpu_shader_fp64 SURFACE (glUniform*d
// conformance, the fp64 built-ins, the state queries), and turning the string on is a
// decision about all of it rather than about the shader path alone.
if (MG_Config::Features.AdvertiseFp64) {
extensions.push_back(E_GL_ARB_gpu_shader_fp64);
}
// GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension string); // GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension string);
// only advertised when the device actually supports timestamp queries and the // only advertised when the device actually supports timestamp queries and the
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off. // MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
@@ -552,6 +666,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
extensions.push_back(E_GL_EXT_texture_filter_anisotropic); extensions.push_back(E_GL_EXT_texture_filter_anisotropic);
extensions.push_back(E_GL_ARB_texture_filter_anisotropic); extensions.push_back(E_GL_ARB_texture_filter_anisotropic);
} }
// A cube map array is a 6n-layer VkImage viewed as VK_IMAGE_VIEW_TYPE_CUBE_ARRAY, and that
// view type cannot be created without the imageCubeArray device feature - so the string
// follows the feature, not the version, exactly as the per-layer attachment bit does.
//
// Named for the application's benefit rather than the suite's: measured on Adreno 830,
// KHR-GL43.texture_gather.plain-gather-*-cube-array already passed without the string, so
// this unlocks no conformance case. It is advertised because the feature is real and
// because an application that feature-detects cube map arrays off the string (rather than
// off the 4.0 version) would otherwise decline a path this backend serves.
if (cubeMapArraySupported) {
extensions.push_back(E_GL_ARB_texture_cube_map_array);
}
return extensions; return extensions;
} }
@@ -615,8 +741,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
funcsTable.GL.MemoryBarrierByRegion = MemoryBarrierByRegion; funcsTable.GL.MemoryBarrierByRegion = MemoryBarrierByRegion;
funcsTable.GL.BindImageTexture = BindImageTexture; funcsTable.GL.BindImageTexture = BindImageTexture;
funcsTable.GL.GetIntegeri_v = GetIntegeri_v; funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
funcsTable.GL.GetProgramiv = GetProgramiv;
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding; funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
funcsTable.GL.FenceSync = FenceSync; funcsTable.GL.FenceSync = FenceSync;
funcsTable.GL.ClientWaitSync = ClientWaitSync; funcsTable.GL.ClientWaitSync = ClientWaitSync;
@@ -660,6 +784,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_vulkanCaps = capabilities; m_vulkanCaps = capabilities;
UpdateDynamicBackendParameters(); UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions(); UpdateAdvertisedExtensions();
if (MGB_CTX_LIVE) {
MGB_CTX->InvalidateCompileEnv();
}
MutableFormatCapabilities().Clear(); MutableFormatCapabilities().Clear();
} }
@@ -670,9 +797,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// real device timestamp support. ApplyVulkanCapabilitiesForTesting may // real device timestamp support. ApplyVulkanCapabilitiesForTesting may
// run without a renderer; no timer query is advertised then. Rebuilding // run without a renderer; no timer query is advertised then. Rebuilding
// the whole list keeps re-runs idempotent. // the whole list keeps re-runs idempotent.
// The opt-in emulated compute path (SubgroupSupportPolicy.h) carries the
// extension by itself on devices with no native subgroup support at all; a
// device with native subgroups always advertises - and uses - those.
const Bool subgroupSupportAdvertised =
m_vulkanCaps.SupportsShaderSubgroup ||
ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup);
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions( m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(), subgroupSupportAdvertised, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported()); pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported(),
m_vulkanCaps.SupportsImageCubeArray);
} }
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() { void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
@@ -720,6 +855,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static constexpr SizeT kMaxAdvertisedShaderStorageBlockSize = 512ull * 1024ull * 1024ull; static constexpr SizeT kMaxAdvertisedShaderStorageBlockSize = 512ull * 1024ull * 1024ull;
m_dynamicParameters.UniformBufferOffsetAlignment = m_vulkanCaps.UniformBufferOffsetAlignment; m_dynamicParameters.UniformBufferOffsetAlignment = m_vulkanCaps.UniformBufferOffsetAlignment;
m_dynamicParameters.ShaderStorageBufferOffsetAlignment = m_vulkanCaps.ShaderStorageBufferOffsetAlignment;
m_dynamicParameters.AliasedLineWidthRangeMin = m_vulkanCaps.AliasedLineWidthRangeMin; m_dynamicParameters.AliasedLineWidthRangeMin = m_vulkanCaps.AliasedLineWidthRangeMin;
m_dynamicParameters.AliasedLineWidthRangeMax = m_vulkanCaps.AliasedLineWidthRangeMax; m_dynamicParameters.AliasedLineWidthRangeMax = m_vulkanCaps.AliasedLineWidthRangeMax;
// Without the samplerAnisotropy feature the limit is unusable, so report 1.0 (no anisotropy) // Without the samplerAnisotropy feature the limit is unusable, so report 1.0 (no anisotropy)
@@ -768,14 +904,89 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// the Uint32 attribute masks the draw path passes around are both bounded by MAX_VERTEX_ATTRIBS. // the Uint32 attribute masks the draw path passes around are both bounded by MAX_VERTEX_ATTRIBS.
m_dynamicParameters.MaxVertexAttribs = std::min( m_dynamicParameters.MaxVertexAttribs = std::min(
m_vulkanCaps.MaxVertexAttribs, static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS)); m_vulkanCaps.MaxVertexAttribs, static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_vulkanCaps.MaxComputeShaderStorageBlocks; // Vulkan descriptor limits are not GL limits, and a GL application reads an advertised
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_vulkanCaps.MaxCombinedShaderStorageBlocks; // limit as an amount it may actually USE. Adreno answers the per-stage/per-set descriptor
m_dynamicParameters.MaxComputeUniformBlocks = m_vulkanCaps.MaxComputeUniformBlocks; // queries at descriptor-indexing scale - the same driver whose
// GL_MAX_SHADER_STORAGE_BLOCK_SIZE is clamped from 2147483647 further down - so
// KHR-GL44.multi_bind.dispatch_bind_buffers_base read GL_MAX_COMPUTE_UNIFORM_BLOCKS,
// created that many buffers and spliced that many UBO declarations into a single compute
// shader: ~14 s of allocation, then death on std::bad_alloc. Its sibling
// dispatch_bind_buffers_range hard-codes 4 buffers and passes, which is the clean
// discriminator. Every ceiling below is far above what any desktop driver advertises for
// these (84-96 for the binding families) and far below a descriptor-indexing count, so it
// can only lower a limit that was never usable in the first place. The zero floor is not
// decoration: a driver reporting UINT32_MAX used to arrive here as -1.
const auto clampLimit = [](const char* name, Int reported, Int ceiling) {
const Int clamped = std::min(std::max(reported, 0), ceiling);
if (clamped != reported) {
MGLOG_I("DirectVulkan: clamped %s from %d to %d", name, reported, clamped);
}
return clamped;
};
// GL 4.6 required minimums, for the record: MAX_COMPUTE_UNIFORM_BLOCKS 12,
// MAX_COMPUTE/COMBINED_SHADER_STORAGE_BLOCKS 8, MAX_SHADER_STORAGE_BUFFER_BINDINGS 8,
// MAX_UNIFORM_BUFFER_BINDINGS 84, MAX_TEXTURE_BUFFER_SIZE 65536.
constexpr Int kMaxAdvertisedBufferBlocks = 256;
constexpr Int kMaxAdvertisedTextureBufferSize = 1 << 27; // texels; what desktop GL reports
m_dynamicParameters.MaxComputeShaderStorageBlocks =
clampLimit("GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS", m_vulkanCaps.MaxComputeShaderStorageBlocks,
kMaxAdvertisedBufferBlocks);
m_dynamicParameters.MaxCombinedShaderStorageBlocks =
clampLimit("GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", m_vulkanCaps.MaxCombinedShaderStorageBlocks,
kMaxAdvertisedBufferBlocks);
m_dynamicParameters.MaxComputeUniformBlocks =
clampLimit("GL_MAX_COMPUTE_UNIFORM_BLOCKS", m_vulkanCaps.MaxComputeUniformBlocks,
kMaxAdvertisedBufferBlocks);
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations; m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
m_dynamicParameters.MaxShaderStorageBufferBindings = m_vulkanCaps.MaxShaderStorageBufferBindings; // The six per-axis compute limits, from the same VkPhysicalDeviceLimits fields
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize; // GLFunctionsTable::GetIntegeri_v (DirectVulkan.cpp) reads live. Carried here so that
// MGPCaps has them once the table entry retires (plan B section 4.4.1); GL_Getter floors
// them. Not clamped: unlike the block counts these are not amounts an application
// allocates, and the frontend already raises them to the GL minimum.
for (SizeT axis = 0; axis < 3; ++axis) {
m_dynamicParameters.MaxComputeWorkGroupCount[axis] = m_vulkanCaps.MaxComputeWorkGroupCount[axis];
m_dynamicParameters.MaxComputeWorkGroupSize[axis] = m_vulkanCaps.MaxComputeWorkGroupSize[axis];
}
m_dynamicParameters.MaxShaderStorageBufferBindings =
clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings,
kMaxAdvertisedBufferBlocks);
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. Vulkan has one descriptor limit for every
// stage (maxPerStageDescriptorStorageBuffers, which is what MaxComputeShaderStorageBlocks
// carries), so the stage limits differ only by whether the stage can have blocks at all.
//
// Deliberately NOT gated on vertexPipelineStoresAndAtomics, unlike the per-stage image
// uniforms below. That gate reads as the obvious one and is wrong here in practice: a
// Mali-G925-Immortalis reports vertexPipelineStoresAndAtomics=false (supported AND
// enabled) and yet runs all 433 KHR-GL43.constant_expressions.*_tess_* cases correctly
// through this backend - those write their result through a storage block declared in a
// tessellation stage. Gating would report 0 and turn 433 passing cases into
// "unsupported", removing function that demonstrably works.
//
// The asymmetry with DirectGLES is real and is the point. There, 0 prevents a program
// the driver refuses outright at link time; the honest limit converts a silent
// wrong-render into a capability an application can route around. Here there is no such
// failure to prevent, so the limit stays at what the device can address. If a Vulkan
// device is ever found that genuinely rejects such a pipeline, the gate belongs at
// pipeline creation where the rejection is observable, not on a feature bit this driver
// reports inaccurately.
{
const Int maxPerStageStorageBlocks =
std::min(std::max(m_dynamicParameters.MaxComputeShaderStorageBlocks, 0),
std::min(std::max(m_dynamicParameters.MaxCombinedShaderStorageBlocks, 0),
std::max(m_dynamicParameters.MaxShaderStorageBufferBindings, 0)));
m_dynamicParameters.MaxVertexShaderStorageBlocks = maxPerStageStorageBlocks;
m_dynamicParameters.MaxTessControlShaderStorageBlocks = maxPerStageStorageBlocks;
m_dynamicParameters.MaxTessEvaluationShaderStorageBlocks = maxPerStageStorageBlocks;
// The one hard capability in the set: no geometry stage means no blocks in it.
m_dynamicParameters.MaxGeometryShaderStorageBlocks =
m_vulkanCaps.SupportsGeometryShader ? maxPerStageStorageBlocks : 0;
m_dynamicParameters.MaxFragmentShaderStorageBlocks = maxPerStageStorageBlocks;
}
m_dynamicParameters.MaxTextureBufferSize = clampLimit(
"GL_MAX_TEXTURE_BUFFER_SIZE", m_vulkanCaps.MaxTextureBufferSize, kMaxAdvertisedTextureBufferSize);
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment; m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings; m_dynamicParameters.MaxUniformBufferBindings = clampLimit(
"GL_MAX_UNIFORM_BUFFER_BINDINGS", m_vulkanCaps.MaxUniformBufferBindings, kMaxAdvertisedBufferBlocks);
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize; m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
m_dynamicParameters.MaxImageUnits = std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0); m_dynamicParameters.MaxImageUnits = std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0); m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0);
@@ -797,8 +1008,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Int maxSupportedDrawBuffers = static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS); const Int maxSupportedDrawBuffers = static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers); m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers);
m_dynamicParameters.MaxColorAttachments = std::min(m_vulkanCaps.MaxColorAttachments, maxSupportedDrawBuffers); m_dynamicParameters.MaxColorAttachments = std::min(m_vulkanCaps.MaxColorAttachments, maxSupportedDrawBuffers);
m_dynamicParameters.MaxClipDistances = m_vulkanCaps.MaxClipDistances; // Same shape as the image-uniform limits three lines above: maxClipDistances is reported
// by every device, but declaring ClipDistance in a module needs the shaderClipDistance
// FEATURE, which VulkanRenderer enables exactly where the physical device has it. Without
// it the limit describes a capacity no shader may use, so report none.
m_dynamicParameters.MaxClipDistances =
m_vulkanCaps.SupportsShaderClipDistance ? std::max(m_vulkanCaps.MaxClipDistances, 0) : 0;
// The cull pair, gated on its own feature. shaderCullDistance is separate from
// shaderClipDistance and VulkanRenderer enables it independently, so it gets its own
// gate rather than riding on the clip one.
m_dynamicParameters.MaxCullDistances =
m_vulkanCaps.SupportsShaderCullDistance ? std::max(m_vulkanCaps.MaxCullDistances, 0) : 0;
// GL 4.6 core 11.1.3.10: the combined limit is at least as large as either half. A device
// with only one of the two features must not report a combined capacity that implies the
// other, so the gate is "either feature" and the value never drops below what is enabled.
m_dynamicParameters.MaxCombinedClipAndCullDistances =
(m_vulkanCaps.SupportsShaderClipDistance || m_vulkanCaps.SupportsShaderCullDistance)
? std::max({m_vulkanCaps.MaxCombinedClipAndCullDistances, m_dynamicParameters.MaxClipDistances,
m_dynamicParameters.MaxCullDistances})
: 0;
m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports; m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports;
// Assigned explicitly rather than left to the struct's defaults, like every other
// parameter here, so a second fill cannot inherit a stale value. GL_UNDEFINED_VERTEX is
// the truthful answer for DirectVulkan and a legal one (GL 4.6 table 23.65): which vertex
// provokes is chosen per pipeline by VulkanRenderer::SelectProvokingVertexMode out of
// VK_EXT_provoking_vertex, provokingVertexModePerPipeline and the topology, so there is no
// one convention to name. Vulkan's own default is FIRST, which is the opposite of the
// GL_LAST_VERTEX_CONVENTION this used to claim unconditionally.
m_dynamicParameters.LayerProvokingVertex = GL_UNDEFINED_VERTEX;
m_dynamicParameters.ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
m_dynamicParameters.MaxViewportWidth = m_vulkanCaps.MaxViewportWidth; m_dynamicParameters.MaxViewportWidth = m_vulkanCaps.MaxViewportWidth;
m_dynamicParameters.MaxViewportHeight = m_vulkanCaps.MaxViewportHeight; m_dynamicParameters.MaxViewportHeight = m_vulkanCaps.MaxViewportHeight;
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin; m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
@@ -843,7 +1081,60 @@ namespace MobileGL::MG_Backend::DirectVulkan {
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray); DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
} }
} }
m_dynamicParameters.SupportsFloat64VertexAttributes = m_vulkanCaps.SupportsShaderFloat64; // The device feature the whole fp64 story hangs off. With it, a module keeps its
// OpCapability Float64 and real doubles reach the driver; without it the transpile
// narrows every 64-bit float to 32 (ShaderTranspiler::DemoteFloat64Pass), because
// VUID-VkShaderModuleCreateInfo-pCode-08740 forbids the capability outright and no
// pipeline could be built from such a module. lavapipe reports it; Adreno and Mali both
// report VK_FALSE, so on every real mobile device this is false and the demotion runs
// exactly as it always has.
m_dynamicParameters.SupportsShaderFloat64 = m_vulkanCaps.SupportsShaderFloat64;
// shaderTessellationAndGeometryPointSize, both stage families from the one feature.
// False arms the shared phase-B point-size demotion, whose modules then carry no
// TessellationPointSize/GeometryPointSize capability and build without the feature.
// MOBILEGL_POINT_SIZE_DEMOTION=1 pretends it is absent so the demotion can be
// exercised on a healthy driver (lavapipe advertises the feature); =0 restores the
// detected answer's declines.
{
Bool supportsStagePointSize = m_vulkanCaps.SupportsTessellationAndGeometryPointSize;
switch (MG_Config::Features.PointSizeDemotion) {
case MG_Config::QuirkOverride::ForceOn:
MGLOG_I("DirectVulkan: MOBILEGL_POINT_SIZE_DEMOTION=1 - treating tessellation/geometry "
"gl_PointSize as unhosted so the demotion runs on this driver");
supportsStagePointSize = false;
break;
case MG_Config::QuirkOverride::ForceOff:
MGLOG_I("DirectVulkan: MOBILEGL_POINT_SIZE_DEMOTION=0 - keeping the built-in and the "
"plain declines regardless of the device feature");
supportsStagePointSize = true;
break;
case MG_Config::QuirkOverride::Auto:
break;
}
m_dynamicParameters.SupportsTessellationPointSize = supportsStagePointSize;
m_dynamicParameters.SupportsGeometryPointSize = supportsStagePointSize;
}
// Never, on any device, and DELIBERATELY NOT COUPLED to the line above even though it
// once tracked the same feature. It used to, because a `dvec` input needed Float64 to
// exist in the module at all; a 64-bit vertex FETCH was already impossible
// (VK_FORMAT_R64*_SFLOAT is optional and lavapipe reports zero bufferFeatures for all
// four), so the attribute arrived as its 32-bit word pair and PackDoubleVertexInputsPass
// bitcast it back.
//
// Re-coupling it does not work, and the reason is worth recording because it is not
// obvious: this flag decides the VkFormat from the VAO ATTRIBUTE alone, and the attribute
// does not know what the shader declared. glVertexAttribFormat(GL_DOUBLE) against a plain
// `in vec4` is not only legal but the common case
// (KHR-GL43.vertex_attrib_binding.basic-input-case4 does exactly that, and case5 adds
// normalized=GL_TRUE), and advanced-bindingUpdate feeds a dvec3 the same way - GL defines
// all of them as "doubles in memory, converted to float". Turning the flag on turns the
// narrowing OFF for every one of them and the attributes come back unfetched.
//
// What keeps the two halves honest instead is a per-MODULE decision: a vertex module that
// declares a 64-bit float INPUT is demoted whole, even where the backend has native fp64,
// so `dvec` inputs are `vec` inputs on this backend exactly as they always were. See
// ShaderCompiler::SanitizeAndOptimizeBinary.
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
m_dynamicParameters.MaxShaderStorageBlockSize = m_dynamicParameters.MaxShaderStorageBlockSize =
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize); std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
if (m_vulkanCaps.SupportsShaderSubgroup) { if (m_vulkanCaps.SupportsShaderSubgroup) {
@@ -852,6 +1143,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.SubgroupSupportedFeatures = m_dynamicParameters.SubgroupSupportedFeatures =
mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations); mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
m_dynamicParameters.SubgroupQuadOperationsInAllStages = m_vulkanCaps.SubgroupQuadOperationsInAllStages; m_dynamicParameters.SubgroupQuadOperationsInAllStages = m_vulkanCaps.SubgroupQuadOperationsInAllStages;
} else if (ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup)) {
// MOBILEGL_MAGMA_EMULATE_SUBGROUP on a device with no native subgroups: the
// advertised values describe the 32-lane virtual subgroup the compute
// lowering implements (SubgroupSupportPolicy.h / EmulateSubgroupsPass).
// GL requires the advertisement and the execution to agree, and on this
// path the emulation is what executes; only the compute stage is offered.
m_dynamicParameters.SubgroupSize = kEmulatedSubgroupSize;
m_dynamicParameters.SubgroupSupportedStages = kEmulatedSubgroupStages;
m_dynamicParameters.SubgroupSupportedFeatures = kEmulatedSubgroupFeatures;
m_dynamicParameters.SubgroupQuadOperationsInAllStages = false;
MGLOG_I("DirectVulkan: emulating 32-lane compute subgroups "
"(MOBILEGL_MAGMA_EMULATE_SUBGROUP, no native subgroup support)");
} else { } else {
m_dynamicParameters.SubgroupSize = 0; m_dynamicParameters.SubgroupSize = 0;
m_dynamicParameters.SubgroupSupportedStages = 0; m_dynamicParameters.SubgroupSupportedStages = 0;
@@ -62,19 +62,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// POST screen shows. // POST screen shows.
// Static identity of the Magma renderer (renderer/backend names, target GL/GLSL // Static identity of the Magma renderer (renderer/backend names, target GL/GLSL
// versions, ExtraVendor) with the baseline extension advertisement (no shader // versions, ExtraVendor) with the baseline extension advertisement (no runtime-gated
// subgroup, no timer queries). A live backend copies this in its constructor and // capabilities). A live backend copies this in its constructor and
// reconciles the Extensions in UpdateAdvertisedExtensions once real capabilities // reconciles the Extensions in UpdateAdvertisedExtensions once real capabilities
// exist; callers that need the advertised list for a known capability set must // exist; callers that need the advertised list for a known capability set must
// use BuildAdvertisedExtensions instead. // use BuildAdvertisedExtensions instead.
const RendererInfo& GetRendererIdentity(); const RendererInfo& GetRendererIdentity();
// The full OpenGL extension list Magma advertises (glGetString(GL_EXTENSIONS)) for // The full OpenGL extension list Magma advertises (glGetString(GL_EXTENSIONS)) for
// a device with the given raw capabilities. The MOBILEGL_DISABLE_SUBGROUP and // a device with the given raw capabilities. The MOBILEGL_MAGMA_DISABLE_SUBGROUP and
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass // MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
// the detected device support (passing an already-gated value is harmless). // the detected device support (passing an already-gated value is harmless).
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported, Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
Bool anisotropicFilteringSupported); Bool anisotropicFilteringSupported,
Bool nonZeroIndirectBaseInstanceSupported,
Bool cubeMapArraySupported);
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact // Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
// string an initialized backend returns from GetBackendAPIVersionString (and that // string an initialized backend returns from GetBackendAPIVersionString (and that
+162 -182
View File
@@ -10,9 +10,11 @@
#include "DirectVulkanResourceState.h" #include "DirectVulkanResourceState.h"
#include "MG_Backend/BackendObjects.h" #include "MG_Backend/BackendObjects.h"
#include "MG_State/GLState/Core.h" #include "MG_State/GLState/Core.h"
#include <MG_Pipe/PipeInputsSwitch.h>
#include "MG_State/GLState/ErrorState/ErrorInfo.h" #include "MG_State/GLState/ErrorState/ErrorInfo.h"
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h" #include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h" #include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
#include "MG_Util/Metrics/PipeStats.h"
#include "MG_Util/Metrics/TextureMetrics.h" #include "MG_Util/Metrics/TextureMetrics.h"
#include "MG_Util/Miscellany/IndexGenerator.h" #include "MG_Util/Miscellany/IndexGenerator.h"
#include <atomic> #include <atomic>
@@ -69,9 +71,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// slot's ownership unambiguous. // slot's ownership unambiguous.
Uint64 programLifetimeId = 0; Uint64 programLifetimeId = 0;
Uint32 backendStateVersion = 0; Uint32 backendStateVersion = 0;
// glShaderStorageBlockBinding deliberately does NOT bump the backend state
// version, and the pipeline composite is unnamed so the in-place patch in
// DirectVulkan::ShaderStorageBlockBinding can never reach its slot - the
// mirror replay bumps only the program's block-binding version. Without this
// key the composite's slot kept serving the pre-rebind block.binding.
Uint32 blockBindingVersion = 0;
Vector<StorageBlockResource> storageBlocks; Vector<StorageBlockResource> storageBlocks;
Vector<BufferVariableResource> bufferVariables; Vector<BufferVariableResource> bufferVariables;
GLint computeWorkGroupSize[3] = {1, 1, 1};
}; };
struct DrawElementsIndirectCommand { struct DrawElementsIndirectCommand {
@@ -156,18 +163,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& cache = g_programResourceCaches[program.GetExternalIndex()]; auto& cache = g_programResourceCaches[program.GetExternalIndex()];
const Uint64 programLifetimeId = program.GetLifetimeId(); const Uint64 programLifetimeId = program.GetLifetimeId();
const Uint32 backendStateVersion = program.GetBackendStateVersion(); const Uint32 backendStateVersion = program.GetBackendStateVersion();
const Uint32 blockBindingVersion = program.GetBlockBindingVersion();
// The lifetime id must match too: a new program that reuses a deleted // The lifetime id must match too: a new program that reuses a deleted
// program's name and happens to land on the same backendStateVersion (both // program's name and happens to land on the same backendStateVersion (both
// count from zero) would otherwise be served the dead program's reflection. // count from zero) would otherwise be served the dead program's reflection.
if (cache.programLifetimeId == programLifetimeId && if (cache.programLifetimeId == programLifetimeId &&
cache.backendStateVersion == backendStateVersion && cache.backendStateVersion == backendStateVersion &&
(!cache.storageBlocks.empty() || !cache.bufferVariables.empty())) { (!cache.storageBlocks.empty() || !cache.bufferVariables.empty())) {
if (cache.blockBindingVersion != blockBindingVersion) {
// Only the block bindings moved (glShaderStorageBlockBinding, or the
// pipeline composite's mirror replay - neither touches the backend
// state version): the reflection itself is unchanged, so re-apply the
// overrides by name instead of re-running spirv-reflect. Overrides
// only ever accumulate, so a block without one still holds its
// declared binding.
for (auto& block : cache.storageBlocks) {
const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name);
if (rebound >= 0) block.binding = static_cast<Uint32>(rebound);
}
cache.blockBindingVersion = blockBindingVersion;
}
return cache; return cache;
} }
cache = {}; cache = {};
cache.programLifetimeId = programLifetimeId; cache.programLifetimeId = programLifetimeId;
cache.backendStateVersion = backendStateVersion; cache.backendStateVersion = backendStateVersion;
cache.blockBindingVersion = blockBindingVersion;
Vector<SpvReflectShaderModule> modules; Vector<SpvReflectShaderModule> modules;
Vector<Bool> validModules; Vector<Bool> validModules;
@@ -187,16 +209,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
for (auto& module : modules) { for (auto& module : modules) {
for (Uint32 entryIndex = 0; entryIndex < module.entry_point_count; ++entryIndex) {
const auto& entryPoint = module.entry_points[entryIndex];
if ((entryPoint.shader_stage & SPV_REFLECT_SHADER_STAGE_COMPUTE_BIT) == 0) {
continue;
}
cache.computeWorkGroupSize[0] = static_cast<GLint>(std::max<Uint32>(entryPoint.local_size.x, 1));
cache.computeWorkGroupSize[1] = static_cast<GLint>(std::max<Uint32>(entryPoint.local_size.y, 1));
cache.computeWorkGroupSize[2] = static_cast<GLint>(std::max<Uint32>(entryPoint.local_size.z, 1));
}
uint32_t bindingCount = 0; uint32_t bindingCount = 0;
SpvReflectResult result = spvReflectEnumerateDescriptorBindings(&module, &bindingCount, nullptr); SpvReflectResult result = spvReflectEnumerateDescriptorBindings(&module, &bindingCount, nullptr);
if (result != SPV_REFLECT_RESULT_SUCCESS || bindingCount == 0) { if (result != SPV_REFLECT_RESULT_SUCCESS || bindingCount == 0) {
@@ -256,27 +268,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
MG_State::GLState::ProgramObject* TryGetDirectVulkanProgram(GLuint program) { MG_State::GLState::ProgramObject* TryGetDirectVulkanProgram(GLuint program) {
if (!MG_State::pGLContext->ValidateProgramName(program)) { if (!MGB_CTX->ValidateProgramName(program)) {
return nullptr; return nullptr;
} }
auto& programObject = MG_State::pGLContext->GetProgramObject(program); auto& programObject = MGB_CTX->GetProgramObject(program);
return programObject.get(); return programObject.get();
} }
const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) { const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) {
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (drawBuffer) { if (drawBuffer) {
drawBuffer->SyncPersistentMappedRange(); drawBuffer->SyncPersistentMappedRange();
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect); const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
if (drawBuffer->MappedData() == nullptr || commandOffset + requiredBytes > drawBuffer->GetSize()) { if (drawBuffer->MappedData() == nullptr || commandOffset + requiredBytes > drawBuffer->GetSize()) {
MGLOG_E("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label); MGLOG_E_ONCE("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
return nullptr; return nullptr;
} }
return drawBuffer->MappedData() + commandOffset; return drawBuffer->MappedData() + commandOffset;
} }
if (!indirect) { if (!indirect) {
MGLOG_E("%s skipped: indirect pointer is null", label); MGLOG_E_ONCE("%s skipped: indirect pointer is null", label);
return nullptr; return nullptr;
} }
@@ -323,64 +335,64 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearBufferfi called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearBufferfi called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearBufferfi called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearBufferfi called with null GL context");
pVulkanRenderer->ClearBufferfi(buffer, drawbuffer, depth, stencil); pVulkanRenderer->ClearBufferfi(buffer, drawbuffer, depth, stencil);
} }
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value) { void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearBufferfv called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearBufferfv called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearBufferfv called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearBufferfv called with null GL context");
pVulkanRenderer->ClearBufferfv(buffer, drawbuffer, value); pVulkanRenderer->ClearBufferfv(buffer, drawbuffer, value);
} }
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value) { void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearBufferuiv called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearBufferuiv called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearBufferuiv called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearBufferuiv called with null GL context");
pVulkanRenderer->ClearBufferuiv(buffer, drawbuffer, value); pVulkanRenderer->ClearBufferuiv(buffer, drawbuffer, value);
} }
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value) { void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearBufferiv called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearBufferiv called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearBufferiv called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearBufferiv called with null GL context");
pVulkanRenderer->ClearBufferiv(buffer, drawbuffer, value); pVulkanRenderer->ClearBufferiv(buffer, drawbuffer, value);
} }
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer, void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLfloat* value) { GLint drawbuffer, const GLfloat* value) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferfv called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferfv called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearNamedFramebufferfv called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearNamedFramebufferfv called with null GL context");
pVulkanRenderer->ClearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value); pVulkanRenderer->ClearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
} }
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer, void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLint* value) { GLint drawbuffer, const GLint* value) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferiv called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferiv called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearNamedFramebufferiv called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearNamedFramebufferiv called with null GL context");
pVulkanRenderer->ClearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value); pVulkanRenderer->ClearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
} }
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer, void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLuint* value) { GLint drawbuffer, const GLuint* value) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferuiv called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferuiv called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearNamedFramebufferuiv called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearNamedFramebufferuiv called with null GL context");
pVulkanRenderer->ClearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value); pVulkanRenderer->ClearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
} }
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer, void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, GLfloat depth, GLint stencil) { GLint drawbuffer, GLfloat depth, GLint stencil) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferfi called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferfi called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearNamedFramebufferfi called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ClearNamedFramebufferfi called with null GL context");
pVulkanRenderer->ClearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil); pVulkanRenderer->ClearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
} }
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) { void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsIndirect called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsIndirect called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElementsIndirect called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawElementsIndirect called with null GL context");
pVulkanRenderer->MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride); pVulkanRenderer->MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
} }
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) { void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawArraysIndirect called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawArraysIndirect called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawArraysIndirect called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawArraysIndirect called with null GL context");
if (drawcount <= 0) { if (drawcount <= 0) {
return; return;
@@ -388,7 +400,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// With a bound GL_DRAW_INDIRECT_BUFFER the command parameters may be GPU-written // With a bound GL_DRAW_INDIRECT_BUFFER the command parameters may be GPU-written
// (e.g. by a compute shader), so consume them natively on the GPU. // (e.g. by a compute shader), so consume them natively on the GPU.
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (drawBuffer) { if (drawBuffer) {
pVulkanRenderer->MultiDrawArraysIndirect(mode, indirect, drawcount, stride); pVulkanRenderer->MultiDrawArraysIndirect(mode, indirect, drawcount, stride);
return; return;
@@ -398,7 +410,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
stride = sizeof(DrawArraysIndirectCommand); stride = sizeof(DrawArraysIndirectCommand);
} }
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) { if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
MGLOG_E("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu", MGLOG_E_ONCE("MultiDrawArraysIndirect skipped: stride %d is smaller than command size %zu",
stride, sizeof(DrawArraysIndirectCommand)); stride, sizeof(DrawArraysIndirectCommand));
return; return;
} }
@@ -431,13 +443,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride) { GLsizei maxdrawcount, GLsizei stride) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsIndirectCount called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsIndirectCount called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElementsIndirectCount called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawElementsIndirectCount called with null GL context");
pVulkanRenderer->MultiDrawElementsIndirectCount(mode, type, indirect, drawcount, maxdrawcount, stride); pVulkanRenderer->MultiDrawElementsIndirectCount(mode, type, indirect, drawcount, maxdrawcount, stride);
} }
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount, void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride) { GLsizei maxdrawcount, GLsizei stride) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawArraysIndirectCount called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawArraysIndirectCount called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawArraysIndirectCount called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawArraysIndirectCount called with null GL context");
if (maxdrawcount <= 0) { if (maxdrawcount <= 0) {
return; return;
@@ -446,20 +458,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
stride = sizeof(DrawArraysIndirectCommand); stride = sizeof(DrawArraysIndirectCommand);
} }
if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) { if (stride < static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand))) {
MGLOG_E("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu", MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: stride %d is smaller than command size %zu",
stride, sizeof(DrawArraysIndirectCommand)); stride, sizeof(DrawArraysIndirectCommand));
return; return;
} }
auto parameterBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject(); auto parameterBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
if (!parameterBuffer || drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) { if (!parameterBuffer || drawcount < 0 || static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
MGLOG_E("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range"); MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range");
return; return;
} }
parameterBuffer->SyncPersistentMappedRange(); parameterBuffer->SyncPersistentMappedRange();
if (parameterBuffer->MappedData() == nullptr) { if (parameterBuffer->MappedData() == nullptr) {
MGLOG_E("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read parameter buffer"); MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read parameter buffer");
return; return;
} }
@@ -482,7 +494,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex, GLuint baseinstance) { GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsInstancedBaseVertexBaseInstance called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsInstancedBaseVertexBaseInstance called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElementsInstancedBaseVertexBaseInstance called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawElementsInstancedBaseVertexBaseInstance called with null GL context");
DrawIndexedCmd payload{}; DrawIndexedCmd payload{};
payload.mode = mode; payload.mode = mode;
@@ -509,17 +521,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) { void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsIndirect called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsIndirect called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElementsIndirect called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawElementsIndirect called with null GL context");
const SizeT indexSize = MG_Util::GetGLTypeSize(type); const SizeT indexSize = MG_Util::GetGLTypeSize(type);
if (indexSize == 0) { if (indexSize == 0) {
MGLOG_E("DrawElementsIndirect skipped: unsupported index type 0x%x", type); MGLOG_E_ONCE("DrawElementsIndirect skipped: unsupported index type 0x%x", type);
return; return;
} }
// With a bound GL_DRAW_INDIRECT_BUFFER the command parameters may be GPU-written // With a bound GL_DRAW_INDIRECT_BUFFER the command parameters may be GPU-written
// (e.g. by a compute shader), so consume them natively on the GPU. // (e.g. by a compute shader), so consume them natively on the GPU.
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (drawBuffer) { if (drawBuffer) {
pVulkanRenderer->MultiDrawElementsIndirect(mode, type, indirect, 1, 0); pVulkanRenderer->MultiDrawElementsIndirect(mode, type, indirect, 1, 0);
return; return;
@@ -553,7 +565,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount, void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
GLuint baseinstance) { GLuint baseinstance) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArraysInstancedBaseInstance called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArraysInstancedBaseInstance called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawArraysInstancedBaseInstance called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawArraysInstancedBaseInstance called with null GL context");
DrawCmd payload{}; DrawCmd payload{};
payload.mode = mode; payload.mode = mode;
@@ -568,11 +580,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
void DrawArraysIndirect(GLenum mode, const void* indirect) { void DrawArraysIndirect(GLenum mode, const void* indirect) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArraysIndirect called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArraysIndirect called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawArraysIndirect called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawArraysIndirect called with null GL context");
// With a bound GL_DRAW_INDIRECT_BUFFER the command parameters may be GPU-written // With a bound GL_DRAW_INDIRECT_BUFFER the command parameters may be GPU-written
// (e.g. by a compute shader), so consume them natively on the GPU. // (e.g. by a compute shader), so consume them natively on the GPU.
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (drawBuffer) { if (drawBuffer) {
pVulkanRenderer->MultiDrawArraysIndirect(mode, indirect, 1, 0); pVulkanRenderer->MultiDrawArraysIndirect(mode, indirect, 1, 0);
return; return;
@@ -602,47 +614,47 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
GLsizei height, GLint border) { GLsizei height, GLint border) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyTexImage2D called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyTexImage2D called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyTexImage2D called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::CopyTexImage2D called with null GL context");
pVulkanRenderer->CopyTexSubImage2D(target, level, 0, 0, x, y, width, height); pVulkanRenderer->CopyTexSubImage2D(target, level, 0, 0, x, y, width, height);
} }
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height) { GLsizei height) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyTexSubImage2D called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyTexSubImage2D called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyTexSubImage2D called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::CopyTexSubImage2D called with null GL context");
pVulkanRenderer->CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height); pVulkanRenderer->CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
} }
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, void CopyImageSubData(const CopyImageEndpoint& src,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, const CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyImageSubData called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyImageSubData called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyImageSubData called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::CopyImageSubData called with null GL context");
pVulkanRenderer->CopyImageSubData(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, pVulkanRenderer->CopyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ,
dstTexture, dstTarget, dstLevel, dstX, dstY, dstZ, dst, dstTarget, dstLevel, dstX, dstY, dstZ,
srcWidth, srcHeight, srcDepth); srcWidth, srcHeight, srcDepth);
} }
void GenerateMipmap(GLenum target) { void GenerateMipmap(GLenum target) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GenerateMipmap called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GenerateMipmap called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::GenerateMipmap called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::GenerateMipmap called with null GL context");
pVulkanRenderer->GenerateMipmap(target); pVulkanRenderer->GenerateMipmap(target);
} }
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) { void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DispatchCompute called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DispatchCompute called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DispatchCompute called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DispatchCompute called with null GL context");
pVulkanRenderer->DispatchCompute(numGroupsX, numGroupsY, numGroupsZ); pVulkanRenderer->DispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
} }
void DispatchComputeIndirect(GLintptr indirect) { void DispatchComputeIndirect(GLintptr indirect) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DispatchComputeIndirect called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DispatchComputeIndirect called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DispatchComputeIndirect called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DispatchComputeIndirect called with null GL context");
pVulkanRenderer->DispatchComputeIndirect(indirect); pVulkanRenderer->DispatchComputeIndirect(indirect);
} }
void MemoryBarrier(GLbitfield barriers) { void MemoryBarrier(GLbitfield barriers) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MemoryBarrier called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MemoryBarrier called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MemoryBarrier called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MemoryBarrier called with null GL context");
pVulkanRenderer->MemoryBarrier(barriers); pVulkanRenderer->MemoryBarrier(barriers);
} }
@@ -661,130 +673,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
(void)format; (void)format;
} }
// The two compute limits are the only indexed pnames a backend genuinely owns: they come
// from the physical device, and MG_Impl/GLImpl/Getter/GL_Getter.cpp asks for them here so it
// can raise the answer to the GL required minimum. The same six numbers are carried in
// DynamicBackendParameters::MaxComputeWorkGroupCount/Size (filled at capability init from
// the same limits), which is their MGPCaps carrier once this entry retires - the
// AdvertisedLimitsScenario pins the two against each other. Every other indexed pname names FRONTEND
// state (the indexed buffer bindings, the per-unit texture/sampler bindings, the image-unit
// bindings, the viewport rectangles, the indexed capabilities) and is answered there before
// the table is consulted, so the arms this function used to carry for
// GL_SHADER_STORAGE_BUFFER_* and GL_IMAGE_BINDING_* were unreachable duplicates - and not
// even faithful ones: the frontend reports the range glBindBufferRange was ASKED for,
// verbatim, while these clamped it to the buffer's current storage.
void GetIntegeri_v(GLenum target, GLuint index, GLint* data) { void GetIntegeri_v(GLenum target, GLuint index, GLint* data) {
if (!data) return; if (!data) return;
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GetIntegeri_v called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GetIntegeri_v called with null VulkanRenderer");
if (index >= 3) {
*data = 0;
return;
}
switch (target) { switch (target) {
case GL_MAX_COMPUTE_WORK_GROUP_COUNT: case GL_MAX_COMPUTE_WORK_GROUP_COUNT:
if (index >= 3) {
*data = 0;
return;
}
*data = static_cast<GLint>( *data = static_cast<GLint>(
pVulkanRenderer->GetPhysicalDevice().properties.limits.maxComputeWorkGroupCount[index]); pVulkanRenderer->GetPhysicalDevice().properties.limits.maxComputeWorkGroupCount[index]);
return; return;
case GL_MAX_COMPUTE_WORK_GROUP_SIZE: case GL_MAX_COMPUTE_WORK_GROUP_SIZE:
if (index >= 3) {
*data = 0;
return;
}
*data = static_cast<GLint>( *data = static_cast<GLint>(
pVulkanRenderer->GetPhysicalDevice().properties.limits.maxComputeWorkGroupSize[index]); pVulkanRenderer->GetPhysicalDevice().properties.limits.maxComputeWorkGroupSize[index]);
return; return;
case GL_SHADER_STORAGE_BUFFER_BINDING: {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
auto& obj = point.GetBoundObject();
*data = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
return;
}
case GL_SHADER_STORAGE_BUFFER_START: {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
*data = static_cast<GLint>(point.GetRange().start);
return;
}
case GL_SHADER_STORAGE_BUFFER_SIZE: {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
auto& obj = point.GetBoundObject();
if (!obj) {
*data = 0;
return;
}
const auto& range = point.GetRange();
const auto start = std::min(range.start, obj->GetSize());
const auto end = std::min(range.end, obj->GetSize());
*data = static_cast<GLint>(end - start);
return;
}
case GL_IMAGE_BINDING_NAME:
case GL_IMAGE_BINDING_LEVEL:
case GL_IMAGE_BINDING_LAYERED:
case GL_IMAGE_BINDING_LAYER:
case GL_IMAGE_BINDING_ACCESS:
case GL_IMAGE_BINDING_FORMAT: {
if (index >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
*data = 0;
return;
}
auto& imageBinding = MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(index));
if (target == GL_IMAGE_BINDING_NAME) {
*data = imageBinding.Texture ? static_cast<GLint>(imageBinding.Texture->GetExternalIndex()) : 0;
} else if (target == GL_IMAGE_BINDING_LEVEL) {
*data = imageBinding.Level;
} else if (target == GL_IMAGE_BINDING_LAYERED) {
*data = imageBinding.Layered;
} else if (target == GL_IMAGE_BINDING_LAYER) {
*data = imageBinding.Layer;
} else if (target == GL_IMAGE_BINDING_ACCESS) {
*data = static_cast<GLint>(imageBinding.Access);
} else {
*data = static_cast<GLint>(imageBinding.Format);
}
return;
}
default: default:
*data = 0; *data = 0;
return; return;
} }
} }
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data) {
if (!data) return;
switch (target) {
case GL_SHADER_STORAGE_BUFFER_START: {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
*data = static_cast<GLint64>(point.GetRange().start);
return;
}
case GL_SHADER_STORAGE_BUFFER_SIZE: {
auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, index);
auto& obj = point.GetBoundObject();
if (!obj) {
*data = 0;
return;
}
const auto& range = point.GetRange();
const auto start = std::min(range.start, obj->GetSize());
const auto end = std::min(range.end, obj->GetSize());
*data = static_cast<GLint64>(end - start);
return;
}
default:
*data = 0;
return;
}
}
void GetProgramiv(GLuint program, GLenum pname, GLint* params) {
if (!params) return;
auto* programObject = TryGetDirectVulkanProgram(program);
if (!programObject) {
params[0] = 0;
return;
}
switch (pname) {
case GL_COMPUTE_WORK_GROUP_SIZE: {
auto& cache = GetProgramResourceCache(*programObject);
params[0] = cache.computeWorkGroupSize[0];
params[1] = cache.computeWorkGroupSize[1];
params[2] = cache.computeWorkGroupSize[2];
return;
}
default:
params[0] = 0;
return;
}
}
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) { void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) {
auto* programObject = TryGetDirectVulkanProgram(program); auto* programObject = TryGetDirectVulkanProgram(program);
if (!programObject || storageBlockName == nullptr) return; if (!programObject || storageBlockName == nullptr) return;
@@ -792,7 +714,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
? pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings ? pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings
: 0; : 0;
if (storageBlockBinding >= static_cast<GLuint>(maxBindings)) { if (storageBlockBinding >= static_cast<GLuint>(maxBindings)) {
MG_State::pGLContext->RecordError( MGB_CTX->RecordError(
ErrorCode::InvalidValue, ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("DirectVulkan", __func__, "Shader storage binding is out of range.")); MakeUnique<GenericErrorInfo>("DirectVulkan", __func__, "Shader storage binding is out of range."));
return; return;
@@ -801,31 +723,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// already recorded the new binding on the program - which is what reseeds this cache // already recorded the new binding on the program - which is what reseeds this cache
// whenever it is rebuilt. Writing the entry here as well keeps an ALREADY-BUILT cache // whenever it is rebuilt. Writing the entry here as well keeps an ALREADY-BUILT cache
// (the common case: the very next draw reads it) from having to be thrown away. // (the common case: the very next draw reads it) from having to be thrown away.
auto& cache = GetProgramResourceCache(*programObject); //
// Resolve the index BEFORE taking the reference, and bounds-check the way the
// sibling getter does. GetShaderStorageBlockIndex re-enters GetProgramResourceCache,
// which indexes g_programResourceCaches and can therefore insert - and that map is
// open-addressed, so a rehash MOVES its entries and a reference taken before the
// call is left dangling. Binding a program's storage block
// while another program's entry was still absent from the cache was a reproducible
// segfault (ProgramPipelineScenario's two storage-block cases, in one process).
const GLuint blockIndex = GetShaderStorageBlockIndex(*programObject, storageBlockName); const GLuint blockIndex = GetShaderStorageBlockIndex(*programObject, storageBlockName);
if (blockIndex == GL_INVALID_INDEX) return; if (blockIndex == GL_INVALID_INDEX) return;
auto& cache = GetProgramResourceCache(*programObject);
if (blockIndex >= cache.storageBlocks.size()) return;
cache.storageBlocks[blockIndex].binding = storageBlockBinding; cache.storageBlocks[blockIndex].binding = storageBlockBinding;
} }
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) { void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ReadPixels called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ReadPixels called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ReadPixels called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::ReadPixels called with null GL context");
pVulkanRenderer->ReadPixels(x, y, width, height, format, type, pixels); pVulkanRenderer->ReadPixels(x, y, width, height, format, type, pixels);
} }
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) { void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GetTexImage called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GetTexImage called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::GetTexImage called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::GetTexImage called with null GL context");
pVulkanRenderer->GetTexImage(target, level, format, type, pixels); pVulkanRenderer->GetTexImage(target, level, format, type, pixels);
} }
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget, void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,
GLint level, GLenum format, GLenum type, GLsizei bufSize, GLvoid* pixels) { GLint level, GLenum format, GLenum type, GLsizei bufSize, GLvoid* pixels) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GetTextureImage called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::GetTextureImage called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::GetTextureImage called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::GetTextureImage called with null GL context");
pVulkanRenderer->GetTextureImage(texture, uploadTarget, level, format, type, bufSize, pixels); pVulkanRenderer->GetTextureImage(texture, uploadTarget, level, format, type, bufSize, pixels);
} }
void Clear(GLbitfield mask) { void Clear(GLbitfield mask) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Clear called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Clear called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::Clear called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::Clear called with null GL context");
pVulkanRenderer->Clear(mask); pVulkanRenderer->Clear(mask);
} }
@@ -853,7 +784,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false; return false;
} }
const Uint8* indexBytes = nullptr; const Uint8* indexBytes = nullptr;
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray(); const auto& vao = *MGB_CTX->GetBoundVertexArray();
const auto& indexBufferShared = vao.GetIndexBufferBindingSlot().GetBoundObject(); const auto& indexBufferShared = vao.GetIndexBufferBindingSlot().GetBoundObject();
if (indexBufferShared != nullptr) { if (indexBufferShared != nullptr) {
const SizeT offset = reinterpret_cast<SizeT>(indices); const SizeT offset = reinterpret_cast<SizeT>(indices);
@@ -884,7 +815,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DrawArrays(GLenum mode, GLint first, GLsizei count) { void DrawArrays(GLenum mode, GLint first, GLsizei count) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArrays called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArrays called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawArrays called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawArrays called with null GL context");
if (mode == GL_LINE_LOOP) { if (mode == GL_LINE_LOOP) {
if (count < 2) { if (count < 2) {
@@ -909,7 +840,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) { void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElements called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElements called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElements called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawElements called with null GL context");
if (mode == GL_LINE_LOOP) { if (mode == GL_LINE_LOOP) {
Vector<Uint32> closedIndices; Vector<Uint32> closedIndices;
@@ -932,7 +863,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) { void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawArrays called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawArrays called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawArrays called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawArrays called with null GL context");
if (drawcount <= 0) { if (drawcount <= 0) {
return; return;
} }
@@ -966,6 +897,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (drawcount <= 0) { if (drawcount <= 0) {
return; return;
} }
// With no element-array buffer bound, every indices[i] is a client pointer into a
// separate CPU allocation, not an offset into one shared buffer. The batched payload
// below cannot express that: it carries ONE index-buffer view for the whole batch and
// turns each pointer into a firstIndex relative to it. Replay the sub-draws through
// the single-draw entry point instead - it snapshots each client range into its own
// transient slice, which is exactly what the unrolled draws this must match do.
// (The batch used to be built this way; the shared-view rewrite that added
// MultiDrawIndexedCmd left the client-memory shape addressing a view whose byte
// offset is a hardcoded 0, so UploadAndBindIndexBuffer saw a null client pointer,
// declined the whole batch and painted nothing.)
const auto& vao = *MGB_CTX->GetBoundVertexArray();
if (vao.GetIndexBufferBindingSlot().GetBoundObject() == nullptr) {
for (GLsizei i = 0; i < drawcount; ++i) {
if (count[i] <= 0) {
continue;
}
DrawElementsBaseVertex(mode, count[i], type, indices[i],
basevertex != nullptr ? basevertex[i] : 0);
}
return;
}
MultiDrawIndexedCmd payload{}; MultiDrawIndexedCmd payload{};
payload.mode = mode; payload.mode = mode;
payload.indexBufferView.indexType = type; payload.indexBufferView.indexType = type;
@@ -977,7 +931,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// shift - the hardware divide was the hottest instruction of this loop. // shift - the hardware divide was the hottest instruction of this loop.
const SizeT indexSize = MG_Util::GetGLTypeSize(type); const SizeT indexSize = MG_Util::GetGLTypeSize(type);
if (indexSize == 0) { if (indexSize == 0) {
MGLOG_E("MultiDrawElements skipped: unsupported index type 0x%x", type); MGLOG_E_ONCE("MultiDrawElements skipped: unsupported index type 0x%x", type);
return; return;
} }
const Uint32 indexSizeShift = static_cast<Uint32>(std::countr_zero(indexSize)); const Uint32 indexSizeShift = static_cast<Uint32>(std::countr_zero(indexSize));
@@ -1015,13 +969,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount) { GLsizei drawcount) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElements called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElements called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElements called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawElements called with null GL context");
MultiDrawElementsImpl(mode, count, type, indices, drawcount, nullptr); MultiDrawElementsImpl(mode, count, type, indices, drawcount, nullptr);
} }
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) { void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsBaseVertex called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsBaseVertex called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElementsBaseVertex called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::DrawElementsBaseVertex called with null GL context");
if (mode == GL_LINE_LOOP) { if (mode == GL_LINE_LOOP) {
Vector<Uint32> closedIndices; Vector<Uint32> closedIndices;
if (BuildClosedLineLoopIndices(count, type, indices, closedIndices)) { if (BuildClosedLineLoopIndices(count, type, indices, closedIndices)) {
@@ -1045,14 +999,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex) { GLsizei drawcount, const GLint* basevertex) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsBaseVertex called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsBaseVertex called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElementsBaseVertex called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::MultiDrawElementsBaseVertex called with null GL context");
MultiDrawElementsImpl(mode, count, type, indices, drawcount, basevertex); MultiDrawElementsImpl(mode, count, type, indices, drawcount, basevertex);
} }
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter) { GLint dstY1, GLbitfield mask, GLenum filter) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::BlitFramebuffer called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::BlitFramebuffer called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::BlitFramebuffer called with null GL context"); MOBILEGL_ASSERT(MGB_CTX_LIVE, "DirectVulkan::BlitFramebuffer called with null GL context");
pVulkanRenderer->BlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); pVulkanRenderer->BlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
} }
@@ -1153,6 +1107,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
SharedPtr<VkTimerQueryManager::TimestampRecord> end; SharedPtr<VkTimerQueryManager::TimestampRecord> end;
// Kind::Occlusion - pool slots recorded between Begin/End; summed at result time. // Kind::Occlusion - pool slots recorded between Begin/End; summed at result time.
Vector<Uint32> occlusionSlots; Vector<Uint32> occlusionSlots;
// Kind::XfbGenerated - reroute-pool slots for the span's XFB-INACTIVE
// draws, where the renderer's reroute is armed (the affected driver's
// stream query counts nothing without an open capture; see
// VulkanRenderer::BeginXfbQueryForDraw). Summed alongside the stream
// slots above, which keep the span's XFB-active draws.
Vector<Uint32> rerouteSlots;
// Renderer generation the records were written under (see // Renderer generation the records were written under (see
// g_rendererGeneration). A stale generation resolves as available // g_rendererGeneration). A stale generation resolves as available
// with a final zero result: the records' pool indices and frame // with a final zero result: the records' pool indices and frame
@@ -1162,11 +1122,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// stale queries are always safe to delete. // stale queries are always safe to delete.
Uint64 rendererGeneration = 0; Uint64 rendererGeneration = 0;
// Kind::XfbGenerated - the frontend's paused-draw primitive counter when the // Kind::XfbGenerated - the frontend's paused-draw primitive counter when the
// query began. VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT counts only what the // query began. On the affected drivers VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT
// capture saw, so a draw made while the span was paused is invisible to it - // counts only what the capture saw, so a draw made while the span was paused is
// but GL_PRIMITIVES_GENERATED counts what the last vertex processing stage // invisible to it - but GL_PRIMITIVES_GENERATED counts what the last vertex
// emitted regardless. The delta closes that gap at result time. // processing stage emitted regardless. The delta closes that gap at result time.
Uint64 pausedPrimitiveSnapshot = 0; Uint64 pausedPrimitiveSnapshot = 0;
// ...unless the GPU already counted those paused draws when the span opened -
// through the reroute pool (VulkanRenderer::BeginXfbQueryForDraw reroutes every
// draw with no open capture, paused ones included) or, where the probe measured
// the stream query as counting capture-less draws, through the stream slot the
// paused draw still takes. Adding the CPU delta on top would count them twice,
// and the CPU counter is the weaker source anyway: only 3 of the ~15 draw entry
// points write it and it answers 0 for GL_PATCHES.
Bool pausedPrimitivesCountedByGpu = false;
}; };
} // namespace } // namespace
@@ -1260,13 +1228,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (query->kind == VulkanTimerQuery::Kind::XfbWritten || if (query->kind == VulkanTimerQuery::Kind::XfbWritten ||
query->kind == VulkanTimerQuery::Kind::XfbGenerated) { query->kind == VulkanTimerQuery::Kind::XfbGenerated) {
Uint64 primitives = 0; Uint64 primitives = 0;
if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots, if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots, query->rerouteSlots,
query->kind == VulkanTimerQuery::Kind::XfbGenerated, query->kind == VulkanTimerQuery::Kind::XfbGenerated,
primitives)) { primitives)) {
return false; return false;
} }
if (query->kind == VulkanTimerQuery::Kind::XfbGenerated && MG_State::pGLContext != nullptr) { if (query->kind == VulkanTimerQuery::Kind::XfbGenerated &&
primitives += MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() - !query->pausedPrimitivesCountedByGpu && MGB_CTX_LIVE) {
primitives += MGB_CTX->GetTransformFeedbackPausedPrimitiveCounter() -
query->pausedPrimitiveSnapshot; query->pausedPrimitiveSnapshot;
} }
*outNanoseconds = primitives; *outNanoseconds = primitives;
@@ -1313,7 +1282,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
query->kind = generated ? VulkanTimerQuery::Kind::XfbGenerated : VulkanTimerQuery::Kind::XfbWritten; query->kind = generated ? VulkanTimerQuery::Kind::XfbGenerated : VulkanTimerQuery::Kind::XfbWritten;
query->rendererGeneration = GetRendererGeneration(); query->rendererGeneration = GetRendererGeneration();
query->pausedPrimitiveSnapshot = query->pausedPrimitiveSnapshot =
MG_State::pGLContext ? MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() : 0; MGB_CTX_LIVE ? MGB_CTX->GetTransformFeedbackPausedPrimitiveCounter() : 0;
// Read AFTER StartXfbQueryCapture, which is where a failed reroute-pool creation
// disarms: the answer is then what this span will actually do for every draw.
query->pausedPrimitivesCountedByGpu = generated && pVulkanRenderer->ArePausedDrawsGpuCounted();
return query; return query;
} }
@@ -1324,7 +1296,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return; return;
} }
pVulkanRenderer->StopXfbQueryCapture( pVulkanRenderer->StopXfbQueryCapture(
query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots); query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots,
query->rerouteSlots);
} }
BackendQueryHandle BeginOcclusionQuery() { BackendQueryHandle BeginOcclusionQuery() {
@@ -1358,5 +1331,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void Present() { void Present() {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Present called with null VulkanRenderer"); MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Present called with null VulkanRenderer");
pVulkanRenderer->Present(); pVulkanRenderer->Present();
// THE frame boundary for the MGPipe counters, at the backend entry point rather
// than inside VulkanRenderer::Present: that function has an early return for the
// no-usable-swapchain case, and a suspended frame is still a frame the counters
// must close.
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::OnPresent();
}
} }
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
@@ -82,9 +82,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLsizei height, GLint border); GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height); GLsizei height);
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, void CopyImageSubData(const CopyImageEndpoint& src,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, const CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth); GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target); void GenerateMipmap(GLenum target);
@@ -95,8 +95,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
GLenum format); GLenum format);
void GetIntegeri_v(GLenum target, GLuint index, GLint* data); void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding); void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels); void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels); void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
@@ -111,6 +111,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
if (buffer.IsValid()) { if (buffer.IsValid()) {
// Outgrown, not dead: every BufferSlice handed out from this frame's arena so far
// still names it, and those slices stay in service until the frame slot is rewound
// (VkBufferResource::transientSlice, the converted-vertex-stream cache, the draw
// memos). The release therefore has to survive every mid-frame reclaim and land on
// the next ResetFrame of this slot - see VkBufferManager::CollectAllDeferredReleases.
m_deferredReleases[frameIndex].push_back(std::move(buffer)); m_deferredReleases[frameIndex].push_back(std::move(buffer));
} }
@@ -205,7 +205,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// commands away. The device is gone on that path anyway - stay silent-safe // commands away. The device is gone on that path anyway - stay silent-safe
// rather than trade a lost device for a barrier into a closed buffer. // rather than trade a lost device for a barrier into a closed buffer.
if (frame.hasCommandBufferRecorded) { if (frame.hasCommandBufferRecorded) {
MGLOG_E("TransitionToPresent: command buffer already closed; skipping the present barrier"); MGLOG_E_ONCE("TransitionToPresent: command buffer already closed; skipping the present barrier");
return false; return false;
} }
@@ -0,0 +1,498 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaPipeArms.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include <Config.h>
#if MOBILEGL_PIPE_PUSH
// kMGPipeSubsystem* - the runtime bitmask's named bits - and MGPipeHandle itself. Both are
// header-only constant/POD declarations, and both are push-only, so the pull build's include
// graph is unchanged (G1).
#include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/MGPipeHandles.h>
#endif
#include <cstdlib>
// Magma's arm selector for the P2 Track H / render-state re-keys (P2 brief D14), and the
// {slot, gen} mint the re-keyed sites are written against.
//
// Two switches decide which arm a re-keyed site runs, and they are NOT the same switch:
//
// MOBILEGL_PIPE_PUSH (compile) - is the pushed state there to be keyed on at all
// Features.PipePush (runtime bitmask) - is THIS subsystem migrated in THIS run
// MOBILEGL_PIPE_LEGACY_MEMOS (compile) - is the pre-handle arm compiled beside it
// Features.PipeLegacyMemos (runtime) - may the pre-handle arm be ENTERED in this run
//
// ARCHITECTURE.md 9.6's point: once a handle wave lands, a clear MOBILEGL_PIPE_PUSH bit is
// only a valid A/B while the legacy arm is still compiled, because with the bit clear the
// backend would otherwise still run the re-keyed code. So a clear bit selects the legacy
// arm, and a run that has explicitly disabled the legacy arm may not fall into it.
//
// D14 spends that last sentence at STARTUP, not per draw: "a Track-H subsystem whose bit is
// clear is a startup Fatal{PipeLegacyMemosDisabled}". Nothing in the draw path aborts, and
// nothing outside Track H consults the legacy-memo lever at all - see
// MagmaPipeValidateSubsystemConfiguration below for both halves of that rule.
//
// The whole header is inert in a pull build: MOBILEGL_PIPE_PUSH is 0 there, every helper
// below is behind it, and the pull build's translation units are byte-identical (G1).
namespace MobileGL::MG_Backend::DirectVulkan {
#if MOBILEGL_PIPE_PUSH
// Is `subsystemBit` (MG_Pipe/MGPipe.h's kMGPipeSubsystem*) migrated in this run?
inline Bool MagmaPipeSubsystemOn(Uint64 subsystemBit) {
return (MG_Config::Features.PipePush & subsystemBit) != 0;
}
// ---------------------------------------------------------------------------------
// D14's startup gate
// ---------------------------------------------------------------------------------
//
// Called once from VulkanRenderer::Initialize(), i.e. only when Magma is the backend
// that is actually running. It answers exactly one question and it answers it before the
// first draw: is there an arm for Magma's Track-H subsystem in this configuration?
//
// Three deliberate boundaries, each of which the per-draw shape this replaces got wrong:
//
// * ONLY Magma's own Track-H bit is checked. Espryt's bit 5 is Espryt's business (a
// DirectVulkan run does not execute one line of DirectGLES' re-key), so
// MOBILEGL_PIPE_PUSH=0x20 must not kill a Magma run, and MOBILEGL_PIPE_PUSH=0x40 must
// not kill an Espryt one.
// * bit 0 (kMGPipeSubsystemRenderState) is NOT Track H and is NOT fatal. It is not a
// memo re-key at all: it decides where the pipeline memo's STATE KEY comes from, and
// a clear bit there simply means the client is not pushing render-state CSOs in this
// run, which GetOrCreatePipeline answers with its own state hash. D14 labels bits 5
// and 6 "Track H" and labels bit 0 nothing of the sort.
// * it is Fatal at STARTUP, once, not on a draw. A per-draw abort inside
// GetOrCreatePipeline turns a configuration mistake into a mid-frame crash and puts a
// branch nobody needs on the hottest path in the backend.
//
// [declared deviation from D14, review v2 minor 2] D14's runtime row reads "false: the
// legacy arm is never entered", and D14's compile-switch row names ComputePipelineStateHash
// as part of the pre-handle arm. Those two together would make MOBILEGL_PIPE_LEGACY_MEMOS=0
// with bit 0 CLEAR a contradiction: the pipeline memo has no CSO handle to key on, so it
// keys on a state hash, and in a build that compiles the pre-handle arm that hash IS
// ComputePipelineStateHash. Magma does not make that fatal - bit 0 is not Track H, and
// there is a correct answer (the state hash) where for bits 5/6 there is none - but it no
// longer does it SILENTLY: the combination is named once, at startup, right here.
inline void MagmaPipeValidateSubsystemConfiguration() {
if (!MG_Config::Features.PipeLegacyMemos &&
!MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemRenderState)) {
MGLOG_W("MGPipe: MOBILEGL_PIPE_LEGACY_MEMOS=0 with kMGPipeSubsystemRenderState (bit 0 "
"of MOBILEGL_PIPE_PUSH) clear - Magma's pipeline memo has no CSO handle to key "
"on, so every draw whose pipeline-state version moved runs the pre-handle STATE "
"HASH instead. That is not a Track-H subsystem and not fatal, but it is not the "
"handle arm either: set bit 0 (MOBILEGL_PIPE_PUSH=0x%llx) if this run was meant "
"to measure it.",
static_cast<unsigned long long>(MG_Config::Features.PipePush |
MG_Pipe::kMGPipeSubsystemRenderState));
}
#if MOBILEGL_PIPE_LEGACY_MEMOS
// The pre-handle arm is compiled AND the operator has not forbidden entering it, so a
// clear bit is an ordinary, valid A/B: the site takes the legacy arm.
if (MG_Config::Features.PipeLegacyMemos) return;
#endif
if (MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) return;
#if MOBILEGL_PIPE_LEGACY_MEMOS
const char* const why = "this run has MOBILEGL_PIPE_LEGACY_MEMOS=0";
#else
const char* const why =
"this build has cmake -DMOBILEGL_PIPE_LEGACY_MEMOS=OFF, which compiles no such arm";
#endif
MGLOG_F("MGPipe: Fatal{PipeLegacyMemosDisabled} Magma's Track-H subsystem "
"(kMGPipeSubsystemMagmaVertexInput, bit 6 of MOBILEGL_PIPE_PUSH) is clear, so the "
"vertex-input cache and the VAO draw memo want the pre-handle arm - but %s. Set "
"bit 6 (MOBILEGL_PIPE_PUSH=0x%llx, or the default 0x%llx), or allow the legacy arm.",
why,
static_cast<unsigned long long>(MG_Config::Features.PipePush |
MG_Pipe::kMGPipeSubsystemMagmaVertexInput),
static_cast<unsigned long long>(MG_Pipe::kMGPipeSubsystemsMigratedAtP2));
std::abort();
}
// "Does this Track-H site run the handle arm?" - the ONE question every re-keyed Track-H
// site asks, so that they cannot disagree with each other or with the startup gate.
inline Bool MagmaPipeTrackHArmIsHandles(Uint64 trackHBit) {
#if MOBILEGL_PIPE_LEGACY_MEMOS
return MagmaPipeSubsystemOn(trackHBit);
#else
// No pre-handle arm exists in this build, and MagmaPipeValidateSubsystemConfiguration
// has already made a clear bit a startup Fatal, so the handle arm is the only arm a
// running process can be on.
(void)trackHBit;
return true;
#endif
}
// ---------------------------------------------------------------------------------
// Negative control C (P2 brief D18): MOBILEGL_PIPE_HANDLE_ABA_CONTROL
// ---------------------------------------------------------------------------------
//
// "Is the object-identity half of every vertex-input memo key deliberately defeated in
// this run?" - the ONE question the control's sites ask, for the same reason
// MagmaPipeTrackHArmIsHandles exists: three sites deciding separately could disagree,
// and a control that defeats two of three guards proves nothing.
//
// WHAT IT DEFEATS, AND WHY IT IS SPELLED AS "REPLACE THE IDENTITY WITH A CONSTANT"
// RATHER THAN "USE THE HEAP ADDRESS".
//
// D18 wrote the control as "hash attr.Buffer.get() instead of GetLifetimeId(), and skip
// the vaoLifetimeId compare", on the theory that a deleted object's replacement lands at
// the freed heap block and so reproduces the key. Measured, it does not: in
// HandleRecycleScenario the GL NAMES come back (glGen* hands the deleted name straight
// out) but the C++ heap blocks do not - a VertexArrayObject is 3920 bytes, too large for
// glibc's tcache, so its chunk goes to the unsorted bin and is split by the very next
// allocation the replacement path makes. Four create/delete cycles in one run produced
// four distinct addresses, ~1 MiB apart. With no address reuse there is nothing for
// "hash the address" to collide with: the replacement hashes differently, indexes a
// different memo slot, and inherits nothing - so the arm asserted stale pixels and saw
// fresh ones, which is a FAILING negative control that had stopped controlling anything.
//
// So the control no longer asks the allocator for the collision; it manufactures it. On
// both arms the object identity is replaced by a constant, which is the strongest form of
// "the allocator handed the block back" and is deterministic. That covers strictly more
// than D18's spelling, and in particular it reaches the arm P2 SHIPS: on the handle arm
// the constant defeats the OBJECT IDENTITY THAT SELECTS THE SLOT - the key the handle arm
// ships - so the replacement VAO is handed the dead one's memo entry and its content hash.
// Defeating only the retired lifetime-id/address guards would leave that key untested,
// which is exactly the vacuity this control exists to catch.
//
// WHAT IT DOES NOT COVER, AND WHY NO REPRODUCER OF THIS SHAPE CAN [fix-aba review v1,
// MAJOR 1]. It does NOT exercise the GENERATION half of {slot, gen}:
//
// * this mint has no death notification - nothing in MG_Backend/DirectVulkan consumes
// NotifyStateObjectDestroyed - so a slot returns to the free list only through
// OnFrameBoundary's age sweep (kSweepInterval 256, kRetireAgeBoundaries 1024, below);
// * HandleRecycleScenario issues five frame boundaries, so the free list is empty when
// the replacement VAO acquires and it gets a BRAND-NEW slot at Gen 1 (measured:
// redVao slot=2 gen=1, greenVao slot=3 gen=1). The knob-off FRESH verdict there is
// decided by the SLOT alone, and deleting the ++Gen below leaves all four arms green;
// * a genuine slot REUSE needs >= 1024 idle boundaries after the dead object's last
// draw, which necessarily puts the two draws in different frames - and the only memo
// that carries a GPU slice rather than a layout, ResolvedVertexBindings, declines
// across frames by design. The two requirements are mutually exclusive, so the
// generation is out of reach of any same-frame pixel reproducer for this memo.
//
// The generation is covered where it IS expressible, over this mint and the claim rule
// MagmaPipeClaimSlotMemos below: MG_Test/Pipe/MagmaPipeIdentityTest.cpp drives a real
// retire -> reuse and asserts that a memo stamped at {slot, gen=N} is not served at
// {slot, gen=N+1} with the knob off and IS served with it on. Deleting the ++Gen reds that
// suite; it is the only place in the tree where that deletion is caught.
//
// Everything the control does NOT defeat is as load-bearing as what it does. It never
// touches a guard that is not an IDENTITY guard: the resolved-bindings memo's frame
// serial, its slice-epoch compares and its host-map check all stay in force, so a green
// AbaControl arm still means "a replacement object was handed its dead predecessor's
// resolved vertex bindings because the identity halves of the keys were defeated", not
// "every safety net was switched off until something broke".
//
// Off by default (Config.h), set only by the HandleRecycle AbaControl ctest lanes, and
// #if MOBILEGL_PIPE_PUSH throughout, so no shipping pull build can even parse it.
inline Bool MagmaPipeAbaControlDefeatsIdentity() {
return MG_Config::Features.PipeHandleAbaControl;
}
// The single consumer-table entry every VAO collapses onto while the control is on. Slot
// 0 is a real, ordinary entry of both tables (MagmaPipeSlotIndex maps the first allocatable
// handle onto it), so nothing about the tables changes shape for the control's sake.
inline constexpr Uint32 kMagmaPipeAbaControlSlotIndex = 0;
// ---------------------------------------------------------------------------------
// The {slot, gen} mint
// ---------------------------------------------------------------------------------
//
// Maps a frontend object's never-reused lifetime id to a dense {slot, gen}. Three
// properties, and the third is the one review v2 got wrong:
//
// 1. exact identity - Gen moves whenever a slot changes owner, so a stale handle can
// never match a live object even if the allocator hands back the same heap address
// (the ABA HandleRecycleScenario reproduces);
// 2. dense slots - the slot IS an index, so a consumer's per-slot table needs no hash,
// no probe and no mix;
// 3. NO CAPACITY CLIFF. A live object's handle never changes while the object is being
// drawn, whatever the working set size.
//
// Property 3 is why this is not the fixed 2-way set-associative LRU the previous round
// shipped. That structure evicted a LIVE object once the working set passed its capacity,
// and every consumer memo keyed on the handle died with it: measured on a verbatim
// transcription, 54% of uses lost their handle 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, which is the Minecraft chunk shape this exists for). Two of the three memos it
// fed - the content-hash memo and the resolved-state memo - had NO capacity before this
// package: they were unbounded mutable fields on VertexArrayObject. Introducing eviction
// there turns one ComputeHash per VAO reconfiguration into one per DRAW, and, once the
// buffer table thrashes too, makes the vertex-input content hash a per-draw value that
// inserts a fresh heap-allocated BackendVertexInputState into an unbounded map on every
// draw. That is a worse leak than the one it was introduced to avoid.
//
// So: grow on demand, and reclaim by AGE instead of by capacity.
//
// * Acquire hits an UnorderedMap<lifetimeId, slotIndex>, in front of which sits a
// one-entry memo. Every re-keyed site in a draw asks about the SAME VAO, so the memo
// turns the five-or-six acquisitions a draw makes into one map probe plus five Uint64
// compares - less than the address multiply plus two-way probe the pre-handle arm ran.
// * OnFrameBoundary retires slots whose object has not been drawn for
// kRetireAgeBoundaries boundaries and returns them to a free list, so the table's
// footprint tracks the LIVE DRAWN working set, not objects ever created. That is the
// property MG_Impl/Pipe/SlotAllocator cannot have here: nothing in P2 can call its
// Free (the tracker emits no object-class state, BufferBackendOps::OnDestroy is handed
// a BackendBufferResource rather than the BufferObject, and VertexArrayObject has no
// death hook at all - adding one is D13's explicit-destroy work, which covers Espryt's
// six kinds, not VertexElementsCso), so an allocator here would grow by one SlotState
// plus one map node per object EVER created, for the life of the process, on a
// platform with an LMK. Age-based reclamation is the stand-in for the death
// notification, and it is exactly as ABA-proof, because reuse bumps Gen.
// * A retire costs at most one memo recompute if the object is drawn again - the same
// price a cache miss costs - and it is charged only to objects that went idle for
// ~1024 frames, never to a hot one.
//
// Memory: one map node plus one 24-byte Entry per live object, i.e. tens of bytes against
// the kilobyte a VertexArrayObject or a BufferObject already costs the frontend. There is
// no capacity to size off a device measurement because there is no capacity; what the
// device run in D.4.2 can still want is the number itself, so the high-water mark is
// logged at MGLOG_D on the allocate-a-new-slot branch (once per new object, never on a
// draw - ROADMAP.md:7).
//
// Single-threaded, like the rest of the renderer. Owned per VulkanRenderer (see
// MagmaPipeIdentityTables): a process-global would share one table, and one reclamation
// clock, across two live contexts.
class MagmaPipeIdentityTable {
public:
explicit MagmaPipeIdentityTable(const char* kindName) : m_kindName(kindName) {}
// Slots ever minted. A consumer table indexed by MagmaPipeSlotIndex() needs this many
// entries; MagmaPipeSlotTable below grows itself, so nobody has to ask.
Uint32 Count() const { return static_cast<Uint32>(m_entries.size()); }
// Objects currently holding a slot - the live working set this table tracks.
Uint32 LiveCount() const { return static_cast<Uint32>(m_index.size()); }
MG_Pipe::MGPipeHandle Acquire(Uint64 lifetimeId) {
// Unreachable: MG_State hands out lifetime ids from 1 precisely so that a
// zero-initialised memo slot cannot carry a live object's id. Guarded anyway so
// that a zero can never be minted into a slot and then indexed with.
if (lifetimeId == 0) return MG_Pipe::kMGPipeNullHandle;
// The one-entry front memo. Cleared by any retire, so it can never serve a slot
// that has been handed back to the free list.
if (lifetimeId == m_lastLifetimeId) {
m_entries[m_lastIndex].LastUse = m_boundary;
return m_lastHandle;
}
Uint32 index = 0;
const auto it = m_index.find(lifetimeId);
if (it != m_index.end()) {
index = it->second;
} else {
index = ClaimSlot();
m_entries[index].LifetimeId = lifetimeId;
m_index.emplace(lifetimeId, index);
}
Entry& entry = m_entries[index];
entry.LastUse = m_boundary;
m_lastLifetimeId = lifetimeId;
m_lastIndex = index;
m_lastHandle = MG_Pipe::MGPipeHandle{index + MG_Pipe::kMGPipeFirstAllocatableSlot,
entry.Gen};
return m_lastHandle;
}
// Ages the table and returns idle slots to the free list. Same shape and the same
// self-gating as VertexInputStateFactory::OnFrameBoundary, which is what the reclaimed
// slots' consumers use.
void OnFrameBoundary() {
++m_boundary;
if ((m_boundary % kSweepInterval) != 0) return;
SizeT retired = 0;
for (auto it = m_index.begin(); it != m_index.end();) {
Entry& entry = m_entries[it->second];
if ((m_boundary - entry.LastUse) > kRetireAgeBoundaries) {
entry.LifetimeId = 0;
m_freeSlots.push_back(it->second);
it = m_index.erase(it);
++retired;
} else {
++it;
}
}
if (retired != 0) {
// A retired slot's Gen has not moved yet - it moves when the slot is reused -
// so a front memo pointing at one would still hand out a handle the consumer
// tables would accept. Drop it.
m_lastLifetimeId = 0;
m_lastHandle = MG_Pipe::kMGPipeNullHandle;
MGLOG_D("MagmaPipeIdentityTable(%s): retired %zu idle slots, %u live of %u minted",
m_kindName, retired, LiveCount(), Count());
}
}
private:
// Sweep cadence and retirement age, deliberately the same numbers
// VertexInputStateFactory::OnFrameBoundary uses for the entries these slots key: a slot
// retired earlier than its cache entry would mint a new handle for an object whose
// entry is still live and still correct, which is a pure waste.
static constexpr Uint64 kSweepInterval = 256;
static constexpr Uint64 kRetireAgeBoundaries = 1024;
struct Entry {
Uint64 LifetimeId = 0;
Uint64 LastUse = 0;
// Moves ONLY on slot reuse, never on respecify: an object that keeps its slot keeps
// its generation, which is what makes a memo survive a reconfiguration.
Uint32 Gen = 0;
};
Uint32 ClaimSlot() {
while (!m_freeSlots.empty()) {
const Uint32 index = m_freeSlots.back();
m_freeSlots.pop_back();
// MGPipeHandles.h:52-58 defends the Gen wrap only in a debug allocator, and
// MOBILEGL_ASSERT is compiled out of every build P2 runs (Defines.h: asserts are
// live only at MOBILEGL_LOG_ACTIVE_LEVEL == DEBUG). So the wrap is handled on the
// RELEASE path instead of asserted: a slot that has been reused 2^32 times is
// permanently retired rather than wrapped, because a wrapped Gen would let a
// stale handle match a live object. It costs one slot.
if (m_entries[index].Gen == ~Uint32{0}) {
MGLOG_W("MagmaPipeIdentityTable(%s): slot %u reached generation 2^32-1 and is "
"retired for good; {slot, gen} stays unique",
m_kindName, index + MG_Pipe::kMGPipeFirstAllocatableSlot);
continue;
}
++m_entries[index].Gen;
return index;
}
const Uint32 index = static_cast<Uint32>(m_entries.size());
m_entries.push_back(Entry{});
m_entries[index].Gen = 1;
// The high-water mark, at powers of two from 1024 up: at most a handful of lines
// for a whole session, emitted from the allocate-a-NEW-slot branch, i.e. once per
// object this backend has ever seen and never on a draw (ROADMAP.md:7).
//
// [narrow, declared deviation from D20's "MGLOG_D for anything non-critical"] This
// one is I, not D, because D is compiled out of every build that ships and of every
// build P2 measures, and this line IS the measurement review v2's MAJOR 1 asks for:
// the live-object high-water mark of minecraft-1.21.4-in-world and
// ...-sodium-in-world, which nothing on desktop reaches and no gate here can see.
// The structure no longer has a capacity to size off it, so the number is evidence
// rather than a tuning input - but D.4.2 should still read it out of the device log,
// and it cannot read a line that was compiled away.
const SizeT minted = m_entries.size();
if (minted >= 1024 && (minted & (minted - 1)) == 0) {
MGLOG_I("MagmaPipeIdentityTable(%s): high-water %zu slots minted, %u live",
m_kindName, minted, LiveCount());
}
return index;
}
const char* m_kindName = "";
Uint64 m_boundary = 0;
Vector<Entry> m_entries;
Vector<Uint32> m_freeSlots;
UnorderedMap<Uint64, Uint32> m_index;
// One-entry front memo (see Acquire). m_lastLifetimeId == 0 means "empty": a live
// object's lifetime id is never 0.
Uint64 m_lastLifetimeId = 0;
Uint32 m_lastIndex = 0;
MG_Pipe::MGPipeHandle m_lastHandle = MG_Pipe::kMGPipeNullHandle;
};
// The two mints one renderer owns. Per renderer, NOT process-global: two live contexts (or
// a context recreation, which destroys and rebuilds the renderer) would otherwise share one
// table and one reclamation clock, and both consumer tables are per-instance already.
class MagmaPipeIdentityTables {
public:
// A VAO is kind VertexElementsCso: that is the gallium-shaped CSO a vertex array
// resolves to, and the only kind in MGPipeKind that names vertex-input state.
MG_Pipe::MGPipeHandle HandleOf(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
return kind == MG_Pipe::MGPipeKind::Buffer ? m_buffers.Acquire(lifetimeId)
: m_vaos.Acquire(lifetimeId);
}
void OnFrameBoundary() {
m_vaos.OnFrameBoundary();
m_buffers.OnFrameBoundary();
}
const MagmaPipeIdentityTable& Vaos() const { return m_vaos; }
const MagmaPipeIdentityTable& Buffers() const { return m_buffers; }
private:
MagmaPipeIdentityTable m_vaos{"VertexElementsCso"};
MagmaPipeIdentityTable m_buffers{"Buffer"};
};
// The table entry a handle names. Every per-slot table Magma keeps is indexed by this.
//
// A null handle has no slot, and it is unreachable here: both lifetime-id sources start at
// 1 (VertexArrayObject.cpp, BufferObject.cpp), so Acquire's zero guard never fires. The
// ternary, not the assertion, is what has effect in a shipped build (Defines.h compiles
// MOBILEGL_ASSERT out at INFO), and slot 0 of a consumer table is a real entry that a null
// handle can never match, because MGPipeHandleIsNull is also what the consumers compare.
inline Uint32 MagmaPipeSlotIndex(const MG_Pipe::MGPipeHandle& handle) {
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
"a null MGPipeHandle has no slot to index a per-slot table with");
return MG_Pipe::MGPipeHandleIsNull(handle)
? 0u
: handle.Slot - MG_Pipe::kMGPipeFirstAllocatableSlot;
}
// A grow-on-demand per-slot table whose ENTRY ADDRESSES NEVER MOVE.
//
// D12.4 asks for a grow-on-demand Vector, and with an unbounded mint that is what a
// consumer needs - but a Vector that grows relocates its elements, and the draw path holds
// references into these entries across nested calls. Chunks of kChunkEntries are appended
// instead: the Vector of owning pointers reallocates, the chunks never do, so an entry
// reference is valid for the life of the table. That is the same guarantee the fixed table
// it replaces gave, without the fixed capacity.
template <typename T, Uint32 kChunkEntries = 256>
class MagmaPipeSlotTable {
public:
T& operator[](Uint32 index) {
const Uint32 chunk = index / kChunkEntries;
while (m_chunks.size() <= chunk) {
m_chunks.push_back(MakeUnique<Chunk>());
}
return m_chunks[chunk]->Entries[index % kChunkEntries];
}
SizeT Capacity() const { return m_chunks.size() * kChunkEntries; }
private:
struct Chunk {
T Entries[kChunkEntries] = {};
};
Vector<UniquePtr<Chunk>> m_chunks;
};
// The claim rule every per-slot memo table uses, in one place so that the rule and the
// negative control that defeats it cannot drift apart between consumers - and so that the
// unit suite which drives a REAL slot reuse (MG_Test/Pipe/MagmaPipeIdentityTest.cpp) tests
// this code rather than a copy of it.
//
// The SLOT picks the entry; the WHOLE handle - Gen included - decides whether the entry is
// this object's. A slot the mint recycled for a different object comes back with a moved
// Gen, so the compare fails and the entry is cleared rather than inherited. That is the
// half HandleRecycleScenario cannot reach (see MagmaPipeAbaControlDefeatsIdentity).
//
// With negative control C on, every object collapses onto one entry and the entry is handed
// back UNCLEARED and UNCLAIMED - at once "the replacement reproduced its predecessor's
// slot" and "the slot was reused and Gen did not move".
//
// `Memos` needs a MG_Pipe::MGPipeHandle member named Owner and a default constructor that
// means "empty"; VertexInputStateFactory::VaoBackendMemos is the one production instance.
template <typename Memos, Uint32 kChunkEntries>
inline Memos& MagmaPipeClaimSlotMemos(MagmaPipeSlotTable<Memos, kChunkEntries>& table,
const MG_Pipe::MGPipeHandle& handle) {
if (MagmaPipeAbaControlDefeatsIdentity()) {
return table[kMagmaPipeAbaControlSlotIndex];
}
Memos& memos = table[MagmaPipeSlotIndex(handle)];
if (!(memos.Owner == handle)) {
memos = Memos{};
memos.Owner = handle;
}
return memos;
}
#endif // MOBILEGL_PIPE_PUSH
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -201,11 +201,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.minSampleShading, sizeof(payload.minSampleShading)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleMask, sizeof(payload.sampleMask)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
XXHASH_VERIFY( XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable))); XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.passthroughTessControlKey,
sizeof(payload.passthroughTessControlKey)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.viewportCount, sizeof(payload.viewportCount)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
@@ -252,6 +258,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
VkPipeline pipeline = CreatePipeline(payload); VkPipeline pipeline = CreatePipeline(payload);
// A failed creation must never be memoized. Caching VK_NULL_HANDLE served the null back for
// the rest of the process, so one transient driver rejection turned every later draw with
// the same state into a vkCmdBindPipeline(VK_NULL_HANDLE) - the SIGSEGV behind 9 of the 15
// CTS process deaths. Retrying costs one failed vkCreateGraphicsPipelines per draw, which
// is the correct price for a broken pipeline and is bounded by the draw itself being
// skipped.
if (pipeline == VK_NULL_HANDLE) {
// Unlatched, like the CreatePipeline report it accompanies: a pipeline MobileGL
// assembled and the driver refused is a broken invariant, not an expected failure,
// so it stays loud for as long as it is reachable. Raised from MGLOG_I once the
// Log.h ordering fix made MGLOG_E live in INFO builds.
MGLOG_E("PipelineFactory::GetOrCreatePipeline: creation failed for hash=0x%llx "
"programHash=0x%llx; not caching the failure",
static_cast<unsigned long long>(hash),
static_cast<unsigned long long>(payload.programHash));
return VK_NULL_HANDLE;
}
m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash, payload.renderPass, m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash, payload.renderPass,
m_frameCounter}); m_frameCounter});
return pipeline; return pipeline;
@@ -389,8 +412,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
tessellation.patchControlPoints = payload.patchControlPoints; tessellation.patchControlPoints = payload.patchControlPoints;
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
vpci.viewportCount = 1; // Both counts move together: GL has one scissor rectangle per viewport, and Vulkan
vpci.scissorCount = 1; // requires viewportCount == scissorCount whenever both are dynamic
// (VUID-VkPipelineViewportStateCreateInfo-scissorCount-04136). The caller has already
// clamped this to the device's multiViewport capability.
vpci.viewportCount = std::max<Uint32>(payload.viewportCount, 1u);
vpci.scissorCount = vpci.viewportCount;
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
raster.polygonMode = payload.polygonMode; raster.polygonMode = payload.polygonMode;
@@ -413,6 +440,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
ms.rasterizationSamples = payload.rasterizationSamples; ms.rasterizationSamples = payload.rasterizationSamples;
ms.sampleShadingEnable = payload.sampleShadingEnable ? VK_TRUE : VK_FALSE;
// Ignored by Vulkan unless sampleShadingEnable is set, but written unconditionally so the
// struct's bytes match the hash the payload was keyed by.
ms.minSampleShading = payload.minSampleShading;
// GL_SAMPLE_MASK / glSampleMaski. Left at nullptr - which Vulkan reads as all-ones - until
// now, so glSampleMaski was a silent no-op on this backend while DirectGLES forwarded it.
// The pointer has to outlive the vkCreateGraphicsPipelines call, which the payload does.
ms.pSampleMask = payload.sampleMask;
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE; depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE;
@@ -458,9 +493,56 @@ namespace MobileGL::MG_Backend::DirectVulkan {
blend.attachmentCount = payload.colorAttachmentCount; blend.attachmentCount = payload.colorAttachmentCount;
blend.pAttachments = colorAttachments.empty() ? nullptr : colorAttachments.data(); blend.pAttachments = colorAttachments.empty() ? nullptr : colorAttachments.data();
// A GL program may have a tessellation EVALUATION stage and no CONTROL stage: GL 4.6 core
// 11.2.2 gives it a fixed-function pass-through instead. Vulkan has no such stage, and
// VUID-VkGraphicsPipelineCreateInfo-pStages-00730 requires both tessellation stages or
// neither - so the renderer synthesizes the pass-through GL describes and hands it in
// here (see ProgramFactory::GetOrCreatePassthroughTessControlStage).
//
// The refusal below is what keeps the half-tessellated shape away from the driver when
// there is no synthesized stage to add - because Mali does not reject it, it dereferences
// null INSIDE vkCreateGraphicsPipelines and takes the process down (SIGSEGV, fault addr
// 0x34, on Mali-G715/r54p2 and Mali-G925/r49p1 alike; Adreno and lavapipe merely render
// wrong). Returning VK_NULL_HANDLE routes this through the same path a driver rejection
// takes: the draw is skipped, nothing is memoised, and the process survives.
const Vector<VkPipelineShaderStageCreateInfo>* effectiveStages = payload.stages;
Vector<VkPipelineShaderStageCreateInfo> stagesWithPassthrough;
if (payload.passthroughTessControlStage.module != VK_NULL_HANDLE) {
stagesWithPassthrough = *payload.stages;
stagesWithPassthrough.push_back(payload.passthroughTessControlStage);
effectiveStages = &stagesWithPassthrough;
}
{
VkShaderStageFlags stagesPresent = 0;
for (const auto& stageInfo : *effectiveStages) {
stagesPresent |= stageInfo.stage;
}
const Bool hasTessControl = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT) != 0;
const Bool hasTessEval = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT) != 0;
if (hasTessControl != hasTessEval) {
// Latched, and the latch is the point: a failed creation is deliberately never
// memoised (see GetOrCreatePipeline), so a program in this state re-enters here
// once per draw, every frame - and a refusal diagnostic that repeats per draw is
// noise, not a diagnostic. One line names the program; the draws it explains are
// all the same draw.
static Bool s_warnedHalfTessellatedPipeline = false;
if (!s_warnedHalfTessellatedPipeline) {
s_warnedHalfTessellatedPipeline = true;
MGLOG_E_ONCE("PipelineFactory::CreatePipeline: refusing a pipeline with %s tessellation stage and "
"no %s stage (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). programHash=0x%llx "
"patchControlPoints=%u. Its draws are skipped; logged once.",
hasTessEval ? "an evaluation" : "a control",
hasTessEval ? "control" : "evaluation",
static_cast<unsigned long long>(payload.programHash),
payload.patchControlPoints);
}
return VK_NULL_HANDLE;
}
}
VkGraphicsPipelineCreateInfo gpi{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; VkGraphicsPipelineCreateInfo gpi{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
gpi.stageCount = static_cast<Uint32>(payload.stages->size()); gpi.stageCount = static_cast<Uint32>(effectiveStages->size());
gpi.pStages = payload.stages->data(); gpi.pStages = effectiveStages->data();
gpi.pVertexInputState = payload.vertexInputState; gpi.pVertexInputState = payload.vertexInputState;
gpi.pInputAssemblyState = &ia; gpi.pInputAssemblyState = &ia;
gpi.pTessellationState = gpi.pTessellationState =
@@ -477,6 +559,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipeline pipeline = VK_NULL_HANDLE; VkPipeline pipeline = VK_NULL_HANDLE;
const VkResult result = vkCreateGraphicsPipelines(m_device, m_pipelineCache, 1, &gpi, nullptr, &pipeline); const VkResult result = vkCreateGraphicsPipelines(m_device, m_pipelineCache, 1, &gpi, nullptr, &pipeline);
// Loud, at MGLOG_F, and deliberately NOT latched. vkCreateGraphicsPipelines refusing a
// pipeline MobileGL assembled is a should-never-happen state, and the driver's own
// answer is VK_ERROR_UNKNOWN - no information at all - so this dump is the entire
// diagnosis. It is not an expected failure mode, so the one-shot rule that quiets W/E
// does not apply: while this is reachable it should keep saying so on every draw.
// GetOrCreatePipeline deliberately does not cache the failure, which is what makes that
// repetition happen; if the repetition ever needs to stop, fix the pipeline, not the log.
if (result != VK_SUCCESS) { if (result != VK_SUCCESS) {
MGLOG_F("PipelineFactory::CreatePipeline failed: result=%s (%d) programHash=0x%llx vertexInputHash=0x%llx stageCount=%u topology=%s(%d) colorAttachmentCount=%u samples=%s(%d) subpass=%u", MGLOG_F("PipelineFactory::CreatePipeline failed: result=%s (%d) programHash=0x%llx vertexInputHash=0x%llx stageCount=%u topology=%s(%d) colorAttachmentCount=%u samples=%s(%d) subpass=%u",
VkResultToString(result), VkResultToString(result),
@@ -507,6 +596,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MGLOG_F("PipelineFactory::CreatePipeline vertex input: bindingCount=%u attributeCount=%u", MGLOG_F("PipelineFactory::CreatePipeline vertex input: bindingCount=%u attributeCount=%u",
payload.vertexInputState->vertexBindingDescriptionCount, payload.vertexInputState->vertexBindingDescriptionCount,
payload.vertexInputState->vertexAttributeDescriptionCount); payload.vertexInputState->vertexAttributeDescriptionCount);
// The driver's own answer is VK_ERROR_UNKNOWN, i.e. no information at all, so the only
// way to work out WHICH shader it choked on (the open sampler-array-in-struct
// investigation) is to name the modules. MGLOG_I, not _D: this is part of a
// should-never-happen report and must survive in the INFO-level builds that CTS
// actually runs against, alongside the MGLOG_F lines above.
if (payload.stageSpirvDigests) {
for (SizeT i = 0; i < payload.stageSpirvDigests->size(); ++i) {
const auto& digest = (*payload.stageSpirvDigests)[i];
MGLOG_I("PipelineFactory::CreatePipeline spirv[%zu]: stage=0x%x words=%u bytes=%zu "
"hash=0x%llx",
i, digest.stage, digest.wordCount,
static_cast<SizeT>(digest.wordCount) * sizeof(Uint32),
static_cast<unsigned long long>(digest.hash));
}
} else {
MGLOG_I("PipelineFactory::CreatePipeline: no SPIR-V digests attached to the payload");
}
if (payload.stages) {
for (SizeT i = 0; i < payload.stages->size(); ++i) {
const auto& stage = (*payload.stages)[i];
// VkShaderModule is a non-dispatchable handle: a pointer on 64-bit but a
// plain uint64_t on 32-bit ABIs, where a cast to const void* is ill-formed
// (broke the armeabi-v7a build). Print it as the 64-bit value it is.
MGLOG_I("PipelineFactory::CreatePipeline stage[%zu]: stage=0x%x module=0x%llx entry=%s "
"specialization=%d",
i, static_cast<Uint32>(stage.stage),
static_cast<unsigned long long>(reinterpret_cast<Uint64>(stage.module)),
stage.pName ? stage.pName : "(null)", stage.pSpecializationInfo ? 1 : 0);
}
}
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) { for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
const auto& attachment = payload.colorBlendAttachments[i]; const auto& attachment = payload.colorBlendAttachments[i];
MGLOG_F("PipelineFactory::CreatePipeline colorAttachment[%u]: blend=%d colorWriteMask=0x%x srcColor=%d dstColor=%d colorOp=%d srcAlpha=%d dstAlpha=%d alphaOp=%d", MGLOG_F("PipelineFactory::CreatePipeline colorAttachment[%u]: blend=%d colorWriteMask=0x%x srcColor=%d dstColor=%d colorOp=%d srcAlpha=%d dstAlpha=%d alphaOp=%d",
@@ -14,6 +14,16 @@
#include <Includes.h> #include <Includes.h>
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
// Enough of a fingerprint to identify the exact module the driver rejected without keeping the
// SPIR-V alive for every program in the cache: a driver that answers VK_ERROR_UNKNOWN tells us
// nothing, so the log has to carry the shader's identity itself. Diagnostic only - never part
// of any pipeline or program hash.
struct ShaderStageSpirvDigest {
Uint32 stage = 0; // VkShaderStageFlagBits
Uint32 wordCount = 0;
Uint64 hash = 0;
};
class PipelineFactory { class PipelineFactory {
public: public:
using HashType = Uint64; using HashType = Uint64;
@@ -27,11 +37,47 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkRenderPass renderPass = VK_NULL_HANDLE; VkRenderPass renderPass = VK_NULL_HANDLE;
Uint32 colorAttachmentCount = 1; Uint32 colorAttachmentCount = 1;
VkSampleCountFlagBits rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; VkSampleCountFlagBits rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
// glEnable(GL_SAMPLE_SHADING) + glMinSampleShading, which Vulkan bakes into the
// pipeline rather than exposing as dynamic state - so both are part of the pipeline's
// identity and both are hashed. The renderer leaves the enable false unless the
// device's sampleRateShading feature was enabled
// (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784).
Bool sampleShadingEnable = false;
Float minSampleShading = 0.0f;
// glEnable(GL_SAMPLE_MASK) + glSampleMaski, the fixed-function coverage mask, already
// reduced to what GL says this draw gets (VulkanRenderer::ResolveEffectiveSampleMask:
// all-ones unless the target is genuinely multisampled). Pipeline state like the two
// above - Vulkan has no dynamic sample mask before VK_EXT_extended_dynamic_state3 -
// so it is hashed with them, and all-ones has to keep producing the pipeline a null
// pSampleMask always did.
//
// TWO words, though GL only ever fills the first. GL_MAX_SAMPLE_MASK_WORDS is clamped
// to 1 on both backends, so glSampleMaski writes index 0 and nothing else - but the
// count Vulkan READS is ceil(rasterizationSamples / 32), which is 2 on a 64-sample
// target, and GetAdvertisedMaxSamples does not cap the driver's sample count. A
// single Uint32 here let such a pipeline read one word past the member (the next
// struct field). The second word is all-ones: full coverage for samples 32..63, which
// is the only honest answer when GL has no state describing them.
Uint32 sampleMask[2] = {0xffffffffu, 0xffffffffu};
Uint32 subpass = 0; Uint32 subpass = 0;
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
Bool primitiveRestartEnable = false; Bool primitiveRestartEnable = false;
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology. // GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
Uint32 patchControlPoints = 3; Uint32 patchControlPoints = 3;
// ProgramFactory::ComputePassthroughTessControlKey of the synthesized pass-through
// tessellation control stage below, or 0 when this pipeline has none. Hashed, because
// the levels glPatchParameterfv set are compiled INTO that module and are not a
// function of the program or of patchControlPoints - see the note on
// passthroughTessControlStage.
Uint64 passthroughTessControlKey = 0;
// How many of ARB_viewport_array's viewports this pipeline rasterizes into. 1 for
// every program that never assigns gl_ViewportIndex, which is all of them outside the
// conformance suite - the wide shape costs a longer vkCmdSetViewport/Scissor per state
// change and can cost hardware fast paths, so it is opt-in per program. Baked into the
// pipeline (viewportCount is not dynamic without VK_EXT_extended_dynamic_state) and
// therefore hashed; the DYNAMIC viewport/scissor arrays the draw pushes must have
// exactly this many elements (VUID-vkCmdDraw-viewportCount-03417/-03418).
Uint32 viewportCount = 1;
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL; VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT; VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE; VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
@@ -61,7 +107,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool fragmentReplacesDepth = false; Bool fragmentReplacesDepth = false;
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{}; Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr; const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
// The tessellation control stage this renderer synthesized for a program that has
// an evaluation stage and none of its own (GL 4.6 core 11.2.2 gives such a program a
// fixed-function pass-through; Vulkan has no such thing and
// VUID-VkGraphicsPipelineCreateInfo-pStages-00730 forbids the half-tessellated
// pipeline outright). Appended to `stages` at creation. A null module means the
// renderer could not build one, and CreatePipeline refuses the pipeline - the same
// refusal it applies when `stages` itself is half-tessellated.
//
// NOT hashed directly: it is a pure function of the program, of patchControlPoints and
// of the default tessellation levels - the first two of which ComputeHash already
// mixes in, and the third of which arrives through passthroughTessControlKey above.
VkPipelineShaderStageCreateInfo passthroughTessControlStage{};
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr; const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
// Diagnostic only; may be null. Read solely from the pipeline-creation failure path.
const Vector<ShaderStageSpirvDigest>* stageSpirvDigests = nullptr;
}; };
explicit PipelineFactory(VkDevice device, const VulkanRendererConfig& config); explicit PipelineFactory(VkDevice device, const VulkanRendererConfig& config);
File diff suppressed because it is too large Load Diff
@@ -9,6 +9,7 @@
#pragma once #pragma once
#include "../VkIncludes.h" #include "../VkIncludes.h"
#include "PipelineFactory.h"
#include "MG_State/GLState/ProgramState/ProgramObject.h" #include "MG_State/GLState/ProgramState/ProgramObject.h"
#include "MG_State/GLState/ProgramState/ShaderObject.h" #include "MG_State/GLState/ProgramState/ShaderObject.h"
#include "MG_State/GLState/TextureState/TextureEnum.h" #include "MG_State/GLState/TextureState/TextureEnum.h"
@@ -32,7 +33,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
CombinedImageSampler, CombinedImageSampler,
UniformTexelBuffer, UniformTexelBuffer,
StorageBuffer, StorageBuffer,
StorageImage StorageImage,
// GLSL `imageBuffer` - a buffer texture reached through an IMAGE unit rather than a
// texture unit. Vulkan spells it VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, which is a
// VkBufferView like UniformTexelBuffer and not a VkImageView like StorageImage: it is
// the one image uniform whose descriptor is a buffer. Appended, never inserted -
// DescriptorKeyHash mixes the enumerator's value.
StorageTexelBuffer
}; };
enum class CompileOptionBit : Uint { enum class CompileOptionBit : Uint {
@@ -52,19 +59,73 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// recorded while GL transform feedback is active, so plain draws keep the // recorded while GL transform feedback is active, so plain draws keep the
// undecorated variant. // undecorated variant.
XfbCapture = 1 << 6, XfbCapture = 1 << 6,
// Rewrites the fragment stage's gl_FragCoord reads to GL's bottom-left window
// origin. Vulkan's gl_FragCoord.y IS the framebuffer row being written, and the
// default framebuffer's image is stored in display (top-left) order, so a shader
// that reads gl_FragCoord there sees `height - y_GL`. Set together with
// PositionYFlip (the two are the same fact about the same draws) except under a
// quarter turn, which this renderer does not convert rectangles for either.
FragCoordYFlip = 1 << 7,
// Replaces the vertex stage's gl_BaseVertex reads with zero. GL defines the builtin
// as zero for every drawing command that has no baseVertex parameter - all the
// DrawArrays forms - while Vulkan's BaseVertex reports firstVertex there. Set only
// for a non-indexed draw whose program actually reads the builtin, so nothing else
// acquires a second program/pipeline variant. See ZeroBaseVertexPass.
ZeroBaseVertex = 1 << 8,
}; };
using CompileOptionFlags = Flags<CompileOptionBit>; using CompileOptionFlags = Flags<CompileOptionBit>;
using HashType = Uint64; using HashType = Uint64;
// The gl_PerVertex members a pass-through tessellation control stage may have to carry,
// in the order glslang declares them - which is the order a redeclaration must use.
// Which of them exist is a function of the neighbouring stage's GLSL VERSION
// (gl_CullDistance joins the block at #version 450), so the mask is read off that
// stage's SPIR-V rather than assumed. See ReflectPerVertexInputMembers.
enum class PerVertexMemberBit : Uint32 {
Position = 1u << 0,
PointSize = 1u << 1,
ClipDistance = 1u << 2,
CullDistance = 1u << 3,
};
// What a program parsed below #version 450 carries, and the fallback when a module's
// block cannot be read.
static constexpr Uint32 kDefaultPerVertexMembers =
static_cast<Uint32>(PerVertexMemberBit::Position) | static_cast<Uint32>(PerVertexMemberBit::PointSize) |
static_cast<Uint32>(PerVertexMemberBit::ClipDistance);
struct UpdateAfterBindLimits {
Bool enabled = false;
Uint32 maxPerStageSamplers = 0;
Uint32 maxPerStageUniformBuffers = 0;
Uint32 maxPerStageStorageBuffers = 0;
Uint32 maxPerStageSampledImages = 0;
Uint32 maxPerStageStorageImages = 0;
Uint32 maxPerStageResources = 0;
Uint32 maxSetSamplers = 0;
Uint32 maxSetUniformBuffers = 0;
Uint32 maxSetUniformBuffersDynamic = 0;
Uint32 maxSetStorageBuffers = 0;
Uint32 maxSetStorageBuffersDynamic = 0;
Uint32 maxSetSampledImages = 0;
Uint32 maxSetStorageImages = 0;
};
struct VkProgramObject { struct VkProgramObject {
static constexpr Uint32 kMaxVertexInputLocations = 32; static constexpr Uint32 kMaxVertexInputLocations = 32;
HashType hash = 0; HashType hash = 0;
Vector<VkPipelineShaderStageCreateInfo> stages; Vector<VkPipelineShaderStageCreateInfo> stages;
Vector<VkShaderModule> modules; Vector<VkShaderModule> modules;
// Parallel to stages; identifies the exact module bytes handed to the driver when a
// pipeline creation fails. Sixteen bytes per stage instead of keeping the SPIR-V.
Vector<ShaderStageSpirvDigest> stageSpirvDigests;
// Layout data (previously in separate VkProgramLayout) // Layout data (previously in separate VkProgramLayout)
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE; VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
// True only when this layout passed every descriptor-indexing feature and
// update-after-bind limit gate at reflection time. It controls both the
// layout/binding flags and the pool class used by UniformManager.
Bool usesUpdateAfterBind = false;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
Vector<DescriptorBindingKind> bindingKinds; Vector<DescriptorBindingKind> bindingKinds;
// The bindings this program actually declares, ascending. bindingKinds is sized to the // The bindings this program actually declares, ascending. bindingKinds is sized to the
@@ -75,8 +136,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<Uint32> activeBindings; Vector<Uint32> activeBindings;
Vector<Uint32> dynamicBindings; Vector<Uint32> dynamicBindings;
Vector<Int> uniformBlockIndexByBinding; Vector<Int> uniformBlockIndexByBinding;
// Descriptor count per binding (1 except for UBO instance arrays, which occupy one // Descriptor count per binding (1 except for a descriptor ARRAY - a UBO or storage
// binding with descriptorCount = N). // block instance array, an image uniform array or a sampler uniform array - each of
// which occupies one binding with descriptorCount = N).
Vector<Uint16> bindingDescriptorCounts; Vector<Uint16> bindingDescriptorCounts;
// Per-element GL uniform block indices for arrayed UBO bindings (count > 1); // Per-element GL uniform block indices for arrayed UBO bindings (count > 1);
// element 0 of a non-arrayed binding stays in uniformBlockIndexByBinding. // element 0 of a non-arrayed binding stays in uniformBlockIndexByBinding.
@@ -85,6 +147,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<Int> samplerUniformLocationByBinding; Vector<Int> samplerUniformLocationByBinding;
Vector<TextureTarget> samplerTextureTargetByBinding; Vector<TextureTarget> samplerTextureTargetByBinding;
Vector<SamplerNumericDomain> samplerNumericDomainByBinding; Vector<SamplerNumericDomain> samplerNumericDomainByBinding;
// Shared by StorageImage and StorageTexelBuffer bindings: a binding is one kind or
// the other, never both, and both need exactly the same thing - the format the
// shader declared, so the per-draw resolve can tell a typed declaration from a
// formatless one. Kept as one pair rather than two so the move operations below
// cannot drift out of sync with a field that only one kind populates.
Vector<VkFormat> storageImageFormatByBinding; Vector<VkFormat> storageImageFormatByBinding;
Vector<Bool> storageImageUsesBindingFormatByBinding; Vector<Bool> storageImageUsesBindingFormatByBinding;
Vector<String> storageBlockNameByBinding; Vector<String> storageBlockNameByBinding;
@@ -92,6 +159,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Set once during ReflectLayout so the per-draw path can skip the whole // Set once during ReflectLayout so the per-draw path can skip the whole
// storage-image preparation for the overwhelming majority of programs. // storage-image preparation for the overwhelming majority of programs.
Bool hasStorageImages = false; Bool hasStorageImages = false;
// Something about this program's descriptors could not be resolved - an opaque
// uniform array whose elements have no addressable uniform locations (the
// multi-dimensional case), or a binding remap that failed outright. The binding
// STAYS DECLARED in the descriptor set layout; declining is done here, by refusing
// every draw, and BindProgramUniformBuffers returns false so the draw setup skips
// the draw exactly as it does for any other bind failure.
//
// Keeping the layout intact is the load-bearing half. Shrinking it instead - which
// is what the first cut of this did - leaves the shader reading a descriptor the
// layout never declared, and lavapipe segfaults on that inside PIPELINE CREATION,
// in a JIT worker thread, before any draw runs where a refusal could help. The
// reason was logged once at MGLOG_I when the descriptor was declined.
Bool declinedDescriptors = false;
Int globalUboBinding = -1; Int globalUboBinding = -1;
Uint32 activeVertexInputLocationMask = 0; Uint32 activeVertexInputLocationMask = 0;
Array<GLenum, kMaxVertexInputLocations> vertexInputTypes{}; Array<GLenum, kMaxVertexInputLocations> vertexInputTypes{};
@@ -104,6 +184,68 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline // gl_FragDepth); shader-computed depth is immune to the cross-pipeline
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite). // position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
Bool fragmentReplacesDepth = false; Bool fragmentReplacesDepth = false;
// The vertex module declares the BaseVertex builtin. Selects the ZeroBaseVertex
// program variant for non-indexed draws, and is deliberately a property of the
// PROGRAM rather than of the variant: the zeroed variant leaves the variable
// declared, so both variants answer the same and the draw path can ask either.
Bool readsBaseVertexBuiltin = false;
// Some pre-rasterization stage assigns gl_ViewportIndex. Its pipeline declares
// viewportCount = the renderer's rasterizable viewport count instead of 1, and its
// draws push the whole viewport/scissor array; every other program keeps the
// single-viewport fast path untouched. Part of the program's identity (folded into
// the pipeline hash through programHash), so no memo can serve the wrong shape.
Bool writesViewportIndexBuiltin = false;
// This program has a tessellation EVALUATION stage and no tessellation CONTROL
// stage. GL allows that (4.6 core 11.2.2: with no control shader the input patch
// is passed through unmodified, the output patch size is PATCH_VERTICES, and the
// levels come from the PATCH_DEFAULT_*_LEVEL state); Vulkan does not - either both
// tessellation stages are present or neither
// (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). So the draw path has to supply
// the pass-through stage GL describes; see GetOrCreatePassthroughTessControlStage.
// True when this program was built AS a transform-feedback capture variant but its
// last pre-rasterization module does NOT carry the Xfb execution mode - so the
// renderer must decline the capture span instead of issuing
// vkCmdBeginTransformFeedbackEXT against it
// (VUID-vkCmdBeginTransformFeedbackEXT-None-04128).
//
// Two ways to get here, and neither is visible from GL state, which is all
// BeginXfbCaptureForDraw otherwise consults: the clip/XFB validation backstop had to
// rewind past the capture decoration, or XfbCaptureDecoratePass resolved none of the
// requested varyings and returned without changing anything (its own MGLOG_E path)
// while its runner still reported success. Both used to ship a non-Xfb module under
// an Xfb-flagged cache entry - the flag and the layout are part of the program cache
// key, so it was sticky for every later captured draw of the program, not a glitch.
Bool xfbCaptureDeclined = false;
// The program has a tessellation or geometry module declaring TessellationPointSize /
// GeometryPointSize on a device whose shaderTessellationAndGeometryPointSize feature
// is off, so a pipeline built from it is invalid usage
// (VUID-RuntimeSpirv-PointSize-06439). Its draws are refused in SetupDraw rather than
// handed to the driver - the same contract PipelineFactory's half-tessellated refusal
// implements one level up, and the counterpart of the DirectGLES arm that reports a
// driver with neither point-size extension by name.
//
// Sticky by construction, which is what makes ONE log line honest: the flag lives on
// the cache entry, so every later draw of the same program variant reads the same
// answer instead of re-deciding it.
Bool pointSizeCapabilityUnsupported = false;
Bool needsPassthroughTessControl = false;
// ...and the pass-through this renderer can synthesize carries gl_Position and
// nothing else, so it is only correct when the evaluation stage's inputs are
// built-ins. A user-defined varying would arrive at the evaluation stage
// UNWRITTEN once a control stage sits between it and the vertex stage, which is
// silently wrong pixels rather than a crash - so those programs are declined
// instead (PipelineFactory::CreatePipeline refuses the pipeline and the draw is
// skipped). See ReflectPassthroughTessControlNeed.
Bool passthroughTessControlEmulatable = false;
// Which gl_PerVertex members the evaluation stage's `in gl_PerVertex gl_in[]` block
// actually carries, as a PerVertexMemberBit mask read off its SPIR-V. The synthesized
// control stage has to redeclare the SAME shape: glslang appends gl_CullDistance to
// that block from #version 450 upward, so a 450/460 program - and every ESSL program,
// which the source processor rewrites to "#version 460 core" - carries four members
// where a 430 program carries three. A fixed three-member pass-through fed the
// evaluation stage a differently-shaped block, which is the black-frame-no-error case
// this whole family is written around.
Uint32 passthroughPerVertexMembers = 0;
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives // Frame-boundary counter value of the last GetOrCreateProgram hit; drives
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised // cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
// entry pointer re-stamps use through a const reference (StampProgramUse). // entry pointer re-stamps use through a const reference (StampProgramUse).
@@ -118,7 +260,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
hash = other.hash; hash = other.hash;
stages = std::move(other.stages); stages = std::move(other.stages);
modules = std::move(other.modules); modules = std::move(other.modules);
// Must travel with `modules`: these digests name the SPIR-V those exact
// shader modules were built from, and the pipeline-failure diagnostics
// print the two together. Leaving it behind used to merely lose the
// digests on a rehash; now that the cache is a robin-hood table, insertion
// SWAPS two entries, and a field that no move touches stays behind in the
// slot - pairing one program's modules with another program's digests, so
// a pipeline failure would be reported against the wrong SPIR-V.
stageSpirvDigests = std::move(other.stageSpirvDigests);
descriptorSetLayout = other.descriptorSetLayout; descriptorSetLayout = other.descriptorSetLayout;
usesUpdateAfterBind = other.usesUpdateAfterBind;
pipelineLayout = other.pipelineLayout; pipelineLayout = other.pipelineLayout;
bindingKinds = std::move(other.bindingKinds); bindingKinds = std::move(other.bindingKinds);
activeBindings = std::move(other.activeBindings); activeBindings = std::move(other.activeBindings);
@@ -136,6 +287,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding); storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding); storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
hasStorageImages = other.hasStorageImages; hasStorageImages = other.hasStorageImages;
declinedDescriptors = other.declinedDescriptors;
globalUboBinding = other.globalUboBinding; globalUboBinding = other.globalUboBinding;
activeVertexInputLocationMask = other.activeVertexInputLocationMask; activeVertexInputLocationMask = other.activeVertexInputLocationMask;
vertexInputTypes = other.vertexInputTypes; vertexInputTypes = other.vertexInputTypes;
@@ -145,11 +297,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
producerOutputComponentCount = other.producerOutputComponentCount; producerOutputComponentCount = other.producerOutputComponentCount;
fragmentInputComponentCount = other.fragmentInputComponentCount; fragmentInputComponentCount = other.fragmentInputComponentCount;
fragmentReplacesDepth = other.fragmentReplacesDepth; fragmentReplacesDepth = other.fragmentReplacesDepth;
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
needsPassthroughTessControl = other.needsPassthroughTessControl;
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
passthroughPerVertexMembers = other.passthroughPerVertexMembers;
lastUsedFrame = other.lastUsedFrame; lastUsedFrame = other.lastUsedFrame;
other.hash = 0; other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE; other.descriptorSetLayout = VK_NULL_HANDLE;
other.usesUpdateAfterBind = false;
other.pipelineLayout = VK_NULL_HANDLE; other.pipelineLayout = VK_NULL_HANDLE;
other.hasStorageImages = false; other.hasStorageImages = false;
other.declinedDescriptors = false;
other.globalUboBinding = -1; other.globalUboBinding = -1;
other.activeVertexInputLocationMask = 0; other.activeVertexInputLocationMask = 0;
other.activeFragmentOutputLocationMask = 0; other.activeFragmentOutputLocationMask = 0;
@@ -157,6 +316,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.producerOutputComponentCount = 0; other.producerOutputComponentCount = 0;
other.fragmentInputComponentCount = 0; other.fragmentInputComponentCount = 0;
other.fragmentReplacesDepth = false; other.fragmentReplacesDepth = false;
other.readsBaseVertexBuiltin = false;
other.writesViewportIndexBuiltin = false;
other.needsPassthroughTessControl = false;
other.passthroughTessControlEmulatable = false;
other.passthroughPerVertexMembers = 0;
other.lastUsedFrame = 0; other.lastUsedFrame = 0;
} }
VkProgramObject& operator=(VkProgramObject&& other) noexcept { VkProgramObject& operator=(VkProgramObject&& other) noexcept {
@@ -167,7 +331,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
hash = other.hash; hash = other.hash;
stages = std::move(other.stages); stages = std::move(other.stages);
modules = std::move(other.modules); modules = std::move(other.modules);
stageSpirvDigests = std::move(other.stageSpirvDigests); // travels with `modules` - see the move ctor
descriptorSetLayout = other.descriptorSetLayout; descriptorSetLayout = other.descriptorSetLayout;
usesUpdateAfterBind = other.usesUpdateAfterBind;
pipelineLayout = other.pipelineLayout; pipelineLayout = other.pipelineLayout;
bindingKinds = std::move(other.bindingKinds); bindingKinds = std::move(other.bindingKinds);
activeBindings = std::move(other.activeBindings); activeBindings = std::move(other.activeBindings);
@@ -185,6 +351,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding); storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding); storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
hasStorageImages = other.hasStorageImages; hasStorageImages = other.hasStorageImages;
declinedDescriptors = other.declinedDescriptors;
globalUboBinding = other.globalUboBinding; globalUboBinding = other.globalUboBinding;
activeVertexInputLocationMask = other.activeVertexInputLocationMask; activeVertexInputLocationMask = other.activeVertexInputLocationMask;
vertexInputTypes = other.vertexInputTypes; vertexInputTypes = other.vertexInputTypes;
@@ -194,11 +361,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
producerOutputComponentCount = other.producerOutputComponentCount; producerOutputComponentCount = other.producerOutputComponentCount;
fragmentInputComponentCount = other.fragmentInputComponentCount; fragmentInputComponentCount = other.fragmentInputComponentCount;
fragmentReplacesDepth = other.fragmentReplacesDepth; fragmentReplacesDepth = other.fragmentReplacesDepth;
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
needsPassthroughTessControl = other.needsPassthroughTessControl;
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
passthroughPerVertexMembers = other.passthroughPerVertexMembers;
lastUsedFrame = other.lastUsedFrame; lastUsedFrame = other.lastUsedFrame;
other.hash = 0; other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE; other.descriptorSetLayout = VK_NULL_HANDLE;
other.usesUpdateAfterBind = false;
other.pipelineLayout = VK_NULL_HANDLE; other.pipelineLayout = VK_NULL_HANDLE;
other.hasStorageImages = false; other.hasStorageImages = false;
other.declinedDescriptors = false;
other.globalUboBinding = -1; other.globalUboBinding = -1;
other.activeVertexInputLocationMask = 0; other.activeVertexInputLocationMask = 0;
other.activeFragmentOutputLocationMask = 0; other.activeFragmentOutputLocationMask = 0;
@@ -206,6 +380,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.producerOutputComponentCount = 0; other.producerOutputComponentCount = 0;
other.fragmentInputComponentCount = 0; other.fragmentInputComponentCount = 0;
other.fragmentReplacesDepth = false; other.fragmentReplacesDepth = false;
other.readsBaseVertexBuiltin = false;
other.writesViewportIndexBuiltin = false;
other.needsPassthroughTessControl = false;
other.passthroughTessControlEmulatable = false;
other.passthroughPerVertexMembers = 0;
other.lastUsedFrame = 0; other.lastUsedFrame = 0;
return *this; return *this;
} }
@@ -233,6 +412,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
modules.clear(); modules.clear();
stages.clear(); stages.clear();
stageSpirvDigests.clear(); // the modules they describe are gone
} }
}; };
@@ -248,21 +428,64 @@ namespace MobileGL::MG_Backend::DirectVulkan {
virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0; virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0;
}; };
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16, // How this factory's compute modules implement GL_KHR_shader_subgroup. Computed
Bool shaderDrawParametersEnabled = false, // once at renderer initialization (SubgroupSupportPolicy.h + the device's
Bool unformattedFloatStorageImagesEnabled = false) // subgroup properties) so lowering can never disagree with the advertised
// capabilities. Native subgroup operations always execute natively; the two
// repair passes patch modules AROUND them, and the emulation only replaces them
// on opted-in devices with no subgroup support at all.
struct SubgroupLoweringPolicy {
Bool emulateSubgroups = false; // MOBILEGL_MAGMA_EMULATE_SUBGROUP, no-native-support devices
Bool fixIterationRPSubgroupScratch = false; // patch iterationRP's under-declared scratch
Bool fixIterationRPBarrier = false; // repair Program 203's shared-scratch race
Bool deriveNumSubgroups = false; // repair the NumSubgroups builtin
Bool requireFullSubgroups = false; // computeFullSubgroups enabled on the device
Uint32 nativeSubgroupSize = 0;
// Full-subgroup launches are bounded by this device limit; a dispatch whose
// workgroup needs more subgroups than this cannot request the flag.
Uint32 maxComputeWorkgroupSubgroups = 0;
// VkPhysicalDeviceLimits::maxComputeSharedMemorySize; bounds the scratch the
// emulation pass may add (0 falls back to the Vulkan minimum, 16384).
Uint32 maxComputeSharedMemoryBytes = 0;
};
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings,
Bool shaderDrawParametersEnabled,
Bool unformattedFloatStorageImagesEnabled,
Bool tessellationAndGeometryPointSizeEnabled,
Bool enableSpirvValidation,
UpdateAfterBindLimits updateAfterBindLimits,
SubgroupLoweringPolicy subgroupPolicy)
: m_device(device), m_maxBindings(maxBindings), m_config(config), : m_device(device), m_maxBindings(maxBindings), m_config(config),
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled), m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) { m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled),
m_tessellationAndGeometryPointSizeEnabled(tessellationAndGeometryPointSizeEnabled),
m_enableSpirvValidation(enableSpirvValidation),
m_updateAfterBindLimits(updateAfterBindLimits),
m_subgroupPolicy(subgroupPolicy) {
VkProgramObject::s_device = device; VkProgramObject::s_device = device;
} }
~ProgramFactory() = default; // Destroys the pass-through tessellation control modules. Runs while the device is
// still alive for the same reason ~VkProgramObject's does: this factory outlives
// nothing that owns the device.
~ProgramFactory();
ProgramFactory(const ProgramFactory&) = delete; ProgramFactory(const ProgramFactory&) = delete;
HashType ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const; HashType ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const;
const VkProgramObject& GetOrCreateProgram( const VkProgramObject& GetOrCreateProgram(
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags); const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
// The default framebuffer's current image height, baked as a literal into every
// FragCoordYFlip variant (there is no push-constant or specialization channel here, and
// adding one for a value that changes only on swapchain recreation would cost the draw
// path more than a recompile costs a resize). It is therefore part of those variants'
// identity: ComputeHash mixes it in when the bit is set, so a height change re-keys them
// and leaves every other program's hash untouched. Setting a NEW height also bumps the
// cache-structure epoch, because a caller holding a memoised VkProgramObject* would
// otherwise keep using a module compiled against the old height.
void SetDefaultFramebufferHeight(Uint32 height);
Uint32 GetDefaultFramebufferHeight() const { return m_defaultFramebufferHeight; }
// Bumped whenever m_cache's STRUCTURE changes (any insert or erase): the cache is // Bumped whenever m_cache's STRUCTURE changes (any insert or erase): the cache is
// an open-addressing map holding entries by value, so both moves existing entries. // an open-addressing map holding entries by value, so both moves existing entries.
// A caller that memoised a VkProgramObject* may keep dereferencing it only while // A caller that memoised a VkProgramObject* may keep dereferencing it only while
@@ -283,6 +506,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static VkShaderStageFlagBits ToVkStage(ShaderStage stage); static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format); static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType); static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
// The same question for an IMAGE uniform (`image2D`, `uimageBuffer`, ...), which the
// sampler form above deliberately does not answer. Kept separate rather than folded in
// because the two are asked in different places for different reasons: a sampler's domain
// decides a sampled VIEW format, an image's decides what a placeholder descriptor for an
// UNBOUND image unit must be (see UniformManager::AcquireUnboundTexelBufferView and
// GetUnboundStorageImageTexture) - a formatless `writeonly` declaration reflects no
// format at all, and the numeric domain is then the only thing that constrains it.
static SamplerNumericDomain UniformTypeToImageNumericDomain(GLenum glType);
// True when any entry point declares the DepthReplacing execution mode, i.e. the // True when any entry point declares the DepthReplacing execution mode, i.e. the
// shader assigns gl_FragDepth. Exposed so the blended depth-write quirk's exemption // shader assigns gl_FragDepth. Exposed so the blended depth-write quirk's exemption
// can be pinned by tests. A false negative loses the exemption, so such a shader is // can be pinned by tests. A false negative loses the exemption, so such a shader is
@@ -291,6 +522,61 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// True when an entry point reads the InstanceIndex builtin. Only gates a diagnostic: // True when an entry point reads the InstanceIndex builtin. Only gates a diagnostic:
// without shaderDrawParameters such a shader cannot have gl_InstanceID rebased. // without shaderDrawParameters such a shader cannot have gl_InstanceID rebased.
static Bool ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule); static Bool ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule);
// True when an entry point declares the BaseVertex builtin, i.e. when a non-indexed
// draw with this program has to take the ZeroBaseVertex variant.
static Bool ReflectedReadsBaseVertexBuiltin(const SpvReflectShaderModule& reflectModule);
// Shared by the two above: does any entry point list an input variable decorated with
// this builtin?
static Bool ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
// True when an entry point writes the ViewportIndex builtin (gl_ViewportIndex), i.e. when
// the program can route primitives to a viewport other than 0 and its pipeline therefore
// has to declare more than one. Asks about OUTPUT variables because that is the direction
// a pre-rasterization stage declares it in.
static Bool ReflectedWritesViewportIndexBuiltin(const SpvReflectShaderModule& reflectModule);
static Bool ReflectedDeclaresOutputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes for a
// program that has an evaluation stage and no control stage, for an input patch of
// `patchVertices` control points. Returned BY VALUE (a stage description is a POD, and
// the cache below is a rehashing map, so a pointer into it would not survive the next
// distinct patch size). `.module == VK_NULL_HANDLE` means the stage could not be built:
// the caller then has no control stage to inject, and CreatePipeline refuses the
// pipeline rather than handing the driver a half-tessellated one.
//
// Keyed on the patch size, the six default tessellation levels AND the gl_PerVertex
// member set, because all three decide what the generator emits. The size comes from
// PATCH_VERTICES and the levels from PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL
// - draw state rather than link state, and the CTS case that motivated this links at the
// default 3 and draws at 4. The member set comes from the neighbouring evaluation stage's
// own SPIR-V, so two programs at different GLSL versions need different modules. The
// pipeline cache re-keys on the same inputs, so the module a pipeline was built with is
// part of that pipeline's identity. Compiling is bounded by the number of distinct
// (size, levels, members) combinations a program draws with - one or two in practice -
// and only ever happens for the rare program that has no control stage at all.
VkPipelineShaderStageCreateInfo GetOrCreatePassthroughTessControlStage(Uint32 patchVertices,
const FloatVec4& defaultOuterLevel,
const FloatVec2& defaultInnerLevel,
Uint32 perVertexMembers);
// Source of the module above. Exposed for tests: the generated GLSL is the whole
// contract with the evaluation stage, so it is worth pinning independently of a device.
static String BuildPassthroughTessControlSource(Uint32 patchVertices, const FloatVec4& defaultOuterLevel,
const FloatVec2& defaultInnerLevel, Uint32 perVertexMembers);
// The identity of one such module: everything the generator bakes in, folded into a
// 64-bit key over the raw bits (so -0.0 and +0.0 key apart, which is harmless, and NaN
// keys to itself, which is what matters). Shared with PipelineFactory, which mixes the
// same value into the pipeline hash so a pipeline can never be handed a module built for
// different levels or a different block shape.
static Uint64 ComputePassthroughTessControlKey(Uint32 patchVertices, const FloatVec4& defaultOuterLevel,
const FloatVec2& defaultInnerLevel, Uint32 perVertexMembers);
// The PerVertexMemberBit mask of the INPUT per-vertex block a module declares, read
// straight out of its SPIR-V (OpMemberDecorate ... BuiltIn on the struct behind the one
// Input variable that is an array of a Block-decorated struct). Zero when the module has
// no such block. Exposed for tests, which is the only way to pin the shape agreement
// without a device.
static Uint32 ReflectPerVertexInputMembers(const Vector<Uint>& spirv);
private: private:
struct ProgramLookupCache { struct ProgramLookupCache {
@@ -301,14 +587,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}; };
static TextureTarget UniformTypeToTextureTarget(GLenum glType); static TextureTarget UniformTypeToTextureTarget(GLenum glType);
void ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders, // `stages` is ALWAYS ProgramObject::GetLinkedShaderStages() - one entry per module of
// `spirv`, at the same index. Taking the stages rather than the shader objects is what
// keeps the program's live attach list, which is a longer and differently-indexed list
// the moment a glAttachShader lands after the link, from being passed here by mistake.
void ReflectVertexInputs(const Vector<ShaderStage>& stages,
const Vector<Vector<Uint>>& spirv, const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const; VkProgramObject& entry) const;
void ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders, void ReflectViewportIndexUsage(const Vector<ShaderStage>& stages,
const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const;
void ReflectFragmentOutputs(const Vector<ShaderStage>& stages,
const Vector<Vector<Uint>>& spirv, const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const; VkProgramObject& entry) const;
void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv, void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const; VkProgramObject& entry) const;
// Fills needsPassthroughTessControl / passthroughTessControlEmulatable off the linked
// modules. Const and reflection-only: it decides nothing about the pipeline, it only
// records what the evaluation stage's input interface is made of.
void ReflectPassthroughTessControlNeed(const Vector<ShaderStage>& stages,
const Vector<Vector<Uint>>& spirv,
VkProgramObject& entry) const;
VkDevice m_device = VK_NULL_HANDLE; VkDevice m_device = VK_NULL_HANDLE;
Uint32 m_maxBindings = 0; Uint32 m_maxBindings = 0;
@@ -320,12 +619,42 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// True only when the logical device enabled both // True only when the logical device enabled both
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat. // shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
Bool m_unformattedFloatStorageImagesEnabled = false; Bool m_unformattedFloatStorageImagesEnabled = false;
// True when the logical device enabled shaderTessellationAndGeometryPointSize. When it is
// FALSE a program whose tessellation or geometry module declares TessellationPointSize /
// GeometryPointSize is refused at build time (see VkProgramObject::
// pointSizeCapabilityUnsupported) instead of being handed to the driver as invalid usage.
Bool m_tessellationAndGeometryPointSizeEnabled = false;
// Startup snapshot used only by internally synthesized shader modules, which do not
// originate from a ProgramLinkTask.
Bool m_enableSpirvValidation = false;
// Device feature and limit gate resolved before vkCreateDevice. Keeping it in
// the factory lets each reflected layout choose ordinary descriptors when its
// own counts would exceed the update-after-bind budget.
UpdateAfterBindLimits m_updateAfterBindLimits{};
SubgroupLoweringPolicy m_subgroupPolicy{};
// See SetDefaultFramebufferHeight. 0 means "not known yet"; the FragCoordYFlip bit is
// never set before the swapchain exists, so no variant can be compiled against it.
Uint32 m_defaultFramebufferHeight = 0;
mutable ProgramLookupCache m_lastLookup; mutable ProgramLookupCache m_lastLookup;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging. // Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameCounter = 0; Uint64 m_frameCounter = 0;
// See GetCacheStructureEpoch(). Starts at 1 so a zero-initialized memo can never match. // See GetCacheStructureEpoch(). Starts at 1 so a zero-initialized memo can never match.
Uint64 m_cacheStructureEpoch = 1; Uint64 m_cacheStructureEpoch = 1;
IEvictionObserver* m_evictionObserver = nullptr; IEvictionObserver* m_evictionObserver = nullptr;
// Pass-through tessellation control stages by the identity of what was compiled into
// them - the input patch size and the six default tessellation levels, folded into one
// 64-bit key by ComputePassthroughTessControlKey (the levels are float state, so the map
// cannot simply be keyed on the patch size any more). A failed build is cached as
// VK_NULL_HANDLE so a broken generator costs one compile, not one per draw.
//
// Hard-capped, because the key is application-controlled: glPatchParameterfv clamps
// nothing, so an application that recomputes a level per frame mints a new key per frame.
// Reaching the cap destroys every module and starts over (see the flush in
// GetOrCreatePassthroughTessControlStage); the cap is far above what any program that
// holds its levels still will ever need. The gl_PerVertex member set is in the key too
// and adds only a handful of values, so it does not move the cap in practice.
static constexpr SizeT kMaxPassthroughTessControlStages = 64;
UnorderedMap<Uint64, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
static inline XXH64_state_t* m_hashState = XXH64_createState(); static inline XXH64_state_t* m_hashState = XXH64_createState();
}; };
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
@@ -157,7 +157,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MGLOG_I("Got %d surface formats:", swapchainCapabilities.surfaceFormats.size()); MGLOG_I("Got %d surface formats:", swapchainCapabilities.surfaceFormats.size());
for (const auto& sf : swapchainCapabilities.surfaceFormats) { for (const auto& sf : swapchainCapabilities.surfaceFormats) {
MGLOG_I(" [%s, %s]", string_VkFormat(sf.format), string_VkColorSpaceKHR(sf.colorSpace)); MGLOG_D(" [%s, %s]", string_VkFormat(sf.format), string_VkColorSpaceKHR(sf.colorSpace));
} }
const auto pickedSurfaceFormat = ChooseSwapchainSurfaceFormat(swapchainCapabilities.surfaceFormats); const auto pickedSurfaceFormat = ChooseSwapchainSurfaceFormat(swapchainCapabilities.surfaceFormats);
@@ -166,7 +166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MGLOG_I("Got %d present modes:", swapchainCapabilities.presentModes.size()); MGLOG_I("Got %d present modes:", swapchainCapabilities.presentModes.size());
for (const auto& pm : swapchainCapabilities.presentModes) { for (const auto& pm : swapchainCapabilities.presentModes) {
MGLOG_I(" %s", string_VkPresentModeKHR(pm)); MGLOG_D(" %s", string_VkPresentModeKHR(pm));
} }
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes); const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes);
File diff suppressed because it is too large Load Diff
@@ -26,12 +26,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
public: public:
struct SamplerBindingOverride { struct SamplerBindingOverride {
Uint32 binding = 0; Uint32 binding = 0;
Uint32 element = 0;
MG_State::GLState::ITextureObject* texture = nullptr; MG_State::GLState::ITextureObject* texture = nullptr;
const MG_State::GLState::SamplerObject* sampler = nullptr; const MG_State::GLState::SamplerObject* sampler = nullptr;
VkImageView imageView = VK_NULL_HANDLE; VkImageView imageView = VK_NULL_HANDLE;
VkImageLayout imageLayout = VK_IMAGE_LAYOUT_UNDEFINED;
Bool forceNearestFiltering = false;
}; };
Bool Initialize(VkDevice device, VkBufferManager* bufferManager, struct SamplerImageFeedbackBinding {
Uint32 samplerBinding = 0;
Uint32 samplerElement = 0;
MG_State::GLState::ITextureObject* texture = nullptr;
const MG_State::GLState::SamplerObject* sampler = nullptr;
SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown;
};
// `physicalDevice` is only ever asked for format properties: a placeholder descriptor for
// an unbound texel-buffer binding has to be built from a format the DEVICE accepts as a
// texel buffer, and there is no other route to that answer from here.
Bool Initialize(VkDevice device, VkPhysicalDevice physicalDevice, VkBufferManager* bufferManager,
ProgramFactory* programFactory, ProgramFactory* programFactory,
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount, VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
Uint32 maxBindings = 16, Uint32 setsPerFrame = 64, Uint32 maxBindings = 16, Uint32 setsPerFrame = 64,
@@ -53,10 +67,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// caches - a live layout's entry must never be purged (its sets would be // caches - a live layout's entry must never be purged (its sets would be
// unreachable pool slots), so there is deliberately no age-based sweep here. // unreachable pool slots), so there is deliberately no age-based sweep here.
void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout); void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout);
// One record per visited CombinedImageSampler binding (post fallback substitution, // One record per visited CombinedImageSampler DESCRIPTOR (post fallback substitution,
// in binding order): the resolved texture and effective sampler, as never-reused // in binding order, and within a binding in array-element order): the resolved texture
// lifetime ids so a freed-and-reallocated object at the same heap address can only // and effective sampler, as never-reused lifetime ids so a freed-and-reallocated object
// MISS a comparison, never false-hit it (same ABA rule as SamplerResolveMemo). // at the same heap address can only MISS a comparison, never false-hit it (same ABA
// rule as SamplerResolveMemo). An arrayed binding contributes one record per element -
// element granularity is required, or swapping the textures of two elements of the same
// array would leave the record list identical and the fast path would keep a stale set.
struct SampledBindingRecord { struct SampledBindingRecord {
Uint64 textureLifetimeId = 0; Uint64 textureLifetimeId = 0;
Uint64 samplerLifetimeId = 0; Uint64 samplerLifetimeId = 0;
@@ -76,6 +93,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program, Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures) const; Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
Bool CollectSamplerImageFeedback(
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<SamplerImageFeedbackBinding>& outBindings) const;
static Bool SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel,
GLint imageLevel, GLenum imageAccess);
// samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that // samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that
// every input of every combined-image-sampler resolution is unchanged since the // every input of every combined-image-sampler resolution is unchanged since the
// previous draw's resolve - same (texture, sampler) per binding, texture params // previous draw's resolve - same (texture, sampler) per binding, texture params
@@ -88,7 +111,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 frameIndex, Uint32 frameIndex,
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
const SamplerBindingOverride* samplerBindingOverride = nullptr, const SamplerBindingOverride* samplerBindingOverride = nullptr,
Bool samplerDescriptorsUnchangedHint = false); Bool samplerDescriptorsUnchangedHint = false,
const Vector<SamplerBindingOverride>* samplerBindingOverrides = nullptr);
// Pure format-policy helper kept public for host regression tests. Formatted storage // Pure format-policy helper kept public for host regression tests. Formatted storage
// images use their shader qualifier; transformed float images use glBindImageTexture's // images use their shader qualifier; transformed float images use glBindImageTexture's
@@ -111,6 +135,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDescriptorPool handle = VK_NULL_HANDLE; VkDescriptorPool handle = VK_NULL_HANDLE;
Uint32 maxSets = 0; Uint32 maxSets = 0;
Uint32 allocatedSets = 0; Uint32 allocatedSets = 0;
Bool updateAfterBind = false;
}; };
// A cached descriptor set together with the pool it was allocated from, so a // A cached descriptor set together with the pool it was allocated from, so a
@@ -143,8 +168,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// texture after the fallback substitution (may still be null when no fallback // texture after the fallback substitution (may still be null when no fallback
// exists), effective sampler = unit override else the texture's own sampler. // exists), effective sampler = unit override else the texture's own sampler.
// False = the binding is skipped (unbound with a non-2D fallback target). // False = the binding is skipped (unbound with a non-2D fallback target).
// `element` indexes a sampler array inside the binding; see ResolveSamplerDescriptor.
Bool ResolveSampledBinding(const MG_State::GLState::ProgramObject& program, Bool ResolveSampledBinding(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element,
MG_State::GLState::ITextureObject*& outTexture, MG_State::GLState::ITextureObject*& outTexture,
const MG_State::GLState::SamplerObject*& outSampler) const; const MG_State::GLState::SamplerObject*& outSampler) const;
// Raw-pointer variant for the per-draw sampled-texture walk (CollectSampledTextures): // Raw-pointer variant for the per-draw sampled-texture walk (CollectSampledTextures):
@@ -152,27 +178,81 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// only need the pointer skip the SharedPtr copy's atomic refcount churn. // only need the pointer skip the SharedPtr copy's atomic refcount churn.
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw( static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
const MG_State::GLState::ProgramObject& program, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding); const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element);
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const; // `numericDomain` is the sampler's class, and it matters only for the multisample arm -
// see GetFallbackMultisampleTexture for why the single-sampled fallback can ignore it.
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(
TextureTarget target, SamplerNumericDomain numericDomain) const;
// The multisample arm of GetFallbackTexture. One object per (target, numeric domain) and
// no upload path: a multisample image cannot be written by a transfer, so its texels stay
// undefined - which is what GL promises for a texelFetch on an incomplete multisample
// texture - and it cannot carry MUTABLE_FORMAT, so its format has to match the sampler's
// class outright rather than being reinterpreted at view time.
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackMultisampleTexture(
TextureTarget target, SamplerNumericDomain numericDomain) const;
// ---- placeholders for UNBOUND image-backed descriptors -------------------------
// GL lets a program declare `samplerBuffer`, `imageBuffer` or `image2D` and bind nothing
// to the unit it names: the fetch is then undefined (GL 4.6 core 8.9 for an incomplete
// buffer texture, 8.26 for an image unit with no texture) - undefined VALUES, not a
// dropped draw. Vulkan has no unwritten descriptor, so something valid has to sit in the
// set or the whole draw or dispatch is lost, which is what these two build. Same shape as
// VkBufferManager::AcquireUnboundStorageDescriptor, one level up: per FORMAT rather than
// one shared object, because a descriptor whose format disagrees with the shader's
// declaration is invalid Vulkan even when nothing ever reads it.
//
// `declaredFormat` is the format the SHADER declared (VK_FORMAT_UNDEFINED for a sampled
// texel buffer, which never carries one, or for a formatless `writeonly` image);
// `numericDomain` decides the format when there is no declaration and is the fallback
// class when the device cannot use the declared one as a texel buffer.
VkBufferView AcquireUnboundTexelBufferView(VkFormat declaredFormat, SamplerNumericDomain numericDomain,
Bool storage);
// A 1x1 (x1 layer, or 6 faces for a cube) texture of `format`, shaped for `target` so the
// view the descriptor gets has the view type the shader's image declaration demands.
// Null for a target with no single-sampled placeholder shape - multisample images, whose
// descriptor needs a multisample view that this cannot stand in for.
SharedPtr<MG_State::GLState::ITextureObject> GetUnboundStorageImageTexture(TextureTarget target,
VkFormat format) const;
// The (target, format) pair a storage-image binding's placeholder is keyed by, resolved
// from reflection alone. False when the binding has no placeholder shape.
Bool ResolveUnboundStorageImagePlaceholder(const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
TextureTarget& outTarget, VkFormat& outFormat) const;
// `element` indexes a sampler ARRAY inside one binding; each element carries its own
// independently assigned GL texture unit, so it selects the texture, the sampler
// override and the fallback separately from its neighbours.
//
// trustUnchangedHint: reuse this binding's cached VkDescriptorImageInfo outright // trustUnchangedHint: reuse this binding's cached VkDescriptorImageInfo outright
// (see BindProgramUniformBuffers' samplerDescriptorsUnchangedHint for the proof // (see BindProgramUniformBuffers' samplerDescriptorsUnchangedHint for the proof
// obligations the caller carries). // obligations the caller carries). The cache is keyed by binding alone, so it is
// used ONLY for single-descriptor bindings - see m_samplerResolveMemo.
Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program, Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
VkDescriptorImageInfo& outImageInfo, Uint32 element, VkDescriptorImageInfo& outImageInfo,
Bool trustUnchangedHint = false) const; Bool trustUnchangedHint = false) const;
Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride, Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride,
VkDescriptorImageInfo& outImageInfo) const; VkDescriptorImageInfo& outImageInfo) const;
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program, Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 frameIndex, VkBufferView& outBufferView); Uint32 frameIndex, VkBufferView& outBufferView);
// GLSL `imageBuffer`: the same VkBufferView descriptor as the sampled texel buffer above,
// but resolved from an IMAGE unit (glBindImageTexture) rather than a texture unit, and
// made GPU-resident-writable because the shader may store to it. No `element` parameter:
// an imageBuffer ARRAY is refused at program creation, so a binding is always one
// descriptor (see the array gate in RemapDescriptorBindingsForVulkan).
Bool ResolveStorageTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 frameIndex, VkBufferView& outBufferView);
// `element` indexes a block INSTANCE array's descriptors; it is 0 for every ordinary
// block. Each element resolves through its own GL storage block, and so its own GL
// binding point, buffer and glBindBufferRange window.
Bool ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program, Bool ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
VkDescriptorBufferInfo& outBufferInfo) const; Uint32 element, VkDescriptorBufferInfo& outBufferInfo) const;
// `element` indexes an image ARRAY inside one binding; each element carries its own
// independently assigned GL image unit.
Bool ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer, Bool ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
VkDescriptorImageInfo& outImageInfo) const; Uint32 element, VkDescriptorImageInfo& outImageInfo) const;
// Result of resolving a UBO binding: either a zero-copy direct bind to the app's resident // Result of resolving a UBO binding: either a zero-copy direct bind to the app's resident
// VkBuffer (the GLES backend's approach - no per-draw copy) or the CPU payload to upload. // VkBuffer (the GLES backend's approach - no per-draw copy) or the CPU payload to upload.
struct UboBindResult { struct UboBindResult {
@@ -201,8 +281,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint, void BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint,
VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet, VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet,
const Vector<Uint32>& dynamicOffsets); const Vector<Uint32>& dynamicOffsets);
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const; Bool CreateDescriptorPool(Uint32 maxSets, Bool updateAfterBind, VkDescriptorPool& outPool) const;
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex); Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind);
VkResult AllocateDescriptorSetsFromActivePool( VkResult AllocateDescriptorSetsFromActivePool(
Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet); Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet);
VkResult AcquireDescriptorSet(Uint32 frameIndex, VkResult AcquireDescriptorSet(Uint32 frameIndex,
@@ -210,6 +290,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDescriptorSet& outDescriptorSet); VkDescriptorSet& outDescriptorSet);
VkDevice m_device = VK_NULL_HANDLE; VkDevice m_device = VK_NULL_HANDLE;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
VkBufferManager* m_bufferManager = nullptr; VkBufferManager* m_bufferManager = nullptr;
ProgramFactory* m_programFactory = nullptr; ProgramFactory* m_programFactory = nullptr;
Vector<FrameResources> m_frames; Vector<FrameResources> m_frames;
@@ -222,6 +303,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkTextureManager* m_textureManager = nullptr; VkTextureManager* m_textureManager = nullptr;
VkSamplerManager* m_samplerManager = nullptr; VkSamplerManager* m_samplerManager = nullptr;
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D; mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
// Keyed by (arrayed, numeric domain); see GetFallbackMultisampleTexture. Lazily populated,
// never evicted - at most six tiny 1x1 images - and torn down with the manager.
mutable UnorderedMap<Uint32, SharedPtr<MG_State::GLState::ITextureObject>> m_fallbackMultisampleTextures;
// See AcquireUnboundTexelBufferView / GetUnboundStorageImageTexture. Both are lazily
// populated, never evicted (a program's declared formats are a fixed, tiny set) and torn
// down with the manager. The texel views are keyed by format AND by storage-vs-sampled
// because the two descriptor kinds demand different format FEATURES of the device, so one
// format can be usable for one and not the other. Deliberately NOT the per-frame
// texelBufferViews list: those are destroyed at every frame boundary, and these must
// outlive it or the placeholder would be rebuilt for every unbound binding every frame.
UnorderedMap<Uint64, VkBufferView> m_unboundTexelBufferViews;
mutable UnorderedMap<Uint64, SharedPtr<MG_State::GLState::ITextureObject>> m_unboundStorageImageTextures;
// Per-draw scratch buffers for BindProgramUniformBuffers: reused (clear keeps // Per-draw scratch buffers for BindProgramUniformBuffers: reused (clear keeps
// capacity) so the descriptor-write path stops allocating on every draw. // capacity) so the descriptor-write path stops allocating on every draw.
@@ -319,8 +412,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// lifetime id, so a freed-and-reallocated sampler or texture at the same heap address // lifetime id, so a freed-and-reallocated sampler or texture at the same heap address
// always gets a fresh id and misses (a raw pointer would false-hit that ABA) - so a // always gets a fresh id and misses (a raw pointer would false-hit that ABA) - so a
// stale guess can only miss and fall through to the hash, never resolve wrong. Still // stale guess can only miss and fall through to the hash, never resolve wrong. Still
// reset each frame alongside the descriptor-set cache. Indexed by binding. // reset each frame alongside the descriptor-set cache. Indexed by binding, but the
// whole-descriptor entry is additionally keyed by program lifetime: Vulkan binding
// numbers are layout-local and unrelated programs routinely reuse binding 0/1.
struct SamplerResolveMemo { struct SamplerResolveMemo {
Uint64 infoProgramLifetimeId = 0;
Uint64 samplerLifetimeId = 0; Uint64 samplerLifetimeId = 0;
Uint64 textureLifetimeId = 0; Uint64 textureLifetimeId = 0;
VkSampler sampler = VK_NULL_HANDLE; VkSampler sampler = VK_NULL_HANDLE;
@@ -341,6 +437,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// proves every resolve input unchanged; cleared with the per-frame reset // proves every resolve input unchanged; cleared with the per-frame reset
// (the cached VkSampler outlives a frame only via a fresh resolve, which // (the cached VkSampler outlives a frame only via a fresh resolve, which
// also re-stamps it against VkSamplerManager's frame-boundary sweep). // also re-stamps it against VkSamplerManager's frame-boundary sweep).
//
// This one field is keyed by binding but describes ONE descriptor, so it is
// written and read only for single-descriptor bindings. A sampler ARRAY's
// elements share the binding and would overwrite each other here - the last
// element resolved would then be handed to element 0 on the next hinted draw.
// Every other field above is self-validating (each compares its full key
// before reuse, and the view-format entry is a pure function of format and
// numeric domain), so an arrayed binding may keep using those.
VkDescriptorImageInfo info{}; VkDescriptorImageInfo info{};
Bool infoValid = false; Bool infoValid = false;
}; };
@@ -7,7 +7,9 @@
// End of Source File Header // End of Source File Header
#include "VertexInputStateFactory.h" #include "VertexInputStateFactory.h"
#include "MagmaPipeArms.h"
#include "MG_Util/Converters/MGToStr/DataTypeConverter.h" #include "MG_Util/Converters/MGToStr/DataTypeConverter.h"
#include <MG_Backend/BackendObjects.h>
#include <utility> #include <utility>
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
@@ -44,25 +46,149 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// capture came back holding a dead VAO's vertex data (0,0,0,1 - the previous // capture came back holding a dead VAO's vertex data (0,0,0,1 - the previous
// test's positions) instead of its own. // test's positions) instead of its own.
// Zero for client memory (no buffer), which is a distinct identity of its own. // Zero for client memory (no buffer), which is a distinct identity of its own.
const Uint64 bufferKey = attr.Buffer ? attr.Buffer->GetLifetimeId() : 0; //
// P2 D12.4 / ARCHITECTURE.md 9.5: under the handle arm the identity is the
// buffer's {slot, gen} rather than its lifetime id - "lifetimeId -> gen mixed
// into every server-side content hash". The two are equally ABA-proof (the
// allocator maps one onto the other and bumps Gen only on slot REUSE); what
// changes is that the key is now the identity the SERVER will be handed once
// buffers travel as handles, instead of a number only the client can mint.
Uint64 bufferKey = attr.Buffer ? attr.Buffer->GetLifetimeId() : 0;
#if MOBILEGL_PIPE_PUSH
if (attr.Buffer) {
// The SAME arm question the other four re-keyed sites ask, through the same
// helper: a site that decided for itself could silently key on the pre-handle
// identity while its neighbours keyed on the handle.
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
const MG_Pipe::MGPipeHandle handle =
m_identity->HandleOf(MG_Pipe::MGPipeKind::Buffer, attr.Buffer->GetLifetimeId());
bufferKey = static_cast<Uint64>(handle.Slot) | (static_cast<Uint64>(handle.Gen) << 32);
}
if (MagmaPipeAbaControlDefeatsIdentity()) {
// Negative control C (P2 brief D18), on WHICHEVER arm this run is on - the
// pre-handle lifetime id and the handle's {slot, gen} are the same guard
// wearing two hats, and a control that defeated only the retired one would
// say nothing about the key P2 ships.
//
// The identity is replaced by a constant rather than by the raw
// BufferObject*, because the address is not recycled in practice and so
// never collides (see MagmaPipeAbaControlDefeatsIdentity). Zero is what a
// key with NO buffer identity in it looks like - the exact defect this
// hash was fixed for: "the hash is what TryBindResolvedVertexBindings
// accepts as proof that a memoised binding still reads the buffer it was
// resolved from", and with the identity gone it accepts a binding resolved
// from a different buffer. HandleRecycleScenario.AbaControl then draws a
// replacement VAO and gets its dead predecessor's vertex data.
bufferKey = 0;
}
}
#endif
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey))); XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
} }
return XXH64_digest(m_hashState); return XXH64_digest(m_hashState);
} }
#if MOBILEGL_PIPE_PUSH
VertexInputStateFactory::VaoBackendMemos& VertexInputStateFactory::MemosFor(
const MG_State::GLState::VertexArrayObject& vao) const {
const MG_Pipe::MGPipeHandle handle =
m_identity->HandleOf(MG_Pipe::MGPipeKind::VertexElementsCso, vao.GetLifetimeId());
// One entry per mintable slot, grown on demand: the mint has no capacity, so neither
// does this, and no two live VAOs can share an entry however large the working set is.
// There is no probe in front of it because the mint itself is one - a one-entry memo
// hit for every acquisition after this draw's first, and a hash probe otherwise.
//
// The claim rule - the slot picks the entry, the whole handle (Gen included) decides
// whose it is - and negative control C's defeat of it are MagmaPipeArms.h's
// MagmaPipeClaimSlotMemos, so that the unit suite which drives a REAL slot reuse
// (MG_Test/Pipe/MagmaPipeIdentityTest.cpp) exercises this code and not a copy of it.
// What the control defeats HERE is the identity that SELECTS the entry: every VAO
// collapses onto one, handed back uncleared, so the replacement inherits the dead
// VAO's content hash and its resolved-entry pointer. The GENERATION half is the unit
// suite's business, for the reason MagmaPipeAbaControlDefeatsIdentity spells out.
return MagmaPipeClaimSlotMemos(m_vaoMemos, handle);
}
#endif
#if MOBILEGL_PIPE_PUSH
Bool VertexInputStateFactory::TryGetMemoizedHash(const MG_State::GLState::VertexArrayObject& vao,
Uint64& outHash) const {
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
const VaoBackendMemos& memos = MemosFor(vao);
if (memos.HashConfigVersion != vao.GetConfigVersion()) return false;
outHash = memos.Hash;
return true;
}
#if MOBILEGL_PIPE_LEGACY_MEMOS
return vao.GetBackendHashMemo(outHash);
#else
return false;
#endif
}
#endif
VertexInputStateFactory::HashType VertexInputStateFactory::GetOrComputeHash( VertexInputStateFactory::HashType VertexInputStateFactory::GetOrComputeHash(
const MG_State::GLState::VertexArrayObject& vao) const { const MG_State::GLState::VertexArrayObject& vao) const {
HashType hash = 0; HashType hash = 0;
#if MOBILEGL_PIPE_PUSH
// P2 D12.5: the same memo, on the backend's side of the boundary.
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
VaoBackendMemos& memos = MemosFor(vao);
if (memos.HashConfigVersion == vao.GetConfigVersion()) {
return memos.Hash;
}
hash = ComputeHash(vao);
memos.Hash = hash;
memos.HashConfigVersion = vao.GetConfigVersion();
return hash;
}
#endif
#if MOBILEGL_PIPE_LEGACY_MEMOS
if (!vao.GetBackendHashMemo(hash)) { if (!vao.GetBackendHashMemo(hash)) {
hash = ComputeHash(vao); hash = ComputeHash(vao);
vao.SetBackendHashMemo(hash); vao.SetBackendHashMemo(hash);
} }
#endif
return hash; return hash;
} }
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState( const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao) { const MG_State::GLState::VertexArrayObject& vao) {
#if MOBILEGL_PIPE_PUSH
// P2 D12.5: the same per-draw fast path, but the resolved-entry pointer lives in this
// factory's slot-indexed table instead of on the frontend VAO. The eviction epoch
// survives the move and is still what stops a stale pointer being dereferenced: the
// POINTEE is a cache entry this factory can erase at a frame boundary, and moving the
// memo does not change that.
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
VaoBackendMemos& memos = MemosFor(vao);
if (memos.StateConfigVersion == vao.GetConfigVersion() && memos.State != nullptr &&
memos.StateEpoch == m_evictionEpoch) {
const auto* memoEntry = static_cast<const BackendVertexInputState*>(memos.State);
memoEntry->lastUsedFrameBoundary = m_frameBoundaryCounter;
return *memoEntry;
}
const BackendVertexInputState& resolved =
GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
// MemosFor is re-taken rather than kept live across GetOrCreateVertexInputState:
// the reference is not worth holding across a call that can resize the table.
VaoBackendMemos& stamp = MemosFor(vao);
stamp.State = &resolved;
stamp.StateEpoch = m_evictionEpoch;
stamp.StateConfigVersion = vao.GetConfigVersion();
// The AUX memo is deliberately NOT stamped here: its two words already live in
// VulkanRenderer::VaoDrawMemo (layoutHash / layoutAuxMasks) and its getter has no
// live reader anywhere, so the handle arm retires it rather than moving it.
return resolved;
}
#endif
#if !MOBILEGL_PIPE_LEGACY_MEMOS
// Unreachable: with no legacy arm compiled MagmaPipeTrackHArmIsHandles is a compile-
// time true, so the handle arm above always returns. Written out rather than left to
// fall off the end so the function still has a return on every path a compiler sees.
return GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
#else
// Per-draw fast path: the VAO carries a pointer to its resolved entry, // Per-draw fast path: the VAO carries a pointer to its resolved entry,
// valid while its config version and the cache's eviction epoch both // valid while its config version and the cache's eviction epoch both
// match - no re-hash, no map lookup. // match - no re-hash, no map lookup.
@@ -82,6 +208,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
vao.SetBackendAuxMemo(entry.layoutHash, vao.SetBackendAuxMemo(entry.layoutHash,
PackVertexInputAuxMasks(entry.unsupportedAttribMask, entry.attributeLocationMask)); PackVertexInputAuxMasks(entry.unsupportedAttribMask, entry.attributeLocationMask));
return entry; return entry;
#endif // MOBILEGL_PIPE_LEGACY_MEMOS
} }
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState( const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
@@ -107,10 +234,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue; continue;
} }
const VkFormat sourceVkFormat = VkFormat sourceVkFormat =
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong); ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
VertexStreamConversion conversion = VertexStreamConversion::None;
// Gated on the SAME flag ToVkVertexFormat gates its 64-bit path on, and that is
// load-bearing rather than belt-and-braces: the narrowing is only correct because the
// shader's `dvec` input is a `vec` by the time the pipeline is built, and what
// guarantees that is the flag being clear. It is clear on every backend today, and a
// program with a 64-bit float vertex input is demoted WHOLE for the same reason even
// where the device has native fp64 (ProgramSpirvTask::GenerateSpirv). With the flag
// set, a dvec3/dvec4 would be declined by ToVkVertexFormat AND left 64-bit in the
// module, so a float32 stream would be fed to a Float64 input.
const Bool narrowFloat64Arrays =
MG_Backend::pActiveBackendObject == nullptr ||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes;
if (sourceVkFormat == VK_FORMAT_UNDEFINED && attr.Type == DataType::Float64 && narrowFloat64Arrays) {
// No native 64-bit fetch here (see ToVkVertexFormat's Float64 case), but the
// source bytes are ordinary IEEE-754 doubles and DemoteFloat64Pass has already
// narrowed every dvec input to a vec, so the array is narrowed to match rather
// than dropped. Mirrors what DirectGLES does for the same state.
const VkFormat narrowedFormat = ToFloat32VertexFormat(attr.Size);
if (narrowedFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(narrowedFormat)) {
sourceVkFormat = narrowedFormat;
conversion = VertexStreamConversion::Float64ToFloat32;
MGLOG_W_ONCE("Vertex attribute location=%u is a 64-bit (GL_DOUBLE) array; fetching it at "
"float32 precision through format=%d (size=%d long=%s)",
location, static_cast<Int>(narrowedFormat), attr.Size, attr.IsLong ? "true" : "false");
}
}
if (sourceVkFormat == VK_FORMAT_UNDEFINED) { if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is " MGLOG_E_ONCE("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
"enabled but cannot be mapped to a VkFormat", "enabled but cannot be mapped to a VkFormat",
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size); location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
unsupportedAttribMask |= (1u << location); unsupportedAttribMask |= (1u << location);
@@ -118,14 +271,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
VkFormat vkFormat = sourceVkFormat; VkFormat vkFormat = sourceVkFormat;
VertexStreamConversion conversion = VertexStreamConversion::None; if (conversion == VertexStreamConversion::None && !SupportsVertexBufferFormat(vkFormat)) {
if (!SupportsVertexBufferFormat(vkFormat)) {
if (IsScaledIntegerVertexFormat(vkFormat)) { if (IsScaledIntegerVertexFormat(vkFormat)) {
const VkFormat fallbackFormat = ToFloat32VertexFormat(attr.Size); const VkFormat fallbackFormat = ToFloat32VertexFormat(attr.Size);
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) { if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
vkFormat = fallbackFormat; vkFormat = fallbackFormat;
conversion = VertexStreamConversion::ScaledIntegerToFloat32; conversion = VertexStreamConversion::ScaledIntegerToFloat32;
MGLOG_W("Vertex attribute location=%u format=%d lacks " MGLOG_W_ONCE("Vertex attribute location=%u format=%d lacks "
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d " "VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d "
"(type=%s size=%d normalized=%s integer=%s)", "(type=%s size=%d normalized=%s integer=%s)",
location, static_cast<Int>(sourceVkFormat), static_cast<Int>(vkFormat), location, static_cast<Int>(sourceVkFormat), static_cast<Int>(vkFormat),
@@ -135,7 +287,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
if (conversion == VertexStreamConversion::None) { if (conversion == VertexStreamConversion::None) {
MGLOG_E("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): " MGLOG_E_ONCE("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT is unavailable and no semantic fallback exists", "VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT is unavailable and no semantic fallback exists",
location, static_cast<Int>(sourceVkFormat), location, static_cast<Int>(sourceVkFormat),
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size); MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
@@ -146,15 +298,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra); const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
if (attribByteSize == 0) { if (attribByteSize == 0) {
MGLOG_E("Vertex attribute with unknown component size (location=%u, type=%s): the array is " MGLOG_E_ONCE("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
"enabled but cannot be sized", "enabled but cannot be sized",
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str()); location, MG_Util::ConvertDataTypeToString(attr.Type).c_str());
unsupportedAttribMask |= (1u << location); unsupportedAttribMask |= (1u << location);
continue; continue;
} }
const Uint32 sourceStride = // Verbatim, zero included. The frontend already resolved a pointer call's
attr.Stride > 0 ? static_cast<Uint32>(attr.Stride) : static_cast<Uint32>(attribByteSize); // "tightly packed" stride 0 into the element size (see VertexAttribute::Stride),
// so a zero here is the binding model's stride 0 - every vertex reads the same
// element - which is exactly what a zero VkVertexInputBindingDescription::stride
// means. Substituting the element size fetched a fresh element per vertex and ran
// off the end of the buffer (KHR-GL43.vertex_attrib_binding.basic-input-case7/8).
// Client-memory arrays cannot reach zero: they only exist on the pointer path.
const Uint32 sourceStride = static_cast<Uint32>(attr.Stride);
const Bool packedAttribute = attr.Type == DataType::Int2101010Rev || const Bool packedAttribute = attr.Type == DataType::Int2101010Rev ||
attr.Type == DataType::Uint2101010Rev; attr.Type == DataType::Uint2101010Rev;
const SizeT requiredAlignment = packedAttribute ? attribByteSize : GetComponentSize(attr.Type); const SizeT requiredAlignment = packedAttribute ? attribByteSize : GetComponentSize(attr.Type);
@@ -169,16 +327,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one // unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one
// attribute into a tightly packed transient stream without changing its format. // attribute into a tightly packed transient stream without changing its format.
conversion = VertexStreamConversion::Repack; conversion = VertexStreamConversion::Repack;
MGLOG_W("Vertex attribute location=%u uses Vulkan-incompatible alignment " MGLOG_W_ONCE("Vertex attribute location=%u uses Vulkan-incompatible alignment "
"(offset=%zu stride=%u required=%zu); using a tightly packed stream", "(offset=%zu stride=%u required=%zu); using a tightly packed stream",
location, attr.Offset, sourceStride, requiredAlignment); location, attr.Offset, sourceStride, requiredAlignment);
} }
Uint32 stride = sourceStride; Uint32 stride = sourceStride;
if (conversion == VertexStreamConversion::Repack) { // A converted stream is tightly packed, so its stride is the converted element
stride = static_cast<Uint32>(attribByteSize); // size - unless the source stride is zero, which does not describe a packing at
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32) { // all but "never advance". That survives the conversion unchanged: the draw path
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float))); // converts exactly one element and every vertex reads it.
if (sourceStride != 0) {
if (conversion == VertexStreamConversion::Repack) {
stride = static_cast<Uint32>(attribByteSize);
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32 ||
conversion == VertexStreamConversion::Float64ToFloat32) {
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
}
} }
const VkVertexInputRate inputRate = const VkVertexInputRate inputRate =
(attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE; (attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE;
@@ -275,8 +440,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) { if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) {
it = m_cache.erase(it); it = m_cache.erase(it);
// Invalidate every VAO's state-pointer memo: the erased node's // Invalidate every VAO's state-pointer memo: the erased node's
// address may be reused by a future insert. // address may be reused by a future insert. Advance through the
// process-wide source so the value stays unique across factory
// instances (see the member comment). With no legacy arm the memos
// live in this factory and die with it, so a per-instance bump is
// enough - P2 D12.5.
#if MOBILEGL_PIPE_LEGACY_MEMOS
m_evictionEpoch = ++s_evictionEpochSource;
#else
++m_evictionEpoch; ++m_evictionEpoch;
#endif
} else { } else {
++it; ++it;
} }
@@ -316,6 +489,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// for every R64 float format, so a native 64-bit vertex fetch is simply unavailable there // for every R64 float format, so a native 64-bit vertex fetch is simply unavailable there
// while shaderFloat64 is not. Both halves key off nothing but the attribute being long, // while shaderFloat64 is not. Both halves key off nothing but the attribute being long,
// so they always agree without extra plumbing. // so they always agree without extra plumbing.
//
// ... as long as the shader half still runs. It does not when the backend has declared
// no 64-bit vertex attribute support: DemoteFloat64Pass has already narrowed every
// `dvec` input to a `vec` by then, so PackDoubleVertexInputsPass finds nothing to pack
// and a UINT-formatted attribute would be fed to a float input - garbage with no
// diagnostic anywhere. Declining here hands the attribute to the caller's
// Float64ToFloat32 fallback instead, which narrows the source doubles to match the
// demoted `vec` input - the same thing DirectGLES does for the same state. The
// frontend RECORDS the format either way, so this gate is the only thing standing
// between a legal glVertexAttribLFormat and a mismatched pipeline.
if (MG_Backend::pActiveBackendObject == nullptr ||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
return VK_FORMAT_UNDEFINED;
}
if (!isLong || isInteger || normalized) return VK_FORMAT_UNDEFINED; if (!isLong || isInteger || normalized) return VK_FORMAT_UNDEFINED;
switch (size) { switch (size) {
case 1: return VK_FORMAT_R32G32_UINT; case 1: return VK_FORMAT_R32G32_UINT;
@@ -7,8 +7,12 @@
// End of Source File Header // End of Source File Header
#pragma once #pragma once
// MG_Pipe::MGPipeHandle for the P2 D12.5 memo table below. A header of constexpr constants,
// so the pull build gains nothing from it.
#include <MG_Pipe/MGPipeHandles.h>
#include "Config.h" #include "Config.h"
#include "MagmaPipeArms.h"
#include "VertexInputStateBuilder.h" #include "VertexInputStateBuilder.h"
#include "MG_State/GLState/VertexArrayState/VertexArrayObject.h" #include "MG_State/GLState/VertexArrayState/VertexArrayObject.h"
#include <Includes.h> #include <Includes.h>
@@ -23,6 +27,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
None = 0, None = 0,
Repack, Repack,
ScaledIntegerToFloat32, ScaledIntegerToFloat32,
// GL_DOUBLE source data narrowed to a tightly packed float32 stream: the fetch half
// of the fp64 demotion the shader side already does unconditionally.
Float64ToFloat32,
}; };
struct BackendVertexInputState { struct BackendVertexInputState {
@@ -67,8 +74,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}; };
}; };
#if MOBILEGL_PIPE_PUSH
// The mint is the RENDERER's (MagmaPipeIdentityTables), not a process-global and not
// this factory's: VulkanRenderer::LookupVaoDrawMemo has to derive the same {slot, gen}
// for the same VAO, and a table that outlived the context it was minted for would share
// one reclamation clock across two live contexts (review v2 minor 4).
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice,
MagmaPipeIdentityTables& identity):
m_config(config), m_physicalDevice(physicalDevice), m_identity(&identity) {}
#else
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice): VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice):
m_config(config), m_physicalDevice(physicalDevice) {} m_config(config), m_physicalDevice(physicalDevice) {}
#endif
~VertexInputStateFactory() = default; ~VertexInputStateFactory() = default;
VertexInputStateFactory(const VertexInputStateFactory&) = delete; VertexInputStateFactory(const VertexInputStateFactory&) = delete;
@@ -83,6 +100,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Memoized ComputeHash: reuses the VAO's cached hash while its config version // Memoized ComputeHash: reuses the VAO's cached hash while its config version
// is unchanged. Use this on per-draw paths. // is unchanged. Use this on per-draw paths.
HashType GetOrComputeHash(const MG_State::GLState::VertexArrayObject& vao) const; HashType GetOrComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
#if MOBILEGL_PIPE_PUSH
// The VAO's content hash IF it has already been memoized, without computing one.
// P2 D12.5: the three draw-path readers that used to ask the VAO object this
// question ask the factory instead, because that is where the memo lives once the
// frontend object stops carrying the backend's state.
Bool TryGetMemoizedHash(const MG_State::GLState::VertexArrayObject& vao, Uint64& outHash) const;
#endif
const BackendVertexInputState& GetOrCreateVertexInputState( const BackendVertexInputState& GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao, HashType hash); const MG_State::GLState::VertexArrayObject& vao, HashType hash);
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao); const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
@@ -109,12 +133,52 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static VkFormat ToFloat32VertexFormat(Int componentCount); static VkFormat ToFloat32VertexFormat(Int componentCount);
Bool SupportsVertexBufferFormat(VkFormat format) const; Bool SupportsVertexBufferFormat(VkFormat format) const;
#if MOBILEGL_PIPE_PUSH
// ---- P2 D12.5: the backend's memos, off the frontend VAO and into the backend ----
//
// The two facts that used to live as `mutable` fields on VertexArrayObject
// (Get/SetBackendHashMemo and Get/SetBackendStateMemo), kept here instead, keyed on
// the VAO's {slot, gen} and guarded by exactly the same config version. A frontend
// state object holding the backend's raw pointer is what P2 retires: under split the
// backend is in another process and its cache entry has no address a client could
// store, so the memo has to live on the side that owns the pointee.
//
// The AUX memo is not carried over: its two words moved into VaoDrawMemo::layoutHash
// and layoutAuxMasks long ago and its getter has no live reader anywhere in the tree,
// so the handle arm simply stops writing it (D12.5 says delete rather than move).
struct VaoBackendMemos {
// Whose memos these are. The identity table can recycle a slot for a different
// VAO under LRU pressure, and the handle compare - Gen included - is what says
// the contents are this object's and not its predecessor's.
MG_Pipe::MGPipeHandle Owner = MG_Pipe::kMGPipeNullHandle;
Uint64 Hash = 0;
Uint32 HashConfigVersion = ~0u;
const void* State = nullptr;
Uint64 StateEpoch = 0;
Uint32 StateConfigVersion = ~0u;
};
// Grow-on-demand (D12.4), one entry per slot the renderer's mint has ever handed
// out, and NO CAPACITY: these two memos had none before this package either - they
// were unbounded mutable fields on the VertexArrayObject itself - and re-introducing
// eviction here is what review v2 rejected. MagmaPipeSlotTable grows in chunks so an
// entry reference stays valid across the nested GetOrCreateVertexInputState call.
// 48 B per live VAO, reclaimed with the slot when the object goes idle.
mutable MagmaPipeSlotTable<VaoBackendMemos> m_vaoMemos;
// The renderer's {slot, gen} mint (see the constructor). Never null under push.
MagmaPipeIdentityTables* m_identity = nullptr;
// The entry belonging to `vao`, claimed (and cleared) if the slot currently holds
// someone else's.
VaoBackendMemos& MemosFor(const MG_State::GLState::VertexArrayObject& vao) const;
#endif
const VulkanRendererConfig& m_config; const VulkanRendererConfig& m_config;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE; VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
// Values are heap-allocated: FastSTL::unordered_map is open-addressing, // Values are heap-allocated: UnorderedMap is open-addressing, so INSERT
// so INSERT invalidates references to stored values. The draw path (and // invalidates references to stored values - and so does ERASE, which shifts
// the VAOs' state-pointer memos) hold entry pointers across inserts; // the rest of the probe cluster into the hole and therefore moves entries
// only the unique_ptr cell moves, never the pointee. // other than the erased one. The draw path (and the VAOs' state-pointer
// memos) hold entry pointers across both; only the unique_ptr cell moves,
// never the pointee.
UnorderedMap<HashType, UniquePtr<BackendVertexInputState>> m_cache; UnorderedMap<HashType, UniquePtr<BackendVertexInputState>> m_cache;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging. // Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameBoundaryCounter = 0; Uint64 m_frameBoundaryCounter = 0;
@@ -123,7 +187,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// construction); a memo is honored only while its recorded epoch // construction); a memo is honored only while its recorded epoch
// matches, so an evicted entry can never be dereferenced through a // matches, so an evicted entry can never be dereferenced through a
// stale memo. // stale memo.
//
// Drawn from a process-wide source, never a per-instance counter: the VAO
// memos outlive this factory (they live on the frontend context's VAOs, the renderer
// is destroyed and recreated on EGL surface release/re-create), so a fresh
// factory restarting at a dead factory's epoch value would honor its
// dangling entry pointers. The constructor takes a value strictly greater
// than anything a predecessor ever stamped, so a dead factory's memo can
// never compare equal here - the same never-reused idiom as the lifetime ids.
// Single-threaded like the rest of the factory (renderer-thread only).
//
// P2 D12.5: the process-wide source is the LEGACY arm's need. It exists because the
// memos live on the frontend VAOs and therefore outlive the factory. The handle arm's
// memo table is owned by this factory and dies with it, so a per-instance counter is
// enough there and the epoch shrinks back to what it looks like it should be.
#if MOBILEGL_PIPE_LEGACY_MEMOS
static inline Uint64 s_evictionEpochSource = 0;
Uint64 m_evictionEpoch = ++s_evictionEpochSource;
#else
Uint64 m_evictionEpoch = 1; Uint64 m_evictionEpoch = 1;
#endif
static inline XXH64_state_t* m_hashState = XXH64_createState(); static inline XXH64_state_t* m_hashState = XXH64_createState();
}; };
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
@@ -10,12 +10,23 @@
#include "../DirectVulkan.h" #include "../DirectVulkan.h"
#include "VulkanRenderer.h" #include "VulkanRenderer.h"
#include "MG_Util/Metrics/PipeStats.h"
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
namespace { namespace {
constexpr VmaAllocationCreateFlags kResidentBufferAllocationFlags = constexpr VmaAllocationCreateFlags kResidentBufferAllocationFlags =
VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
constexpr SizeT kLiveResourcePruneThreshold = 256; constexpr SizeT kLiveResourcePruneThreshold = 256;
// See VkBufferManager::AcquireUnboundStorageDescriptor. 256 bytes: comfortably past
// every minStorageBufferOffsetAlignment in the wild, and free.
constexpr VkDeviceSize kUnboundStorageDescriptorBytes = 256;
// See VkBufferManager::AcquireUnboundTexelBufferDescriptor. The same 256 bytes, for the
// same reason plus one: a texel buffer view's range must be a whole number of texels of
// whatever format the placeholder is asked for, and 256 divides by every texel size in
// the GL image-format table (1, 2, 4, 8 and 16 bytes).
constexpr VkDeviceSize kUnboundTexelBufferDescriptorBytes = 256;
// A zero-copy persistent buffer is created once and never recreated (the app holds // A zero-copy persistent buffer is created once and never recreated (the app holds
// its mapped pointer), and may be bound to any role, so it carries every usage. // its mapped pointer), and may be bound to any role, so it carries every usage.
// TRANSFER_DST is added by CreateResidentStorage. // TRANSFER_DST is added by CreateResidentStorage.
@@ -23,7 +34,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT |
VK_BUFFER_USAGE_TRANSFER_SRC_BIT; // "Every usage" has to mean every usage: a buffer texture reached through an IMAGE
// unit takes a VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER descriptor, and the write is
// invalid unless the buffer was created with this bit. Nothing asked for it until
// imageBuffer support existed, so the omission was invisible.
VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
// Appended to kPersistentBackedUsage when VK_EXT_transform_feedback is enabled // Appended to kPersistentBackedUsage when VK_EXT_transform_feedback is enabled
// (see VkBufferManagerInitInfo::transformFeedbackUsageEnabled). // (see VkBufferManagerInitInfo::transformFeedbackUsageEnabled).
constexpr VkBufferUsageFlags kTransformFeedbackUsage = constexpr VkBufferUsageFlags kTransformFeedbackUsage =
@@ -126,6 +141,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
m_transientUploadArena.Shutdown(); m_transientUploadArena.Shutdown();
m_unboundStorageBuffer.Destroy();
m_unboundTexelBuffer.Destroy();
DestroyAllDeferredReleases(); DestroyAllDeferredReleases();
ReleaseAllLiveResources(); ReleaseAllLiveResources();
m_copyProvider = nullptr; m_copyProvider = nullptr;
@@ -161,12 +178,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
void VkBufferManager::CollectAllDeferredReleases() { void VkBufferManager::CollectAllDeferredReleases() {
// Per-resource releases only. Every one of them was deferred behind a BumpSliceEpoch,
// so no memo can still name the handle, and the caller has proved the GPU is idle.
//
// The transient arena's releases are deliberately NOT collected here. A buffer lands
// there when the arena outgrows it mid-frame (BufferArena::EnsureCapacity), and at
// that moment every slice already handed out from this frame's arena still names it -
// VkBufferResource::transientSlice above all, which AcquireStreamedSlice keeps
// serving for the whole frame serial on the strength of transientFrameSerial alone.
// Nothing bumps the slice epoch for those other resources, so freeing the buffer
// here left the streamed memo handing a destroyed VkBuffer to vkCmdBindIndexBuffer
// (llvmpipe then faulted inside the draw; the Create/Flywheel indirect retrace died
// exactly this way). Mid-frame drains do not advance m_frameSerial, so they must not
// free arena storage either: the arena's own ResetFrame/BeginFrame is the point where
// the slot's slices stop being reachable, and that is where these releases land.
for (Uint32 frameIndex = 0; frameIndex < m_deferredBufferReleases.size(); ++frameIndex) { for (Uint32 frameIndex = 0; frameIndex < m_deferredBufferReleases.size(); ++frameIndex) {
CollectDeferredReleases(frameIndex); CollectDeferredReleases(frameIndex);
} }
for (Uint32 frameIndex = 0; frameIndex < m_transientUploadArena.GetFrameCount(); ++frameIndex) {
m_transientUploadArena.CollectDeferredReleases(frameIndex);
}
} }
void VkBufferManager::NotifyDeviceIdle() { void VkBufferManager::NotifyDeviceIdle() {
@@ -203,8 +231,38 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool VkBufferManager::UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data, Bool VkBufferManager::UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data,
VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice) { VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice) {
(void)kind; if (!m_transientUploadArena.Upload(frameIndex, data, size, alignment, outSlice)) {
return m_transientUploadArena.Upload(frameIndex, data, size, alignment, outSlice); return false;
}
if (MG_Util::PipeStats::Enabled()) {
// The single chokepoint for Magma's per-draw staging. Uniform is deliberately
// absent: its bytes are counted by the caller, which is the only place that
// knows whether the payload is the default block (stage-ubo-global) or a named
// one repacked into the ring (stage-ubo-named), and counting here as well would
// double every uniform byte.
switch (kind) {
case BufferKind::Vertex:
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
static_cast<Uint64>(size));
break;
case BufferKind::Index:
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageIndexClient,
static_cast<Uint64>(size));
break;
case BufferKind::Indirect:
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageIndirectCmd,
static_cast<Uint64>(size));
break;
case BufferKind::TextureBuffer:
case BufferKind::ShaderStorage:
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
break;
case BufferKind::Uniform:
break;
}
}
return true;
} }
Bool VkBufferManager::InitializeTransientArenas() { Bool VkBufferManager::InitializeTransientArenas() {
@@ -287,7 +345,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.requiredFlags = requiredFlags, .requiredFlags = requiredFlags,
}); });
if (!created || resource.buffer.Map() == nullptr) { if (!created || resource.buffer.Map() == nullptr) {
MGLOG_E("VkBufferManager::CreateResidentStorage failed (size=%llu)", MGLOG_E_ONCE("VkBufferManager::CreateResidentStorage failed (size=%llu)",
static_cast<unsigned long long>(size)); static_cast<unsigned long long>(size));
resource.buffer.Destroy(); resource.buffer.Destroy();
resource.storageSize = 0; resource.storageSize = 0;
@@ -309,10 +367,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false; return false;
} }
if (!resource.buffer.Upload(bufferObject.MappedData(), size, 0)) { if (!resource.buffer.Upload(bufferObject.MappedData(), size, 0)) {
MGLOG_E("VkBufferManager::SwapStorageAndUploadAll: upload failed"); MGLOG_E_ONCE("VkBufferManager::SwapStorageAndUploadAll: upload failed");
resource.pendingFullUpload = true; resource.pendingFullUpload = true;
return false; return false;
} }
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
}
resource.pendingFullUpload = false; resource.pendingFullUpload = false;
return true; return true;
} }
@@ -327,6 +388,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static_cast<VkDeviceSize>(size), 16, staging)) { static_cast<VkDeviceSize>(size), 16, staging)) {
return false; return false;
} }
if (MG_Util::PipeStats::Enabled()) {
// The staging fill is the host copy; the vkCmdCopyBuffer below is the device
// half of the same bytes and is not counted twice.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
}
VkCommandBuffer commandBuffer = m_copyProvider->AcquireBufferCopyCommandBuffer(); VkCommandBuffer commandBuffer = m_copyProvider->AcquireBufferCopyCommandBuffer();
if (commandBuffer == VK_NULL_HANDLE) { if (commandBuffer == VK_NULL_HANDLE) {
return false; return false;
@@ -368,6 +434,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
BumpSliceEpoch(*resource); BumpSliceEpoch(*resource);
// Any cached streaming slice refers to the previous contents. // Any cached streaming slice refers to the previous contents.
resource->transientFrameSerial = 0; resource->transientFrameSerial = 0;
// Redefining the store hands any adopted mapping back to the CPU shadow
// (BufferObject::RedefineStorage), so a buffer that reaches here persistent-mapped
// is an ordinary resident one again: it needs the busy-tracking and conditional
// orphan below, and the next AcquirePersistentMap has to mint storage for the new
// store rather than hand back a mapping of the old one.
resource->persistentMapped = false;
if (!resource->buffer.IsValid()) { if (!resource->buffer.IsValid()) {
return; // streaming-only resource: shadow + serial are enough return; // streaming-only resource: shadow + serial are enough
} }
@@ -388,8 +460,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
if (!resource->buffer.Upload(bufferObject.MappedData(), size, 0)) { if (!resource->buffer.Upload(bufferObject.MappedData(), size, 0)) {
MGLOG_E("VkBufferManager::OnRespecify: in-place upload failed"); MGLOG_E_ONCE("VkBufferManager::OnRespecify: in-place upload failed");
resource->pendingFullUpload = true; resource->pendingFullUpload = true;
} else if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
} }
} }
@@ -413,8 +487,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!IsResourceBusy(*resource)) { if (!IsResourceBusy(*resource)) {
if (!resource->buffer.Upload(bufferObject.MappedData() + offset, if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) { static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
MGLOG_E("VkBufferManager::OnSubData: host upload failed"); MGLOG_E_ONCE("VkBufferManager::OnSubData: host upload failed");
resource->pendingFullUpload = true; resource->pendingFullUpload = true;
} else if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
} }
return; return;
} }
@@ -450,8 +527,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if ((appAccess & BufferMappingAccessBit::Unsynchronized) || !IsResourceBusy(*resource)) { if ((appAccess & BufferMappingAccessBit::Unsynchronized) || !IsResourceBusy(*resource)) {
if (!resource->buffer.Upload(bufferObject.MappedData() + offset, if (!resource->buffer.Upload(bufferObject.MappedData() + offset,
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) { static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
MGLOG_E("VkBufferManager::OnFlushMappedRange: host upload failed"); MGLOG_E_ONCE("VkBufferManager::OnFlushMappedRange: host upload failed");
resource->pendingFullUpload = true; resource->pendingFullUpload = true;
} else if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
} }
return; return;
} }
@@ -522,6 +602,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Uint8* seed = bufferObject.MappedData(); const Uint8* seed = bufferObject.MappedData();
if (seed != nullptr) { if (seed != nullptr) {
resource->buffer.Upload(seed, size, 0); resource->buffer.Upload(seed, size, 0);
if (MG_Util::PipeStats::Enabled()) {
// The one-time seed of a persistent map. Everything the app writes AFTER
// this goes straight through the mapping and is persistent-map-push
// territory (unwired, D4/D-B4), not this class.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
} }
resource->persistentMapped = true; resource->persistentMapped = true;
resource->pendingFullUpload = false; resource->pendingFullUpload = false;
@@ -542,7 +629,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize()); const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
if (size == 0) { if (size == 0) {
MGLOG_E("VkBufferManager::AcquireResidentSlice failed: buffer size is zero"); MGLOG_E_ONCE("VkBufferManager::AcquireResidentSlice failed: buffer size is zero");
return false; return false;
} }
@@ -564,12 +651,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false; return false;
} }
if (!resource->buffer.Upload(bufferObject->MappedData(), size, 0)) { if (!resource->buffer.Upload(bufferObject->MappedData(), size, 0)) {
MGLOG_E("VkBufferManager::AcquireResidentSlice failed: initial upload failed"); MGLOG_E_ONCE("VkBufferManager::AcquireResidentSlice failed: initial upload failed");
resource->buffer.Destroy(); resource->buffer.Destroy();
resource->storageSize = 0; resource->storageSize = 0;
resource->usageFlags = 0; resource->usageFlags = 0;
return false; return false;
} }
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
resource->pendingFullUpload = false; resource->pendingFullUpload = false;
} }
@@ -599,7 +690,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize()); const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
if (size == 0) { if (size == 0) {
MGLOG_E("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero"); MGLOG_E_ONCE("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
return false; return false;
} }
@@ -649,6 +740,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
outSlice)) { outSlice)) {
return false; return false;
} }
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
}
resource->transientSlice = outSlice; resource->transientSlice = outSlice;
resource->transientFrameSerial = m_frameSerial; resource->transientFrameSerial = m_frameSerial;
resource->transientChangeSerial = changeSerial; resource->transientChangeSerial = changeSerial;
@@ -685,6 +779,70 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_deferredResourceReleases[frameIndex].clear(); m_deferredResourceReleases[frameIndex].clear();
} }
BufferSlice VkBufferManager::AcquireUnboundStorageDescriptor() {
if (!m_unboundStorageBuffer.IsValid()) {
if (m_initInfo.allocator == nullptr) {
return {};
}
// Host-visible so the zero fill needs no command buffer: this can be reached from
// descriptor resolution, which runs inside an already-open recording and must not
// start a copy of its own. The size is a whole minStorageBufferOffsetAlignment-safe
// block rather than 4 bytes so that a shader which does read the block gets a
// plausible unsized-array length instead of one that rounds to zero.
const Bool created = m_unboundStorageBuffer.Create({
.allocator = m_initInfo.allocator,
.size = kUnboundStorageDescriptorBytes,
.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
.allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT |
VMA_ALLOCATION_CREATE_MAPPED_BIT,
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
});
if (!created) {
MGLOG_E_ONCE("VkBufferManager::AcquireUnboundStorageDescriptor: placeholder creation failed");
m_unboundStorageBuffer.Destroy();
return {};
}
if (void* mapped = m_unboundStorageBuffer.GetMappedData()) {
Memset(mapped, 0, static_cast<SizeT>(kUnboundStorageDescriptorBytes));
}
}
return m_unboundStorageBuffer.GetSlice();
}
BufferSlice VkBufferManager::AcquireUnboundTexelBufferDescriptor() {
if (!m_unboundTexelBuffer.IsValid()) {
if (m_initInfo.allocator == nullptr) {
return {};
}
// A SECOND placeholder rather than more usage bits on the storage-block one. The two
// are independent failure domains: a device that refuses this allocation must not
// take the storage-block placeholder - and with it the fix this one is a sibling of -
// down with it. Host-visible and zero-filled for the same reason as that one: this is
// reached from descriptor resolution, inside an already-open recording, which must
// not start a copy of its own.
const Bool created = m_unboundTexelBuffer.Create({
.allocator = m_initInfo.allocator,
.size = kUnboundTexelBufferDescriptorBytes,
.usage = VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT |
VK_BUFFER_USAGE_TRANSFER_DST_BIT,
.memoryUsage = VMA_MEMORY_USAGE_AUTO,
.allocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT |
VMA_ALLOCATION_CREATE_MAPPED_BIT,
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
});
if (!created) {
MGLOG_E_ONCE("VkBufferManager::AcquireUnboundTexelBufferDescriptor: placeholder creation failed");
m_unboundTexelBuffer.Destroy();
return {};
}
if (void* mapped = m_unboundTexelBuffer.GetMappedData()) {
Memset(mapped, 0, static_cast<SizeT>(kUnboundTexelBufferDescriptorBytes));
}
}
return m_unboundTexelBuffer.GetSlice();
}
VkBufferUsageFlags VkBufferManager::GetVkBufferUsage(BufferKind kind) { VkBufferUsageFlags VkBufferManager::GetVkBufferUsage(BufferKind kind) {
switch (kind) { switch (kind) {
case BufferKind::Vertex: case BufferKind::Vertex:
@@ -697,7 +855,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case BufferKind::Uniform: case BufferKind::Uniform:
return VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; return VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
case BufferKind::TextureBuffer: case BufferKind::TextureBuffer:
return VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT; // Both texel roles, for the same reason vertex/index carry both bits: one GL buffer
// texture can be read as a samplerBuffer and written as an imageBuffer, and which of
// the two it is only becomes known when a shader that uses it is bound - long after
// the resident buffer was created. A VkBufferView for a storage-texel descriptor is
// invalid unless the buffer was created with the storage bit, so a buffer that
// acquired only the uniform bit could never be given one.
return VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT;
case BufferKind::ShaderStorage: case BufferKind::ShaderStorage:
return VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT; return VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT;
case BufferKind::Indirect: case BufferKind::Indirect:
@@ -102,10 +102,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Recreate all per-frame transient arenas // Recreate all per-frame transient arenas
Bool RecreateTransientArenas(Uint32 frameCount); Bool RecreateTransientArenas(Uint32 frameCount);
void BeginFrame(Uint32 frameIndex); void BeginFrame(Uint32 frameIndex);
// Drains every frame slot's deferred buffer/resource releases (and the // Drains every frame slot's deferred buffer/resource releases. Only valid when
// transient arena's parked superseded blocks). Only valid when the // the caller has proven every queue submission complete; used by the present-less
// caller has proven every queue submission complete; used by the // frame-boundary drain. Deliberately does NOT touch the transient arena's parked
// present-less frame-boundary drain. // superseded blocks: those are still named by this frame's slices (see the
// definition), and only a frame rewind retires them.
void CollectAllDeferredReleases(); void CollectAllDeferredReleases();
// All previously submitted GPU work has completed (vkDeviceWaitIdle). // All previously submitted GPU work has completed (vkDeviceWaitIdle).
void NotifyDeviceIdle(); void NotifyDeviceIdle();
@@ -119,6 +120,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data, VkDeviceSize size, Bool UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data, VkDeviceSize size,
VkDeviceSize alignment, BufferSlice& outSlice); VkDeviceSize alignment, BufferSlice& outSlice);
// The descriptor a shader storage block gets when the program declares it and the
// application bound no buffer at its GL binding point. GL 4.6 core 7.8 makes that a
// legal state - the block simply has no store, so reads are undefined and writes go
// nowhere - whereas Vulkan has no such thing as an unwritten descriptor, so something
// real has to sit in the set or the whole draw/dispatch is lost. One zero-filled
// buffer, created once and shared by every unbound binding: bindings that are only
// declared (the case this exists for) never touch it, and one that is actually read
// sees zeros, which is inside GL's "undefined". robustBufferAccess bounds anything
// that indexes past it.
BufferSlice AcquireUnboundStorageDescriptor();
// The store a texel-buffer descriptor - `samplerBuffer` or `imageBuffer` - gets when the
// unit the program's uniform names has no buffer texture on it, or the buffer texture on
// it has no GL buffer attached. Both are legal GL states that make a fetch return
// undefined values (GL 4.6 core 8.9: a buffer texture with no attached buffer object is
// incomplete, and sampling an incomplete texture is undefined - not a lost draw), and both
// used to take the whole draw or dispatch with them. The VIEW over this - one per format,
// and the descriptor is a VkBufferView, not a buffer - is built by
// UniformManager::AcquireUnboundTexelBufferView.
BufferSlice AcquireUnboundTexelBufferDescriptor();
// Draw-time acquire for resident (device-storage) buffers: ensures the // Draw-time acquire for resident (device-storage) buffers: ensures the
// resource exists and is fully uploaded, marks it used this frame. // resource exists and is fully uploaded, marks it used this frame.
Bool AcquireResidentSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject, Bool AcquireResidentSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
@@ -179,6 +201,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkBufferManagerInitInfo m_initInfo{}; VkBufferManagerInitInfo m_initInfo{};
BufferArena m_transientUploadArena; BufferArena m_transientUploadArena;
// See AcquireUnboundStorageDescriptor. Lazily created, never re-created, torn down
// with the manager.
VkBufferObject m_unboundStorageBuffer;
// See AcquireUnboundTexelBufferDescriptor. Same lifetime rules.
VkBufferObject m_unboundTexelBuffer;
IBufferCopyCommandProvider* m_copyProvider = nullptr; IBufferCopyCommandProvider* m_copyProvider = nullptr;
Vector<Vector<VkBufferObject>> m_deferredBufferReleases; Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases; Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
@@ -76,7 +76,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkResult result = const VkResult result =
vmaCreateBuffer(m_allocator, &bufferInfo, &allocationInfo, &m_buffer, &m_allocation, nullptr); vmaCreateBuffer(m_allocator, &bufferInfo, &allocationInfo, &m_buffer, &m_allocation, nullptr);
if (result != VK_SUCCESS) { if (result != VK_SUCCESS) {
MGLOG_E("VkBufferObject::Create failed: vmaCreateBuffer returned %d", result); MGLOG_E_ONCE("VkBufferObject::Create failed: vmaCreateBuffer returned %d", result);
m_allocator = nullptr; m_allocator = nullptr;
m_buffer = VK_NULL_HANDLE; m_buffer = VK_NULL_HANDLE;
m_allocation = nullptr; m_allocation = nullptr;
@@ -108,7 +108,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkResult mapResult = vmaMapMemory(m_allocator, m_allocation, &m_mappedData); const VkResult mapResult = vmaMapMemory(m_allocator, m_allocation, &m_mappedData);
if (mapResult != VK_SUCCESS || m_mappedData == nullptr) { if (mapResult != VK_SUCCESS || m_mappedData == nullptr) {
MGLOG_E("VkBufferObject::Map failed: vmaMapMemory returned %d", mapResult); MGLOG_E_ONCE("VkBufferObject::Map failed: vmaMapMemory returned %d", mapResult);
m_mappedData = nullptr; m_mappedData = nullptr;
return nullptr; return nullptr;
} }
@@ -138,14 +138,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool wasMapped = IsMapped(); const Bool wasMapped = IsMapped();
void* mapped = wasMapped ? m_mappedData : Map(); void* mapped = wasMapped ? m_mappedData : Map();
if (mapped == nullptr) { if (mapped == nullptr) {
MGLOG_E("VkBufferObject::Upload failed: unable to map buffer"); MGLOG_E_ONCE("VkBufferObject::Upload failed: unable to map buffer");
return false; return false;
} }
Memcpy(static_cast<Uint8*>(mapped) + offset, data, static_cast<SizeT>(size)); Memcpy(static_cast<Uint8*>(mapped) + offset, data, static_cast<SizeT>(size));
const VkResult flushResult = vmaFlushAllocation(m_allocator, m_allocation, offset, size); const VkResult flushResult = vmaFlushAllocation(m_allocator, m_allocation, offset, size);
if (flushResult != VK_SUCCESS) { if (flushResult != VK_SUCCESS) {
MGLOG_E("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult); MGLOG_E_ONCE("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
if (!wasMapped) { if (!wasMapped) {
Unmap(); Unmap();
} }
@@ -170,7 +170,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize); const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
if (result != VK_SUCCESS) { if (result != VK_SUCCESS) {
MGLOG_E("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result); MGLOG_E_ONCE("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
return false; return false;
} }
return true; return true;
@@ -8,7 +8,12 @@
#include "VkClearManager.h" #include "VkClearManager.h"
// For the shared ResolveAttachmentLayerCount (and the ToVulkanLevelExtent it is built on): the
// clear key's layer span has to be the same one the render pass builds its attachment view from.
#include "VkTextureManager.h"
#include "MG_State/GLState/Core.h" #include "MG_State/GLState/Core.h"
#include <MG_Pipe/PipeInputsSwitch.h>
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h" #include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h" #include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
@@ -50,7 +55,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (payload.colorEncoding != ClearColorEncoding::Float) return; if (payload.colorEncoding != ClearColorEncoding::Float) return;
// With GL_FRAMEBUFFER_SRGB enabled GL performs the encoding itself, so the driver doing it // With GL_FRAMEBUFFER_SRGB enabled GL performs the encoding itself, so the driver doing it
// is exactly right and there is nothing to undo. // is exactly right and there is nothing to undo.
if (MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)) return; if (MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)) return;
if (ResolveSrgbAttachmentWriteFormat(destinationFormat, false) == destinationFormat) return; if (ResolveSrgbAttachmentWriteFormat(destinationFormat, false) == destinationFormat) return;
// sRGB -> linear (GL 4.6 core 8.24), applied to the colour channels only: alpha is stored // sRGB -> linear (GL 4.6 core 8.24), applied to the colour channels only: alpha is stored
@@ -100,13 +105,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return ResolveAttachmentBaseArrayLayer(uploadTarget); return ResolveAttachmentBaseArrayLayer(uploadTarget);
} }
static Uint32 ResolveAttachmentLayerCount( // ResolveAttachmentLayerCount used to be duplicated here, reading attachment.GetSize().z()
const MG_State::GLState::FramebufferAttachmentObject& attachment) { // raw - no ToVulkanLevelExtent remap for a 1D array, no six-faces arm for a cube map. That is
if (attachment.IsLayered()) { // not a cosmetic difference: the count below is not key-only, it is written straight into
return static_cast<Uint32>(std::max(attachment.GetSize().z(), 1)); // VkImageSubresourceRange::layerCount by MaterializePendingClearForTexture, which then POPS
} // the entry - so a layered cube map's glClear reached one face and the other five were lost
return 1u; // for good, while the very same queued clear cleared all six through the render pass's
} // LOAD_OP_CLEAR. The helper now lives once, in VkTextureManager.h beside ToVulkanLevelExtent.
static const MG_State::GLState::FramebufferAttachmentObject* GetClearableAttachment( static const MG_State::GLState::FramebufferAttachmentObject* GetClearableAttachment(
const MG_State::GLState::FramebufferObject& drawFbo, FramebufferAttachmentType attachmentType) { const MG_State::GLState::FramebufferObject& drawFbo, FramebufferAttachmentType attachmentType) {
@@ -122,8 +127,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return &attachment; return &attachment;
} }
PendingClearKey VkClearManager::MakePendingClearKey(MG_State::GLState::ITextureObject* texture, Uint32 mipLevel, // The texture a pending clear is actually ABOUT. A clear issued through a GL texture view
// (ARB_texture_view) targets the storage it views, so it must queue against - and be found
// by - the storage texture; keying it on the view instead left the clear invisible to every
// materialisation done through the parent's name (and vice versa), so the image stayed in
// VK_IMAGE_LAYOUT_UNDEFINED and the readback was dropped as unreadable.
static MG_State::GLState::ITextureObject* ClearStorageTextureOf(MG_State::GLState::ITextureObject* texture) {
if (texture == nullptr) {
return nullptr;
}
const auto& storageOwner = texture->GetViewStorageOwner();
return storageOwner ? storageOwner.get() : texture;
}
PendingClearKey VkClearManager::MakePendingClearKey(MG_State::GLState::ITextureObject* rawTexture, Uint32 mipLevel,
Uint32 baseArrayLayer, Uint32 layerCount) { Uint32 baseArrayLayer, Uint32 layerCount) {
MG_State::GLState::ITextureObject* texture = ClearStorageTextureOf(rawTexture);
if (rawTexture != nullptr && texture != rawTexture) {
// The caller named a level and a layer of the VIEW; the key describes the STORAGE, so
// both have to be shifted into its numbering (GL 4.6 core 8.18). Without this a clear
// of a view's level 0 would collide with a clear of the storage's level 0 even when
// the view opened onto level 1.
mipLevel += static_cast<Uint32>(rawTexture->GetViewMinLevel());
baseArrayLayer += static_cast<Uint32>(rawTexture->GetViewMinLayer());
}
return PendingClearKey { return PendingClearKey {
.texture = texture, .texture = texture,
.textureLifetimeId = texture ? texture->GetLifetimeId() : 0, .textureLifetimeId = texture ? texture->GetLifetimeId() : 0,
@@ -157,6 +184,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
TextureIdentity VkClearManager::MakeTextureIdentity(MG_State::GLState::ITextureObject* texture) { TextureIdentity VkClearManager::MakeTextureIdentity(MG_State::GLState::ITextureObject* texture) {
// Same rule as VkTextureManager::MakeTextureIdentity: a GL texture view is identified by
// the storage it views. A clear posted against a view and one posted against its parent
// target the same image, so they have to coalesce rather than queue independently.
if (texture != nullptr) {
const auto& storageOwner = texture->GetViewStorageOwner();
if (storageOwner) {
texture = storageOwner.get();
}
}
return TextureIdentity { return TextureIdentity {
.texture = texture, .texture = texture,
.lifetimeId = texture ? texture->GetLifetimeId() : 0, .lifetimeId = texture ? texture->GetLifetimeId() : 0,
@@ -166,7 +202,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void VkClearManager::MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src) { void VkClearManager::MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src) {
dst.mask |= src.mask; dst.mask |= src.mask;
if ((src.mask & GL_COLOR_BUFFER_BIT) != 0) { if ((src.mask & GL_COLOR_BUFFER_BIT) != 0) {
// The whole colour story travels together (same rule as
// VkRenderPassManager::QueueRenderbufferClear): a glClearBufferiv/uiv
// payload carries its value in colorInt/colorUint and its branch selector
// in colorEncoding - dropping them here would leave the pending clear
// reading as an all-zero float one.
dst.color = src.color; dst.color = src.color;
dst.colorEncoding = src.colorEncoding;
dst.colorInt = src.colorInt;
dst.colorUint = src.colorUint;
} }
if ((src.mask & GL_DEPTH_BUFFER_BIT) != 0) { if ((src.mask & GL_DEPTH_BUFFER_BIT) != 0) {
dst.depth = src.depth; dst.depth = src.depth;
@@ -278,9 +322,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return; return;
} }
const PendingClearKey key = MakePendingClearKey(texture.get()); const auto& storageOwner = texture->GetViewStorageOwner();
const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;
const PendingClearKey key = MakePendingClearKey(storageTexture.get());
const std::lock_guard<std::mutex> lock(m_mutex); const std::lock_guard<std::mutex> lock(m_mutex);
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture; m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
auto& pending = m_pendingClears[key]; auto& pending = m_pendingClears[key];
MergeClearPayload(pending, clearPayload); MergeClearPayload(pending, clearPayload);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed); m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
@@ -297,8 +343,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
const PendingClearKey key = MakePendingClearKey(attachment); const PendingClearKey key = MakePendingClearKey(attachment);
// The alive entry must hold the STORAGE object, because the key names it:
// LockTextureIdentityLocked cross-checks the two, and registering a view here under its
// storage's identity made every lookup of this clear fail that check and silently report
// "nothing pending" - which is how a clear issued through a view's framebuffer vanished.
const auto& storageOwner = texture->GetViewStorageOwner();
const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;
const std::lock_guard<std::mutex> lock(m_mutex); const std::lock_guard<std::mutex> lock(m_mutex);
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture; m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
auto& pending = m_pendingClears[key]; auto& pending = m_pendingClears[key];
MergeClearPayload(pending, clearPayload); MergeClearPayload(pending, clearPayload);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed); m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
@@ -313,6 +365,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false; // per-draw hot path: nothing pending anywhere return false; // per-draw hot path: nothing pending anywhere
} }
texture = ClearStorageTextureOf(texture);
const Uint64 lifetimeId = texture->GetLifetimeId(); const Uint64 lifetimeId = texture->GetLifetimeId();
const std::lock_guard<std::mutex> lock(m_mutex); const std::lock_guard<std::mutex> lock(m_mutex);
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) { for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
@@ -403,6 +456,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false; // per-draw hot path: nothing pending anywhere return false; // per-draw hot path: nothing pending anywhere
} }
texture = ClearStorageTextureOf(texture);
const Uint64 lifetimeId = texture->GetLifetimeId(); const Uint64 lifetimeId = texture->GetLifetimeId();
const std::lock_guard<std::mutex> lock(m_mutex); const std::lock_guard<std::mutex> lock(m_mutex);
SharedPtr<MG_State::GLState::ITextureObject> liveTexture; SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
@@ -114,7 +114,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
class VkClearManager { class VkClearManager {
public: public:
static PendingClearKey MakePendingClearKey(const MG_State::GLState::FramebufferAttachmentObject& attachment); static PendingClearKey MakePendingClearKey(const MG_State::GLState::FramebufferAttachmentObject& attachment);
static PendingClearKey MakePendingClearKey(MG_State::GLState::ITextureObject* texture, Uint32 mipLevel = 0, // Resolves a GL texture view to the storage it views before keying; see the definition.
static PendingClearKey MakePendingClearKey(MG_State::GLState::ITextureObject* rawTexture, Uint32 mipLevel = 0,
Uint32 baseArrayLayer = 0, Uint32 layerCount = 1); Uint32 baseArrayLayer = 0, Uint32 layerCount = 1);
Bool Initialize(); Bool Initialize();
@@ -13,6 +13,7 @@
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h" #include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h" #include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Metrics/TextureMetrics.h" #include "MG_Util/Metrics/TextureMetrics.h"
#include <MG_Pipe/PipeInputsSwitch.h>
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) { static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
@@ -67,28 +68,58 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
static Uint32 ResolveAttachmentBaseArrayLayer(const MG_State::GLState::FramebufferAttachmentObject& attachment) { static Uint32 ResolveAttachmentBaseArrayLayer(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
// Every branch has to go through ToStorageArrayLayer, including the two that name layer 0
// implicitly: a layered attachment of a texture VIEW starts at the view's first layer, not
// at the image's, and a cube FACE index is a layer index like any other. Leaving either
// unshifted made the render pass write layers [0, n) while the clear key, the blit, the
// copy and the readback for the same attachment all addressed [minLayer, minLayer + n) -
// they resolve the layer through their own copies of this helper, which do shift.
const auto* texture = attachment.GetTexture().get();
if (attachment.IsLayered()) { if (attachment.IsLayered()) {
return 0; return ToStorageArrayLayer(texture, 0);
} }
const TextureUploadTarget uploadTarget = attachment.GetTextureUploadTarget(); const TextureUploadTarget uploadTarget = attachment.GetTextureUploadTarget();
if (!IsCubeMapFaceUploadTarget(uploadTarget)) { if (!IsCubeMapFaceUploadTarget(uploadTarget)) {
return static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0)); return ToStorageArrayLayer(texture, attachment.GetTextureLayer());
} }
return static_cast<Uint32>(uploadTarget) - static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX); const Int face =
static_cast<Int>(uploadTarget) - static_cast<Int>(TextureUploadTarget::CubeMapPositiveX);
return ToStorageArrayLayer(texture, face);
} }
static Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAttachmentObject& attachment) { // ResolveAttachmentLayerCount lives in VkTextureManager.h, beside ToVulkanLevelExtent, because
if (attachment.IsLayered()) { // VkClearManager needs the SAME answer: its pending-clear key's layerCount becomes a real
return static_cast<Uint32>(std::max(attachment.GetSize().z(), 1)); // VkImageSubresourceRange when a clear is materialised outside a render pass. See the header.
}
return 1u;
}
// VUID-VkFramebufferCreateInfo-flags-04113: every view handed to vkCreateFramebuffer must have
// been created as VK_IMAGE_VIEW_TYPE_2D or VK_IMAGE_VIEW_TYPE_2D_ARRAY. The image's OWN view
// type is not a legal answer for several of the targets GL can attach, and returning it
// unchanged is what took the process down on every layered 3D / cube-map-array attachment:
// a 3D view is refused outright by the layer-span guard in GetOrCreateAttachmentViewAtMipLevel
// (3D images have arrayLayers == 1) and a CUBE_ARRAY view is built happily and then rejected -
// or dereferenced - by the driver inside vkCreateFramebuffer.
//
// A 2D_ARRAY view is the legal spelling of all three: over a 2D-array-compatible 3D image its
// "layers" are the mip's z slices (VUID-VkImageViewCreateInfo-image-04970), and over a
// CUBE_COMPATIBLE 2D image - which is what both cube targets are - its layers are the faces.
//
// Knowingly NOT remapped: VK_IMAGE_VIEW_TYPE_1D / _1D_ARRAY, which 04113 also forbids. There is
// no legal alternative for them (a VK_IMAGE_TYPE_1D image admits no 2D-family view at all), so
// the only honest answer would be to decline the attachment - and every driver this has run on,
// lavapipe included, accepts them. Declining would turn working GL_TEXTURE_1D[_ARRAY] render
// targets into skipped draws to satisfy a VU nothing enforces. Left as-is, deliberately.
static VkImageViewType ResolveAttachmentViewType( static VkImageViewType ResolveAttachmentViewType(
const MG_State::GLState::FramebufferAttachmentObject& attachment, const MG_State::GLState::FramebufferAttachmentObject& attachment,
const VkTextureManager::TextureResource& resource) { const VkTextureManager::TextureResource& resource) {
if (attachment.IsLayered()) { if (attachment.IsLayered()) {
return resource.viewType; switch (resource.viewType) {
case VK_IMAGE_VIEW_TYPE_3D:
case VK_IMAGE_VIEW_TYPE_CUBE:
case VK_IMAGE_VIEW_TYPE_CUBE_ARRAY:
return VK_IMAGE_VIEW_TYPE_2D_ARRAY;
default:
return resource.viewType;
}
} }
// A non-layered attachment names ONE layer, so the view over it is a plain 2D view whatever // A non-layered attachment names ONE layer, so the view over it is a plain 2D view whatever
// the image's own view type is. The cube-face upload targets always meant this; a cube map // the image's own view type is. The cube-face upload targets always meant this; a cube map
@@ -96,8 +127,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// a single layer is not a legal attachment. The CUBE arm is inert today - no frontend path // a single layer is not a legal attachment. The CUBE arm is inert today - no frontend path
// produces a non-layered cube attachment without a face upload target - and is kept for // produces a non-layered cube attachment without a face upload target - and is kept for
// symmetry with CUBE_ARRAY. // symmetry with CUBE_ARRAY.
//
// 3D belongs in the same list and was missing from it, which is why the "per-slice
// attachment view is a 2D view whose array layer is the slice" branch in
// GetOrCreateAttachmentViewAtMipLevel was unreachable: glFramebufferTextureLayer on a
// GL_TEXTURE_3D asked for a 3D view (illegal as an attachment) whose span was then checked
// against arrayLayers == 1, so every slice above z = 0 came back VK_NULL_HANDLE.
if (IsCubeMapFaceUploadTarget(attachment.GetTextureUploadTarget()) || if (IsCubeMapFaceUploadTarget(attachment.GetTextureUploadTarget()) ||
resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY || resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE) { resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY || resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE ||
resource.viewType == VK_IMAGE_VIEW_TYPE_3D) {
return VK_IMAGE_VIEW_TYPE_2D; return VK_IMAGE_VIEW_TYPE_2D;
} }
return resource.viewType; return resource.viewType;
@@ -123,7 +161,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
if (!attachment.IsComplete()) { if (!attachment.IsComplete()) {
MGLOG_W("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u has an incomplete texture attachment; using VK_ATTACHMENT_UNUSED", MGLOG_W_ONCE("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u has an incomplete texture attachment; using VK_ATTACHMENT_UNUSED",
drawBufferIndex, drawBufferIndex,
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(), MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
fbo.GetExternalIndex()); fbo.GetExternalIndex());
@@ -132,7 +170,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto* texture = attachment.GetTexture().get(); auto* texture = attachment.GetTexture().get();
if (texture == nullptr) { if (texture == nullptr) {
MGLOG_W("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u resolved to a null texture; using VK_ATTACHMENT_UNUSED", MGLOG_W_ONCE("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u resolved to a null texture; using VK_ATTACHMENT_UNUSED",
drawBufferIndex, drawBufferIndex,
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(), MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
fbo.GetExternalIndex()); fbo.GetExternalIndex());
@@ -311,54 +349,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT; VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
if (!TryResolveSampleCountFlagBits(renderbuffer->GetSamples(), sampleCount)) { if (!TryResolveSampleCountFlagBits(renderbuffer->GetSamples(), sampleCount)) {
MGLOG_E("GetOrCreateRenderbufferResource: unsupported renderbuffer sample count %d for renderbuffer %u", MGLOG_E_ONCE("GetOrCreateRenderbufferResource: unsupported renderbuffer sample count %d for renderbuffer %u",
renderbuffer->GetSamples(), renderbuffer->GetSamples(),
renderbuffer->GetExternalIndex()); renderbuffer->GetExternalIndex());
return nullptr; return nullptr;
} }
const auto internalFormat = renderbuffer->GetInternalFormat(); const auto internalFormat = renderbuffer->GetInternalFormat();
// Three-channel color formats widen to their RGBA twin exactly like textures do // ONE resolver, shared with textures (VkTextureManager::ResolveTextureFormatInfo), so a
// (VkTextureManager::ResolveTextureFormatInfo): blits/resolves between a // renderbuffer and a texture of the same GL format cannot disagree about their VkFormat.
// renderbuffer and a texture of the same GL format then see one VkFormat. // `expandRgbToRgba` / `componentByteCount` / `alphaBytes` describe how to reshape a SHADOW
const VkFormat format = [&]() -> VkFormat { // UPLOAD, and a renderbuffer has none, so only `.format` is taken.
switch (internalFormat) { //
case TextureInternalFormat::RGB: // This used to be a hand-maintained second copy of that table, and it was missing exactly
case TextureInternalFormat::RGB8: // four rows: RGBA2 and RGBA12 fell through to ConvertTextureInternalFormatToVkEnum's
case TextureInternalFormat::R3G3B2: // VK_FORMAT_UNDEFINED (no image at all - bound as a draw buffer the attachment became
case TextureInternalFormat::RGB4: // VK_ATTACHMENT_UNUSED and every draw into it was dropped), while RGBA4 and RGB5A1 fell
case TextureInternalFormat::RGB5: // through to the 16-bit packed formats and then faced 32-bit R8G8B8A8_UNORM textures across
return VK_FORMAT_R8G8B8A8_UNORM; // a size-incompatible vkCmdCopyImage.
case TextureInternalFormat::SRGB8: const VkFormat format = ResolveTextureFormatInfo(internalFormat).format;
return VK_FORMAT_R8G8B8A8_SRGB;
case TextureInternalFormat::RGB8Snorm:
return VK_FORMAT_R8G8B8A8_SNORM;
case TextureInternalFormat::RGB10:
case TextureInternalFormat::RGB12:
case TextureInternalFormat::RGB16:
return VK_FORMAT_R16G16B16A16_UNORM;
case TextureInternalFormat::RGB16Snorm:
return VK_FORMAT_R16G16B16A16_SNORM;
case TextureInternalFormat::RGB16F:
return VK_FORMAT_R16G16B16A16_SFLOAT;
case TextureInternalFormat::RGB32F:
return VK_FORMAT_R32G32B32A32_SFLOAT;
case TextureInternalFormat::RGB8I:
return VK_FORMAT_R8G8B8A8_SINT;
case TextureInternalFormat::RGB8UI:
return VK_FORMAT_R8G8B8A8_UINT;
case TextureInternalFormat::RGB16I:
return VK_FORMAT_R16G16B16A16_SINT;
case TextureInternalFormat::RGB16UI:
return VK_FORMAT_R16G16B16A16_UINT;
case TextureInternalFormat::RGB32I:
return VK_FORMAT_R32G32B32A32_SINT;
case TextureInternalFormat::RGB32UI:
return VK_FORMAT_R32G32B32A32_UINT;
default:
return MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
}
}();
const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format); const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format);
// Renderbuffers are never sampled (GL has no way to bind one to a sampler), so the // Renderbuffers are never sampled (GL has no way to bind one to a sampler), so the
// usage set is attachment + transfer: transfer covers readback (vkCmdCopyImageToBuffer), // usage set is attachment + transfer: transfer covers readback (vkCmdCopyImageToBuffer),
@@ -457,7 +466,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, imageInfo.flags, m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, imageInfo.flags,
&imageFormatProperties); &imageFormatProperties);
if (imageFormatResult != VK_SUCCESS || (imageFormatProperties.sampleCounts & sampleCount) == 0) { if (imageFormatResult != VK_SUCCESS || (imageFormatProperties.sampleCounts & sampleCount) == 0) {
MGLOG_E("GetOrCreateRenderbufferResource: unsupported renderbuffer format=%d samples=%d for renderbuffer %u", MGLOG_E_ONCE("GetOrCreateRenderbufferResource: unsupported renderbuffer format=%d samples=%d for renderbuffer %u",
static_cast<Int>(format), static_cast<Int>(format),
static_cast<Int>(sampleCount), static_cast<Int>(sampleCount),
renderbuffer->GetExternalIndex()); renderbuffer->GetExternalIndex());
@@ -602,7 +611,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// sRGB attachments switch between their sRGB and UNORM-twin views with this // sRGB attachments switch between their sRGB and UNORM-twin views with this
// capability (ResolveSrgbAttachmentWriteFormat), changing the render pass formats. // capability (ResolveSrgbAttachmentWriteFormat), changing the render pass formats.
const Bool framebufferSrgbEnabled = const Bool framebufferSrgbEnabled =
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb); MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
XXHASH_VERIFY(XXH64_update(m_hashState, &framebufferSrgbEnabled, sizeof(framebufferSrgbEnabled))); XXHASH_VERIFY(XXH64_update(m_hashState, &framebufferSrgbEnabled, sizeof(framebufferSrgbEnabled)));
auto& drawBuffers = fbo.GetDrawBuffers(); auto& drawBuffers = fbo.GetDrawBuffers();
XXHASH_VERIFY(XXH64_update(m_hashState, drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0]))); XXHASH_VERIFY(XXH64_update(m_hashState, drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0])));
@@ -633,11 +642,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (att.IsTexture()) { if (att.IsTexture()) {
const Uint64 textureLifetimeId = att.GetTexture()->GetLifetimeId(); const Uint64 textureLifetimeId = att.GetTexture()->GetLifetimeId();
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLifetimeId, sizeof(textureLifetimeId))); XXHASH_VERIFY(XXH64_update(m_hashState, &textureLifetimeId, sizeof(textureLifetimeId)));
const Int textureLevel = att.GetTextureLevel(); const Int textureLevel = static_cast<Int>(ToStorageMipLevel(att.GetTexture().get(),
att.GetTextureLevel()));
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLevel, sizeof(textureLevel))); XXHASH_VERIFY(XXH64_update(m_hashState, &textureLevel, sizeof(textureLevel)));
const TextureUploadTarget textureUploadTarget = att.GetTextureUploadTarget(); const TextureUploadTarget textureUploadTarget = att.GetTextureUploadTarget();
XXHASH_VERIFY(XXH64_update(m_hashState, &textureUploadTarget, sizeof(textureUploadTarget))); XXHASH_VERIFY(XXH64_update(m_hashState, &textureUploadTarget, sizeof(textureUploadTarget)));
const Int textureLayer = att.GetTextureLayer(); const Int textureLayer = static_cast<Int>(ToStorageArrayLayer(att.GetTexture().get(),
att.GetTextureLayer()));
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayer, sizeof(textureLayer))); XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayer, sizeof(textureLayer)));
const Bool textureLayered = att.IsLayered(); const Bool textureLayered = att.IsLayered();
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayered, sizeof(textureLayered))); XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayered, sizeof(textureLayered)));
@@ -754,7 +765,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return XXH64_digest(m_hashState); return XXH64_digest(m_hashState);
} }
RenderPassEntry& VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo, RenderPassEntry* VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex, Uint32 swapchainImageIndex,
Bool drawUsesDepthStencil) { Bool drawUsesDepthStencil) {
// Resolve the default-FBO depth flavor (see the header comment): keep the // Resolve the default-FBO depth flavor (see the header comment): keep the
@@ -831,6 +842,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// recreated since (texture + renderbuffer image epochs), and no pending clear (which alters // recreated since (texture + renderbuffer image epochs), and no pending clear (which alters
// load ops). Any of these differing forces the full recompute below. Portable to VK 1.1. // load ops). Any of these differing forces the full recompute below. Portable to VK 1.1.
if (activeRenderPass != nullptr && m_rpFastValid && m_rpFastFbo == &fbo && if (activeRenderPass != nullptr && m_rpFastValid && m_rpFastFbo == &fbo &&
m_rpFastFboLifetimeId == fbo.GetLifetimeId() &&
m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex && m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex &&
m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() && m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() &&
m_rpFastRbEpoch == m_renderbufferImageEpoch && m_rpFastRbEpoch == m_renderbufferImageEpoch &&
@@ -839,7 +851,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto activeIt = m_renderPasses.find(activeRenderPass->hash); auto activeIt = m_renderPasses.find(activeRenderPass->hash);
if (activeIt != m_renderPasses.end()) { if (activeIt != m_renderPasses.end()) {
activeIt->second.lastUsedFrame = m_frameCounter; activeIt->second.lastUsedFrame = m_frameCounter;
return activeIt->second; return &activeIt->second;
} }
} }
@@ -855,6 +867,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// epochs AFTER ComputeHash: its attachment SyncTexture can create an image (bump the epoch). // epochs AFTER ComputeHash: its attachment SyncTexture can create an image (bump the epoch).
m_rpFastValid = true; m_rpFastValid = true;
m_rpFastFbo = &fbo; m_rpFastFbo = &fbo;
m_rpFastFboLifetimeId = fbo.GetLifetimeId();
m_rpFastFboVersion = fbo.GetObjectVersion(); m_rpFastFboVersion = fbo.GetObjectVersion();
m_rpFastSwapchainIndex = swapchainImageIndex; m_rpFastSwapchainIndex = swapchainImageIndex;
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch(); m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
@@ -862,13 +875,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_rpFastRenderPassHash = activeRenderPass->hash; m_rpFastRenderPassHash = activeRenderPass->hash;
m_rpFastHadDepthStencil = activeIt->second.hasDepthStencilAttachment; m_rpFastHadDepthStencil = activeIt->second.hasDepthStencilAttachment;
activeIt->second.lastUsedFrame = m_frameCounter; activeIt->second.lastUsedFrame = m_frameCounter;
return activeIt->second; return &activeIt->second;
} }
auto hash = ComputeHash(fbo, swapchainImageIndex, true, includeDefaultFboDepthStencil); auto hash = ComputeHash(fbo, swapchainImageIndex, true, includeDefaultFboDepthStencil);
auto it = m_renderPasses.find(hash); auto it = m_renderPasses.find(hash);
if (it != m_renderPasses.end()) { if (it != m_renderPasses.end()) {
it->second.lastUsedFrame = m_frameCounter; it->second.lastUsedFrame = m_frameCounter;
return it->second; return &it->second;
} }
Bool isDefaultFbo = fbo.IsDefaultFramebuffer(); Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
@@ -929,7 +942,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const auto& renderbuffer = rbAtt.GetRenderbuffer(); const auto& renderbuffer = rbAtt.GetRenderbuffer();
auto* rbResource = GetOrCreateRenderbufferResource(renderbuffer); auto* rbResource = GetOrCreateRenderbufferResource(renderbuffer);
if (rbResource == nullptr || (rbResource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) { if (rbResource == nullptr || (rbResource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
MGLOG_E("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color " MGLOG_E_ONCE("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color "
"renderbuffer %u; using VK_ATTACHMENT_UNUSED", "renderbuffer %u; using VK_ATTACHMENT_UNUSED",
i, fbo.GetExternalIndex(), renderbuffer->GetExternalIndex()); i, fbo.GetExternalIndex(), renderbuffer->GetExternalIndex());
continue; continue;
@@ -950,7 +963,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkImageLayout trackedRbLayout = rbResource->layout; const VkImageLayout trackedRbLayout = rbResource->layout;
const Bool rbFramebufferSrgb = const Bool rbFramebufferSrgb =
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb); MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
const VkFormat rbAttachmentFormat = const VkFormat rbAttachmentFormat =
ResolveSrgbAttachmentWriteFormat(rbResource->format, rbFramebufferSrgb); ResolveSrgbAttachmentWriteFormat(rbResource->format, rbFramebufferSrgb);
rbDesc.flags = 0; rbDesc.flags = 0;
@@ -991,8 +1004,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
textureResources.emplace_back(nullptr); textureResources.emplace_back(nullptr);
attachmentViews.emplace_back(rbAttachmentFormat != rbResource->format ? rbResource->unormTwinView attachmentViews.emplace_back(rbAttachmentFormat != rbResource->format ? rbResource->unormTwinView
: rbResource->view); : rbResource->view);
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE, if (attachmentViews.back() == VK_NULL_HANDLE) {
"GetOrCreateRenderPass: renderbuffer view missing at color attachment %d", i); MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u has no usable view for color attachment "
"%u on FBO %u; declining the render pass",
renderbuffer->GetExternalIndex(), i, fbo.GetExternalIndex());
return nullptr;
}
colorAttachmentRefs[i].attachment = rbAttachmentIndex; colorAttachmentRefs[i].attachment = rbAttachmentIndex;
continue; continue;
@@ -1004,7 +1021,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue; continue;
auto& att = fbo.GetAttachment(drawbuf); auto& att = fbo.GetAttachment(drawbuf);
const Uint32 attachmentMipLevel = static_cast<Uint32>(std::max(att.GetTextureLevel(), 0)); const Uint32 attachmentMipLevel = ToStorageMipLevel(att.GetTexture().get(), att.GetTextureLevel());
const auto textureTarget = texture->GetTarget(); const auto textureTarget = texture->GetTarget();
const Uint32 attachmentIndex = static_cast<Uint32>(attachmentDescriptions.size()); const Uint32 attachmentIndex = static_cast<Uint32>(attachmentDescriptions.size());
attachmentDescriptions.emplace_back(); attachmentDescriptions.emplace_back();
@@ -1047,8 +1064,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.key = VkClearManager::MakePendingClearKey(att) .key = VkClearManager::MakePendingClearKey(att)
}); });
} }
const IntVec2 attachmentExtent = // Same remap as ResolveAttachmentLayerCount, for the same reason: a
ResolveRenderPassFramebufferExtent(isDefaultFbo, att.GetSize(), swapchainExtent); // 1D-array attachment's GL height is its layer count, and using it as the
// framebuffer height asks for a framebuffer taller than the VK_IMAGE_TYPE_1D
// image it is built over.
const IntVec2 attachmentExtent = ResolveRenderPassFramebufferExtent(
isDefaultFbo, ToVulkanLevelExtent(texture->GetTarget(), att.GetSize()), swapchainExtent);
if (width == 0) if (width == 0)
width = attachmentExtent.x(); width = attachmentExtent.x();
if (height == 0) if (height == 0)
@@ -1076,12 +1097,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
attachmentViews.emplace_back(swapchainViews[swapchainImageIndex]); attachmentViews.emplace_back(swapchainViews[swapchainImageIndex]);
} else { } else {
auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture); auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
MOBILEGL_ASSERT(textureResource, if (textureResource == nullptr) {
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at color attachment %d", i); // SyncTextureResource legitimately declines - an unsupported format,
// sample count or image-flag combination, or a vkCreateImage the driver
// refused. There is no image to attach, so there is no render pass.
MGLOG_E_ONCE("GetOrCreateRenderPass: textureId=%d could not be backed for color "
"attachment %u on FBO %u; declining the render pass",
texture->GetExternalIndex(), i, fbo.GetExternalIndex());
return nullptr;
}
textureResources.emplace_back(textureResource); textureResources.emplace_back(textureResource);
desc.format = ResolveSrgbAttachmentWriteFormat( desc.format = ResolveSrgbAttachmentWriteFormat(
textureResource->format, textureResource->format,
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)); MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb));
attachmentSampleCount = textureResource->sampleCount; attachmentSampleCount = textureResource->sampleCount;
trackedColorLayout = textureResource->layout; trackedColorLayout = textureResource->layout;
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo { trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
@@ -1098,14 +1126,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
attachmentViews.emplace_back( attachmentViews.emplace_back(
m_textureManager.GetOrCreateAttachmentViewAtMipLevel( m_textureManager.GetOrCreateAttachmentViewAtMipLevel(
*texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType)); *texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType));
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE, if (attachmentViews.back() == VK_NULL_HANDLE) {
"GetOrCreateRenderPass: GetOrCreateAttachmentView failed at color attachment %d", i); MGLOG_E_ONCE("GetOrCreateRenderPass: no attachment view for textureId=%d mip=%u layers "
"[%u, %u) viewType=%d at color attachment %u on FBO %u; declining the "
"render pass",
texture->GetExternalIndex(), attachmentMipLevel, baseArrayLayer,
baseArrayLayer + layerCount, static_cast<Int>(attachmentViewType), i,
fbo.GetExternalIndex());
return nullptr;
}
} }
desc.samples = attachmentSampleCount; desc.samples = attachmentSampleCount;
adoptRenderPassSampleCount(attachmentSampleCount, "color", texture->GetExternalIndex()); adoptRenderPassSampleCount(attachmentSampleCount, "color", texture->GetExternalIndex());
if (!hasClear && trackedColorLayout == VK_IMAGE_LAYOUT_UNDEFINED) { if (!hasClear && trackedColorLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
MGLOG_W("GetOrCreateRenderPass: color attachment textureId=%d starts with undefined layout and no clear; " MGLOG_W_ONCE("GetOrCreateRenderPass: color attachment textureId=%d starts with undefined layout and no clear; "
"using LOAD_OP_DONT_CARE", "using LOAD_OP_DONT_CARE",
texture->GetExternalIndex()); texture->GetExternalIndex());
desc.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; desc.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
@@ -1142,7 +1177,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (a.IsTexture() && b.IsTexture()) { if (a.IsTexture() && b.IsTexture()) {
return a.GetTexture().get() == b.GetTexture().get() && return a.GetTexture().get() == b.GetTexture().get() &&
a.GetTextureUploadTarget() == b.GetTextureUploadTarget() && a.GetTextureUploadTarget() == b.GetTextureUploadTarget() &&
a.GetTextureLevel() == b.GetTextureLevel(); ToStorageMipLevel(a.GetTexture().get(), a.GetTextureLevel()) ==
ToStorageMipLevel(b.GetTexture().get(), b.GetTextureLevel());
} }
if (a.IsRenderbuffer() && b.IsRenderbuffer()) { if (a.IsRenderbuffer() && b.IsRenderbuffer()) {
return a.GetRenderbuffer().get() == b.GetRenderbuffer().get(); return a.GetRenderbuffer().get() == b.GetRenderbuffer().get();
@@ -1161,7 +1197,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) && isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) &&
!sameDepthStencilAttachmentObject(depthAtt, stencilAtt); !sameDepthStencilAttachmentObject(depthAtt, stencilAtt);
if (hasDistinctDepthAndStencilAttachments) { if (hasDistinctDepthAndStencilAttachments) {
MGLOG_E("GetOrCreateRenderPass: separate depth/stencil attachments are not supported yet; using the depth attachment and ignoring the standalone stencil attachment for framebuffer %u", MGLOG_E_ONCE("GetOrCreateRenderPass: separate depth/stencil attachments are not supported yet; using the depth attachment and ignoring the standalone stencil attachment for framebuffer %u",
fbo.GetExternalIndex()); fbo.GetExternalIndex());
} }
if (selectedDepthStencilAttachment != nullptr) { if (selectedDepthStencilAttachment != nullptr) {
@@ -1191,20 +1227,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} else if (selectedDepthStencilAttachment->IsTexture()) { } else if (selectedDepthStencilAttachment->IsTexture()) {
auto& texture = *selectedDepthStencilAttachment->GetTexture(); auto& texture = *selectedDepthStencilAttachment->GetTexture();
depthTextureResource = m_textureManager.SyncTextureAndGetDescriptor(texture); depthTextureResource = m_textureManager.SyncTextureAndGetDescriptor(texture);
MOBILEGL_ASSERT(depthTextureResource, if (depthTextureResource == nullptr) {
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at depth attachment"); MGLOG_E_ONCE("GetOrCreateRenderPass: textureId=%d could not be backed for the depth/stencil "
"attachment of FBO %u; declining the render pass",
texture.GetExternalIndex(), fbo.GetExternalIndex());
return nullptr;
}
trackedDepthLayout = depthTextureResource->layout; trackedDepthLayout = depthTextureResource->layout;
depthAttachmentDescription.format = depthTextureResource->format; depthAttachmentDescription.format = depthTextureResource->format;
depthAttachmentSampleCount = depthTextureResource->sampleCount; depthAttachmentSampleCount = depthTextureResource->sampleCount;
depthAttachmentId = static_cast<Int>(texture.GetExternalIndex()); depthAttachmentId = static_cast<Int>(texture.GetExternalIndex());
attachmentExtent = attachmentExtent = ResolveRenderPassFramebufferExtent(
ResolveRenderPassFramebufferExtent(isDefaultFbo, selectedDepthStencilAttachment->GetSize(), isDefaultFbo,
swapchainExtent); ToVulkanLevelExtent(texture.GetTarget(), selectedDepthStencilAttachment->GetSize()),
swapchainExtent);
} else { } else {
const auto& renderbuffer = selectedDepthStencilAttachment->GetRenderbuffer(); const auto& renderbuffer = selectedDepthStencilAttachment->GetRenderbuffer();
depthRenderbufferResource = GetOrCreateRenderbufferResource(renderbuffer); depthRenderbufferResource = GetOrCreateRenderbufferResource(renderbuffer);
MOBILEGL_ASSERT(depthRenderbufferResource, if (depthRenderbufferResource == nullptr) {
"GetOrCreateRenderPass: GetOrCreateRenderbufferResource failed at depth attachment"); MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u could not be backed for the depth/stencil "
"attachment of FBO %u; declining the render pass",
renderbuffer->GetExternalIndex(), fbo.GetExternalIndex());
return nullptr;
}
trackedDepthLayout = depthRenderbufferResource->layout; trackedDepthLayout = depthRenderbufferResource->layout;
depthAttachmentDescription.format = depthRenderbufferResource->format; depthAttachmentDescription.format = depthRenderbufferResource->format;
depthAttachmentSampleCount = depthRenderbufferResource->sampleCount; depthAttachmentSampleCount = depthRenderbufferResource->sampleCount;
@@ -1223,7 +1268,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
depthAttachmentDescription.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; depthAttachmentDescription.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
depthAttachmentDescription.initialLayout = loadInfo.initialLayout; depthAttachmentDescription.initialLayout = loadInfo.initialLayout;
if (trackedDepthLayout == VK_IMAGE_LAYOUT_UNDEFINED && (!clearDepth || !clearStencil)) { if (trackedDepthLayout == VK_IMAGE_LAYOUT_UNDEFINED && (!clearDepth || !clearStencil)) {
MGLOG_W("GetOrCreateRenderPass: depth/stencil attachment id=%d starts with undefined layout " MGLOG_W_ONCE("GetOrCreateRenderPass: depth/stencil attachment id=%d starts with undefined layout "
"and partial/no clear; using DONT_CARE for uncleared aspects", "and partial/no clear; using DONT_CARE for uncleared aspects",
depthAttachmentId); depthAttachmentId);
} }
@@ -1252,7 +1297,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} else if (selectedDepthStencilAttachment->IsTexture()) { } else if (selectedDepthStencilAttachment->IsTexture()) {
auto& texture = *selectedDepthStencilAttachment->GetTexture(); auto& texture = *selectedDepthStencilAttachment->GetTexture();
const Uint32 attachmentMipLevel = const Uint32 attachmentMipLevel =
static_cast<Uint32>(std::max(selectedDepthStencilAttachment->GetTextureLevel(), 0)); ToStorageMipLevel(selectedDepthStencilAttachment->GetTexture().get(),
selectedDepthStencilAttachment->GetTextureLevel());
MOBILEGL_ASSERT(depthTextureResource->layout != VK_IMAGE_LAYOUT_UNDEFINED || MOBILEGL_ASSERT(depthTextureResource->layout != VK_IMAGE_LAYOUT_UNDEFINED ||
depthAttachmentDescription.loadOp != VK_ATTACHMENT_LOAD_OP_LOAD, depthAttachmentDescription.loadOp != VK_ATTACHMENT_LOAD_OP_LOAD,
"GetOrCreateRenderPass: depth attachment textureId=%d has undefined tracked layout with LOAD_OP_LOAD", "GetOrCreateRenderPass: depth attachment textureId=%d has undefined tracked layout with LOAD_OP_LOAD",
@@ -1277,8 +1323,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
attachmentViews.emplace_back( attachmentViews.emplace_back(
m_textureManager.GetOrCreateAttachmentViewAtMipLevel( m_textureManager.GetOrCreateAttachmentViewAtMipLevel(
texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType)); texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType));
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE, if (attachmentViews.back() == VK_NULL_HANDLE) {
"GetOrCreateRenderPass: GetOrCreateAttachmentView failed at depth attachment"); MGLOG_E_ONCE("GetOrCreateRenderPass: no attachment view for textureId=%d mip=%u layers [%u, %u) "
"viewType=%d at the depth/stencil attachment of FBO %u; declining the render pass",
texture.GetExternalIndex(), attachmentMipLevel, baseArrayLayer,
baseArrayLayer + layerCount, static_cast<Int>(attachmentViewType),
fbo.GetExternalIndex());
return nullptr;
}
if (width == 0 || height == 0) { if (width == 0 || height == 0) {
width = attachmentExtent.x(); width = attachmentExtent.x();
height = attachmentExtent.y(); height = attachmentExtent.y();
@@ -1300,6 +1352,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}); });
textureResources.emplace_back(nullptr); textureResources.emplace_back(nullptr);
attachmentViews.emplace_back(depthRenderbufferResource->view); attachmentViews.emplace_back(depthRenderbufferResource->view);
if (attachmentViews.back() == VK_NULL_HANDLE) {
MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u has no usable view for the depth/stencil "
"attachment of FBO %u; declining the render pass",
renderbuffer->GetExternalIndex(), fbo.GetExternalIndex());
return nullptr;
}
if (width == 0 || height == 0) { if (width == 0 || height == 0) {
width = attachmentExtent.x(); width = attachmentExtent.x();
height = attachmentExtent.y(); height = attachmentExtent.y();
@@ -1397,8 +1455,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
renderPassCreateInfo.dependencyCount = 2; renderPassCreateInfo.dependencyCount = 2;
renderPassCreateInfo.pDependencies = subpassDependencies; renderPassCreateInfo.pDependencies = subpassDependencies;
// NOT VK_VERIFY. VkIncludes.h states the rule this function now lives by: VK_VERIFY is the
// INVARIANT check - a should-never-happen state, fatal-logged unlatched and trapped in a
// DEBUG build - and "a soft, recoverable failure must therefore NOT be routed through
// VK_VERIFY. Check the VkResult directly and report it with MGLOG_E_ONCE". A decline here
// is recoverable by construction: the caller drops the draw. Routing it through VK_VERIFY
// would have made the recovery dead code in a DEBUG build (the TRAP fires inside the macro,
// before the handle is ever examined) and, in an INFO build, printed an UNLATCHED fatal
// line on every draw for the life of the process - a decline caches nothing, so every
// later draw to the same framebuffer re-enters this path and fails again.
VkRenderPass renderPass = VK_NULL_HANDLE; VkRenderPass renderPass = VK_NULL_HANDLE;
VK_VERIFY(vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass)); const VkResult renderPassResult =
vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass);
if (renderPassResult != VK_SUCCESS || renderPass == VK_NULL_HANDLE) {
MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateRenderPass failed (%s, %d) for FBO %u; declining the "
"render pass",
VkResultToString(renderPassResult), static_cast<Int>(renderPassResult),
fbo.GetExternalIndex());
return nullptr;
}
// Framebuffer // Framebuffer
VkFramebufferCreateInfo framebufferCreateInfo; VkFramebufferCreateInfo framebufferCreateInfo;
@@ -1411,8 +1486,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
framebufferCreateInfo.width = width; framebufferCreateInfo.width = width;
framebufferCreateInfo.height = height; framebufferCreateInfo.height = height;
framebufferCreateInfo.layers = framebufferLayers; framebufferCreateInfo.layers = framebufferLayers;
// Direct VkResult check, for the same reason as vkCreateRenderPass above.
VkFramebuffer framebuffer = VK_NULL_HANDLE; VkFramebuffer framebuffer = VK_NULL_HANDLE;
VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferCreateInfo, nullptr, &framebuffer)); const VkResult framebufferResult =
vkCreateFramebuffer(m_device, &framebufferCreateInfo, nullptr, &framebuffer);
if (framebufferResult != VK_SUCCESS || framebuffer == VK_NULL_HANDLE) {
// The render pass has no entry to own it yet, so it is destroyed here rather than
// leaked - RenderPassEntry's destructor is the only other thing that would.
MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateFramebuffer failed (%s, %d) for FBO %u (%dx%d, "
"%u attachments, %u layers); declining the render pass",
VkResultToString(framebufferResult), static_cast<Int>(framebufferResult),
fbo.GetExternalIndex(), width, height,
static_cast<Uint32>(attachmentViews.size()), framebufferLayers);
vkDestroyRenderPass(m_device, renderPass, nullptr);
return nullptr;
}
IntVec2 extent = {width, height}; IntVec2 extent = {width, height};
RenderPassEntry renderPassEntry { RenderPassEntry renderPassEntry {
hash, hash,
@@ -1437,7 +1525,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
extent.y()); extent.y());
auto [insertedIt, _] = m_renderPasses.emplace(hash, Move(renderPassEntry)); auto [insertedIt, _] = m_renderPasses.emplace(hash, Move(renderPassEntry));
insertedIt->second.lastUsedFrame = m_frameCounter; insertedIt->second.lastUsedFrame = m_frameCounter;
return insertedIt->second; return &insertedIt->second;
} }
void VkRenderPassManager::OnPresent() { void VkRenderPassManager::OnPresent() {
@@ -1507,7 +1595,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ClearAttachmentPayload clearPayload{}; ClearAttachmentPayload clearPayload{};
SharedPtr<MG_State::GLState::ITextureObject> liveTexture; SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
if (pending.hasInlinePayload) { if (pending.hasInlinePayload) {
clearPayload = pending.inlinePayload; // The inline payload was snapshotted when the entry was CREATED, but the
// clear VALUE is not part of the entry's hash - a cache hit with a newer
// glClear would replay the creation-time value and drop the new one (the
// texture path below is immune because it re-reads the live payload).
// Same defense as ClearAttachmentsOnActiveRenderPass: prefer the live
// pending clear, fall back to the snapshot only when none is queued.
if (s_renderPassManager != nullptr &&
s_renderPassManager->GetPendingRenderbufferClear(pending.renderbuffer, clearPayload)) {
if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0 && pending.renderbuffer != nullptr &&
MG_Util::GetBaseInternalFormatComponentCount(pending.renderbuffer->GetInternalFormat()) ==
3) {
// RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1.
ForceOpaqueClearAlpha(clearPayload);
}
} else {
clearPayload = pending.inlinePayload;
}
} else { } else {
if (pending.key.texture == nullptr || if (pending.key.texture == nullptr ||
!s_clearManager->GetPendingClear(pending.key, clearPayload, liveTexture)) { !s_clearManager->GetPendingClear(pending.key, clearPayload, liveTexture)) {
@@ -101,6 +101,42 @@ namespace MobileGL::MG_Backend::DirectVulkan {
std::swap(layers, that.layers); std::swap(layers, that.layers);
std::swap(lastUsedFrame, that.lastUsedFrame); std::swap(lastUsedFrame, that.lastUsedFrame);
} }
// Move ASSIGNMENT, not just construction. The move constructor above and the
// destructor below each independently suppress the implicit one, which left the
// type move-constructible but not move-assignable - and therefore not swappable,
// which std::swap(pair&, pair&) requires. That was invisible while UnorderedMap
// only ever move-CONSTRUCTED an element into a fresh slot. ska::flat_hash_map
// probes robin-hood: inserting swaps the entry being placed against the one
// already sitting in the slot whenever it has travelled further from its desired
// position, so the mapped type has to be swappable or the table fails to
// instantiate at all.
//
// SWAP SEMANTICS, exactly like the move constructor: this does not release the
// destination's handles, it parks them in `that`, which destroys them when it
// dies. That is correct for the only caller - std::swap, whose temporary expires
// immediately - and it is what keeps the three-move sequence from destroying a
// live render pass. It is NOT correct for a hand-written `a = std::move(b)` where
// `a` held live handles and `b` outlives the statement: those handles would then
// survive until `b` dies. There is no such caller; add a destroy-then-steal
// assignment before writing one.
RenderPassEntry& operator=(RenderPassEntry&& that) noexcept {
if (this != &that) {
std::swap(hash, that.hash);
std::swap(renderPass, that.renderPass);
std::swap(framebuffer, that.framebuffer);
std::swap(compatibilityHash, that.compatibilityHash);
std::swap(pendingClearAttachments, that.pendingClearAttachments);
std::swap(trackedAttachmentLayouts, that.trackedAttachmentLayouts);
std::swap(attachmentCount, that.attachmentCount);
std::swap(colorAttachmentCount, that.colorAttachmentCount);
std::swap(hasDepthStencilAttachment, that.hasDepthStencilAttachment);
std::swap(sampleCount, that.sampleCount);
std::swap(extent, that.extent);
std::swap(layers, that.layers);
std::swap(lastUsedFrame, that.lastUsedFrame);
}
return *this;
}
RenderPassEntry( RenderPassEntry(
Uint64 hash, Uint64 hash,
VkRenderPass renderpass, VkRenderPass renderpass,
@@ -207,9 +243,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// draw against a depth-less active pass resolves to a new (incompatible) // draw against a depth-less active pass resolves to a new (incompatible)
// entry, which the caller's compatibility check turns into a pass split; // entry, which the caller's compatibility check turns into a pass split;
// the new pass's depth loads DONT_CARE (content was undefined all along). // the new pass's depth loads DONT_CARE (content was undefined all along).
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo, //
Uint32 swapchainImageIndex, // Returns NULLPTR when this framebuffer cannot be represented as a Vulkan render pass at
Bool drawUsesDepthStencil = true); // all - a texture the texture manager declined to back (an unsupported format or sample
// count), or an attachment view it cannot construct (a layer span the image has no room
// for, a 3D image whose format was refused 2D-array compatibility). This used to be
// unrepresentable: the function returned a reference, so the only thing the two fallible
// calls it builds on could do was trip a MOBILEGL_ASSERT - which is compiled out of every
// INFO build - and then dereference the null resource, or hand VK_NULL_HANDLE to
// vkCreateFramebuffer. That took the whole process down (51 lost CTS records over 21
// bodies, one runner restart each) where a declined draw is merely a wrong picture.
//
// EVERY caller must handle nullptr by dropping the operation, exactly as the draw path
// already drops a draw whose sampler descriptor could not be resolved
// (UniformManager::BindProgramUniformBuffers). The failure paths log MGLOG_E_ONCE
// themselves, so a caller needs no message of its own.
[[nodiscard]] RenderPassEntry* GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex,
Bool drawUsesDepthStencil = true);
void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload, void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
const MG_State::GLState::FramebufferObject& drawFbo); const MG_State::GLState::FramebufferObject& drawFbo);
void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload, void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
@@ -253,6 +304,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// or a pending clear. Portable to Vulkan 1.1 (no dynamic_rendering / imageless FB needed). // or a pending clear. Portable to Vulkan 1.1 (no dynamic_rendering / imageless FB needed).
Bool m_rpFastValid = false; Bool m_rpFastValid = false;
const MG_State::GLState::FramebufferObject* m_rpFastFbo = nullptr; const MG_State::GLState::FramebufferObject* m_rpFastFbo = nullptr;
// The FBO's never-reused lifetime id joins the raw pointer + Uint16 version:
// a deleted FBO reallocated at the same address whose fresh setup performed
// the same number of version bumps would otherwise compare equal (both count
// from 0), serving the dead framebuffer's pass to the new object.
Uint64 m_rpFastFboLifetimeId = 0;
Uint16 m_rpFastFboVersion = 0; Uint16 m_rpFastFboVersion = 0;
Uint32 m_rpFastSwapchainIndex = 0; Uint32 m_rpFastSwapchainIndex = 0;
Uint64 m_rpFastTexEpoch = 0; Uint64 m_rpFastTexEpoch = 0;
@@ -315,26 +371,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 deferredAtFrame = 0; Uint64 deferredAtFrame = 0;
}; };
// Node-based std::unordered_map, deliberately not FastSTL's open-addressing UnorderedMap: // Node-based std::unordered_map, deliberately NOT the open-addressing UnorderedMap:
// callers cache a RenderbufferResource* - or a bare &resource->layout - and then make further // callers cache a RenderbufferResource* - or a bare &resource->layout - and then make further
// calls that touch this map. BlitFramebuffer is the one that bit: it resolves the source and // calls that touch this map. BlitFramebuffer is the one that bit: it resolves the source and
// destination colour bindings (ResolveColorBlitBinding caches &rbResource->layout), then // destination colour bindings (ResolveColorBlitBinding caches &rbResource->layout), then
// materializes the source's pending clear, which looks that same resource up again. FastSTL's // materializes the source's pending clear, which looks that same resource up again. Growing
// operator[] runs its load-factor check before find_key and reallocates the whole bucket array // an open-addressed table relocates every element, so the cached pointer went on to name
// when occupancy crosses it, so even a plain lookup relocates every element; erase only // freed storage still holding the pre-clear VK_IMAGE_LAYOUT_UNDEFINED; BlitFramebuffer bailed
// tombstones and never decrements the occupancy, so the doubling keeps firing. After a // out at "source image layout is undefined", silently dropping the blit -
// relocation the cached pointer names freed storage still holding the pre-clear // renderbuffers_storage_multisample read back zero instead of the clear colour on exactly the
// VK_IMAGE_LAYOUT_UNDEFINED, and BlitFramebuffer bails out at "source image layout is // iterations that grew the table.
// undefined", silently dropping the blit - renderbuffers_storage_multisample read back zero
// instead of the clear colour on exactly the iterations that grew the table.
// //
// Reordering the materialize ahead of the resolves - the fix ReadPixels got - does not cover // Reordering the materialize ahead of the resolves - the fix ReadPixels got - does not cover
// this: the destination resolve still runs after the source pointer is taken. The depth blit, // this: the destination resolve still runs after the source pointer is taken. The depth blit,
// GetOrCreateRenderPass's depthRenderbufferResource and ReadDepthStencilPixels cache the same // GetOrCreateRenderPass's depthRenderbufferResource and ReadDepthStencilPixels cache the same
// kind of pointer, so the invariant belongs in the container rather than in a per-call-site // kind of pointer, so the invariant belongs in the container rather than in a per-call-site
// ordering rule. m_textureResources is node-based for the same reason. This buys stability // ordering rule. m_textureResources is node-based for the same reason.
// across rehash and insert only - erase still invalidates the erased element, which is safe //
// here because a renderbuffer that is an FBO attachment is held alive by that attachment. // The case for keeping this node-based got STRONGER with ska::flat_hash_map, so do not read
// the paragraph above as merely historical: ska erases by shifting the rest of the probe
// cluster backwards into the hole, so erasing one renderbuffer relocates OTHER renderbuffers'
// entries - a cached pointer can now be invalidated by a key it has nothing to do with, which
// no call-site ordering rule can defend against. (What did change: ska's operator[] returns on
// a hit before it runs its grow check, so a plain lookup of a PRESENT key no longer relocates.
// That narrows the insert hazard; it does not touch the erase one.)
std::unordered_map<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources; std::unordered_map<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears; UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases; Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
@@ -21,6 +21,156 @@ namespace MobileGL::MG_Backend::DirectVulkan {
sampler.GetWrapR() == SamplerWrapMode::ClampToBorder; sampler.GetWrapR() == SamplerWrapMode::ClampToBorder;
} }
// The numeric domain the texture is SAMPLED in. Vulkan splits VkBorderColor into a float
// family and an integer family and requires the sampler's choice to match the image view's
// format (a float border on an integer view, or the reverse, is undefined) - so the domain
// comes from the TEXTURE, while the value comes from whichever GL entry point wrote it.
enum class BorderColorDomain {
Float,
SignedInteger,
UnsignedInteger
};
BorderColorDomain ResolveBorderColorDomain(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::R8I:
case TextureInternalFormat::R16I:
case TextureInternalFormat::R32I:
case TextureInternalFormat::RG8I:
case TextureInternalFormat::RG16I:
case TextureInternalFormat::RG32I:
case TextureInternalFormat::RGB8I:
case TextureInternalFormat::RGB16I:
case TextureInternalFormat::RGB32I:
case TextureInternalFormat::RGBA8I:
case TextureInternalFormat::RGBA16I:
case TextureInternalFormat::RGBA32I:
return BorderColorDomain::SignedInteger;
case TextureInternalFormat::R8UI:
case TextureInternalFormat::R16UI:
case TextureInternalFormat::R32UI:
case TextureInternalFormat::RG8UI:
case TextureInternalFormat::RG16UI:
case TextureInternalFormat::RG32UI:
case TextureInternalFormat::RGB8UI:
case TextureInternalFormat::RGB16UI:
case TextureInternalFormat::RGB32UI:
case TextureInternalFormat::RGBA8UI:
case TextureInternalFormat::RGBA16UI:
case TextureInternalFormat::RGBA32UI:
case TextureInternalFormat::RGB10A2UI:
return BorderColorDomain::UnsignedInteger;
default:
return BorderColorDomain::Float;
}
}
Bool IsSignedNormalizedFormat(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::R8Snorm:
case TextureInternalFormat::R16Snorm:
case TextureInternalFormat::RG8Snorm:
case TextureInternalFormat::RG16Snorm:
case TextureInternalFormat::RGB8Snorm:
case TextureInternalFormat::RGB16Snorm:
case TextureInternalFormat::RGBA8Snorm:
case TextureInternalFormat::RGBA16Snorm:
return true;
default:
return false;
}
}
// GL 4.6 core 8.14.2: "The border values are clamped before they are used, according to the
// format in which texture components are stored. For signed and unsigned normalized
// fixed-point formats, border values are clamped to [-1,1] and [0,1] respectively. For
// floating-point and integer formats, border values are clamped to the representable range of
// the format." Every clause of that sentence is a real case here - the clamp is not just the
// normalized one.
//
// Only the 32-bit float formats are genuinely unclamped: every finite float is representable
// in them. Half-float has a finite maximum, and the two packed "float" formats are UNSIGNED,
// so a negative border on them must come back as 0 rather than as a negative number the
// driver delivers verbatim through VK_BORDER_COLOR_FLOAT_CUSTOM_EXT.
struct FloatBorderRange {
Bool clamped = true;
Float minValue = 0.0f;
Float maxValue = 1.0f;
};
FloatBorderRange ResolveFloatBorderRange(TextureInternalFormat format, Bool isSignedNormalized) {
switch (format) {
case TextureInternalFormat::R32F:
case TextureInternalFormat::RG32F:
case TextureInternalFormat::RGB32F:
case TextureInternalFormat::RGBA32F:
return {false, 0.0f, 0.0f};
case TextureInternalFormat::R16F:
case TextureInternalFormat::RG16F:
case TextureInternalFormat::RGB16F:
case TextureInternalFormat::RGBA16F:
return {true, -65504.0f, 65504.0f};
// Unsigned packed floats: no sign bit at all. 65024 is the largest 11-bit float; the
// 10-bit blue channel tops out lower (64512) and RGB9E5 higher (65408), but the bound
// that matters for correctness is the lower one, and a single conservative upper bound
// costs nothing a real border colour will ever notice.
case TextureInternalFormat::R11FG11FB10F:
return {true, 0.0f, 64512.0f};
case TextureInternalFormat::RGB9E5:
return {true, 0.0f, 65408.0f};
default:
return {true, isSignedNormalized ? -1.0f : 0.0f, 1.0f};
}
}
// Per-component representable range of an integer texture format, as Int64 so that the whole
// signed and unsigned 32-bit ranges are expressible in one type and the clamp can be written
// once for both domains. Alpha is carried separately because RGB10_A2UI is the one format
// whose alpha is narrower than its colour channels.
struct IntegerBorderRange {
Int64 rgbMin = 0;
Int64 rgbMax = 0;
Int64 alphaMin = 0;
Int64 alphaMax = 0;
};
IntegerBorderRange ResolveIntegerBorderRange(TextureInternalFormat format) {
const auto uniform = [](Int64 low, Int64 high) { return IntegerBorderRange{low, high, low, high}; };
switch (format) {
case TextureInternalFormat::R8I:
case TextureInternalFormat::RG8I:
case TextureInternalFormat::RGB8I:
case TextureInternalFormat::RGBA8I:
return uniform(-128, 127);
case TextureInternalFormat::R16I:
case TextureInternalFormat::RG16I:
case TextureInternalFormat::RGB16I:
case TextureInternalFormat::RGBA16I:
return uniform(-32768, 32767);
case TextureInternalFormat::R8UI:
case TextureInternalFormat::RG8UI:
case TextureInternalFormat::RGB8UI:
case TextureInternalFormat::RGBA8UI:
return uniform(0, 255);
case TextureInternalFormat::R16UI:
case TextureInternalFormat::RG16UI:
case TextureInternalFormat::RGB16UI:
case TextureInternalFormat::RGBA16UI:
return uniform(0, 65535);
case TextureInternalFormat::R32UI:
case TextureInternalFormat::RG32UI:
case TextureInternalFormat::RGB32UI:
case TextureInternalFormat::RGBA32UI:
return uniform(0, 4294967295LL);
case TextureInternalFormat::RGB10A2UI:
return {0, 1023, 0, 3};
default:
// The signed 32-bit formats, and anything unexpected: the full int32 range, i.e. a
// clamp that cannot alter a value the GL entry points could have carried.
return uniform(-2147483648LL, 2147483647LL);
}
}
Bool IsDepthTextureFormat(TextureInternalFormat format) { Bool IsDepthTextureFormat(TextureInternalFormat format) {
switch (format) { switch (format) {
case TextureInternalFormat::DepthComponent: case TextureInternalFormat::DepthComponent:
@@ -72,6 +222,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_config = initInfo.config; m_config = initInfo.config;
m_samplerAnisotropySupported = initInfo.samplerAnisotropySupported; m_samplerAnisotropySupported = initInfo.samplerAnisotropySupported;
m_maxSamplerAnisotropy = std::max(initInfo.maxSamplerAnisotropy, 1.0f); m_maxSamplerAnisotropy = std::max(initInfo.maxSamplerAnisotropy, 1.0f);
m_customBorderColorSupported = initInfo.customBorderColorSupported;
m_maxCustomBorderColorSamplers = initInfo.maxCustomBorderColorSamplers;
m_customBorderColorSamplerCount = 0;
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_config != nullptr, MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_config != nullptr,
"VkSamplerManager::Initialize failed: invalid initialization info"); "VkSamplerManager::Initialize failed: invalid initialization info");
return true; return true;
@@ -102,6 +255,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_device = VK_NULL_HANDLE; m_device = VK_NULL_HANDLE;
m_config = nullptr; m_config = nullptr;
m_frameBoundaryCounter = 0; m_frameBoundaryCounter = 0;
m_customBorderColorSupported = false;
m_maxCustomBorderColorSamplers = 0;
m_customBorderColorSamplerCount = 0;
} }
void VkSamplerManager::OnFrameBoundary() { void VkSamplerManager::OnFrameBoundary() {
@@ -123,6 +279,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (m_device != VK_NULL_HANDLE && entry.handle != VK_NULL_HANDLE) { if (m_device != VK_NULL_HANDLE && entry.handle != VK_NULL_HANDLE) {
vkDestroySampler(m_device, entry.handle, nullptr); vkDestroySampler(m_device, entry.handle, nullptr);
} }
if (entry.usesCustomBorderColor && m_customBorderColorSamplerCount > 0) {
--m_customBorderColorSamplerCount;
}
it = m_samplers.erase(it); it = m_samplers.erase(it);
} else { } else {
++it; ++it;
@@ -131,8 +290,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture, Bool forceNearestFiltering, Bool singleLevelView,
Bool forceNearestFiltering, Bool singleLevelView) const { const ResolvedBorderColor& borderColor) const {
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null"); MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion)); XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
@@ -166,8 +325,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode))); XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
const auto compareFunc = sampler.GetSamplerCompareFunc(); const auto compareFunc = sampler.GetSamplerCompareFunc();
XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc))); XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc)));
const auto borderColor = ResolveVkBorderColor(sampler, texture); // The resolved enum AND, when it is one of the *_CUSTOM_EXT values, the sixteen bytes of the
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor, sizeof(borderColor))); // colour itself: two samplers that differ only in a custom border colour carry the same enum
// and would otherwise collide onto whichever one was created first.
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor.color, sizeof(borderColor.color)));
if (borderColor.isCustom) {
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor.customValue, sizeof(borderColor.customValue)));
}
return XXH64_digest(m_hashState); return XXH64_digest(m_hashState);
} }
@@ -183,7 +347,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// allocation for a genuinely single-level image) and faults the GPU - the same failure // allocation for a genuinely single-level image) and faults the GPU - the same failure
// the default-framebuffer blit shader had to work around with an explicit-LOD sample. // the default-framebuffer blit shader had to work around with an explicit-LOD sample.
const Bool singleLevelView = viewLevelCount == 1; const Bool singleLevelView = viewLevelCount == 1;
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering, singleLevelView); // Resolved once and used for both the key and the create-info; see ResolvedBorderColor.
const ResolvedBorderColor borderColor = ResolveBorderColor(sampler, texture);
const Uint64 key = BuildSamplerKey(sampler, forceNearestFiltering, singleLevelView, borderColor);
auto it = m_samplers.find(key); auto it = m_samplers.find(key);
if (it != m_samplers.end()) { if (it != m_samplers.end()) {
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter; it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
@@ -211,9 +377,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Must match BuildSamplerKey's resolution exactly. // Must match BuildSamplerKey's resolution exactly.
samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView); samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod); samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture); samplerInfo.borderColor = borderColor.color;
samplerInfo.unnormalizedCoordinates = VK_FALSE; samplerInfo.unnormalizedCoordinates = VK_FALSE;
// VK_EXT_custom_border_color. `format` stays UNDEFINED, which is legal only because
// customBorderColorWithoutFormat was required alongside customBorderColors at device
// creation - a GL sampler object has no idea which texture it will be paired with.
VkSamplerCustomBorderColorCreateInfoEXT customBorderColorInfo{};
if (borderColor.isCustom) {
customBorderColorInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CUSTOM_BORDER_COLOR_CREATE_INFO_EXT;
customBorderColorInfo.customBorderColor = borderColor.customValue;
customBorderColorInfo.format = VK_FORMAT_UNDEFINED;
customBorderColorInfo.pNext = samplerInfo.pNext;
samplerInfo.pNext = &customBorderColorInfo;
}
VkSampler vkSampler = VK_NULL_HANDLE; VkSampler vkSampler = VK_NULL_HANDLE;
VK_VERIFY(vkCreateSampler(m_device, &samplerInfo, nullptr, &vkSampler), "vkCreateSampler(texture)"); VK_VERIFY(vkCreateSampler(m_device, &samplerInfo, nullptr, &vkSampler), "vkCreateSampler(texture)");
@@ -222,6 +400,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.externalIndex = sampler.GetExternalIndex(); entry.externalIndex = sampler.GetExternalIndex();
entry.version = sampler.GetVersion(); entry.version = sampler.GetVersion();
entry.lastUsedFrameBoundary = m_frameBoundaryCounter; entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
entry.usesCustomBorderColor = borderColor.isCustom;
if (entry.usesCustomBorderColor) {
++m_customBorderColorSamplerCount;
}
m_samplers[key] = entry; m_samplers[key] = entry;
return vkSampler; return vkSampler;
} }
@@ -281,39 +463,148 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
} }
VkBorderColor VkSamplerManager::ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler, VkSamplerManager::ResolvedBorderColor VkSamplerManager::ResolveBorderColor(
const MG_State::GLState::ITextureObject& texture) { const MG_State::GLState::SamplerObject& sampler, const MG_State::GLState::ITextureObject& texture) const {
ResolvedBorderColor resolved{};
if (!UsesBorderColor(sampler)) { if (!UsesBorderColor(sampler)) {
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK; return resolved; // FLOAT_TRANSPARENT_BLACK, never sampled
} }
// Border colour is sampler state: a bound sampler object supplies its own, and a texture // Border colour is sampler state: a bound sampler object supplies its own, and a texture
// with none reaches the very same value through the sampler object it owns. // with none reaches the very same value through the sampler object it owns.
const auto& borderColor = sampler.GetBorderColor(); const auto format = texture.GetFormat();
const Bool isDepthTexture = IsDepthTextureFormat(texture.GetFormat()); const auto domain = ResolveBorderColorDomain(format);
const Bool canUseCustom = m_customBorderColorSupported && m_maxCustomBorderColorSamplers > 0 &&
m_customBorderColorSamplerCount < m_maxCustomBorderColorSamplers;
if (isDepthTexture) { if (domain != BorderColorDomain::Float) {
// An integer image view REQUIRES an integer border colour, whatever the value is - even
// (0,0,0,1). The value itself is whichever integer form the application wrote; a float
// border on an integer texture is nonsense GL leaves undefined, so the derived integer
// representation (a plain cast) is as good an answer as any.
//
// Clamped to the format's representable range FIRST, per GL 4.6 core 8.14.2, and read
// through Int64 so the whole signed and unsigned 32-bit ranges are expressible at once.
//
// Which representation to start from is the TEXTURE's domain, not the entry-point form
// the application used. GL 4.6 core 8.10 stores an "I"-form border colour unmodified with
// an integer internal data type and does not define a sign conversion between the two
// integer forms, so the stored bits are reinterpreted in the sampled format's own
// signedness. Measured, not assumed: a border of -1 written with glTexParameterIiv
// against a GL_R8UI texture samples as 255 on the ES driver, i.e. as 0xFFFFFFFF clamped
// to the format's maximum - see the IntegerBorderColorScenario case that pins it. Picking
// the representation by the FORM instead would answer 0 here, which is a defensible
// reading of the same spec text but puts DirectVulkan at odds with DirectGLES - and
// DirectGLES cannot deviate, it forwards the value to the driver verbatim. Cross-backend
// agreement decides it.
const auto range = ResolveIntegerBorderRange(format);
const auto& borderColorI = sampler.GetBorderColorI();
const auto& borderColorUI = sampler.GetBorderColorUI();
const Bool startFromUnsigned = domain == BorderColorDomain::UnsignedInteger;
Int64 clamped[4];
for (SizeT channel = 0; channel < 4; ++channel) {
const Int64 raw = startFromUnsigned ? static_cast<Int64>(borderColorUI[channel])
: static_cast<Int64>(borderColorI[channel]);
const Int64 low = channel == 3 ? range.alphaMin : range.rgbMin;
const Int64 high = channel == 3 ? range.alphaMax : range.rgbMax;
clamped[channel] = std::clamp(raw, low, high);
}
// Matched against the CLAMPED value, so a border the format cannot hold still lands on
// the palette entry it clamps to rather than missing every one of them.
const Bool allZeroRgb = clamped[0] == 0 && clamped[1] == 0 && clamped[2] == 0;
if (allZeroRgb && clamped[3] == 0) {
resolved.color = VK_BORDER_COLOR_INT_TRANSPARENT_BLACK;
return resolved;
}
if (allZeroRgb && clamped[3] == 1) {
resolved.color = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
return resolved;
}
if (clamped[0] == 1 && clamped[1] == 1 && clamped[2] == 1 && clamped[3] == 1) {
resolved.color = VK_BORDER_COLOR_INT_OPAQUE_WHITE;
return resolved;
}
if (canUseCustom) {
resolved.color = VK_BORDER_COLOR_INT_CUSTOM_EXT;
resolved.isCustom = true;
for (SizeT channel = 0; channel < 4; ++channel) {
if (domain == BorderColorDomain::UnsignedInteger) {
resolved.customValue.uint32[channel] = static_cast<Uint32>(clamped[channel]);
} else {
resolved.customValue.int32[channel] = static_cast<Int32>(clamped[channel]);
}
}
return resolved;
}
// No custom colour available: pick the nearest of the three integer palette entries
// rather than always answering transparent black, which is what turned an integer border
// of (-1,-1,-1,-1) into 0 and broke the CTS's clamped-texel detection outright.
const Bool opaque = clamped[3] != 0;
const Bool bright = clamped[0] != 0 || clamped[1] != 0 || clamped[2] != 0;
resolved.color = !opaque ? VK_BORDER_COLOR_INT_TRANSPARENT_BLACK
: (bright ? VK_BORDER_COLOR_INT_OPAQUE_WHITE : VK_BORDER_COLOR_INT_OPAQUE_BLACK);
return resolved;
}
// Float domain. GL 4.6 core 8.14.2/8.23: the border colour is interpreted in the texture's
// format, so it is clamped to that format's representable range first. Without the clamp the
// CTS's border of (255,255,255,255) on a GL_RGBA8 texture matched none of the palette entries
// and fell through to transparent black - every border texel sampled 0 where the test wanted
// 255. The range is per format class, not just the normalized [0,1] / [-1,1] pair: only the
// 32-bit float formats are unclamped.
FloatVec4 borderColor = sampler.GetBorderColor();
if (const auto range = ResolveFloatBorderRange(format, IsSignedNormalizedFormat(format)); range.clamped) {
borderColor = FloatVec4(std::clamp(borderColor.x(), range.minValue, range.maxValue),
std::clamp(borderColor.y(), range.minValue, range.maxValue),
std::clamp(borderColor.z(), range.minValue, range.maxValue),
std::clamp(borderColor.w(), range.minValue, range.maxValue));
}
// A depth texture samples one component, so only x decides - and its alpha reads as 1.
if (IsDepthTextureFormat(format)) {
if (NearlyEqual(borderColor.x(), 1.0f)) { if (NearlyEqual(borderColor.x(), 1.0f)) {
return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
return resolved;
} }
if (NearlyEqual(borderColor.x(), 0.0f)) { if (NearlyEqual(borderColor.x(), 0.0f)) {
return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK; resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
return resolved;
} }
} }
const Bool rgbZero = NearlyEqual(borderColor.x(), 0.0f) && NearlyEqual(borderColor.y(), 0.0f) && const Bool rgbZero = NearlyEqual(borderColor.x(), 0.0f) && NearlyEqual(borderColor.y(), 0.0f) &&
NearlyEqual(borderColor.z(), 0.0f); NearlyEqual(borderColor.z(), 0.0f);
if (rgbZero && NearlyEqual(borderColor.w(), 0.0f)) { if (rgbZero && NearlyEqual(borderColor.w(), 0.0f)) {
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK; resolved.color = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
return resolved;
} }
if (rgbZero && NearlyEqual(borderColor.w(), 1.0f)) { if (rgbZero && NearlyEqual(borderColor.w(), 1.0f)) {
return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK; resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
return resolved;
} }
if (NearlyEqual(borderColor.x(), 1.0f) && NearlyEqual(borderColor.y(), 1.0f) && if (NearlyEqual(borderColor.x(), 1.0f) && NearlyEqual(borderColor.y(), 1.0f) &&
NearlyEqual(borderColor.z(), 1.0f) && NearlyEqual(borderColor.w(), 1.0f)) { NearlyEqual(borderColor.z(), 1.0f) && NearlyEqual(borderColor.w(), 1.0f)) {
return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
return resolved;
} }
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK; if (canUseCustom) {
resolved.color = VK_BORDER_COLOR_FLOAT_CUSTOM_EXT;
resolved.isCustom = true;
resolved.customValue.float32[0] = borderColor.x();
resolved.customValue.float32[1] = borderColor.y();
resolved.customValue.float32[2] = borderColor.z();
resolved.customValue.float32[3] = borderColor.w();
return resolved;
}
// Nearest of the three float palette entries. Transparent black stays the answer for a
// transparent border, which is what the old unconditional fallback got right by accident.
const Bool opaque = borderColor.w() >= 0.5f;
const Bool bright = (borderColor.x() + borderColor.y() + borderColor.z()) >= 1.5f;
resolved.color = !opaque ? VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK
: (bright ? VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE : VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK);
return resolved;
} }
} // namespace MobileGL::MG_Backend::DirectVulkan } // namespace MobileGL::MG_Backend::DirectVulkan
@@ -28,6 +28,13 @@ public:
Bool samplerAnisotropySupported = false; Bool samplerAnisotropySupported = false;
// VkPhysicalDeviceLimits::maxSamplerAnisotropy. // VkPhysicalDeviceLimits::maxSamplerAnisotropy.
Float maxSamplerAnisotropy = 1.0f; Float maxSamplerAnisotropy = 1.0f;
// VK_EXT_custom_border_color was enabled with BOTH customBorderColors and
// customBorderColorWithoutFormat; see VulkanRenderer::m_customBorderColorFeatureEnabled.
Bool customBorderColorSupported = false;
// VkPhysicalDeviceCustomBorderColorPropertiesEXT::maxCustomBorderColorSamplers. A hard device
// limit on how many LIVE samplers may carry a custom border colour, so the cache counts them
// and falls back to the snapped predefined value once it is reached.
Uint32 maxCustomBorderColorSamplers = 0;
}; };
Bool Initialize(const InitInfo& initInfo); Bool Initialize(const InitInfo& initInfo);
@@ -52,6 +59,21 @@ public:
// boundaries. // boundaries.
void OnFrameBoundary(); void OnFrameBoundary();
// What GL_TEXTURE_BORDER_COLOR resolves to for one (sampler, texture) pair. `color` is always a
// legal VkBorderColor; when `isCustom` it is one of the *_CUSTOM_EXT values and `customValue`
// carries the actual components in a VkSamplerCustomBorderColorCreateInfoEXT.
//
// Resolved ONCE per GetOrCreateSampler call and threaded into both the cache key and the
// create-info, so the two cannot disagree - the same discipline the resolved anisotropy needs,
// and here it also makes the maxCustomBorderColorSamplers fallback deterministic: whether a
// custom colour was affordable is decided before the key is built, not twice with a budget
// change in between.
struct ResolvedBorderColor {
VkBorderColor color = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
VkClearColorValue customValue{};
Bool isCustom = false;
};
private: private:
struct SamplerCacheEntry { struct SamplerCacheEntry {
VkSampler handle = VK_NULL_HANDLE; VkSampler handle = VK_NULL_HANDLE;
@@ -60,17 +82,18 @@ private:
// Frame boundary of the last cache hit; entries idle past the // Frame boundary of the last cache hit; entries idle past the
// OnFrameBoundary retirement age have their VkSampler destroyed. // OnFrameBoundary retirement age have their VkSampler destroyed.
Uint64 lastUsedFrameBoundary = 0; Uint64 lastUsedFrameBoundary = 0;
// Counted against maxCustomBorderColorSamplers for as long as this entry lives.
Bool usesCustomBorderColor = false;
}; };
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Bool forceNearestFiltering,
const MG_State::GLState::ITextureObject& texture, Bool singleLevelView, const ResolvedBorderColor& borderColor) const;
Bool forceNearestFiltering, Bool singleLevelView) const;
static VkFilter ToVkFilter(SamplerFilterMode mode); static VkFilter ToVkFilter(SamplerFilterMode mode);
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode); static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode); static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
static VkCompareOp ToVkCompareOp(SamplerCompareFunc func); static VkCompareOp ToVkCompareOp(SamplerCompareFunc func);
static VkBorderColor ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler, ResolvedBorderColor ResolveBorderColor(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture); const MG_State::GLState::ITextureObject& texture) const;
// The anisotropy Vulkan will actually apply: 1.0 (i.e. disabled) unless the feature is on and // The anisotropy Vulkan will actually apply: 1.0 (i.e. disabled) unless the feature is on and
// the sampler filters linearly both ways, otherwise the GL request clamped to the device limit. // the sampler filters linearly both ways, otherwise the GL request clamped to the device limit.
// GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly // GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly
@@ -82,6 +105,12 @@ private:
const VulkanRendererConfig* m_config = nullptr; const VulkanRendererConfig* m_config = nullptr;
Bool m_samplerAnisotropySupported = false; Bool m_samplerAnisotropySupported = false;
Float m_maxSamplerAnisotropy = 1.0f; Float m_maxSamplerAnisotropy = 1.0f;
Bool m_customBorderColorSupported = false;
Uint32 m_maxCustomBorderColorSamplers = 0;
// Live cache entries carrying a custom border colour. Kept in step with the entries themselves
// in exactly the three places one can appear or disappear: creation, the OnFrameBoundary sweep,
// and Shutdown.
Uint32 m_customBorderColorSamplerCount = 0;
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers; UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging. // Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameBoundaryCounter = 0; Uint64 m_frameBoundaryCounter = 0;
File diff suppressed because it is too large Load Diff
@@ -10,8 +10,10 @@
#include "../VkIncludes.h" #include "../VkIncludes.h"
#include <Includes.h> #include <Includes.h>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include <MG_State/GLState/TextureState/TextureObject.h> #include <MG_State/GLState/TextureState/TextureObject.h>
#include <vk_mem_alloc.h> #include <vk_mem_alloc.h>
#include <algorithm>
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
@@ -22,6 +24,102 @@ class ITextureObject;
namespace MobileGL::MG_Backend::DirectVulkan { namespace MobileGL::MG_Backend::DirectVulkan {
enum class SamplerNumericDomain : Uint8; enum class SamplerNumericDomain : Uint8;
// What VkFormat a GL internal format is BACKED with, and how a shadow upload has to be reshaped to
// fit it. This is not the same question as "is there an exact VkFormat for this GL format", which is
// what ConvertTextureInternalFormatToVkEnum answers: several GL formats have no Vulkan twin at all
// (RGBA2, RGBA12) and several three-channel ones are deliberately widened to their four-channel twin
// because Vulkan devices rarely support the 3-channel layouts.
//
// SHARED, and it must stay the only answer to that question. A renderbuffer and a texture of the
// same GL format have to resolve to the SAME VkFormat or every blit, resolve and glCopyImageSubData
// between them crosses a size-incompatible pair, which vkCmdCopyImage leaves undefined
// (VUID-vkCmdCopyImage-srcImage-01548). The renderbuffer path used to carry a hand-maintained second
// copy of this table that was missing four rows - RGBA2, RGBA4, RGB5A1 and RGBA12 - so those four
// renderbuffer formats either got no image at all or a 16-bit-packed one facing a 32-bit texture.
struct TextureFormatInfo {
VkFormat format = VK_FORMAT_UNDEFINED;
// The GL format has three channels and is carried in a four-channel image; a shadow upload has
// to be expanded, inserting `alphaBytes` after every `componentByteCount * 3` source bytes.
Bool expandRgbToRgba = false;
Uint32 componentByteCount = 0;
Array<Uint8, 4> alphaBytes = {0, 0, 0, 0};
};
// Callers that only need the backing VkFormat (a renderbuffer has no shadow upload to reshape) take
// `.format` and ignore the rest.
TextureFormatInfo ResolveTextureFormatInfo(TextureInternalFormat format);
// A GL 1D-ARRAY level keeps its LAYER COUNT in the state-side HEIGHT: that is what
// glTexImage2D(GL_TEXTURE_1D_ARRAY, width, layers) means, and the frontend records the level
// as {width, layers, 1} (see GL_Texture.cpp's AllocateStorage and the completeness walk in
// TextureObject.cpp, which shrinks only x down the chain). Vulkan packs it the other way: a
// 1D array is a VK_IMAGE_TYPE_1D image whose extent.height MUST be 1 and whose layers live in
// arrayLayers - i.e. in the slot this backend reads out of z. So every place that turns a GL
// level size into Vulkan image geometry has to move the count across first, and every GL-space
// sub-box that rides along with it has to move its y the same way. DirectGLES performs the
// identical remap onto the ES 2D array it maps 1D arrays to (GetBackendUploadSize).
//
// Applied to nothing else: a 2D array, a cube array and a 3D texture all already carry their
// depth/layer count in z, which is where the Vulkan side expects it.
inline IntVec3 ToVulkanLevelExtent(TextureTarget stateTarget, const IntVec3& glTexelSize) {
if (stateTarget == TextureTarget::Texture1DArray) {
return {glTexelSize.x(), 1, glTexelSize.y()};
}
return glTexelSize;
}
// How many Vulkan array layers (or, for a 3D image, z slices) a GL framebuffer attachment spans.
//
// THE ONE COPY, deliberately. This used to exist twice - privately in VkRenderPassManager.cpp and
// again in VkClearManager.cpp - and the two are not independent: the render pass builds the
// attachment view and VkFramebufferCreateInfo::layers from one, while the CLEAR key built from the
// other is written verbatim into VkImageSubresourceRange::layerCount when a queued glClear is
// materialised outside a render pass (MaterializePendingClearForTexture). They are two consumers
// of the same GL clear, so any disagreement means the same glClear produces two different pictures
// depending only on which path happens to consume it first - and the materialise path then POPS
// the entry, so the other one never runs. Fixing one copy and leaving the other is exactly how
// that split gets introduced; keep them the same function.
//
// Two shapes make this more than `size.z()`:
// * GL_TEXTURE_1D_ARRAY keeps its layer count in the state-side HEIGHT (see ToVulkanLevelExtent
// just above), so z reads 1 and every layer above the first was silently dropped.
// * GL_TEXTURE_CUBE_MAP is attached layered as its REPRESENTATIVE upload target, the +X face
// (ResolveRepresentableFramebufferTextureUploadTarget), and one face's level size has z = 1 -
// but a layered cube attachment names all six faces (GL 4.6 core 9.2.8), which are the image's
// six array layers. A cube ARRAY needs no such arm: its representative target carries 6n in z.
inline Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (!attachment.IsLayered()) {
return 1u;
}
const auto& texture = attachment.GetTexture();
const TextureTarget target = texture != nullptr ? texture->GetTarget() : TextureTarget::Unknown;
if (target == TextureTarget::TextureCubeMap) {
return 6u;
}
return static_cast<Uint32>(std::max(ToVulkanLevelExtent(target, attachment.GetSize()).z(), 1));
}
// A GL framebuffer attachment's level/layer, and a GL image unit's, are relative to the texture
// the application NAMED. When that texture was created by glTextureView (ARB_texture_view) they
// are relative to the VIEW, and have to be shifted into the storage image's numbering before they
// can index a Vulkan subresource - DirectVulkan gives a view no image of its own, it shares the
// storage texture's (VkTextureManager::StorageTextureOf).
//
// Apply EXACTLY ONCE, at the boundary where a GL level/layer becomes a subresource index. Every
// GetOrCreate*View entry point below expects values that have already been through here, and so
// does everything that reads or copies an attachment directly. Both are identity on a plain
// texture (TEXTURE_VIEW_MIN_LEVEL / MIN_LAYER are 0 there), so the conversion is unconditional
// and there is no second, view-only code path to keep in step.
inline Uint32 ToStorageMipLevel(const MG_State::GLState::ITextureObject* texture, Int glLevel) {
const Uint32 level = static_cast<Uint32>(glLevel > 0 ? glLevel : 0);
return texture != nullptr ? level + static_cast<Uint32>(texture->GetViewMinLevel()) : level;
}
inline Uint32 ToStorageArrayLayer(const MG_State::GLState::ITextureObject* texture, Int glLayer) {
const Uint32 layer = static_cast<Uint32>(glLayer > 0 ? glLayer : 0);
return texture != nullptr ? layer + static_cast<Uint32>(texture->GetViewMinLayer()) : layer;
}
class VkTextureManager { class VkTextureManager {
public: public:
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass // Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
@@ -120,17 +218,35 @@ public:
} }
}; };
// Layer range and aspect join the key because a GL texture view (ARB_texture_view) can
// differ from its storage on either: the Better Clouds shape samples ONE D24S8 image
// through two GL names in one draw, the parent with the stencil aspect and the view with
// the depth aspect, and a layer-sliced view of an array texture names a sub-range of the
// same image. Without these two fields those views would alias each other in the cache.
struct SampledImageViewKey { struct SampledImageViewKey {
Uint32 baseMipLevel = 0; Uint32 baseMipLevel = 0;
Uint32 levelCount = 1; Uint32 levelCount = 1;
Uint32 baseArrayLayer = 0;
Uint32 layerCount = 1;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D; VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkFormat format = VK_FORMAT_UNDEFINED; VkFormat format = VK_FORMAT_UNDEFINED;
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_COLOR_BIT;
// GL_TEXTURE_SWIZZLE_* is per-texture state, so two views over one storage with the
// same window but different swizzles are different views. Baked into the key because
// a GL texture view's ONLY sampled view lives in this cache: unlike the storage
// texture's own sampledView, which SyncTextureViews rebuilds whenever the params
// version moves, nothing else would ever notice a swizzle change on a view.
Uint32 componentSwizzle = 0;
Bool operator==(const SampledImageViewKey& other) const { Bool operator==(const SampledImageViewKey& other) const {
return baseMipLevel == other.baseMipLevel && return baseMipLevel == other.baseMipLevel &&
levelCount == other.levelCount && levelCount == other.levelCount &&
baseArrayLayer == other.baseArrayLayer &&
layerCount == other.layerCount &&
viewType == other.viewType && viewType == other.viewType &&
format == other.format; format == other.format &&
aspect == other.aspect &&
componentSwizzle == other.componentSwizzle;
} }
}; };
@@ -138,10 +254,15 @@ public:
SizeT operator()(const SampledImageViewKey& key) const { SizeT operator()(const SampledImageViewKey& key) const {
SizeT hash = std::hash<Uint32>{}(key.baseMipLevel); SizeT hash = std::hash<Uint32>{}(key.baseMipLevel);
hash ^= std::hash<Uint32>{}(key.levelCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2); hash ^= std::hash<Uint32>{}(key.levelCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(key.baseArrayLayer) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) + hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
0x9e3779b9u + (hash << 6) + (hash >> 2); 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) + hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.format)) +
0x9e3779b9u + (hash << 6) + (hash >> 2); 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.aspect)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(key.componentSwizzle) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash; return hash;
} }
}; };
@@ -206,6 +327,12 @@ public:
// as defense-in-depth: any path that grows the level set (which resizes the sampled view) // as defense-in-depth: any path that grows the level set (which resizes the sampled view)
// busts the skip even if it failed to bump the content version. // busts the skip even if it failed to bump the content version.
Uint32 syncedMipLevelCount = 0; Uint32 syncedMipLevelCount = 0;
// Snapshot of ITextureObject::GetShapeVersion() at the last successful sync. The content
// version alone does NOT cover a re-specification: glTexImage2D(..., nullptr) on an
// already-defined level changes its size or format and dirties no texel, so it moves the
// shape version and nothing else. Without this in the early-out key the image, its views
// and therefore imageSize() all keep answering with the texture's PREVIOUS shape.
Uint64 syncedShapeVersion = 0;
TextureResource() = default; TextureResource() = default;
TextureResource(const TextureResource&) = delete; TextureResource(const TextureResource&) = delete;
@@ -237,6 +364,7 @@ public:
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration); std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
std::swap(this->syncedContentVersion, that.syncedContentVersion); std::swap(this->syncedContentVersion, that.syncedContentVersion);
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount); std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
std::swap(this->syncedShapeVersion, that.syncedShapeVersion);
} }
void Reset() { void Reset() {
@@ -300,6 +428,7 @@ public:
syncedTextureParamsVersion = 0; syncedTextureParamsVersion = 0;
syncedContentVersion = 0; syncedContentVersion = 0;
syncedMipLevelCount = 0; syncedMipLevelCount = 0;
syncedShapeVersion = 0;
} }
~TextureResource() { ~TextureResource() {
@@ -310,6 +439,11 @@ public:
static inline VmaAllocator s_allocator = VK_NULL_HANDLE; static inline VmaAllocator s_allocator = VK_NULL_HANDLE;
}; };
struct SampledTextureSnapshot {
VkImageView imageView = VK_NULL_HANDLE;
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
};
Bool Initialize(const InitInfo& initInfo); Bool Initialize(const InitInfo& initInfo);
void Shutdown(); void Shutdown();
void BeginFrame(Uint32 frameIndex); void BeginFrame(Uint32 frameIndex);
@@ -326,6 +460,58 @@ public:
// present-less frame-boundary drain. // present-less frame-boundary drain.
void CollectAllDeferredReleases(); void CollectAllDeferredReleases();
// ---- GL texture views (ARB_texture_view / GL 4.6 core 8.18) ----
// The GL texture whose STORAGE backs the given one: itself, or - for a texture created by
// glTextureView - the texture it views. Every image-scoped question (which VkImage, its
// LAYOUT, its uploads, its extent, its usage) must be asked of this object, because a view
// has none of its own; only the VkImageViews differ per GL texture object. Sharing one
// TextureResource is not an optimisation, it is the only correct arrangement: layout is a
// property of the image, and VulkanRenderer caches raw pointers straight to the resource's
// layout field, so a second resource aliasing the same image would desynchronise the moment
// either of them transitioned it.
static MG_State::GLState::ITextureObject& StorageTextureOf(MG_State::GLState::ITextureObject& texture);
// The window a GL texture object opens onto its storage image. For a plain texture this is
// the resource's own full extent; for a view it is the sub-range, format and aspect
// glTextureView gave it. Views built from a non-default window must live in the KEYED caches
// (attachmentViews / alternateSampledViews), never in the per-mip vectors, which belong to
// the storage texture's own defaults.
struct TextureViewWindow {
Uint32 baseMipLevel = 0;
Uint32 levelCount = 1;
Uint32 baseArrayLayer = 0;
Uint32 layerCount = 1;
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkImageAspectFlags sampledAspect = VK_IMAGE_ASPECT_COLOR_BIT;
VkComponentMapping components{VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B,
VK_COMPONENT_SWIZZLE_A};
Bool isTextureView = false;
};
// The four component swizzles packed into one value, for the sampled-view cache key.
static Uint32 PackComponentSwizzle(const VkComponentMapping& components) {
return (static_cast<Uint32>(components.r) & 0xFFu) | ((static_cast<Uint32>(components.g) & 0xFFu) << 8) |
((static_cast<Uint32>(components.b) & 0xFFu) << 16) |
((static_cast<Uint32>(components.a) & 0xFFu) << 24);
}
TextureViewWindow ResolveTextureViewWindow(MG_State::GLState::ITextureObject& texture,
const TextureResource& resource) const;
// Records what a GL texture view needs of the image it views, so the next sync of the
// STORAGE texture creates (or recreates and copies forward) an image the view can be built
// over. See m_viewRequestedImageFlags for why this is lazy rather than unconditional.
void NoteTextureViewImageRequirements(MG_State::GLState::ITextureObject& viewTexture,
MG_State::GLState::ITextureObject& storageTexture);
VkImageCreateFlags GetViewRequestedImageFlags(const MG_State::GLState::ITextureObject& storageTexture) const;
// Appends every format a GL texture view reinterprets this storage as, for the narrowed
// VkImageFormatListCreateInfo the image is created with.
void AppendViewRequestedFormats(const MG_State::GLState::ITextureObject& storageTexture,
Vector<VkFormat>& outFormats) const;
// Builds (and caches, keyed by the whole window) one sampled VkImageView over a storage
// image. Shared back end of every GL-texture-view sampled path.
VkImageView GetOrCreateWindowedSampledView(MG_State::GLState::ITextureObject& texture,
TextureResource& resource, const TextureViewWindow& window);
TextureResource* SyncTextureAndGetDescriptor( TextureResource* SyncTextureAndGetDescriptor(
MG_State::GLState::ITextureObject& texture); MG_State::GLState::ITextureObject& texture);
VkImageView GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel); VkImageView GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
@@ -343,6 +529,13 @@ public:
VkImageLayout newLayout); VkImageLayout newLayout);
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
// Copies the complete sampler-visible mip range into a transient sampled image. The source is
// restored to its prior layout, so image-store descriptors continue to name the original image.
// The transient ownership is tied to the current frame slot and is safe through its submission.
Bool SnapshotTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture,
SamplerNumericDomain numericDomain,
VkPipelineStageFlags consumerShaderStageMask,
SampledTextureSnapshot& outSnapshot);
// Recording-generation bookkeeping for the pre-pass command stream. The // Recording-generation bookkeeping for the pre-pass command stream. The
// generation advances every time the frame command buffer (re)begins // generation advances every time the frame command buffer (re)begins
@@ -379,17 +572,33 @@ public:
// true - a false positive merely ends the render pass, a false negative would skip a barrier. // true - a false positive merely ends the render pass, a false negative would skip a barrier.
Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const; Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const;
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect); // `depthStencilTextureMode` is the texture's GL_DEPTH_STENCIL_TEXTURE_MODE; it only decides
// anything for an image that carries both aspects. Defaulted so the call sites that have no
// texture in hand keep the depth-aspect answer they have always given.
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect,
GLenum depthStencilTextureMode = GL_DEPTH_COMPONENT);
static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain); static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain);
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat); static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat); static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
// Moves `image` to `newLayout` and writes the new layout back through `trackedLayout`.
//
// The barrier covers EVERY array layer of the image, and there is deliberately no layer
// parameter to say otherwise: layout here is tracked per IMAGE (one `TextureResource::layout`,
// or one caller-owned variable), so a barrier narrower than the image would leave the layers it
// skipped in the old layout while the tracker claims they moved. Every transfer against a
// framebuffer attachment above layer 0 - glReadPixels, glBlitFramebuffer, glCopyTexSubImage,
// glCopyImageSubData - then ran its copy on a layer no barrier had transitioned.
//
// The mip range IS a parameter, because mip levels really are transitioned piecewise (see
// UpdateTrackedImageLayoutAfterAttachmentWrite and the mipmap generation loops): those callers
// move the complement of the level they wrote so the whole image converges on one layout again.
// Nothing does, or can, do that per layer.
static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout, static Bool TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout,
VkImageLayout newLayout, VkPipelineStageFlags srcStageMask, VkImageLayout newLayout, VkPipelineStageFlags srcStageMask,
VkPipelineStageFlags dstStageMask, VkAccessFlags srcAccessMask, VkPipelineStageFlags dstStageMask, VkAccessFlags srcAccessMask,
VkAccessFlags dstAccessMask, VkImageAspectFlags aspectMask, VkAccessFlags dstAccessMask, VkImageAspectFlags aspectMask,
Uint32 baseMipLevel = 0, Uint32 levelCount = 1, Uint32 baseMipLevel = 0, Uint32 levelCount = 1);
Uint32 layerCount = 1);
SizeT CollectGarbage(); SizeT CollectGarbage();
@@ -517,6 +726,19 @@ private:
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources; std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture). // Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
std::unordered_set<TextureIdentity, TextureIdentityHash> m_storageImageTextures; std::unordered_set<TextureIdentity, TextureIdentityHash> m_storageImageTextures;
// Extra VkImageCreateFlags a GL texture view needs on the storage image it views, keyed by
// the STORAGE texture's identity. Requested lazily, exactly like STORAGE usage above and for
// the same reason: VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT costs bandwidth compression on tilers
// (it is what VK_KHR_image_format_list exists to claw back), so setting it on every
// immutable-storage texture would tax every glTexStorage2D render target in a game for a
// feature almost none of them use. A SAME-format view - which is the common case, and the
// Better Clouds case - needs no flag at all and therefore costs nothing.
std::unordered_map<TextureIdentity, VkImageCreateFlags, TextureIdentityHash> m_viewRequestedImageFlags;
// Every VkFormat a GL texture view has asked to reinterpret this storage as. The narrowed
// VkImageFormatListCreateInfo the image is created with must name them: the list is a promise
// that NO other format will ever be viewed, and building a view outside it is
// VUID-VkImageViewCreateInfo-pNext-01585. Keyed, like the flags above, by the STORAGE texture.
std::unordered_map<TextureIdentity, std::unordered_set<VkFormat>, TextureIdentityHash> m_viewRequestedFormats;
// Supported multisample counts per format, so repeat texture syncs do not // Supported multisample counts per format, so repeat texture syncs do not
// re-query vkGetPhysicalDeviceImageFormatProperties. // re-query vkGetPhysicalDeviceImageFormatProperties.
std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat; std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat;
@@ -15,7 +15,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(initInfo.device != VK_NULL_HANDLE, "VkTimerQueryManager::Initialize requires valid VkDevice"); MOBILEGL_ASSERT(initInfo.device != VK_NULL_HANDLE, "VkTimerQueryManager::Initialize requires valid VkDevice");
MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkTimerQueryManager::Initialize requires non-zero frame count"); MOBILEGL_ASSERT(initInfo.frameCount > 0, "VkTimerQueryManager::Initialize requires non-zero frame count");
if (initInfo.timestampValidBits == 0 || initInfo.timestampPeriodNs <= 0.0f || initInfo.slotsPerPool == 0) { if (initInfo.timestampValidBits == 0 || initInfo.timestampPeriodNs <= 0.0f || initInfo.slotsPerPool == 0) {
MGLOG_W("VkTimerQueryManager: timestamps unsupported (validBits=%u, period=%f, slots=%u)", MGLOG_W_ONCE("VkTimerQueryManager: timestamps unsupported (validBits=%u, period=%f, slots=%u)",
initInfo.timestampValidBits, initInfo.timestampPeriodNs, initInfo.slotsPerPool); initInfo.timestampValidBits, initInfo.timestampPeriodNs, initInfo.slotsPerPool);
return false; return false;
} }
@@ -35,7 +35,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (auto& poolState : m_pools) { for (auto& poolState : m_pools) {
const VkResult result = vkCreateQueryPool(m_device, &poolInfo, nullptr, &poolState.pool); const VkResult result = vkCreateQueryPool(m_device, &poolInfo, nullptr, &poolState.pool);
if (result != VK_SUCCESS) { if (result != VK_SUCCESS) {
MGLOG_E("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result)); MGLOG_E_ONCE("VkTimerQueryManager: vkCreateQueryPool failed with %s", VkResultToString(result));
Shutdown(); Shutdown();
return false; return false;
} }
@@ -90,7 +90,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& poolState = m_pools[frameIndex]; auto& poolState = m_pools[frameIndex];
if (poolState.cursor >= m_slotsPerPool) { if (poolState.cursor >= m_slotsPerPool) {
if (!poolState.exhaustionWarned) { if (!poolState.exhaustionWarned) {
MGLOG_W("VkTimerQueryManager: frame %u timestamp pool exhausted (%u slots); further timer queries " MGLOG_W_ONCE("VkTimerQueryManager: frame %u timestamp pool exhausted (%u slots); further timer queries "
"this frame fall back to the frontend path", "this frame fall back to the frontend path",
frameIndex, m_slotsPerPool); frameIndex, m_slotsPerPool);
poolState.exhaustionWarned = true; poolState.exhaustionWarned = true;
@@ -120,7 +120,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_device, m_pools[record.poolIndex].pool, record.slot, 1, sizeof(resultWithAvailability), m_device, m_pools[record.poolIndex].pool, record.slot, 1, sizeof(resultWithAvailability),
resultWithAvailability, sizeof(Uint64), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT); resultWithAvailability, sizeof(Uint64), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
if (result != VK_SUCCESS && result != VK_NOT_READY) { if (result != VK_SUCCESS && result != VK_NOT_READY) {
MGLOG_E("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result)); MGLOG_E_ONCE("VkTimerQueryManager: vkGetQueryPoolResults failed with %s", VkResultToString(result));
return false; return false;
} }
if (resultWithAvailability[1] == 0) { if (resultWithAvailability[1] == 0) {
File diff suppressed because it is too large Load Diff
@@ -9,6 +9,7 @@
#pragma once #pragma once
#include "Config.h" #include "Config.h"
#include "FrameContext.h" #include "FrameContext.h"
#include "MagmaPipeArms.h"
#include "PipelineFactory.h" #include "PipelineFactory.h"
#include "ProgramFactory.h" #include "ProgramFactory.h"
#include "SwapchainObject.h" #include "SwapchainObject.h"
@@ -23,6 +24,14 @@
#include "VkTimerQueryManager.h" #include "VkTimerQueryManager.h"
#include "MG_Util/Math/VectorTypes.h" #include "MG_Util/Math/VectorTypes.h"
#include <Includes.h> #include <Includes.h>
#include <MG_Backend/BackendObject.h>
#include <MG_Pipe/MGPipeHandles.h>
#include <MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h>
#if MOBILEGL_PIPE_PUSH
// The applier's CSO store: MGPipeApplier().BoundRenderStateCso is what the pipeline memo
// keys on after P2 (D12.1). Push-only, so the pull build's include graph is unchanged.
#include <MG_Pipe/PipeApply.h>
#endif
#include <vk_mem_alloc.h> #include <vk_mem_alloc.h>
#include "../VkIncludes.h" #include "../VkIncludes.h"
@@ -197,9 +206,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLbitfield mask, GLenum filter); GLbitfield mask, GLenum filter);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
GLint x, GLint y, GLsizei width, GLsizei height); GLint x, GLint y, GLsizei width, GLsizei height);
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, void CopyImageSubData(const CopyImageEndpoint& srcEndpoint,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, const CopyImageEndpoint& dstEndpoint,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth); GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target); void GenerateMipmap(GLenum target);
@@ -211,10 +220,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLsizei height, GLenum format, GLenum type, void* pixels); GLsizei height, GLenum format, GLenum type, void* pixels);
// Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage; // Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage;
// expects command recording to be active and any render pass already ended. // expects command recording to be active and any render pass already ended.
//
// `defaultFramebufferOrientation` is set only when the source is the swapchain's
// depth/stencil image, which this renderer stores display-side-up: the copy rect then
// has to be mapped out of GL's bottom-origin space and the copied rows re-oriented on
// the way back, exactly as the colour ReadPixels path does.
// `sourceLayerCount` above 1 says the `height` rows the client is owed are stored as that
// many ARRAY LAYERS of a one-row image rather than as rows of one layer - the shape a GL
// 1D array has in Vulkan. The two produce byte-identical tightly-packed readbacks, so
// only the copy region differs; everything after it is written against `height`.
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout, void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer, VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
void* pixels); void* pixels, Bool defaultFramebufferOrientation = false,
Uint32 sourceLayerCount = 1);
// Same-extent depth blit between images of different depth formats: host // Same-extent depth blit between images of different depth formats: host
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer). // round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat, Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
@@ -224,6 +243,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout, GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
VkImageLayout dstRestoreLayout, Bool stencilAspect); VkImageLayout dstRestoreLayout, Bool stencilAspect);
static SizeT GetReadbackTexelSize(VkFormat sourceFormat); static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
// Map a GL bottom-left-origin rectangle into the display-oriented swapchain image.
// Quarter-turn surface transforms swap the copy extent's axes.
static Bool MapDefaultFramebufferReadbackRect(GLint x, GLint y, GLsizei width, GLsizei height,
VkExtent2D imageExtent,
VkSurfaceTransformFlagBitsKHR preTransform,
VkOffset2D* imageOffset, VkExtent2D* imageCopyExtent);
// Reorder a tightly packed block copied with MapDefaultFramebufferReadbackRect back into
// GL row order. The input block has swapped dimensions for 90/270 degree transforms.
static Bool RemapDefaultFramebufferReadback(const Uint8* rawPixels, Uint32 logicalWidth,
Uint32 logicalHeight,
VkSurfaceTransformFlagBitsKHR preTransform,
SizeT texelSize, Uint8* outPixels);
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat, static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
GLsizei width, GLsizei height, GLenum destinationFormat, GLsizei width, GLsizei height, GLenum destinationFormat,
GLenum destinationType, SizeT destinationRowStride, GLenum destinationType, SizeT destinationRowStride,
@@ -293,6 +324,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is // The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is
// honored rather than accepted-and-ignored. // honored rather than accepted-and-ignored.
Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; } Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; }
// ARB_base_instance extends indirect command records with a non-zero firstInstance and
// requires gl_InstanceID to remain zero-based. Vulkan needs both features to honor that
// complete contract: one legalizes the command word, the other enables the shader rebase.
Bool IsNonZeroIndirectBaseInstanceSupported() const {
return m_drawIndirectFirstInstanceFeatureEnabled && m_shaderDrawParametersFeatureEnabled;
}
// Ensures the frame command buffer is recording (same lazy pattern as // Ensures the frame command buffer is recording (same lazy pattern as
// SetupDraw) and writes a bottom-of-pipe timestamp into the current // SetupDraw) and writes a bottom-of-pipe timestamp into the current
// frame's pool. Null when unsupported or the pool is exhausted. // frame's pool. Null when unsupported or the pool is exhausted.
@@ -363,6 +400,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 samplerBinding = 0; Uint32 samplerBinding = 0;
}; };
// A single-sample staging image for multisample-resolve blits that also have to change
// orientation. vkCmdResolveImage cannot flip (it takes one offset per side, not the
// invertible pair vkCmdBlitImage takes), so a resolve into or out of the default
// framebuffer used to land the mirrored band. Resolving here first and then blitting from
// here separates the two operations, and each one then does only what it can express.
//
// Pooled rather than created per blit: the CTS runs hundreds of these back to back, and
// create-destroy per call would both cost allocations and, worse, need per-call deferred
// destruction to outlive the recording. It grows to the largest extent asked for and is
// reused; format changes recreate it.
struct MultisampleResolveScratchImage {
VkImage image = VK_NULL_HANDLE;
VmaAllocation allocation = VK_NULL_HANDLE;
VkFormat format = VK_FORMAT_UNDEFINED;
VkExtent2D extent = {0, 0};
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
};
MultisampleResolveScratchImage m_msResolveScratch;
// Returns a scratch image at least `extent` in size with exactly `format`, transitioned to
// TRANSFER_DST and ready to be resolved into. Null image on failure (the caller then falls
// back to the direct resolve).
Bool AcquireMultisampleResolveScratchImage(VkCommandBuffer commandBuffer, VkFormat format,
VkExtent2D extent);
void DestroyMultisampleResolveScratchImage();
struct DeferredDepthMipmapCleanup { struct DeferredDepthMipmapCleanup {
Vector<VkImageView> imageViews; Vector<VkImageView> imageViews;
Vector<VkFramebuffer> framebuffers; Vector<VkFramebuffer> framebuffers;
@@ -445,15 +507,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void* m_platformDisplay = nullptr; void* m_platformDisplay = nullptr;
void* m_platformLibrary = nullptr; void* m_platformLibrary = nullptr;
void* m_platformCloseDisplay = nullptr; void* m_platformCloseDisplay = nullptr;
// Some real ICDs (e.g. NVIDIA's proprietary Linux driver) don't implement // Whether the loader exposes VK_EXT_headless_surface, detected once in
// VK_EXT_headless_surface at all. Detected once in CreateInstance() from the // CreateInstance() from the enumerated instance extensions. On desktop an
// enumerated instance extensions; when false, CreateSurface() falls back to a // offscreen surface REQUIRES it: false is a clean, loud bring-up failure, never
// hidden Xlib window instead of vkCreateHeadlessSurfaceEXT. // a substituted window. (Android is the one exception and has its own path -
// no Mali/Adreno driver seen so far exposes the extension, so a windowless
// context is given an AImageReader ANativeWindow that is never displayed.)
Bool m_headlessSurfaceSupported = true; Bool m_headlessSurfaceSupported = true;
// Set when CreateSurface() had to create its own Xlib window for the fallback
// above (rather than being handed one by the caller), so Shutdown() knows it
// owns that window and must destroy it.
Bool m_ownsFallbackXlibWindow = false;
// Android has the same shortfall: no Mali/Adreno driver seen so far exposes // Android has the same shortfall: no Mali/Adreno driver seen so far exposes
// VK_EXT_headless_surface, so a windowless (EGL pbuffer) context gets an // VK_EXT_headless_surface, so a windowless (EGL pbuffer) context gets an
// AImageReader's ANativeWindow to hand the WSI instead. Nothing is ever // AImageReader's ANativeWindow to hand the WSI instead. Nothing is ever
@@ -508,7 +568,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool m_samplerAnisotropyFeatureEnabled = false; Bool m_samplerAnisotropyFeatureEnabled = false;
Bool m_shaderDrawParametersExtensionEnabled = false; Bool m_shaderDrawParametersExtensionEnabled = false;
Bool m_shaderDrawParametersFeatureEnabled = false; Bool m_shaderDrawParametersFeatureEnabled = false;
// Native subgroup topology, queried at device creation for the compute-module
// subgroup repairs (SubgroupSupportPolicy.h) and the REQUIRE_FULL_SUBGROUPS
// stage flag; 0 / false when the device has no usable compute subgroups or
// MOBILEGL_MAGMA_DISABLE_SUBGROUP forced them off.
Uint32 m_nativeSubgroupSize = 0;
Bool m_nativeSubgroupSupported = false;
Bool m_computeFullSubgroupsFeatureEnabled = false;
// VkPhysicalDeviceSubgroupSizeControlProperties::maxComputeWorkgroupSubgroups;
// 0 when the extension (and therefore the full-subgroups flag) is unavailable.
Uint32 m_maxComputeWorkgroupSubgroups = 0;
Bool m_unformattedFloatStorageImagesEnabled = false; Bool m_unformattedFloatStorageImagesEnabled = false;
// Set only after descriptor-indexing feature AND property queries prove that
// update-after-bind is legal for every descriptor category this renderer emits.
ProgramFactory::UpdateAfterBindLimits m_updateAfterBindLimits{};
// fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates // fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
// per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and // per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and
// drive a runtime fallback when the device lacks them. // drive a runtime fallback when the device lacks them.
@@ -519,6 +592,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent. // needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
Bool m_dualSrcBlendFeatureEnabled = false; Bool m_dualSrcBlendFeatureEnabled = false;
Bool m_primitiveTopologyListRestartFeatureEnabled = false; Bool m_primitiveTopologyListRestartFeatureEnabled = false;
// shaderTessellationAndGeometryPointSize gates the PointSize built-in in a tessellation
// or geometry stage, which desktop GL treats as an ordinary per-vertex output (writable,
// and capturable by name through transform feedback). Cached at device creation and
// handed to ProgramFactory, which refuses a program whose tessellation or geometry module
// declares the matching SPIR-V capability while this is false - SetupDraw then skips its
// draws (VkProgramObject::pointSizeCapabilityUnsupported) rather than building a pipeline
// that is invalid usage.
Bool m_tessellationAndGeometryPointSizeFeatureEnabled = false;
// VK_EXT_custom_border_color. Vulkan's four predefined VkBorderColor values cover only
// transparent/opaque black and opaque white; GL_TEXTURE_BORDER_COLOR is an arbitrary vec4 (or
// an arbitrary ivec4/uvec4 through the "I" entry points). Without this extension a border
// colour outside the palette has to be snapped to the nearest predefined one. Both features
// are required together: customBorderColorWithoutFormat is what lets a sampler carry a custom
// colour without naming the image format it will be paired with, which GL's sampler objects
// cannot know. maxCustomBorderColorSamplers is a real device limit, so the sampler cache has
// to be able to fall back to the snapped value once it is reached.
Bool m_customBorderColorFeatureEnabled = false;
Uint32 m_maxCustomBorderColorSamplers = 0;
// sampleRateShading gates VkPipelineMultisampleStateCreateInfo::sampleShadingEnable, i.e.
// glEnable(GL_SAMPLE_SHADING) + glMinSampleShading. Unlike dualSrcBlend this does NOT
// hard-fail the draw when absent: sample shading is a rate hint, and every sample-rate
// pipeline is still correct (just not per-sample) at the default rate - so the enable is
// dropped and the draw proceeds, which is what a GL implementation with SAMPLES=1 does too.
Bool m_sampleRateShadingFeatureEnabled = false;
// multiViewport gates rasterizing into more than one of ARB_viewport_array's 16 viewports
// (gl_ViewportIndex). m_maxRasterizableViewports is min(MAX_VIEWPORTS, device limit), or 1
// when the feature is off, and is the viewportCount a gl_ViewportIndex-writing pipeline
// declares - it is NOT what GL_MAX_VIEWPORTS reports, which is the frontend state width.
Bool m_multiViewportFeatureEnabled = false;
Uint32 m_maxRasterizableViewports = 1;
// Union of shader stages sampled-read barriers may name; built at device creation // Union of shader stages sampled-read barriers may name; built at device creation
// because geometry/tessellation stage bits are invalid in a barrier when their // because geometry/tessellation stage bits are invalid in a barrier when their
// feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and // feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and
@@ -579,8 +682,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// per object: one group of four slots each, handed out on first use. // per object: one group of four slots each, handed out on first use.
static constexpr SizeT kXfbCounterObjectSlots = 16; static constexpr SizeT kXfbCounterObjectSlots = 16;
VkBufferObject m_xfbCounterBuffer; VkBufferObject m_xfbCounterBuffer;
UnorderedMap<Uint, Uint32> m_xfbCounterSlotByObject; // Which transform feedback object owns each slot group, by the frontend's never-reused
Uint32 m_xfbNextCounterSlot = 0; // lifetime id (0 = the slot is free). This used to be an UnorderedMap keyed on the GL
// NAME, which is recycled by glGenTransformFeedbacks: a deleted-and-recreated object
// inherited the dead one's slot, and since nothing ever removed an entry the map also
// grew for the life of the context. A fixed table cannot do either: a group is taken over
// only from an owner with no OPEN span (see CurrentXfbCounterSlot), so an object whose
// counters can still be resumed never loses them, and a dead object's group comes back.
Array<Uint64, kXfbCounterObjectSlots> m_xfbCounterSlotOwner{};
// Tie-break among reclaimable groups only; never on its own, because the paused span the
// groups exist for is by construction the least recently used one.
Array<Uint64, kXfbCounterObjectSlots> m_xfbCounterSlotLastUse{};
Uint64 m_xfbCounterSlotUseSerial = 0;
// Set for a slot once a captured draw has been recorded into its span; selects // Set for a slot once a captured draw has been recorded into its span; selects
// counter-buffer resume on the next captured draw of the same span. // counter-buffer resume on the next captured draw of the same span.
Array<Bool, kXfbCounterObjectSlots> m_xfbCountersValid{}; Array<Bool, kXfbCounterObjectSlots> m_xfbCountersValid{};
@@ -622,15 +735,74 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<Uint32> m_xfbQueryActiveSlots[2]; Vector<Uint32> m_xfbQueryActiveSlots[2];
Bool m_xfbQuerySlotOpen = false; Bool m_xfbQuerySlotOpen = false;
Uint32 m_xfbQueryOpenSlot = 0; Uint32 m_xfbQueryOpenSlot = 0;
// GL_PRIMITIVES_GENERATED reroute for draws made while transform feedback is
// INACTIVE. The stream pool's primitivesNeeded is defined to count those draws
// too, but a Mali driver (and Mesa lavapipe) answers 0 unless a capture span
// is open (the CTS's tessellator-measuring shape). Where the bring-up probe
// finds that defect with a working control - or
// MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE forces it - such draws accumulate the
// GENERATED count through this pool instead, whose type the arming picks:
// VK_QUERY_TYPE_PRIMITIVES_GENERATED_EXT where the device hosts the dedicated
// query with its rasterizer-discard feature (exact semantics by definition -
// the extension exists because GL needs this count without a capture), else a
// VK_QUERY_TYPE_PIPELINE_STATISTICS pool over clipping-stage invocations (one
// per primitive reaching primitive clipping - after every vertex processing
// stage, before rasterizer discard - which is the same set).
// XFB-ACTIVE draws keep the stream slot (exact today, and WRITTEN needs it);
// every draw with no open capture - a PAUSED span's draws included - takes a
// reroute slot, and the span then ignores the frontend's CPU paused-primitive
// counter rather than adding it on top (see IsPrimGenRerouteArmed): that
// counter is written by only 3 of the ~15 draw entry points and answers 0 for
// GL_PATCHES, so it cannot price the draws this reroute exists to repair. One
// GL query span may therefore hold slots of both pools.
Bool m_pipelineStatisticsQueryFeatureEnabled = false;
// VK_EXT_primitives_generated_query: base feature, and the
// ...WithRasterizerDiscard feature without which a discarding draw inside the
// query is invalid usage (so the reroute never picks the dedicated pool on a
// base-only device - GL applications toggle discard freely).
Bool m_primitivesGeneratedQueryFeatureEnabled = false;
Bool m_primitivesGeneratedQueryDiscardFeatureEnabled = false;
// tessellationShader was enabled at device creation (it is taken whenever the
// device advertises it); gates the probe's PATCHES shape.
Bool m_tessellationShaderFeatureEnabled = false;
MG_Util::SelfTest::PrimGenRerouteKind m_primGenRerouteKind =
MG_Util::SelfTest::PrimGenRerouteKind::None;
// The bring-up probe measured this device's stream query as counting draws made
// with no capture span open (the StreamCounts verdict) - so it counts the
// PAUSED-span ones too, through the stream slot they take when nothing is
// rerouted. Only the probe can know this, so it stays false wherever the probe
// is not consulted (the forced arms), which keeps those lanes' accounting as it
// was.
Bool m_primGenStreamCountsXfbInactiveDraws = false;
VkQueryPool m_primGenReroutePool = VK_NULL_HANDLE;
Uint32 m_primGenRerouteSlotCursor = 0;
Vector<Uint32> m_primGenRerouteActiveSlots;
Bool m_primGenRerouteSlotOpen = false;
Uint32 m_primGenRerouteOpenSlot = 0;
// Runs the bring-up probe (memoized per process) and decides
// m_primGenRerouteKind. Called at the end of device creation: it records on
// m_graphicsQueue, which nothing else is using yet.
void ArmPrimGenReroute();
public: public:
// Whether a GENERATED span opened now will have the draws made while the GL
// span is PAUSED counted on the GPU - through the reroute pool, which takes
// every draw with no open capture, or (where the reroute is not armed because
// the stream query was measured to count capture-less draws) through the stream
// slot such a draw still takes. The frontend's CPU paused-primitive counter
// must not be added on top of either: it would double count, and it cannot
// price the draws that matter anyway - only 3 of the ~15 draw entry points
// write it and it answers 0 for GL_PATCHES. Read once per span, after
// StartXfbQueryCapture (whose pool creation may disarm the reroute).
Bool ArePausedDrawsGpuCounted() const;
// kind: 0 = PRIMITIVES_WRITTEN, 1 = PRIMITIVES_GENERATED. // kind: 0 = PRIMITIVES_WRITTEN, 1 = PRIMITIVES_GENERATED.
Bool StartXfbQueryCapture(Uint32 kind); Bool StartXfbQueryCapture(Uint32 kind);
void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots); void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots, Vector<Uint32>& outRerouteSlots);
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, Bool wantGenerated, Uint64& outPrimitives); Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, const Vector<Uint32>& rerouteSlots,
Bool wantGenerated, Uint64& outPrimitives);
private: private:
void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer); void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer, Bool xfbActive);
void EndXfbQueryForDraw(VkCommandBuffer commandBuffer); void EndXfbQueryForDraw(VkCommandBuffer commandBuffer);
VkCommandPool m_commandPool = VK_NULL_HANDLE; VkCommandPool m_commandPool = VK_NULL_HANDLE;
@@ -655,29 +827,186 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 programHash = 0; Uint64 programHash = 0;
Uint64 vertexInputHash = 0; Uint64 vertexInputHash = 0;
Uint64 renderPassHash = 0; Uint64 renderPassHash = 0;
// VALUE hash of the pipeline-relevant fixed-function state (see // The PRE-HANDLE arm's key component (P2 brief D12.1), and 0 in every entry the
// ComputePipelineStateHash), not the monotonic pipeline-state version: // handle arm mints. VALUE hash of the pipeline-relevant fixed-function state (see
// the version never repeats, so a per-draw GL_BLEND toggle would miss // ComputePipelineStateHash), not the monotonic pipeline-state version: the version
// all entries forever even though the state alternates between two // never repeats, so a per-draw GL_BLEND toggle would miss all entries forever even
// values the memo already holds. // though the state alternates between two values the memo already holds.
Uint64 pipelineStateHash = 0; Uint64 pipelineStateHash = 0;
#if MOBILEGL_PIPE_PUSH
// The HANDLE arm's key component, and the whole of D12.1: the CLIENT already
// hashed the pipeline subset of RenderStateParameters and minted a content-
// addressed CSO for it (MG_Pipe/MGPipeRenderStateSpans.h, MG_Impl/Pipe/CsoCache),
// so re-hashing the same 396 bytes here was work the boundary had already done.
// Two draws share a CSO handle exactly when their pipeline bytes are equal, and
// the client's subset is a strict SUPERSET of what ComputePipelineStateHash read,
// so the handle discriminates at least as finely as the hash it replaces.
//
// renderPassHash STAYS beside it and is what keeps this key complete: the CSO
// carries GL state only, while colorAttachmentCount and the rasterization sample
// count - which ComputePipelineStateHash folded in through its signature and
// through ResolveEffectiveSampleMask - are render-pass facts that the render-pass
// hash already separates.
//
// Null in an entry minted by the legacy arm, so entries of the two arms can never
// match each other: the compare below tests BOTH components.
MG_Pipe::MGPipeHandle renderStateCso = MG_Pipe::kMGPipeNullHandle;
#endif
ProgramFactory::CompileOptionFlags transformFlags = {}; ProgramFactory::CompileOptionFlags transformFlags = {};
// Baked into the pipeline (PipelineFactory::ComputeHash mixes it), and NOT derivable
// from anything else in this key: it depends on whether the draw is indexed and on the
// index type, neither of which the mode/program/state hashes carry. Without it an
// indexed and a non-indexed draw over the same program and state collide on one entry
// and the second one gets the first one's restart setting.
Bool primitiveRestartEnable = false;
VkPipeline pipeline = VK_NULL_HANDLE; VkPipeline pipeline = VK_NULL_HANDLE;
}; };
static constexpr Uint32 kPipelineMemoSize = 8; static constexpr Uint32 kPipelineMemoSize = 8;
PipelineMemoEntry m_pipelineMemo[kPipelineMemoSize]; PipelineMemoEntry m_pipelineMemo[kPipelineMemoSize];
Uint32 m_pipelineMemoCount = 0; Uint32 m_pipelineMemoCount = 0;
Uint32 m_pipelineMemoNext = 0; Uint32 m_pipelineMemoNext = 0;
// Hash of every fixed-function GL state the pipeline payload reads that the
// memo key's other fields (mode / program / vertex input / render pass / #if MOBILEGL_PIPE_PUSH
// transform flags) do not already pin down. Equal hash under an equal rest // P2 D12.1's arm selector, and the whole of the pipeline memo's re-key. Returns the
// of key => byte-identical PipelineCreatePayload. Cached per pipeline-state // render-state CSO this draw is keyed on, or the null handle when the pre-handle arm
// is the one that runs.
//
// Under the handle arm the memo's state key IS this handle. The client hashed those
// 396 pipeline bytes when it minted the CSO (MGPipeComputePipelineSubsetHash), so
// recomputing an overlapping hash here was work the boundary had already done; the
// client's pipeline subset is a strict SUPERSET of what ComputePipelineStateHash read,
// so the handle discriminates at least as finely as the hash it replaces. What the
// handle does NOT carry is the render-pass side - colorAttachmentCount and the
// rasterization sample count, which ComputePipelineStateHash folded in through its
// signature and through ResolveEffectiveSampleMask - and that is exactly why
// entry.renderPassHash stays in the key beside it.
//
// The arm is live only when the render-state subsystem is migrated in this run AND the
// client has actually bound a 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
// delete_render_state clears the binding (MG_Pipe/PipeApply.cpp), so the null handle is
// reachable on any tree. Keying every draw on it would alias every render state onto
// one memo entry, so a null handle means "fall back to a state hash" - never an abort,
// and never a per-draw consultation of the legacy-memo lever: bit 0 is not a Track-H
// subsystem (D14 labels only bits 5 and 6 that), and the lever's Fatal is a STARTUP
// one, in MagmaPipeValidateSubsystemConfiguration.
//
// The fallback is warned ONCE rather than logged at debug, and that is deliberate: a
// silent fallback is what makes "the CSO arm never ran" easy to miss. W is compiled in
// at every shipped log level.
//
// The latch is a plain member bool, NOT MGLOG_W_ONCE. MOBILEGL_LOG_ONCE_INTERNAL
// (MG_Util/Debug/Log.h) is an UNCONDITIONAL std::atomic_flag::test_and_set - a locked
// xchg, executed on every evaluation, not "one static bool test" as an earlier round of
// this comment claimed - and this site is on the per-draw pipeline path in the very
// configuration that reaches it (no tracker: every draw). ROADMAP.md:7 forbids leaving
// instrumentation on a hot path, so the once-ness is one non-atomic, always-predicted
// load of a member that is false exactly once. Single-threaded like the rest of the
// renderer, and per renderer rather than per process, which is also the right scope: a
// second context that never binds a CSO deserves to say so.
//
// What the absence of this warning from a run's log proves, EXACTLY: that no draw took
// the fallback WHILE bit 0 was set. With kMGPipeSubsystemRenderState clear the function
// returns before the latch, so absence proves nothing at all - and no draw is keyed on a
// handle either. Grep the mask out of the log beside it (review v2 minor 3).
//
// Push-only by construction: the pull build does not compile this function at all, so
// its two callers are statement-for-statement what they were (G1).
//
// [routed to the integrator, review v2 minor 11] MG_Pipe::MGPipeApplier() is ONE
// process-global applier (MG_Pipe/PipeApply.cpp), not the per-context CSO store D2
// specifies. In a multi-context process this reads whatever CSO another context last
// bound. The defect is package A's and the fix belongs there; Magma is its only P2
// consumer, so it is named here rather than left for both reviews to assume the other
// caught it.
MG_Pipe::MGPipeHandle ResolveBoundRenderStateCso() const {
if (!MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemRenderState)) {
return MG_Pipe::kMGPipeNullHandle;
}
const MG_Pipe::MGPipeHandle boundCso = MG_Pipe::MGPipeApplier().BoundRenderStateCso;
if (MG_Pipe::MGPipeHandleIsNull(boundCso) && !m_pipelineCsoFallbackWarned) {
m_pipelineCsoFallbackWarned = true;
MGLOG_W("MGPipe: kMGPipeSubsystemRenderState is on but no render-state CSO is "
"bound; the pipeline memo is running on a state hash, not on the CSO "
"handle (no tracker on this build, or a draw between "
"delete_render_state and the next bind)");
}
return boundCso;
}
// Latch for the warning above. Mutable because the resolve is const and the latch is
// not part of the renderer's observable state.
mutable Bool m_pipelineCsoFallbackWarned = false;
// The memo key's STATE-HASH half, for a draw that has no CSO handle to key on: the
// pre-handle arm, and the fallback of D12.1's handle arm. Cached on the pipeline-state
// version plus the two render-pass facts the hash's inputs depend on, so an unchanged
// (version, colorAttachmentCount, sampleCount) proves the bytes are unchanged.
//
// [deviation from D12.1] The brief deletes this gate and its cached fields outright.
// They cannot go while a no-CSO draw is reachable - and it is, on any tree: a draw
// between delete_render_state and the next bind has no handle. On a tree whose tracker
// binds a CSO these five words are written once and never read again; they retire for
// real when the pull path does, at P13.
Uint64 ResolveFallbackPipelineStateHash(Uint renderStateVersion, Uint32 colorAttachmentCount,
VkSampleCountFlagBits rasterizationSamples) {
if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion ||
m_pipelineStateHashColorCount != colorAttachmentCount ||
m_pipelineStateHashSampleCount != rasterizationSamples) {
#if MOBILEGL_PIPE_LEGACY_MEMOS
m_pipelineStateHash =
ComputePipelineStateHash(colorAttachmentCount, rasterizationSamples);
#else
m_pipelineStateHash = ComputePipelineSubsetStateHashFallback();
#endif
m_pipelineStateHashVersion = renderStateVersion;
m_pipelineStateHashColorCount = colorAttachmentCount;
m_pipelineStateHashSampleCount = rasterizationSamples;
m_pipelineStateHashValid = true;
}
return m_pipelineStateHash;
}
#endif // MOBILEGL_PIPE_PUSH
#if MOBILEGL_PIPE_PUSH && !MOBILEGL_PIPE_LEGACY_MEMOS
// The same answer as ComputePipelineStateHash, computed from the P2 chunk table
// instead of from a hand-written field list, for the build that compiles no
// pre-handle arm (cmake -DMOBILEGL_PIPE_LEGACY_MEMOS=OFF). It is the CLIENT's own
// hash function - MGPipeComputePipelineSubsetHash over the 396 pipeline bytes - so a
// draw keyed on it and a draw keyed on a CSO handle are keyed on the same equivalence
// class of state, and the render-pass facts stay separated by renderPassHash either
// way. This is what makes the no-legacy build RUNNABLE rather than a configuration
// that aborts on the first draw that arrives without a CSO.
Uint64 ComputePipelineSubsetStateHashFallback() const;
#endif
#if MOBILEGL_PIPE_LEGACY_MEMOS
// THE PRE-HANDLE ARM (P2 brief D12.1 / D14). Hash of every fixed-function GL state the
// pipeline payload reads that the memo key's other fields (mode / program / vertex
// input / render pass / transform flags) do not already pin down. Equal hash under an
// equal rest of key => byte-identical PipelineCreatePayload. Cached per pipeline-state
// version: the version is monotonic and bumps on every pipeline-state // version: the version is monotonic and bumps on every pipeline-state
// change, so an unchanged (version, colorAttachmentCount) proves the state // change, so an unchanged (version, colorAttachmentCount) proves the state
// bytes are unchanged and the hash can be reused without re-reading them. // bytes are unchanged and the hash can be reused without re-reading them.
Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount) const; //
// The handle arm computes none of this: the client hashed the same bytes when it
// minted the CSO, so all five cached-hash members below exist only to avoid a
// re-hash the handle arm never performs.
Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount,
VkSampleCountFlagBits rasterizationSamples) const;
#endif
// The effective GL_SAMPLE_MASK word for a draw at this rasterization sample count; see
// the definition for the GL-vs-Vulkan rule it reconciles. Shared by the pipeline payload
// and the pipeline-state memo word so the two cannot disagree. NOT part of the legacy
// arm: it is a PAYLOAD computation that depends on rasterizationSamples, so it survives
// the re-key and keeps reading Multisample / SampleMask / SampleMaskValue out of the
// working block.
Uint32 ResolveEffectiveSampleMask(VkSampleCountFlagBits rasterizationSamples) const;
// ResolveFallbackPipelineStateHash's cache. Written once and never read again on a
// build whose client binds a render-state CSO; see that function for why it survives
// the re-key at all.
Uint m_pipelineStateHashVersion = 0; Uint m_pipelineStateHashVersion = 0;
Uint32 m_pipelineStateHashColorCount = 0; Uint32 m_pipelineStateHashColorCount = 0;
// The sample count the cached hash was computed at. A pipeline-state input now depends on
// it (the effective sample mask), so a draw that changes only the target's sample count
// has to recompute rather than reuse.
VkSampleCountFlagBits m_pipelineStateHashSampleCount = VK_SAMPLE_COUNT_1_BIT;
Uint64 m_pipelineStateHash = 0; Uint64 m_pipelineStateHash = 0;
Bool m_pipelineStateHashValid = false; Bool m_pipelineStateHashValid = false;
// GetShaderTransformFlags memo. NOT pure in the pre-transform alone: the // GetShaderTransformFlags memo. NOT pure in the pre-transform alone: the
@@ -697,7 +1026,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Drops every memoized pipeline handle. Required at command-buffer // Drops every memoized pipeline handle. Required at command-buffer
// boundaries and whenever any pipeline may have been destroyed. Also drops // boundaries and whenever any pipeline may have been destroyed. Also drops
// the cached pipeline-state hash: the same boundaries can retire the GL // the cached pipeline-state hash: the same boundaries can retire the GL
// context whose monotonic version the cache is keyed on. // context whose monotonic version the cache is keyed on. The handle arm has no
// such cache to drop - a CSO handle is not derived from a monotonic version.
void InvalidatePipelineMemo() { void InvalidatePipelineMemo() {
m_pipelineMemoCount = 0; m_pipelineMemoCount = 0;
m_pipelineMemoNext = 0; m_pipelineMemoNext = 0;
@@ -719,7 +1049,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Skip the per-draw CollectSampledTextures walk (~5% of the render thread) when the sampled // Skip the per-draw CollectSampledTextures walk (~5% of the render thread) when the sampled
// texture SET is provably unchanged from the previous draw: same program (lifetime id + // texture SET is provably unchanged from the previous draw: same program (lifetime id +
// backend-state version, which covers sampler-uniform reassignment / relink) and transform // backend-state version, which covers sampler-uniform reassignment / relink) and transform
// flags, and no texture bind/unbind/delete since (GetTextureBindGeneration). On a hit, // flags, no texture bind/unbind/delete since (GetTextureBindGeneration), and nothing that
// moves a texture's shape or a sampler's parameters since (GetSamplingResolutionGeneration
// - membership depends on mipmap-completeness, which both of those decide). On a hit,
// m_sampledTexturesScratch still holds the previous draw's list and steps 2-4 (feedback / // m_sampledTexturesScratch still holds the previous draw's list and steps 2-4 (feedback /
// layout probe / transition) re-run on it, so layout correctness is unaffected - only the GL // layout probe / transition) re-run on it, so layout correctness is unaffected - only the GL
// walk is skipped. The program lifetime id (never reused, unlike the GL name) and the // walk is skipped. The program lifetime id (never reused, unlike the GL name) and the
@@ -730,6 +1062,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 m_lastSampledSetProgramVersion = 0; Uint32 m_lastSampledSetProgramVersion = 0;
ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {}; ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {};
Uint64 m_lastSampledSetBindGeneration = 0; Uint64 m_lastSampledSetBindGeneration = 0;
Uint64 m_lastSampledSetSamplingGeneration = 0;
// Set from the draw's resolved VkProgramObject on both the full and the fast setup paths;
// read by BeginXfbCaptureForDraw, which has only GL state otherwise. See
// VkProgramObject::xfbCaptureDeclined.
Bool m_currentDrawXfbCaptureDeclined = false;
// Memo for the per-draw explicit-LOD-0 eligibility probe // Memo for the per-draw explicit-LOD-0 eligibility probe
// (ProgramSamplesOnlySingleLevelTextures): same key family as the // (ProgramSamplesOnlySingleLevelTextures): same key family as the
@@ -742,9 +1079,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 m_lastLodProgramVersion = 0; Uint32 m_lastLodProgramVersion = 0;
Uint64 m_lastLodBindGeneration = 0; Uint64 m_lastLodBindGeneration = 0;
Uint64 m_lastLodParamsSum = 0; Uint64 m_lastLodParamsSum = 0;
// Sampling-resolution generation at probe time. The probe reads the effective
// sampler's filters/aniso/LOD range, whose setters bump only this counter -
// the params-version sum above never moves for them.
Uint64 m_lastLodSamplingGeneration = 0;
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {}; ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {}; ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
// Does the current program's vertex stage declare the BaseVertex builtin? A property
// of the program's SPIR-V, so (lifetime id, backend-state version) is the whole key.
//
// Memoized rather than re-asked because asking means resolving the UN-zeroed program
// variant, and a program that only ever draws non-indexed would then compile a variant
// no draw uses AND re-stamp its use every draw, so the idle sweep could never retire
// it. With the memo the answer is known before the first lookup and only the variant
// the draw actually needs is resolved.
Bool m_lastBaseVertexQueryValid = false;
Uint64 m_lastBaseVertexProgramLifetimeId = 0;
Uint32 m_lastBaseVertexProgramVersion = 0;
Bool m_lastBaseVertexReads = false;
// Snapshot behind TrySetupDrawFastPath. Values only: the program and // Snapshot behind TrySetupDrawFastPath. Values only: the program and
// render-pass caches are open-addressing maps whose entries move on // render-pass caches are open-addressing maps whose entries move on
// insert, so no pointers into them are cached; the pipeline handle is // insert, so no pointers into them are cached; the pipeline handle is
@@ -764,14 +1118,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// common shape), and "the VAO did not move" would then skip the layout // common shape), and "the VAO did not move" would then skip the layout
// re-resolve for a different VAO. // re-resolve for a different VAO.
Uint64 vaoLifetimeId = 0; Uint64 vaoLifetimeId = 0;
#if MOBILEGL_PIPE_PUSH
// P2 D12.4: the handle arm's answer to the same question, and one compare rather
// than the pair above. Kept BESIDE them rather than replacing them because the
// pre-handle arm is still compiled (MOBILEGL_PIPE_LEGACY_MEMOS) and this snapshot
// is a value struct, not a wire type.
MG_Pipe::MGPipeHandle vaoHandle = MG_Pipe::kMGPipeNullHandle;
#endif
Uint32 vaoConfigVersion = 0; Uint32 vaoConfigVersion = 0;
const void* drawFbo = nullptr; const void* drawFbo = nullptr;
// Never-reused lifetime id beside the raw pointer + Uint16 version: a
// deleted FBO recycled at the same address with the same fresh version
// count would otherwise compare equal (same ABA as the render-pass
// manager's fast-path memo).
Uint64 drawFboLifetimeId = 0;
Uint16 fboVersion = 0; Uint16 fboVersion = 0;
Bool drawFboIsDefault = false; Bool drawFboIsDefault = false;
Uint renderStateVersion = 0; Uint renderStateVersion = 0;
Uint64 bindGeneration = 0; Uint64 bindGeneration = 0;
Uint32 baseTransformFlags = 0; Uint32 baseTransformFlags = 0;
Uint32 resolvedTransformFlags = 0; Uint32 resolvedTransformFlags = 0;
// What ResolvePrimitiveRestartEnable answered for the draw this snapshot was taken
// from, i.e. what its pipeline's primitiveRestartEnable was built with. `aspects`
// already separates indexed from non-indexed draws, but not one index TYPE from
// another, and a restart index that fits GL_UNSIGNED_INT but not GL_UNSIGNED_SHORT
// makes those two draws want different pipelines.
Bool primitiveRestartEnable = false;
Uint64 renderPassHash = 0; Uint64 renderPassHash = 0;
Uint32 imageIndex = 0; Uint32 imageIndex = 0;
Uint64 textureEraseEpoch = 0; Uint64 textureEraseEpoch = 0;
@@ -789,12 +1161,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// re-resolve just the pipeline against the active pass; a change that // re-resolve just the pipeline against the active pass; a change that
// flips it must fall back to the full path's pass selection. // flips it must fall back to the full path's pass selection.
Bool drawUsesDepthStencil = false; Bool drawUsesDepthStencil = false;
// The snapshotting draw's pipeline viewportCount. A pure function of the PROGRAM
// (writesViewportIndexBuiltin) and of a device feature fixed at renderer init, both
// of which the programLifetimeId/programVersion guards above already pin - carried
// here so the fast path does not re-fetch the program object to re-derive it.
Uint32 viewportCount = 1;
IntVec2 renderPassExtent = {0, 0}; IntVec2 renderPassExtent = {0, 0};
// colorAttachmentCount of the snapshotting draw's render pass: the // colorAttachmentCount of the snapshotting draw's render pass: the
// pipeline-state hash input, so the fast path can refresh that hash and // pipeline-state hash input, so the fast path can refresh that hash and
// probe the pipeline memo after a state change without re-fetching the // probe the pipeline memo after a state change without re-fetching the
// render-pass entry (the pass itself is pinned by renderPassHash above). // render-pass entry (the pass itself is pinned by renderPassHash above).
Uint32 renderPassColorCount = 0; Uint32 renderPassColorCount = 0;
// Pinned with the colour count and for the same reason: the fast path recomputes the
// pipeline-state value hash from the snapshot, and that hash reads the sample count.
VkSampleCountFlagBits renderPassSampleCount = VK_SAMPLE_COUNT_1_BIT;
VkPipeline pipeline = VK_NULL_HANDLE; VkPipeline pipeline = VK_NULL_HANDLE;
// layoutHash of the snapshotting draw's vertex-input state. The pipeline and // layoutHash of the snapshotting draw's vertex-input state. The pipeline and
// the vertex-input pre-flight depend on the VAO only through this (plus the // the vertex-input pre-flight depend on the VAO only through this (plus the
@@ -856,6 +1236,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// already sampleable. // already sampleable.
Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch; Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch; Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
Vector<UniformManager::SamplerImageFeedbackBinding> m_samplerImageFeedbackScratch;
Vector<UniformManager::SamplerBindingOverride> m_samplerImageBindingOverridesScratch;
Vector<VkBuffer> m_vertexBuffersScratch; Vector<VkBuffer> m_vertexBuffersScratch;
Vector<VkDeviceSize> m_vertexOffsetsScratch; Vector<VkDeviceSize> m_vertexOffsetsScratch;
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch; Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
@@ -982,6 +1364,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkBuffer indexVkBuffer = VK_NULL_HANDLE; VkBuffer indexVkBuffer = VK_NULL_HANDLE;
VkDeviceSize indexSliceOffset = 0; VkDeviceSize indexSliceOffset = 0;
Uint64 indexFrameSerial = 0; Uint64 indexFrameSerial = 0;
// The EBO carried a host map when the slice was recorded - the mirror of
// anyBufferMapped on the vertex half. A shadow-backed (non-adopted)
// persistent map mutates its shadow with no API call and no epoch bump, so
// the one-compare rescue must decline and re-run the acquire, whose
// SyncPersistentMappedRange is the push-down. A map taken AFTER the record
// is already covered: AcquirePersistentMap bumps the slice epoch for the
// request itself, adopted or declined.
Bool indexBufferMapped = false;
// Bound per draw (first bindingCount elements). // Bound per draw (first bindingCount elements).
VkBuffer vkBuffers[kMaxBindings] = {}; VkBuffer vkBuffers[kMaxBindings] = {};
@@ -1009,6 +1399,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// - bindings revalidates per draw exactly as before (frame serial, content // - bindings revalidates per draw exactly as before (frame serial, content
// hash, per-binding live buffer pointers and slice epochs). // hash, per-binding live buffer pointers and slice epochs).
struct alignas(64) VaoDrawMemo { struct alignas(64) VaoDrawMemo {
#if MOBILEGL_PIPE_PUSH
// P2 D12.4: the handle arm's key, and the ONLY key it needs. {slot, gen} is an
// identity, so the pointer-plus-lifetime-id pair below stops being a key here;
// the slot also picks the table entry, so the address hash and the two-way probe
// go with it. Null in an entry that has never been claimed.
MG_Pipe::MGPipeHandle vaoHandle = MG_Pipe::kMGPipeNullHandle;
#endif
const MG_State::GLState::VertexArrayObject* vaoKey = nullptr; const MG_State::GLState::VertexArrayObject* vaoKey = nullptr;
// The VAO's never-reused lifetime id, checked alongside vaoKey. The pointer // The VAO's never-reused lifetime id, checked alongside vaoKey. The pointer
// ALONE is not an identity: a deleted VAO's heap address is handed straight // ALONE is not an identity: a deleted VAO's heap address is handed straight
@@ -1035,8 +1432,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// fixed table also makes every VaoDrawMemo/ResolvedVertexBindings pointer // fixed table also makes every VaoDrawMemo/ResolvedVertexBindings pointer
// stable for the duration of a draw, which the EBO memo handoff // stable for the duration of a draw, which the EBO memo handoff
// (m_currentDrawResolvedEntry) relies on. // (m_currentDrawResolvedEntry) relies on.
//
// [deviation from D12.4, deliberate and narrow] The brief asks for a grow-on-demand
// Vector. This one stays FIXED at exactly the capacity and exactly the 2-way victim
// rule it has on the base ref, and only its KEY changes (a {slot, gen} handle instead
// of a hashed heap address plus a lifetime id). Two reasons, and the second is the
// whole of review v2's MAJOR 1:
// * a VaoDrawMemo is ~450 B (ResolvedVertexBindings dominates), so growing this
// table with the live VAO set is megabytes on a platform with an LMK, where the
// other two memos are 48 B and can afford it;
// * this is the ONLY one of the three memos that had a capacity before this package.
// Losing an entry here costs a vertex-binding re-resolve, exactly what losing it
// cost on the base ref, so at any working-set size this table is no worse than what
// it replaces - and strictly better below capacity, where the handle is a bijection
// with the slot and the two-way probe never collides at all. The other two memos
// (VertexInputStateFactory::m_vaoMemos) had NO capacity, so they keep having none.
static constexpr Uint32 kVaoDrawMemoSlotCount = 2048; // power of two static constexpr Uint32 kVaoDrawMemoSlotCount = 2048; // power of two
Vector<VaoDrawMemo> m_vaoDrawMemoTable; Vector<VaoDrawMemo> m_vaoDrawMemoTable;
#if MOBILEGL_PIPE_PUSH
// The renderer's {slot, gen} mint, shared with its VertexInputStateFactory so both
// derive the same handle for the same VAO. Per renderer, never a process-global: a
// global would share one table and one reclamation clock across two live contexts and
// outlive every one of them (review v2 minor 4).
MagmaPipeIdentityTables m_pipeIdentity;
// The VAO's {slot, gen}. A one-entry memo hit for every acquisition after a draw's
// first, so there is no second memo in front of it here.
MG_Pipe::MGPipeHandle ResolveVaoHandle(const MG_State::GLState::VertexArrayObject& vao) {
return m_pipeIdentity.HandleOf(MG_Pipe::MGPipeKind::VertexElementsCso,
vao.GetLifetimeId());
}
#endif
// "Is this VAO's content hash already memoized?", asked of whichever side owns the
// memo (P2 D12.5). Force-inlined and defined in the class body so that the PULL
// build's three readers keep compiling to the very same two loads they always did -
// G1 admits no resize, and an out-of-line call here would be one.
[[gnu::always_inline]] inline Bool VaoContentHashIfKnown(
const MG_State::GLState::VertexArrayObject& vao, Uint64& outHash) const {
#if MOBILEGL_PIPE_PUSH
return m_vertexInputStateFactory->TryGetMemoizedHash(vao, outHash);
#else
return vao.GetBackendHashMemo(outHash);
#endif
}
// Finds the slot holding `vao`, or recycles the older of its two candidate // Finds the slot holding `vao`, or recycles the older of its two candidate
// slots into an empty memo keyed on `vao`. Never returns null. // slots into an empty memo keyed on `vao`. Never returns null.
VaoDrawMemo* LookupVaoDrawMemo(const MG_State::GLState::VertexArrayObject* vao); VaoDrawMemo* LookupVaoDrawMemo(const MG_State::GLState::VertexArrayObject* vao);
@@ -1060,13 +1497,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void CreateSwapchain(); void CreateSwapchain();
void CreateCommandPool(); void CreateCommandPool();
// Whether THIS draw's primitive stream restarts, and therefore what
// VkPipelineInputAssemblyStateCreateInfo::primitiveRestartEnable must be. Resolved by the
// caller because it needs two facts a pipeline cannot see: whether the draw is indexed at
// all (GL primitive restart acts on the index stream, so it is a no-op for glDrawArrays),
// and the index TYPE (an application restart index that does not fit the type matches no
// index, so that draw restarts nowhere - see UploadAndBindIndexBuffer).
Bool ResolvePrimitiveRestartEnable(Flags<DrawSetupAspect> aspects,
const IndexBufferView* pIndexBufferView) const;
VkPipeline GetOrCreatePipeline( VkPipeline GetOrCreatePipeline(
GLenum mode, GLenum mode,
const MG_State::GLState::ProgramObject& program, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, const ProgramFactory::VkProgramObject& programObj,
ProgramFactory::CompileOptionFlags transformFlags, ProgramFactory::CompileOptionFlags transformFlags,
const MG_State::GLState::VertexArrayObject& vao, const MG_State::GLState::VertexArrayObject& vao,
const RenderPassEntry& renderPassEntry); const RenderPassEntry& renderPassEntry,
Bool primitiveRestartEnable);
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj); VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
void DestroyComputePipelines(); void DestroyComputePipelines();
// Takes the frame rather than a command buffer: a first-time storage-usage upgrade has to // Takes the frame rather than a command buffer: a first-time storage-usage upgrade has to
@@ -1075,11 +1522,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
FrameContext::FrameData& frame, FrameContext::FrameData& frame,
const MG_State::GLState::ProgramObject& program, const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj); const ProgramFactory::VkProgramObject& programObj);
// Vulkan forbids a sampled descriptor and writable storage descriptor from naming the
// same image subresource in one shader operation. Snapshot only the sampler side; the
// storage descriptor continues to name the application texture.
Bool PrepareSamplerImageFeedbackSnapshots(
FrameContext::FrameData& frame,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
VkPipelineStageFlags consumerShaderStageMask);
// The per-draw dynamic-state tail (viewport, scissor, blend constants, depth // The per-draw dynamic-state tail (viewport, scissor, blend constants, depth
// bias, line width, stencil), gated behind one render-state-parameters-version // bias, line width, stencil), gated behind one render-state-parameters-version
// compare per command buffer - see the gate fields in DynamicStateShadow. // compare per command buffer - see the gate fields in DynamicStateShadow.
void ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo); // viewportCount is the bound pipeline's declared viewport count: 1 for every program that
// does not write gl_ViewportIndex (the memoized fast path), otherwise the renderer's
// rasterizable viewport count, which takes the unmemoized array path.
void ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo,
Uint32 viewportCount = 1);
void ApplyMultiViewportDynamicState(VkCommandBuffer commandBuffer, Uint32 viewportCount, const IntVec2& extent,
VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo);
VkRect2D ComputeGLScissorRect(Uint32 index, const IntVec2& extent,
VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo) const;
// How many viewports a draw with this program rasterizes into: 1 unless the program
// assigns gl_ViewportIndex AND the device enabled multiViewport. Both the pipeline's
// baked viewportCount and the dynamic arrays come from this one answer, so they cannot
// disagree.
Uint32 ResolveDrawViewportCount(Bool programWritesViewportIndex) const {
return programWritesViewportIndex && m_multiViewportFeatureEnabled ? m_maxRasterizableViewports : 1u;
}
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao, Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
const ProgramFactory::VkProgramObject& programObj, const ProgramFactory::VkProgramObject& programObj,
@@ -1110,16 +1580,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLenum filter); GLenum filter);
// Clears one z slice of a VK_IMAGE_TYPE_3D colour image. See the call site in // Clears one layer of a colour image through a throwaway render pass whose entire content
// MaterializePendingClearForTexture for why a transfer clear cannot do this. // is its LOAD_OP_CLEAR. Two callers, both of which a transfer clear cannot serve: a z
// slice of a VK_IMAGE_TYPE_3D image (vkCmdClearColorImage cannot name one), and a
// MULTISAMPLE image (which carries no TRANSFER_DST usage at all). `finalLayout` is the
// layout the caller already tracks for the whole image, so this never has to touch
// resource->layout.
Bool ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer, Bool ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel, MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
Uint32 depthSlice, const VkClearValue& clearValue); Uint32 depthSlice, const VkClearValue& clearValue,
VkImageLayout finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer, Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture); MG_State::GLState::ITextureObject& texture);
// The multisample arm of the above. Split out rather than branched inline because it
// shares none of the transfer path: a multisample image carries no TRANSFER_DST usage, so
// neither the TRANSFER_DST transition nor vkCmdClearColorImage is legal on one.
Bool MaterializeMultisamplePendingClear(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture,
VkTextureManager::TextureResource& resource,
const Vector<PendingClearEntry>& pendingClears);
Bool MaterializePendingClearForRenderbuffer( Bool MaterializePendingClearForRenderbuffer(
VkCommandBuffer commandBuffer, VkCommandBuffer commandBuffer,
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer); const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
// The default framebuffer's twin of the two above. It cannot go through
// MaterializePendingClearForTexture: the default FBO's colour attachment is a
// placeholder texture object, and syncing THAT would clear a texture image nobody
// presents instead of the acquired swapchain image.
Bool MaterializePendingClearForDefaultFramebuffer(VkCommandBuffer commandBuffer,
MG_State::GLState::FramebufferObject& fbo,
FramebufferAttachmentType attachmentType);
// Its depth/stencil half: a different image (the swapchain's depth/stencil twin), a
// different clear command and per-aspect masking.
Bool MaterializePendingDepthStencilClearForDefaultFramebuffer(
VkCommandBuffer commandBuffer, const MG_State::GLState::FramebufferAttachmentObject& attachment,
const ClearAttachmentPayload& payload);
VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry); VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry);
Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame, Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
MG_State::GLState::ITextureObject& texture, MG_State::GLState::ITextureObject& texture,
@@ -0,0 +1,63 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/SubgroupSupportPolicy.h
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Config.h>
#include <Includes.h>
namespace MobileGL::MG_Backend::DirectVulkan {
// The single decision point for how DirectVulkan implements GL_KHR_shader_subgroup,
// shared by capability advertisement (BackendObject) and module lowering
// (VulkanRenderer / ProgramFactory) so the two can never disagree.
//
// Native subgroups are the implementation whenever the device has them, whatever
// their width - subgroup operations execute on the hardware paths they were made
// for. Module-level repairs keep the GL contract intact around them:
// - FixIterationRPSubgroupScratchPass patches the one known pack bug: iterationRP's
// prefixSumCache[32], under-declared for sub-16-lane devices (8-lane lavapipe);
// - FixIterationRPBarrierPass repairs Program 203's race between two reductions
// reusing that scratch, when explicitly enabled;
// - DeriveNumSubgroupsPass replaces the one builtin drivers get wrong
// (gl_NumSubgroups) with the value the rest of the topology implies.
// The 32-lane shared-memory emulation (EmulateSubgroupsPass) is a LAST RESORT for
// devices with no subgroup support at all, and only when the user opts in with
// MOBILEGL_MAGMA_EMULATE_SUBGROUP=1; it never replaces available native operations.
inline constexpr Uint32 kEmulatedSubgroupSize = 32u;
inline constexpr Uint32 kEmulatedSubgroupStages = GL_COMPUTE_SHADER_BIT;
inline constexpr Uint32 kEmulatedSubgroupFeatures =
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_VOTE_BIT_KHR |
GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR | GL_SUBGROUP_FEATURE_BALLOT_BIT_KHR |
GL_SUBGROUP_FEATURE_SHUFFLE_BIT_KHR | GL_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT_KHR |
GL_SUBGROUP_FEATURE_CLUSTERED_BIT_KHR | GL_SUBGROUP_FEATURE_QUAD_BIT_KHR;
inline Bool ShouldEmulateSubgroups(const Bool nativeSubgroupSupported) {
return MG_Config::Features.MagmaEmulateSubgroup && !nativeSubgroupSupported &&
!MG_Config::Features.MagmaDisableSubgroup;
}
inline Bool ShouldFixIterationRPSubgroupScratch() {
// Auto is ON: the patch is fingerprint-gated to iterationRP's reduction and
// grows one under-declared array; every other module passes through untouched.
return MG_Config::Features.MagmaFixIterationRPSubgroupScratch !=
MG_Config::QuirkOverride::ForceOff;
}
inline Bool ShouldFixIterationRPBarrier() {
return MG_Config::Features.MagmaIterationRPFixBarrier;
}
inline Bool ShouldDeriveNumSubgroups() {
// Auto is ON: gl_NumSubgroups must agree with the gl_SubgroupID range for the GL
// contract to hold, and the derived ceil() value is the one the renderer can pin
// with REQUIRE_FULL_SUBGROUPS - the driver builtin is the value with no
// cross-driver guarantee (Adreno returns 1 for an 8-subgroup dispatch).
return MG_Config::Features.MagmaDeriveNumSubgroups != MG_Config::QuirkOverride::ForceOff;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -74,6 +74,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// The context line (__VA_ARGS__ = its own format string + args) must be a SEPARATE log // The context line (__VA_ARGS__ = its own format string + args) must be a SEPARATE log
// call: appending its format to the base format while its arguments precede the base // call: appending its format to the base format while its arguments precede the base
// arguments makes every conversion read the wrong slot (a %s pulling an int crashes). // arguments makes every conversion read the wrong slot (a %s pulling an int crashes).
//
// MGLOG_F and deliberately NOT latched. VK_VERIFY is the invariant-check macro: a Vulkan call
// MobileGL believes it has already made legal came back non-success, which is a
// should-never-happen state, not an expected failure mode a user hits. Those fast-fail loudly
// and keep saying so - the log-quietness rules that latch W/E cover expected failures (driver
// capability gaps, app misuse), not broken internal invariants. MOBILEGL_ASSERT below traps in
// a DEBUG build; MGLOG_F is what makes the same condition visible in an INFO test run, where
// the assert is compiled out by contract.
//
// A soft, recoverable failure must therefore NOT be routed through VK_VERIFY. Check the
// VkResult directly and report it with MGLOG_E_ONCE - see VkTextureManager::SyncTextureResource,
// where a driver legitimately refuses an image the format pre-check accepted.
#define VK_VERIFY(expr, ...) \ #define VK_VERIFY(expr, ...) \
do { \ do { \
VkResult _vk_verify_result = (expr); \ VkResult _vk_verify_result = (expr); \
+179
View File
@@ -0,0 +1,179 @@
// MobileGL - MobileGL/MG_Backend/MGPipe/PipeInputs.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The backend-side half of the PipeInputs block: the poison Fatal with its verb name, the
// name lookups the runtime knobs need, and - in a verify build - the per-field equality,
// the entry comparator and the corruption injector. Compiled only under MOBILEGL_PIPE_PUSH
// (CMakeLists.txt appends it to SOURCE_FILES there), so the pull build never sees it. Spells
// no MG_State global: everything that reads the live context lives in MG_Impl/Pipe/PipeFill.cpp.
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <cstdint>
#include <cstring>
namespace MobileGL::MG_Pipe {
const char* MGPipeVerbName(MGPipeVerb verb) {
const auto index = static_cast<SizeT>(verb);
return index < kMGPipeVerbCount ? kMGPipeVerbNames[index] : "<none>";
}
[[noreturn]] void MGPipeInputPoisonFatalForVerb(MGPipeInputField field, MGPipeVerb verb) {
MGPipeInputPoisonFatal(field, MGPipeVerbName(verb));
}
Optional<MGPipeInputField> MGPipeFindInputField(const char* name) {
if (name == nullptr) return std::nullopt;
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
if (std::strcmp(kMGPipeInputFieldNames[i], name) == 0) return static_cast<MGPipeInputField>(i);
}
return std::nullopt;
}
Optional<MGPipeVerb> MGPipeFindVerb(const char* name) {
if (name == nullptr) return std::nullopt;
for (SizeT i = 0; i < kMGPipeVerbCount; ++i) {
if (std::strcmp(kMGPipeVerbNames[i], name) == 0) return static_cast<MGPipeVerb>(i);
}
return std::nullopt;
}
#if MOBILEGL_PIPE_VERIFY
namespace {
using CurrentVertexAttributeValue = PipeInputs::CurrentVertexAttributeValue;
// Every overload is declared up front: the array overloads recurse into their element
// type, and a call inside a template only sees what was declared before the template.
template <class T>
Bool StorageEqual(const T& a, const T& b);
template <class T>
Bool StorageEqual(T* const& a, T* const& b);
template <class T>
Bool StorageEqual(const SharedPtr<T>& a, const SharedPtr<T>& b);
template <class T, SizeT N>
Bool StorageEqual(const T (&a)[N], const T (&b)[N]);
Bool StorageEqual(const PipeInputs::IndexedCapabilities& a, const PipeInputs::IndexedCapabilities& b);
Bool StorageEqual(const CurrentVertexAttributeValue& a, const CurrentVertexAttributeValue& b);
template <class T>
void CorruptStorage(T& v);
template <class T>
void CorruptStorage(T*& p);
template <class T>
void CorruptStorage(SharedPtr<T>& p);
template <class T, SizeT N>
void CorruptStorage(T (&a)[N]);
void CorruptStorage(PipeInputs::IndexedCapabilities& c);
void CorruptStorage(CurrentVertexAttributeValue& v);
// ---- equality over one field's storage ----
// O-class storage compares by identity: a raw pointer into the context, or the object a
// SharedPtr owns. Everything else goes through G4's MGPipeFieldEqual, recursing through
// C arrays element-wise.
template <class T>
Bool StorageEqual(T* const& a, T* const& b) {
return a == b;
}
template <class T>
Bool StorageEqual(const SharedPtr<T>& a, const SharedPtr<T>& b) {
return a.get() == b.get();
}
template <class T, SizeT N>
Bool StorageEqual(const T (&a)[N], const T (&b)[N]) {
for (SizeT i = 0; i < N; ++i) {
if (!StorageEqual(a[i], b[i])) return false;
}
return true;
}
Bool StorageEqual(const PipeInputs::IndexedCapabilities& a, const PipeInputs::IndexedCapabilities& b) {
return StorageEqual(a.Blend, b.Blend) && StorageEqual(a.ScissorTest, b.ScissorTest);
}
// Three scalar arrays and nothing else (Core.h), so a bitwise compare has no padding to
// false-differ on and keeps a NaN float attribute equal to itself. The size assertion is
// what turns a fourth member into a build break rather than a blind spot.
Bool StorageEqual(const CurrentVertexAttributeValue& a, const CurrentVertexAttributeValue& b) {
static_assert(sizeof(CurrentVertexAttributeValue) == 3 * 4 * 4,
"CurrentVertexAttributeValue grew a member; update the comparator");
return std::memcmp(&a, &b, sizeof(CurrentVertexAttributeValue)) == 0;
}
template <class T>
Bool StorageEqual(const T& a, const T& b) {
return MGPipeFieldEqual(a, b);
}
// ---- corruption of one field's storage ----
// Every shape is perturbed in a way the comparator above must see: a Bool flips, a
// scalar or enum moves by one, a pointer's low bits are flipped (never dereferenced:
// the snapshot is only ever compared), a SharedPtr becomes an aliasing pointer to a
// flipped address with no control block, an array corrupts its first element, and any
// other struct has its first byte XOR'ed with 0x5A.
template <class T>
T* FlipPointer(T* p) {
return reinterpret_cast<T*>(reinterpret_cast<std::uintptr_t>(p) ^ 0x5A);
}
template <class T>
void CorruptStorage(T*& p) {
p = FlipPointer(p);
}
template <class T>
void CorruptStorage(SharedPtr<T>& p) {
p = SharedPtr<T>(SharedPtr<T>(), FlipPointer(p.get()));
}
template <class T, SizeT N>
void CorruptStorage(T (&a)[N]) {
CorruptStorage(a[0]);
}
void CorruptStorage(PipeInputs::IndexedCapabilities& c) {
CorruptStorage(c.Blend);
}
void CorruptStorage(CurrentVertexAttributeValue& v) {
v.floatValue[0] += 1.f;
}
template <class T>
void CorruptStorage(T& v) {
if constexpr (std::is_same_v<T, Bool>) {
v = !v;
} else if constexpr (std::is_enum_v<T>) {
v = static_cast<T>(static_cast<std::underlying_type_t<T>>(v) + 1);
} else if constexpr (std::is_arithmetic_v<T>) {
v = static_cast<T>(v + 1);
} else {
static_assert(std::is_trivially_copyable_v<T>, "PipeInputs storage must be trivially copyable");
unsigned char first = 0;
std::memcpy(&first, &v, 1);
first ^= 0x5A;
std::memcpy(&v, &first, 1);
}
}
} // namespace
Bool MGPipeInputsFieldEqual(MGPipeInputField field, const PipeInputs& a, const PipeInputs& b) {
// A forwarded field has no storage and is equal by definition; VisitStorage answers
// false for it, hence the explicit sticky test first.
if (kMGPipeInputFieldSticky[static_cast<SizeT>(field)]) return true;
return PipeInputs::VisitStorage(field, a, b, [](const auto& x, const auto& y) { return StorageEqual(x, y); });
}
Bool MGPipeVerifyInputs(const PipeInputs& pushed, const PipeInputs& snapshot, const MGPipeFieldMask& mask,
MGPipeInputField* outField) {
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
const auto field = static_cast<MGPipeInputField>(i);
if (!MGPipeFieldMaskHas(mask, field)) continue;
if (MGPipeInputsFieldEqual(field, pushed, snapshot)) continue;
if (outField != nullptr) *outField = field;
return false;
}
return true;
}
Bool MGPipeApplyVerifyCorruption(PipeInputs& snapshot, MGPipeInputField field) {
return PipeInputs::VisitStorage(field, snapshot, snapshot, [](auto& x, auto&) {
CorruptStorage(x);
return true;
});
}
#endif // MOBILEGL_PIPE_VERIFY
} // namespace MobileGL::MG_Pipe
+721
View File
@@ -0,0 +1,721 @@
// MobileGL - MobileGL/MG_Backend/MGPipe/PipeInputs.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <MG_Pipe/MGPipe.h>
// The frontend types the accessors return. Allowed here: P13 keeps this include for the
// verify arm (ARCHITECTURE.md 9.5). This header spells no MG_State global - every read of
// the live context happens on the client side, in MG_Impl/Pipe/PipeFill.cpp.
#include <MG_State/GLState/Core.h>
// MOBILEGL_PIPE_POISON: the per-verb generation stamps and the read-side
// Fatal{UnmigratedPipeInput} check. Derived here, once. The repository's debug gate is
// MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG (Defines.h); the verify CI build is
// Release/INFO with MOBILEGL_BUILD_DISAGGREGATED=OFF, so the third arm is what arms the poison
// there without dragging MG_Remote in.
#if MOBILEGL_PIPE_PUSH && (MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG || MOBILEGL_BUILD_DISAGGREGATED || \
MOBILEGL_PIPE_VERIFY)
#define MOBILEGL_PIPE_POISON 1
#else
#define MOBILEGL_PIPE_POISON 0
#endif
namespace MobileGL::MG_Pipe {
// PipeInputs.cpp. The poison Fatal with the verb's name ("<none>" before the first
// verb): MGLOG_F + std::abort(), live at every log level on purpose - this is not
// MOBILEGL_ASSERT, which is inert in INFO builds.
[[noreturn]] void MGPipeInputPoisonFatalForVerb(MGPipeInputField field, MGPipeVerb verb);
// kMGPipeVerbNames[verb], or "<none>" for kVerbCount (no verb has been filled yet).
const char* MGPipeVerbName(MGPipeVerb verb);
// Name lookups for the runtime knobs (MOBILEGL_PIPE_VERIFY_CORRUPT names a field,
// MOBILEGL_PIPE_POISON_OMIT a Verb:Field pair). Empty on an unknown name.
Optional<MGPipeInputField> MGPipeFindInputField(const char* name);
Optional<MGPipeVerb> MGPipeFindVerb(const char* name);
// The read-side poison check, on every non-forwarded accessor. Under MOBILEGL_PIPE_POISON
// a read of a field whose stamp is older than the current verb serial is
// Fatal{UnmigratedPipeInput, "Field@Verb"}; otherwise the accessor is a plain load.
#if MOBILEGL_PIPE_POISON
#define MGP_INPUT_CHECK(Field) \
do { \
if (!::MobileGL::MG_Pipe::MGPipeInputFieldIsFresh(m_filled, (Field))) { \
::MobileGL::MG_Pipe::MGPipeInputPoisonFatalForVerb((Field), m_currentVerb); \
} \
} while (0)
#else
#define MGP_INPUT_CHECK(Field) ((void)0)
#endif
// The compare-at-read hook of the MOBILEGL_PIPE_VERIFY comparator (P1 brief D8), defined
// in MG_Impl/Pipe/PipeFill.cpp: re-reads the field from the live context and compares it
// against the stored value, and reports the FIRST divergence as
// Fatal{PipeVerifyDiffer, "Field@Verb", verb=<serial>, where=read} (the indices go in a
// preceding MGLOG_E). Only the live block (gPipeInputs) is verified; a snapshot's own
// accessors are plain loads. Off in every other build.
struct PipeInputs;
#if MOBILEGL_PIPE_VERIFY
void MGPipeVerifyReadHook(const PipeInputs& self, MGPipeInputField field, Uint index0, Uint index1);
#define MGP_INPUT_VERIFY_READ(Field, Index0, Index1) \
::MobileGL::MG_Pipe::MGPipeVerifyReadHook(*this, (Field), static_cast<Uint>(Index0), static_cast<Uint>(Index1))
#else
#define MGP_INPUT_VERIFY_READ(Field, Index0, Index1) ((void)0)
#endif
// The V/O storage of every field that has storage, by field id. The seven F-class
// (forwarded) fields have none. PipeInputs::VisitStorage dispatches on this list, which
// is what keeps the comparator and the corruption injector one function each instead of
// two sixty-way switches.
// clang-format off
#define MGP_INPUT_STORAGE_LIST(X) \
X(GetActiveTextureUnit, m_activeTextureUnit) \
X(GetBlendColor, m_blendColor) \
X(GetBlendEquationIndexed, m_blendEquation) \
X(GetBlendFuncIndexed, m_blendFunc) \
X(GetBoundTransformFeedbackName, m_boundTransformFeedbackName) \
X(GetBoundVertexArray, m_boundVertexArray) \
X(GetBufferBindingSlot, m_bufferBindingSlot) \
X(GetBufferBindingPoint, m_bufferBindingPointBase) \
X(GetTouchedBufferBindingPointCount, m_touchedBindingPointCount) \
X(GetClampReadColor, m_clampReadColor) \
X(GetClearColor, m_clearColor) \
X(GetClearDepth, m_clearDepth) \
X(GetClearStencil, m_clearStencil) \
X(GetColorMaskIndexed, m_colorMask) \
X(GetCullFaceMode, m_cullFaceMode) \
X(GetCurrentVertexAttribute, m_currentVertexAttribute) \
X(GetDepthFunc, m_depthFunc) \
X(GetDepthMask, m_depthMask) \
X(GetDepthRangeIndexed, m_depthRange) \
X(GetFramebufferBindingSlot, m_framebufferBindingSlot) \
X(GetImageTextureBinding, m_imageTextureBindingBase) \
X(GetLineWidth, m_lineWidth) \
X(GetLogicOp, m_logicOp) \
X(GetMaxTouchedTextureUnit, m_maxTouchedTextureUnit) \
X(GetMinSampleShadingValue, m_minSampleShadingValue) \
X(GetPatchDefaultInnerLevel, m_patchDefaultInnerLevel) \
X(GetPatchDefaultOuterLevel, m_patchDefaultOuterLevel) \
X(GetPatchVertices, m_patchVertices) \
X(GetPipelineStateVersion, m_pipelineStateVersion) \
X(GetPixelStoreParameters, m_pixelStore) \
X(GetPolygonModeFront, m_polygonModeFront) \
X(GetPolygonOffsetFactor, m_polygonOffsetFactor) \
X(GetPolygonOffsetUnits, m_polygonOffsetUnits) \
X(GetPrimitiveRestartIndex, m_primitiveRestartIndex) \
X(GetProgramForDispatch, m_programForDispatch) \
X(GetProgramForDraw, m_programForDraw) \
X(GetProvokingVertexMode, m_provokingVertexMode) \
X(GetRenderStateParameters, m_renderState) \
X(GetRenderStateParametersVersion, m_renderStateParametersVersion) \
X(GetSamplingResolutionGeneration, m_samplingResolutionGeneration) \
X(GetScissorBox, m_scissorBox) \
X(GetStencilState, m_stencil) \
X(GetTextureBindGeneration, m_textureBindGeneration) \
X(GetTextureContextId, m_textureContextId) \
X(GetTextureUnitObject, m_textureUnitBase) \
X(GetTransformFeedbackCapturedVertices, m_transformFeedbackCapturedVertices) \
X(GetTransformFeedbackGeneration, m_transformFeedbackGeneration) \
X(GetTransformFeedbackPausedPrimitiveCounter, m_transformFeedbackPausedPrimitiveCounter) \
X(GetTransformFeedbackProgram, m_transformFeedbackProgram) \
X(GetViewport, m_viewport) \
X(GetViewportIndexed, m_viewportIndexed) \
X(IsCapabilityEnabled, m_capability) \
X(IsCapabilityEnabledIndexed, m_capabilityIndexed) \
X(IsTransformFeedbackActive, m_transformFeedbackActive) \
X(IsTransformFeedbackPaused, m_transformFeedbackPaused) \
X(GetBoundTransformFeedbackLifetimeId, m_boundTransformFeedbackLifetimeId)
// clang-format on
// The seven F-class fields, for the arithmetic below and for the sticky table's proof.
// The forwarded set IS the sticky set (PipeFields.def marks the same seven rows F and
// sticky), so an eighth sticky row without a forwarder is refused here, not by a test.
inline constexpr SizeT kMGPipeForwardedFieldCount = 7;
static_assert(kMGPipeForwardedFieldCount == kMGPipeInputStickyFieldCount,
"the forwarded (F-class) fields and the sticky fields of PipeFields.def are the same seven rows");
// The block the backends read instead of GLContext (ARCHITECTURE.md 9.2 phase A, P1 brief
// D4). One struct, three storage classes, and every accessor keeps the NAME, PARAMETERS
// and RETURN TYPE of its GLContext counterpart (MG_State/GLState/Core.h) so the strangler
// sed is type-neutral:
//
// V (value) copied out of GLContext at fill time by calling the same accessor;
// no derivation logic is re-implemented here, which is what keeps the
// copy semantically identical by construction.
// O (object reference) a SharedPtr copy, or a raw pointer to the live GLContext-owned
// slot/array for the accessors that return a non-const reference into
// the context. Identity is what phase C turns into a handle.
// F (forwarded) argument-keyed lookups and reverse-channel calls, defined out of
// line in MG_Impl/Pipe/PipeFill.cpp (the client side, where the live
// context may be spelled). Sticky: stamped once by the first fill that
// sees a live context.
//
// Every non-forwarded accessor is MGP_INPUT_CHECK (poison) -> MGP_INPUT_VERIFY_READ
// (compare-at-read) -> the storage. Both macros expand to nothing when their switch is
// off, so a plain MOBILEGL_PIPE_PUSH build's accessor is a load.
struct PipeInputs {
using GLContext = MG_State::GLState::GLContext;
using BufferObject = MG_State::GLState::BufferObject;
using BufferTarget = ::MobileGL::BufferTarget;
using FramebufferObject = MG_State::GLState::FramebufferObject;
using FramebufferTarget = ::MobileGL::FramebufferTarget;
using VertexArrayObject = MG_State::GLState::VertexArrayObject;
using ProgramObject = MG_State::GLState::ProgramObject;
using ITextureObject = MG_State::GLState::ITextureObject;
using TextureUnit = MG_State::GLState::TextureUnit;
using ImageTextureBinding = MG_State::GLState::ImageTextureBinding;
using CurrentVertexAttributeValue = MG_State::GLState::CurrentVertexAttributeValue;
static constexpr SizeT kBufferTargetCount = static_cast<SizeT>(BufferTarget::BufferTargetCount);
static constexpr SizeT kFramebufferTargetCount = static_cast<SizeT>(FramebufferTarget::FramebufferTargetCount);
static constexpr SizeT kCapabilityCount = static_cast<SizeT>(CapabilityInput::CapabilityInputCount);
static constexpr SizeT kMaxViewports = RenderStateParameters::MAX_VIEWPORTS;
static constexpr SizeT kMaxVertexAttribs = VertexArrayObject::MAX_VERTEX_ATTRIBS;
static constexpr SizeT kStencilFaceCount = static_cast<SizeT>(StencilFace::StencilFaceCount);
// IsCapabilityEnabledIndexed's two indexed capabilities, the only ones GLContext keeps
// indexed state for (RenderState::IsCapabilityEnabledIndexed).
struct IndexedCapabilities {
Bool Blend[kMGMaxDrawBuffers];
Bool ScissorTest[kMaxViewports];
};
// ---- identity / liveness (not fields) ----
// Whether a live GLContext exists. Forwarded (PipeFill.cpp): under push MGB_CTX_LIVE
// must be true as soon as a context exists, fill or no fill, which is what today's
// null-context guards test.
Bool IsLive() const;
// The live GLContext's address at the last fill; serves MGB_CTX_IDENTITY.
const void* ContextIdentity() const { return m_contextIdentity; }
// The verb of the last fill, kVerbCount before the first one.
MGPipeVerb CurrentVerb() const { return m_currentVerb; }
#if MOBILEGL_PIPE_POISON
const MGPipeFilledState& FilledState() const { return m_filled; }
#endif
// ---- V: values ----
Int GetActiveTextureUnit() const {
MGP_INPUT_CHECK(MGPipeInputField::GetActiveTextureUnit);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetActiveTextureUnit, 0, 0);
return m_activeTextureUnit;
}
const FloatVec4& GetBlendColor() const {
MGP_INPUT_CHECK(MGPipeInputField::GetBlendColor);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBlendColor, 0, 0);
return m_blendColor;
}
void GetBlendEquationIndexed(Uint index, BlendEquation& color, BlendEquation& alpha) const {
MGP_INPUT_CHECK(MGPipeInputField::GetBlendEquationIndexed);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBlendEquationIndexed, index, 0);
if (index >= kMGMaxDrawBuffers) {
MOBILEGL_ASSERT(false, "Blend equation index out of range: %u", index);
return;
}
color = m_blendEquation[index][0];
alpha = m_blendEquation[index][1];
}
void GetBlendFuncIndexed(Uint index, BlendFactor& srcRGB, BlendFactor& dstRGB, BlendFactor& srcAlpha,
BlendFactor& dstAlpha) const {
MGP_INPUT_CHECK(MGPipeInputField::GetBlendFuncIndexed);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBlendFuncIndexed, index, 0);
if (index >= kMGMaxDrawBuffers) {
MOBILEGL_ASSERT(false, "Blend func index out of range: %u", index);
return;
}
srcRGB = m_blendFunc[index][0];
dstRGB = m_blendFunc[index][1];
srcAlpha = m_blendFunc[index][2];
dstAlpha = m_blendFunc[index][3];
}
// Dead field: filled, read by no backend since the D21 XFB counter-slot rekey; kept so
// the vendored inventory row keeps its mapping (Coverage.def).
Uint GetBoundTransformFeedbackName() const {
MGP_INPUT_CHECK(MGPipeInputField::GetBoundTransformFeedbackName);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBoundTransformFeedbackName, 0, 0);
return m_boundTransformFeedbackName;
}
SizeT GetTouchedBufferBindingPointCount(BufferTarget target) const {
MGP_INPUT_CHECK(MGPipeInputField::GetTouchedBufferBindingPointCount);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTouchedBufferBindingPointCount, static_cast<Uint>(target), 0);
return m_touchedBindingPointCount[static_cast<SizeT>(target)];
}
GLenum GetClampReadColor() const {
MGP_INPUT_CHECK(MGPipeInputField::GetClampReadColor);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetClampReadColor, 0, 0);
return m_clampReadColor;
}
const FloatVec4& GetClearColor() const {
MGP_INPUT_CHECK(MGPipeInputField::GetClearColor);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetClearColor, 0, 0);
return m_clearColor;
}
Float GetClearDepth() const {
MGP_INPUT_CHECK(MGPipeInputField::GetClearDepth);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetClearDepth, 0, 0);
return m_clearDepth;
}
Uint32 GetClearStencil() const {
MGP_INPUT_CHECK(MGPipeInputField::GetClearStencil);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetClearStencil, 0, 0);
return m_clearStencil;
}
BoolVec4 GetColorMaskIndexed(Uint index) const {
MGP_INPUT_CHECK(MGPipeInputField::GetColorMaskIndexed);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetColorMaskIndexed, index, 0);
return m_colorMask[index];
}
CullFaceMode GetCullFaceMode() const {
MGP_INPUT_CHECK(MGPipeInputField::GetCullFaceMode);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetCullFaceMode, 0, 0);
return m_cullFaceMode;
}
const CurrentVertexAttributeValue& GetCurrentVertexAttribute(Uint index) const {
MGP_INPUT_CHECK(MGPipeInputField::GetCurrentVertexAttribute);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetCurrentVertexAttribute, index, 0);
if (index >= kMaxVertexAttribs) {
static const CurrentVertexAttributeValue defaultValue{};
MGLOG_E_ONCE("PipeInputs::GetCurrentVertexAttribute: index %u is out of range", index);
return defaultValue;
}
return m_currentVertexAttribute[index];
}
DepthTestFunc GetDepthFunc() const {
MGP_INPUT_CHECK(MGPipeInputField::GetDepthFunc);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetDepthFunc, 0, 0);
return m_depthFunc;
}
Bool GetDepthMask() const {
MGP_INPUT_CHECK(MGPipeInputField::GetDepthMask);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetDepthMask, 0, 0);
return m_depthMask;
}
const FloatVec2& GetDepthRangeIndexed(Uint index) const {
MGP_INPUT_CHECK(MGPipeInputField::GetDepthRangeIndexed);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetDepthRangeIndexed, index, 0);
if (index >= kMaxViewports) {
MOBILEGL_ASSERT(false, "Depth range index out of range: %u", index);
return m_depthRange[0];
}
return m_depthRange[index];
}
Float GetLineWidth() const {
MGP_INPUT_CHECK(MGPipeInputField::GetLineWidth);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetLineWidth, 0, 0);
return m_lineWidth;
}
LogicOperation GetLogicOp() const {
MGP_INPUT_CHECK(MGPipeInputField::GetLogicOp);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetLogicOp, 0, 0);
return m_logicOp;
}
Int GetMaxTouchedTextureUnit() const {
MGP_INPUT_CHECK(MGPipeInputField::GetMaxTouchedTextureUnit);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetMaxTouchedTextureUnit, 0, 0);
return m_maxTouchedTextureUnit;
}
Float GetMinSampleShadingValue() const {
MGP_INPUT_CHECK(MGPipeInputField::GetMinSampleShadingValue);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetMinSampleShadingValue, 0, 0);
return m_minSampleShadingValue;
}
const FloatVec2& GetPatchDefaultInnerLevel() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPatchDefaultInnerLevel);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPatchDefaultInnerLevel, 0, 0);
return m_patchDefaultInnerLevel;
}
const FloatVec4& GetPatchDefaultOuterLevel() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPatchDefaultOuterLevel);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPatchDefaultOuterLevel, 0, 0);
return m_patchDefaultOuterLevel;
}
Uint GetPatchVertices() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPatchVertices);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPatchVertices, 0, 0);
return m_patchVertices;
}
Uint GetPipelineStateVersion() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPipelineStateVersion);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPipelineStateVersion, 0, 0);
return m_pipelineStateVersion;
}
Uint GetRenderStateParametersVersion() const {
MGP_INPUT_CHECK(MGPipeInputField::GetRenderStateParametersVersion);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetRenderStateParametersVersion, 0, 0);
return m_renderStateParametersVersion;
}
PixelStoreParameters GetPixelStoreParameters(Bool isUnpack) const {
MGP_INPUT_CHECK(MGPipeInputField::GetPixelStoreParameters);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPixelStoreParameters, isUnpack ? 1u : 0u, 0);
return m_pixelStore[isUnpack ? 1 : 0];
}
GLenum GetPolygonModeFront() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPolygonModeFront);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPolygonModeFront, 0, 0);
return m_polygonModeFront;
}
Float GetPolygonOffsetFactor() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPolygonOffsetFactor);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPolygonOffsetFactor, 0, 0);
return m_polygonOffsetFactor;
}
Float GetPolygonOffsetUnits() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPolygonOffsetUnits);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPolygonOffsetUnits, 0, 0);
return m_polygonOffsetUnits;
}
Uint32 GetPrimitiveRestartIndex() const {
MGP_INPUT_CHECK(MGPipeInputField::GetPrimitiveRestartIndex);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPrimitiveRestartIndex, 0, 0);
return m_primitiveRestartIndex;
}
ProvokingVertexMode GetProvokingVertexMode() const {
MGP_INPUT_CHECK(MGPipeInputField::GetProvokingVertexMode);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetProvokingVertexMode, 0, 0);
return m_provokingVertexMode;
}
const RenderStateParameters& GetRenderStateParameters() const {
MGP_INPUT_CHECK(MGPipeInputField::GetRenderStateParameters);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetRenderStateParameters, 0, 0);
return m_renderState;
}
Uint64 GetSamplingResolutionGeneration() const {
MGP_INPUT_CHECK(MGPipeInputField::GetSamplingResolutionGeneration);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetSamplingResolutionGeneration, 0, 0);
return m_samplingResolutionGeneration;
}
const IntVec4& GetScissorBox() const {
MGP_INPUT_CHECK(MGPipeInputField::GetScissorBox);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetScissorBox, 0, 0);
return m_scissorBox;
}
const StencilFaceState& GetStencilState(StencilFace face) const {
MGP_INPUT_CHECK(MGPipeInputField::GetStencilState);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetStencilState, static_cast<Uint>(face), 0);
return m_stencil[face == StencilFace::Back ? 1 : 0];
}
Uint64 GetTextureBindGeneration() const {
MGP_INPUT_CHECK(MGPipeInputField::GetTextureBindGeneration);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTextureBindGeneration, 0, 0);
return m_textureBindGeneration;
}
Uint64 GetTextureContextId() const {
MGP_INPUT_CHECK(MGPipeInputField::GetTextureContextId);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTextureContextId, 0, 0);
return m_textureContextId;
}
Uint64 GetTransformFeedbackCapturedVertices() const {
MGP_INPUT_CHECK(MGPipeInputField::GetTransformFeedbackCapturedVertices);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTransformFeedbackCapturedVertices, 0, 0);
return m_transformFeedbackCapturedVertices;
}
Uint64 GetTransformFeedbackGeneration() const {
MGP_INPUT_CHECK(MGPipeInputField::GetTransformFeedbackGeneration);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTransformFeedbackGeneration, 0, 0);
return m_transformFeedbackGeneration;
}
Uint64 GetTransformFeedbackPausedPrimitiveCounter() const {
MGP_INPUT_CHECK(MGPipeInputField::GetTransformFeedbackPausedPrimitiveCounter);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTransformFeedbackPausedPrimitiveCounter, 0, 0);
return m_transformFeedbackPausedPrimitiveCounter;
}
Uint64 GetBoundTransformFeedbackLifetimeId() const {
MGP_INPUT_CHECK(MGPipeInputField::GetBoundTransformFeedbackLifetimeId);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBoundTransformFeedbackLifetimeId, 0, 0);
return m_boundTransformFeedbackLifetimeId;
}
IntVec4 GetViewport() const {
MGP_INPUT_CHECK(MGPipeInputField::GetViewport);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetViewport, 0, 0);
return m_viewport;
}
const FloatVec4& GetViewportIndexed(Uint index) const {
MGP_INPUT_CHECK(MGPipeInputField::GetViewportIndexed);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetViewportIndexed, index, 0);
if (index >= kMaxViewports) {
MOBILEGL_ASSERT(false, "Viewport index out of range: %u", index);
return m_viewportIndexed[0];
}
return m_viewportIndexed[index];
}
Bool IsCapabilityEnabled(CapabilityInput cap) const {
MGP_INPUT_CHECK(MGPipeInputField::IsCapabilityEnabled);
MGP_INPUT_VERIFY_READ(MGPipeInputField::IsCapabilityEnabled, static_cast<Uint>(cap), 0);
const auto index = static_cast<SizeT>(cap);
return index < kCapabilityCount ? m_capability[index] : false;
}
// Blend and ScissorTest are the only indexed capabilities GLContext keeps; no backend
// asks for another (VulkanRenderer asks Blend). Any other cap is a read the fill cannot
// have served: Fatal{UnmigratedPipeInput} naming the field and the verb, the cap in a
// preceding MGLOG_E.
Bool IsCapabilityEnabledIndexed(CapabilityInput cap, Uint index) const {
MGP_INPUT_CHECK(MGPipeInputField::IsCapabilityEnabledIndexed);
MGP_INPUT_VERIFY_READ(MGPipeInputField::IsCapabilityEnabledIndexed, static_cast<Uint>(cap), index);
if (cap == CapabilityInput::Blend) {
return index < kMGMaxDrawBuffers ? m_capabilityIndexed.Blend[index] : false;
}
if (cap == CapabilityInput::ScissorTest) {
return index < kMaxViewports ? m_capabilityIndexed.ScissorTest[index] : false;
}
MGLOG_E("PipeInputs::IsCapabilityEnabledIndexed: no indexed storage for cap=%d (index=%u)",
static_cast<int>(cap), index);
MGPipeInputPoisonFatalForVerb(MGPipeInputField::IsCapabilityEnabledIndexed, m_currentVerb);
}
Bool IsTransformFeedbackActive() const {
MGP_INPUT_CHECK(MGPipeInputField::IsTransformFeedbackActive);
MGP_INPUT_VERIFY_READ(MGPipeInputField::IsTransformFeedbackActive, 0, 0);
return m_transformFeedbackActive;
}
Bool IsTransformFeedbackPaused() const {
MGP_INPUT_CHECK(MGPipeInputField::IsTransformFeedbackPaused);
MGP_INPUT_VERIFY_READ(MGPipeInputField::IsTransformFeedbackPaused, 0, 0);
return m_transformFeedbackPaused;
}
// ---- O: object references ----
const SharedPtr<VertexArrayObject>& GetBoundVertexArray() {
MGP_INPUT_CHECK(MGPipeInputField::GetBoundVertexArray);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBoundVertexArray, 0, 0);
return m_boundVertexArray;
}
// A target the fill left null (one outside GlobalBufferTargets / BufferBindPointTargets,
// or a read before any fill) is a read the fill cannot have served: the poison Fatal,
// the target in a preceding MGLOG_E.
BindingSlot<BufferObject>& GetBufferBindingSlot(BufferTarget target) {
MGP_INPUT_CHECK(MGPipeInputField::GetBufferBindingSlot);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBufferBindingSlot, static_cast<Uint>(target), 0);
const auto index = static_cast<SizeT>(target);
if (index >= kBufferTargetCount || m_bufferBindingSlot[index] == nullptr) {
MGLOG_E("PipeInputs::GetBufferBindingSlot: no slot for target=%d", static_cast<int>(target));
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetBufferBindingSlot, m_currentVerb);
}
return *m_bufferBindingSlot[index];
}
BindingSlotRange1D<BufferObject>& GetBufferBindingPoint(BufferTarget target, Uint index) {
MGP_INPUT_CHECK(MGPipeInputField::GetBufferBindingPoint);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetBufferBindingPoint, static_cast<Uint>(target), index);
const auto targetIndex = static_cast<SizeT>(target);
if (targetIndex >= kBufferTargetCount || m_bufferBindingPointBase[targetIndex] == nullptr) {
MGLOG_E("PipeInputs::GetBufferBindingPoint: no binding points for target=%d (index=%u)",
static_cast<int>(target), index);
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetBufferBindingPoint, m_currentVerb);
}
// The live storage is Array<Array<BindingSlotRange1D, BufferBindingPointCount>, N>
// (BufferState.h), so base[index] is the live slot GLContext would hand out.
return m_bufferBindingPointBase[targetIndex][index];
}
BindingSlot<FramebufferObject>& GetFramebufferBindingSlot(FramebufferTarget target) {
MGP_INPUT_CHECK(MGPipeInputField::GetFramebufferBindingSlot);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetFramebufferBindingSlot, static_cast<Uint>(target), 0);
const auto index = static_cast<SizeT>(target);
if (index >= kFramebufferTargetCount || m_framebufferBindingSlot[index] == nullptr) {
MGLOG_E("PipeInputs::GetFramebufferBindingSlot: no slot for target=%d", static_cast<int>(target));
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetFramebufferBindingSlot, m_currentVerb);
}
return *m_framebufferBindingSlot[index];
}
ImageTextureBinding& GetImageTextureBinding(Int unit) {
MGP_INPUT_CHECK(MGPipeInputField::GetImageTextureBinding);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetImageTextureBinding, static_cast<Uint>(unit), 0);
if (m_imageTextureBindingBase == nullptr) {
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetImageTextureBinding, m_currentVerb);
}
return m_imageTextureBindingBase[unit];
}
const ImageTextureBinding& GetImageTextureBinding(Int unit) const {
MGP_INPUT_CHECK(MGPipeInputField::GetImageTextureBinding);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetImageTextureBinding, static_cast<Uint>(unit), 0);
if (m_imageTextureBindingBase == nullptr) {
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetImageTextureBinding, m_currentVerb);
}
return m_imageTextureBindingBase[unit];
}
const SharedPtr<ProgramObject>& GetProgramForDispatch() {
MGP_INPUT_CHECK(MGPipeInputField::GetProgramForDispatch);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetProgramForDispatch, 0, 0);
return m_programForDispatch;
}
const SharedPtr<ProgramObject>& GetProgramForDraw() {
MGP_INPUT_CHECK(MGPipeInputField::GetProgramForDraw);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetProgramForDraw, 0, 0);
return m_programForDraw;
}
const SharedPtr<ProgramObject>& GetTransformFeedbackProgram() const {
MGP_INPUT_CHECK(MGPipeInputField::GetTransformFeedbackProgram);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTransformFeedbackProgram, 0, 0);
return m_transformFeedbackProgram;
}
TextureUnit& GetTextureUnitObject(Int unit) {
MGP_INPUT_CHECK(MGPipeInputField::GetTextureUnitObject);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTextureUnitObject, static_cast<Uint>(unit), 0);
if (m_textureUnitBase == nullptr) {
MGPipeInputPoisonFatalForVerb(MGPipeInputField::GetTextureUnitObject, m_currentVerb);
}
return m_textureUnitBase[unit];
}
// ---- F: forwarded to the live context (MG_Impl/Pipe/PipeFill.cpp); sticky ----
// Each takes an argument that is not verb state - a GL name, a lifetime id, a target -
// i.e. it is a lookup or a reverse-channel write, not a state read; there is no value
// the filler could copy and no verb whose fill could make it stale. Phase C replaces
// them with handle tables and callbacks.
// They carry no MGP_INPUT_CHECK / MGP_INPUT_VERIFY_READ (the declared exception to
// P1 brief D4's "every accessor body"): a forward is a live call, not a stored value,
// and InvalidateCompileEnv is reached from backend initialisation before any verb has
// filled, where a check would be Fatal{...@<none>} on every start. Their sticky stamp
// is therefore consulted by no accessor; the tests pin it through
// MGPipeInputFieldIsFresh directly.
SizeT GetBufferBindingPointCount(BufferTarget target) const;
const SharedPtr<ProgramObject>& GetProgramObject(Uint index);
const SharedPtr<ITextureObject>& GetTextureObject(Uint index);
Bool HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const;
void InvalidateCompileEnv();
Bool ValidateProgramName(Uint index) const;
// Dropped with an MGLOG_E_ONCE when no context is live; today's guarded sites never
// reach it without one.
void RecordError(ErrorCode code, UniquePtr<ErrorInfo> info);
// ---- the storage visitor ----
// Calls fn(a.<member>, b.<member>) for the field's storage and returns its result; returns
// false without calling fn for a forwarded field, which has none. The comparator's
// per-field equality and the verify corruption injector are both one call of this.
template <class Fn>
static Bool VisitStorage(MGPipeInputField field, PipeInputs& a, PipeInputs& b, Fn&& fn) {
switch (field) {
#define MGP_INPUT_VISIT(Field, Member) \
case MGPipeInputField::Field: \
return fn(a.Member, b.Member);
MGP_INPUT_STORAGE_LIST(MGP_INPUT_VISIT)
#undef MGP_INPUT_VISIT
default:
return false;
}
}
template <class Fn>
static Bool VisitStorage(MGPipeInputField field, const PipeInputs& a, const PipeInputs& b, Fn&& fn) {
switch (field) {
#define MGP_INPUT_VISIT(Field, Member) \
case MGPipeInputField::Field: \
return fn(a.Member, b.Member);
MGP_INPUT_STORAGE_LIST(MGP_INPUT_VISIT)
#undef MGP_INPUT_VISIT
default:
return false;
}
}
private:
// The one door into the storage from the client side (MG_Impl/Pipe/PipeFill.cpp):
// the filler's per-field copies and stamps, and the verify snapshot.
friend struct MGPipeFillAccess;
// The other door, and the one that exists because of what this block IS after P2:
// the server's working RenderStateParameters. MG_Pipe/PipeApply.cpp scatters
// bind_render_state's and set_dynamic_state's chunks straight into m_renderState,
// which is why DirectGLES' SyncRenderState is not one line changed. It deliberately
// does NOT stamp the poison generations - a stamp says "the filler published this
// for THIS verb", which is the walk's statement, not the applier's.
friend struct MGPipeApplyAccess;
// ---- identity ----
const void* m_contextIdentity = nullptr;
Bool m_live = false;
MGPipeVerb m_currentVerb = MGPipeVerb::kVerbCount;
#if MOBILEGL_PIPE_POISON
MGPipeFilledState m_filled{};
#endif
// ---- V ----
Int m_activeTextureUnit = 0;
FloatVec4 m_blendColor{};
BlendEquation m_blendEquation[kMGMaxDrawBuffers][2]{};
BlendFactor m_blendFunc[kMGMaxDrawBuffers][4]{};
Uint m_boundTransformFeedbackName = 0;
SizeT m_touchedBindingPointCount[kBufferTargetCount]{};
GLenum m_clampReadColor = 0;
FloatVec4 m_clearColor{};
Float m_clearDepth = 0.f;
Uint32 m_clearStencil = 0;
BoolVec4 m_colorMask[kMGMaxDrawBuffers]{};
CullFaceMode m_cullFaceMode{};
CurrentVertexAttributeValue m_currentVertexAttribute[kMaxVertexAttribs]{};
DepthTestFunc m_depthFunc{};
Bool m_depthMask = false;
FloatVec2 m_depthRange[kMaxViewports]{};
Float m_lineWidth = 0.f;
LogicOperation m_logicOp{};
Int m_maxTouchedTextureUnit = -1;
Float m_minSampleShadingValue = 0.f;
FloatVec2 m_patchDefaultInnerLevel{};
FloatVec4 m_patchDefaultOuterLevel{};
Uint m_patchVertices = 0;
Uint m_pipelineStateVersion = 0;
Uint m_renderStateParametersVersion = 0;
PixelStoreParameters m_pixelStore[2]{}; // [0] = pack, [1] = unpack
GLenum m_polygonModeFront = 0;
Float m_polygonOffsetFactor = 0.f;
Float m_polygonOffsetUnits = 0.f;
Uint32 m_primitiveRestartIndex = 0;
ProvokingVertexMode m_provokingVertexMode{};
RenderStateParameters m_renderState{};
Uint64 m_samplingResolutionGeneration = 0;
Uint64 m_textureBindGeneration = 0;
Uint64 m_textureContextId = 0;
IntVec4 m_scissorBox{};
StencilFaceState m_stencil[kStencilFaceCount]{};
Uint64 m_transformFeedbackCapturedVertices = 0;
Uint64 m_transformFeedbackGeneration = 0;
Uint64 m_transformFeedbackPausedPrimitiveCounter = 0;
Uint64 m_boundTransformFeedbackLifetimeId = 0;
IntVec4 m_viewport{};
FloatVec4 m_viewportIndexed[kMaxViewports]{};
Bool m_capability[kCapabilityCount]{};
IndexedCapabilities m_capabilityIndexed{};
Bool m_transformFeedbackActive = false;
Bool m_transformFeedbackPaused = false;
// ---- O ----
SharedPtr<VertexArrayObject> m_boundVertexArray;
BindingSlot<BufferObject>* m_bufferBindingSlot[kBufferTargetCount]{};
BindingSlotRange1D<BufferObject>* m_bufferBindingPointBase[kBufferTargetCount]{};
BindingSlot<FramebufferObject>* m_framebufferBindingSlot[kFramebufferTargetCount]{};
ImageTextureBinding* m_imageTextureBindingBase = nullptr;
SharedPtr<ProgramObject> m_programForDispatch;
SharedPtr<ProgramObject> m_programForDraw;
SharedPtr<ProgramObject> m_transformFeedbackProgram;
TextureUnit* m_textureUnitBase = nullptr;
};
// The single global the backends read through MGB_CTX (ARCHITECTURE.md 9.2). An inline
// variable: no .cpp is needed for the definition.
inline PipeInputs gPipeInputs{};
// Every field has storage or is forwarded, and nothing else.
#define MGP_INPUT_COUNT_ONE(Field, Member) +1
static_assert(0 MGP_INPUT_STORAGE_LIST(MGP_INPUT_COUNT_ONE) + kMGPipeForwardedFieldCount == kMGPipeInputFieldCount,
"MGP_INPUT_STORAGE_LIST plus the seven forwarded fields is not the PipeInputs field set");
#undef MGP_INPUT_COUNT_ONE
// The docs budget ~20 KB; the block is a few KB.
static_assert(sizeof(PipeInputs) < 20 * 1024, "PipeInputs outgrew its budget");
#if MOBILEGL_PIPE_VERIFY
// PipeInputs.cpp. Per-field equality for the entry compare (P1 brief D8): V by value
// through G4's MGPipeFieldEqual (bitwise floats, field-wise structs), O by identity, F
// always equal (no storage).
Bool MGPipeInputsFieldEqual(MGPipeInputField field, const PipeInputs& a, const PipeInputs& b);
// PipeInputs.cpp. The entry compare: every field in `mask` of the pushed block against the
// snapshot, first differing field out. Exported from the shared library on purpose - the
// retrace-verify CI job proves it swapped in a verify build by finding this symbol with
// nm -D, so a "green" run against a library without the comparator cannot happen.
#if defined(__GNUC__) || defined(__clang__)
__attribute__((visibility("default")))
#endif
Bool MGPipeVerifyInputs(const PipeInputs& pushed, const PipeInputs& snapshot, const MGPipeFieldMask& mask,
MGPipeInputField* outField);
// PipeInputs.cpp. Negative control A: perturbs one field's storage (flip a Bool, +1 a
// scalar, ^0x5A the first byte of a struct, flip a pointer's low bits - never
// dereferenced, the snapshot is only ever compared). Returns false for a forwarded field,
// which has nothing to corrupt.
Bool MGPipeApplyVerifyCorruption(PipeInputs& snapshot, MGPipeInputField field);
#endif
} // namespace MobileGL::MG_Pipe
+4 -1
View File
@@ -42,4 +42,7 @@ set_tests_properties(SanityBench PROPERTIES LABELS benchmark)
add_subdirectory(Program) add_subdirectory(Program)
add_subdirectory(Buffer) add_subdirectory(Buffer)
add_subdirectory(Driver) add_subdirectory(Driver)
add_subdirectory(Container)
add_subdirectory(ShaderCache)
add_subdirectory(Transpile)
@@ -0,0 +1,20 @@
cmake_minimum_required(VERSION 3.24)
add_executable(
UnorderedMapBench
UnorderedMapBench.cpp
)
target_include_directories(UnorderedMapBench PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
UnorderedMapBench PRIVATE
benchmark::benchmark
${LINK_LIBRARIES}
)
add_test(NAME UnorderedMapBench COMMAND UnorderedMapBench --benchmark_counters_tabular=true)
set_tests_properties(UnorderedMapBench PROPERTIES LABELS benchmark)
@@ -0,0 +1,248 @@
// MobileGL - MobileGL/MG_Benchmark/Container/UnorderedMapBench.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// The standing performance observatory for MobileGL::UnorderedMap.
//
// This benchmarks the ALIAS, never a concrete table, so whatever UnorderedMap
// names today is what gets measured - swap the container in MG_Util/Types.h and
// re-run this same binary to get a directly comparable set of numbers. That is
// the point of it: the container sits on per-draw paths, so a change to it needs
// evidence, and the evidence should be produced the same way every time.
//
// The workloads are the shapes the tree actually exercises, not generic hash-map
// microbenchmarks. Four key shapes, because they stress a hash function very
// differently:
// * SEQUENTIAL dense small integers - GL object names from the index generator
// (buffer/texture/framebuffer/sampler registries).
// * POINTER real heap addresses - StateBackendObjectRegistry keys on
// StateObject*. These are aligned, so their low bits are the
// least random part of the key; a table that indexes on raw low
// bits clusters badly here and one that mixes first does not.
// Taken from the real allocator rather than a synthetic stride,
// which would flatter whichever table mixes its bits.
// * DIGEST already well-mixed 64-bit values - the XXH64 pipeline,
// vertex-input-state and program memos.
// * NAME short strings - uniform/attribute name to location maps.
//
// Sizes sweep from 8 upward because the per-draw memos are usually SMALL; a table
// that only wins at 4096 entries has not won anything that matters here.
//
// Run: build-linux/MobileGL/MG_Benchmark/Container/UnorderedMapBench
// or: ctest -R UnorderedMapBench (label: benchmark)
#include <cstdint>
#include <memory>
#include <random>
#include <string>
#include <vector>
#include <benchmark/benchmark.h>
#include "MG_Util/Types.h"
using namespace MobileGL;
namespace {
constexpr Int64 kMinSize = 8;
constexpr Int64 kMaxSize = 4096;
// Keep the real allocations alive for the whole process: the POINTER shape is
// only honest if the keys are addresses the allocator actually handed out, and
// they have to stay unique (a freed address can be handed out twice).
std::vector<std::unique_ptr<char[]>>& PointerKeyStorage() {
static std::vector<std::unique_ptr<char[]>> storage;
return storage;
}
Vector<Uint64> SequentialKeys(SizeT n) {
Vector<Uint64> keys;
keys.reserve(n);
for (SizeT i = 0; i < n; ++i) keys.push_back(static_cast<Uint64>(i) + 1);
return keys;
}
Vector<Uint64> PointerKeys(SizeT n) {
auto& storage = PointerKeyStorage();
Vector<Uint64> keys;
keys.reserve(n);
std::mt19937_64 rng(0xBEEF);
std::vector<std::unique_ptr<char[]>> churn;
for (SizeT i = 0; i < n; ++i) {
// State objects are not all one size, and the allocator sees other
// traffic between them - a single uniform stride is not what this
// registry ever sees.
const SizeT sz = 96 + (rng() % 192);
auto p = std::make_unique<char[]>(sz);
keys.push_back(reinterpret_cast<Uint64>(p.get()));
storage.push_back(std::move(p));
if ((rng() & 3) == 0) churn.push_back(std::make_unique<char[]>(32 + (rng() % 128)));
}
return keys;
}
Vector<Uint64> DigestKeys(SizeT n) {
Vector<Uint64> keys;
keys.reserve(n);
std::mt19937_64 rng(0xC0FFEE);
for (SizeT i = 0; i < n; ++i) keys.push_back(rng());
return keys;
}
Vector<String> NameKeys(SizeT n) {
static const char* kPrefixes[] = {"u_", "a_", "mc_", "iris_", "gl_", "v_"};
Vector<String> keys;
keys.reserve(n);
for (SizeT i = 0; i < n; ++i) {
keys.push_back(String(kPrefixes[i % 6]) + "Uniform" + std::to_string(i) + "_xyz");
}
return keys;
}
// Key sets are built once per size and shared: generating them inside the timed
// loop would measure the generator (and, for POINTER, the allocator) instead of
// the table.
template <typename KeyVec, KeyVec (*Make)(SizeT)>
const KeyVec& CachedKeys(SizeT n) {
static UnorderedMap<SizeT, KeyVec> cache;
auto it = cache.find(n);
if (it != cache.end()) return it->second;
return cache.emplace(n, Make(n)).first->second;
}
template <typename Key>
UnorderedMap<Key, Uint64> Populated(const Vector<Key>& keys) {
UnorderedMap<Key, Uint64> map;
for (SizeT i = 0; i < keys.size(); ++i) map[keys[i]] = i;
return map;
}
// ---- the workloads ----------------------------------------------------
// The dominant per-draw operation by a wide margin: a populated cache that is
// read far more often than it is written.
template <typename KeyVec, KeyVec (*Make)(SizeT)>
void LookupHit(benchmark::State& state) {
const auto& keys = CachedKeys<KeyVec, Make>(static_cast<SizeT>(state.range(0)));
auto map = Populated(keys);
for (auto _ : state) {
for (const auto& k : keys) {
auto it = map.find(k);
benchmark::DoNotOptimize(it->second);
}
}
state.SetItemsProcessed(state.iterations() * static_cast<Int64>(keys.size()));
}
// "Is this resource cached yet?" answered NO - the probe length on a miss is a
// different cost from a hit, and resource caches ask this constantly.
template <typename KeyVec, KeyVec (*Make)(SizeT)>
void LookupMiss(benchmark::State& state) {
const SizeT n = static_cast<SizeT>(state.range(0));
const auto& keys = CachedKeys<KeyVec, Make>(n);
auto map = Populated(keys);
const KeyVec absent = Make(n); // same shape, never inserted
for (auto _ : state) {
for (const auto& k : absent) {
benchmark::DoNotOptimize(map.find(k) != map.end());
}
}
state.SetItemsProcessed(state.iterations() * static_cast<Int64>(absent.size()));
}
// Building a cache from empty, rehashes included.
template <typename KeyVec, KeyVec (*Make)(SizeT)>
void InsertGrow(benchmark::State& state) {
const auto& keys = CachedKeys<KeyVec, Make>(static_cast<SizeT>(state.range(0)));
for (auto _ : state) {
UnorderedMap<typename KeyVec::value_type, Uint64> map;
for (SizeT i = 0; i < keys.size(); ++i) map[keys[i]] = i;
benchmark::DoNotOptimize(map.size());
}
state.SetItemsProcessed(state.iterations() * static_cast<Int64>(keys.size()));
}
// Cache eviction and refill: erase half by key, put them back. This is the
// aged-out-entry sweep the pipeline and vertex-input caches do.
template <typename KeyVec, KeyVec (*Make)(SizeT)>
void EraseChurn(benchmark::State& state) {
const auto& keys = CachedKeys<KeyVec, Make>(static_cast<SizeT>(state.range(0)));
for (auto _ : state) {
state.PauseTiming();
auto map = Populated(keys);
state.ResumeTiming();
for (SizeT i = 0; i < keys.size(); i += 2) benchmark::DoNotOptimize(map.erase(keys[i]));
for (SizeT i = 0; i < keys.size(); i += 2) map[keys[i]] = i;
benchmark::DoNotOptimize(map.size());
}
state.SetItemsProcessed(state.iterations() * static_cast<Int64>(keys.size()));
}
// Mass eviction: erase-while-iterating across the whole table. This is the loop
// shape that a container's erase()-return contract can get wrong, and the one
// that fed garbage handles to vkDestroyPipeline when it was wrong before.
template <typename KeyVec, KeyVec (*Make)(SizeT)>
void EraseSweep(benchmark::State& state) {
const auto& keys = CachedKeys<KeyVec, Make>(static_cast<SizeT>(state.range(0)));
for (auto _ : state) {
state.PauseTiming();
auto map = Populated(keys);
state.ResumeTiming();
for (auto it = map.begin(); it != map.end();) it = map.erase(it);
benchmark::DoNotOptimize(map.size());
}
state.SetItemsProcessed(state.iterations() * static_cast<Int64>(keys.size()));
}
// Whole-table walks: the per-frame sweeps that age entries out, and the
// teardown loops that destroy every Vulkan object a cache owns.
template <typename KeyVec, KeyVec (*Make)(SizeT)>
void Iterate(benchmark::State& state) {
const auto& keys = CachedKeys<KeyVec, Make>(static_cast<SizeT>(state.range(0)));
auto map = Populated(keys);
for (auto _ : state) {
Uint64 acc = 0;
for (const auto& entry : map) acc += entry.second;
benchmark::DoNotOptimize(acc);
}
state.SetItemsProcessed(state.iterations() * static_cast<Int64>(keys.size()));
}
} // namespace
#define MGL_MAP_BENCH(WORKLOAD, SHAPE, VEC, MAKER) \
BENCHMARK_TEMPLATE(WORKLOAD, VEC, MAKER) \
->Name(#WORKLOAD "/" #SHAPE) \
->RangeMultiplier(8) \
->Range(kMinSize, kMaxSize)
MGL_MAP_BENCH(LookupHit, sequential, Vector<Uint64>, SequentialKeys);
MGL_MAP_BENCH(LookupHit, pointer, Vector<Uint64>, PointerKeys);
MGL_MAP_BENCH(LookupHit, digest, Vector<Uint64>, DigestKeys);
MGL_MAP_BENCH(LookupHit, name, Vector<String>, NameKeys);
MGL_MAP_BENCH(LookupMiss, sequential, Vector<Uint64>, SequentialKeys);
MGL_MAP_BENCH(LookupMiss, pointer, Vector<Uint64>, PointerKeys);
MGL_MAP_BENCH(LookupMiss, digest, Vector<Uint64>, DigestKeys);
MGL_MAP_BENCH(LookupMiss, name, Vector<String>, NameKeys);
MGL_MAP_BENCH(InsertGrow, sequential, Vector<Uint64>, SequentialKeys);
MGL_MAP_BENCH(InsertGrow, pointer, Vector<Uint64>, PointerKeys);
MGL_MAP_BENCH(InsertGrow, digest, Vector<Uint64>, DigestKeys);
MGL_MAP_BENCH(InsertGrow, name, Vector<String>, NameKeys);
MGL_MAP_BENCH(EraseChurn, sequential, Vector<Uint64>, SequentialKeys);
MGL_MAP_BENCH(EraseChurn, digest, Vector<Uint64>, DigestKeys);
MGL_MAP_BENCH(EraseChurn, name, Vector<String>, NameKeys);
MGL_MAP_BENCH(EraseSweep, sequential, Vector<Uint64>, SequentialKeys);
MGL_MAP_BENCH(EraseSweep, digest, Vector<Uint64>, DigestKeys);
MGL_MAP_BENCH(Iterate, sequential, Vector<Uint64>, SequentialKeys);
MGL_MAP_BENCH(Iterate, digest, Vector<Uint64>, DigestKeys);
BENCHMARK_MAIN();
+47 -1
View File
@@ -11,5 +11,51 @@ endif()
add_executable(DriverBench DriverBench.c) add_executable(DriverBench DriverBench.c)
target_link_libraries(DriverBench PRIVATE dl) target_link_libraries(DriverBench PRIVATE dl)
# WHY EVERY ENTRY HERE CARRIES A PASS_REGULAR_EXPRESSION.
#
# DriverBench prints one CSV row per case it ran and exits 0 whatever it ran. Before this, a ctest
# entry naming a case therefore could not answer the only question it exists to ask: an argument
# matching nothing in kBenchCases selected no case, printed only the header row, and still exited
# 0. DriverBench.c now refuses an unknown case name (exit 2), which closes it at the source - but
# the entry must be able to go red for the reason it exists WITHOUT depending on that check
# staying in the binary, so each entry also requires the case's own output row to appear.
#
# The regex is what a healthy run of that case prints and nothing else does: the case name at the
# start of a line, then the frames / ops-per-frame / median-ms / ns-per-op / fps columns
# (run_case()). A rename, a drop from kBenchCases, a boot_egl() failure or
# a crash part-way through the case all remove that row and turn the entry red.
#
# Note that a PASS_REGULAR_EXPRESSION makes ctest ignore the process exit code (cmCTestRunTest:
# success is `retVal == 0 || !RequiredRegularExpressions.empty()`), which is why the row itself
# has to be the evidence rather than a companion to the rc.
add_test(NAME DriverBench COMMAND DriverBench draw_tiny) add_test(NAME DriverBench COMMAND DriverBench draw_tiny)
set_tests_properties(DriverBench PROPERTIES LABELS benchmark) # draw_tiny's a/ops scale with $DRIVERBENCH_DRAWS (main()), so only the shape of
# the row is pinned here, not the column values.
set_tests_properties(DriverBench PROPERTIES
LABELS benchmark
PASS_REGULAR_EXPRESSION "(^|\n)draw_tiny,[0-9]+,[0-9]+,[0-9.]+,[0-9.]+,[0-9.]+")
# The Blaze3D blend toggle, as its own entry.
#
# mc_state_toggle is glEnable(GL_BLEND) / glBlendFuncSeparate / glDrawElements /
# glDisable(GL_BLEND) / glDrawElements, 46 times - the measured vanilla-frame rate, and the exact
# shape ROADMAP.md writes down as the microbenchmark P2 owes the GO/NO-GO. It is the workload the
# whole "push at validate, not in the setter" decision was made for: a per-setter design pays for
# every toggle, and a CSO that is minted twice and then reused pays for none of them.
#
# The case has existed in kBenchCases since P0 and nothing ran it, so nothing noticed if it broke.
# Exposing it costs about 1.2 s inside an existing three-minute job, and it means the number the
# P2 report quotes comes from a case CI has been executing all along rather than from a code path
# whose first run is the day it is measured.
#
# Like the entry above, this runs against whatever $DRIVERBENCH_EGL_LIB names (the system driver
# when unset) - the ctest entry is a "does this case still run" gate, not the measurement. The
# measurement is run_driver_bench.sh against each of {native, espryt, magma}.
add_test(NAME DriverBenchStateToggle COMMAND DriverBench mc_state_toggle)
# The ops-per-frame column is pinned to 46 here, unlike the entry above: the mc_* cases are
# excluded from the $DRIVERBENCH_DRAWS scaling on purpose ("the mc_* rates are measured and must
# not move, or the numbers stop being comparable", main()), so 46 toggles per frame
# is part of what "this case still runs" means. Change the workload and this entry says so.
set_tests_properties(DriverBenchStateToggle PROPERTIES
LABELS benchmark
PASS_REGULAR_EXPRESSION "(^|\n)mc_state_toggle,[0-9]+,46,[0-9.]+,[0-9.]+,[0-9.]+")
@@ -476,6 +476,28 @@ int main(int argc, char** argv) {
if (getenv("DRIVERBENCH_FRAMES")) g_frames = atoi(getenv("DRIVERBENCH_FRAMES")); if (getenv("DRIVERBENCH_FRAMES")) g_frames = atoi(getenv("DRIVERBENCH_FRAMES"));
if (getenv("DRIVERBENCH_SPRITES")) g_mixSprites = atol(getenv("DRIVERBENCH_SPRITES")); if (getenv("DRIVERBENCH_SPRITES")) g_mixSprites = atol(getenv("DRIVERBENCH_SPRITES"));
/* A requested case name that matches nothing used to select nothing, print the header row and
* exit 0 - so a caller that names a case (run_driver_bench.sh, and the two ctest entries in
* CMakeLists.txt) could not tell "the case ran" from "the case has been renamed or deleted".
* Refuse it here, before any GL work, so the refusal reaches a caller that has no display
* either, and name what does exist so the fix is obvious. */
int unknownCases = 0;
for (int j = 1; j < argc; ++j) {
int known = 0;
for (int i = 0; i < kBenchCaseCount; ++i)
if (strcmp(argv[j], kBenchCases[i].name) == 0) known = 1;
if (!known) {
fprintf(stderr, "DriverBench: no case named '%s'\n", argv[j]);
unknownCases = 1;
}
}
if (unknownCases) {
fprintf(stderr, "DriverBench: the %d cases in kBenchCases are:\n", kBenchCaseCount);
for (int i = 0; i < kBenchCaseCount; ++i)
fprintf(stderr, " %s\n", kBenchCases[i].name);
return 2;
}
if (boot_egl()) return 1; if (boot_egl()) return 1;
build_resources(); build_resources();
@@ -0,0 +1,21 @@
cmake_minimum_required(VERSION 3.24)
add_executable(
TranslationCacheBench
TranslationCacheBench.cpp
)
target_include_directories(TranslationCacheBench PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
${MGL_ROOT}/3rdparty/SPIRV-Reflect
)
target_link_libraries(
TranslationCacheBench PRIVATE
benchmark::benchmark
${LINK_LIBRARIES}
)
add_test(NAME TranslationCacheBench COMMAND TranslationCacheBench --benchmark_counters_tabular=true)
set_tests_properties(TranslationCacheBench PROPERTIES LABELS benchmark)
@@ -0,0 +1,457 @@
// MobileGL - MobileGL/MG_Benchmark/ShaderCache/TranslationCacheBench.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// What the two-level shader translation memo is worth, measured on the workload that
// motivated it: the KHR-GL33.texture_swizzle.smoke_* shape, where one case builds 2592
// programs out of a handful of distinct sources.
//
// Four pairs of cases, each Off/On:
//
// ProgramLink - the whole glCompileShader + glLinkProgram path for one program, with
// FRESH SHADER OBJECTS every iteration. This is the CTS shape exactly,
// and it is the headline case now. It used to be the PESSIMISTIC one:
// a hit still paid for both glslang parses, because the parse happens
// at glCompileShader - a different entry point from the one L1
// memoizes - and fresh shader objects meant ShaderCompileAdoptionMap
// could not hand the earlier parse over either. L1c is what closed
// that: the compile half of the memo recognises each stage's source
// and publishes its verdict without parsing, so on a hit this case now
// constructs no glslang object at all.
//
// SharedShaderLink - the same program population with the shader objects KEPT ALIVE, so
// the parses happen once outside the measured loop whatever the cache
// does. That makes it the CONTROL for L1c rather than a target: its
// numbers should not move, and if they do, L1c has added cost to a
// path it was supposed to leave alone.
//
// DeferredParseLink - the shape where L1c could LOSE: a constant vertex source (which
// hits L1c and therefore skips its parse) against a fresh fragment
// source every iteration (which makes the PROGRAM key miss, so the
// skipped parse has to happen inside the link after all). Same parse
// count either way, so the pair should land within noise; see its own
// header below.
//
// EsslTranspile - the DirectGLES backend segment: the SPIR-V pass chain plus
// SPIRV-Cross. Runs the driver-INDEPENDENT half of the real chain (the
// passes SyncToBackend runs unconditionally, plus the two stage-gated
// ones a fragment module reaches) so the miss path costs what
// production costs; the capability-gated passes need a live ES driver
// and are not reachable from a benchmark process.
//
// Every On case runs with a warm cache: the first iteration misses and every one after it
// hits, which is exactly the steady state of a 2592-program smoke case.
#include <benchmark/benchmark.h>
#include <string>
#include "Config.h"
#include "Includes.h"
#include "Init.h"
#include "MG_Impl/GLImpl/Program/GL_Program.h"
#include "MG_State/GLState/Core.h"
#include "MG_State/GLState/ProgramState/ProgramTranslationCache.h"
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
#include "MG_Util/ShaderTranspiler/TranslationCache.h"
#include "MG_Util/ShaderTranspiler/Types.h"
using namespace MobileGL;
using namespace MobileGL::MG_Util::ShaderTranspiler;
namespace {
const char* kVertexSource = R"(#version 460
layout(location = 0) in vec3 aPos;
out vec3 vPos;
out vec2 vUv;
void main() {
vPos = aPos;
vUv = aPos.xy * 0.5 + 0.5;
gl_Position = vec4(aPos, 1.0);
}
)";
// Shaped after gl3cTextureSwizzleTests.cpp's template: a sampler of one type, one
// TEXTURE_ACCESS, one CHANNEL, and an output whose BASIC_TYPE is the only thing that
// varies within a case. Padded with enough real arithmetic that the translation chain
// is doing work rather than measuring fixed overheads.
// `padLines` = 0 is the honest CTS size: gl3cTextureSwizzleTests' smoke template is a
// handful of lines, and that is the workload the memo exists for. The padded variant is
// kept alongside it because a shaderpack stage is orders of magnitude bigger, and the
// two bracket the ratio the cache is worth in practice.
String SwizzleLikeFragment(const String& prefix, const int padLines) {
String source = "#version 460\n";
source += "in vec3 vPos;\n";
source += "in vec2 vUv;\n";
source += "layout(location = 0) out " + prefix + "vec4 fragColor;\n";
source += "uniform sampler2D uTex;\n";
source += "uniform vec4 uTint;\n";
source += "uniform mat4 uModel;\n";
source += "uniform float uArr[8];\n";
source += "void main() {\n";
source += " vec4 s = texture(uTex, vUv);\n";
source += " float acc = s.r;\n";
for (int i = 0; i < padLines; ++i) {
source += " acc = acc * 1.0001 + sin(acc + " + std::to_string(i) + ".0) * cos(acc);\n";
}
source += " for (int i = 0; i < 8; ++i) acc += uArr[i];\n";
source += " vec4 p = uModel * vec4(vPos, 1.0);\n";
source += " fragColor = " + prefix + "vec4((s + uTint) * acc + p);\n";
source += "}\n";
return source;
}
class CacheModeScope {
public:
explicit CacheModeScope(const Bool enabled)
: m_saved(MG_Config::Features.ShaderTranslationCache) {
MG_Config::Features.ShaderTranslationCache =
enabled ? MG_Config::QuirkOverride::ForceOn : MG_Config::QuirkOverride::ForceOff;
}
~CacheModeScope() { MG_Config::Features.ShaderTranslationCache = m_saved; }
private:
const MG_Config::QuirkOverride m_saved;
};
class SyncCompileScope {
public:
SyncCompileScope() : m_saved(MG_Config::Features.AsyncShaderCompile) {
MG_Config::Features.AsyncShaderCompile = MG_Config::QuirkOverride::ForceOff;
}
~SyncCompileScope() { MG_Config::Features.AsyncShaderCompile = m_saved; }
private:
const MG_Config::QuirkOverride m_saved;
};
// One program, built the way the CTS builds one: fresh shader objects every time.
void LinkOneProgram(const String& vertexSource, const String& fragmentSource) {
using namespace MG_Impl::GLImpl;
const GLuint vs = CreateShader(GL_VERTEX_SHADER);
const char* vsText = vertexSource.c_str();
ShaderSource(vs, 1, &vsText, nullptr);
CompileShader(vs);
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
const char* fsText = fragmentSource.c_str();
ShaderSource(fs, 1, &fsText, nullptr);
CompileShader(fs);
const GLuint program = CreateProgram();
AttachShader(program, vs);
AttachShader(program, fs);
LinkProgram(program);
benchmark::DoNotOptimize(program);
DeleteProgram(program);
DeleteShader(vs);
DeleteShader(fs);
}
Vector<Uint32> BuildSanitizedFragmentSpirv(const String& fragmentSource) {
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fragmentSource};
auto shader = ShaderCompiler::CompileShader(attrib);
if (!shader) return {};
ProgramAttrib programAttrib{.shaders = {shader.value()}};
auto program = ShaderCompiler::LinkProgram(programAttrib);
if (!program) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_FRAGMENT_SHADER}, .program = *program.value()};
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
if (!binary || binary->empty()) return {};
Vector<Uint32> sanitized;
if (!ShaderCompiler::SanitizeAndOptimizeBinary(binary->front(), sanitized)) return {};
return sanitized;
}
// The driver-independent part of BackendProgramObjectImpl::TranspileSpirvToEssl, in the
// same order. What is missing is only the capability-gated passes (viewport lowering,
// multisample clamping, noperspective emulation, the image-format bake), which cannot
// fire without a live ES driver to arm them.
Bool TranspileLikeDirectGles(const Vector<Uint32>& spirv, const Uint esslVersion, String& outEssl) {
Vector<Uint32> a;
const Vector<Uint32>* effective = &spirv;
if (ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(*effective, a, false) && !a.empty()) {
effective = &a;
}
Vector<Uint32> b;
if (ShaderCompiler::LowerRectImages(*effective, b, false) && !b.empty()) effective = &b;
Vector<Uint32> c;
if (ShaderCompiler::Lower1DArrayImagesForEssl(*effective, c, false) && !c.empty()) effective = &c;
Vector<Uint32> d;
if (ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(*effective, d, false) && !d.empty()) {
effective = &d;
}
SpvcSession session(*effective, SessionUsageBit::Transpile);
spvc_compiler_options options;
if (session.CreateOptions(&options) != SPVC_SUCCESS) return false;
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, esslVersion);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
session.SetOptions(options);
const char* result = nullptr;
session.Compile(&result);
if (!result) return false;
outEssl = result;
return true;
}
EsslTranslationKeyInputs EsslInputsFor(const Vector<Uint32>& spirv) {
EsslTranslationKeyInputs inputs;
inputs.spirv = &spirv;
inputs.shaderType = GL_FRAGMENT_SHADER;
inputs.maxColorTextureSamples = 4;
inputs.maxIntegerSamples = 1;
inputs.maxDepthTextureSamples = 4;
inputs.advertisedMaxSamples = 4;
inputs.esslVersion = 320;
return inputs;
}
} // namespace
// ---------------------------------------------------------------------------------------
// L1, in situ: the full glCompileShader + glLinkProgram path for a repeated program.
// ---------------------------------------------------------------------------------------
// Arg(0) = the CTS smoke size; Arg(120) = a heavy stage, bracketing the ratio.
static void BM_ProgramLink_CacheOff(benchmark::State& state) {
MobileGL::Initialize();
const SyncCompileScope sync;
const CacheModeScope cache(false);
const String vs = kVertexSource;
const String fs = SwizzleLikeFragment("", static_cast<int>(state.range(0)));
for (auto _ : state) {
LinkOneProgram(vs, fs);
}
state.SetLabel("MOBILEGL_SHADER_CACHE=0");
}
BENCHMARK(BM_ProgramLink_CacheOff)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
static void BM_ProgramLink_CacheOn(benchmark::State& state) {
MobileGL::Initialize();
const SyncCompileScope sync;
const CacheModeScope cache(true);
const String vs = kVertexSource;
const String fs = SwizzleLikeFragment("", static_cast<int>(state.range(0)));
LinkOneProgram(vs, fs); // prime, so the measured loop is the steady state
const TranslationCacheStats before = MG_State::GLState::GetProgramTranslationCache().Stats();
const TranslationCacheStats parseBefore = GetShaderParseVerdictCache().Stats();
for (auto _ : state) {
LinkOneProgram(vs, fs);
}
const TranslationCacheStats stats = MG_State::GLState::GetProgramTranslationCache().Stats();
const TranslationCacheStats parseStats = GetShaderParseVerdictCache().Stats();
state.counters["L1_hits"] = static_cast<double>(stats.hits - before.hits);
state.counters["L1_misses"] = static_cast<double>(stats.misses - before.misses);
// Two stages per iteration, so a clean run shows L1c_hits == 2 * iterations and zero
// misses: every glCompileShader in the loop skipped its parse.
state.counters["L1c_hits"] = static_cast<double>(parseStats.hits - parseBefore.hits);
state.counters["L1c_misses"] = static_cast<double>(parseStats.misses - parseBefore.misses);
}
BENCHMARK(BM_ProgramLink_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
// ---------------------------------------------------------------------------------------
// L1, the shape the memo actually exists for: MANY PROGRAMS OUT OF THE SAME SHADERS.
//
// The pair above deletes its shader objects every iteration, which forces a fresh glslang
// parse per iteration no matter what the link does - glCompileShader parses, and that is a
// DIFFERENT entry point from the one L1 memoizes. It is a real workload (what an application
// that never reuses a shader object pays) but it is the pessimistic one, and the residual it
// leaves is the parse, not the link.
//
// This pair keeps the shader objects alive, so the parses happen once before the measured
// loop and the L1 hit then skips the link, mapIO, the SPIR-V, the reflection and the routing
// outright.
//
// SINCE L1c THIS IS THE CONTROL, NOT THE TARGET. Nothing inside the measured loop calls
// glCompileShader, so L1c cannot fire here at all - which is exactly what makes the pair
// useful: it is the shape that says whether the compile-side memo has slowed the LINK path
// down. Its numbers should be indistinguishable from the pre-L1c ones.
// ---------------------------------------------------------------------------------------
namespace {
struct SharedShaders {
GLuint vs = 0;
GLuint fs = 0;
};
SharedShaders MakeSharedShaders(const String& vertexSource, const String& fragmentSource) {
using namespace MG_Impl::GLImpl;
SharedShaders shaders;
shaders.vs = CreateShader(GL_VERTEX_SHADER);
const char* vsText = vertexSource.c_str();
ShaderSource(shaders.vs, 1, &vsText, nullptr);
CompileShader(shaders.vs);
shaders.fs = CreateShader(GL_FRAGMENT_SHADER);
const char* fsText = fragmentSource.c_str();
ShaderSource(shaders.fs, 1, &fsText, nullptr);
CompileShader(shaders.fs);
return shaders;
}
void LinkFromSharedShaders(const SharedShaders& shaders) {
using namespace MG_Impl::GLImpl;
const GLuint program = CreateProgram();
AttachShader(program, shaders.vs);
AttachShader(program, shaders.fs);
LinkProgram(program);
benchmark::DoNotOptimize(program);
DeleteProgram(program);
}
} // namespace
static void BM_SharedShaderLink_CacheOff(benchmark::State& state) {
MobileGL::Initialize();
const SyncCompileScope sync;
const CacheModeScope cache(false);
const SharedShaders shaders =
MakeSharedShaders(kVertexSource, SwizzleLikeFragment("", static_cast<int>(state.range(0))));
for (auto _ : state) {
LinkFromSharedShaders(shaders);
}
state.SetLabel("MOBILEGL_SHADER_CACHE=0");
}
BENCHMARK(BM_SharedShaderLink_CacheOff)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
static void BM_SharedShaderLink_CacheOn(benchmark::State& state) {
MobileGL::Initialize();
const SyncCompileScope sync;
const CacheModeScope cache(true);
const SharedShaders shaders =
MakeSharedShaders(kVertexSource, SwizzleLikeFragment("", static_cast<int>(state.range(0))));
LinkFromSharedShaders(shaders); // prime, so the measured loop is the steady state
const TranslationCacheStats before = MG_State::GLState::GetProgramTranslationCache().Stats();
for (auto _ : state) {
LinkFromSharedShaders(shaders);
}
const TranslationCacheStats stats = MG_State::GLState::GetProgramTranslationCache().Stats();
state.counters["L1_hits"] = static_cast<double>(stats.hits - before.hits);
state.counters["L1_misses"] = static_cast<double>(stats.misses - before.misses);
}
BENCHMARK(BM_SharedShaderLink_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
// ---------------------------------------------------------------------------------------
// L2, component: the DirectGLES SPIR-V pass chain plus SPIRV-Cross for one stage.
// ---------------------------------------------------------------------------------------
static void BM_EsslTranspile_CacheOff(benchmark::State& state) {
MobileGL::Initialize();
const Vector<Uint32> spirv =
BuildSanitizedFragmentSpirv(SwizzleLikeFragment("", static_cast<int>(state.range(0))));
if (spirv.empty()) {
state.SkipWithError("could not build the fragment module");
return;
}
String essl;
for (auto _ : state) {
if (!TranspileLikeDirectGles(spirv, 320, essl)) {
state.SkipWithError("transpile failed");
break;
}
benchmark::DoNotOptimize(essl.data());
}
state.SetLabel("MOBILEGL_SHADER_CACHE=0");
}
BENCHMARK(BM_EsslTranspile_CacheOff)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
static void BM_EsslTranspile_CacheOn(benchmark::State& state) {
MobileGL::Initialize();
const Vector<Uint32> spirv =
BuildSanitizedFragmentSpirv(SwizzleLikeFragment("", static_cast<int>(state.range(0))));
if (spirv.empty()) {
state.SkipWithError("could not build the fragment module");
return;
}
BoundedTranslationCache<EsslTranslationResult> cache("bench L2", 64, 8u << 20);
const EsslTranslationKeyInputs inputs = EsslInputsFor(spirv);
for (auto _ : state) {
const TranslationCacheKey key = BuildEsslTranslationKey(inputs);
EsslTranslationResultPtr hit = cache.Find(key);
if (!hit) {
auto payload = MakeShared<EsslTranslationResult>();
if (!TranspileLikeDirectGles(spirv, inputs.esslVersion, payload->essl)) {
state.SkipWithError("transpile failed");
break;
}
cache.Insert(key, EsslTranslationResultPtr(payload), EsslTranslationResultBytes(*payload));
hit = payload;
}
benchmark::DoNotOptimize(hit->essl.data());
}
const TranslationCacheStats stats = cache.Stats();
state.counters["L2_hits"] = static_cast<double>(stats.hits);
state.counters["L2_misses"] = static_cast<double>(stats.misses);
}
BENCHMARK(BM_EsslTranspile_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
// ---------------------------------------------------------------------------------------
// L1c, the shape where it could LOSE rather than win: the DEFERRED PARSE.
// ---------------------------------------------------------------------------------------
// A stage whose compile hits L1c holds no AST, so if the program-level key then MISSES, the
// parse it skipped has to happen anyway - inside the link, via ClaimParsedShader. The parse
// is moved, not removed, and this pair is what says whether moving it costs anything.
//
// The shape forces exactly that, every iteration: one CONSTANT vertex source (hits L1c after
// the first iteration) linked against a FRESH fragment source each time (misses L1c, and
// makes the program key miss too). So:
//
// cache off - two parses at glCompileShader, then the link.
// cache on - one parse at glCompileShader (the fragment), one deferred parse inside the
// link (the vertex), then the link.
//
// The parse count is identical, so these two should land within noise of each other. If the
// On arm is materially SLOWER, L1c is charging for something - the per-compile key build and
// hash over the full preprocessed source, or the loss of the claim-CAS reuse - and that cost
// shows up here and nowhere else.
//
// The distinct fragment sources also churn both front-end levels through their FIFO caps,
// which is the eviction behaviour a real shaderpack load produces; over a long run the
// constant vertex entry is occasionally evicted by that churn and re-inserted, so the L1c
// hit rate reported below is high but not exactly 1.0 per iteration.
namespace {
String UniqueFragmentSource(const Uint64 serial, const int padLines) {
return SwizzleLikeFragment("", padLines) +
"\n// unique-" + std::to_string(serial) + "\n";
}
} // namespace
static void BM_DeferredParseLink_CacheOff(benchmark::State& state) {
MobileGL::Initialize();
const SyncCompileScope sync;
const CacheModeScope cache(false);
const String vs = kVertexSource;
Uint64 serial = 0;
for (auto _ : state) {
LinkOneProgram(vs, UniqueFragmentSource(serial++, static_cast<int>(state.range(0))));
}
state.SetLabel("MOBILEGL_SHADER_CACHE=0");
}
BENCHMARK(BM_DeferredParseLink_CacheOff)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
static void BM_DeferredParseLink_CacheOn(benchmark::State& state) {
MobileGL::Initialize();
const SyncCompileScope sync;
const CacheModeScope cache(true);
const String vs = kVertexSource;
Uint64 serial = 0;
LinkOneProgram(vs, UniqueFragmentSource(~0ull, static_cast<int>(state.range(0)))); // prime the vertex entry
const TranslationCacheStats before = MG_State::GLState::GetProgramTranslationCache().Stats();
const TranslationCacheStats parseBefore = GetShaderParseVerdictCache().Stats();
for (auto _ : state) {
LinkOneProgram(vs, UniqueFragmentSource(serial++, static_cast<int>(state.range(0))));
}
const TranslationCacheStats stats = MG_State::GLState::GetProgramTranslationCache().Stats();
const TranslationCacheStats parseStats = GetShaderParseVerdictCache().Stats();
// Expected shape: L1 all misses (every program is new), L1c one hit (vertex) and one miss
// (fragment) per iteration.
state.counters["L1_hits"] = static_cast<double>(stats.hits - before.hits);
state.counters["L1_misses"] = static_cast<double>(stats.misses - before.misses);
state.counters["L1c_hits"] = static_cast<double>(parseStats.hits - parseBefore.hits);
state.counters["L1c_misses"] = static_cast<double>(parseStats.misses - parseBefore.misses);
}
BENCHMARK(BM_DeferredParseLink_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
BENCHMARK_MAIN();
@@ -0,0 +1,20 @@
cmake_minimum_required(VERSION 3.24)
# Deliberately NOT a google-benchmark target: the interesting quantity is a per-stage
# breakdown of one program build, which needs its own clock around sub-steps that share
# set-up, and a plain main() keeps the output a table this can be read straight out of.
add_executable(
TranspileProfile
TranspileProfile.cpp
)
target_include_directories(TranspileProfile PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
${MGL_ROOT}/3rdparty/SPIRV-Reflect
)
target_link_libraries(
TranspileProfile PRIVATE
${LINK_LIBRARIES}
)
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -21,7 +21,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
EGLStateContext* GetState() { EGLStateContext* GetState() {
if (!MG_State::pEGLContext) { if (!MG_State::pEGLContext) {
MGLOG_E("pEGLContext is null. MG_State may not be initialized."); MGLOG_E_ONCE("pEGLContext is null. MG_State may not be initialized.");
} }
return MG_State::pEGLContext.get(); return MG_State::pEGLContext.get();
} }
@@ -146,7 +146,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
auto* backendObject = GetBackendObject(state); auto* backendObject = GetBackendObject(state);
if (!backendObject) { if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!"); MGLOG_E_ONCE("activeBackendObject not initialized!");
state->DestroySurface(dpy, surface); state->DestroySurface(dpy, surface);
return EGL_NO_SURFACE; return EGL_NO_SURFACE;
} }
@@ -172,11 +172,11 @@ namespace MobileGL::MG_Impl::EGLImpl {
auto* backendObject = GetBackendObject(state); auto* backendObject = GetBackendObject(state);
if (!backendObject) { if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!"); MGLOG_E_ONCE("activeBackendObject not initialized!");
return EGL_FALSE; return EGL_FALSE;
} }
if (!backendObject->SwapEGLBuffers(dpy, draw)) { if (!backendObject->SwapEGLBuffers(dpy, draw)) {
MGLOG_E("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw); MGLOG_E_ONCE("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw);
state->SetError(EGL_BAD_SURFACE); state->SetError(EGL_BAD_SURFACE);
return EGL_FALSE; return EGL_FALSE;
} }
@@ -211,7 +211,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
auto* backendObject = GetBackendObject(state); auto* backendObject = GetBackendObject(state);
if (!backendObject) { if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!"); MGLOG_E_ONCE("activeBackendObject not initialized!");
return EGL_FALSE; return EGL_FALSE;
} }
if (!backendObject->InitializeEGLDisplay(dpy, major, minor)) { if (!backendObject->InitializeEGLDisplay(dpy, major, minor)) {
@@ -265,7 +265,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (releaseCurrentRequest) { if (releaseCurrentRequest) {
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) { if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) { if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
MGLOG_E("eglMakeCurrent release failed in backend thread=%s", threadId.c_str()); MGLOG_E_ONCE("eglMakeCurrent release failed in backend thread=%s", threadId.c_str());
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext); state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
state->SetError(EGL_BAD_ACCESS); state->SetError(EGL_BAD_ACCESS);
return EGL_FALSE; return EGL_FALSE;
@@ -277,12 +277,12 @@ namespace MobileGL::MG_Impl::EGLImpl {
auto* backendObject = GetBackendObject(state); auto* backendObject = GetBackendObject(state);
if (!backendObject) { if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!"); MGLOG_E_ONCE("activeBackendObject not initialized!");
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext); state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
return EGL_FALSE; return EGL_FALSE;
} }
if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) { if (!backendObject->MakeEGLCurrent(dpy, draw, read, ctx)) {
MGLOG_E("eglMakeCurrent backend attach failed thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(), MGLOG_E_ONCE("eglMakeCurrent backend attach failed thread=%s dpy=%p draw=%p read=%p ctx=%p", threadId.c_str(),
dpy, draw, read, ctx); dpy, draw, read, ctx);
state->SetError(EGL_BAD_ACCESS); state->SetError(EGL_BAD_ACCESS);
state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext); state->MakeCurrent(oldDisplay, oldDraw, oldRead, oldContext);
@@ -703,7 +703,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
auto* backendObject = GetBackendObject(state); auto* backendObject = GetBackendObject(state);
if (!backendObject) { if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!"); MGLOG_E_ONCE("activeBackendObject not initialized!");
state->DestroySurface(dpy, surface); state->DestroySurface(dpy, surface);
return EGL_NO_SURFACE; return EGL_NO_SURFACE;
} }
@@ -726,7 +726,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
} }
auto* backendObject = GetBackendObject(state); auto* backendObject = GetBackendObject(state);
if (!backendObject) { if (!backendObject) {
MGLOG_E("activeBackendObject not initialized!"); MGLOG_E_ONCE("activeBackendObject not initialized!");
return EGL_FALSE; return EGL_FALSE;
} }
width = std::max<EGLint>(width, 1); width = std::max<EGLint>(width, 1);
@@ -764,7 +764,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
MGLOG_D("eglGetProcAddress(%s)", name); MGLOG_D("eglGetProcAddress(%s)", name);
void* proc = MG_Impl::GetProcAddress(name); void* proc = MG_Impl::GetProcAddress(name);
if (!proc) { if (!proc) {
MGLOG_W("Failed to get function: %s", name); MGLOG_D("Failed to get function: %s", name);
return nullptr; return nullptr;
} }
return (__eglMustCastToProperFunctionPointerType)proc; return (__eglMustCastToProperFunctionPointerType)proc;
+132 -40
View File
@@ -18,6 +18,7 @@
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h> #include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToMG/BufferEnumConverter.h> #include <MG_Util/Converters/GLToMG/BufferEnumConverter.h>
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h> #include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
#include <MG_Util/Texture/PixelStoreProcessor.h>
namespace MobileGL::MG_Impl::GLImpl { namespace MobileGL::MG_Impl::GLImpl {
namespace { namespace {
@@ -31,6 +32,8 @@ namespace MobileGL::MG_Impl::GLImpl {
NamedBufferData, NamedBufferData,
NamedBufferSubData, NamedBufferSubData,
CopyNamedBufferSubData, CopyNamedBufferSubData,
ClearBufferData,
ClearBufferSubData,
ClearNamedBufferData, ClearNamedBufferData,
ClearNamedBufferSubData, ClearNamedBufferSubData,
MapBufferRange, MapBufferRange,
@@ -65,6 +68,10 @@ namespace MobileGL::MG_Impl::GLImpl {
return "NamedBufferSubData"; return "NamedBufferSubData";
case BufferOp::CopyNamedBufferSubData: case BufferOp::CopyNamedBufferSubData:
return "CopyNamedBufferSubData"; return "CopyNamedBufferSubData";
case BufferOp::ClearBufferData:
return "ClearBufferData";
case BufferOp::ClearBufferSubData:
return "ClearBufferSubData";
case BufferOp::ClearNamedBufferData: case BufferOp::ClearNamedBufferData:
return "ClearNamedBufferData"; return "ClearNamedBufferData";
case BufferOp::ClearNamedBufferSubData: case BufferOp::ClearNamedBufferSubData:
@@ -143,16 +150,6 @@ namespace MobileGL::MG_Impl::GLImpl {
return 0; return 0;
} }
// The pattern is replicated verbatim, which is only the whole story while the client
// layout already matches the internal format - the case every entry point in practice
// uses, and the only one the conversion machinery here can express. Say so rather than
// quietly writing a differently-sized pattern.
const SizeT sourceSize = MG_Util::GetInputBytesPerPixel(inputFormat, pixelType);
if (sourceSize != elementSize) {
MGLOG_W("%s: clear pattern is %zu bytes but internalformat 0x%X stores %zu; "
"converting between them is not implemented",
GetBufferOpName(op), sourceSize, internalformat, elementSize);
}
return elementSize; return elementSize;
} }
@@ -194,27 +191,59 @@ namespace MobileGL::MG_Impl::GLImpl {
return true; return true;
} }
void ClearNamedBufferRange_State(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size, Bool BuildClearPattern(GLenum internalformat, GLenum format, GLenum type, const void* data,
GLenum format, GLenum type, const void* data, BufferOp op) { SizeT patternSize, BufferOp op, Vector<Uint8>& pattern) {
const TextureInternalFormat internal = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
const TextureInputFormat inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
const TexturePixelDataType inputType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
Vector<Uint8> zeroInput;
const void* inputPixel = data;
if (inputPixel == nullptr) {
const SizeT inputSize = MG_Util::GetInputBytesPerPixel(inputFormat, inputType);
if (inputSize == 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
"format and type do not describe a source pixel."));
return false;
}
zeroInput.resize(inputSize);
inputPixel = zeroInput.data();
}
if (!MG_Util::PixelStoreProcessor::ConvertOnePixelToInternal(
internal, inputFormat, inputType, inputPixel, pattern)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", GetBufferOpName(op),
std::format("Cannot convert one ({}, {}) pixel into internalformat 0x{:X}.",
MG_Util::ConvertGLEnumToString(format), MG_Util::ConvertGLEnumToString(type),
internalformat)));
return false;
}
if (data == nullptr) {
// GL defines a null clear value as all zero bits in the destination store, while
// retaining the format/type validation above.
pattern.assign(patternSize, 0);
}
return true;
}
void ClearBufferRange_State(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
GLenum internalformat, GLintptr offset, GLsizeiptr size,
GLenum format, GLenum type, const void* data, BufferOp op) {
const SizeT patternSize = GetClearPatternSize(internalformat, format, type, op); const SizeT patternSize = GetClearPatternSize(internalformat, format, type, op);
if (patternSize == 0) return; if (patternSize == 0) return;
auto bufferObject = GetNamedBufferObject(buffer, op);
if (!bufferObject) return;
if (!ValidateBufferClearRange(bufferObject, offset, size, patternSize, op)) return; if (!ValidateBufferClearRange(bufferObject, offset, size, patternSize, op)) return;
if (size == 0) return; if (size == 0) return;
Vector<Uint8> clearData(static_cast<SizeT>(size)); Vector<Uint8> pattern;
if (data) { if (!BuildClearPattern(internalformat, format, type, data, patternSize, op, pattern)) return;
const auto* pattern = static_cast<const Uint8*>(data); bufferObject->FillSubData({pattern.data(), pattern.size()}, static_cast<SizeT>(offset),
for (SizeT at = 0; at < clearData.size(); at += patternSize) { static_cast<SizeT>(size));
Memcpy(clearData.data() + at, pattern, patternSize);
}
} else {
Memset(clearData.data(), 0, clearData.size());
}
bufferObject->UploadSubData({clearData.data(), clearData.size()}, static_cast<SizeT>(offset));
} }
auto& GetBufferBindingSlot(BufferTarget target) { auto& GetBufferBindingSlot(BufferTarget target) {
@@ -1197,17 +1226,34 @@ namespace MobileGL::MG_Impl::GLImpl {
static_cast<SizeT>(writeOffset), static_cast<SizeT>(size)); static_cast<SizeT>(writeOffset), static_cast<SizeT>(size));
} }
void ClearBufferData_State(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) {
auto bufferObject = GetBoundBufferObject(target, BufferOp::ClearBufferData);
if (!bufferObject) return;
ClearBufferRange_State(bufferObject, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
type, data, BufferOp::ClearBufferData);
}
void ClearBufferSubData_State(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size,
GLenum format, GLenum type, const void* data) {
auto bufferObject = GetBoundBufferObject(target, BufferOp::ClearBufferSubData);
if (!bufferObject) return;
ClearBufferRange_State(bufferObject, internalformat, offset, size, format, type, data,
BufferOp::ClearBufferSubData);
}
void ClearNamedBufferData_State(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) { void ClearNamedBufferData_State(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) {
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::ClearNamedBufferData); auto bufferObject = GetNamedBufferObject(buffer, BufferOp::ClearNamedBufferData);
if (!bufferObject) return; if (!bufferObject) return;
ClearNamedBufferRange_State(buffer, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format, ClearBufferRange_State(bufferObject, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
type, data, BufferOp::ClearNamedBufferData); type, data, BufferOp::ClearNamedBufferData);
} }
void ClearNamedBufferSubData_State(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size, void ClearNamedBufferSubData_State(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size,
GLenum format, GLenum type, const void* data) { GLenum format, GLenum type, const void* data) {
ClearNamedBufferRange_State(buffer, internalformat, offset, size, format, type, data, auto bufferObject = GetNamedBufferObject(buffer, BufferOp::ClearNamedBufferSubData);
BufferOp::ClearNamedBufferSubData); if (!bufferObject) return;
ClearBufferRange_State(bufferObject, internalformat, offset, size, format, type, data,
BufferOp::ClearNamedBufferSubData);
} }
void* MapNamedBuffer_State(GLuint buffer, GLenum access) { void* MapNamedBuffer_State(GLuint buffer, GLenum access) {
@@ -1491,8 +1537,8 @@ namespace MobileGL::MG_Impl::GLImpl {
// offset and size, which is also how glBindBuffersRange spells "reset this element" // offset and size, which is also how glBindBuffersRange spells "reset this element"
// (a NULL buffers array, or a zero entry inside one). // (a NULL buffers array, or a zero entry inside one).
static Bool ValidateBufferRangeOffsetAndSize(GLenum target, GLintptr offset, GLsizeiptr size, static Bool ValidateBufferRangeOffsetAndSize(GLenum target, GLintptr offset, GLsizeiptr size,
const char* funcName) { const char* funcName, Bool hasBuffer = true) {
if (size <= 0) { if (hasBuffer && size <= 0) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
@@ -1527,16 +1573,27 @@ namespace MobileGL::MG_Impl::GLImpl {
return false; return false;
} }
} }
// A transform feedback capture binding is addressed in 32-bit components, so BOTH the // GL 4.6 core 6.1.1 constrains the OFFSET to a multiple of four for both
// offset and the size must be multiples of 4. // TRANSFORM_FEEDBACK_BUFFER and ATOMIC_COUNTER_BUFFER (the atomic-counter one has no
if (target == GL_TRANSFORM_FEEDBACK_BUFFER && ((offset % 4) != 0 || (size % 4) != 0)) { // queryable alignment pname, which is why it was missing here), and the SIZE only for
// transform feedback, whose capture is written in whole 32-bit components. Extending the
// size rule to atomic counters as well breaks a legal bind: the conformance suite splits
// MAX_ATOMIC_COUNTER_BUFFER_SIZE evenly across the binding points and that quotient is
// not required to land on four.
if ((target == GL_TRANSFORM_FEEDBACK_BUFFER || target == GL_ATOMIC_COUNTER_BUFFER) && (offset % 4) != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
std::format("offset ({}) must be a multiple of 4 for {}.", offset,
MG_Util::ConvertGLEnumToString(target))));
return false;
}
if (target == GL_TRANSFORM_FEEDBACK_BUFFER && hasBuffer && (size % 4) != 0) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>( MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", funcName, "MG_Impl/GLImpl", funcName,
std::format("offset ({}) and size ({}) must both be multiples of 4 for " std::format("size ({}) must be a multiple of 4 for GL_TRANSFORM_FEEDBACK_BUFFER.", size)));
"GL_TRANSFORM_FEEDBACK_BUFFER.",
offset, size)));
return false; return false;
} }
return true; return true;
@@ -1548,7 +1605,12 @@ namespace MobileGL::MG_Impl::GLImpl {
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target); BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return; if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) return; if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) return;
if (buffer != 0 && !ValidateBufferRangeOffsetAndSize(target, offset, size, __func__)) return; // The target's alignment rules are a property of the BINDING POINT, not of the buffer,
// so they apply even when buffer is zero - which is exactly how
// KHR-GL43.shader_storage_buffer_object.negative-api-bind probes the SSBO alignment
// (glBindBufferRange(SHADER_STORAGE_BUFFER, 0, 0, alignment - 1, 0)). Only the size
// rules need a buffer, since buffer 0 detaches the binding point and ignores size.
if (!ValidateBufferRangeOffsetAndSize(target, offset, size, __func__, /*hasBuffer: */ buffer != 0)) return;
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) { if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, ErrorCode::InvalidOperation,
@@ -1646,6 +1708,15 @@ namespace MobileGL::MG_Impl::GLImpl {
CopyNamedBufferSubData_State(readBuffer, writeBuffer, readOffset, writeOffset, size); CopyNamedBufferSubData_State(readBuffer, writeBuffer, readOffset, writeOffset, size);
} }
void ClearBufferData(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) {
ClearBufferData_State(target, internalformat, format, type, data);
}
void ClearBufferSubData(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
GLenum type, const void* data) {
ClearBufferSubData_State(target, internalformat, offset, size, format, type, data);
}
void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) { void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) {
ClearNamedBufferData_State(buffer, internalformat, format, type, data); ClearNamedBufferData_State(buffer, internalformat, format, type, data);
} }
@@ -1732,10 +1803,30 @@ namespace MobileGL::MG_Impl::GLImpl {
return BufferImpl::ValidateBufferBindingPointRange(bufferTarget, first, count, funcName); return BufferImpl::ValidateBufferBindingPointRange(bufferTarget, first, count, funcName);
} }
// ARB_multi_bind states the equivalence to a loop of single binds "except that ... buffers
// will not be created if they do not exist": glBindBuffer instantiates a name glGenBuffers
// merely reserved, glBindBuffers* must refuse it and raise INVALID_OPERATION instead
// (KHR-GL44.multi_bind.errors_bind_buffers).
//
// Deliberately PER ELEMENT, not all-or-nothing: the equivalence the extension defines is a
// loop, so a bad entry costs its own binding point and nothing else. Rejecting the whole
// call instead cost multi_bind.functional_bind_buffers_base its bindings.
static Bool IsExistingBufferForMultiBind(GLuint buffer, GLsizei index, const char* funcName) {
if (buffer == 0 || MG_State::pGLContext->ValidateBufferObject(buffer)) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", funcName,
std::format("buffers[{}] ({}) is not the name of an existing buffer object.", index, buffer)));
return false;
}
void BindBuffersBase(GLenum target, GLuint first, GLsizei count, const GLuint* buffers) { void BindBuffersBase(GLenum target, GLuint first, GLsizei count, const GLuint* buffers) {
if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return; if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
for (GLsizei i = 0; i < count; ++i) { for (GLsizei i = 0; i < count; ++i) {
BindBufferBase_State(target, first + i, buffers ? buffers[i] : 0); const GLuint buffer = buffers ? buffers[i] : 0;
if (!IsExistingBufferForMultiBind(buffer, i, __func__)) continue;
BindBufferBase_State(target, first + i, buffer);
} }
} }
@@ -1749,6 +1840,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const GLsizeiptr* sizes) { const GLsizeiptr* sizes) {
if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return; if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
for (GLsizei i = 0; i < count; ++i) { for (GLsizei i = 0; i < count; ++i) {
if (buffers && !IsExistingBufferForMultiBind(buffers[i], i, __func__)) continue;
if (!buffers || buffers[i] == 0) { if (!buffers || buffers[i] == 0) {
BindBufferBase_State(target, first + i, 0); BindBufferBase_State(target, first + i, 0);
} else { } else {
@@ -27,6 +27,9 @@ namespace MobileGL::MG_Impl::GLImpl {
void NamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, const void* data); void NamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, const void* data);
void CopyNamedBufferSubData(GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset, void CopyNamedBufferSubData(GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset,
GLsizeiptr size); GLsizeiptr size);
void ClearBufferData(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data);
void ClearBufferSubData(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
GLenum type, const void* data);
void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data); void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data);
void ClearNamedBufferSubData(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, void ClearNamedBufferSubData(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
GLenum type, const void* data); GLenum type, const void* data);
@@ -13,6 +13,7 @@
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h> #include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h> #include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
#include <MG_Util/Converters/MGToStr/BufferEnumConverter.h> #include <MG_Util/Converters/MGToStr/BufferEnumConverter.h>
#include <MG_Util/ShaderTranspiler/Types.h>
namespace MobileGL::MG_Impl::GLImpl::BufferImpl { namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
Bool ValidateBufferTarget(BufferTarget target) { Bool ValidateBufferTarget(BufferTarget target) {
@@ -67,6 +68,13 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
// binding points in GL 3.3 (no ARB_transform_feedback3). // binding points in GL 3.3 (no ARB_transform_feedback3).
pointCount = std::min<SizeT>(pointCount, 4); pointCount = std::min<SizeT>(pointCount, 4);
} }
if (target == BufferTarget::AtomicCounter) {
// GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, which is NOT the state layer's array
// size: a counter buffer reaches a shader only as a lowered storage block, so the
// reserved range is the ceiling, and glGetIntegerv advertises the same number.
pointCount = std::min<SizeT>(
pointCount, static_cast<SizeT>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS));
}
return pointCount; return pointCount;
} }
} // namespace } // namespace
+271
View File
@@ -0,0 +1,271 @@
// MobileGL - MobileGL/MG_Impl/GLImpl/Debug/GL_Debug.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "GL_Debug.h"
#include <cstring>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/Error.h>
#include <MG_Impl/GLImpl/Query/GL_Query.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace {
// Must agree with what GL_Getter answers for GL_MAX_DEBUG_GROUP_STACK_DEPTH and
// GL_MAX_DEBUG_MESSAGE_LENGTH / GL_MAX_LABEL_LENGTH; an application that sizes a buffer
// off the query and then trips a different limit here would have no way to explain it.
constexpr SizeT kMaxDebugGroupStackDepth = 64;
constexpr GLsizei kMaxDebugMessageLength = 1024;
constexpr GLsizei kMaxLabelLength = 256;
// The debug state KHR_debug makes per-context. Held here rather than on GLContext because
// nothing else in MobileGL reads it, and it is keyed on the context id so a
// destroyed-and-recreated context starts with an empty stack and no labels - which the
// unit tests, which recreate the context between cases, depend on.
struct DebugState {
Uint64 contextId = 0;
// The messages pushed with glPushDebugGroup, innermost last. The base group GL creates
// the context with is implicit and is what makes the reported depth start at 1.
Vector<String> groupStack;
// Keyed by (identifier, name); see MakeObjectLabelKey.
UnorderedMap<Uint64, String> objectLabels;
};
DebugState& State() {
static DebugState state;
const Uint64 contextId = MG_State::pGLContext ? MG_State::pGLContext->GetTextureContextId() : 0;
if (state.contextId != contextId) {
state.contextId = contextId;
state.groupStack.clear();
state.objectLabels.clear();
}
return state;
}
Uint64 MakeObjectLabelKey(GLenum identifier, GLuint name) {
return (static_cast<Uint64>(identifier) << 32) | static_cast<Uint64>(name);
}
void RecordDebugError(ErrorCode code, const char* caller, const String& message) {
MG_State::pGLContext->RecordError(code, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, message));
}
// GL 4.6 core 20.2: only an APPLICATION or THIRD_PARTY source may be injected; the rest
// are reserved for the implementation itself.
Bool ValidateInjectedSource(GLenum source, const char* caller) {
if (source == GL_DEBUG_SOURCE_APPLICATION || source == GL_DEBUG_SOURCE_THIRD_PARTY) {
return true;
}
RecordDebugError(ErrorCode::InvalidEnum, caller,
std::format("source {} is not GL_DEBUG_SOURCE_APPLICATION or "
"GL_DEBUG_SOURCE_THIRD_PARTY.",
MG_Util::ConvertGLEnumToString(source)));
return false;
}
// A negative length means the string is NUL-terminated (GL 4.6 core 20.2), which is how
// every one of these entry points spells "just use the whole thing".
Bool ValidateDebugStringLength(GLsizei length, const GLchar* text, GLsizei limit, const char* caller,
const char* what) {
const GLsizei effective =
length < 0 ? static_cast<GLsizei>(text != nullptr ? std::strlen(text) : 0) : length;
if (effective < limit) {
return true;
}
RecordDebugError(ErrorCode::InvalidValue, caller,
std::format("{} length {} is not less than the {} limit of {}.", what, effective, what,
limit));
return false;
}
String MakeDebugString(GLsizei length, const GLchar* text) {
if (text == nullptr) return {};
return length < 0 ? String(text) : String(text, static_cast<SizeT>(length));
}
// Whether `name` currently names an object of `identifier`'s type. GL 4.6 core 20.5 makes
// labelling something that does not exist INVALID_VALUE, and every type KHR_debug lists
// has a frontend name check - so this is answered exactly rather than waved through.
// GL_DISPLAY_LIST is deliberately absent: it exists only in the compatibility profile,
// which MobileGL does not expose, so it falls to the INVALID_ENUM path below.
Bool ValidateLabelledObject(GLenum identifier, GLuint name, Bool& outIdentifierKnown) {
outIdentifierKnown = true;
auto* context = MG_State::pGLContext.get();
switch (identifier) {
case GL_BUFFER:
return context->ValidateBufferName(name);
case GL_SHADER:
return context->ValidateShaderName(name);
case GL_PROGRAM:
return context->ValidateProgramName(name);
case GL_VERTEX_ARRAY:
return context->ValidateVertexArrayName(name);
case GL_QUERY:
return IsQuery(name) == GL_TRUE;
case GL_PROGRAM_PIPELINE:
return context->ValidateProgramPipelineName(name);
case GL_TRANSFORM_FEEDBACK:
return context->ValidateTransformFeedbackName(name);
case GL_SAMPLER:
return context->ValidateSamplerName(name);
case GL_TEXTURE:
return context->ValidateTextureName(name);
case GL_RENDERBUFFER:
return context->ValidateRenderbufferName(name);
case GL_FRAMEBUFFER:
// Name 0 is the default framebuffer, which is a real, labellable object.
return name == 0 || context->ValidateFramebufferName(name);
default:
outIdentifierKnown = false;
return false;
}
}
} // namespace
GLint GetDebugGroupStackDepth() {
// GL 4.6 core 20.6: the context is created with one group already on the stack, so the
// reported depth is one more than the number of pushes the application has made.
return static_cast<GLint>(State().groupStack.size()) + 1;
}
void PushDebugGroup(GLenum source, GLuint id, GLsizei length, const GLchar* message) {
static_cast<void>(id);
if (!ValidateInjectedSource(source, __func__)) return;
if (!ValidateDebugStringLength(length, message, kMaxDebugMessageLength, __func__, "message")) return;
auto& state = State();
if (state.groupStack.size() + 1 >= kMaxDebugGroupStackDepth) {
// Not INVALID_*: KHR_debug gives the group stack its own error code.
RecordDebugError(ErrorCode::StackOverflow, __func__,
std::format("the debug group stack is already {} deep, which is its maximum.",
kMaxDebugGroupStackDepth));
return;
}
state.groupStack.push_back(MakeDebugString(length, message));
MGLOG_D("glPushDebugGroup(%s) -> depth %d", state.groupStack.back().c_str(), GetDebugGroupStackDepth());
}
void PopDebugGroup() {
auto& state = State();
if (state.groupStack.empty()) {
// The base group the context was created with may not be popped (GL 4.6 core 20.6).
RecordDebugError(ErrorCode::StackUnderflow, __func__,
"the debug group stack holds only the group the context was created with.");
return;
}
MGLOG_D("glPopDebugGroup(%s)", state.groupStack.back().c_str());
state.groupStack.pop_back();
}
void DebugMessageInsert(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length,
const GLchar* buf) {
static_cast<void>(id);
if (!ValidateInjectedSource(source, __func__)) return;
switch (type) {
case GL_DEBUG_TYPE_ERROR:
case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR:
case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR:
case GL_DEBUG_TYPE_PORTABILITY:
case GL_DEBUG_TYPE_PERFORMANCE:
case GL_DEBUG_TYPE_MARKER:
case GL_DEBUG_TYPE_PUSH_GROUP:
case GL_DEBUG_TYPE_POP_GROUP:
case GL_DEBUG_TYPE_OTHER:
break;
default:
RecordDebugError(ErrorCode::InvalidEnum, __func__,
std::format("type {} is not a debug message type.",
MG_Util::ConvertGLEnumToString(type)));
return;
}
switch (severity) {
case GL_DEBUG_SEVERITY_HIGH:
case GL_DEBUG_SEVERITY_MEDIUM:
case GL_DEBUG_SEVERITY_LOW:
case GL_DEBUG_SEVERITY_NOTIFICATION:
break;
default:
RecordDebugError(ErrorCode::InvalidEnum, __func__,
std::format("severity {} is not a debug message severity.",
MG_Util::ConvertGLEnumToString(severity)));
return;
}
if (!ValidateDebugStringLength(length, buf, kMaxDebugMessageLength, __func__, "message")) return;
// No callback is ever invoked and the message log is empty by construction
// (GL_MAX_DEBUG_LOGGED_MESSAGES is 1 and glGetDebugMessageLog returns nothing), so the
// application-visible effect is exactly the error checking above. The text still reaches
// MobileGL's own log, where it is worth having next to the calls it annotates - at debug
// level, so an application that inserts a message per draw costs nothing in a release build.
MGLOG_D("glDebugMessageInsert: %s", MakeDebugString(length, buf).c_str());
}
void ObjectLabel(GLenum identifier, GLuint name, GLsizei length, const GLchar* label) {
Bool identifierKnown = false;
const Bool objectExists = ValidateLabelledObject(identifier, name, identifierKnown);
if (!identifierKnown) {
RecordDebugError(ErrorCode::InvalidEnum, __func__,
std::format("identifier {} is not a labellable object type.",
MG_Util::ConvertGLEnumToString(identifier)));
return;
}
if (!objectExists) {
RecordDebugError(ErrorCode::InvalidValue, __func__,
std::format("{} {} is not the name of an existing object.",
MG_Util::ConvertGLEnumToString(identifier), name));
return;
}
if (!ValidateDebugStringLength(length, label, kMaxLabelLength, __func__, "label")) return;
auto& labels = State().objectLabels;
const Uint64 key = MakeObjectLabelKey(identifier, name);
if (label == nullptr) {
// GL 4.6 core 20.5: a NULL label removes any label the object had.
labels.erase(key);
return;
}
labels[key] = MakeDebugString(length, label);
}
void GetObjectLabel(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label) {
if (bufSize < 0) {
RecordDebugError(ErrorCode::InvalidValue, __func__, "bufSize must not be negative.");
return;
}
Bool identifierKnown = false;
const Bool objectExists = ValidateLabelledObject(identifier, name, identifierKnown);
if (!identifierKnown) {
RecordDebugError(ErrorCode::InvalidEnum, __func__,
std::format("identifier {} is not a labellable object type.",
MG_Util::ConvertGLEnumToString(identifier)));
return;
}
if (!objectExists) {
RecordDebugError(ErrorCode::InvalidValue, __func__,
std::format("{} {} is not the name of an existing object.",
MG_Util::ConvertGLEnumToString(identifier), name));
return;
}
const auto& labels = State().objectLabels;
const auto it = labels.find(MakeObjectLabelKey(identifier, name));
const String& text = it != labels.end() ? it->second : String{};
// GL 4.6 core 20.5: the returned length excludes the NUL, and an unlabelled object hands
// back an empty string with length 0 rather than an error.
SizeT copied = 0;
if (label != nullptr && bufSize > 0) {
copied = std::min(text.size(), static_cast<SizeT>(bufSize) - 1);
std::memcpy(label, text.data(), copied);
label[copied] = '\0';
}
if (length != nullptr) {
*length = static_cast<GLsizei>(copied);
}
}
} // namespace MobileGL::MG_Impl::GLImpl
+42
View File
@@ -0,0 +1,42 @@
// MobileGL - MobileGL/MG_Impl/GLImpl/Debug/GL_Debug.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
namespace MobileGL::MG_Impl::GLImpl {
// KHR_debug, core since GL 4.3 (GL 4.6 core 20). Applications use these to annotate a capture
// and to name their objects; Better Clouds calls all four for exactly that.
//
// MobileGL implements the STATE and the ERRORS, and deliberately does not forward the calls to
// the host driver. Two independent reasons:
//
// * glObjectLabel names a FRONTEND object. MobileGL's texture 5 is not the ES driver's
// texture 5 (and under DirectVulkan it is not a driver object at all), so forwarding the
// pair verbatim would label an unrelated object or a nonexistent one - worse than not
// labelling.
// * A debug GROUP is only meaningful if it brackets the commands the application issued
// inside it. Neither backend emits its work at the moment the GL call arrives: DirectGLES
// defers and reorders state sync and uploads around draws, and DirectVulkan is usually not
// even recording a command buffer here. A forwarded push/pop would therefore enclose the
// wrong commands, which is a misleading capture rather than a helpful one.
//
// What the application can rely on is the observable contract: the group stack depth is real
// (GL_DEBUG_GROUP_STACK_DEPTH tracks it, and over/underflow raise the errors KHR_debug
// specifies), and a label written with glObjectLabel comes back from glGetObjectLabel.
void PushDebugGroup(GLenum source, GLuint id, GLsizei length, const GLchar* message);
void PopDebugGroup();
void DebugMessageInsert(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length,
const GLchar* buf);
void ObjectLabel(GLenum identifier, GLuint name, GLsizei length, const GLchar* label);
void GetObjectLabel(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label);
// Current depth of the debug group stack, for GL_DEBUG_GROUP_STACK_DEPTH. The base group the
// context is created with counts, so this is never below 1 (GL 4.6 core 20.6).
GLint GetDebugGroupStackDepth();
} // namespace MobileGL::MG_Impl::GLImpl
File diff suppressed because it is too large Load Diff
@@ -32,8 +32,10 @@ namespace MobileGL::MG_Impl::GLImpl {
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ); void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
void DispatchComputeIndirect(GLintptr indirect); void DispatchComputeIndirect(GLintptr indirect);
void PatchParameteri(GLenum pname, GLint value); void PatchParameteri(GLenum pname, GLint value);
void PatchParameterfv(GLenum pname, const GLfloat* values);
void MemoryBarrier(GLbitfield barriers); void MemoryBarrier(GLbitfield barriers);
void MemoryBarrierByRegion(GLbitfield barriers); void MemoryBarrierByRegion(GLbitfield barriers);
void TextureBarrier();
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride); void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride); void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
@@ -20,17 +20,18 @@
#include "../Framebuffer/GL_Framebuffer.h" #include "../Framebuffer/GL_Framebuffer.h"
#include "../VertexArray/GL_VertexArray.h" #include "../VertexArray/GL_VertexArray.h"
#include "../Sync/GL_Sync.h" #include "../Sync/GL_Sync.h"
#include "../Debug/GL_Debug.h"
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#define DECLARE_GL_FUNCTION_STUB_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) { #define DECLARE_GL_FUNCTION_STUB_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
#define DECLARE_GL_FUNCTION_STUB_END(type, name, ...) \ #define DECLARE_GL_FUNCTION_STUB_END(type, name, ...) \
MGLOG_W("Stub function: %s(...)", __FUNCTION__); \ MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__); \
return (type)1; \ return (type)1; \
} }
#define DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(type, name, ...) \ #define DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(type, name, ...) \
MGLOG_W("Stub function: %s(...)", __FUNCTION__); \ MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__); \
} }
#define DECLARE_GL_FUNCTION_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) { #define DECLARE_GL_FUNCTION_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
@@ -159,7 +160,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, ReleaseShaderCompiler) DECLARE_GL_FUNCTION_S
DECLARE_GL_FUNCTION_HEAD(void, RenderbufferStorage, GLenum target, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, RenderbufferStorage, target, internalformat, width, height) DECLARE_GL_FUNCTION_HEAD(void, RenderbufferStorage, GLenum target, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, RenderbufferStorage, target, internalformat, width, height)
DECLARE_GL_FUNCTION_HEAD(void, SampleCoverage, GLfloat value, GLboolean invert) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SampleCoverage, value, invert) DECLARE_GL_FUNCTION_HEAD(void, SampleCoverage, GLfloat value, GLboolean invert) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SampleCoverage, value, invert)
DECLARE_GL_FUNCTION_HEAD(void, Scissor, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Scissor, x, y, width, height) DECLARE_GL_FUNCTION_HEAD(void, Scissor, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Scissor, x, y, width, height)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShaderBinary, GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShaderBinary, count, shaders, binaryformat, binary, length) DECLARE_GL_FUNCTION_HEAD(void, ShaderBinary, GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderBinary, count, shaders, binaryformat, binary, length)
DECLARE_GL_FUNCTION_HEAD(void, ShaderSource, GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderSource, shader, count, string, length) DECLARE_GL_FUNCTION_HEAD(void, ShaderSource, GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderSource, shader, count, string, length)
DECLARE_GL_FUNCTION_HEAD(void, StencilFunc, GLenum func, GLint ref, GLuint mask) DECLARE_GL_FUNCTION_END_NO_RETURN(void, StencilFunc, func, ref, mask) DECLARE_GL_FUNCTION_HEAD(void, StencilFunc, GLenum func, GLint ref, GLuint mask) DECLARE_GL_FUNCTION_END_NO_RETURN(void, StencilFunc, func, ref, mask)
DECLARE_GL_FUNCTION_HEAD(void, StencilFuncSeparate, GLenum face, GLenum func, GLint ref, GLuint mask) DECLARE_GL_FUNCTION_END_NO_RETURN(void, StencilFuncSeparate, face, func, ref, mask) DECLARE_GL_FUNCTION_HEAD(void, StencilFuncSeparate, GLenum face, GLenum func, GLint ref, GLuint mask) DECLARE_GL_FUNCTION_END_NO_RETURN(void, StencilFuncSeparate, face, func, ref, mask)
@@ -378,27 +379,13 @@ DECLARE_GL_FUNCTION_HEAD(void, VertexBindingDivisor, GLuint bindingindex, GLuint
DECLARE_GL_FUNCTION_STUB_HEAD(void, BlendBarrier) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BlendBarrier) DECLARE_GL_FUNCTION_STUB_HEAD(void, BlendBarrier) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BlendBarrier)
DECLARE_GL_FUNCTION_HEAD(void, CopyImageSubData, GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyImageSubData, srcName, srcTarget, srcLevel, srcX, srcY, srcZ, dstName, dstTarget, dstLevel, dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth) DECLARE_GL_FUNCTION_HEAD(void, CopyImageSubData, GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyImageSubData, srcName, srcTarget, srcLevel, srcX, srcY, srcZ, dstName, dstTarget, dstLevel, dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DebugMessageControl, GLenum source, GLenum type, GLenum severity, GLsizei count, const GLuint* ids, GLboolean enabled) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DebugMessageControl, source, type, severity, count, ids, enabled) DECLARE_GL_FUNCTION_STUB_HEAD(void, DebugMessageControl, GLenum source, GLenum type, GLenum severity, GLsizei count, const GLuint* ids, GLboolean enabled) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DebugMessageControl, source, type, severity, count, ids, enabled)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DebugMessageInsert, GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* buf) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DebugMessageInsert, source, type, id, severity, length, buf) DECLARE_GL_FUNCTION_HEAD(void, DebugMessageInsert, GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* buf) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DebugMessageInsert, source, type, id, severity, length, buf)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DebugMessageCallback, GLDEBUGPROC callback, const void* userParam) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DebugMessageCallback, callback, userParam) DECLARE_GL_FUNCTION_STUB_HEAD(void, DebugMessageCallback, GLDEBUGPROC callback, const void* userParam) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DebugMessageCallback, callback, userParam)
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint, GetDebugMessageLog, GLuint count, GLsizei bufSize, GLenum* sources, GLenum* types, GLuint* ids, GLenum* severities, GLsizei* lengths, GLchar* messageLog) DECLARE_GL_FUNCTION_STUB_END(GLuint, GetDebugMessageLog, count, bufSize, sources, types, ids, severities, lengths, messageLog) DECLARE_GL_FUNCTION_STUB_HEAD(GLuint, GetDebugMessageLog, GLuint count, GLsizei bufSize, GLenum* sources, GLenum* types, GLuint* ids, GLenum* severities, GLsizei* lengths, GLchar* messageLog) DECLARE_GL_FUNCTION_STUB_END(GLuint, GetDebugMessageLog, count, bufSize, sources, types, ids, severities, lengths, messageLog)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PushDebugGroup, GLenum source, GLuint id, GLsizei length, const GLchar* message) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PushDebugGroup, source, id, length, message) DECLARE_GL_FUNCTION_HEAD(void, PushDebugGroup, GLenum source, GLuint id, GLsizei length, const GLchar* message) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PushDebugGroup, source, id, length, message)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PopDebugGroup) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PopDebugGroup) DECLARE_GL_FUNCTION_HEAD(void, PopDebugGroup) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PopDebugGroup)
MOBILEGL_GL_API void glObjectLabel(GLenum identifier, GLuint name, GLsizei length, const GLchar* label) { DECLARE_GL_FUNCTION_HEAD(void, ObjectLabel, GLenum identifier, GLuint name, GLsizei length, const GLchar* label) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ObjectLabel, identifier, name, length, label)
(void)identifier; DECLARE_GL_FUNCTION_HEAD(void, GetObjectLabel, GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetObjectLabel, identifier, name, bufSize, length, label)
(void)name;
(void)length;
(void)label;
}
MOBILEGL_GL_API void glGetObjectLabel(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label) {
(void)identifier;
(void)name;
if (length) {
*length = 0;
}
if (label && bufSize > 0) {
label[0] = '\0';
}
}
DECLARE_GL_FUNCTION_STUB_HEAD(void, ObjectPtrLabel, const void* ptr, GLsizei length, const GLchar* label) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ObjectPtrLabel, ptr, length, label) DECLARE_GL_FUNCTION_STUB_HEAD(void, ObjectPtrLabel, const void* ptr, GLsizei length, const GLchar* label) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ObjectPtrLabel, ptr, length, label)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetObjectPtrLabel, const void* ptr, GLsizei bufSize, GLsizei* length, GLchar* label) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetObjectPtrLabel, ptr, bufSize, length, label) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetObjectPtrLabel, const void* ptr, GLsizei bufSize, GLsizei* length, GLchar* label) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetObjectPtrLabel, ptr, bufSize, length, label)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetPointerv, GLenum pname, void** params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetPointerv, pname, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetPointerv, GLenum pname, void** params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetPointerv, pname, params)
@@ -424,7 +411,7 @@ DECLARE_GL_FUNCTION_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLs
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformuiv, GLuint program, GLint location, GLsizei bufSize, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformuiv, program, location, bufSize, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformuiv, GLuint program, GLint location, GLsizei bufSize, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformuiv, program, location, bufSize, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, MinSampleShading, GLfloat value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MinSampleShading, value) DECLARE_GL_FUNCTION_HEAD(void, MinSampleShading, GLfloat value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MinSampleShading, value)
DECLARE_GL_FUNCTION_HEAD(void, PatchParameteri, GLenum pname, GLint value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PatchParameteri, pname, value) DECLARE_GL_FUNCTION_HEAD(void, PatchParameteri, GLenum pname, GLint value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PatchParameteri, pname, value)
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIiv, GLenum target, GLenum pname, const GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIiv, target, pname, params) DECLARE_GL_FUNCTION_HEAD(void, TexParameterIiv, GLenum target, GLenum pname, const GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIiv, target, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIuiv, GLenum target, GLenum pname, const GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIuiv, target, pname, params) DECLARE_GL_FUNCTION_HEAD(void, TexParameterIuiv, GLenum target, GLenum pname, const GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIuiv, target, pname, params)
@@ -725,8 +712,8 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, LoadName, GLuint name) DECLARE_GL_FUNCTION_S
DECLARE_GL_FUNCTION_STUB_HEAD(void, PushName, GLuint name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PushName, name) DECLARE_GL_FUNCTION_STUB_HEAD(void, PushName, GLuint name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PushName, name)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PopName) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PopName) DECLARE_GL_FUNCTION_STUB_HEAD(void, PopName) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PopName)
DECLARE_GL_FUNCTION_HEAD(void, ClampColor, GLenum target, GLenum clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClampColor, target, clamp) DECLARE_GL_FUNCTION_HEAD(void, ClampColor, GLenum target, GLenum clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClampColor, target, clamp)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BeginConditionalRender, GLuint id, GLenum mode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BeginConditionalRender, id, mode) DECLARE_GL_FUNCTION_HEAD(void, BeginConditionalRender, GLuint id, GLenum mode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginConditionalRender, id, mode)
DECLARE_GL_FUNCTION_STUB_HEAD(void, EndConditionalRender, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndConditionalRender) DECLARE_GL_FUNCTION_HEAD(void, EndConditionalRender) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndConditionalRender)
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI1i, GLuint index, GLint x) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI1i, index, x) DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI1i, GLuint index, GLint x) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI1i, index, x)
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI2i, GLuint index, GLint x, GLint y) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI2i, index, x, y) DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI2i, GLuint index, GLint x, GLint y) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI2i, index, x, y)
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI3i, GLuint index, GLint x, GLint y, GLint z) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI3i, index, x, y, z) DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI3i, GLuint index, GLint x, GLint y, GLint z) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI3i, index, x, y, z)
@@ -936,7 +923,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveSubroutineName, GLuint program, GLe
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformSubroutinesuiv, GLenum shadertype, GLsizei count, const GLuint* indices) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformSubroutinesuiv, shadertype, count, indices) DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformSubroutinesuiv, GLenum shadertype, GLsizei count, const GLuint* indices) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformSubroutinesuiv, shadertype, count, indices)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetUniformSubroutineuiv, GLenum shadertype, GLint location, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetUniformSubroutineuiv, shadertype, location, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetUniformSubroutineuiv, GLenum shadertype, GLint location, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetUniformSubroutineuiv, shadertype, location, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStageiv, GLuint program, GLenum shadertype, GLenum pname, GLint* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStageiv, program, shadertype, pname, values) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStageiv, GLuint program, GLenum shadertype, GLenum pname, GLint* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStageiv, program, shadertype, pname, values)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PatchParameterfv, GLenum pname, const GLfloat* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PatchParameterfv, pname, values) DECLARE_GL_FUNCTION_HEAD(void, PatchParameterfv, GLenum pname, const GLfloat* values) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PatchParameterfv, pname, values)
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedback, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedback, mode, id) DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedback, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedback, mode, id)
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStream, GLenum mode, GLuint id, GLuint stream) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStream, mode, id, stream) DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStream, GLenum mode, GLuint id, GLuint stream) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStream, mode, id, stream)
DECLARE_GL_FUNCTION_HEAD(void, BeginQueryIndexed, GLenum target, GLuint index, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginQueryIndexed, target, index, id) DECLARE_GL_FUNCTION_HEAD(void, BeginQueryIndexed, GLenum target, GLuint index, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginQueryIndexed, target, index, id)
@@ -969,24 +956,24 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL3dv, GLuint index, const GLdoub
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL4dv, GLuint index, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL4dv, index, v) DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL4dv, GLuint index, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL4dv, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLPointer, GLuint index, GLint size, GLenum type, GLsizei stride, const void* pointer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLPointer, index, size, type, stride, pointer) DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLPointer, GLuint index, GLint size, GLenum type, GLsizei stride, const void* pointer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLPointer, index, size, type, stride, pointer)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexAttribLdv, GLuint index, GLenum pname, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexAttribLdv, index, pname, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexAttribLdv, GLuint index, GLenum pname, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexAttribLdv, index, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportArrayv, GLuint first, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportArrayv, first, count, v) DECLARE_GL_FUNCTION_HEAD(void, ViewportArrayv, GLuint first, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportArrayv, first, count, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportIndexedf, GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportIndexedf, index, x, y, w, h) DECLARE_GL_FUNCTION_HEAD(void, ViewportIndexedf, GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportIndexedf, index, x, y, w, h)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ViewportIndexedfv, index, v) DECLARE_GL_FUNCTION_HEAD(void, ViewportIndexedfv, GLuint index, const GLfloat* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ViewportIndexedfv, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorArrayv, GLuint first, GLsizei count, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorArrayv, first, count, v) DECLARE_GL_FUNCTION_HEAD(void, ScissorArrayv, GLuint first, GLsizei count, const GLint* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorArrayv, first, count, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexed, GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexed, index, left, bottom, width, height) DECLARE_GL_FUNCTION_HEAD(void, ScissorIndexed, GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorIndexed, index, left, bottom, width, height)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexedv, index, v) DECLARE_GL_FUNCTION_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ScissorIndexedv, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeArrayv, first, count, v) DECLARE_GL_FUNCTION_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DepthRangeArrayv, first, count, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeIndexed, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeIndexed, index, n, f) DECLARE_GL_FUNCTION_HEAD(void, DepthRangeIndexed, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DepthRangeIndexed, index, n, f)
DECLARE_GL_FUNCTION_HEAD(void, GetFloati_v, GLenum target, GLuint index, GLfloat* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetFloati_v, target, index, data) DECLARE_GL_FUNCTION_HEAD(void, GetFloati_v, GLenum target, GLuint index, GLfloat* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetFloati_v, target, index, data)
DECLARE_GL_FUNCTION_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdouble* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetDoublei_v, target, index, data) DECLARE_GL_FUNCTION_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdouble* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetDoublei_v, target, index, data)
DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysInstancedBaseInstance, mode, first, count, instancecount, baseinstance) DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysInstancedBaseInstance, mode, first, count, instancecount, baseinstance)
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseInstance, mode, count, type, indices, instancecount, baseinstance) DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseInstance, mode, count, type, indices, instancecount, baseinstance)
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertexBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertexBaseInstance, mode, count, type, indices, instancecount, basevertex, baseinstance) DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertexBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertexBaseInstance, mode, count, type, indices, instancecount, basevertex, baseinstance)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params) DECLARE_GL_FUNCTION_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount) DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount)
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount) DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data) DECLARE_GL_FUNCTION_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferSubData, GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferSubData, target, internalformat, offset, size, format, type, data) DECLARE_GL_FUNCTION_HEAD(void, ClearBufferSubData, GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferSubData, target, internalformat, offset, size, format, type, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetInternalformati64v, GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetInternalformati64v, target, internalformat, pname, count, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetInternalformati64v, GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetInternalformati64v, target, internalformat, pname, count, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth) DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth)
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateTexImage, GLuint texture, GLint level) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateTexImage, texture, level) DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateTexImage, GLuint texture, GLint level) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateTexImage, texture, level)
@@ -996,18 +983,18 @@ DECLARE_GL_FUNCTION_HEAD(void, MultiDrawArraysIndirect, GLenum mode, const void*
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawElementsIndirect, GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawElementsIndirect, mode, type, indirect, drawcount, stride) DECLARE_GL_FUNCTION_HEAD(void, MultiDrawElementsIndirect, GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawElementsIndirect, mode, type, indirect, drawcount, stride)
DECLARE_GL_FUNCTION_HEAD(GLint, GetProgramResourceLocationIndex, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLint, GetProgramResourceLocationIndex, program, programInterface, name) DECLARE_GL_FUNCTION_HEAD(GLint, GetProgramResourceLocationIndex, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLint, GetProgramResourceLocationIndex, program, programInterface, name)
DECLARE_GL_FUNCTION_HEAD(void, ShaderStorageBlockBinding, GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderStorageBlockBinding, program, storageBlockIndex, storageBlockBinding) DECLARE_GL_FUNCTION_HEAD(void, ShaderStorageBlockBinding, GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderStorageBlockBinding, program, storageBlockIndex, storageBlockBinding)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers) DECLARE_GL_FUNCTION_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers)
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset) DECLARE_GL_FUNCTION_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset)
DECLARE_GL_FUNCTION_HEAD(void, BufferStorage, GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BufferStorage, target, size, data, flags) DECLARE_GL_FUNCTION_HEAD(void, BufferStorage, GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BufferStorage, target, size, data, flags)
DECLARE_GL_FUNCTION_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data) DECLARE_GL_FUNCTION_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data)
DECLARE_GL_FUNCTION_HEAD(void, ClearTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data) DECLARE_GL_FUNCTION_HEAD(void, ClearTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data)
DECLARE_GL_FUNCTION_HEAD(void, BindBuffersBase, GLenum target, GLuint first, GLsizei count, const GLuint* buffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersBase, target, first, count, buffers) DECLARE_GL_FUNCTION_HEAD(void, BindBuffersBase, GLenum target, GLuint first, GLsizei count, const GLuint* buffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersBase, target, first, count, buffers)
DECLARE_GL_FUNCTION_HEAD(void, BindBuffersRange, GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizeiptr* sizes) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersRange, target, first, count, buffers, offsets, sizes) DECLARE_GL_FUNCTION_HEAD(void, BindBuffersRange, GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizeiptr* sizes) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBuffersRange, target, first, count, buffers, offsets, sizes)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTextures, first, count, textures) DECLARE_GL_FUNCTION_HEAD(void, BindTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindTextures, first, count, textures)
DECLARE_GL_FUNCTION_HEAD(void, BindSamplers, GLuint first, GLsizei count, const GLuint* samplers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindSamplers, first, count, samplers) DECLARE_GL_FUNCTION_HEAD(void, BindSamplers, GLuint first, GLsizei count, const GLuint* samplers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindSamplers, first, count, samplers)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindImageTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindImageTextures, first, count, textures) DECLARE_GL_FUNCTION_HEAD(void, BindImageTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindImageTextures, first, count, textures)
DECLARE_GL_FUNCTION_HEAD(void, BindVertexBuffers, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizei* strides) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindVertexBuffers, first, count, buffers, offsets, strides) DECLARE_GL_FUNCTION_HEAD(void, BindVertexBuffers, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizei* strides) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindVertexBuffers, first, count, buffers, offsets, strides)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClipControl, GLenum origin, GLenum depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClipControl, origin, depth) DECLARE_GL_FUNCTION_HEAD(void, ClipControl, GLenum origin, GLenum depth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClipControl, origin, depth)
DECLARE_GL_FUNCTION_HEAD(void, CreateTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateTransformFeedbacks, n, ids) DECLARE_GL_FUNCTION_HEAD(void, CreateTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateTransformFeedbacks, n, ids)
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferBase, GLuint xfb, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferBase, xfb, index, buffer) DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferBase, GLuint xfb, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferBase, xfb, index, buffer)
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferRange, GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferRange, xfb, index, buffer, offset, size) DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferRange, GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferRange, xfb, index, buffer, offset, size)
@@ -1060,9 +1047,9 @@ DECLARE_GL_FUNCTION_HEAD(void, TextureStorage3DMultisample, GLuint texture, GLsi
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage1D, texture, level, xoffset, width, format, type, pixels) DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage1D, texture, level, xoffset, width, format, type, pixels)
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, type, pixels) DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, type, pixels)
DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels) DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data) DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data) DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data) DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width) DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height) DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height)
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height) DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height)
@@ -1120,11 +1107,11 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnConvolutionFilter, GLenum target, GLenum
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnSeparableFilter, GLenum target, GLenum format, GLenum type, GLsizei rowBufSize, void* row, GLsizei columnBufSize, void* column, void* span) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnSeparableFilter, target, format, type, rowBufSize, row, columnBufSize, column, span) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnSeparableFilter, GLenum target, GLenum format, GLenum type, GLsizei rowBufSize, void* row, GLsizei columnBufSize, void* column, void* span) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnSeparableFilter, target, format, type, rowBufSize, row, columnBufSize, column, span)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnHistogram, GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnHistogram, target, reset, format, type, bufSize, values) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnHistogram, GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnHistogram, target, reset, format, type, bufSize, values)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnMinmax, GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnMinmax, target, reset, format, type, bufSize, values) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnMinmax, GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnMinmax, target, reset, format, type, bufSize, values)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureBarrier, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureBarrier, ) DECLARE_GL_FUNCTION_HEAD(void, TextureBarrier, void) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBarrier, )
DECLARE_GL_FUNCTION_STUB_HEAD(void, SpecializeShader, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SpecializeShader, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue) DECLARE_GL_FUNCTION_HEAD(void, SpecializeShader, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SpecializeShader, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawArraysIndirectCount, GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawArraysIndirectCount, mode, indirect, drawcount, maxdrawcount, stride) DECLARE_GL_FUNCTION_HEAD(void, MultiDrawArraysIndirectCount, GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawArraysIndirectCount, mode, indirect, drawcount, maxdrawcount, stride)
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawElementsIndirectCount, GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawElementsIndirectCount, mode, type, indirect, drawcount, maxdrawcount, stride) DECLARE_GL_FUNCTION_HEAD(void, MultiDrawElementsIndirectCount, GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawElementsIndirectCount, mode, type, indirect, drawcount, maxdrawcount, stride)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PolygonOffsetClamp, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PolygonOffsetClamp, factor, units, clamp) DECLARE_GL_FUNCTION_HEAD(void, PolygonOffsetClamp, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PolygonOffsetClamp, factor, units, clamp)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBoxARB, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END(void, PrimitiveBoundingBoxARB, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW) DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBoxARB, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END(void, PrimitiveBoundingBoxARB, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW)
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint64, GetTextureHandleARB, GLuint texture) DECLARE_GL_FUNCTION_STUB_END(GLuint64, GetTextureHandleARB, texture) DECLARE_GL_FUNCTION_STUB_HEAD(GLuint64, GetTextureHandleARB, GLuint texture) DECLARE_GL_FUNCTION_STUB_END(GLuint64, GetTextureHandleARB, texture)
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint64, GetTextureSamplerHandleARB, GLuint texture, GLuint sampler) DECLARE_GL_FUNCTION_STUB_END(GLuint64, GetTextureSamplerHandleARB, texture, sampler) DECLARE_GL_FUNCTION_STUB_HEAD(GLuint64, GetTextureSamplerHandleARB, GLuint texture, GLuint sampler) DECLARE_GL_FUNCTION_STUB_END(GLuint64, GetTextureSamplerHandleARB, texture, sampler)
@@ -1163,7 +1150,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramLocalParameterdvARB, GLenum target
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramLocalParameterfvARB, GLenum target, GLuint index, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramLocalParameterfvARB, target, index, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramLocalParameterfvARB, GLenum target, GLuint index, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramLocalParameterfvARB, target, index, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStringARB, GLenum target, GLenum pname, void* string) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStringARB, target, pname, string) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStringARB, GLenum target, GLenum pname, void* string) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStringARB, target, pname, string)
DECLARE_GL_FUNCTION_STUB_HEAD(void, FramebufferTextureFaceARB, GLenum target, GLenum attachment, GLuint texture, GLint level, GLenum face) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, FramebufferTextureFaceARB, target, attachment, texture, level, face) DECLARE_GL_FUNCTION_STUB_HEAD(void, FramebufferTextureFaceARB, GLenum target, GLenum attachment, GLuint texture, GLint level, GLenum face) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, FramebufferTextureFaceARB, target, attachment, texture, level, face)
DECLARE_GL_FUNCTION_STUB_HEAD(void, SpecializeShaderARB, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SpecializeShaderARB, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue) DECLARE_GL_FUNCTION_HEAD(void, SpecializeShaderARB, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SpecializeShader, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform1i64ARB, GLint location, GLint64 x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform1i64ARB, location, x) DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform1i64ARB, GLint location, GLint64 x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform1i64ARB, location, x)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform2i64ARB, GLint location, GLint64 x, GLint64 y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform2i64ARB, location, x, y) DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform2i64ARB, GLint location, GLint64 x, GLint64 y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform2i64ARB, location, x, y)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform3i64ARB, GLint location, GLint64 x, GLint64 y, GLint64 z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform3i64ARB, location, x, y, z) DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform3i64ARB, GLint location, GLint64 x, GLint64 y, GLint64 z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform3i64ARB, location, x, y, z)
@@ -1848,9 +1835,9 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetBooleanIndexedvEXT, GLenum target, GLuint
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage3DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage3DEXT, texture, target, level, internalformat, width, height, depth, border, imageSize, bits) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage3DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage3DEXT, texture, target, level, internalformat, width, height, depth, border, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage2DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage2DEXT, texture, target, level, internalformat, width, height, border, imageSize, bits) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage2DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage2DEXT, texture, target, level, internalformat, width, height, border, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage1DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage1DEXT, texture, target, level, internalformat, width, border, imageSize, bits) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureImage1DEXT, GLuint texture, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureImage1DEXT, texture, target, level, internalformat, width, border, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3DEXT, texture, target, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, bits) DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage3DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2DEXT, texture, target, level, xoffset, yoffset, width, height, format, imageSize, bits) DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage2DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1DEXT, texture, target, level, xoffset, width, format, imageSize, bits) DECLARE_GL_FUNCTION_HEAD(void, CompressedTextureSubImage1DEXT, GLuint texture, GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureImageEXT, GLuint texture, GLenum target, GLint lod, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureImageEXT, texture, target, lod, img) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureImageEXT, GLuint texture, GLenum target, GLint lod, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureImageEXT, texture, target, lod, img)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage3DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage3DEXT, texunit, target, level, internalformat, width, height, depth, border, imageSize, bits) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage3DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage3DEXT, texunit, target, level, internalformat, width, height, depth, border, imageSize, bits)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage2DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage2DEXT, texunit, target, level, internalformat, width, height, border, imageSize, bits) DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedMultiTexImage2DEXT, GLenum texunit, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void* bits) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedMultiTexImage2DEXT, texunit, target, level, internalformat, width, height, border, imageSize, bits)
@@ -2062,7 +2049,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetPixelTransformParameterivEXT, GLenum targ
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetPixelTransformParameterfvEXT, GLenum target, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetPixelTransformParameterfvEXT, target, pname, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, GetPixelTransformParameterfvEXT, GLenum target, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetPixelTransformParameterfvEXT, target, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfEXT, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfEXT, pname, param) DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfEXT, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfEXT, pname, param)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfvEXT, GLenum pname, const GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfvEXT, pname, params) DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfvEXT, GLenum pname, const GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfvEXT, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PolygonOffsetClampEXT, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PolygonOffsetClampEXT, factor, units, clamp) DECLARE_GL_FUNCTION_HEAD(void, PolygonOffsetClampEXT, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PolygonOffsetClamp, factor, units, clamp)
DECLARE_GL_FUNCTION_HEAD(void, ProvokingVertexEXT, GLenum mode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProvokingVertex, mode) DECLARE_GL_FUNCTION_HEAD(void, ProvokingVertexEXT, GLenum mode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProvokingVertex, mode)
DECLARE_GL_FUNCTION_STUB_HEAD(void, RasterSamplesEXT, GLuint samples, GLboolean fixedsamplelocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, RasterSamplesEXT, samples, fixedsamplelocations) DECLARE_GL_FUNCTION_STUB_HEAD(void, RasterSamplesEXT, GLuint samples, GLboolean fixedsamplelocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, RasterSamplesEXT, samples, fixedsamplelocations)
DECLARE_GL_FUNCTION_STUB_HEAD(void, SecondaryColor3bEXT, GLbyte red, GLbyte green, GLbyte blue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SecondaryColor3bEXT, red, green, blue) DECLARE_GL_FUNCTION_STUB_HEAD(void, SecondaryColor3bEXT, GLbyte red, GLbyte green, GLbyte blue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SecondaryColor3bEXT, red, green, blue)
@@ -2559,7 +2546,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateImageBarrierNV, GLboolean synchro
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateImagePaletteNV, GLuint viewport, GLuint first, GLsizei count, const GLenum* rates) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateImagePaletteNV, viewport, first, count, rates) DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateImagePaletteNV, GLuint viewport, GLuint first, GLsizei count, const GLenum* rates) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateImagePaletteNV, viewport, first, count, rates)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateSampleOrderNV, GLenum order) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateSampleOrderNV, order) DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateSampleOrderNV, GLenum order) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateSampleOrderNV, order)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateSampleOrderCustomNV, GLenum rate, GLuint samples, const GLint* locations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateSampleOrderCustomNV, rate, samples, locations) DECLARE_GL_FUNCTION_STUB_HEAD(void, ShadingRateSampleOrderCustomNV, GLenum rate, GLuint samples, const GLint* locations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShadingRateSampleOrderCustomNV, rate, samples, locations)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureBarrierNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureBarrierNV, ) DECLARE_GL_FUNCTION_HEAD(void, TextureBarrierNV, void) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBarrier, )
DECLARE_GL_FUNCTION_STUB_HEAD(void, TexImage2DMultisampleCoverageNV, GLenum target, GLsizei coverageSamples, GLsizei colorSamples, GLint internalFormat, GLsizei width, GLsizei height, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexImage2DMultisampleCoverageNV, target, coverageSamples, colorSamples, internalFormat, width, height, fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_HEAD(void, TexImage2DMultisampleCoverageNV, GLenum target, GLsizei coverageSamples, GLsizei colorSamples, GLint internalFormat, GLsizei width, GLsizei height, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexImage2DMultisampleCoverageNV, target, coverageSamples, colorSamples, internalFormat, width, height, fixedSampleLocations)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TexImage3DMultisampleCoverageNV, GLenum target, GLsizei coverageSamples, GLsizei colorSamples, GLint internalFormat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexImage3DMultisampleCoverageNV, target, coverageSamples, colorSamples, internalFormat, width, height, depth, fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_HEAD(void, TexImage3DMultisampleCoverageNV, GLenum target, GLsizei coverageSamples, GLsizei colorSamples, GLint internalFormat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexImage3DMultisampleCoverageNV, target, coverageSamples, colorSamples, internalFormat, width, height, depth, fixedSampleLocations)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureImage2DMultisampleNV, GLuint texture, GLenum target, GLsizei samples, GLint internalFormat, GLsizei width, GLsizei height, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureImage2DMultisampleNV, texture, target, samples, internalFormat, width, height, fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureImage2DMultisampleNV, GLuint texture, GLenum target, GLsizei samples, GLint internalFormat, GLsizei width, GLsizei height, GLboolean fixedSampleLocations) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureImage2DMultisampleNV, texture, target, samples, internalFormat, width, height, fixedSampleLocations)
@@ -2585,7 +2572,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedbackNV, GLenum target, GLui
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacksNV, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacksNV, n, ids) DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacksNV, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacksNV, n, ids)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacksNV, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacksNV, n, ids) DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacksNV, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacksNV, n, ids)
MOBILEGL_GL_API GLboolean glIsTransformFeedbackNV(GLuint id) { MOBILEGL_GL_API GLboolean glIsTransformFeedbackNV(GLuint id) {
MGLOG_W("Stub function: %s(...)", __FUNCTION__); MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__);
return GL_FALSE; return GL_FALSE;
} }
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedbackNV, ) DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedbackNV, )
@@ -3181,5 +3168,5 @@ MOBILEGL_GL_API void glVertexAttribDivisorARB(GLuint index, GLuint divisor) {
} }
MOBILEGL_GL_API void glWindowRectanglesEXT(GLenum mode, GLsizei count, const GLint* box) { MOBILEGL_GL_API void glWindowRectanglesEXT(GLenum mode, GLsizei count, const GLint* box) {
MGLOG_W("Stub function: %s(...)", __FUNCTION__); MGLOG_W_ONCE("Stub function: %s(...)", __FUNCTION__);
} }
@@ -13,7 +13,9 @@
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
#include <MG_Util/Metrics/TextureMetrics.h> #include <MG_Util/Metrics/TextureMetrics.h>
#include <MG_Impl/GLImpl/Texture/Validators.h> #include <MG_Impl/GLImpl/Texture/Validators.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_State/GLState/ErrorState/Error.h> #include <MG_State/GLState/ErrorState/Error.h>
#include <MG_Impl/Pipe/PipeFill.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h> #include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h> #include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h> #include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
@@ -473,6 +475,75 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
} }
// GL 4.6 core 9.2.8 conditions that depend only on the framebuffer and the attachment
// point. Shared, because glFramebufferTexture / 1D / 2D / 3D / TextureLayer are aliases of
// one another in that section and a CTS case that walks the family must not get five
// different answers - which is exactly what happened when these lived in one helper that
// only two of the five went through.
Bool ValidateFramebufferTextureAttachmentPoint(const char* functionName,
const SharedPtr<MG_State::GLState::FramebufferObject>&
framebufferObject,
FramebufferAttachmentType attachmentType) {
// "An INVALID_OPERATION error is generated if COLOR_ATTACHMENTm is used with m greater
// than or equal to MAX_COLOR_ATTACHMENTS."
if (!FramebufferImpl::ValidateColorAttachmentInRange(attachmentType, functionName)) return false;
// "An INVALID_OPERATION error is generated if zero is bound to target." MobileGL keeps
// a real FramebufferObject for framebuffer 0, so a null test can never see this - the
// object is always there, and framebuffer 0 has to be recognised by identity instead,
// the same comparison DrawBuffers_State makes. Without this an attach onto the default
// framebuffer silently REPLACED its colour attachment, permanently desynchronising it
// from what the swapchain keeps publishing.
const auto& defaultFramebufferInfo = FramebufferImpl::pDefaultFramebufferInfo;
if (!framebufferObject ||
(defaultFramebufferInfo && framebufferObject == defaultFramebufferInfo->defaultFBO)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", functionName,
"No framebuffer object is bound to the target; the default framebuffer's attachments "
"cannot be named."));
return false;
}
return true;
}
// The other half of 9.2.8: "level must be greater than or equal to zero", and for a
// texture with immutable storage it "must be smaller than the number of levels the texture
// has". Split from the attachment-point half because the caller only has a texture object
// once the detach (texture == 0) case is behind it.
Bool ValidateFramebufferTextureLevel(const char* functionName,
const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
GLint level) {
if (level < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Texture level must be non-negative."));
return false;
}
if (!textureObject || !textureObject->IsImmutable()) {
// A mutable texture has no level bound here: a level it has not specified yet is
// not an error, it just leaves the framebuffer incomplete.
return true;
}
// GetAddressableLevelCount(), NOT GetImmutableLevels(): for a VIEW the latter is
// deliberately the ORIGINAL texture's count (GL 4.6 core 8.18 defines
// TEXTURE_IMMUTABLE_LEVELS on a view that way), which is far too large a bound - a
// two-level view onto a ten-level texture would accept level 5 and attach an image
// nothing can draw into.
const Uint levelBound = textureObject->GetAddressableLevelCount();
if (static_cast<Uint>(level) >= levelBound) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", functionName,
std::format("Texture level {} is beyond the {} level(s) this texture has.", level,
levelBound)));
return false;
}
return true;
}
void AttachFramebufferTextureWithUploadTarget(const char* functionName, GLenum target, GLenum attachment, void AttachFramebufferTextureWithUploadTarget(const char* functionName, GLenum target, GLenum attachment,
GLuint texture, GLint level, GLuint texture, GLint level,
TextureUploadTarget textureUploadTarget, Bool layered = false) { TextureUploadTarget textureUploadTarget, Bool layered = false) {
@@ -481,10 +552,24 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
if (attachment == GL_DEPTH_STENCIL_ATTACHMENT) { if (attachment == GL_DEPTH_STENCIL_ATTACHMENT) {
// `layered` has to travel with the split. GL_DEPTH_STENCIL_ATTACHMENT is only a
// shorthand for attaching the same image to both halves (GL 4.6 core 9.2.6), so
// whether glFramebufferTexture made it LAYERED is a property of the call, not of
// which half is being recorded - and dropping it here (the parameter defaults to
// false) recorded a non-layered depth/stencil attachment beside a layered colour
// one for every layered target. That is an inconsistent framebuffer by 9.4.1's
// own rule, and downstream it means the depth/stencil attachment covers layer 0
// alone: DirectVulkan built its view with layerCount 1 under a framebuffer
// declaring N layers (VUID-VkFramebufferCreateInfo-flags-04535), and DirectGLES
// attached one layer of it beside a layered colour target, which the driver
// answers with GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS - every draw silently
// produced nothing. This is the shape
// texture_cube_map_array.stencil_attachments_*_layered and
// geometry_shader.layered_framebuffer.stencil_support are built on.
AttachFramebufferTextureWithUploadTarget(functionName, target, GL_DEPTH_ATTACHMENT, texture, level, AttachFramebufferTextureWithUploadTarget(functionName, target, GL_DEPTH_ATTACHMENT, texture, level,
textureUploadTarget); textureUploadTarget, layered);
AttachFramebufferTextureWithUploadTarget(functionName, target, GL_STENCIL_ATTACHMENT, texture, level, AttachFramebufferTextureWithUploadTarget(functionName, target, GL_STENCIL_ATTACHMENT, texture, level,
textureUploadTarget); textureUploadTarget, layered);
return; return;
} }
@@ -496,13 +581,7 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget); auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget);
auto& framebufferObject = bindingSlot.GetBoundObject(); auto& framebufferObject = bindingSlot.GetBoundObject();
if (!framebufferObject) { if (!ValidateFramebufferTextureAttachmentPoint(functionName, framebufferObject, attachmentType)) return;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Framebuffer target is bound to no framebuffer object."));
return;
}
if (texture == 0) { if (texture == 0) {
framebufferObject->Detach(attachmentType); framebufferObject->Detach(attachmentType);
@@ -517,6 +596,7 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Texture object {} is not valid.", texture))); std::format("Texture object {} is not valid.", texture)));
return; return;
} }
if (!ValidateFramebufferTextureLevel(functionName, textureObject, level)) return;
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget); const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
if (expectedTextureTarget == TextureTarget::Unknown || if (expectedTextureTarget == TextureTarget::Unknown ||
@@ -537,6 +617,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void BlitFramebuffer_Backend(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, void BlitFramebuffer_Backend(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) {
MGP_FILL(BlitFramebuffer);
MG_Backend::gBackendFunctionsTable.GL.BlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, MG_Backend::gBackendFunctionsTable.GL.BlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1,
mask, filter); mask, filter);
} }
@@ -547,9 +628,10 @@ namespace MobileGL::MG_Impl::GLImpl {
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) {
auto blitNamedFramebuffer = MG_Backend::gBackendFunctionsTable.GL.BlitNamedFramebuffer; auto blitNamedFramebuffer = MG_Backend::gBackendFunctionsTable.GL.BlitNamedFramebuffer;
if (!blitNamedFramebuffer) { if (!blitNamedFramebuffer) {
MGLOG_E("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit."); MGLOG_E_ONCE("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
return; return;
} }
MGP_FILL(BlitNamedFramebuffer);
blitNamedFramebuffer(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, blitNamedFramebuffer(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
dstY1, mask, filter); dstY1, mask, filter);
} }
@@ -558,9 +640,10 @@ namespace MobileGL::MG_Impl::GLImpl {
GLenum buffer, GLint drawbuffer, const GLfloat* value) { GLenum buffer, GLint drawbuffer, const GLfloat* value) {
auto clearNamedFramebufferfv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfv; auto clearNamedFramebufferfv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfv;
if (!clearNamedFramebufferfv) { if (!clearNamedFramebufferfv) {
MGLOG_E("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear."); MGLOG_E_ONCE("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
return; return;
} }
MGP_FILL(ClearNamedFramebufferfv);
clearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value); clearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
} }
@@ -568,9 +651,10 @@ namespace MobileGL::MG_Impl::GLImpl {
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
auto clearNamedFramebufferfi = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfi; auto clearNamedFramebufferfi = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfi;
if (!clearNamedFramebufferfi) { if (!clearNamedFramebufferfi) {
MGLOG_E("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear."); MGLOG_E_ONCE("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
return; return;
} }
MGP_FILL(ClearNamedFramebufferfi);
clearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil); clearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
} }
@@ -578,9 +662,10 @@ namespace MobileGL::MG_Impl::GLImpl {
GLenum buffer, GLint drawbuffer, const GLint* value) { GLenum buffer, GLint drawbuffer, const GLint* value) {
auto clearNamedFramebufferiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferiv; auto clearNamedFramebufferiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferiv;
if (!clearNamedFramebufferiv) { if (!clearNamedFramebufferiv) {
MGLOG_E("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear."); MGLOG_E_ONCE("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
return; return;
} }
MGP_FILL(ClearNamedFramebufferiv);
clearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value); clearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
} }
@@ -588,9 +673,10 @@ namespace MobileGL::MG_Impl::GLImpl {
GLenum buffer, GLint drawbuffer, const GLuint* value) { GLenum buffer, GLint drawbuffer, const GLuint* value) {
auto clearNamedFramebufferuiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferuiv; auto clearNamedFramebufferuiv = MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferuiv;
if (!clearNamedFramebufferuiv) { if (!clearNamedFramebufferuiv) {
MGLOG_E("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear."); MGLOG_E_ONCE("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
return; return;
} }
MGP_FILL(ClearNamedFramebufferuiv);
clearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value); clearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
} }
@@ -617,7 +703,67 @@ namespace MobileGL::MG_Impl::GLImpl {
if (MG_Backend::pActiveBackendObject == nullptr) { if (MG_Backend::pActiveBackendObject == nullptr) {
return std::numeric_limits<Int>::max(); return std::numeric_limits<Int>::max();
} }
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, 1); return GetAdvertisedMaxSamples();
}
// GL_MAX_SAMPLES is the ceiling over all formats; an integer format has its own
// (GL_MAX_INTEGER_SAMPLES) and GL 4.6 core 9.2.4 makes exceeding it INVALID_OPERATION.
// The multisample TEXTURE path resolves the limit per format the same way
// (GL_Texture.cpp, GetMaxSupportedTextureSamples), and both now enforce exactly what their
// pname advertises. The integer ceiling used to be floored at GL_MAX_SAMPLES so that the
// frontend would accept a count it had advertised globally - but on Adreno and Mali the
// integer path is genuinely one sample, and accepting four only moved the failure from an
// honest INVALID_OPERATION here to a silently under-allocated renderbuffer.
// The head of the per-format renderbuffer sample list the backend probed, or 0 when nothing
// was probed for it. Same shape as GetProbedMaxTextureSamples in GL_Texture.cpp, and reads
// the same cache glGetInternalformativ(GL_RENDERBUFFER, ..., GL_SAMPLES) answers from.
static Int GetProbedMaxRenderbufferSamples(TextureInternalFormat format) {
if (MG_Backend::pActiveBackendObject == nullptr) {
return 0;
}
const SizeT targetIndex = MG_Backend::GetRenderbufferFormatCapabilityTargetIndex();
const SizeT formatIndex = static_cast<SizeT>(format);
if (targetIndex >= MG_Backend::kFormatCapabilityTargetCount ||
formatIndex >= MG_Backend::kFormatCapabilityFormatCount) {
return 0;
}
const auto& sampleCounts =
MG_Backend::pActiveBackendObject->GetFormatCapabilities().SampleCounts[targetIndex][formatIndex];
return sampleCounts.empty() ? 0 : sampleCounts.front();
}
Int GetMaxRenderbufferSamplesForFormat_State(TextureInternalFormat format) {
if (MG_Backend::pActiveBackendObject == nullptr) {
return std::numeric_limits<Int>::max();
}
GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(format);
GLenum normalizedFormat = GL_RGBA;
GLenum normalizedType = GL_UNSIGNED_BYTE;
MG_Util::TextureFormatProcessor::NormalizePixelFormat(normalizedInternalFormat,
PixelFormatNormalizeOptionBit::None,
&normalizedInternalFormat, &normalizedFormat,
&normalizedType);
const Bool isIntegerFormat = normalizedFormat == GL_RED_INTEGER || normalizedFormat == GL_RG_INTEGER ||
normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER;
// The per-format probe first, for the same reason the texture path takes it first: GL 4.6
// core 9.2.4 words the error as "samples is greater than the maximum number of samples
// supported for internalformat (see GetInternalformativ)", and
// glGetInternalformativ(GL_RENDERBUFFER, ..., GL_SAMPLES) is answered from exactly this
// list. It was never consulted here - the TODO that deferred it was written before the
// query was backed and had gone stale - so a format whose multisample probes fail inside
// a category that allows four was accepted at four, quietly allocated at one by
// ClampSamplesToBackendSupport, and then reported as four by
// glGetRenderbufferParameteriv(GL_RENDERBUFFER_SAMPLES).
const Int probedMaxSamples = GetProbedMaxRenderbufferSamples(format);
if (probedMaxSamples > 0) {
return probedMaxSamples;
}
if (!isIntegerFormat) {
return GetMaxRenderbufferSamples_State();
}
// Exactly what glGetIntegerv(GL_MAX_INTEGER_SAMPLES) reports.
return GetAdvertisedIntegerMaxSamples();
} }
Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) { Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) {
@@ -641,7 +787,7 @@ namespace MobileGL::MG_Impl::GLImpl {
return true; return true;
} }
Bool ValidateRenderbufferStorageSamples_State(GLsizei samples, const char* caller) { Bool ValidateRenderbufferStorageSamples_State(GLsizei samples, TextureInternalFormat format, const char* caller) {
if (samples < 0) { if (samples < 0) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, ErrorCode::InvalidValue,
@@ -649,9 +795,12 @@ namespace MobileGL::MG_Impl::GLImpl {
return false; return false;
} }
const Int maxSamples = GetMaxRenderbufferSamples_State(); // Per-internalformat, from the probe list glGetInternalformativ answers with, falling back
// to the format's category pname where nothing was probed. (This carried a TODO deferring
// the per-format resolution "once glGetInternalformativ is backed"; it has been backed for
// both renderbuffers and multisample textures since, so the deferral was collected.)
const Int maxSamples = GetMaxRenderbufferSamplesForFormat_State(format);
if (samples > maxSamples) { if (samples > maxSamples) {
// TODO: Use per-internalformat renderbuffer sample limits once glGetInternalformativ is backed.
// GL 4.6 core 9.2.4 makes asking for more samples than the format supports // GL 4.6 core 9.2.4 makes asking for more samples than the format supports
// INVALID_OPERATION, not INVALID_VALUE - the count is well formed, this format just // INVALID_OPERATION, not INVALID_VALUE - the count is well formed, this format just
// cannot deliver it. Only a negative count is INVALID_VALUE. // cannot deliver it. Only a negative count is INVALID_VALUE.
@@ -659,7 +808,7 @@ namespace MobileGL::MG_Impl::GLImpl {
ErrorCode::InvalidOperation, ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>( MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", caller, "MG_Impl/GLImpl", caller,
std::format("Sample count {} exceeds GL_MAX_SAMPLES ({}).", samples, maxSamples))); std::format("Sample count {} exceeds this format's sample limit ({}).", samples, maxSamples)));
return false; return false;
} }
return true; return true;
@@ -684,7 +833,7 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureInternalFormat format = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat); TextureInternalFormat format = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
if (!TextureImpl::ValidateTextureInternalFormat(format)) return; if (!TextureImpl::ValidateTextureInternalFormat(format)) return;
if (!ValidateRenderbufferStorageSamples_State(samples, kCaller)) return; if (!ValidateRenderbufferStorageSamples_State(samples, format, kCaller)) return;
if (!ValidateRenderbufferStorageSize_State(width, height, kCaller)) return; if (!ValidateRenderbufferStorageSize_State(width, height, kCaller)) return;
renderbufferObject->AllocateStorage({width, height}); renderbufferObject->AllocateStorage({width, height});
@@ -931,7 +1080,8 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureInternalFormat format = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat); TextureInternalFormat format = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
if (!TextureImpl::ValidateTextureInternalFormat(format)) return; if (!TextureImpl::ValidateTextureInternalFormat(format)) return;
if (!ValidateRenderbufferStorageSamples_State(samples, "NamedRenderbufferStorageMultisample_State")) return; if (!ValidateRenderbufferStorageSamples_State(samples, format, "NamedRenderbufferStorageMultisample_State"))
return;
if (!ValidateRenderbufferStorageSize_State(width, height, "NamedRenderbufferStorageMultisample_State")) return; if (!ValidateRenderbufferStorageSize_State(width, height, "NamedRenderbufferStorageMultisample_State")) return;
renderbufferObject->AllocateStorage({width, height}); renderbufferObject->AllocateStorage({width, height});
@@ -1013,13 +1163,7 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget); auto& bindingSlot = MG_State::pGLContext->GetFramebufferBindingSlot(framebufferTarget);
auto& framebufferObject = bindingSlot.GetBoundObject(); auto& framebufferObject = bindingSlot.GetBoundObject();
if (!framebufferObject) { if (!ValidateFramebufferTextureAttachmentPoint(functionName, framebufferObject, attachmentType)) return;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Framebuffer target is bound to no framebuffer object."));
return;
}
if (texture == 0) { if (texture == 0) {
framebufferObject->Detach(attachmentType); framebufferObject->Detach(attachmentType);
@@ -1034,6 +1178,7 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Texture object {} is not valid.", texture))); std::format("Texture object {} is not valid.", texture)));
return; return;
} }
if (!ValidateFramebufferTextureLevel(functionName, textureObject, level)) return;
if (layer < 0) { if (layer < 0) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, ErrorCode::InvalidValue,
@@ -1156,6 +1301,13 @@ namespace MobileGL::MG_Impl::GLImpl {
"Framebuffer target is bound to no framebuffer object.")); "Framebuffer target is bound to no framebuffer object."));
return; return;
} }
// glFramebufferTexture2D is by far the most-used member of the family and the only one
// that inlines its own logic instead of going through the shared helper, so the 9.2.8
// conditions have to be asked here explicitly.
if (!ValidateFramebufferTextureAttachmentPoint("FramebufferTexture2D_State", framebufferObject,
attachmentType)) {
return;
}
if (texture == 0) { if (texture == 0) {
framebufferObject->Detach(attachmentType); framebufferObject->Detach(attachmentType);
@@ -1170,6 +1322,7 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Texture object {} is not valid.", texture))); std::format("Texture object {} is not valid.", texture)));
return; return;
} }
if (!ValidateFramebufferTextureLevel("FramebufferTexture2D_State", textureObject, level)) return;
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget); const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
if (expectedTextureTarget == TextureTarget::Unknown || if (expectedTextureTarget == TextureTarget::Unknown ||
@@ -1206,6 +1359,12 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
// The name's validity is an INVALID_VALUE condition (GL 4.6 core 9.2.8), and it has to be
// asked BEFORE the object is resolved: reporting the miss as the INVALID_OPERATION below
// pre-empted the shared helper's ValidateTextureName and answered the wrong error code for
// every texture name that was never generated.
if (!TextureImpl::ValidateTextureName(texture, true)) return;
auto& textureObject = MG_State::pGLContext->GetTextureObject(texture); auto& textureObject = MG_State::pGLContext->GetTextureObject(texture);
if (!textureObject) { if (!textureObject) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
@@ -1256,13 +1415,10 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Texture object {} is not valid.", texture))); std::format("Texture object {} is not valid.", texture)));
return; return;
} }
if (level < 0) { // The whole level condition, not just its negative half: glNamedFramebufferTexture and
MG_State::pGLContext->RecordError( // glFramebufferTexture are equivalent in 9.2.8, so an out-of-range immutable level has to
ErrorCode::InvalidValue, // be rejected on both or a CTS case gets two answers for one rule.
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "NamedFramebufferTexture_State", if (!ValidateFramebufferTextureLevel("NamedFramebufferTexture_State", textureObject, level)) return;
"Texture level must be non-negative."));
return;
}
TextureUploadTarget textureUploadTarget = TextureUploadTarget::Unknown; TextureUploadTarget textureUploadTarget = TextureUploadTarget::Unknown;
Bool layered = false; Bool layered = false;
@@ -2578,18 +2734,30 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void ClearBufferfi_Backend(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { void ClearBufferfi_Backend(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
MGP_FILL(ClearBufferfi);
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfi(buffer, drawbuffer, depth, stencil); MG_Backend::gBackendFunctionsTable.GL.ClearBufferfi(buffer, drawbuffer, depth, stencil);
} }
void ClearBufferfv_Backend(GLenum buffer, GLint drawbuffer, const GLfloat* value) { void ClearBufferfv_Backend(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
MGP_FILL(ClearBufferfv);
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfv(buffer, drawbuffer, value); MG_Backend::gBackendFunctionsTable.GL.ClearBufferfv(buffer, drawbuffer, value);
} }
void ClearBufferuiv_Backend(GLenum buffer, GLint drawbuffer, const GLuint* value) { void ClearBufferuiv_Backend(GLenum buffer, GLint drawbuffer, const GLuint* value) {
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
MGP_FILL(ClearBufferuiv);
MG_Backend::gBackendFunctionsTable.GL.ClearBufferuiv(buffer, drawbuffer, value); MG_Backend::gBackendFunctionsTable.GL.ClearBufferuiv(buffer, drawbuffer, value);
} }
void ClearBufferiv_Backend(GLenum buffer, GLint drawbuffer, const GLint* value) { void ClearBufferiv_Backend(GLenum buffer, GLint drawbuffer, const GLint* value) {
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
MGP_FILL(ClearBufferiv);
MG_Backend::gBackendFunctionsTable.GL.ClearBufferiv(buffer, drawbuffer, value); MG_Backend::gBackendFunctionsTable.GL.ClearBufferiv(buffer, drawbuffer, value);
} }
@@ -2837,6 +3005,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void ReadPixels_Backend(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) { void ReadPixels_Backend(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
MGP_FILL(ReadPixels);
MG_Backend::gBackendFunctionsTable.GL.ReadPixels(x, y, width, height, format, type, pixels); MG_Backend::gBackendFunctionsTable.GL.ReadPixels(x, y, width, height, format, type, pixels);
} }
@@ -3118,15 +3287,55 @@ namespace MobileGL::MG_Impl::GLImpl {
GetNamedFramebufferAttachmentParameteriv_State(framebuffer, attachment, pname, params); GetNamedFramebufferAttachmentParameteriv_State(framebuffer, attachment, pname, params);
} }
// The three argument errors GL 4.6 core 18.3.1 asks a blit for. They have to be raised here,
// in the backend-independent frontend: DirectGLES drains the driver's error queue around the
// blit on purpose (that is how the resolve fallback probes the driver), so an ES-side
// rejection never reaches the application and glGetError() answered GL_NO_ERROR for a call
// the spec requires to fail (KHR-GL30.api.coverage's glBlitFramebuffer sub-check). DirectVulkan
// already dropped the bad-filter and LINEAR-with-depth/stencil calls on the floor with a log
// line (VulkanRenderer::BlitFramebuffer), so the only thing that changes for it is that the
// error is now visible where the spec says it should be.
static Bool ValidateBlitMaskAndFilter(const char* functionName, GLbitfield mask, GLenum filter) {
constexpr GLbitfield kBlitMaskBits = GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT;
if ((mask & ~kBlitMaskBits) != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"mask contains bits other than GL_COLOR_BUFFER_BIT, "
"GL_DEPTH_BUFFER_BIT and GL_STENCIL_BUFFER_BIT."));
return false;
}
if (filter != GL_NEAREST && filter != GL_LINEAR) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"filter must be GL_NEAREST or GL_LINEAR."));
return false;
}
// Depth and stencil have no meaningful interpolation, so GL_LINEAR is rejected outright
// rather than downgraded - even when the mask also carries the colour bit.
if (filter == GL_LINEAR && (mask & (GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"GL_LINEAR filtering is not allowed when mask includes "
"GL_DEPTH_BUFFER_BIT or GL_STENCIL_BUFFER_BIT."));
return false;
}
return true;
}
void BlitNamedFramebuffer(GLuint readFramebuffer, GLuint drawFramebuffer, GLint srcX0, GLint srcY0, GLint srcX1, void BlitNamedFramebuffer(GLuint readFramebuffer, GLuint drawFramebuffer, GLint srcX0, GLint srcY0, GLint srcX1,
GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask,
GLenum filter) { GLenum filter) {
if (!ValidateBlitMaskAndFilter(__func__, mask, filter)) return;
BlitNamedFramebuffer_State(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, BlitNamedFramebuffer_State(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
dstY1, mask, filter); dstY1, mask, filter);
} }
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter) { GLint dstY1, GLbitfield mask, GLenum filter) {
if (!ValidateBlitMaskAndFilter(__func__, mask, filter)) return;
BlitFramebuffer_Backend(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); BlitFramebuffer_Backend(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
} }
File diff suppressed because it is too large Load Diff
@@ -24,4 +24,25 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data); void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
GLenum GetError(); GLenum GetError();
GLenum GetGraphicsResetStatus(); GLenum GetGraphicsResetStatus();
// The GL_MAX_SAMPLES value MobileGL advertises, i.e. the driver's value floored to the GL
// core minimum of 4. This is the RENDERBUFFER ceiling; the three per-category texture
// ceilings below have a minimum of one and are reported as probed.
GLint GetAdvertisedMaxSamples();
// Exactly what GL_MAX_COLOR_TEXTURE_SAMPLES / GL_MAX_DEPTH_TEXTURE_SAMPLES /
// GL_MAX_INTEGER_SAMPLES report: the probed backend limit floored at the GL 4.6 core minimum
// of ONE (table 23.53). Exported so the frontend's storage validation enforces exactly what
// the query promised - it used to floor both at 4 and then let the backend quietly
// under-allocate whatever the driver could not actually provide.
GLint GetAdvertisedColorTextureMaxSamples();
GLint GetAdvertisedDepthTextureMaxSamples();
GLint GetAdvertisedIntegerMaxSamples();
// What glGetIntegerv(GL_SAMPLES) answers for the CURRENT draw framebuffer: the largest sample
// count over its attachments, and 0 for a single-sample or default framebuffer (GL 4.6 core
// 9.2.3 / 22.2 - GL_SAMPLE_BUFFERS is 1 exactly when this is non-zero).
//
// Shared rather than duplicated because two callers need the identical number and disagreeing
// would be a silent bug: the query itself, and the draw path's write of the reserved
// gl_NumSamples stand-in - a shader comparing gl_NumSamples against glGetIntegerv(GL_SAMPLES)
// is exactly what the sample_variables CTS does.
GLint ResolveDrawFramebufferSampleCount();
} // namespace MobileGL::MG_Impl::GLImpl } // namespace MobileGL::MG_Impl::GLImpl
File diff suppressed because it is too large Load Diff
@@ -13,6 +13,12 @@ namespace MobileGL::MG_Impl::GLImpl {
void AttachShader(GLuint program, GLuint shader); void AttachShader(GLuint program, GLuint shader);
void BindAttribLocation(GLuint program, GLuint index, const GLchar* name); void BindAttribLocation(GLuint program, GLuint index, const GLchar* name);
void CompileShader(GLuint shader); void CompileShader(GLuint shader);
// GL_ARB_gl_spirv, core since 4.6. The pair is a two-step operation: glShaderBinary attaches
// the module to one or more shader objects, glSpecializeShader names its entry point and
// supplies its specialization constants and is what actually compiles them.
void ShaderBinary(GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length);
void SpecializeShader(GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants,
const GLuint* pConstantIndex, const GLuint* pConstantValue);
GLuint CreateProgram(void); GLuint CreateProgram(void);
GLuint CreateShader(GLenum type); GLuint CreateShader(GLenum type);
void DeleteProgram(GLuint program); void DeleteProgram(GLuint program);
@@ -140,6 +146,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params); const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name); GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name); GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
void GetActiveAtomicCounterBufferiv(GLuint program, GLuint bufferIndex, GLenum pname, GLint* params);
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding); void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
void Uniform1d(GLint location, GLdouble v0); void Uniform1d(GLint location, GLdouble v0);
void Uniform1dv(GLint location, GLsizei count, const GLdouble* value); void Uniform1dv(GLint location, GLsizei count, const GLdouble* value);
@@ -19,16 +19,26 @@ namespace MobileGL::MG_Impl::GLImpl {
code, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", function, Move(message))); code, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", function, Move(message)));
} }
// A pipeline name only names an object once it has been bound or created; querying a // GL 4.6 core 7.4 asks only that the name came from GenProgramPipelines and has not been
// reserved-but-unmaterialised name is INVALID_OPERATION (GL 4.6 core 7.4). // deleted - so a name that was reserved and never bound is legal here, and the command
// MATERIALIZES it rather than rejecting it.
//
// Requiring a bound object instead is what broke every separable-program conformance case
// across three families: the CTS reserves a name, calls glUseProgramStages three times and
// only then binds, which is the order the spec's own example uses. Each of those calls
// failed with INVALID_OPERATION, so the stage programs were never recorded - the pipeline
// stayed empty, GetProgramForDraw flattened nothing and the draw painted nothing, and the
// rejected calls' error was left in the queue for the harness to find. One cause, both
// symptoms.
const SharedPtr<MG_State::GLState::ProgramPipelineObject>* TryGetPipeline(GLuint pipeline, const SharedPtr<MG_State::GLState::ProgramPipelineObject>* TryGetPipeline(GLuint pipeline,
const char* function) { const char* function) {
if (!MG_State::pGLContext->IsProgramPipelineObject(pipeline)) { const auto& object = MG_State::pGLContext->MaterializeProgramPipelineObject(pipeline);
if (!object) {
RecordPipelineError(ErrorCode::InvalidOperation, function, RecordPipelineError(ErrorCode::InvalidOperation, function,
std::format("Program pipeline {} does not exist.", pipeline)); std::format("Program pipeline {} does not exist.", pipeline));
return nullptr; return nullptr;
} }
return &MG_State::pGLContext->GetProgramPipelineObject(pipeline); return &object;
} }
Bool ValidatePipelineCount(GLsizei n, const char* function) { Bool ValidatePipelineCount(GLsizei n, const char* function) {
@@ -182,6 +192,15 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Program {} has not been linked successfully.", program)); std::format("Program {} has not been linked successfully.", program));
return; return;
} }
// GL 4.6 core 7.4: "INVALID_OPERATION is generated if program was not linked with its
// PROGRAM_SEPARABLE status set". The LATCHED flag is the one that decides - a program
// whose live flag was cleared after a separable link is still a legal stage, and a
// program whose live flag was set after a non-separable link is not.
if (!programObject->GetLinkedSeparable()) {
RecordPipelineError(ErrorCode::InvalidOperation, __func__,
std::format("Program {} was not linked as a separable program.", program));
return;
}
} }
const GLbitfield selected = stages == GL_ALL_SHADER_BITS ? kAllStageBits : stages; const GLbitfield selected = stages == GL_ALL_SHADER_BITS ? kAllStageBits : stages;
@@ -19,7 +19,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
// "<getAtomicCounterBlockName()>_<binding>" (ParseContextBase.cpp), one per GL // "<getAtomicCounterBlockName()>_<binding>" (ParseContextBase.cpp), one per GL
// atomic-counter binding point. That block IS the GL_ATOMIC_COUNTER_BUFFER resource // atomic-counter binding point. That block IS the GL_ATOMIC_COUNTER_BUFFER resource
// and its trailing number IS GL_BUFFER_BINDING; its members stay GL_UNIFORMs. // and its trailing number IS GL_BUFFER_BINDING; its members stay GL_UNIFORMs.
constexpr const char* kAtomicCounterBlockPrefix = "gl_AtomicCounterBlock"; constexpr const char* kAtomicCounterBlockPrefix = MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX;
enum class BlockKind { enum class BlockKind {
Uniform, // a real GL uniform block Uniform, // a real GL uniform block
@@ -81,19 +81,18 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
// The enumerated spelling of an array resource is "name[0]". glslang already applies // The enumerated spelling of an array resource is "name[0]". glslang already applies
// that to uniforms and buffer variables (EShReflectionBasicArraySuffix), but never to // that to uniforms and buffer variables (EShReflectionBasicArraySuffix), but never to
// stage inputs/outputs, so those get it here. // stage inputs/outputs, so those get it here.
String WithArraySuffix(const String& name, const glslang::TType* type) { String WithArraySuffix(const String& name, const ProgramObject::TypeFacts& type) {
if (type == nullptr || !type->isArray() || EndsWithZeroSubscript(name)) return name; if (!type.isArray || EndsWithZeroSubscript(name)) return name;
return name + "[0]"; return name + "[0]";
} }
// GL_ARRAY_SIZE: element count for a sized array, 0 for a runtime-sized one // GL_ARRAY_SIZE: element count for a sized array, 0 for a runtime-sized one
// (a shader storage block's unsized trailing member), 1 for a non-array. // (a shader storage block's unsized trailing member), 1 for a non-array.
GLint ArraySizeOf(const glslang::TType* type, GLint reflectedSize) { // `record.arraySize` is already the sized-array/reflected-size resolution; the only
if (type != nullptr && type->isArray()) { // extra rule here is GL's 0 for a runtime-sized array.
if (!type->isSizedArray()) return 0; GLint ArraySizeOf(const ProgramObject::ResourceReflection& record) {
return type->getOuterArraySize(); if (record.type.isArray && !record.type.isSizedArray) return 0;
} return record.arraySize;
return reflectedSize < 1 ? 1 : reflectedSize;
} }
// Two spellings name the same resource when they are equal, or differ only by the // Two spellings name the same resource when they are equal, or differ only by the
@@ -174,22 +173,21 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
return static_cast<GLint>(element); return static_cast<GLint>(element);
} }
BlockKind ClassifyBlock(const glslang::TObjectReflection& block) { BlockKind ClassifyBlock(const ProgramObject::BlockReflection& block) {
if (std::strstr(block.name.c_str(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != nullptr) { if (std::strstr(block.name.c_str(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != nullptr) {
return BlockKind::GlobalUbo; return BlockKind::GlobalUbo;
} }
if (IsAtomicCounterBlockName(block.name)) return BlockKind::AtomicCounter; if (IsAtomicCounterBlockName(block.name)) return BlockKind::AtomicCounter;
const glslang::TType* type = block.getType(); if (block.type.isBuffer) return BlockKind::Storage;
if (type != nullptr && type->getQualifier().storage == glslang::EvqBuffer) return BlockKind::Storage;
return BlockKind::Uniform; return BlockKind::Uniform;
} }
// std140/std430 column stride, the same vec4-rounded rule ProgramObject applies to // std140/std430 column stride, the same vec4-rounded rule ProgramObject applies to
// uniform matrices. 0 for a non-matrix. // uniform matrices. 0 for a non-matrix.
GLint MatrixStrideOf(const glslang::TType* type) { GLint MatrixStrideOf(const ProgramObject::TypeFacts& type) {
if (type == nullptr || !type->isMatrix()) return 0; if (!type.isMatrix) return 0;
const bool rowMajor = type->getQualifier().layoutMatrix == glslang::ElmRowMajor; const bool rowMajor = type.layoutMatrix == static_cast<Int>(glslang::ElmRowMajor);
const int strideVectorComponents = rowMajor ? type->getMatrixCols() : type->getMatrixRows(); const int strideVectorComponents = rowMajor ? type.matrixCols : type.matrixRows;
constexpr int scalarSize = 4; constexpr int scalarSize = 4;
const int vectorAlignment = (strideVectorComponents <= 1) ? scalarSize const int vectorAlignment = (strideVectorComponents <= 1) ? scalarSize
: (strideVectorComponents == 2) ? 2 * scalarSize : (strideVectorComponents == 2) ? 2 * scalarSize
@@ -197,9 +195,9 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
return (vectorAlignment + 15) & ~15; return (vectorAlignment + 15) & ~15;
} }
GLint IsRowMajorOf(const glslang::TType* type) { GLint IsRowMajorOf(const ProgramObject::TypeFacts& type) {
if (type == nullptr || !type->isMatrix()) return 0; if (!type.isMatrix) return 0;
return type->getQualifier().layoutMatrix == glslang::ElmRowMajor ? 1 : 0; return type.layoutMatrix == static_cast<Int>(glslang::ElmRowMajor) ? 1 : 0;
} }
GLint MappedLocation(Int rawLocation) { GLint MappedLocation(Int rawLocation) {
@@ -210,14 +208,69 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
// ---- model construction -------------------------------------------------------- // ---- model construction --------------------------------------------------------
void BuildBlocks(ProgramObject& program, const glslang::TProgram& reflection, Model& model, // GL_REFERENCED_BY_*_SHADER for an ARRAYED block instance, refined per element.
//
// glslang records a block reference by walking up to the base symbol and calling
// addBlockName with the whole ARRAY type, which ORs the referencing stage into every
// element at once - it has not resolved the subscript yet at that point. So reading
// "e[0].b" marks both TrickyBlock[0] and TrickyBlock[1] as referenced by the fragment
// stage (KHR-GL43.program_interface_query.uniform-block-types).
//
// The MEMBER masks are exact: EShReflectionAllBlockVariables enumerates every member of
// every element with the stage mask suppressed, and only the dereference chain actually
// walked turns a bit on - and that chain carries the subscript. So the union of a block
// instance's members is the reference set of that instance.
//
// Applied ONLY to arrayed instances, because for a scalar block glslang is already exact.
// Note the union is used even when it is empty: an array element nobody dereferenced has
// no member bits and is genuinely referenced by nobody, which is the whole point - falling
// back to the block's own mask there would restore the over-approximation.
Vector<Uint32> BuildBlockStagesFromMembers(const ProgramObject::LinkArtifacts& reflection,
Int blockCount) {
Vector<Uint32> stagesByBlock(static_cast<SizeT>(blockCount < 0 ? 0 : blockCount), 0u);
const Int uniformCount = static_cast<Int>(reflection.uniformReflection.size());
for (Int index = 0; index < uniformCount; ++index) {
const auto& uniform = reflection.uniformReflection[index];
const Int owner = uniform.index;
if (owner < 0 || owner >= blockCount) continue;
stagesByBlock[static_cast<SizeT>(owner)] |= static_cast<Uint32>(uniform.stages);
}
return stagesByBlock;
}
// UNIFORM blocks only, and that scope is load-bearing rather than cautious. The member
// names glslang produces for a uniform block array carry the subscript
// ("TrickyBlock[0].b", via EShReflectionStrictArraySuffix), so each element's members are
// distinct entries and the bits land on the right one. A SHADER STORAGE block array does
// NOT get that treatment - its buffer variables reflect under one subscript-free spelling
// shared by every element - so a union over them credits element 0 and starves the rest.
// KHR-GL43.program_interface_query.ssb-types is the case that says so: it reads ss[0] and
// ss[1] and requires both to report the fragment stage, which only glslang's own
// (deliberately over-approximating) block mask gets right. Storage and atomic-counter
// blocks therefore keep that mask untouched.
Uint32 UniformBlockStages(const ProgramObject::BlockReflection& block, const Vector<Uint32>& stagesFromMembers,
Int tIndex) {
String arrayBase;
Uint element = 0;
Bool malformed = false;
if (!SplitTrailingSubscript(block.name, arrayBase, element, malformed) || malformed) {
return static_cast<Uint32>(block.stages);
}
if (tIndex < 0 || tIndex >= static_cast<Int>(stagesFromMembers.size())) {
return static_cast<Uint32>(block.stages);
}
return stagesFromMembers[static_cast<SizeT>(tIndex)];
}
void BuildBlocks(ProgramObject& program, const ProgramObject::LinkArtifacts& reflection, Model& model,
Vector<BlockKind>& blockKind, Vector<Int>& blockInterfaceIndex) { Vector<BlockKind>& blockKind, Vector<Int>& blockInterfaceIndex) {
const Int blockCount = const_cast<glslang::TProgram&>(reflection).getNumUniformBlocks(); const Int blockCount = static_cast<Int>(reflection.blockReflection.size());
blockKind.assign(blockCount, BlockKind::Uniform); blockKind.assign(blockCount, BlockKind::Uniform);
blockInterfaceIndex.assign(blockCount, -1); blockInterfaceIndex.assign(blockCount, -1);
const Vector<Uint32> stagesFromMembers = BuildBlockStagesFromMembers(reflection, blockCount);
for (Int tIndex = 0; tIndex < blockCount; ++tIndex) { for (Int tIndex = 0; tIndex < blockCount; ++tIndex) {
const auto& block = const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex); const auto& block = reflection.blockReflection[tIndex];
const BlockKind kind = ClassifyBlock(block); const BlockKind kind = ClassifyBlock(block);
blockKind[tIndex] = kind; blockKind[tIndex] = kind;
if (kind == BlockKind::AtomicCounter) { if (kind == BlockKind::AtomicCounter) {
@@ -238,7 +291,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
// glShaderStorageBlockBinding wins over the declaration (GL 4.6 §7.6.2 - // glShaderStorageBlockBinding wins over the declaration (GL 4.6 §7.6.2 -
// exactly the same rule GL_UNIFORM_BLOCK follows through // exactly the same rule GL_UNIFORM_BLOCK follows through
// GetUniformBlockBinding below). // GetUniformBlockBinding below).
const GLint declared = block.getBinding(); const GLint declared = block.binding;
resource.bufferBinding = declared < 0 ? 0 : declared + BlockArrayElement(block.name); resource.bufferBinding = declared < 0 ? 0 : declared + BlockArrayElement(block.name);
const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name); const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name);
if (rebound >= 0) resource.bufferBinding = static_cast<GLint>(rebound); if (rebound >= 0) resource.bufferBinding = static_cast<GLint>(rebound);
@@ -252,38 +305,53 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
// GL_UNIFORM_BLOCK keeps the index space glUniformBlockBinding and // GL_UNIFORM_BLOCK keeps the index space glUniformBlockBinding and
// glGetActiveUniformBlockiv already use, so an index handed out here is usable // glGetActiveUniformBlockiv already use, so an index handed out here is usable
// with them (which is exactly what the CTS does). // with them (which is exactly what the CTS does).
const Int glBlockCount = program.GetActiveUniformBlocksCount(); const Int glBlockCount = program.GetGlUniformBlockCount();
for (Int glIndex = 0; glIndex < glBlockCount; ++glIndex) { for (Int glIndex = 0; glIndex < glBlockCount; ++glIndex) {
// The block-space index the block-keyed accessors want; the two spaces differ
// whenever the program also has a storage or atomic counter block, which
// glslang files under the same reflection list (no EShReflectionSeparateBuffers).
const Int blockIndex = program.BlockIndexFromGlUniformBlock(static_cast<Uint>(glIndex));
Resource resource; Resource resource;
resource.name = program.GetUniformBlockName(glIndex); resource.name = program.GetUniformBlockName(static_cast<Uint>(blockIndex));
resource.bufferBinding = static_cast<GLint>(program.GetUniformBlockBinding(glIndex)); resource.bufferBinding = static_cast<GLint>(program.GetUniformBlockBinding(static_cast<Uint>(blockIndex)));
resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(glIndex)); resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(static_cast<Uint>(blockIndex)));
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(glIndex)); const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(blockIndex));
if (tIndex >= 0 && tIndex < blockCount) { if (tIndex >= 0 && tIndex < blockCount) {
resource.stages = resource.stages = UniformBlockStages(reflection.blockReflection[tIndex],
static_cast<Uint32>(const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex).stages); stagesFromMembers, tIndex);
} }
model.uniformBlocks.push_back(Move(resource)); model.uniformBlocks.push_back(Move(resource));
} }
} }
void BuildUniformsAndBufferVariables(ProgramObject& program, const glslang::TProgram& reflection, Model& model, void BuildUniformsAndBufferVariables(ProgramObject& program,
const ProgramObject::LinkArtifacts& reflection, Model& model,
const Vector<BlockKind>& blockKind, const Vector<BlockKind>& blockKind,
const Vector<Int>& blockInterfaceIndex) { const Vector<Int>& blockInterfaceIndex) {
const Uint uniformCount = program.GetUniformCount(); // Walks the TPROGRAM uniform space, not the GL one. A buffer variable is not a GL
for (Uint glIndex = 0; glIndex < uniformCount; ++glIndex) { // uniform (GL 4.6 core 7.3.1) and DoReflection therefore keeps it out of the GL
const Int tIndex = program.TProgramUniformIndex(glIndex); // active-uniform index space - but GL_BUFFER_VARIABLE still has to enumerate it, and
const auto& refl = const_cast<glslang::TProgram&>(reflection).getUniform(tIndex); // this is the only place that does. GL uniforms keep their GL index as their
const glslang::TType* type = refl.getType(); // GL_UNIFORM resource index: the GL space is a subsequence of this one, so pushing
// the GL-visible entries in this order preserves the correspondence.
const Int tUniformCount = static_cast<Int>(reflection.uniformReflection.size());
for (Int tIndex = 0; tIndex < tUniformCount; ++tIndex) {
const auto& refl = ProgramObject::UniformAtIn(reflection, tIndex);
const auto& type = refl.type;
const Int owner = refl.index; const Int owner = refl.index;
const BlockKind kind = (owner >= 0 && owner < static_cast<Int>(blockKind.size())) const BlockKind kind = (owner >= 0 && owner < static_cast<Int>(blockKind.size()))
? blockKind[owner] ? blockKind[owner]
: BlockKind::GlobalUbo; : BlockKind::GlobalUbo;
const Int glIndex = program.GlUniformIndexFromTProgram(tIndex);
// Everything except a buffer variable is enumerated through the GL space, so a
// uniform the relaxed parse swept out of it (a declared-but-dead default-block
// one) stays out of GL_UNIFORM too.
if (kind != BlockKind::Storage && glIndex < 0) continue;
Resource resource; Resource resource;
resource.name = refl.name; resource.name = refl.name;
resource.type = static_cast<GLenum>(refl.glDefineType); resource.type = static_cast<GLenum>(refl.glDefineType);
resource.arraySize = ArraySizeOf(type, refl.size); resource.arraySize = ArraySizeOf(refl);
resource.stages = static_cast<Uint32>(refl.stages); resource.stages = static_cast<Uint32>(refl.stages);
if (kind == BlockKind::Storage) { if (kind == BlockKind::Storage) {
@@ -311,11 +379,12 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
resource.atomicCounterBufferIndex = blockInterfaceIndex[owner]; resource.atomicCounterBufferIndex = blockInterfaceIndex[owner];
resource.location = -1; resource.location = -1;
} else { } else {
resource.blockIndex = program.GetActiveUniformBlockIndex(glIndex); const Uint glUniformIndex = static_cast<Uint>(glIndex);
resource.offset = program.GetActiveUniformOffset(glIndex); resource.blockIndex = program.GetActiveUniformBlockIndex(glUniformIndex);
resource.arrayStride = program.GetActiveUniformArrayStride(glIndex); resource.offset = program.GetActiveUniformOffset(glUniformIndex);
resource.matrixStride = program.GetActiveUniformMatrixStride(glIndex); resource.arrayStride = program.GetActiveUniformArrayStride(glUniformIndex);
resource.isRowMajor = program.GetActiveUniformIsRowMajor(glIndex); resource.matrixStride = program.GetActiveUniformMatrixStride(glUniformIndex);
resource.isRowMajor = program.GetActiveUniformIsRowMajor(glUniformIndex);
// A member of a named uniform block has no location, whatever the // A member of a named uniform block has no location, whatever the
// frontend's own location table says (it hands one out to every uniform // frontend's own location table says (it hands one out to every uniform
// so glUniform* can address block members through the global UBO). // so glUniform* can address block members through the global UBO).
@@ -334,12 +403,16 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
static_cast<GLuint>(i)); static_cast<GLuint>(i));
} }
} }
for (SizeT blockIndex = 0; blockIndex < model.uniformBlocks.size(); ++blockIndex) { for (SizeT glBlockIndex = 0; glBlockIndex < model.uniformBlocks.size(); ++glBlockIndex) {
// Members of an arrayed block are reflected once, against instance [0]. // Members of an arrayed block are reflected once, against instance [0].
const Int owner = static_cast<Int>(program.GetUniformBlockMemberOwnerIndex(static_cast<Uint>(blockIndex))); // GetUniformBlockMemberOwnerIndex takes and answers BLOCK indices, while
// Resource::blockIndex is a GL_UNIFORM_BLOCK index, so translate both ways.
const Int blockIndex = program.BlockIndexFromGlUniformBlock(static_cast<Uint>(glBlockIndex));
const Int owner = program.GlUniformBlockIndexFromBlock(
static_cast<Int>(program.GetUniformBlockMemberOwnerIndex(static_cast<Uint>(blockIndex))));
for (SizeT i = 0; i < model.uniforms.size(); ++i) { for (SizeT i = 0; i < model.uniforms.size(); ++i) {
if (model.uniforms[i].blockIndex == owner) { if (model.uniforms[i].blockIndex == owner) {
model.uniformBlocks[blockIndex].activeVariables.push_back(static_cast<GLuint>(i)); model.uniformBlocks[glBlockIndex].activeVariables.push_back(static_cast<GLuint>(i));
} }
} }
} }
@@ -352,49 +425,67 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
} }
} }
void BuildStageIO(ProgramObject& program, const glslang::TProgram& reflection, Model& model) { // A built-in interface block that a shader redeclares with fewer members keeps the
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection); // omitted ones in its type when the redeclaration is ANONYMOUS - glslang hides them
// (basic type void) instead of erasing them, because the original shared declaration
// has to stay usable. Only the instance-named form erases. So a separable vertex
// program that redeclares `out gl_PerVertex { vec4 gl_Position; }` still carries
// gl_PointSize and gl_ClipDistance through the block-unwrapping reflection, and they
// are not part of its output interface.
Bool IsHiddenBlockMember(const ProgramObject::TypeFacts& type) { return type.isVoid; }
const Int inputCount = mutableReflection.getNumPipeInputs(); void BuildStageIO(ProgramObject& program, const ProgramObject::LinkArtifacts& reflection, Model& model) {
const Int inputCount = static_cast<Int>(reflection.pipeInputReflection.size());
for (Int index = 0; index < inputCount; ++index) { for (Int index = 0; index < inputCount; ++index) {
const auto& refl = mutableReflection.getPipeInput(index); const auto& refl = reflection.pipeInputReflection[index];
const glslang::TType* type = refl.getType(); const auto& type = refl.type;
if (IsHiddenBlockMember(type)) continue;
Resource resource; Resource resource;
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V // The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V
// names; GL enumerates the GL spellings. // names; GL enumerates the GL spellings.
const String& glName = ProgramObject::NormalizeBuiltinPipeInputName(refl.name); const String& glName = ProgramObject::NormalizeBuiltinPipeInputName(refl.name);
resource.name = WithArraySuffix(glName, type); resource.name = WithArraySuffix(glName, type);
resource.type = static_cast<GLenum>(refl.glDefineType); resource.type = static_cast<GLenum>(refl.glDefineType);
resource.arraySize = ArraySizeOf(type, refl.size); resource.arraySize = ArraySizeOf(refl);
resource.location = program.GetAttributeLocation(refl.name); resource.location = program.GetAttributeLocation(refl.name);
if (resource.location < 0) resource.location = MappedLocation(static_cast<Int>(refl.layoutLocation())); if (resource.location < 0) resource.location = MappedLocation(refl.location);
resource.isPerPatch = (type != nullptr && type->getQualifier().patch) ? 1 : 0; resource.isPerPatch = type.isPatch ? 1 : 0;
resource.stages = static_cast<Uint32>(refl.stages); resource.stages = static_cast<Uint32>(refl.stages);
model.programInputs.push_back(Move(resource)); model.programInputs.push_back(Move(resource));
} }
const Int outputCount = mutableReflection.getNumPipeOutputs(); // A color number, and therefore a color INDEX, exists only for a fragment stage's
// outputs. The output interface belongs to the program's last stage, so for a
// separable tessellation/geometry/vertex program these are varyings: asking the
// frag-data maps about them can still answer a location (a tess-control output
// carries its own layout(location=N)), and a location then manufactures a color
// index of 0 where GL requires -1
// (KHR-GL43.program_interface_query.separate-programs-tess-control).
const Bool lastStageIsFragment = reflection.lastStageIsFragment;
const Int outputCount = static_cast<Int>(reflection.pipeOutputReflection.size());
for (Int index = 0; index < outputCount; ++index) { for (Int index = 0; index < outputCount; ++index) {
const auto& refl = mutableReflection.getPipeOutput(index); const auto& refl = reflection.pipeOutputReflection[index];
const glslang::TType* type = refl.getType(); const auto& type = refl.type;
if (IsHiddenBlockMember(type)) continue;
Resource resource; Resource resource;
resource.name = WithArraySuffix(refl.name, type); resource.name = WithArraySuffix(refl.name, type);
resource.type = static_cast<GLenum>(refl.glDefineType); resource.type = static_cast<GLenum>(refl.glDefineType);
resource.arraySize = ArraySizeOf(type, refl.size); resource.arraySize = ArraySizeOf(refl);
resource.location = MappedLocation(program.GetFragmentDataLocation(refl.name.c_str())); resource.location = MappedLocation(program.GetFragmentDataLocation(refl.name.c_str()));
if (resource.location < 0) { if (resource.location < 0 || !lastStageIsFragment) {
// A built-in output (gl_FragDepth, gl_SampleMask) and a non-fragment stage // A built-in output (gl_FragDepth, gl_SampleMask) has no location, and a
// output both have no location, and therefore no color index either. // non-fragment stage's outputs have no color number at all - either way there
// is no color index.
resource.locationIndex = -1; resource.locationIndex = -1;
} else { } else {
resource.locationIndex = program.GetFragmentDataIndex(refl.name.c_str()); resource.locationIndex = program.GetFragmentDataIndex(refl.name.c_str());
// glBindFragDataLocationIndexed wins; otherwise the shader's // glBindFragDataLocationIndexed wins; otherwise the shader's
// layout(index = N), which the frag-data maps never saw. // layout(index = N), which the frag-data maps never saw.
if (resource.locationIndex == 0 && type != nullptr && type->getQualifier().hasIndex()) { if (resource.locationIndex == 0 && type.hasIndex) {
resource.locationIndex = static_cast<GLint>(type->getQualifier().layoutIndex); resource.locationIndex = static_cast<GLint>(type.layoutIndex);
} }
} }
resource.isPerPatch = (type != nullptr && type->getQualifier().patch) ? 1 : 0; resource.isPerPatch = type.isPatch ? 1 : 0;
resource.stages = static_cast<Uint32>(refl.stages); resource.stages = static_cast<Uint32>(refl.stages);
model.programOutputs.push_back(Move(resource)); model.programOutputs.push_back(Move(resource));
} }
@@ -434,15 +525,14 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
Model BuildModel(ProgramObject& program) { Model BuildModel(ProgramObject& program) {
Model model; Model model;
if (!program.GetLinkStatus()) return model; if (!program.GetLinkStatus()) return model;
const glslang::TProgram* reflection = program.GetReflection(); const ProgramObject::LinkArtifacts& reflection = program.GetLinkReflection();
if (reflection == nullptr) return model;
model.valid = true; model.valid = true;
Vector<BlockKind> blockKind; Vector<BlockKind> blockKind;
Vector<Int> blockInterfaceIndex; Vector<Int> blockInterfaceIndex;
BuildBlocks(program, *reflection, model, blockKind, blockInterfaceIndex); BuildBlocks(program, reflection, model, blockKind, blockInterfaceIndex);
BuildUniformsAndBufferVariables(program, *reflection, model, blockKind, blockInterfaceIndex); BuildUniformsAndBufferVariables(program, reflection, model, blockKind, blockInterfaceIndex);
BuildStageIO(program, *reflection, model); BuildStageIO(program, reflection, model);
BuildXfb(program, model); BuildXfb(program, model);
return model; return model;
} }
+312 -23
View File
@@ -12,6 +12,7 @@
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/ErrorInfo.h> #include <MG_State/GLState/ErrorState/ErrorInfo.h>
#include <MG_Impl/Pipe/PipeFill.h>
namespace MobileGL::MG_Impl::GLImpl { namespace MobileGL::MG_Impl::GLImpl {
namespace { namespace {
@@ -31,8 +32,15 @@ namespace MobileGL::MG_Impl::GLImpl {
Bool ended = false; Bool ended = false;
Bool resultCached = false; Bool resultCached = false;
Uint64 cachedResult = 0; Uint64 cachedResult = 0;
// Transform feedback primitive counter at BeginQuery time. // The transform feedback primitive counter matching this query's target, at
// BeginQuery time.
Uint64 counterSnapshot = 0; Uint64 counterSnapshot = 0;
// Capture-draw counters at BeginQuery time: how many capture draws the CPU
// accounting had reproduced exactly, and how many of those it could not (a
// geometry stage amplifies). Their deltas decide whether the CPU result may
// stand in for the backend's.
Uint64 accountedCaptureDrawSnapshot = 0;
Uint64 geometryCaptureDrawSnapshot = 0;
}; };
// Query calls may arrive from any thread (launchers migrate the context // Query calls may arrive from any thread (launchers migrate the context
@@ -52,6 +60,62 @@ namespace MobileGL::MG_Impl::GLImpl {
GLuint g_activePrimitivesGeneratedQueryId = 0; GLuint g_activePrimitivesGeneratedQueryId = 0;
// Id of the query active on GL_SAMPLES_PASSED (0 = none). // Id of the query active on GL_SAMPLES_PASSED (0 = none).
GLuint g_activeSamplesPassedQueryId = 0; GLuint g_activeSamplesPassedQueryId = 0;
// Ids of the queries active on the GL_ARB_pipeline_statistics_query targets, one slot per
// target (0 = none). A map rather than a field per target: the eleven behave identically
// and none of them has any state beyond "which object is counting".
UnorderedMap<GLenum, GLuint> g_activePipelineStatisticsQueryIds;
// Whether MobileGL puts GL_ARB_tessellation_shader in its extension string. Read from the
// ADVERTISED list rather than from a capability bit for the same reason
// BackendSupportsTextureViews does (GL_Texture.cpp): it makes "MobileGL claims tessellation
// support" and "the tessellation-conditional API surface is open" the same fact by
// construction, so the day a backend starts advertising the string the surface below opens
// with it and no second edit is owed.
Bool AdvertisesTessellationShaderExtension() {
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
if (!activeBackendObject) return false;
const auto& extensions = activeBackendObject->GetRendererInfo().RendererGLInfo.Extensions;
return std::find(extensions.begin(), extensions.end(), E_GL_ARB_tessellation_shader) != extensions.end();
}
// The eleven pipeline-statistics counters (GL 4.6 core table 4.3 / ARB_pipeline_statistics_query).
// A 4.6 core context ACCEPTS the nine unconditional ones at glBeginQuery - there is no query
// by which an application could learn otherwise before calling. MobileGL instruments none of
// them, and says so the way GL 4.6 core 4.2.1 provides for: GL_QUERY_COUNTER_BITS answers
// zero for these targets, which is the spec's own signal that the counter is unsupported and
// its results indeterminate. That is an honest zero, not an advertised capability - the
// alternative, GL_INVALID_ENUM on a core entry point, is both non-conformant AND less
// informative.
//
// The two TESSELLATION targets are the exception, because ARB_pipeline_statistics_query
// makes them conditional on tessellation support rather than unconditional, and the only
// thing an application (or the conformance suite) can read to decide whether an
// implementation has it is the GL_ARB_tessellation_shader string. MobileGL does not emit it
// today, so these two answer GL_INVALID_ENUM: an API surface that accepts a
// tessellation-conditional token while withholding the string that announces the condition
// is self-contradictory, and it is the contradiction the suite catches
// (KHR-GL46.pipeline_statistics_query_tests_ARB.api_coverage_unsupported_calls, whose
// support probe is gl4cPipelineStatisticsQueryTests.cpp:1166-1176). The gate is the
// advertisement itself, not a hardcoded "no", so this is one switch and not two.
Bool IsPipelineStatisticsQueryTarget(GLenum target) {
switch (target) {
case GL_VERTICES_SUBMITTED:
case GL_PRIMITIVES_SUBMITTED:
case GL_VERTEX_SHADER_INVOCATIONS:
case GL_GEOMETRY_SHADER_INVOCATIONS:
case GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED:
case GL_FRAGMENT_SHADER_INVOCATIONS:
case GL_COMPUTE_SHADER_INVOCATIONS:
case GL_CLIPPING_INPUT_PRIMITIVES:
case GL_CLIPPING_OUTPUT_PRIMITIVES:
return true;
case GL_TESS_CONTROL_SHADER_PATCHES:
case GL_TESS_EVALUATION_SHADER_INVOCATIONS:
return AdvertisesTessellationShaderExtension();
default:
return false;
}
}
Bool TimerQueryDisabled() { Bool TimerQueryDisabled() {
return MG_Config::Features.DisableTimerQuery; return MG_Config::Features.DisableTimerQuery;
@@ -101,6 +165,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void ResetQueryObjectLocked(QueryObject* queryObject) { void ResetQueryObjectLocked(QueryObject* queryObject) {
if (queryObject->backendHandle) { if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) { if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle); deleteBackendQuery(queryObject->backendHandle);
} }
queryObject->backendHandle = nullptr; queryObject->backendHandle = nullptr;
@@ -115,6 +180,7 @@ namespace MobileGL::MG_Impl::GLImpl {
void EndTimeElapsedQueryLocked(QueryObject* queryObject) { void EndTimeElapsedQueryLocked(QueryObject* queryObject) {
const auto endTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.EndTimeElapsedQuery; const auto endTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.EndTimeElapsedQuery;
if (endTimeElapsedQuery && queryObject->backendHandle) { if (endTimeElapsedQuery && queryObject->backendHandle) {
MGP_FILL(EndTimeElapsedQuery);
endTimeElapsedQuery(queryObject->backendHandle); endTimeElapsedQuery(queryObject->backendHandle);
} }
queryObject->active = false; queryObject->active = false;
@@ -122,6 +188,46 @@ namespace MobileGL::MG_Impl::GLImpl {
g_activeTimeElapsedQueryId = 0; g_activeTimeElapsedQueryId = 0;
} }
// The CPU accounting counter a transform feedback query target reads: what the capture
// buffers took for GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, and everything the capture
// stage assembled - a paused span included - for GL_PRIMITIVES_GENERATED. One counter
// for both targets would report the clamped written count as the generated one.
Uint64 TransformFeedbackCounterForTarget(GLenum target) {
return target == GL_PRIMITIVES_GENERATED
? MG_State::pGLContext->GetTransformFeedbackGeneratedCounter()
: MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter();
}
// The span's CPU accounting delta. Saturating: a snapshot left above its counter (a
// context switch between Begin and End, a counter that never moved) would otherwise
// wrap to 2^64-1, which GetQueryObjectuiv hands the app as 4294967295.
Uint64 TransformFeedbackCpuResult(const QueryObject* queryObject) {
const Uint64 counter = TransformFeedbackCounterForTarget(queryObject->target);
return counter > queryObject->counterSnapshot ? counter - queryObject->counterSnapshot : 0;
}
// Whether this ended span's result should come from the CPU accounting rather than from
// the backend query it also ran. Three conditions, all necessary:
// * the backend asked for it (DirectGLES, whose ES driver counter is the unreliable
// one; DirectVulkan never sets the bit and so is untouched by any of this);
// * the target is PRIMITIVES_WRITTEN. GL_PRIMITIVES_GENERATED counts primitives
// whether or not a capture is active, and the accounting only ever sees capture
// draws, so the backend's counter is the more complete answer there;
// * the span was fully accounted: at least one capture draw reached the accounting
// (the instanced, indirect and multi-draw entry points do not call it at all, so a
// span made of those is invisible to it) and none of them amplified through a
// geometry stage, which the CPU cannot model.
Bool PrefersCpuTransformFeedbackResult(const QueryObject* queryObject) {
if (!MG_Backend::gBackendFunctionsTable.GL.PrefersCpuXfbPrimitiveAccounting) return false;
if (queryObject->target != GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN) return false;
if (MG_State::pGLContext->GetTransformFeedbackGeometryCaptureDraws() !=
queryObject->geometryCaptureDrawSnapshot) {
return false;
}
return MG_State::pGLContext->GetTransformFeedbackAccountedCaptureDraws() !=
queryObject->accountedCaptureDrawSnapshot;
}
// Shared GetQueryObject* implementation. Returns false when an error // Shared GetQueryObject* implementation. Returns false when an error
// was recorded and no value should be written back. `outValueProduced`, when given, // was recorded and no value should be written back. `outValueProduced`, when given,
// additionally distinguishes "succeeded with a value" from "succeeded but the result is not // additionally distinguishes "succeeded with a value" from "succeeded but the result is not
@@ -154,6 +260,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
Uint64 result = 0; Uint64 result = 0;
const auto getQueryResult64 = MG_Backend::gBackendFunctionsTable.GL.GetQueryResult64; const auto getQueryResult64 = MG_Backend::gBackendFunctionsTable.GL.GetQueryResult64;
MGP_FILL(GetQueryResult64);
if (queryObject->backendHandle && getQueryResult64 && if (queryObject->backendHandle && getQueryResult64 &&
!getQueryResult64(queryObject->backendHandle, /*wait=*/false, &result)) { !getQueryResult64(queryObject->backendHandle, /*wait=*/false, &result)) {
// Not ready. The whole point of the no-wait form is that the caller's // Not ready. The whole point of the no-wait form is that the caller's
@@ -168,6 +275,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
if (queryObject->backendHandle) { if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) { if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle); deleteBackendQuery(queryObject->backendHandle);
} }
queryObject->backendHandle = nullptr; queryObject->backendHandle = nullptr;
@@ -183,6 +291,7 @@ namespace MobileGL::MG_Impl::GLImpl {
return true; return true;
} }
const auto isQueryResultAvailable = MG_Backend::gBackendFunctionsTable.GL.IsQueryResultAvailable; const auto isQueryResultAvailable = MG_Backend::gBackendFunctionsTable.GL.IsQueryResultAvailable;
MGP_FILL(IsQueryResultAvailable);
outValue = (!isQueryResultAvailable || isQueryResultAvailable(queryObject->backendHandle)) ? 1 : 0; outValue = (!isQueryResultAvailable || isQueryResultAvailable(queryObject->backendHandle)) ? 1 : 0;
return true; return true;
} }
@@ -194,6 +303,7 @@ namespace MobileGL::MG_Impl::GLImpl {
Uint64 result = 0; Uint64 result = 0;
if (queryObject->backendHandle) { if (queryObject->backendHandle) {
const auto getQueryResult64 = MG_Backend::gBackendFunctionsTable.GL.GetQueryResult64; const auto getQueryResult64 = MG_Backend::gBackendFunctionsTable.GL.GetQueryResult64;
MGP_FILL(GetQueryResult64);
if (getQueryResult64 && if (getQueryResult64 &&
!getQueryResult64(queryObject->backendHandle, /*wait=*/true, &result)) { !getQueryResult64(queryObject->backendHandle, /*wait=*/true, &result)) {
// The backend could not produce the result YET (e.g. a // The backend could not produce the result YET (e.g. a
@@ -214,6 +324,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// query degrades to a zero result); the backend handle is // query degrades to a zero result); the backend handle is
// consumed and the value cached for later reads. // consumed and the value cached for later reads.
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) { if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle); deleteBackendQuery(queryObject->backendHandle);
} }
queryObject->backendHandle = nullptr; queryObject->backendHandle = nullptr;
@@ -319,10 +430,14 @@ namespace MobileGL::MG_Impl::GLImpl {
queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) { queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery; if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
endOcclusionQuery && queryObject->backendHandle) { endOcclusionQuery && queryObject->backendHandle) {
MGP_FILL(EndOcclusionQuery);
endOcclusionQuery(queryObject->backendHandle); endOcclusionQuery(queryObject->backendHandle);
} }
queryObject->active = false; queryObject->active = false;
g_activeSamplesPassedQueryId = 0; g_activeSamplesPassedQueryId = 0;
} else if (IsPipelineStatisticsQueryTarget(queryObject->target)) {
queryObject->active = false;
g_activePipelineStatisticsQueryIds[queryObject->target] = 0;
} else if (queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || } else if (queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ||
queryObject->target == GL_PRIMITIVES_GENERATED) { queryObject->target == GL_PRIMITIVES_GENERATED) {
queryObject->active = false; queryObject->active = false;
@@ -335,6 +450,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
if (queryObject->backendHandle) { if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) { if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle); deleteBackendQuery(queryObject->backendHandle);
} }
queryObject->backendHandle = nullptr; queryObject->backendHandle = nullptr;
@@ -363,7 +479,9 @@ namespace MobileGL::MG_Impl::GLImpl {
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED || (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) && target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr; MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) { const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target);
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
!isPipelineStatisticsQuery) {
// GL_TIMESTAMP is not a valid BeginQuery target; the occlusion targets // GL_TIMESTAMP is not a valid BeginQuery target; the occlusion targets
// need backend support. // need backend support.
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported."); RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
@@ -379,10 +497,12 @@ namespace MobileGL::MG_Impl::GLImpl {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist."); RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist.");
return; return;
} }
GLuint& activeQueryId = isTransformFeedbackQuery GLuint& activeQueryId = isPipelineStatisticsQuery
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId ? g_activePipelineStatisticsQueryIds[target]
: g_activePrimitivesGeneratedQueryId) : (isTransformFeedbackQuery
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId); ? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
: g_activePrimitivesGeneratedQueryId)
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId));
if (activeQueryId != 0) { if (activeQueryId != 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
"A query is already active on this target."); "A query is already active on this target.");
@@ -401,17 +521,28 @@ namespace MobileGL::MG_Impl::GLImpl {
ResetQueryObjectLocked(queryObject); // discard any previous result ResetQueryObjectLocked(queryObject); // discard any previous result
queryObject->target = target; queryObject->target = target;
queryObject->active = true; queryObject->active = true;
if (isTransformFeedbackQuery) { if (isPipelineStatisticsQuery) {
// Nothing to start: the counter is uninstrumented and GL_QUERY_COUNTER_BITS says so.
// The object still becomes a real, target-latched query so every other rule about it
// (re-use with another target, double-begin, EndQuery pairing) keeps holding.
} else if (isTransformFeedbackQuery) {
// Prefer real GPU transform-feedback queries (exact with geometry shaders); // Prefer real GPU transform-feedback queries (exact with geometry shaders);
// the CPU accounting delta stays as the fallback when the backend lacks them. // the CPU accounting delta stays as the fallback when the backend lacks them.
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery; const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
MGP_FILL(BeginXfbPrimitivesQuery);
queryObject->backendHandle = queryObject->backendHandle =
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr; beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
queryObject->counterSnapshot = MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter(); queryObject->counterSnapshot = TransformFeedbackCounterForTarget(target);
queryObject->accountedCaptureDrawSnapshot =
MG_State::pGLContext->GetTransformFeedbackAccountedCaptureDraws();
queryObject->geometryCaptureDrawSnapshot =
MG_State::pGLContext->GetTransformFeedbackGeometryCaptureDraws();
} else if (isOcclusionQuery) { } else if (isOcclusionQuery) {
MGP_FILL(BeginOcclusionQuery);
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery(); queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
} else { } else {
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery; const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
MGP_FILL(BeginTimeElapsedQuery);
queryObject->backendHandle = queryObject->backendHandle =
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr; (!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
} }
@@ -425,15 +556,19 @@ namespace MobileGL::MG_Impl::GLImpl {
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED || (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) && target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr; MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) { const Bool isPipelineStatisticsQuery = IsPipelineStatisticsQueryTarget(target);
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery &&
!isPipelineStatisticsQuery) {
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported."); RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
return; return;
} }
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex); const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
GLuint& activeQueryId = isTransformFeedbackQuery GLuint& activeQueryId = isPipelineStatisticsQuery
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId ? g_activePipelineStatisticsQueryIds[target]
: g_activePrimitivesGeneratedQueryId) : (isTransformFeedbackQuery
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId); ? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
: g_activePrimitivesGeneratedQueryId)
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId));
if (activeQueryId == 0) { if (activeQueryId == 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on this target."); RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on this target.");
return; return;
@@ -443,17 +578,39 @@ namespace MobileGL::MG_Impl::GLImpl {
activeQueryId = 0; // should not happen; keep state consistent activeQueryId = 0; // should not happen; keep state consistent
return; return;
} }
if (isPipelineStatisticsQuery) {
// The result is a definite zero rather than an unread backend handle, so a later
// GetQueryObject* answers immediately and never waits on something that was never
// started. GL_QUERY_COUNTER_BITS = 0 is what marks that zero indeterminate.
queryObject->cachedResult = 0;
queryObject->resultCached = true;
queryObject->active = false;
queryObject->ended = true;
activeQueryId = 0;
return;
}
if (isTransformFeedbackQuery) { if (isTransformFeedbackQuery) {
if (queryObject->backendHandle) { if (queryObject->backendHandle) {
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) { if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
MGP_FILL(EndXfbPrimitivesQuery);
endXfbPrimitivesQuery(queryObject->backendHandle); endXfbPrimitivesQuery(queryObject->backendHandle);
} }
// Result comes from the GPU query at read time. }
} else { // A backend query that is not going to be read is released here, not left to be
queryObject->cachedResult = // collected later: the span is over, the driver object has nothing left to say.
MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter() - queryObject->counterSnapshot; // Ending it first is what makes that legal.
if (!queryObject->backendHandle || PrefersCpuTransformFeedbackResult(queryObject)) {
if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle);
}
queryObject->backendHandle = nullptr;
}
queryObject->cachedResult = TransformFeedbackCpuResult(queryObject);
queryObject->resultCached = true; queryObject->resultCached = true;
} }
// Otherwise the result comes from the GPU query at read time.
queryObject->active = false; queryObject->active = false;
queryObject->ended = true; queryObject->ended = true;
activeQueryId = 0; activeQueryId = 0;
@@ -462,6 +619,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (isOcclusionQuery) { if (isOcclusionQuery) {
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery; if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
endOcclusionQuery && queryObject->backendHandle) { endOcclusionQuery && queryObject->backendHandle) {
MGP_FILL(EndOcclusionQuery);
endOcclusionQuery(queryObject->backendHandle); endOcclusionQuery(queryObject->backendHandle);
} }
queryObject->active = false; queryObject->active = false;
@@ -500,11 +658,81 @@ namespace MobileGL::MG_Impl::GLImpl {
ResetQueryObjectLocked(queryObject); // discard any previous result ResetQueryObjectLocked(queryObject); // discard any previous result
queryObject->target = target; queryObject->target = target;
const auto queryCounterTimestamp = MG_Backend::gBackendFunctionsTable.GL.QueryCounterTimestamp; const auto queryCounterTimestamp = MG_Backend::gBackendFunctionsTable.GL.QueryCounterTimestamp;
MGP_FILL(QueryCounterTimestamp);
queryObject->backendHandle = queryObject->backendHandle =
(!TimerQueryDisabled() && queryCounterTimestamp) ? queryCounterTimestamp() : nullptr; (!TimerQueryDisabled() && queryCounterTimestamp) ? queryCounterTimestamp() : nullptr;
queryObject->ended = true; queryObject->ended = true;
} }
void BeginConditionalRender(GLuint id, GLenum mode) {
// GL 4.6 core 10.9's eight modes. The _INVERTED half flips the sense of the predicate;
// the BY_REGION half only narrows WHERE an implementation is permitted to discard, so
// treating it as its whole-framebuffer sibling is what an implementation without region
// granularity does. The _NO_WAIT half is a permission to render rather than stall, not an
// obligation - see the resolve below.
Bool inverted = false;
switch (mode) {
case GL_QUERY_WAIT:
case GL_QUERY_NO_WAIT:
case GL_QUERY_BY_REGION_WAIT:
case GL_QUERY_BY_REGION_NO_WAIT:
inverted = false;
break;
case GL_QUERY_WAIT_INVERTED:
case GL_QUERY_NO_WAIT_INVERTED:
case GL_QUERY_BY_REGION_WAIT_INVERTED:
case GL_QUERY_BY_REGION_NO_WAIT_INVERTED:
inverted = true;
break;
default:
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "mode is not a conditional render mode.");
return;
}
if (MG_State::pGLContext->IsConditionalRenderActive()) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Conditional rendering is already active.");
return;
}
{
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
const auto* queryObject = FindQueryObjectLocked(id);
// A generated NAME is not yet a query object; it becomes one at its first use with a
// target (the same rule glIsQuery answers by).
if (!queryObject || (!queryObject->created && queryObject->target == 0)) {
RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "id is not the name of a query object.");
return;
}
if (queryObject->active) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "The query object is still active.");
return;
}
if (queryObject->target != GL_SAMPLES_PASSED && queryObject->target != GL_ANY_SAMPLES_PASSED &&
queryObject->target != GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
"Conditional rendering requires an occlusion query object.");
return;
}
}
// Resolved ONCE, here, and by WAITING even for the _NO_WAIT modes: the spec lets those
// render instead of stalling, so always waiting is conforming and is the only choice that
// gives the whole block one deterministic verdict. Reading it per command instead would
// let a result that lands mid-block change the answer half way through.
Uint64 samplesPassed = 0;
if (!GetQueryObjectValue(id, GL_QUERY_RESULT, __FUNCTION__, samplesPassed)) return;
const Bool passed = samplesPassed != 0;
MG_State::pGLContext->BeginConditionalRender(id, mode, inverted ? passed : !passed);
}
void EndConditionalRender() {
if (!MG_State::pGLContext->IsConditionalRenderActive()) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Conditional rendering is not active.");
return;
}
MG_State::pGLContext->EndConditionalRender();
}
void GetQueryiv(GLenum target, GLenum pname, GLint* params) { void GetQueryiv(GLenum target, GLenum pname, GLint* params) {
if (!params) { if (!params) {
return; return;
@@ -528,7 +756,12 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = static_cast<GLint>(g_activePrimitivesGeneratedQueryId); *params = static_cast<GLint>(g_activePrimitivesGeneratedQueryId);
break; break;
default: default:
*params = 0; if (IsPipelineStatisticsQueryTarget(target)) {
const auto it = g_activePipelineStatisticsQueryIds.find(target);
*params = it != g_activePipelineStatisticsQueryIds.end() ? static_cast<GLint>(it->second) : 0;
} else {
*params = 0;
}
break; break;
} }
return; return;
@@ -539,6 +772,14 @@ namespace MobileGL::MG_Impl::GLImpl {
// entry points / timestamp valid bits at call time, not at table // entry points / timestamp valid bits at call time, not at table
// init), and the MOBILEGL_DISABLE_TIMERQUERY kill switch always // init), and the MOBILEGL_DISABLE_TIMERQUERY kill switch always
// wins. // wins.
if (IsPipelineStatisticsQueryTarget(target)) {
// Zero: GL 4.6 core 4.2.1's way of saying the counter is not implemented and its
// results are indeterminate. The conformance suite reads exactly this and skips
// the functional half of each such target, which is the outcome an uninstrumented
// counter should produce.
*params = 0;
return;
}
if (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED || if (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) { target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
const Bool occlusionSupported = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr; const Bool occlusionSupported = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
@@ -547,6 +788,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
const Bool timerTarget = target == GL_TIME_ELAPSED || target == GL_TIMESTAMP; const Bool timerTarget = target == GL_TIME_ELAPSED || target == GL_TIMESTAMP;
const auto isTimerQuerySupported = MG_Backend::gBackendFunctionsTable.GL.IsTimerQuerySupported; const auto isTimerQuerySupported = MG_Backend::gBackendFunctionsTable.GL.IsTimerQuerySupported;
MGP_FILL(IsTimerQuerySupported);
const Bool supported = const Bool supported =
timerTarget && !TimerQueryDisabled() && isTimerQuerySupported && isTimerQuerySupported(); timerTarget && !TimerQueryDisabled() && isTimerQuerySupported && isTimerQuerySupported();
*params = supported ? 64 : 0; *params = supported ? 64 : 0;
@@ -612,14 +854,24 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
namespace { namespace {
Bool IsPerVertexStreamQueryTarget(GLenum target) {
return target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN;
}
// The indexed query entry points differ from the plain ones only in the vertex // The indexed query entry points differ from the plain ones only in the vertex
// stream they address (GL 4.6 core 4.2.1): index must be below GL_MAX_VERTEX_STREAMS // stream they address (GL 4.6 core 4.2.1): index must be below GL_MAX_VERTEX_STREAMS
// for the two transform feedback targets and zero for every other target. With a // for the two transform feedback targets and zero for every other target. MobileGL
// single vertex stream both bounds are 1, so a valid call is always index 0 and // implements ONE vertex stream, so both bounds are 1 and a valid call is always index 0 -
// forwards to the unindexed implementation. // which is what makes the three forwards below equivalent to the unindexed entry points.
//
// THAT EQUIVALENCE IS THE WHOLE JUSTIFICATION, and it is read out of the getter rather
// than assumed: the moment GL_MAX_VERTEX_STREAMS answers more than one, index 1..3 starts
// reaching EndQueryIndexed and GetQueryIndexediv, which resolve the active query from
// per-TARGET globals and would end - or report - a query begun on a different stream.
// Raising that limit therefore means giving each active query a stream index and
// comparing it here, not just changing the number.
Bool ValidateQueryStreamIndex(const char* function, GLenum target, GLuint index) { Bool ValidateQueryStreamIndex(const char* function, GLenum target, GLuint index) {
const Bool perStreamTarget = const Bool perStreamTarget = IsPerVertexStreamQueryTarget(target);
target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN;
GLint maxVertexStreams = 1; GLint maxVertexStreams = 1;
if (perStreamTarget) { if (perStreamTarget) {
GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams); GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams);
@@ -632,6 +884,7 @@ namespace MobileGL::MG_Impl::GLImpl {
: "index must be zero for this query target."); : "index must be zero for this query target.");
return false; return false;
} }
} // namespace } // namespace
void BeginQueryIndexed(GLenum target, GLuint index, GLuint id) { void BeginQueryIndexed(GLenum target, GLuint index, GLuint id) {
@@ -648,4 +901,40 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return; if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
GetQueryiv(target, pname, params); GetQueryiv(target, pname, params);
} }
void DestroyAllQueryObjects() {
// Detach the registry under the lock, release outside it - same discipline
// (and the same accepted teardown race) as DestroyAllSyncObjects. Without
// this drain, every query the app left undeleted survived full library
// teardown in the process-global registry: the objects and their backend
// wrappers leaked across Destroy/Initialize cycles, stale ids kept
// answering IsQuery == GL_TRUE in the re-initialized library, and a later
// glDeleteQueries could hand the OLD backend's handle to a DIFFERENT
// backend's DeleteBackendQuery, which casts it to the wrong wrapper type.
UnorderedMap<GLuint, QueryObject*> orphans;
{
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
orphans.swap(g_liveQueryObjects);
g_activeTimeElapsedQueryId = 0;
g_activePrimitivesWrittenQueryId = 0;
g_activePrimitivesGeneratedQueryId = 0;
g_activeSamplesPassedQueryId = 0;
}
if (orphans.empty()) {
return;
}
// Backend handles must be released by the backend that created them, so
// this runs while the function table is still populated. Both backends'
// DeleteBackendQuery are generation-guarded, so a handle whose renderer
// or ES context is already gone frees only the wrapper.
const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery;
for (const auto& [_, queryObject] : orphans) {
if (deleteBackendQuery && queryObject->backendHandle) {
MGP_FILL(DeleteBackendQuery);
deleteBackendQuery(queryObject->backendHandle);
}
delete queryObject;
}
MGLOG_D("DestroyAllQueryObjects: reclaimed %zu query object(s) the app left undeleted", orphans.size());
}
} // namespace MobileGL::MG_Impl::GLImpl } // namespace MobileGL::MG_Impl::GLImpl
+14
View File
@@ -29,4 +29,18 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset); void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset); void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
void QueryCounter(GLuint id, GLenum target); void QueryCounter(GLuint id, GLenum target);
// Conditional rendering (GL 4.6 core 10.9). Implemented here rather than beside the drawing
// entry points because the predicate is a QUERY OBJECT's result, and the object registry -
// with the lock that guards it - lives in this file.
void BeginConditionalRender(GLuint id, GLenum mode);
void EndConditionalRender();
// Destroys every still-registered query object exactly as DeleteQueries would.
// GL requires queries to die with their context; called only from full library
// teardown (DestroyImpl), where no context survives on any thread, so the
// process-global registry can be drained wholesale. Must run while the backend
// function table is still populated: each backend handle has to be released by
// the backend that created it, never by a later re-initialized one (whose
// DeleteBackendQuery would cast the wrapper to the wrong backend's type).
// Same contract as DestroyAllSyncObjects.
void DestroyAllQueryObjects();
} // namespace MobileGL::MG_Impl::GLImpl } // namespace MobileGL::MG_Impl::GLImpl
@@ -8,6 +8,7 @@
#include "GL_RenderState.h" #include "GL_RenderState.h"
#include <cmath> #include <cmath>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h> #include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToMG/RenderStateEnumConverter.h> #include <MG_Util/Converters/GLToMG/RenderStateEnumConverter.h>
@@ -19,28 +20,118 @@ namespace MobileGL::MG_Impl::GLImpl {
return std::clamp(static_cast<Float>(value), 0.0f, 1.0f); return std::clamp(static_cast<Float>(value), 0.0f, 1.0f);
} }
static Bool ValidateIndexedBlendCapability(GLenum target, GLuint index, const char* functionName) { // GL 4.6 core 17.3.2 and 22.1 give exactly two indexed capabilities: GL_BLEND, indexed by
if (target != GL_BLEND) { // draw buffer, and GL_SCISSOR_TEST, indexed by viewport. They have DIFFERENT bounds
// (MAX_DRAW_BUFFERS vs MAX_VIEWPORTS), so the limit is picked per target rather than shared.
static Bool ValidateIndexedCapability(GLenum target, GLuint index, const char* functionName) {
GLuint limit = 0;
const char* indexName = nullptr;
switch (target) {
case GL_BLEND:
limit = MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS;
indexName = "Buffer";
break;
case GL_SCISSOR_TEST:
limit = RenderStateParameters::MAX_VIEWPORTS;
indexName = "Viewport";
break;
default:
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum, ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Only GL_BLEND is supported for indexed capability state.")); "Only GL_BLEND and GL_SCISSOR_TEST are supported for indexed "
"capability state."));
return false; return false;
} }
if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) { if (index >= limit) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>( MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"MG_Impl/GLImpl", functionName, String(indexName) + " index " + std::to_string(index) +
"Buffer index " + std::to_string(index) + " is out of range. Max supported is " + " is out of range. Max supported is " + std::to_string(limit - 1) +
std::to_string(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS - 1) + ".")); "."));
return false; return false;
} }
return true; return true;
} }
// ------------------ ARB_viewport_array parameter validation ------------------
// All three families share the same two shapes, so they share the two checkers. GL 4.6 core
// 13.6.1/17.3.2: an out-of-range index is GL_INVALID_VALUE, and so is a negative width or
// height. `first + count == MAX_VIEWPORTS` is LEGAL - only strictly greater is an error,
// which KHR-GL43.viewport_array.api_errors checks explicitly in both directions.
static Bool ValidateViewportIndex(GLuint index, const char* functionName) {
if (index < RenderStateParameters::MAX_VIEWPORTS) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Viewport index " + std::to_string(index) +
" is out of range. Max supported is " +
std::to_string(RenderStateParameters::MAX_VIEWPORTS - 1) + "."));
return false;
}
static Bool ValidateViewportRange(GLuint first, GLsizei count, const char* functionName) {
if (count < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "count must not be negative."));
return false;
}
// Widened before adding: first is a GLuint and count a GLsizei, so `first + count` in
// 32 bits can wrap past MAX_VIEWPORTS and let an out-of-range range through.
const Uint64 last = static_cast<Uint64>(first) + static_cast<Uint64>(count);
if (last > RenderStateParameters::MAX_VIEWPORTS) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"first (" + std::to_string(first) + ") + count (" +
std::to_string(count) + ") exceeds GL_MAX_VIEWPORTS (" +
std::to_string(RenderStateParameters::MAX_VIEWPORTS) + ")."));
return false;
}
return true;
}
template <typename T>
static Bool ValidateNonNegativeExtent(T width, T height, const char* functionName) {
if (width >= T(0) && height >= T(0)) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "Width and height must be non-negative."));
return false;
}
// The array forms are all-or-nothing: one bad element rejects the whole call with a SINGLE
// GL_INVALID_VALUE and leaves every rectangle untouched. api_errors relies on both halves -
// it passes a full 16-element array with exactly one negative extent and then asserts the
// error queue holds exactly one entry.
template <typename T>
static Bool ValidateArrayExtents(GLsizei count, const T* v, const char* functionName) {
for (GLsizei i = 0; i < count; ++i) {
if (v[i * 4 + 2] >= T(0) && v[i * 4 + 3] >= T(0)) continue;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
"Width and height must be non-negative (element " + std::to_string(i) +
")."));
return false;
}
return true;
}
static Bool ValidateNonNullArray(const void* v, const char* functionName) {
if (v != nullptr) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "value pointer cannot be null."));
return false;
}
static Bool TryConvertBlendEquation(GLenum mode, const char* functionName, static Bool TryConvertBlendEquation(GLenum mode, const char* functionName,
::MobileGL::BlendEquation& outEquation) { ::MobileGL::BlendEquation& outEquation) {
outEquation = MG_Util::ConvertGLEnumToBlendEquation(mode); outEquation = MG_Util::ConvertGLEnumToBlendEquation(mode);
@@ -92,16 +183,70 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void Viewport_State(GLint x, GLint y, GLsizei width, GLsizei height) { void Viewport_State(GLint x, GLint y, GLsizei width, GLsizei height) {
if (width < 0 || height < 0) { if (!ValidateNonNegativeExtent(width, height, "Viewport_State")) return;
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "Viewport_State",
"Width abd height must be non-negative."));
return;
}
MG_State::pGLContext->SetViewport(IntVec4(x, y, width, height)); MG_State::pGLContext->SetViewport(IntVec4(x, y, width, height));
} }
// ------------------ ARB_viewport_array setters ------------------
void ViewportArrayv_State(GLuint first, GLsizei count, const GLfloat* v) {
if (!ValidateViewportRange(first, count, "ViewportArrayv_State")) return;
if (count == 0) return;
if (!ValidateNonNullArray(v, "ViewportArrayv_State")) return;
if (!ValidateArrayExtents(count, v, "ViewportArrayv_State")) return;
for (GLsizei i = 0; i < count; ++i) {
MG_State::pGLContext->SetViewportIndexed(first + static_cast<GLuint>(i),
FloatVec4(v[i * 4 + 0], v[i * 4 + 1], v[i * 4 + 2], v[i * 4 + 3]));
}
}
void ViewportIndexedf_State(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) {
if (!ValidateViewportIndex(index, "ViewportIndexedf_State")) return;
if (!ValidateNonNegativeExtent(w, h, "ViewportIndexedf_State")) return;
MG_State::pGLContext->SetViewportIndexed(index, FloatVec4(x, y, w, h));
}
void ScissorArrayv_State(GLuint first, GLsizei count, const GLint* v) {
if (!ValidateViewportRange(first, count, "ScissorArrayv_State")) return;
if (count == 0) return;
if (!ValidateNonNullArray(v, "ScissorArrayv_State")) return;
if (!ValidateArrayExtents(count, v, "ScissorArrayv_State")) return;
for (GLsizei i = 0; i < count; ++i) {
MG_State::pGLContext->SetScissorBoxIndexed(first + static_cast<GLuint>(i),
IntVec4(v[i * 4 + 0], v[i * 4 + 1], v[i * 4 + 2], v[i * 4 + 3]));
}
}
void ScissorIndexed_State(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) {
if (!ValidateViewportIndex(index, "ScissorIndexed_State")) return;
if (!ValidateNonNegativeExtent(width, height, "ScissorIndexed_State")) return;
MG_State::pGLContext->SetScissorBoxIndexed(index, IntVec4(left, bottom, width, height));
}
void DepthRangeArrayv_State(GLuint first, GLsizei count, const GLdouble* v) {
if (!ValidateViewportRange(first, count, "DepthRangeArrayv_State")) return;
if (count == 0) return;
if (!ValidateNonNullArray(v, "DepthRangeArrayv_State")) return;
for (GLsizei i = 0; i < count; ++i) {
MG_State::pGLContext->SetDepthRangeIndexed(
first + static_cast<GLuint>(i),
FloatVec2(ClampUnitFloat(static_cast<GLfloat>(v[i * 2 + 0])),
ClampUnitFloat(static_cast<GLfloat>(v[i * 2 + 1]))));
}
}
void DepthRangeIndexed_State(GLuint index, GLdouble n, GLdouble f) {
if (!ValidateViewportIndex(index, "DepthRangeIndexed_State")) return;
MG_State::pGLContext->SetDepthRangeIndexed(
index, FloatVec2(ClampUnitFloat(static_cast<GLfloat>(n)), ClampUnitFloat(static_cast<GLfloat>(f))));
}
void StencilOpSeparate_State(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) { void StencilOpSeparate_State(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
Bool applyFront = false; Bool applyFront = false;
Bool applyBack = false; Bool applyBack = false;
@@ -174,12 +319,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void Scissor_State(GLint x, GLint y, GLsizei width, GLsizei height) { void Scissor_State(GLint x, GLint y, GLsizei width, GLsizei height) {
if (width < 0 || height < 0) { if (!ValidateNonNegativeExtent(width, height, "Scissor_State")) return;
MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "Scissor_State",
"Width abd height must be non-negative."));
return;
}
MG_State::pGLContext->SetScissorBox(IntVec4(x, y, width, height)); MG_State::pGLContext->SetScissorBox(IntVec4(x, y, width, height));
} }
@@ -188,10 +328,50 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_State::pGLContext->SetSampleCoverage(std::clamp(static_cast<Float>(value), 0.0f, 1.0f), invert == GL_TRUE); MG_State::pGLContext->SetSampleCoverage(std::clamp(static_cast<Float>(value), 0.0f, 1.0f), invert == GL_TRUE);
} }
// ARB_sample_shading / GL 4.6 core 14.3.1: "value is clamped to [0, 1] when specified", so
// there is no error to raise - a caller that asks for 2.0 gets 1.0 and GL_MIN_SAMPLE_SHADING_-
// VALUE reads back 1.0. Was a logging no-op while ARB_sample_shading was advertised, which
// let an application enable GL_SAMPLE_SHADING and then quietly get the driver's default rate.
void MinSampleShading_State(GLfloat value) {
MG_State::pGLContext->SetMinSampleShadingValue(std::clamp(static_cast<Float>(value), 0.0f, 1.0f));
}
void PolygonOffset_State(GLfloat factor, GLfloat units) { void PolygonOffset_State(GLfloat factor, GLfloat units) {
MG_State::pGLContext->SetPolygonOffset(static_cast<Float>(factor), static_cast<Float>(units)); MG_State::pGLContext->SetPolygonOffset(static_cast<Float>(factor), static_cast<Float>(units));
} }
void PolygonOffsetClamp_State(GLfloat factor, GLfloat units, GLfloat clamp) {
// GL 4.6 core 14.6.5 / GL_EXT_polygon_offset_clamp. No error cases: any three floats are
// legal, and clamp = 0 is exactly glPolygonOffset. Whether the backend can APPLY the clamp
// is a separate question (see the DirectGLES/DirectVulkan forwarding); the state is
// recorded either way, because GL_POLYGON_OFFSET_CLAMP has to read back what was written.
MG_State::pGLContext->SetPolygonOffsetClamped(static_cast<Float>(factor), static_cast<Float>(units),
static_cast<Float>(clamp));
}
void ClipControl_State(GLenum origin, GLenum depth) {
// GL 4.5 core 13.5: both arguments are strict enums, and either being wrong is
// GL_INVALID_ENUM with the state left untouched.
if (origin != GL_LOWER_LEFT && origin != GL_UPPER_LEFT) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"glClipControl origin must be GL_LOWER_LEFT or GL_UPPER_LEFT; got " +
MG_Util::ConvertGLEnumToString(origin) + "."));
return;
}
if (depth != GL_NEGATIVE_ONE_TO_ONE && depth != GL_ZERO_TO_ONE) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"glClipControl depth must be GL_NEGATIVE_ONE_TO_ONE or GL_ZERO_TO_ONE; got " +
MG_Util::ConvertGLEnumToString(depth) + "."));
return;
}
MG_State::pGLContext->SetClipControl(origin, depth);
}
void PolygonMode_State(GLenum face, GLenum mode) { void PolygonMode_State(GLenum face, GLenum mode) {
// GL 3.3 core: separate front/back polygon modes were removed in 3.1, so the only legal // GL 3.3 core: separate front/back polygon modes were removed in 3.1, so the only legal
// face is GL_FRONT_AND_BACK. GL_FRONT / GL_BACK must be rejected (some desktop drivers // face is GL_FRONT_AND_BACK. GL_FRONT / GL_BACK must be rejected (some desktop drivers
@@ -335,7 +515,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
GLboolean IsEnabledi_State(GLenum target, GLuint index) { GLboolean IsEnabledi_State(GLenum target, GLuint index) {
if (!ValidateIndexedBlendCapability(target, index, "IsEnabledi_State")) { if (!ValidateIndexedCapability(target, index, "IsEnabledi_State")) {
return GL_FALSE; return GL_FALSE;
} }
@@ -380,7 +560,25 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
*data = IsEnabledi_State(target, index); // GL 4.6 core 22.1: glGetBooleani_v answers EVERY indexed state, not just the indexed
// capabilities - a non-boolean value simply reads back as "is it non-zero". Routing the
// non-capability enums to the pname table glGetIntegeri_v already owns is what makes
// that true; without it a query like glGetBooleani_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 0)
// came back GL_INVALID_ENUM (KHR-GL43.compute_shader.max).
if (MG_Util::ConvertGLEnumToCapabilityInput(target) != CapabilityInput::Unknown) {
*data = IsEnabledi_State(target, index);
return;
}
GLint values[4] = {};
GetIntegeri_v(target, index, values);
// The ARB_viewport_array rectangles are the only multi-component indexed state that
// reaches here; writing element 0 alone would leave the caller's other three untouched.
const GLsizei components = target == GL_VIEWPORT || target == GL_SCISSOR_BOX
? 4
: (target == GL_DEPTH_RANGE ? 2 : 1);
for (GLsizei i = 0; i < components; ++i) {
data[i] = values[i] != 0 ? GL_TRUE : GL_FALSE;
}
} }
GLboolean IsEnabled_State(GLenum cap) { GLboolean IsEnabled_State(GLenum cap) {
@@ -713,7 +911,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void Disablei_State(GLenum target, GLuint index) { void Disablei_State(GLenum target, GLuint index) {
if (!ValidateIndexedBlendCapability(target, index, "Disablei_State")) { if (!ValidateIndexedCapability(target, index, "Disablei_State")) {
return; return;
} }
@@ -731,7 +929,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void Enablei_State(GLenum target, GLuint index) { void Enablei_State(GLenum target, GLuint index) {
if (!ValidateIndexedBlendCapability(target, index, "Enablei_State")) { if (!ValidateIndexedCapability(target, index, "Enablei_State")) {
return; return;
} }
@@ -785,6 +983,44 @@ namespace MobileGL::MG_Impl::GLImpl {
Viewport_State(x, y, width, height); Viewport_State(x, y, width, height);
} }
void ViewportArrayv(GLuint first, GLsizei count, const GLfloat* v) {
ViewportArrayv_State(first, count, v);
}
void ViewportIndexedf(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h) {
ViewportIndexedf_State(index, x, y, w, h);
}
void ViewportIndexedfv(GLuint index, const GLfloat* v) {
// The index is validated before the pointer is touched: glViewportIndexedfv(MAX, nullptr)
// must be one GL_INVALID_VALUE, not a null dereference.
if (!ValidateViewportIndex(index, "ViewportIndexedfv")) return;
if (!ValidateNonNullArray(v, "ViewportIndexedfv")) return;
ViewportIndexedf_State(index, v[0], v[1], v[2], v[3]);
}
void ScissorArrayv(GLuint first, GLsizei count, const GLint* v) {
ScissorArrayv_State(first, count, v);
}
void ScissorIndexed(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height) {
ScissorIndexed_State(index, left, bottom, width, height);
}
void ScissorIndexedv(GLuint index, const GLint* v) {
if (!ValidateViewportIndex(index, "ScissorIndexedv")) return;
if (!ValidateNonNullArray(v, "ScissorIndexedv")) return;
ScissorIndexed_State(index, v[0], v[1], v[2], v[3]);
}
void DepthRangeArrayv(GLuint first, GLsizei count, const GLdouble* v) {
DepthRangeArrayv_State(first, count, v);
}
void DepthRangeIndexed(GLuint index, GLdouble n, GLdouble f) {
DepthRangeIndexed_State(index, n, f);
}
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) { void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) {
StencilOpSeparate_State(face, sfail, dpfail, dppass); StencilOpSeparate_State(face, sfail, dpfail, dppass);
} }
@@ -817,10 +1053,22 @@ namespace MobileGL::MG_Impl::GLImpl {
SampleCoverage_State(value, invert); SampleCoverage_State(value, invert);
} }
void MinSampleShading(GLfloat value) {
MinSampleShading_State(value);
}
void PolygonOffset(GLfloat factor, GLfloat units) { void PolygonOffset(GLfloat factor, GLfloat units) {
PolygonOffset_State(factor, units); PolygonOffset_State(factor, units);
} }
void PolygonOffsetClamp(GLfloat factor, GLfloat units, GLfloat clamp) {
PolygonOffsetClamp_State(factor, units, clamp);
}
void ClipControl(GLenum origin, GLenum depth) {
ClipControl_State(origin, depth);
}
void PolygonMode(GLenum face, GLenum mode) { void PolygonMode(GLenum face, GLenum mode) {
PolygonMode_State(face, mode); PolygonMode_State(face, mode);
} }
@@ -20,6 +20,16 @@ namespace MobileGL::MG_Impl::GLImpl {
void Enablei(GLenum target, GLuint index); void Enablei(GLenum target, GLuint index);
void BlendFunc(GLenum sfactor, GLenum dfactor); void BlendFunc(GLenum sfactor, GLenum dfactor);
void Viewport(GLint x, GLint y, GLsizei width, GLsizei height); void Viewport(GLint x, GLint y, GLsizei width, GLsizei height);
// ARB_viewport_array (core since GL 4.1). Every one of these addresses the same 16-element
// indexed state the classic glViewport/glScissor/glDepthRange trio broadcasts to.
void ViewportArrayv(GLuint first, GLsizei count, const GLfloat* v);
void ViewportIndexedf(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h);
void ViewportIndexedfv(GLuint index, const GLfloat* v);
void ScissorArrayv(GLuint first, GLsizei count, const GLint* v);
void ScissorIndexed(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height);
void ScissorIndexedv(GLuint index, const GLint* v);
void DepthRangeArrayv(GLuint first, GLsizei count, const GLdouble* v);
void DepthRangeIndexed(GLuint index, GLdouble n, GLdouble f);
void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass); void StencilOpSeparate(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass);
void StencilOp(GLenum fail, GLenum zfail, GLenum zpass); void StencilOp(GLenum fail, GLenum zfail, GLenum zpass);
void StencilMaskSeparate(GLenum face, GLuint mask); void StencilMaskSeparate(GLenum face, GLuint mask);
@@ -28,7 +38,10 @@ namespace MobileGL::MG_Impl::GLImpl {
void StencilFunc(GLenum func, GLint ref, GLuint mask); void StencilFunc(GLenum func, GLint ref, GLuint mask);
void Scissor(GLint x, GLint y, GLsizei width, GLsizei height); void Scissor(GLint x, GLint y, GLsizei width, GLsizei height);
void SampleCoverage(GLfloat value, GLboolean invert); void SampleCoverage(GLfloat value, GLboolean invert);
void MinSampleShading(GLfloat value);
void PolygonOffset(GLfloat factor, GLfloat units); void PolygonOffset(GLfloat factor, GLfloat units);
void PolygonOffsetClamp(GLfloat factor, GLfloat units, GLfloat clamp);
void ClipControl(GLenum origin, GLenum depth);
void PolygonMode(GLenum face, GLenum mode); void PolygonMode(GLenum face, GLenum mode);
void PointSize(GLfloat size); void PointSize(GLfloat size);
void PointParameterf(GLenum pname, GLfloat param); void PointParameterf(GLenum pname, GLfloat param);
+153 -48
View File
@@ -9,9 +9,11 @@
#include "GL_Sampler.h" #include "GL_Sampler.h"
#include "Validators.h" #include "Validators.h"
#include "../Getter/GL_Getter.h" #include "../Getter/GL_Getter.h"
#include "../Texture/GL_Texture.h"
#include <MG_State/GLState/Core.h> #include <MG_State/GLState/Core.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h> #include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h> #include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Math/FixedPointConversion.h>
namespace MobileGL::MG_Impl::GLImpl { namespace MobileGL::MG_Impl::GLImpl {
namespace { namespace {
@@ -21,6 +23,50 @@ namespace MobileGL::MG_Impl::GLImpl {
return static_cast<Float>(*(const GLint*)param); return static_cast<Float>(*(const GLint*)param);
} }
// GL_TEXTURE_BORDER_COLOR is the only sampler parameter with more than one component, and it
// is also the only one whose meaning depends on WHICH entry point wrote it. Everything else
// reads exactly one component and does not care.
Bool IsVectorOnlySamplerPname(GLenum pname) {
return pname == GL_TEXTURE_BORDER_COLOR;
}
// A state query returns the value CONVERTED to the type the caller asked for (GL 4.6 core
// 2.2.2 / 6.1), never the other type's bits. These two are the sampler side of the numeric
// casts GetTexParameterfv_State/GetTexParameteriv_State already do on the texture side; the
// sampler path funnels all three spellings through one void* function, which is precisely how
// it came to write a fixed type regardless of the caller.
//
// Truncation rather than rounding for the float -> integer direction, matching the texture
// twin (GetTexParameteriv_State's static_cast<GLint> on MIN_LOD/MAX_LOD/LOD_BIAS): the two
// spellings of the same state disagreeing is the bug being fixed here, and a texture and a
// sampler queried the same way must answer the same number.
void StoreSamplerScalar(void* params, Bool isFloat, Bool isUnsignedInteger, Float value) {
if (isFloat) {
*(GLfloat*)params = value;
return;
}
// Via GLint in both integer spellings: a direct float -> GLuint cast of a negative value
// (GL_TEXTURE_MIN_LOD defaults to -1000) is undefined behaviour, while the two-step
// conversion is the well-defined modular one, and it is what the texture-side
// GetTexParameterIuiv fallback does.
const GLint asInt = static_cast<GLint>(value);
if (isUnsignedInteger) {
*(GLuint*)params = static_cast<GLuint>(asInt);
} else {
*(GLint*)params = asInt;
}
}
void StoreSamplerEnum(void* params, Bool isFloat, Bool isUnsignedInteger, GLenum value) {
if (isFloat) {
*(GLfloat*)params = static_cast<GLfloat>(value);
} else if (isUnsignedInteger) {
*(GLuint*)params = value;
} else {
*(GLint*)params = static_cast<GLint>(value);
}
}
Bool ValidateSamplerParameterValue(GLenum pname, const void* param, Bool isFloat, Bool isUnsignedInteger) { Bool ValidateSamplerParameterValue(GLenum pname, const void* param, Bool isFloat, Bool isUnsignedInteger) {
if (param == nullptr) return false; if (param == nullptr) return false;
@@ -55,8 +101,15 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
} // namespace } // namespace
// `isIntegerCommand` distinguishes the "I" spellings (glSamplerParameterIiv / Iuiv) from the
// plain ones. It only matters for GL_TEXTURE_BORDER_COLOR, and there it decides everything:
// GL 4.6 core 8.10 says the I forms store the components unmodified with an integer internal
// type, while glSamplerParameteriv converts them to floating point with equation 2.2. Routing
// both to the same setter - which is what this file used to do - meant glSamplerParameteriv
// stored raw integers (so a border of 255 became float 255.0 instead of the spec's ~1.19e-7)
// and glSamplerParameterIiv lost the fact that it was ever an integer at all.
void SetSamplerParam_State(GLuint sampler, GLenum pname, const void* param, bool isFloat, void SetSamplerParam_State(GLuint sampler, GLenum pname, const void* param, bool isFloat,
bool isUnsignedInteger) { bool isUnsignedInteger, bool isIntegerCommand) {
if (param == nullptr) return; if (param == nullptr) return;
if (!SamplerImpl::ValidateSamplerName(sampler)) return; if (!SamplerImpl::ValidateSamplerName(sampler)) return;
@@ -111,6 +164,13 @@ namespace MobileGL::MG_Impl::GLImpl {
if (isFloat) { if (isFloat) {
const auto* values = (const GLfloat*)param; const auto* values = (const GLfloat*)param;
samplerObj->SetBorderColor(FloatVec4(values[0], values[1], values[2], values[3])); samplerObj->SetBorderColor(FloatVec4(values[0], values[1], values[2], values[3]));
} else if (!isIntegerCommand) {
// glSamplerParameteriv: GL 4.6 core equation 2.2 into the FLOAT border colour.
const auto* values = (const GLint*)param;
samplerObj->SetBorderColor(FloatVec4(MG_Util::SignedNormalizedInt32ToFloat(values[0]),
MG_Util::SignedNormalizedInt32ToFloat(values[1]),
MG_Util::SignedNormalizedInt32ToFloat(values[2]),
MG_Util::SignedNormalizedInt32ToFloat(values[3])));
} else if (isUnsignedInteger) { } else if (isUnsignedInteger) {
const auto* values = (const GLuint*)param; const auto* values = (const GLuint*)param;
samplerObj->SetBorderColorUI(UintVec4(values[0], values[1], values[2], values[3])); samplerObj->SetBorderColorUI(UintVec4(values[0], values[1], values[2], values[3]));
@@ -127,7 +187,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void GetSamplerParam_State(GLuint sampler, GLenum pname, void* params, bool isFloat, void GetSamplerParam_State(GLuint sampler, GLenum pname, void* params, bool isFloat,
bool isUnsignedInteger) { bool isUnsignedInteger, bool isIntegerCommand) {
if (params == nullptr) return; if (params == nullptr) return;
if (!SamplerImpl::ValidateSamplerName(sampler)) return; if (!SamplerImpl::ValidateSamplerName(sampler)) return;
@@ -140,47 +200,56 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!SamplerImpl::ValidateSamplerObject(sampler)) return; if (!SamplerImpl::ValidateSamplerObject(sampler)) return;
using namespace MG_Util; using namespace MG_Util;
// Every scalar pname goes through StoreSamplerScalar/StoreSamplerEnum so the CALLER'S form
// decides the destination type. Writing a fixed type regardless - which is what these case
// labels used to do - hands back the other type's bit pattern rather than a converted value:
// glGetSamplerParameterfv(GL_TEXTURE_WRAP_S) deposited the integer 10497 into a GLfloat and
// the caller read 1.47e-41, and glGetSamplerParameteriv(GL_TEXTURE_MIN_LOD) deposited the
// IEEE bits of -1000.0f and the caller read -998637568. Sixteen (pname, entry-point) pairs
// were broken this way; only MAX_ANISOTROPY_EXT and BORDER_COLOR branched correctly, which is
// how the same bug class was already found and fixed once for a single pname.
switch (pname) { switch (pname) {
case GL_TEXTURE_WRAP_S: case GL_TEXTURE_WRAP_S:
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapS()); StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapS()));
break; break;
case GL_TEXTURE_WRAP_T: case GL_TEXTURE_WRAP_T:
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapT()); StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapT()));
break; break;
case GL_TEXTURE_WRAP_R: case GL_TEXTURE_WRAP_R:
*(GLuint*)params = MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapR()); StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerWrapModeToGLEnum(samplerObj->GetWrapR()));
break; break;
case GL_TEXTURE_MIN_FILTER: case GL_TEXTURE_MIN_FILTER:
*(GLuint*)params = StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMinFilter(), samplerObj->GetMipmapMode()); MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMinFilter(),
samplerObj->GetMipmapMode()));
break; break;
case GL_TEXTURE_MAG_FILTER: case GL_TEXTURE_MAG_FILTER:
*(GLuint*)params = StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMagFilter(), SamplerMipmapMode::None); MG_Util::ConvertSamplerFilterModeToGLEnum(samplerObj->GetMagFilter(),
SamplerMipmapMode::None));
break; break;
case GL_TEXTURE_MIN_LOD: case GL_TEXTURE_MIN_LOD:
*(GLfloat*)params = samplerObj->GetMinLod(); StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetMinLod());
break; break;
case GL_TEXTURE_MAX_LOD: case GL_TEXTURE_MAX_LOD:
*(GLfloat*)params = samplerObj->GetMaxLod(); StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetMaxLod());
break; break;
case GL_TEXTURE_LOD_BIAS: case GL_TEXTURE_LOD_BIAS:
*(GLfloat*)params = samplerObj->GetLodBias(); StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetLodBias());
break; break;
case GL_TEXTURE_MAX_ANISOTROPY_EXT: case GL_TEXTURE_MAX_ANISOTROPY_EXT:
if (isFloat) { StoreSamplerScalar(params, isFloat, isUnsignedInteger, samplerObj->GetMaxAnisotropy());
*(GLfloat*)params = samplerObj->GetMaxAnisotropy();
} else if (isUnsignedInteger) {
*(GLuint*)params = static_cast<GLuint>(samplerObj->GetMaxAnisotropy());
} else {
*(GLint*)params = static_cast<GLint>(samplerObj->GetMaxAnisotropy());
}
break; break;
case GL_TEXTURE_COMPARE_MODE: case GL_TEXTURE_COMPARE_MODE:
*(GLuint*)params = MG_Util::ConvertSamplerCompareModeToGLEnum(samplerObj->GetCompareMode()); StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerCompareModeToGLEnum(samplerObj->GetCompareMode()));
break; break;
case GL_TEXTURE_COMPARE_FUNC: case GL_TEXTURE_COMPARE_FUNC:
*(GLuint*)params = MG_Util::ConvertSamplerCompareFuncToGLEnum(samplerObj->GetSamplerCompareFunc()); StoreSamplerEnum(params, isFloat, isUnsignedInteger,
MG_Util::ConvertSamplerCompareFuncToGLEnum(samplerObj->GetSamplerCompareFunc()));
break; break;
case GL_TEXTURE_BORDER_COLOR: { case GL_TEXTURE_BORDER_COLOR: {
if (isFloat) { if (isFloat) {
@@ -190,6 +259,16 @@ namespace MobileGL::MG_Impl::GLImpl {
out[1] = color.y(); out[1] = color.y();
out[2] = color.z(); out[2] = color.z();
out[3] = color.w(); out[3] = color.w();
} else if (!isIntegerCommand) {
// glGetSamplerParameteriv: the inverse of the write side, GL 4.6 core equation 2.3.
// Exactly inverse, so a {0,1,2,4} written with glSamplerParameteriv reads back as
// {0,1,2,4}; a bare truncating cast answered {0,0,0,0}.
const auto& color = samplerObj->GetBorderColor();
auto* out = (GLint*)params;
out[0] = MG_Util::FloatToSignedNormalizedInt32(color.x());
out[1] = MG_Util::FloatToSignedNormalizedInt32(color.y());
out[2] = MG_Util::FloatToSignedNormalizedInt32(color.z());
out[3] = MG_Util::FloatToSignedNormalizedInt32(color.w());
} else if (isUnsignedInteger) { } else if (isUnsignedInteger) {
const auto& color = samplerObj->GetBorderColorUI(); const auto& color = samplerObj->GetBorderColorUI();
auto* out = (GLuint*)params; auto* out = (GLuint*)params;
@@ -269,15 +348,13 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
} }
// The number of texture units a sampler may be bound to. GL 3.3 core 3.8.2 names // The number of texture units a sampler may be bound to is the same count a TEXTURE may be
// GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, which is what the backend advertises; the frontend's // bound to - GL 3.3 core 3.8.2 names GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS for both - so it is
// MAX_TEXTURE_IMAGE_UNITS is only the capacity of the unit array, so it is a clamp on the // computed once, in GetCombinedTextureImageUnitCount, and named here for the sampler-side
// answer and never the answer itself - gating on it alone accepts every unit up to 192 no // readers below. Two copies of that arithmetic is how glBindSamplers and glBindTextures would
// matter what the driver reports. // come to disagree about which units exist.
static GLint GetSamplerBindableTextureUnitCount() { static GLint GetSamplerBindableTextureUnitCount() {
GLint maxTextureUnits = 0; return GetCombinedTextureImageUnitCount();
GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxTextureUnits);
return std::min<GLint>(std::max(maxTextureUnits, 0), MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
} }
void BindSampler_State(GLuint unit, GLuint sampler) { void BindSampler_State(GLuint unit, GLuint sampler) {
@@ -294,16 +371,10 @@ namespace MobileGL::MG_Impl::GLImpl {
if (sampler == 0) { if (sampler == 0) {
textureUnit.SetSamplerObject(nullptr); textureUnit.SetSamplerObject(nullptr);
} else { } else {
// GL 3.3 core 3.8.2: BindSampler on a name GenSamplers never returned - or one already // GL 4.6 core 8.2: BindSampler on a name GenSamplers never returned - or one already
// deleted - is INVALID_OPERATION. SamplerParameter* raises INVALID_VALUE for the same // deleted - is INVALID_OPERATION, and so is every other sampler entry point on such a
// name, which is why this cannot go through the shared SamplerImpl validator. // name, so the shared validator answers for all of them.
if (!MG_State::pGLContext->ValidateSamplerName(sampler)) { if (!SamplerImpl::ValidateSamplerName(sampler)) return;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSampler_State",
std::format("Invalid sampler name {}", sampler)));
return;
}
Bool doesSamplerObjectCreated = MG_State::pGLContext->ValidateSamplerObject(sampler); Bool doesSamplerObjectCreated = MG_State::pGLContext->ValidateSamplerObject(sampler);
if (!doesSamplerObjectCreated) { if (!doesSamplerObjectCreated) {
MG_State::pGLContext->CreateSamplerObject(sampler); MG_State::pGLContext->CreateSamplerObject(sampler);
@@ -336,37 +407,71 @@ namespace MobileGL::MG_Impl::GLImpl {
return; return;
} }
// ARB_multi_bind adds one rule the single-bind path does not have: "samplers will not be
// created if they do not exist", so a name that is not an existing sampler OBJECT is
// INVALID_OPERATION here (KHR-GL44.multi_bind.errors_bind_samplers). Per element, not
// all-or-nothing - the extension defines glBindSamplers as a loop, so a bad entry costs
// its own texture unit and leaves the rest of the range bound.
for (GLsizei i = 0; i < count; ++i) { for (GLsizei i = 0; i < count; ++i) {
BindSampler_State(first + i, samplers ? samplers[i] : 0); const GLuint sampler = samplers ? samplers[i] : 0;
if (sampler != 0 && !MG_State::pGLContext->ValidateSamplerObject(sampler)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "BindSamplers",
std::format("samplers[{}] ({}) is not the name of an existing sampler object.", i, sampler)));
continue;
}
BindSampler_State(first + i, sampler);
} }
} }
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */ /* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
void GetSamplerParameteriv(GLuint sampler, GLenum pname, GLint* params) { void GetSamplerParameteriv(GLuint sampler, GLenum pname, GLint* params) {
GetSamplerParam_State(sampler, pname, params, false, false); GetSamplerParam_State(sampler, pname, params, false, false, false);
} }
void SamplerParameterIuiv(GLuint sampler, GLenum pname, const GLuint* param) { void SamplerParameterIuiv(GLuint sampler, GLenum pname, const GLuint* param) {
SetSamplerParam_State(sampler, pname, param, false, true); SetSamplerParam_State(sampler, pname, param, false, true, true);
} }
void SamplerParameterIiv(GLuint sampler, GLenum pname, const GLint* param) { void SamplerParameterIiv(GLuint sampler, GLenum pname, const GLint* param) {
SetSamplerParam_State(sampler, pname, param, false, false); SetSamplerParam_State(sampler, pname, param, false, false, true);
} }
void SamplerParameteriv(GLuint sampler, GLenum pname, const GLint* param) { void SamplerParameteriv(GLuint sampler, GLenum pname, const GLint* param) {
SetSamplerParam_State(sampler, pname, param, false, false); SetSamplerParam_State(sampler, pname, param, false, false, false);
} }
void SamplerParameterfv(GLuint sampler, GLenum pname, const GLfloat* param) { void SamplerParameterfv(GLuint sampler, GLenum pname, const GLfloat* param) {
SetSamplerParam_State(sampler, pname, param, true, false); SetSamplerParam_State(sampler, pname, param, true, false, false);
} }
// GL 4.6 core 8.10: the scalar spellings take "the value of pname", so a pname with more than one
// component is INVALID_ENUM here rather than something to read four components of. Guarding at
// the entry point rather than downstream is also what stops the vector path reading twelve bytes
// past the caller's single stack scalar - taking the address of a by-value argument and handing
// it to a four-component reader is what these used to do. The texture-side twins already answer
// INVALID_ENUM for GL_TEXTURE_BORDER_COLOR (TexParameteri/f name it as unsupported outright).
void SamplerParameteri(GLuint sampler, GLenum pname, GLint param) { void SamplerParameteri(GLuint sampler, GLenum pname, GLint param) {
if (IsVectorOnlySamplerPname(pname)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "SamplerParameteri",
"pname has more than one component and needs a vector form."));
return;
}
SamplerParameteriv(sampler, pname, &param); SamplerParameteriv(sampler, pname, &param);
} }
void SamplerParameterf(GLuint sampler, GLenum pname, GLfloat param) { void SamplerParameterf(GLuint sampler, GLenum pname, GLfloat param) {
if (IsVectorOnlySamplerPname(pname)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "SamplerParameterf",
"pname has more than one component and needs a vector form."));
return;
}
SamplerParameterfv(sampler, pname, &param); SamplerParameterfv(sampler, pname, &param);
} }
@@ -375,15 +480,15 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void GetSamplerParameterIuiv(GLuint sampler, GLenum pname, GLuint* params) { void GetSamplerParameterIuiv(GLuint sampler, GLenum pname, GLuint* params) {
GetSamplerParam_State(sampler, pname, params, false, true); GetSamplerParam_State(sampler, pname, params, false, true, true);
} }
void GetSamplerParameterIiv(GLuint sampler, GLenum pname, GLint* params) { void GetSamplerParameterIiv(GLuint sampler, GLenum pname, GLint* params) {
GetSamplerParam_State(sampler, pname, params, false, false); GetSamplerParam_State(sampler, pname, params, false, false, true);
} }
void GetSamplerParameterfv(GLuint sampler, GLenum pname, GLfloat* params) { void GetSamplerParameterfv(GLuint sampler, GLenum pname, GLfloat* params) {
GetSamplerParam_State(sampler, pname, params, true, false); GetSamplerParam_State(sampler, pname, params, true, false, false);
} }
void GenSamplers(GLsizei count, GLuint* samplers) { void GenSamplers(GLsizei count, GLuint* samplers) {
@@ -12,11 +12,17 @@
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h> #include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
namespace MobileGL::MG_Impl::GLImpl::SamplerImpl { namespace MobileGL::MG_Impl::GLImpl::SamplerImpl {
// GL 4.6 core 8.2: "An INVALID_OPERATION error is generated if sampler is not the name of a
// sampler object previously returned from a call to GenSamplers." That class is shared by every
// sampler entry point - BindSampler, SamplerParameter*, GetSamplerParameter* - so this one gate
// answers for all of them. It used to report INVALID_VALUE (the GL 3.3 wording), which forced
// BindSampler to carry a bespoke duplicate of the same check just to get the class right.
Bool ValidateSamplerName(GLuint sampler) { Bool ValidateSamplerName(GLuint sampler) {
if (!MG_State::pGLContext->ValidateSamplerName(sampler)) { if (!MG_State::pGLContext->ValidateSamplerName(sampler)) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerName", ErrorCode::InvalidOperation,
std::format("Invalid sampler name {}", sampler))); MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerName",
std::format("Invalid sampler name {}", sampler)));
return false; return false;
} }
return true; return true;
+79 -1
View File
@@ -8,6 +8,8 @@
#include "GL_Sync.h" #include "GL_Sync.h"
#include <MG_Backend/BackendObjects.h> #include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/Core.h>
#include <MG_Impl/Pipe/PipeFill.h>
namespace MobileGL::MG_Impl::GLImpl { namespace MobileGL::MG_Impl::GLImpl {
namespace { namespace {
@@ -35,10 +37,27 @@ namespace MobileGL::MG_Impl::GLImpl {
} // namespace } // namespace
GLsync FenceSync(GLenum condition, GLbitfield flags) { GLsync FenceSync(GLenum condition, GLbitfield flags) {
// GL 4.6 core 4.1.2: GL_SYNC_GPU_COMMANDS_COMPLETE is the only condition and the only
// legal flags value is zero; both violations return 0 rather than a handle. A caller that
// then hands the 0 back to glDeleteSync hits the glDeleteSync(0) no-op below.
if (condition != GL_SYNC_GPU_COMMANDS_COMPLETE) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"condition must be GL_SYNC_GPU_COMMANDS_COMPLETE."));
return nullptr;
}
if (flags != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "flags must be zero."));
return nullptr;
}
auto* syncObject = new SyncObject; auto* syncObject = new SyncObject;
syncObject->condition = condition; syncObject->condition = condition;
syncObject->flags = flags; syncObject->flags = flags;
if (const auto backendFenceSync = MG_Backend::gBackendFunctionsTable.GL.FenceSync) { if (const auto backendFenceSync = MG_Backend::gBackendFunctionsTable.GL.FenceSync) {
MGP_FILL(FenceSync);
syncObject->backendHandle = backendFenceSync(); syncObject->backendHandle = backendFenceSync();
} }
const GLsync handle = reinterpret_cast<GLsync>(syncObject); const GLsync handle = reinterpret_cast<GLsync>(syncObject);
@@ -52,24 +71,58 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
GLenum ClientWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) { GLenum ClientWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
// GL 4.6 core 4.1.1: GL_SYNC_FLUSH_COMMANDS_BIT is the only bit this call accepts, and
// any other bit is INVALID_VALUE. Silently ignoring the stray bits used to make a caller
// that passed, say, GL_SYNC_GPU_COMMANDS_COMPLETE by mistake think it had asked for a
// flush it never got.
if ((flags & ~static_cast<GLbitfield>(GL_SYNC_FLUSH_COMMANDS_BIT)) != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"flags must be zero or GL_SYNC_FLUSH_COMMANDS_BIT."));
return GL_WAIT_FAILED;
}
const auto* syncObject = FindSyncObject(sync); const auto* syncObject = FindSyncObject(sync);
if (!syncObject) { if (!syncObject) {
// The spec pairs the GL_WAIT_FAILED return with a recorded INVALID_VALUE; returning
// the enum alone left glGetError() clean and the failure indistinguishable from a
// genuine wait failure on a live sync.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "sync is not the name of a sync object."));
return GL_WAIT_FAILED; return GL_WAIT_FAILED;
} }
const auto backendClientWaitSync = MG_Backend::gBackendFunctionsTable.GL.ClientWaitSync; const auto backendClientWaitSync = MG_Backend::gBackendFunctionsTable.GL.ClientWaitSync;
if (!backendClientWaitSync || !syncObject->backendHandle) { if (!backendClientWaitSync || !syncObject->backendHandle) {
return GL_ALREADY_SIGNALED; // legacy always-signaled fallback return GL_ALREADY_SIGNALED; // legacy always-signaled fallback
} }
MGP_FILL(ClientWaitSync);
return backendClientWaitSync(syncObject->backendHandle, flags, timeout); return backendClientWaitSync(syncObject->backendHandle, flags, timeout);
} }
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) { void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
// GL 4.6 core 4.1.2: the server-side wait takes no flags and no finite timeout - both
// arguments exist only to be forward-compatible, and anything else is INVALID_VALUE.
// Neither backend ever honored a nonzero timeout (DirectGLES hard-codes
// 0/GL_TIMEOUT_IGNORED, DirectVulkan's queue ordering makes the wait implicit), so
// rejecting the call loses no wait that used to happen.
if (flags != 0 || timeout != GL_TIMEOUT_IGNORED) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"flags must be zero and timeout must be GL_TIMEOUT_IGNORED."));
return;
}
const auto* syncObject = FindSyncObject(sync); const auto* syncObject = FindSyncObject(sync);
if (!syncObject) { if (!syncObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "sync is not the name of a sync object."));
return; return;
} }
const auto backendWaitSync = MG_Backend::gBackendFunctionsTable.GL.WaitSync; const auto backendWaitSync = MG_Backend::gBackendFunctionsTable.GL.WaitSync;
if (backendWaitSync && syncObject->backendHandle) { if (backendWaitSync && syncObject->backendHandle) {
MGP_FILL(WaitSync);
backendWaitSync(syncObject->backendHandle, flags, timeout); backendWaitSync(syncObject->backendHandle, flags, timeout);
} }
} }
@@ -90,14 +143,29 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync; const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
if (backendDeleteSync && syncObject->backendHandle) { if (backendDeleteSync && syncObject->backendHandle) {
MGP_FILL(DeleteSync);
backendDeleteSync(syncObject->backendHandle); backendDeleteSync(syncObject->backendHandle);
} }
delete syncObject; delete syncObject;
} }
void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values) { void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values) {
// GL 4.6 core 4.1: a negative bufSize is INVALID_VALUE, an unnamed sync is INVALID_VALUE
// and an unrecognised pname is INVALID_ENUM. All three used to leave glGetError() clean
// and write a plausible-looking zero, which is the one failure mode a caller cannot tell
// apart from a real answer - GL_SYNC_STATUS legitimately answers GL_UNSIGNALED (0x9118),
// but a mistyped pname answered a bare 0 that no query ever returns.
if (bufSize < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufSize must not be negative."));
return;
}
const auto* syncObject = FindSyncObject(sync); const auto* syncObject = FindSyncObject(sync);
if (!syncObject) { if (!syncObject) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "sync is not the name of a sync object."));
if (length) { if (length) {
*length = 0; *length = 0;
} }
@@ -111,6 +179,7 @@ namespace MobileGL::MG_Impl::GLImpl {
break; break;
case GL_SYNC_STATUS: { case GL_SYNC_STATUS: {
const auto backendGetSyncStatus = MG_Backend::gBackendFunctionsTable.GL.GetSyncStatus; const auto backendGetSyncStatus = MG_Backend::gBackendFunctionsTable.GL.GetSyncStatus;
MGP_FILL(GetSyncStatus);
const Bool signaled = !backendGetSyncStatus || !syncObject->backendHandle || const Bool signaled = !backendGetSyncStatus || !syncObject->backendHandle ||
backendGetSyncStatus(syncObject->backendHandle); backendGetSyncStatus(syncObject->backendHandle);
value = signaled ? GL_SIGNALED : GL_UNSIGNALED; value = signaled ? GL_SIGNALED : GL_UNSIGNALED;
@@ -123,7 +192,15 @@ namespace MobileGL::MG_Impl::GLImpl {
value = static_cast<GLint>(syncObject->flags); value = static_cast<GLint>(syncObject->flags);
break; break;
default: default:
break; MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"pname must be GL_OBJECT_TYPE, GL_SYNC_STATUS, GL_SYNC_CONDITION or "
"GL_SYNC_FLAGS."));
if (length) {
*length = 0;
}
return;
} }
if (length) { if (length) {
@@ -156,6 +233,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync; const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
for (const auto& [_, syncObject] : orphans) { for (const auto& [_, syncObject] : orphans) {
if (backendDeleteSync && syncObject->backendHandle) { if (backendDeleteSync && syncObject->backendHandle) {
MGP_FILL(DeleteSync);
backendDeleteSync(syncObject->backendHandle); backendDeleteSync(syncObject->backendHandle);
} }
delete syncObject; delete syncObject;
File diff suppressed because it is too large Load Diff
@@ -8,9 +8,24 @@
#pragma once #pragma once
#include <Includes.h> #include <Includes.h>
#include <MG_State/GLState/TextureState/TextureObject.h>
namespace MobileGL::MG_Impl::GLImpl { namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */ /* @INSERTION_POINT:FUNCTION_DECLARATION@ */
// Answers a texture-image query straight out of the CPU shadow, into client memory or a bound
// PIXEL_PACK_BUFFER. This is the whole of glGetTexImage on a build with no backend readback, and
// it is also the sound fallback for a backend that has no GPU image to read: with no image,
// nothing GPU-side can ever have written the texture, so the shadow IS its content.
//
// It answers a NARROWER contract than glGetTexImage's, and refuses what it cannot do rather than
// answering wrongly. The copy is verbatim: it performs no format or type conversion, and it packs
// rows tightly, honouring only GL_PACK_SWAP_BYTES and the bitmap GL_PACK_LSB_FIRST path. A
// request whose (format, type) texel size differs from the texture's own, or a pixel-store state
// that adds row padding / a row-length override / a skip offset, is rejected with
// GL_INVALID_OPERATION (see ValidateShadowReadbackLayout, which spells out why each is unsafe).
void CopyTextureImageToClientOrPBO_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
TextureUploadTarget textureUploadTarget, GLint level, GLenum format,
GLenum type, GLsizei bufSize, void* pixels, const char* caller);
// The sized internal formats a buffer texture accepts (GL 4.6 core table 8.16). The buffer // The sized internal formats a buffer texture accepts (GL 4.6 core table 8.16). The buffer
// clears take the same list, so it is shared rather than written out twice. // clears take the same list, so it is shared rather than written out twice.
Bool IsBufferTextureInternalFormat(GLenum internalformat); Bool IsBufferTextureInternalFormat(GLenum internalformat);
@@ -37,6 +52,13 @@ namespace MobileGL::MG_Impl::GLImpl {
GLenum format, GLenum type, const void* pixels); GLenum format, GLenum type, const void* pixels);
void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels); GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels);
void CompressedTextureSubImage1D(GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format,
GLsizei imageSize, const void* data);
void CompressedTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
GLsizei height, GLenum format, GLsizei imageSize, const void* data);
void CompressedTextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize,
const void* data);
void TextureParameterf(GLuint texture, GLenum pname, GLfloat param); void TextureParameterf(GLuint texture, GLenum pname, GLfloat param);
void TextureParameterfv(GLuint texture, GLenum pname, const GLfloat* params); void TextureParameterfv(GLuint texture, GLenum pname, const GLfloat* params);
void TextureParameteri(GLuint texture, GLenum pname, GLint param); void TextureParameteri(GLuint texture, GLenum pname, GLint param);
@@ -58,6 +80,8 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetTextureParameteriv(GLuint texture, GLenum pname, GLint* params); void GetTextureParameteriv(GLuint texture, GLenum pname, GLint* params);
void GetTextureLevelParameterfv(GLuint texture, GLint level, GLenum pname, GLfloat* params); void GetTextureLevelParameterfv(GLuint texture, GLint level, GLenum pname, GLfloat* params);
void GetTextureLevelParameteriv(GLuint texture, GLint level, GLenum pname, GLint* params); void GetTextureLevelParameteriv(GLuint texture, GLint level, GLenum pname, GLint* params);
void TextureView(GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel,
GLuint numlevels, GLuint minlayer, GLuint numlayers);
void TexStorage1D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width); void TexStorage1D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width);
void TexStorage2D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height); void TexStorage2D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height);
void TexStorage3D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, void TexStorage3D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
@@ -132,5 +156,11 @@ namespace MobileGL::MG_Impl::GLImpl {
void CompressedTexImage1D(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, void CompressedTexImage1D(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border,
GLsizei imageSize, const void* data); GLsizei imageSize, const void* data);
void BindTexture(GLenum target, GLuint texture); void BindTexture(GLenum target, GLuint texture);
void BindTextures(GLuint first, GLsizei count, const GLuint* textures);
void BindImageTextures(GLuint first, GLsizei count, const GLuint* textures);
void ActiveTexture(GLenum texture); void ActiveTexture(GLenum texture);
// The number of texture image units a texture or a sampler may be bound to: what the backend
// advertises as GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, clamped by the frontend's fixed unit-array
// capacity. Shared so the texture and sampler multi-bind range checks cannot drift apart.
GLint GetCombinedTextureImageUnitCount();
} // namespace MobileGL::MG_Impl::GLImpl } // namespace MobileGL::MG_Impl::GLImpl
+358 -12
View File
@@ -15,6 +15,7 @@
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h> #include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h> #include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h> #include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
#include <MG_Util/Metrics/TextureMetrics.h>
namespace MobileGL::MG_Impl::GLImpl::TextureImpl { namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
Bool ValidateTextureTarget(TextureTarget target) { Bool ValidateTextureTarget(TextureTarget target) {
@@ -102,6 +103,28 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return true; return true;
} }
Bool ValidateCubeMapArrayShape(TextureUploadTarget target, GLsizei width, GLsizei height, GLsizei depth,
const char* caller) {
if (target != TextureUploadTarget::CubeMapArray && target != TextureUploadTarget::ProxyCubeMapArray) {
return true;
}
if (width != height) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Cube map array levels must be square (width == height)"));
return false;
}
if (depth % 6 != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Cube map array depth must be a multiple of six"));
return false;
}
return true;
}
Bool ValidateTextureSizeWithTextureUploadTarget(TextureUploadTarget target, GLsizei width, GLsizei height) { Bool ValidateTextureSizeWithTextureUploadTarget(TextureUploadTarget target, GLsizei width, GLsizei height) {
if (target == TextureUploadTarget::CubeMapPositiveX || target == TextureUploadTarget::CubeMapNegativeX || if (target == TextureUploadTarget::CubeMapPositiveX || target == TextureUploadTarget::CubeMapNegativeX ||
target == TextureUploadTarget::CubeMapPositiveY || target == TextureUploadTarget::CubeMapNegativeY || target == TextureUploadTarget::CubeMapPositiveY || target == TextureUploadTarget::CubeMapNegativeY ||
@@ -312,9 +335,13 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return false; return false;
} }
// TexImage in core 3.3 has no stencil-only upload path (that arrived with GL 4.4). // The stencil-only transfer path arrived with GL 4.4 / ARB_texture_stencil8, and only ever
if (format == TextureInputFormat::StencilIndex) { // pairs with stencil-only storage: against a depth, depth-stencil or colour internal format
return recordInvalidOperation("STENCIL_INDEX is not a valid texture upload format"); // STENCIL_INDEX keeps the pre-4.4 answer (GL CTS packed_pixels feeds exactly that pairing
// and expects INVALID_OPERATION).
if (format == TextureInputFormat::StencilIndex &&
internalFormat != TextureInternalFormat::StencilIndex8) {
return recordInvalidOperation("STENCIL_INDEX requires a stencil-only internal format");
} }
if (IsDepthLikeInputFormat(format) != IsDepthLikeInternalFormat(internalFormat)) { if (IsDepthLikeInputFormat(format) != IsDepthLikeInternalFormat(internalFormat)) {
@@ -353,6 +380,63 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return true; return true;
} }
Bool ValidateTextureLevelExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int level,
const char* caller) {
// A null object is somebody else's error to report - ValidateTextureObject runs
// first at every call site and has already recorded it.
if (!textureObject) return false;
const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
if (mipmapTexture == nullptr) {
// The only non-mipmap storage class is a buffer texture, and GL_TEXTURE_BUFFER is
// not a target glCopyImageSubData accepts at all (it is in the CTS's invalid-target
// set). Declining here is not the error code the spec asks for - that would be
// INVALID_ENUM from a target check this validator is not - but it does keep a
// texture with no image levels whatsoever from reaching a backend that would
// dereference a backend texture it never created.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Texture has no mipmap levels to address."));
return false;
}
// What this number is, exactly, because two other things are almost it and neither is
// safe to assume: it is the number of level SLOTS the shadow has allocated - holes
// included, since MipmapStorage::AllocateLevel grows to level+1 and never fills the gap.
// For a cube map MipmapUploadTargetArray reports face +X's chain rather than the union.
//
// The guarantee that matters is one-sided: this count is always >= the level count the
// backends derive (VkTextureManager::GetUploadMipLevelCount stops at the first level
// with a non-positive extent, so it can only be shorter). That is the safe direction -
// no copy to a level the texture genuinely has is ever rejected here. It is NOT an
// exact match, so the backends keep their own range guard for the band in between: a
// chain with a hole (level 0 and 2 defined, 1 not) is accepted by this predicate and
// declined by the backend, which is a silent no-op rather than a copy. That band is a
// backend storage limitation, not a validation one - rejecting it here with
// INVALID_VALUE would be refusing a copy the spec permits.
const Uint levelCount = mipmapTexture->GetMipmapLevelCount();
if (levelCount == 0) {
// No image has ever been defined on this texture, so the fault is the texture,
// not the number: GL 4.6 core 18.3.2 asks for INVALID_OPERATION when an object a
// copy names is an incomplete texture.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Texture has no image defined at any level."));
return false;
}
if (level < 0 || static_cast<Uint>(level) >= levelCount) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Texture level does not exist in this texture."));
return false;
}
return true;
}
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) { Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) {
if (!textureObject) { if (!textureObject) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
@@ -424,19 +508,281 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
return true; return true;
} }
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2) { namespace {
auto unsizedFormat1 = MG_Util::ConvertInternalFormatToUnsized(format1); // Component set of an UNSIZED base internal format, as the bitmask GL 4.6 SS 8.6
auto unsizedFormat2 = MG_Util::ConvertInternalFormatToUnsized(format2); // reasons about. Colour components are independent bits so "subset" is a plain
if (unsizedFormat1 != unsizedFormat2) { // mask test; depth and stencil are their own components and never satisfy a
// colour request (or each other).
enum : Uint32 {
kComponentR = 1u << 0,
kComponentG = 1u << 1,
kComponentB = 1u << 2,
kComponentA = 1u << 3,
kComponentDepth = 1u << 4,
kComponentStencil = 1u << 5,
};
Uint32 BaseFormatComponents(TextureInternalFormat unsizedFormat) {
switch (unsizedFormat) {
case TextureInternalFormat::Red:
return kComponentR;
case TextureInternalFormat::RG:
return kComponentR | kComponentG;
case TextureInternalFormat::RGB:
return kComponentR | kComponentG | kComponentB;
case TextureInternalFormat::RGBA:
return kComponentR | kComponentG | kComponentB | kComponentA;
case TextureInternalFormat::DepthComponent:
return kComponentDepth;
case TextureInternalFormat::DepthStencil:
return kComponentDepth | kComponentStencil;
default:
return 0;
}
}
} // namespace
CopyImageTexelBlock ResolveCopyImageTexelBlock(TextureInternalFormat format, GLenum compressedFormat) {
CopyImageTexelBlock block{};
if (compressedFormat != GL_NONE) {
const auto info = MG_Util::GetCompressedFormatInfo(compressedFormat);
if (info.blockByteSize != 0) {
block.byteSize = info.blockByteSize;
block.blockWidth = info.blockWidth;
block.blockHeight = info.blockHeight;
block.compressed = true;
return block;
}
}
// The size MobileGL actually stores a texel of this format in, which for every format GL
// gives a required size is that required size. The handful of legacy formats GL leaves
// implementation-defined (R3_G3_B2, RGB4/5/10/12, RGBA2/12) have no view class in table
// 8.22 to be compared against anyway, and this is the size that decides whether a raw
// copy between them would in fact preserve the bytes.
block.byteSize = MG_Util::GetSizedInternalFormatSizeInBytes(format);
return block;
}
Bool ValidateCopyImageFormatCompatibility(const CopyImageTexelBlock& srcBlock,
const CopyImageTexelBlock& dstBlock) {
if (srcBlock.byteSize == 0 || dstBlock.byteSize == 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
"A copied image has no storage whose texel size is known."));
return false;
}
if (srcBlock.byteSize != dstBlock.byteSize) {
MG_State::pGLContext->RecordError( MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation, ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>( MakeUnique<GenericErrorInfo>(
std::format("MG_Impl/GLImpl", "ValidateBaseInternalFormatMatch", "MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
"The base internal format of the two formats do not match ({} vs. {})", std::format("The two images' texel blocks are different sizes ({} vs. {} bytes), so the "
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat1).c_str(), "formats are not copy-compatible.",
MG_Util::ConvertTextureInternalFormatToString(unsizedFormat2).c_str()))); srcBlock.byteSize, dstBlock.byteSize)));
return false;
}
// Two compressed images additionally have to agree on the SHAPE of the block, not only
// its size: an 8-byte 4x4 block and a hypothetical 8-byte 8x8 one hold different texel
// counts, and GL 4.6 core 18.3.2 requires both dimensions to match.
if (srcBlock.compressed && dstBlock.compressed &&
(srcBlock.blockWidth != dstBlock.blockWidth || srcBlock.blockHeight != dstBlock.blockHeight)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "ValidateCopyImageFormatCompatibility",
std::format("The two compressed images have different block dimensions ({}x{} vs. {}x{}).",
srcBlock.blockWidth, srcBlock.blockHeight, dstBlock.blockWidth,
dstBlock.blockHeight)));
return false; return false;
} }
return true; return true;
} // namespace TextureImpl }
Bool ValidateCopyImageBlockAlignment(const CopyImageTexelBlock& block, Int x, Int y, Int width, Int height,
Int imageWidth, Int imageHeight, const char* endpointName) {
if (!block.compressed) return true;
const Int blockWidth = static_cast<Int>(block.blockWidth);
const Int blockHeight = static_cast<Int>(block.blockHeight);
if (blockWidth <= 1 && blockHeight <= 1) return true;
// The origin is unconditional; the extent gets the "or it reaches the edge of the image"
// exemption GL 4.6 core 18.3.2 grants, which is what lets a 16x16 BPTC image be copied
// whole even when the last block is partial.
const Bool originAligned = (x % blockWidth == 0) && (y % blockHeight == 0);
const Bool widthOk = (width % blockWidth == 0) || (x + width == imageWidth);
const Bool heightOk = (height % blockHeight == 0) || (y + height == imageHeight);
if (originAligned && widthOk && heightOk) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "ValidateCopyImageBlockAlignment",
std::format("The {} region [{}, {}] + [{} x {}] is not aligned to the {}x{} compressed block "
"grid of a {} x {} image.",
endpointName, x, y, width, height, blockWidth, blockHeight, imageWidth, imageHeight)));
return false;
}
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat) {
const auto unsizedDest = MG_Util::ConvertInternalFormatToUnsized(destFormat);
const auto unsizedSrc = MG_Util::ConvertInternalFormatToUnsized(srcFormat);
// GL 4.6 SS 8.6: glCopyTexImage* may request a SUBSET of the read buffer's components,
// not an exact match - GL_RGB from an RGBA8 framebuffer is textbook legal and is what
// Minecraft and its mods do. glCopyTexImage2D used to run the exact-match predicate
// above and turn its rejection into an uncaught exception through the C GL ABI, so the
// app died rather than seeing a GL error.
const Uint32 destComponents = BaseFormatComponents(unsizedDest);
const Uint32 srcComponents = BaseFormatComponents(unsizedSrc);
if (destComponents == 0 || srcComponents == 0 || (destComponents & ~srcComponents) != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "ValidateCopyTexImageBaseFormatSubset",
std::format("the read buffer's base internal format {} does not provide every component of "
"the requested internal format {}",
MG_Util::ConvertTextureInternalFormatToString(unsizedSrc),
MG_Util::ConvertTextureInternalFormatToString(unsizedDest))));
return false;
}
return true;
}
// GL 4.6 core table 8.21 ("Compatible internal formats for TextureView"), transcribed whole.
// Written against the raw GLenum rather than TextureInternalFormat on purpose: MobileGL's own
// enum collapses every compressed format onto uncompressed storage and drops formats it
// cannot carry, so classifying the converted value would silently widen the compatibility
// rule - GL_COMPRESSED_RG_RGTC2 and GL_RGBA8 would end up in the same class.
TextureViewClass GetTextureViewClass(GLenum internalformat) {
switch (internalformat) {
case GL_RGBA32F:
case GL_RGBA32UI:
case GL_RGBA32I:
return TextureViewClass::Bits128;
case GL_RGB32F:
case GL_RGB32UI:
case GL_RGB32I:
return TextureViewClass::Bits96;
case GL_RGBA16F:
case GL_RG32F:
case GL_RGBA16UI:
case GL_RG32UI:
case GL_RGBA16I:
case GL_RG32I:
case GL_RGBA16:
case GL_RGBA16_SNORM:
return TextureViewClass::Bits64;
case GL_RGB16:
case GL_RGB16_SNORM:
case GL_RGB16F:
case GL_RGB16UI:
case GL_RGB16I:
return TextureViewClass::Bits48;
case GL_RG16F:
case GL_R11F_G11F_B10F:
case GL_R32F:
case GL_RGB10_A2UI:
case GL_RGBA8UI:
case GL_RG16UI:
case GL_R32UI:
case GL_RGBA8I:
case GL_RG16I:
case GL_R32I:
case GL_RGB10_A2:
case GL_RGBA8:
case GL_RG16:
case GL_RGBA8_SNORM:
case GL_RG16_SNORM:
case GL_SRGB8_ALPHA8:
case GL_RGB9_E5:
return TextureViewClass::Bits32;
case GL_RGB8:
case GL_RGB8_SNORM:
case GL_SRGB8:
case GL_RGB8UI:
case GL_RGB8I:
return TextureViewClass::Bits24;
case GL_R16F:
case GL_RG8UI:
case GL_R16UI:
case GL_RG8I:
case GL_R16I:
case GL_RG8:
case GL_R16:
case GL_RG8_SNORM:
case GL_R16_SNORM:
return TextureViewClass::Bits16;
case GL_R8UI:
case GL_R8I:
case GL_R8:
case GL_R8_SNORM:
return TextureViewClass::Bits8;
case GL_COMPRESSED_RED_RGTC1:
case GL_COMPRESSED_SIGNED_RED_RGTC1:
return TextureViewClass::Rgtc1Red;
case GL_COMPRESSED_RG_RGTC2:
case GL_COMPRESSED_SIGNED_RG_RGTC2:
return TextureViewClass::Rgtc2Rg;
case GL_COMPRESSED_RGBA_BPTC_UNORM:
case GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM:
return TextureViewClass::BptcUnorm;
case GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT:
case GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT:
return TextureViewClass::BptcFloat;
default:
// Every depth/stencil format, every S3TC/ETC/ASTC format and every unsized format
// reaches here. The caller must then demand an EXACT format match.
return TextureViewClass::None;
}
}
// GL 4.6 core table 8.20 ("Legal texture targets for TextureView").
Bool IsLegalTextureViewTargetPair(TextureTarget origTarget, TextureTarget viewTarget) {
switch (origTarget) {
case TextureTarget::Texture1D:
return viewTarget == TextureTarget::Texture1D || viewTarget == TextureTarget::Texture1DArray;
case TextureTarget::Texture2D:
return viewTarget == TextureTarget::Texture2D || viewTarget == TextureTarget::Texture2DArray;
case TextureTarget::Texture3D:
return viewTarget == TextureTarget::Texture3D;
case TextureTarget::TextureCubeMap:
return viewTarget == TextureTarget::TextureCubeMap || viewTarget == TextureTarget::Texture2D ||
viewTarget == TextureTarget::Texture2DArray || viewTarget == TextureTarget::TextureCubeMapArray;
case TextureTarget::TextureRectangle:
return viewTarget == TextureTarget::TextureRectangle;
case TextureTarget::Texture1DArray:
return viewTarget == TextureTarget::Texture1DArray || viewTarget == TextureTarget::Texture1D;
case TextureTarget::Texture2DArray:
return viewTarget == TextureTarget::Texture2DArray || viewTarget == TextureTarget::Texture2D ||
viewTarget == TextureTarget::TextureCubeMap || viewTarget == TextureTarget::TextureCubeMapArray;
case TextureTarget::TextureCubeMapArray:
return viewTarget == TextureTarget::TextureCubeMapArray || viewTarget == TextureTarget::Texture2DArray ||
viewTarget == TextureTarget::Texture2D || viewTarget == TextureTarget::TextureCubeMap;
case TextureTarget::Texture2DMultisample:
case TextureTarget::Texture2DMultisampleArray:
return viewTarget == TextureTarget::Texture2DMultisample ||
viewTarget == TextureTarget::Texture2DMultisampleArray;
case TextureTarget::TextureBuffer:
// The table lists no legal target for a buffer texture: its storage is a buffer
// object, and there is nothing to make a view of.
return false;
default:
return false;
}
}
Uint RequiredTextureViewLayerCount(TextureTarget viewTarget) {
switch (viewTarget) {
case TextureTarget::TextureCubeMap:
return 6;
case TextureTarget::Texture1D:
case TextureTarget::Texture2D:
case TextureTarget::Texture3D:
case TextureTarget::TextureRectangle:
case TextureTarget::Texture2DMultisample:
return 1;
default:
// 1D/2D array, cube-map array, 2D multisample array: any count (the cube-map array's
// "multiple of 6" is checked by the caller).
return 0;
}
}
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl } // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
+75 -1
View File
@@ -20,6 +20,13 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
Bool ValidateTexturePixelDataType(TexturePixelDataType texturePixelDataType); Bool ValidateTexturePixelDataType(TexturePixelDataType texturePixelDataType);
Bool ValidateTextureLevelNumber(Int level); Bool ValidateTextureLevelNumber(Int level);
Bool ValidateTextureSizeWithTextureUploadTarget(TextureUploadTarget target, GLsizei width, GLsizei height); Bool ValidateTextureSizeWithTextureUploadTarget(TextureUploadTarget target, GLsizei width, GLsizei height);
// The two shape rules a cube-map-array level owes (GL 4.6 core 8.5): its faces are square, and
// its depth counts whole cubes. Both are GL_INVALID_VALUE. This used to be spelled inline in
// glTexStorage3D only, which is why glTexImage3D let both violations through - every entry
// point that DEFINES a cube-array level calls this now, so the two cannot drift again. A
// non-cube-array upload target answers true untouched.
Bool ValidateCubeMapArrayShape(TextureUploadTarget target, GLsizei width, GLsizei height, GLsizei depth,
const char* caller);
Bool ValidateTextureSizeRange(Int width, Int height, Int depth); Bool ValidateTextureSizeRange(Int width, Int height, Int depth);
Bool ValidateTextureInternalFormat(TextureInternalFormat format); Bool ValidateTextureInternalFormat(TextureInternalFormat format);
Bool ValidateTextureBorderNumber(Int border); Bool ValidateTextureBorderNumber(Int border);
@@ -30,6 +37,16 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
TextureInternalFormat internalFormat, TextureInternalFormat internalFormat,
TexturePixelDataType type); TexturePixelDataType type);
Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level); Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level);
// "Is <level> a level this texture actually has?", which ValidateTextureLevelNumber above
// does NOT answer - that one only bounds the index by GL_MAX_TEXTURE_SIZE and knows nothing
// about the object. Entry points that resolve a level straight into a backend image
// subresource need this one: a level the texture never had is GL_INVALID_VALUE (GL 4.6 core
// 18.3.2), and passing it through instead reaches the driver as an out-of-range subresource.
// Note the error split is per-entry-point, so this is not universally reusable:
// glClearTexImage owes INVALID_OPERATION for the same out-of-range level and spells its own
// copy of this predicate in GL_Texture.cpp (GetClearTextureObject).
Bool ValidateTextureLevelExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int level,
const char* caller);
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject); Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject);
// Rejects the per-target default texture objects (name 0) with GL_INVALID_OPERATION for entry // Rejects the per-target default texture objects (name 0) with GL_INVALID_OPERATION for entry
// points that require a GenTextures-created texture, e.g. TexStorage* ("An INVALID_OPERATION // points that require a GenTextures-created texture, e.g. TexStorage* ("An INVALID_OPERATION
@@ -40,5 +57,62 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
TextureTarget target); TextureTarget target);
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset, Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset,
Int width, Int yoffset = 0, Int height = 0, Int zoffset = 0, Int depth = 0); Int width, Int yoffset = 0, Int height = 0, Int zoffset = 0, Int depth = 0);
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2); // The texel block of one glCopyImageSubData endpoint, resolved to the two things the
// compatibility rule actually asks about. `compressed` is not redundant with a block bigger
// than 1x1: it is what distinguishes "compressed, and so the region is measured in texels of
// a blocked image" from "uncompressed, and so it is measured in texels".
struct CopyImageTexelBlock {
SizeT byteSize = 0;
Uint blockWidth = 1;
Uint blockHeight = 1;
Bool compressed = false;
};
// `compressedFormat` is the GLenum a glCompressedTexImage* upload recorded for the level, or
// GL_NONE. It has to be asked for separately because MobileGL stores every compressed format
// in uncompressed storage (ConvertGLEnumToTextureInternalFormat), so the TextureInternalFormat
// alone can no longer tell a BPTC image from the RGBA8 backing it.
CopyImageTexelBlock ResolveCopyImageTexelBlock(TextureInternalFormat format, GLenum compressedFormat);
// GL 4.6 core 18.3.2: the two images must be COMPATIBLE, and compatible means their texel
// blocks are the same SIZE - not that they share a base internal format. RGBA32UI into
// RGBA32F is legal (both 128-bit) while RGBA8 into RGBA32F is not, and a compressed image
// pairs with an uncompressed one whose texel is as big as the compressed block.
Bool ValidateCopyImageFormatCompatibility(const CopyImageTexelBlock& srcBlock,
const CopyImageTexelBlock& dstBlock);
// GL 4.6 core 18.3.2: for a compressed image the region's origin must sit on a block
// boundary and its size must be a whole number of blocks - unless the edge it runs to is
// the edge of the image.
Bool ValidateCopyImageBlockAlignment(const CopyImageTexelBlock& block, Int x, Int y, Int width, Int height,
Int imageWidth, Int imageHeight, const char* endpointName);
// GL 4.6 SS 8.6 subset rule for glCopyTexImage*: the read buffer must supply every component
// the requested internalformat asks for, but may supply more.
Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat);
// ---- glTextureView (ARB_texture_view / GL 4.6 core 8.18) ----
// Table 8.21's view classes. `None` is not a class - it means the format has NO entry in the
// table, which the spec turns into a much stricter rule than "same class": such a format can
// only ever be viewed as ITSELF. Every depth, stencil and depth/stencil format lands here,
// which is why the Better Clouds D24S8 view must name GL_DEPTH24_STENCIL8 exactly.
enum class TextureViewClass {
None = 0,
Bits128,
Bits96,
Bits64,
Bits48,
Bits32,
Bits24,
Bits16,
Bits8,
Rgtc1Red,
Rgtc2Rg,
BptcUnorm,
BptcFloat,
};
TextureViewClass GetTextureViewClass(GLenum internalformat);
// Table 8.20: which <target> values glTextureView accepts for a given origtexture target.
Bool IsLegalTextureViewTargetPair(TextureTarget origTarget, TextureTarget viewTarget);
// Table 8.20 again, read the other way: how many layers <target> requires. Returns 0 for the
// targets whose layer count is unconstrained (the array targets), 6 for GL_TEXTURE_CUBE_MAP,
// and 1 for every single-layer target. GL_TEXTURE_CUBE_MAP_ARRAY is special-cased by the
// caller because its constraint is "a multiple of 6", not an exact count.
Uint RequiredTextureViewLayerCount(TextureTarget viewTarget);
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl } // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
@@ -179,6 +179,28 @@ namespace MobileGL::MG_Impl::GLImpl {
return vao; return vao;
} }
// The ARB_vertex_attrib_binding entry points that take no vertex array name modify the
// *bound* vertex array, and in a core profile the default vertex array (name 0) is not
// one: every one of them is INVALID_OPERATION there (GL 4.6 core 10.3.1, and the tail of
// each KHR-GL4x.vertex_attrib_binding.negative-* case checks exactly this). MobileGL
// keeps a real object at name 0 for the compatibility paths, so GetBoundVertexArray
// never returns null and the rule has to be spelled out - behind the same gate the VAO-0
// draw rule already uses (MOBILEGL_RELAXED_SEMANTICS, plus "the context never asked for
// a core profile"), so applications that legitimately run relaxed keep working.
static SharedPtr<MG_State::GLState::VertexArrayObject> GetBoundVertexArrayForBindingApi(const char* funcName) {
auto vao = GetBoundVertexArrayOrError(funcName);
if (!vao) return nullptr;
if (vao->GetExternalIndex() == 0 && !MG_State::IsRelaxedSemanticsActive()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", funcName,
"The default vertex array object cannot be modified in a core profile."));
return nullptr;
}
return vao;
}
static bool ValidateVertexAttribPname(GLenum pname) { static bool ValidateVertexAttribPname(GLenum pname) {
switch (pname) { switch (pname) {
case GL_VERTEX_ATTRIB_ARRAY_ENABLED: case GL_VERTEX_ATTRIB_ARRAY_ENABLED:
@@ -293,9 +315,10 @@ namespace MobileGL::MG_Impl::GLImpl {
auto offset = reinterpret_cast<SizeT>(pointer); auto offset = reinterpret_cast<SizeT>(pointer);
vao->SetAttributeFormat(index, size, dataType, false, stride, offset, true, false); const int effectiveStride = EffectiveVertexStride(stride, size, type);
vao->SetAttributeFormat(index, size, dataType, false, stride, offset, true, false, effectiveStride);
vao->BindAttributeBuffer(index, vbo); vao->BindAttributeBuffer(index, vbo);
vao->MirrorPointerIntoBinding(index, vbo, offset, EffectiveVertexStride(stride, size, type)); vao->MirrorPointerIntoBinding(index, vbo, offset, effectiveStride);
} }
void VertexAttribPointer_State(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, void VertexAttribPointer_State(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride,
@@ -323,9 +346,11 @@ namespace MobileGL::MG_Impl::GLImpl {
// backend can pick the reversed VkFormat / pass GL_BGRA through to a GLES driver. // backend can pick the reversed VkFormat / pass GL_BGRA through to a GLES driver.
const bool isBgra = (size == static_cast<GLint>(GL_BGRA)); const bool isBgra = (size == static_cast<GLint>(GL_BGRA));
const int effectiveSize = isBgra ? 4 : size; const int effectiveSize = isBgra ? 4 : size;
vao->SetAttributeFormat(index, effectiveSize, dataType, normalized, stride, offset, false, isBgra); const int effectiveStride = EffectiveVertexStride(stride, effectiveSize, type);
vao->SetAttributeFormat(index, effectiveSize, dataType, normalized, stride, offset, false, isBgra,
effectiveStride);
vao->BindAttributeBuffer(index, vbo); vao->BindAttributeBuffer(index, vbo);
vao->MirrorPointerIntoBinding(index, vbo, offset, EffectiveVertexStride(stride, effectiveSize, type)); vao->MirrorPointerIntoBinding(index, vbo, offset, effectiveStride);
} }
void BindVertexArray_State(GLuint array) { void BindVertexArray_State(GLuint array) {
@@ -489,10 +514,17 @@ namespace MobileGL::MG_Impl::GLImpl {
// recorded DataType is always Float64 - what IsLong adds is that this is the *unconverted* form, // recorded DataType is always Float64 - what IsLong adds is that this is the *unconverted* form,
// as opposed to VertexAttribFormat(GL_DOUBLE), which asks for a float conversion. // as opposed to VertexAttribFormat(GL_DOUBLE), which asks for a float conversion.
// //
// Whether the backend can feed it is detected, not assumed: DirectVulkan needs shaderFloat64, // Whether the backend can FEED it at full precision is detected, not assumed: DirectVulkan
// and DirectGLES can never have it at all. A backend without it declines here, loudly - GL error // needs shaderFloat64, and DirectGLES can never have it at all. What that costs is PRECISION,
// plus a log line naming the reason - rather than accepting state no draw could honour and // not the call and no longer the array: GL 4.6 core 10.3.2 defines no error for a well-formed
// rendering garbage. The matching startup POST row is in MG_Util/SelfTest/DriverPost.cpp. // glVertexAttribLFormat, and a GL 4.3 context has 64-bit attributes in core, so declining the
// call would be non-conformant and would make the four pure state queries
// (VERTEX_ATTRIB_ARRAY_SIZE / _TYPE / _LONG / _RELATIVE_OFFSET) unanswerable
// (KHR-GL43.vertex_attrib_binding.basic-state1/3). The format is therefore RECORDED here and
// the array is NARROWED to float32 at draw, matching the fp64 demotion every shader already
// gets (DemoteFloat64Pass) - loudly, once, naming the cost. The matching startup POST row is in
// MG_Util/SelfTest/DriverPost.cpp; the draw-side narrowing is DirectGLES/Managers.cpp and, on
// DirectVulkan, VertexInputStateFactory's Float64 case.
static void VertexAttribLFormatSeparate_State(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao, static void VertexAttribLFormatSeparate_State(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao,
GLuint attribindex, GLint size, GLenum type, GLuint attribindex, GLint size, GLenum type,
GLuint relativeoffset) { GLuint relativeoffset) {
@@ -502,15 +534,12 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!MG_Backend::pActiveBackendObject || if (!MG_Backend::pActiveBackendObject ||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) { !MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
MGLOG_I("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this " MGLOG_W_ONCE("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
"backend has no double-precision vertex attribute support - see the " "backend has no double-precision vertex attribute support - the format is recorded "
"\"64-bit vertex attributes\" / \"shaderFloat64\" POST row for what that costs", "and queryable, and the array is FETCHED AT FLOAT32 PRECISION at draw (the same "
"narrowing the shader's dvec inputs already get); see the \"64-bit vertex "
"attributes\" / \"shaderFloat64\" POST row for what that costs",
attribindex); attribindex);
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "VertexAttribLFormat",
"64-bit vertex attributes are not supported by this backend."));
return;
} }
vao->SetAttributeFormatSeparate(attribindex, size, MG_Util::ConvertGLEnumToDataType(type), vao->SetAttributeFormatSeparate(attribindex, size, MG_Util::ConvertGLEnumToDataType(type),
@@ -944,7 +973,7 @@ namespace MobileGL::MG_Impl::GLImpl {
params[0] = static_cast<GLfloat>(attr->Size); params[0] = static_cast<GLfloat>(attr->Size);
return; return;
case GL_VERTEX_ATTRIB_ARRAY_STRIDE: case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
params[0] = static_cast<GLfloat>(attr->Stride); params[0] = static_cast<GLfloat>(attr->LegacyStride);
return; return;
case GL_VERTEX_ATTRIB_ARRAY_TYPE: case GL_VERTEX_ATTRIB_ARRAY_TYPE:
params[0] = static_cast<GLfloat>(MG_Util::ConvertDataTypeToGLEnum(attr->Type)); params[0] = static_cast<GLfloat>(MG_Util::ConvertDataTypeToGLEnum(attr->Type));
@@ -1014,7 +1043,7 @@ namespace MobileGL::MG_Impl::GLImpl {
params[0] = static_cast<GLdouble>(attr->Size); params[0] = static_cast<GLdouble>(attr->Size);
return; return;
case GL_VERTEX_ATTRIB_ARRAY_STRIDE: case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
params[0] = static_cast<GLdouble>(attr->Stride); params[0] = static_cast<GLdouble>(attr->LegacyStride);
return; return;
case GL_VERTEX_ATTRIB_ARRAY_TYPE: case GL_VERTEX_ATTRIB_ARRAY_TYPE:
params[0] = static_cast<GLdouble>(MG_Util::ConvertDataTypeToGLEnum(attr->Type)); params[0] = static_cast<GLdouble>(MG_Util::ConvertDataTypeToGLEnum(attr->Type));
@@ -1079,8 +1108,11 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_VERTEX_ATTRIB_ARRAY_SIZE: case GL_VERTEX_ATTRIB_ARRAY_SIZE:
params[0] = attr->Size; params[0] = attr->Size;
return; return;
// The legacy shadow, not the resolved draw stride: GL 4.6 core table 23.3 defines this
// as the last glVertexAttrib*Pointer argument, which glBindVertexBuffer must not
// overwrite even though it does overwrite what the backend actually reads.
case GL_VERTEX_ATTRIB_ARRAY_STRIDE: case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
params[0] = attr->Stride; params[0] = attr->LegacyStride;
return; return;
case GL_VERTEX_ATTRIB_ARRAY_TYPE: case GL_VERTEX_ATTRIB_ARRAY_TYPE:
params[0] = static_cast<GLint>(MG_Util::ConvertDataTypeToGLEnum(attr->Type)); params[0] = static_cast<GLint>(MG_Util::ConvertDataTypeToGLEnum(attr->Type));
@@ -1138,7 +1170,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
const auto& attr = vao->GetAttribute(index); const auto& attr = vao->GetAttribute(index);
*pointer = reinterpret_cast<void*>(attr.Offset); *pointer = reinterpret_cast<void*>(attr.LegacyPointer);
} }
void GetVertexAttribIiv(GLuint index, GLenum pname, GLint* params) { void GetVertexAttribIiv(GLuint index, GLenum pname, GLint* params) {
@@ -1222,7 +1254,7 @@ namespace MobileGL::MG_Impl::GLImpl {
*param = static_cast<GLint>(attr.Size); *param = static_cast<GLint>(attr.Size);
return; return;
case GL_VERTEX_ATTRIB_ARRAY_STRIDE: case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
*param = static_cast<GLint>(attr.Stride); *param = static_cast<GLint>(attr.LegacyStride);
return; return;
case GL_VERTEX_ATTRIB_ARRAY_TYPE: case GL_VERTEX_ATTRIB_ARRAY_TYPE:
*param = static_cast<GLint>(MG_Util::ConvertDataTypeToGLEnum(attr.Type)); *param = static_cast<GLint>(MG_Util::ConvertDataTypeToGLEnum(attr.Type));
@@ -1294,14 +1326,14 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void BindVertexBuffer(GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride) { void BindVertexBuffer(GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride) {
auto vao = GetBoundVertexArrayOrError("BindVertexBuffer"); auto vao = GetBoundVertexArrayForBindingApi("BindVertexBuffer");
if (!vao) return; if (!vao) return;
VertexBufferBinding_State(vao, bindingindex, buffer, offset, stride, "BindVertexBuffer"); VertexBufferBinding_State(vao, bindingindex, buffer, offset, stride, "BindVertexBuffer");
} }
void BindVertexBuffers(GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, void BindVertexBuffers(GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets,
const GLsizei* strides) { const GLsizei* strides) {
auto vao = GetBoundVertexArrayOrError("BindVertexBuffers"); auto vao = GetBoundVertexArrayForBindingApi("BindVertexBuffers");
if (!vao) return; if (!vao) return;
if (!ValidateVertexBindingRange(first, count, "BindVertexBuffers")) return; if (!ValidateVertexBindingRange(first, count, "BindVertexBuffers")) return;
for (GLsizei i = 0; i < count; ++i) { for (GLsizei i = 0; i < count; ++i) {
@@ -1315,21 +1347,21 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void VertexAttribFormat(GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset) { void VertexAttribFormat(GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset) {
auto vao = GetBoundVertexArrayOrError("VertexAttribFormat"); auto vao = GetBoundVertexArrayForBindingApi("VertexAttribFormat");
if (!vao) return; if (!vao) return;
VertexAttribFormatSeparate_State(vao, attribindex, size, type, normalized, relativeoffset, false, VertexAttribFormatSeparate_State(vao, attribindex, size, type, normalized, relativeoffset, false,
"VertexAttribFormat"); "VertexAttribFormat");
} }
void VertexAttribIFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) { void VertexAttribIFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
auto vao = GetBoundVertexArrayOrError("VertexAttribIFormat"); auto vao = GetBoundVertexArrayForBindingApi("VertexAttribIFormat");
if (!vao) return; if (!vao) return;
VertexAttribFormatSeparate_State(vao, attribindex, size, type, GL_FALSE, relativeoffset, true, VertexAttribFormatSeparate_State(vao, attribindex, size, type, GL_FALSE, relativeoffset, true,
"VertexAttribIFormat"); "VertexAttribIFormat");
} }
void VertexAttribLFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) { void VertexAttribLFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
auto vao = GetBoundVertexArrayOrError("VertexAttribLFormat"); auto vao = GetBoundVertexArrayForBindingApi("VertexAttribLFormat");
if (!vao) return; if (!vao) return;
VertexAttribLFormatSeparate_State(vao, attribindex, size, type, relativeoffset); VertexAttribLFormatSeparate_State(vao, attribindex, size, type, relativeoffset);
} }
@@ -1341,7 +1373,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void VertexAttribBinding(GLuint attribindex, GLuint bindingindex) { void VertexAttribBinding(GLuint attribindex, GLuint bindingindex) {
auto vao = GetBoundVertexArrayOrError("VertexAttribBinding"); auto vao = GetBoundVertexArrayForBindingApi("VertexAttribBinding");
if (!vao) return; if (!vao) return;
if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return; if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return;
if (!ValidateVertexBindingIndex(bindingindex, "VertexAttribBinding")) return; if (!ValidateVertexBindingIndex(bindingindex, "VertexAttribBinding")) return;
@@ -1349,7 +1381,7 @@ namespace MobileGL::MG_Impl::GLImpl {
} }
void VertexBindingDivisor(GLuint bindingindex, GLuint divisor) { void VertexBindingDivisor(GLuint bindingindex, GLuint divisor) {
auto vao = GetBoundVertexArrayOrError("VertexBindingDivisor"); auto vao = GetBoundVertexArrayForBindingApi("VertexBindingDivisor");
if (!vao) return; if (!vao) return;
if (!ValidateVertexBindingIndex(bindingindex, "VertexBindingDivisor")) return; if (!ValidateVertexBindingIndex(bindingindex, "VertexBindingDivisor")) return;
vao->SetBindingDivisor(bindingindex, divisor); vao->SetBindingDivisor(bindingindex, divisor);

Some files were not shown because too many files have changed in this diff Show More