Compare commits

...
Author SHA1 Message Date
swung0x48 e4aab8d0bc [Fix] (Remote): offer inproc no shared images and refuse them on its server 2026-10-10 23:23:36 -04:00
swung0x48 745f59bc3f [Feat] (Remote): give inproc T0 the server's own store and never an AHB 2026-10-10 22:32:19 -04:00
swung0x48 5f9c3d6947 [Docs] (Performance): anland glmark2 before/after 2026-10-10 08:18:17 -04:00
swung0x48 79afd8a8e0 [Docs] (pwait): levers, timeline and latency effect 2026-10-10 08:18:16 -04:00
swung0x48 b733eed45c [CI] (census): drop the partial-SubData case from the magma-buffers baseline 2026-10-10 08:18:16 -04:00
swung0x48 166f47f115 [Fix] (Test): TrackerShareGroup uses Take() for the context slot 2026-10-10 05:25:51 -04:00
swung0x48 96037fc502 [Docs] (P15): pwab device A/B results 2026-10-10 04:14:57 -04:00
swung0x48 2b277b01d4 [Docs] (P15): present-wait levers handoff 2026-10-10 04:14:56 -04:00
swung0x48 71e442de02 [Feat] (Magma): MOBILEGL_MAGMA_EXTRA_SWAPCHAIN_IMAGES knob 2026-10-10 04:14:42 -04:00
swung0x48 fa56e1c671 [Test] (Espryt, Magma): WindowPassGateScenario 2026-10-10 04:14:41 -04:00
swung0x48 2175ee08cf [Perf] (Espryt): flush once per frame before the window pass 2026-10-10 04:14:41 -04:00
swung0x48 add15f0a17 [Perf] (Magma): submit off-screen work ahead of the swapchain acquire wait 2026-10-10 04:14:40 -04:00
swung0x48 f4f2bec711 [Perf] (Espryt): drain small partial writes as a whole-store orphan map 2026-10-10 04:14:25 -04:00
swung0x48 f66d31c881 [Docs] (P15): TLS cut device A/B 2026-10-10 04:14:24 -04:00
swung0x48 a6028ff38a [Fix] (TLS): drop the context reference before clearing the slot at thread exit 2026-10-10 04:14:14 -04:00
swung0x48 3b01067a9b [Perf] (TLS): Espryt context serial and lookup cache share one thread_local block 2026-10-10 04:14:13 -04:00
swung0x48 b5f9ab51ca [Perf] (TLS): inline Magma session and built-slot lookups 2026-10-10 04:14:12 -04:00
swung0x48 f7550322d4 [Perf] (TLS): applier lookup cache in one thread_local block 2026-10-10 04:14:10 -04:00
swung0x48 575be3532d [Perf] (TLS): pGLContext and gPipeInputs become pointer slots 2026-10-10 04:14:09 -04:00
swung0x48 0a206b0963 [Docs] (P15): queue the pwait latency trace 2026-10-10 04:14:08 -04:00
swung0x48 8ebb69d895 [Fix] (Pipe): share-group clocks fire dirty bits 11-14 for sibling-context changes 2026-10-10 04:14:07 -04:00
swung0x48 9a1d3062cb [Docs] (P15): present-wait timeline 2026-10-10 04:14:02 -04:00
swung0x48 a759b4b72d Docs (P15): present-wait analysis uses the existing traces. 2026-10-09 21:58:06 -04:00
swung0x48 4d3626215f Docs (P15): Perfetto timeline rule. 2026-10-09 21:53:01 -04:00
swung0x48 e0b8053e28 Docs (P15): queue the present-wait Perfetto trace. 2026-10-09 21:49:53 -04:00
swung0x48 a44a877080 Docs (P15): dirty mask parked; C8 approved separately. 2026-10-09 21:30:48 -04:00
swung0x48 d5ff64ded8 Docs (P15): per-context pipe dirty mask plan. 2026-10-09 21:28:39 -04:00
swung0x48 b48f55edfd Docs (P15): egc row, global-constants key cuts EmitGlobalConstants 0.022 to 0.014 ms/frame. 2026-10-09 20:27:55 -04:00
swung0x48 1accc7f421 Docs (P15): API 29 vs API 26 A/B, native TLS gives no TLS saving in a dlopen'ed lib. 2026-10-09 20:27:55 -04:00
swung0x48 dce02ae80e Build: -Pmobilegl.androidApi=29 builds a separate API 29 library and plugin; the default stays 26. 2026-10-09 20:27:53 -04:00
swung0x48 e70925d35b Pipe: per-program global-constants key and frontend-written spans replace the client shadow diff. 2026-10-09 18:26:43 -04:00
swung0x48 e9201891f5 Docs (P15): Espryt BSL depth-copy and attachment-shadow items measured on device, no gain (x 51.4 vs b 51.9 fps, MobileGlues 55.8); not landed. 2026-10-09 18:26:42 -04:00
swung0x48 cee9178e22 Bench: bench_session stop restores the pinned clocks. 2026-10-09 18:26:41 -04:00
swung0x48 0650608ddc User guide: env fallback now read consistently. 2026-10-09 14:29:53 -04:00
swung0x48 0b536de3fb Plugin build: honour the MOBILEGL_BUILD_DISAGGREGATED env fallbacks. 2026-10-09 14:25:56 -04:00
swung0x48 e072176c24 Plugin manifest: add the relaxed-semantics title meta-data. 2026-10-09 14:24:56 -04:00
swung0x48 d4688d0037 README: document MOBILEGL_ENABLE_LTO and FRAMES_IN_FLIGHT, link the user guide. 2026-10-09 14:24:35 -04:00
swung0x48 13e8e095b4 Fix the unix:/pipe: transport comment in ConfigLoader. 2026-10-09 14:24:19 -04:00
swung0x48 286e4af582 Docs: MobileGL no longer steps aside for LIBGL_ALWAYS_SOFTWARE. 2026-10-09 14:23:56 -04:00
swung0x48 4e055e4686 Reword the permanent client-window refusal text and drop the stale P12 tag. 2026-10-09 14:23:32 -04:00
swung0x48 9f61016776 Add an end-user guide under userguide/. 2026-10-09 13:36:15 -04:00
swung0x48 9f4d9095a9 Docs (P15): windowed out-of-process smoke of the LTO plugin passes on both backends; aux-socket shared-image send error noted. 2026-10-09 12:54:30 -04:00
swung0x48 6ffd2084fb Docs (P15): open queue puts the CI fix and the user guide first, plus the clock-restore and stale surface-message follow-ups. 2026-10-09 12:54:29 -04:00
swung0x48 c0ea142955 Docs (P15): hand-off of the running m2ab job (2a/2b A/B) and the LTO server-link check. 2026-10-09 12:54:28 -04:00
swung0x48 fe748773f3 Docs (P15): hand-off for the next agents (LTO GL CPU Magma 2.52 -> 2.36, Espryt 2.99 -> 2.90; levers table; WIP branches; harness rules). 2026-10-09 12:54:27 -04:00
swung0x48 1b2e817a97 Magma: wire-store rename spares 4 -> 12 with one fence question per rename, and a 50 us quiet window on unsignaled fence polls (FCL monolith GL CPU 2.44 -> 2.38 ms, 272 -> 278 fps at cpuhunt). 2026-10-09 12:54:27 -04:00
swung0x48 c4032d2022 Bench: freqcheck judges clocks over the measurement windows and thermal signals over the whole run (BSL's idle-GPU warmup sample no longer invalidates a pinned run). 2026-10-09 12:54:25 -04:00
swung0x48 09ae776836 Docs (P15): BSL 3 reps (1c +3.4 % fps on the GPU-bound scene; Magma ~0.99 of MobileGlues, whose runs fail only an out-of-window clock sample). 2026-10-09 12:54:24 -04:00
swung0x48 2b8c04373d Gradle builds the library with LTO by default, and the LTO block adds probed ICF, gc and interposition flags per toolchain. 2026-10-09 12:54:24 -04:00
swung0x48 fcf5ae5659 Magma: an enabled scissor test over a never-written box clips nothing instead of everything (DefaultScissorFollowsSurfaceScenario runs on DirectVulkan instead of skipping). 2026-10-09 12:54:23 -04:00
swung0x48 2bbb67e528 DirectGLES: emulate a same-image glBlitFramebuffer through a scratch copy (red-once: without it ATextureBlittedWithinItselfIsSampledByTheNextDraw fails on DirectGLES and DirectGLES.Split). 2026-10-09 12:54:22 -04:00
swung0x48 db346f2844 Docs (P15): fbm2 row (pipe batch: Magma GL CPU 2.68 -> 2.62, Espryt 3.03 -> 3.00) and BSL check note. 2026-10-09 07:42:24 -04:00
swung0x48 c85c9439a5 Magma: the pipeline, program and sampler hashes read their thread_local state once per hash (GetOrCreatePipeline 0.026 -> 0.013 ms/frame). 2026-10-09 07:42:03 -04:00
swung0x48 485dff1421 Pipe: per-framebuffer state memo, object-held handle hints, per-buffer handle reuse and a cached per-verb gate (ValidateForVerb 0.328 -> 0.282 ms/frame; FCL GL CPU Magma 2.68 -> 2.62, Espryt 3.03 -> 3.00; red-once per piece in its unit case). 2026-10-09 07:42:03 -04:00
swung0x48 9ff97c2e7e CI: the control smoke test's stub ctest follows E1 to ApplierReset as well. 2026-10-09 06:52:45 -04:00
swung0x48 afcc313d9f Docs (P15): fbm row (first FB-state memo within noise, per-target entry misses on A/B/A) and handoff; BSL gpuhunt shows no GPU regression from 1c (53.8 -> 55.7 fps). 2026-10-09 06:41:45 -04:00
swung0x48 9eed5a27d7 CI: the E1 wait-boundary control follows its unit case to ApplierReset (GenerateMipmap went kWaitNone in 2fbcc224), and ScenarioIsolationScenario's four verify-split entries are listed as reaching no verb. 2026-10-09 06:41:45 -04:00
swung0x48 8941eae008 Docs (P15): 1c rows (Magma inproc +5.0 % fps), PARITY-AUDIT M1 cross-FBO deferral, Espryt attrib-format split parked. 2026-10-09 04:50:18 -04:00
swung0x48 533a256482 Magma: wire clears defer into the next pass's load op and pass-begin barriers coalesce (passes 11.5 -> 4.3, barriers 15.5 -> 9.0 per frame; inproc apply 1.41 -> 1.24 ms; red-once: no materialize fails 4 ClearOrder cases, a dropped fold fails 2). 2026-10-09 04:50:17 -04:00
swung0x48 65d4b808c1 Espryt: the surface size is memoized per session until a swap or a draw-surface change (FCL monolith CPU 3.18 -> 3.05 ms/frame; red-once: an uninvalidated memo fails DefaultScissorFollowsSurfaceScenario). 2026-10-09 04:10:20 -04:00
swung0x48 98b2c37384 Docs (P15): monolith diff vs dev and H2 rows; Magma records 12.6 passes and 27.5 barriers/frame against dev's 4.1 and 3.4. 2026-10-09 04:10:18 -04:00
swung0x48 d2acb75766 Test: UploadOrderScenario checks a texture upload lands where the app issued it (red-once: hoisting it past recorded uses fails all 4 Magma cases). 2026-10-09 04:10:17 -04:00
swung0x48 6933228d59 Docs (P15): scoreboard with the fixed dev reference and harness H2 (pinned A720, SF frame source; spread 9.3% -> 1.2-2.9%). 2026-10-09 04:10:16 -04:00
swung0x48 d786f1c072 Magma: the shader blit keeps its views and framebuffer across frames (two views + one VkFramebuffer created and retired per blit before, ~0.03 ms/frame). 2026-10-09 04:10:15 -04:00
swung0x48 79b6deacaf CI: ScenarioIsolationScenario is exempt from split coverage as a mechanism case (its pair only means something run back to back in one process). 2026-10-09 04:09:48 -04:00
swung0x48 262c8985a9 Merge the integration-test scenario isolation fix. 2026-10-08 23:10:12 -04:00
swung0x48 e5ab9d4f06 CI: barriered-validate wrapper builds without the transport, and the field-ownership self-test expects CopyTexImage2D unbarriered since P15. 2026-10-08 23:04:48 -04:00
swung0x48 129355961e [Fix] (MG_IntegrationTest): each scenario starts with no program current and no GL error pending, so a case deleting its current program no longer breaks the next one in the same process. 2026-10-08 21:54:10 -04:00
swung0x48 6cecfcd22f Docs (P15): handoff for the async default and frames in flight. 2026-10-08 19:49:55 -04:00
swung0x48 16867c5c64 Docs (P15): final wait-class verdicts. 2026-10-08 19:49:06 -04:00
swung0x48 0b96a3efbf MOBILEGL_FRAMES_IN_FLIGHT drives both backends and derives the split present credit (default 3 keeps credit 1; the Magma name stays as an alias). 2026-10-08 19:49:05 -04:00
swung0x48 2fbcc22410 Pipe: fire-and-forget is async - mipmap, copies, XFB span and storage-block rows to kWaitNone, inproc creates and PBO readbacks stop waiting, and the default dual block lets a barriered verb cost one wait (FCL BSL: ~40 sync points/frame). 2026-10-08 19:49:04 -04:00
swung0x48 1771381057 Docs (P15): wait-class audit for review and the inproc census and credit results. 2026-10-08 18:46:44 -04:00
swung0x48 2a1f1df6d1 Docs (P15): handoff notes on the inproc serialization and the anland root cause. 2026-10-08 18:02:05 -04:00
swung0x48 cc5a4c0a60 Bench session retries a slow service restart for up to 30 s before reporting it not restored. 2026-10-08 18:02:04 -04:00
swung0x48 7acf3ea8e1 Transport: MOBILEGL_IPC_WAIT_STATS logs the run-ahead client's waits per frame by record op, quiesce reason and barriered verb; the per-frame P65ClientPace line drops to debug. 2026-10-08 18:02:03 -04:00
swung0x48 db915f76f0 Transport: a client parked on the present credit is rung only by the credit, not by every retired drain batch (apply-thread futex wakes 0.056 ms/frame on FCL inproc). 2026-10-08 18:02:03 -04:00
swung0x48 657722ff57 Pipe: a buffer-only glBindVertexBuffer no longer rebuilds and compares the vertex-elements blob (VAO elements version; red-once: BindVertexBufferSwitchesAndOffsets fails on both backends). 2026-10-08 18:02:02 -04:00
swung0x48 58a97bb7b8 Bench session stops the anland app at start and relaunches it at stop, so FCL keeps the foreground. 2026-10-08 18:02:01 -04:00
swung0x48 e00db8e64f Pipe: every verb names its record op in a verb-indexed table (static_assert: one row per verb), so a verb syncs only when its record does; the barriered fallback that quiesced the applier once per frame at the blit is gone (FCL inproc GL-thread wait 1.65 ms/frame; red-once: blit row removed aborts the run-ahead blit test, missing row fails to compile). 2026-10-08 18:02:01 -04:00
swung0x48 19a96929aa [Docs] (P15): handoff with the cpuhunt/gpuhunt references, the apply-thread breakdown and the open guilty-check decision. 2026-10-08 15:26:52 -04:00
swung0x48 500b63ea58 [Perf] (Magma): a renamed wire store reuses its retired buffers once their submissions complete (FCL inproc apply thread 1.74 -> 1.61 ms/frame at cpuhunt). 2026-10-08 15:26:24 -04:00
swung0x48 c8a5abe581 [Perf] (pipe): a vertex-elements record identical to the one its VAO already published is not re-created (FCL Espryt, shadow off: host calls 3724 -> 1228/frame). 2026-10-08 12:31:57 -04:00
swung0x48 49c6394dfd [Tools] (config): debug.mobilegl.env sets MOBILEGL_* knobs in an app-hosted Android process, and the device-state line carries them. 2026-10-08 10:59:10 -04:00
swung0x48 b4e209e12b [Perf] (Espryt): a shadow of each driver VAO's attribute state makes the vertex walk emit only differences (FCL host GLES calls 3717 -> 850/frame, CPU 2.78 -> 2.67 ms at cpuhunt). 2026-10-08 10:49:29 -04:00
swung0x48 6985e47025 [Tools] (bench): push the frequency sampler with CRs stripped, so a CRLF checkout still runs on the device. 2026-10-08 09:32:52 -04:00
swung0x48 3c61689dac [Docs] (P15): two-level CPU target, clock profiles and the measurement-validity rule. 2026-10-08 09:31:27 -04:00
swung0x48 efa415b00c [Tools] (bench): clock profiles, bench sessions that release the perf stack's frequency caps, and a per-run frequency/thermal validity line. 2026-10-08 09:31:26 -04:00
swung0x48 4e27342e81 [Test] (trace): FCL MC 1.21.5 vanilla fixture, a 252-frame window with a dev DirectGLES golden that both backends match (ci:false). 2026-10-08 09:31:25 -04:00
swung0x48 4a8ecf9aaf [Docs] (P15): S0 handoff, parity audit corrections and the bsl workload in matrix_p14. 2026-10-08 06:59:56 -04:00
swung0x48 748ffc31c5 [Perf] (Magma): small busy wire buffers are renamed from a host shadow instead of a staged copy that ends the pass (FCL WriteWireBuffer 0.100 -> 0.077 ms/frame). 2026-10-08 06:59:40 -04:00
swung0x48 855b3b5009 [Perf] (Magma): converted wire vertex streams are cached across frames and default-framebuffer clears stay standalone (BSL Magma 19.5 -> 77.2 fps, MobileGlues 78.2). 2026-10-08 06:59:39 -04:00
swung0x48 4984da923f [Perf] (Magma): wire clears record inside the draw pass, draw passes are cached across frames and blit sets come from per-frame pools (FCL Magma CPU 3.29 -> 2.37 ms/frame). 2026-10-08 06:59:38 -04:00
swung0x48 c7360b47e2 [CI] Apt install steps get 45-minute timeouts; 15 cut off a slow-mirror install. 2026-10-08 05:40:20 -04:00
swung0x48 91f04ebc7c [CI] Network setup steps get 15-minute step timeouts and one retry (Vulkan SDK, Android SDK), and the NDK download retries three times. 2026-10-08 03:09:04 -04:00
swung0x48 84ec06f3fc [Docs] (P15): swap-interval stage, user decisions, and the FCL probe tools. 2026-10-08 00:22:22 -04:00
swung0x48 408b9fcade [Docs] (P15): plan, FCL wait diagnosis and parity-audit seed. 2026-10-08 00:22:22 -04:00
swung0x48 ddfbdab0e8 [Fix] (MG_Test): the guilty-reset case waits for the held drain, not for an ack the hold delays, and the supervisor's refusal line is waited for in its log. 2026-10-07 23:25:09 -04:00
swung0x48 80c6aa39da [Fix] (CI): the spawn-lane parity gate sanctions the inproc-only .SyncValidation. tail, as it does .Mip3DValidation. 2026-10-07 22:20:45 -04:00
swung0x48 92da2486c5 [Fix] (CI): the latch-site gate's DrainRing pattern accepts the drain's poll-head parameter. 2026-10-07 22:13:20 -04:00
swung0x48 a0f4aa830e [Docs] (P14): close-out bench, README, next-phase draw-packet sketch, DEBTS rows and handoff (rd12 monolith Espryt 69.8 -> 91.1, Magma 45.4 -> 83.8 fps). 2026-10-07 21:20:11 -04:00
swung0x48 781a7446c4 [CI] (Magma): the run-ahead and cache sync-validation gates turn on submit-time validation, without which a cross-submission hazard is never reported (a broken upload barrier now reds the run-ahead gate). 2026-10-07 19:49:33 -04:00
swung0x48 86ab28355c [Fix] (MG_IntegrationTest): the upload-between-draws entries get one private log per case and the sync-validation entry is inproc only, as the split gates require. 2026-10-07 19:23:10 -04:00
swung0x48 e058a4507a [Perf] (MG_Remote): the apply loop hands its poll's ring head to the drain, so a woken drain does not poll the GPU-fault verdict twice (Espryt rd12 inproc apply thread 1.34 -> 0.78 ms/frame). 2026-10-07 18:21:12 -04:00
swung0x48 ec973fba84 [Fix] (Magma): a hang-watch marker slot is reused once its submission's fence completed, not once the host read its event. 2026-10-07 17:53:39 -04:00
swung0x48 67f4f311d3 [Test] (MG_IntegrationTest): texels uploaded between two in-flight draws of one frame, every arm plus Magma monolith/inproc under submit-time synchronization validation (red on a broken upload barrier). 2026-10-07 17:53:38 -04:00
swung0x48 c24b3428d6 [Fix] (MG_Remote): the apply drain polls the GPU-fault verdict before a record its last poll could not have seen, so a guilty reset stops the next record again (549a34bb regression; the test now holds that window open). 2026-10-07 17:53:37 -04:00
swung0x48 553eb86d31 [Perf] (Magma): texture level uploads stop waiting for the GPU to drain; queue order and the upload barrier already order them (rd12 monolith 70.4 -> 83.4, openra 367 -> 745 fps). 2026-10-07 16:26:07 -04:00
swung0x48 785df78d77 [Perf] (P14): one UBO-ring lookup per draw, split residual copy/stamp loops, one context read for GPU-write marks, Magma slice stamps from the owner's counter (rd12 monolith Espryt 88.8 -> 90.3, Magma 69.8 -> 70.3 fps). 2026-10-07 16:06:02 -04:00
swung0x48 0bd0abf652 [Docs] (P14): handoff names the dispatched CI runs. 2026-10-07 15:25:15 -04:00
swung0x48 ff53ba3b2b [Docs] (P14): checkpoint handoff. 2026-10-07 15:24:42 -04:00
swung0x48 94705adccd [Perf] (MG_Pipe): inline process applier, a precomputed residual-fill walk, word-wise uniform spans and memo-answered draw bindings (rd12 monolith Espryt 83.5 -> 88.6, Magma 67.0 -> 69.5 fps). 2026-10-07 15:21:58 -04:00
swung0x48 81bdfb1f80 [Perf] (Espryt): twin lookups skip the bucket SharedPtr copy and the native-context state is one thread-local block (rd12 monolith 81.0 -> 84.1 fps). 2026-10-07 15:21:57 -04:00
swung0x48 4fe2edb6fa [Tools] (P14): profile_p14.sh profiles the tcp arm too. 2026-10-07 14:58:23 -04:00
swung0x48 032dcd240e [Perf] (Magma): the wire vertex-input layout is kept on its vertex-elements record and buffer handles resolve through a slot-indexed front. 2026-10-07 14:58:22 -04:00
swung0x48 c1497bb83e [Perf] (MG_Pipe): a vertex array keeps its buffer and index bindings on the server, a small uniform change crosses as a span, and unchanged context values skip their record (rd12 split arms +5-10%). 2026-10-07 14:58:21 -04:00
swung0x48 114bd70122 [Tools] (P14): FCL restore deletes the two properties instead of setting them empty. 2026-10-07 13:21:25 -04:00
swung0x48 0158a115de [Perf] (Magma): a wire draw that provably continues the open pass skips the attachment resolve and pass-key walk (rd12 monolith 63.2 -> 65.5, inproc 89.8 -> 92.6 fps). 2026-10-07 13:21:24 -04:00
swung0x48 8883e496bd [Perf] (MG_Pipe): the residual fill keeps unchanged object pins and a memoised vertex-buffer set keeps its hash (rd12 Espryt monolith +1%). 2026-10-07 13:21:23 -04:00
swung0x48 0cc54748f0 [Tools] (P14): layers.py can list the entries into one layer. 2026-10-07 12:32:18 -04:00
swung0x48 80dc9d45d9 [Perf] (MG_Pipe): a per-VAO memo keeps the emitters' handles and buffer set across draws, and an unchanged context-value record skips its hash (rd12 Espryt monolith +1%). 2026-10-07 12:32:05 -04:00
swung0x48 d07ec0255b [Tools] (P14): benchmark runs default to no per-frame glFinish, plus FCL and layer-split helpers. 2026-10-07 12:15:12 -04:00
swung0x48 4f4297f347 [Tools] (P14): opt-in fps log at swap (debug.mobilegl.fps_log) and a debug.mobilegl.transport override for app-hosted libraries. 2026-10-07 12:15:11 -04:00
swung0x48 5788a6023e [Perf] (Magma): a wire draw whose image descriptors cannot have moved rebinds its descriptor set with fresh buffer offsets (rd12 monolith 60.5 -> 63.3, inproc 83.7 -> 89.7 fps). 2026-10-07 12:15:10 -04:00
swung0x48 afcf8d96f9 [Perf] (Espryt): a VAO twin adopts an unchanged buffer set and element buffer instead of re-emitting them (rd12 monolith 75.2 -> 80.5 fps). 2026-10-07 12:15:10 -04:00
swung0x48 e6281b26de [Perf] (Magma): wire texture and buffer lookups get direct-mapped fronts and a continued pass stops re-marking its attachments (rd12 monolith 59.6 -> 60.5 fps). 2026-10-07 11:25:02 -04:00
swung0x48 eafe1179b3 [Tools] (P14): the profiler records the whole run and the analysis cuts the steady tail. 2026-10-07 10:50:39 -04:00
swung0x48 4ef9705ee0 [Perf] (Magma): wire draws memoise each unit's sampler and skip rebinding an unchanged pipeline, vertex or index binding (rd12 monolith 57.9 -> 60.3 fps). 2026-10-07 10:50:38 -04:00
swung0x48 b556ec3134 [Perf] (Espryt): a clean framebuffer attachment skips its per-draw resync behind the unit list's gate (rd12 monolith 74.5 -> 77.0 fps). 2026-10-07 10:50:37 -04:00
swung0x48 0b0089e599 [Tools] (P14): the sampler and profiler find spawn and tcp server processes by their install path, plus a per-component profile split. 2026-10-07 10:27:01 -04:00
swung0x48 8346d80999 [Perf] (MG_Pipe): lifetime-id slot lookups get a direct-mapped front and buffer binding slots are indexed (rd12 monolith +1.5%). 2026-10-07 10:27:00 -04:00
swung0x48 549a34bb1a [Perf] (MG_Remote): the apply loop asks the GPU-fault verdict every 256 records and at each present, not per record (Espryt rd12 inproc 55.5 -> 60.2 fps). 2026-10-07 10:26:59 -04:00
swung0x48 2b0857b016 [Tools] (P14): sampler sees server processes and GPU busy every tick, arms can name a second build, and a simpleperf profile helper. 2026-10-07 10:07:44 -04:00
swung0x48 a9108e1a0b [Perf] (Espryt): a VAO twin whose buffer set alone moved re-points enabled arrays without the 32-slot enable walk (rd12 monolith 70.3 -> 73.5 fps). 2026-10-07 10:07:43 -04:00
swung0x48 267211e8b4 [Perf] (Magma): wire draws stop rehashing program SPIR-V and stop allocating per draw (rd12 monolith 45.7 -> 56.7 fps). 2026-10-07 10:07:42 -04:00
swung0x48 51c1411696 [Docs] (P14): code-based perf estimate for every arm, written before the benchmark. 2026-10-07 09:18:22 -04:00
swung0x48 d56179d876 [Tools] (P14): pinned-clock perf matrix with a per-thread CPU and GPU-busy sampler. 2026-10-07 09:18:21 -04:00
swung0x48 f8a9466b93 [Fix] (CI): retrace exit detection tolerates a single failed pidof, and artifact cleanup lists jobs in pages the API serves. 2026-10-07 02:19:59 -04:00
swung0x48 242663c9ae [Docs] (P13): P13 closes - exit gates, device smoke and the deferred device list are recorded, and the roadmap moves to P14. 2026-10-06 22:33:02 -04:00
swung0x48 4bac6255fd [Build] (P13 W7): the server-only link check stays out of verify builds, whose comparator reads both halves by design. 2026-10-06 22:20:29 -04:00
swung0x48 f4e98710ff [Docs] (P13 W10): guides and design docs describe the three-build, record-arm-only shape, and three P8 debts close. 2026-10-06 22:17:18 -04:00
swung0x48 4379ceed97 [CI] (P13): the pipe generator's scanner drops its two retired registry exemptions. 2026-10-06 22:17:17 -04:00
swung0x48 87e915382c [Test] (P13 W8): recorder goldens pin the pushed record stream of two fixed GL call sequences, with two negative controls. 2026-10-06 22:17:16 -04:00
swung0x48 db8484cbb9 [Fix] (P13 W8): the transport-less monolith sends set_context_values like the split build's does, and render-state padding is zeroed so equal states share one CSO. 2026-10-06 22:17:16 -04:00
swung0x48 f7360fe210 [CI] (P13): the memo-purity gate drops its last allow entry and the fatal census holds at 70 abort sites. 2026-10-06 21:58:43 -04:00
swung0x48 45513aef51 [Docs] (P13): W9 moves to the post-P13 perf pass as a debt, and the README records the '# P13' gate and the §1.3 cleanup. 2026-10-06 21:43:28 -04:00
swung0x48 cd4f2db53c [Docs] (P13): post-P13 perf work takes the dev build as its baseline, recorded in DEBTS and the handoff. 2026-10-06 21:42:29 -04:00
swung0x48 de6ab16756 [Refactor] (P13): the legacy BufferBackendOps table, Magma's never-populated texture/render-pass/renderbuffer caches and VkClearManager go, and the buffer mock tests drive the resource op table instead (ratchet 36 -> 27). 2026-10-06 21:39:39 -04:00
swung0x48 9987e57f64 [CI] (P13): the unit cases that drove the deleted frontend-keyed entries are logged and retired from the skip census. 2026-10-06 20:55:17 -04:00
swung0x48 fa77b1874f [Refactor] (P13): Espryt's frontend-keyed slot-table entries and the apply-thread refusals go, death notices release twins by handle, and the link ratchet refuses any '# P13' line (49 -> 36 symbols). 2026-10-06 20:55:16 -04:00
swung0x48 556ff46601 [Docs] (P13 W7): the README records the module targets and the server-only link check. 2026-10-06 20:13:29 -04:00
swung0x48 9fa61b84a5 [Build] (P13 W7): the library links from module OBJECT targets along the ratchet's partition, and a server-only link check target must link without the frontend modules. 2026-10-06 19:50:31 -04:00
swung0x48 3fb0054e79 [Docs] (P13 W6): the handoff records how W6 found and removed the frontend arm and what the last nine ratchet symbols need. 2026-10-06 19:32:51 -04:00
swung0x48 922507f8f8 [Refactor] (P13 W6f): Magma's pipeline build reads the draw framebuffer record only, and the frontend texture sync behind it goes; link ratchet 58 -> 49. 2026-10-06 19:31:02 -04:00
swung0x48 9f88379141 [CI] (P13 W6): the skip census records the cube-map-view case's capability skip on the Espryt lanes. 2026-10-06 19:23:50 -04:00
swung0x48 a4c78fe10a [Docs] (P13): the FCL soak table is one table and the death offsets are stated per cluster. 2026-10-06 19:23:17 -04:00
swung0x48 5011c3d868 [Docs] (P13): a matched P12/P13 FCL soak shows the silent death's rate does not depend on the MobileGL library, and the W6 progress is recorded. 2026-10-06 19:23:08 -04:00
swung0x48 737ef5e980 [CI] (P13 W6e): the W6c-e unit removals are logged and retired from the skip census, and the link ratchet falls to 58 symbols. 2026-10-06 18:58:24 -04:00
swung0x48 dab827d54f [Refactor] (P13 W6e): the backends' frontend-framebuffer clear, blit and readback entries go - the verb port owns those slots in every mode - with Magma's frontend render-pass, pending-clear and renderbuffer halves. 2026-10-06 18:57:15 -04:00
swung0x48 ec7fc40ba5 [Docs] (P13): the FCL silent death (status=34, SIGRTMIN+2) is recorded as an FCL-side debt with its soak evidence. 2026-10-06 18:50:30 -04:00
swung0x48 8bc39c79d2 [Test] (P13 W6d): the unmigrated-emulation list is empty, the dead storage-view format case goes, and Espryt's bring-up check is the colour-attachment cap alone. 2026-10-06 18:20:50 -04:00
swung0x48 8abae647d0 [Refactor] (P13 W6d): Espryt's family arm predicates fold to the record arm, checked once at capability bring-up, and the frontend-arm walks they selected go. 2026-10-06 18:08:33 -04:00
swung0x48 dc1e95b4c3 [Refactor] (P13 W6c): Magma's frontend arm goes - the frontend program source, sampled-set walk, placeholder textures, VAO draw memos, identity tables and fast path - leaving the record arm. 2026-10-06 17:55:44 -04:00
swung0x48 887547c707 [Test] (P13 W6): the cube-map view readback case skips where GL_ARB_texture_view is not advertised. 2026-10-06 17:01:56 -04:00
swung0x48 e822c933c9 [CI] (P13 W6b): the frontend-arm unit removals are logged and retired from the skip census. 2026-10-06 16:56:32 -04:00
swung0x48 3aed7ae047 [Test] (P13 W6b): the unmigrated-emulation list loses the frontend arm's shadow readback. 2026-10-06 16:51:20 -04:00
swung0x48 e0113503af [Refactor] (P13 W6b): the frontend-arm code after each record-arm return goes, and the transform-feedback state keeps one role. 2026-10-06 16:46:09 -04:00
swung0x48 742d43aeb4 [Refactor] (P13 W6b): the data-arm predicate folds out of every backend branch; frontend-arm unit cases go, three adapted, and two GL sampling rules move to an integration scenario. 2026-10-06 16:39:52 -04:00
swung0x48 740dd301b9 [CI] (P13 W6a): the skip census retires the deleted frontend-arm cases and the AbaControlHandles lane, and records the retired live-host-writes wire's skip. 2026-10-06 16:21:17 -04:00
swung0x48 d61a4240ee [Refactor] (P13 W6a): the record arm is the only data arm - MOBILEGL_PIPE_DATA_ARM and its frontend-arm pins go, and the cube-face readback rules move to an integration scenario. 2026-10-06 16:16:29 -04:00
swung0x48 4c3e469ae4 [Docs] (P13): Magma monolith's record-arm cost on device is recorded as a debt for after P13. 2026-10-06 16:12:16 -04:00
swung0x48 c1e985c893 [CI] (P13 W5): the verify lane's omitted-fill-point control withholds the pack state at ReadPixels, and the data arm is logged at bring-up. 2026-10-06 15:45:14 -04:00
swung0x48 7fb18840dc [Refactor] (P13 W5 S8): MOBILEGL_BUILD_RECORD_ARM is folded away - the record arm is unconditional - and retired_switches keeps it from coming back. 2026-10-06 15:03:11 -04:00
swung0x48 a0b9ca0772 [Fix] (P13 W5 S7): Magma's T0 import and its self-test stay transport-only inside the record arm. 2026-10-06 14:50:07 -04:00
swung0x48 a37212eac9 [Fix] (P13 W5 S7): Espryt's T0 re-import after a context loss stays transport-only inside the record arm's twin re-arm. 2026-10-06 14:46:52 -04:00
swung0x48 acfee266f5 [Fix] (Build): every library gets the vendored glvnd headers its GlvndVendor.cpp files include, so the Android library without MG_Remote builds. 2026-10-06 14:45:20 -04:00
swung0x48 942bb6d7dd [Fix] (P13 W5 S7): the verb stamp and read verdicts build only with the poison that asks them. 2026-10-06 14:34:27 -04:00
swung0x48 bc8595f27a [CI] (P13 W5 S7): the monolith poison-omission control omits the pack state at ReadPixels, a read the record arm still makes. 2026-10-06 14:22:18 -04:00
swung0x48 6d73529f70 [Test] (P13 W5 S7): the always-false live-host-writes wire's case skips wherever the record arm gives the bit a producer. 2026-10-06 14:19:02 -04:00
swung0x48 e0910ebd85 [Fix] (P13 W5 S7): the verify read hook answers the monolith verb port's neutral-pack window from the applied pack, as for a server-stamped verb. 2026-10-06 14:15:29 -04:00
swung0x48 8efb7f492a [Refactor] (P13 W5 S7): the barrier-pull counter's role-split and strict checks stay on the transport guard. 2026-10-06 14:10:40 -04:00
swung0x48 a71c23bcf3 [Refactor] (P13 W5 S7): the verb stamp and the poison read verdicts follow the record-arm guard; the role split and the server window stay transport. 2026-10-06 14:09:47 -04:00
swung0x48 1cd35d3922 [Refactor] (P13 W5 S7): Magma's wire-object collection, wire-store teardown, depth-resolve entry point and the fast-rebind destroy epoch follow the record-arm guard. 2026-10-06 13:57:26 -04:00
swung0x48 e9a353c324 [Test] (P13 W5 S7): the pinned monolith frontend arm pins it wherever the record arm is built. 2026-10-06 13:53:39 -04:00
swung0x48 557975bf2d [Refactor] (P13 W5 S7): the record verbs' verb-state stamps, the record-reading draw and sampler passes and the image-bindable re-mint follow the record-arm guard. 2026-10-06 13:51:25 -04:00
swung0x48 aa5c12b08c [Fix] (P13 W5 S7): Espryt's shared-image arm in the storage sync stays on the transport guard inside the record arm. 2026-10-06 13:45:23 -04:00
swung0x48 7ac3ce0376 [Fix] (P13 W5 S7): Espryt's shared-image sync stays on the transport guard. 2026-10-06 13:44:53 -04:00
swung0x48 956e3847cd [Refactor] (P13 W5 S7): the record arm's remaining transport-guarded blocks (XFB capture landing, staged-texture follows, view-window mips, tracker membership, unit-window checks) follow the record-arm guard. 2026-10-06 13:42:45 -04:00
swung0x48 4ec65e801a [Fix] (P13 W5 S7): Espryt's copy-image store follow and its driver-write mark build with the record arm. 2026-10-06 13:37:43 -04:00
swung0x48 23593c9f15 [Feature] (P13 W5 S7): the record arm is built into every library, so monolith without MG_Remote runs it too. 2026-10-06 13:30:09 -04:00
swung0x48 1961029043 [Feature] (P13 W5 S7): MOBILEGL_PIPE_PERSISTENT_MPROTECT=0 keeps the tracker's SIGSEGV handler out, and the mprotect arm builds only on Linux and Android. 2026-10-06 13:17:25 -04:00
swung0x48 dd7fbbd7d1 [Test] (P13 W5 S6): the integration peek answers the data arm and the tracker probe arms wherever the record arm is built. 2026-10-06 13:14:58 -04:00
swung0x48 851251671c [CI] (P13 W5 S4): an include-closure probe keeps the record core clear of MG_Remote and the wire serializer. 2026-10-06 13:13:39 -04:00
swung0x48 b1978890b4 [Refactor] (P13 W5 S5): the data-arm predicates, their latch and MOBILEGL_PIPE_DATA_ARM follow the record arm instead of the transport. 2026-10-06 13:00:28 -04:00
swung0x48 ad47dc8c6f [Refactor] (P13 W5 S4c): the XFB lifetime lookup, the deferred-destroy drain and the PBO readback target build without a transport. 2026-10-06 12:58:55 -04:00
swung0x48 0b5e7fad82 [Fix] (P13 W5 S4c): close the moved DeleteTransformFeedback emitter. 2026-10-06 12:52:30 -04:00
swung0x48 8e76f9dcb7 [Refactor] (P13 W5 S4c): the verb emitters, the monolith verb port and the client GPU-write set move to MG_Record under MG_Impl/Pipe/Verb. 2026-10-06 12:51:02 -04:00
swung0x48 ed7800a550 [CI] (P13 W5 S4): the latch-site map follows the record verbs into RecordVerbSink.cpp. 2026-10-06 12:37:43 -04:00
swung0x48 e04c6edb76 [Refactor] (P13 W5 S4): RecordVerbSink names MG_Pipe's clear constants directly. 2026-10-06 12:33:23 -04:00
swung0x48 fe5324c900 [Refactor] (P13 W5 S4): the record verbs move from ServerVerbSink to MG_Record::RecordVerbSink, which ServerVerbSink now extends. 2026-10-06 12:32:04 -04:00
swung0x48 f4be8c380f [Style] (P13 W5): VertexInputStateFactory.cpp includes its own header first. 2026-10-06 12:14:02 -04:00
swung0x48 7f194a8579 [Refactor] (P13 W5 S3): the apply side's backend comes through MG_Backend's ApplyRoleBackend hook, which ServerLoop installs. 2026-10-06 12:13:37 -04:00
swung0x48 bb45648c41 [Refactor] (P13 W5 S3): Magma's wire texture and vertex-input builders and the buffer shadow size follow the record-arm guard. 2026-10-06 12:12:09 -04:00
swung0x48 c285f350b1 [Refactor] (P13 W5 S4): a build without a transport answers the server-role question false. 2026-10-06 12:11:15 -04:00
swung0x48 0f5658f56e [Refactor] (P13 W5 S4): the persistent-map tracker moves to MG_State as MG_Record; the adoption tier's verdict stays in MG_Remote. 2026-10-06 12:09:56 -04:00
swung0x48 17e36589fa [Refactor] (P13 W5 S3): the latch twin and Magma's shared-image binding take their guards; the dual-block diagnostics stay transport. 2026-10-06 12:03:10 -04:00
swung0x48 b13af729e4 [Refactor] (P13 W5 S5): the record arm's three knobs move from IpcTable to RecordArmTable, which every record-arm build parses. 2026-10-06 12:01:49 -04:00
swung0x48 b050c60b32 [Refactor] (P13 W5 S3): caps source, object death and the Magma wire stores' members follow their real guards. 2026-10-06 12:00:37 -04:00
swung0x48 cb81610b2b [Refactor] (P13 W5 S3): the record arm's definitions and its depth-resolve probe source follow the record-arm guard. 2026-10-06 11:58:18 -04:00
swung0x48 1956d6cd6f [Refactor] (P13 W5 S3): transport-only hooks get no-op twins where the record arm calls them, and their guards return to the transport. 2026-10-06 11:56:18 -04:00
swung0x48 c879776a26 [Refactor] (P13 W5 S3): Espryt's shared-image checks stay on the transport guard. 2026-10-06 11:53:51 -04:00
swung0x48 8a2ddc8150 [Refactor] (P13 W5 S3): Magma's funnels, the texture ops and the staged-texture macros follow the record-arm guard; shared images stay transport. 2026-10-06 11:53:24 -04:00
swung0x48 d00d5c9416 [Refactor] (P13 W5 S3): the record stores' includes and the slot tables' refusals follow the record-arm guard. 2026-10-06 11:50:26 -04:00
swung0x48 b26167a095 [Refactor] (P13 W5 S3): Espryt's sampler-view refusal latches through the record seam. 2026-10-06 11:42:22 -04:00
swung0x48 73a185f6d1 [Refactor] (P13 W5 S4): the staged stores move to MG_Backend/Record as MG_Record and die through the record fail seam. 2026-10-06 11:35:24 -04:00
swung0x48 e544121eed [Refactor] (P13 W5 S3): record-arm declarations, role-guard stubs and the T0 split in the backends. 2026-10-06 11:29:37 -04:00
swung0x48 c04347ef88 [Refactor] (P13 W5 S2): GL_Getter's closing brace keeps the guard of the block it closes. 2026-10-06 11:20:09 -04:00
swung0x48 80bfc7ce02 [Test] (GL): name the GL_ARB_shader_objects cases that reach no verb in the verify-split arm census. 2026-10-06 11:14:40 -04:00
swung0x48 a891d0f514 [Docs] (P13 W5): S1 and S2 in the progress table. 2026-10-06 11:12:17 -04:00
swung0x48 b965a18e15 [CI] (P13 W5 S2): the wire declines audit reads MOBILEGL_BUILD_RECORD_ARM as a build-selecting opener. 2026-10-06 10:52:27 -04:00
swung0x48 22739a033c [Refactor] (P13 W5 S2): MG_Backend/DirectVulkan's record-arm guards name MOBILEGL_BUILD_RECORD_ARM. 2026-10-06 10:51:26 -04:00
swung0x48 036d1ac36e [Refactor] (P13 W5 S2): MG_Backend/DirectGLES's record-arm guards name MOBILEGL_BUILD_RECORD_ARM. 2026-10-06 10:51:25 -04:00
swung0x48 2a8bb7e659 [Refactor] (P13 W5 S2): MG_Backend/MGPipe's record-arm guards name MOBILEGL_BUILD_RECORD_ARM. 2026-10-06 10:51:24 -04:00
swung0x48 759bd8ffa5 [Refactor] (P13 W5 S2): MG_Util's record-arm guards name MOBILEGL_BUILD_RECORD_ARM. 2026-10-06 10:51:23 -04:00
swung0x48 06a4ebb645 [Refactor] (P13 W5 S2): MG_Pipe's record-arm guards name MOBILEGL_BUILD_RECORD_ARM. 2026-10-06 10:51:21 -04:00
swung0x48 b16dc20473 [Refactor] (P13 W5 S2): MG_Impl's record-arm guards name MOBILEGL_BUILD_RECORD_ARM. 2026-10-06 10:51:20 -04:00
swung0x48 1d0f908b4a [Refactor] (P13 W5 S2): MG_State's record-arm guards name MOBILEGL_BUILD_RECORD_ARM. 2026-10-06 10:51:14 -04:00
swung0x48 0fbcaed454 [Docs] (P13 W5 S2): the guard classifier's transport words and the rewrite tool. 2026-10-06 10:51:09 -04:00
swung0x48 a6c02a1741 [Build] (P13 W5 S2): MOBILEGL_BUILD_RECORD_ARM, equal to the disaggregated build until the flip. 2026-10-06 10:49:44 -04:00
swung0x48 2f2a515d04 [Fix] (retrace): bump apitrace so glCreate*ObjectARB refresh the typed shader / program map. 2026-10-06 10:43:54 -04:00
swung0x48 b3a8a1f47f [Docs] (P13): record the GL_ARB_shader_objects work. 2026-10-06 10:31:43 -04:00
swung0x48 6baa1bcdb4 [Feat] (GL): the advertised GL_ARB_vertex_shader entry points forward to the core instead of being stubs or missing. 2026-10-06 10:21:06 -04:00
swung0x48 462318c636 [Fix] (GL): a detached shader leaves the GL-visible attach list at once, and re-attaching it before the next link is legal. 2026-10-06 10:14:53 -04:00
swung0x48 c3af0719f0 [Feat] (GL): implement GL_ARB_shader_objects over the core's shared shader / program name space. 2026-10-06 10:11:19 -04:00
swung0x48 c641ed92fb [Docs] (P13 W4b): record the Adreno 750 measurement of the copy-then-refused-readback fix. 2026-10-06 10:02:55 -04:00
swung0x48 3b441531b7 [Fix] (P13 W5 S1): PipeApply.cpp includes the seam header in every build. 2026-10-06 09:52:54 -04:00
swung0x48 534f14e3f2 [Feat] (P13 W5 S1): the fatal vocabulary moves to MG_Pipe and the record arm gets its own fail / latch seam. 2026-10-06 09:48:58 -04:00
swung0x48 ec92ae4d08 [Docs] (P13 W5 S0): the W5 design, its inventory and the pre-move measurements. 2026-10-06 09:45:22 -04:00
swung0x48 5fdc1e46e2 [Docs] (P13 W4b): record the monolith knob lanes and the aliasing store's followed copy. 2026-10-06 09:38:39 -04:00
swung0x48 cfd6c8730b [Fix] (P13 W4b): the aliasing texture store follows glCopyImageSubData into a server-owned destination. 2026-10-06 09:29:11 -04:00
swung0x48 4f5b4fa25d [Test] (P13 W4b): register the CPU-mip and store-read knob lanes on monolith's record arm. 2026-10-06 09:25:30 -04:00
swung0x48 96c3aff78f [Docs] (P13): Magma's monolith flip and the W4d part that landed 2026-10-06 09:16:57 -04:00
swung0x48 88a11b812f [Feat] (P13 W4d): monolith's record arm takes the context values from set_context_values instead of the residual fill 2026-10-06 09:00:52 -04:00
swung0x48 f3a2fd9308 [Feat] (P13 W4): Magma monolith takes the record arm in one flip, with in-process persistent-map donation 2026-10-06 08:59:53 -04:00
swung0x48 c86be0c274 [Docs] (P13): log the W3 follow-up fixes and the Espryt W4 state 2026-10-06 08:21:51 -04:00
swung0x48 2af4728e70 [Feat] (P13 W4b/W4c): Espryt monolith takes the record arm for textures, readbacks, framebuffers, units, images, programs and XFB 2026-10-06 08:14:54 -04:00
swung0x48 a7409e1e9a [Docs] (P13): progress through W3c and how W4a switched Espryt's monolith arm 2026-10-06 08:14:38 -04:00
swung0x48 af84eac8ee [Feat] (P13 W4a): Espryt monolith takes the record arm for buffers, indirect, client arrays, binding points and fp64 through an in-process verb port 2026-10-06 08:14:37 -04:00
swung0x48 081854653a [Feat] (P13 W4): MG_Config::DataArmIsRecord and the W4-W6 MOBILEGL_PIPE_DATA_ARM knob 2026-10-06 08:14:36 -04:00
swung0x48 a32ca0017d [Test] (P13 W3b): the HandleRecycle Handles lanes run their leak cases again on the fixed-on mask, and the retired residual row leaves the census baseline 2026-10-06 08:14:28 -04:00
swung0x48 be86ca175e [Fix] (P13 W3c): the retired wire row dies through SessionFail, and the field-ownership note keeps its generated text 2026-10-06 06:50:18 -04:00
swung0x48 c0404018ec [Test] (P13 W3b): a refused-mask control globs only its own logs, not a longer-named sibling's 2026-10-06 06:19:51 -04:00
swung0x48 6cb6fa88f1 [CI] (P13): the skip census keys parameterised cases without their printed GetParam bytes 2026-10-06 05:28:55 -04:00
swung0x48 7643822606 [Docs] (CI): log the spawn, FencePoll and log-forward flake fixes 2026-10-06 05:23:59 -04:00
swung0x48 75a9ea3f98 [Test] (MG_Remote): bound the F2 flood's notice count by its records, not by one runner's socket growth 2026-10-06 05:21:58 -04:00
swung0x48 e35965d187 [Test] (MG_Remote): park the log-forward sender before the stall flood so one gap stays one notice 2026-10-06 05:17:45 -04:00
swung0x48 cef29781c5 [Test] (P13 W4a): the readback positive control applies where the words come from a shadow 2026-10-06 04:37:28 -04:00
swung0x48 520bafe823 [Docs] (P13): handoff note for the state after W3c 2026-10-06 04:29:15 -04:00
swung0x48 3e2aff1d1d [Fix] (P13 W4a): monolith indirect draws stop reading the command buffer back when no attribute is client memory 2026-10-06 04:27:33 -04:00
swung0x48 883b861490 [CI] (P13 W3b): the restored unit-memo probe is back in the skip-census baseline 2026-10-06 04:23:44 -04:00
swung0x48 938f4b111f [Docs] (CI): record the unit-memo probe's red check and why it was restored 2026-10-06 04:23:35 -04:00
swung0x48 3e36bd6c56 [Test] (P13 W3b): restore the borrowed-slot unit-memo probe, pinned to the mask whose sync body it observes 2026-10-06 04:21:32 -04:00
swung0x48 1d380a6ede [CI] (P13 W3b/W3c): retire the removed lanes' and tests' names from the skip-census baseline 2026-10-06 04:12:33 -04:00
swung0x48 4d2a244382 [CI] (P13 W3b): link ratchet at 171, the VAO attribute-version getter left the server image 2026-10-06 04:12:17 -04:00
swung0x48 daf2b44c71 [Test] (P13 W3b): a GL-level successor for the borrowed-slot unit-memo probe 2026-10-06 04:10:51 -04:00
swung0x48 e2c9098648 [Refactor] (P13 W3c): delete ResidualValueBlock, op 46 becomes a retired declined row and the verify comparator owns the capability check 2026-10-06 04:04:51 -04:00
swung0x48 a4d10dd9b6 [CI] (P13 W3b): retired-switch hygiene gate and the fatal census at its new floor 2026-10-06 04:04:50 -04:00
swung0x48 640c73243d [Test] (P13 W3b): retire the two P2-mask texture-sync probes and log every W3b removal 2026-10-06 04:04:49 -04:00
swung0x48 06362251b0 [Test] (P13 W3b): SanityTest runs the fixed-on subsystem mask 2026-10-06 04:04:49 -04:00
swung0x48 b9c1291f10 [Test] (P13 W3b): the subsystem Off and Refused lanes become named-refusal controls, the On lanes run the full mask 2026-10-06 04:04:48 -04:00
swung0x48 30ebae4346 [Feat] (P13 W3b): subsystem bits 0-13 are fixed on and refused by name at startup, the legacy-memo and texel-retain knobs are gone 2026-10-06 04:04:48 -04:00
swung0x48 5e547add80 [CI] (P13 W3b): memo-key purity has no exempted arm once the legacy memos are gone 2026-10-06 04:04:47 -04:00
swung0x48 a0bff7cee1 [Refactor] (P13 W3b): backends read MG_Pipe::gPipeInputs directly, MGB_CTX and its switch header are gone 2026-10-06 04:04:46 -04:00
swung0x48 fc57a79bf1 [Build] (P13 W3b): no MOBILEGL_PIPE_PUSH macro or CMake variable, drop the HandleRecycle Legacy and AbaControl lanes 2026-10-06 04:04:46 -04:00
swung0x48 915c87056c [Test] (P13 W3b): strip the push/legacy-memo guards and the pull skip stubs from the tests 2026-10-06 04:04:45 -04:00
swung0x48 943aac6afb [Refactor] (P13 W3b): strip the push/legacy-memo guards from both backends 2026-10-06 04:04:44 -04:00
swung0x48 b981244672 [Refactor] (P13 W3b): strip the push/legacy-memo guards from MG_Impl, MG_Pipe and MG_Remote 2026-10-06 04:04:44 -04:00
swung0x48 7184a40a47 [Refactor] (P13 W3b): strip the push/legacy-memo guards from MG_State, MG_Util and the config 2026-10-06 04:04:43 -04:00
swung0x48 9e14c16d4c [Build] (P13 W3b): retire MOBILEGL_PIPE_LEGACY_MEMOS, the pre-handle memo arm is no longer compiled 2026-10-06 04:04:42 -04:00
swung0x48 55de98a006 [Build] (P13 W3a): MOBILEGL_PIPE_PUSH is no longer an option, the macro stays 1 until W3b 2026-10-06 04:04:42 -04:00
swung0x48 3e11c848e8 [Docs] (P13): progress table and the last G1 reading 2026-10-06 04:04:41 -04:00
swung0x48 9f05e66591 [CI] (P13 W2): retire the pull control library, the transport control runs the push library without MG_Remote 2026-10-06 04:04:41 -04:00
swung0x48 68b56165da [Build] (P13 W2): push by default, drop the Gradle pipePush switch so FCL embeds the push monolith 2026-10-06 04:04:40 -04:00
swung0x48 0a47038ae3 [Test] (MG_Remote): read the fence counters before the fence exists, its report can beat a later snapshot 2026-10-06 04:04:39 -04:00
swung0x48 c6cb5dea68 [CI] (P13 W1): skip census baseline from run 37425662897, deduped name sets and a quiet self-test 2026-10-06 04:04:39 -04:00
swung0x48 5ff33cbb77 [CI] (P13 W1): push-without-MG_Remote control and zero-MG_Remote check, monolith means no transport, skip census 2026-10-06 02:39:49 -04:00
swung0x48 138a14019a [Docs] (P13): record the integrator decisions 2026-10-06 02:39:48 -04:00
swung0x48 c9085533a1 [Fix] (trace-replay): bump apitrace to resolve typed shader/program names through their own maps 2026-10-06 02:33:15 -04:00
swung0x48 0cbb1a84d8 [Fix] (MG_Remote): give a spawned server's Welcome the cold-start budget 2026-10-06 02:33:14 -04:00
swung0x48 cde6512b08 [Fix] (CI): avoid IN_LIST in a CTest include and move loopback TCP test ports below the ephemeral range 2026-10-06 02:33:13 -04:00
swung0x48 959f518669 [Docs] (P13): audit and wave plan for retiring the pull path 2026-10-06 00:40:05 -04:00
swung0x48 aee0632a82 [CI] Move Test runs to a new concurrency group so they skip the stuck self-hosted run 2026-10-05 22:45:29 -04:00
swung0x48 1d1cfa6603 [CI] Temporarily run all workflows on GitHub-hosted runners 2026-10-05 11:32:08 -04:00
swung0x48 d409731632 [Fix] (CI): restore hosted builds and pin CMake 3.31.10 2026-10-05 10:41:03 -04:00
swung0x48 039b342b97 [Fix] (CI): separate CPU builds from GPU stages and repair CTest metadata 2026-10-05 10:00:48 -04:00
swung0x48 4eee6ae251 [Fix] (CI): remove empty retrace environment mapping 2026-10-05 09:19:03 -04:00
swung0x48 ee9919ba39 [Fix] (CI): merge Arch WSL runner and prefer it over hosted runners 2026-10-05 09:16:06 -04:00
swung0x48 24889447cc [Fix] (CI): simplify integration job display names 2026-10-05 08:55:52 -04:00
swung0x48 d8edc8baa2 [Fix] (MG_IntegrationTest): validate specialized controls and classify strict skips 2026-10-05 08:45:28 -04:00
441 changed files with 51898 additions and 41499 deletions
@@ -108,7 +108,7 @@ Installed by `xdeploy.sh` (and anland's `anland-build-client.sh`), so any proces
Precedence is environment > `/etc/mobilegl/backend` (backend only) > client.conf > built-in; `MOBILEGL_CONFIG_FILE` / `MOBILEGL_BACKEND_FILE` name other files (empty = off); a server process never reads them.
**Fallback.** Before bringing anything up the client probes the configured endpoint (one non-blocking connect, `MOBILEGL_IPC_PROBE_TIMEOUT_MS`, default 250, 0 = off; a refused abstract socket answers in ~0.2 ms). With no server: the EGL vendor returns `EGL_NO_DISPLAY` and no devices (glvnd moves on to the other vendor), the GLX vendor fails `__glx_Main` (libGLX falls back to the vendor Xwayland names), and `mobilegl_gbm` fails `create_device` (libgbm falls back to the device's own backend). Client queries (`eglQueryString(EGL_NO_DISPLAY)`, `eglGetProcAddress`) never bring a session up. MobileGL also steps aside for a GBM device another backend created and for `LIBGL_ALWAYS_SOFTWARE=1` - that is how a software Xwayland (`ANLAND_XWAYLAND_GLAMOR=0`, KWin's patch sets it then) keeps the software stack. MobileGL never opens an X connection to a display its own process serves (`MG_Util/X11/DisplayGuard.h`; `MOBILEGL_X11_DIAL=0` forbids all), which is what made a glamor Xwayland deadlock on itself before. Show it without stopping the real server:
**Fallback.** Before bringing anything up the client probes the configured endpoint (one non-blocking connect, `MOBILEGL_IPC_PROBE_TIMEOUT_MS`, default 250, 0 = off; a refused abstract socket answers in ~0.2 ms). With no server: the EGL vendor returns `EGL_NO_DISPLAY` and no devices (glvnd moves on to the other vendor), the GLX vendor fails `__glx_Main` (libGLX falls back to the vendor Xwayland names), and `mobilegl_gbm` fails `create_device` (libgbm falls back to the device's own backend). Client queries (`eglQueryString(EGL_NO_DISPLAY)`, `eglGetProcAddress`) never bring a session up. MobileGL also steps aside for a GBM device another backend created. It no longer steps aside for `LIBGL_ALWAYS_SOFTWARE=1` (918d40dd): that is Mesa's software-renderer selection, not a reason to disable MobileGL's entry points, so a process that sets it (a software Xwayland included) is still served by MobileGL while a server answers. MobileGL never opens an X connection to a display its own process serves (`MG_Util/X11/DisplayGuard.h`; `MOBILEGL_X11_DIAL=0` forbids all), which is what made a glamor Xwayland deadlock on itself before. Show it without stopping the real server:
```sh
sed 's|^MOBILEGL_IPC_CONTROL=.*|MOBILEGL_IPC_CONTROL=unix:@nobody|' /etc/mobilegl/client.conf > /tmp/down.conf
sudo -u swung0x48 env -i HOME=/home/swung0x48 PATH=/usr/bin XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-0 \
@@ -0,0 +1,41 @@
name: Configure Test runner
description: Select the installed compiler and graphics stack without modifying the host
inputs:
gpu:
description: Require the native GPU stack when running on 8845
default: "false"
runs:
using: composite
steps:
- shell: bash
env:
NEED_GPU: ${{ inputs.gpu }}
run: |
if [ "$RUNNER_ENVIRONMENT" = "self-hosted" ] && [ "$NEED_GPU" = true ]; then
for tool in clang clang++ cmake ninja ccache; do command -v "$tool"; done
test -r /usr/share/vulkan/icd.d/dzn_icd.json
test -e /dev/dxg
{
echo 'MOBILEGL_CI_NATIVE_GPU=1'
echo 'MOBILEGL_CI_CC=clang'
echo 'MOBILEGL_CI_CXX=clang++'
echo 'MOBILEGL_CI_LLD=ld.lld'
echo 'MOBILEGL_CI_SOFTWARE_RENDERING=OFF'
echo 'VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/dzn_icd.json'
echo 'VK_DRIVER_FILES=/usr/share/vulkan/icd.d/dzn_icd.json'
echo 'EGL_PLATFORM=surfaceless'
echo 'LIBGL_ALWAYS_SOFTWARE=0'
echo 'GALLIUM_DRIVER=d3d12'
echo 'MESA_LOADER_DRIVER_OVERRIDE=d3d12'
} >> "$GITHUB_ENV"
else
{
echo 'MOBILEGL_CI_NATIVE_GPU=0'
echo 'MOBILEGL_CI_CC=clang-20'
echo 'MOBILEGL_CI_CXX=clang++-20'
echo 'MOBILEGL_CI_LLD=ld.lld-20'
echo 'MOBILEGL_CI_SOFTWARE_RENDERING=ON'
echo 'VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.json'
echo 'VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json'
} >> "$GITHUB_ENV"
fi
+1 -1
View File
@@ -46,7 +46,7 @@ require 'DirectVulkan' "$plugin_resource_text" 'V2 DirectVulkan option'
require 'key:MOBILEGL_DISABLE_TIMERQUERY' "$plugin_resource_text" 'V2 timer-query 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_FRAMESINFLIGHT' "$plugin_resource_text" 'V2 Magma frames-in-flight setting'
require 'key:MOBILEGL_FRAMES_IN_FLIGHT' "$plugin_resource_text" 'V2 frames-in-flight setting'
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_ESPRYT_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
+49 -7
View File
@@ -76,12 +76,25 @@ jobs:
${{ runner.os }}-apk-${{ github.job }}-ccache-
- name: Install ccache
timeout-minutes: 45
run: |
sudo apt-get update
sudo apt-get install -y ccache
ccache --version
- name: Setup Android SDK
id: netsetup84
continue-on-error: true
timeout-minutes: 15
uses: android-actions/setup-android@v4
with:
accept-android-sdk-licenses: false
packages: platform-tools
cmdline-tools-version: 13114758
- name: Setup Android SDK (retry)
timeout-minutes: 15
if: ${{ steps.netsetup84.outcome == 'failure' }}
uses: android-actions/setup-android@v4
with:
accept-android-sdk-licenses: false
@@ -92,8 +105,13 @@ jobs:
run: yes | sdkmanager --licenses >/dev/null
- name: Install Android NDK
timeout-minutes: 30
run: |
sdkmanager "ndk;27.3.13750724"
for attempt in 1 2 3; do
sdkmanager "ndk;27.3.13750724" && break
[ "$attempt" = 3 ] && exit 1
echo "sdkmanager attempt $attempt failed; retrying"; sleep 20
done
echo "ndk.dir=$ANDROID_HOME/ndk/27.3.13750724" >> android-plugin/local.properties
- name: Update glslang external sources
@@ -106,7 +124,6 @@ jobs:
-Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO
-Pmobilegl.buildDisaggregated="${MOBILEGL_CI_DISAGGREGATED}"
-Pmobilegl.buildDisaggregatedInproc="${MOBILEGL_CI_DISAGGREGATED}"
-Pmobilegl.pipePush="${MOBILEGL_CI_DISAGGREGATED}"
--parallel --max-workers "$(nproc)"
env:
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
@@ -155,7 +172,6 @@ jobs:
-Pmobilegl.debuggableRelease=true -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO
-Pmobilegl.buildDisaggregated="${MOBILEGL_CI_DISAGGREGATED}"
-Pmobilegl.buildDisaggregatedInproc="${MOBILEGL_CI_DISAGGREGATED}"
-Pmobilegl.pipePush="${MOBILEGL_CI_DISAGGREGATED}"
--parallel --max-workers "$(nproc)"
env:
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
@@ -336,6 +352,18 @@ jobs:
uses: actions/checkout@v6
- name: Setup Android SDK
id: netsetup336
continue-on-error: true
timeout-minutes: 15
uses: android-actions/setup-android@v4
with:
accept-android-sdk-licenses: false
packages: platform-tools
cmdline-tools-version: 13114758
- name: Setup Android SDK (retry)
timeout-minutes: 15
if: ${{ steps.netsetup336.outcome == 'failure' }}
uses: android-actions/setup-android@v4
with:
accept-android-sdk-licenses: false
@@ -415,6 +443,18 @@ jobs:
echo "EMULATOR_PID_FILE=${RUNNER_TEMP}/mobilegl-emulator.pid" >> "$GITHUB_ENV"
- name: Setup Android SDK
id: netsetup415
continue-on-error: true
timeout-minutes: 15
uses: android-actions/setup-android@v4
with:
accept-android-sdk-licenses: false
packages: platform-tools
cmdline-tools-version: 13114758
- name: Setup Android SDK (retry)
timeout-minutes: 15
if: ${{ steps.netsetup415.outcome == 'failure' }}
uses: android-actions/setup-android@v4
with:
accept-android-sdk-licenses: false
@@ -492,10 +532,11 @@ jobs:
# MOBILEGL_IPC_SERVER_PATH IS DELIBERATELY ABSENT. nativeLibraryDir carries an
# install-time hash, so nothing off the device can spell it; TraceReplayActivity
# resolves it from getApplicationInfo() when the env asks for spawn, and an explicit
# value would override that. --use-pbuffer is NOT optional here: until P12 a spawned
# server cannot be handed a window at all, because an ANativeWindow* is a pointer
# value would override that. --use-pbuffer is NOT optional here: a spawned
# server cannot be handed the client's window, because an ANativeWindow* is a pointer
# into the CLIENT's process and SetWindowHandle is refused on the way across with
# Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"} (Rule H).
# Fatal{UnmigratedSurface, "AndroidNativeWindow:use-MOBILEGL_IPC_SURFACE=server"} (Rule H, permanent;
# a windowed client uses MOBILEGL_IPC_SURFACE=server instead).
extra_retrace_args+=(
--env "MOBILEGL_TRANSPORT=spawn;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1;MOBILEGL_GPU_HANG_BUDGET_MS=${MOBILEGL_GPU_HANG_BUDGET_MS}"
--use-pbuffer
@@ -735,7 +776,8 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: |
# Fail before deletion if either paginated inventory cannot be read.
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100" \
# 50 rows per page: the jobs endpoint answers HTTP 502 for 90+ rows on a re-run attempt of a run this size.
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=50" \
--jq '.jobs[] | select(.name | startswith("retrace (")) | select(.conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out" or .conclusion == "action_required") | .name' \
> "${RUNNER_TEMP}/cleanup-jobs.txt"
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" \
+790 -502
View File
File diff suppressed because it is too large Load Diff
-31
View File
@@ -1,31 +0,0 @@
name: Arch WSL GPU smoke
on:
workflow_dispatch:
push:
branches:
- ci/arch-wsl-gpu-runner
paths:
- .github/workflows/wsl-gpu-smoke.yml
- docs/ci/arch-wsl-gpu-runner.md
permissions: {}
concurrency:
group: arch-wsl-gpu-smoke
cancel-in-progress: false
jobs:
gpu-smoke:
runs-on: [self-hosted, Linux, X64, arch, wsl, gpu, gles, vulkan]
timeout-minutes: 5
steps:
- name: Report runner identity
shell: bash
run: |
printf 'Runner: %s\n' "$RUNNER_NAME"
uname -n
uname -a
- name: Verify hardware GLES and Vulkan
shell: bash
run: /usr/local/libexec/mobilegl-runner/gpu-smoke
+249 -135
View File
@@ -31,18 +31,25 @@ option(MOBILEGL_BUILD_DISAGGREGATED_INPROC "Compile the in-process (one-process,
option(MOBILEGL_BUILD_SERVER_SPIKE "Build the P0 spike-A MobileGLServer delivery-chain executable (Android only)" OFF)
option(MOBILEGL_BUILD_EXTMEM_PROBE "Package tools/spikes/extmem_probe as libMobileGLExtmemProbe.so for the trace APK's exec hook (P11 A3, Android only)" OFF)
option(MOBILEGL_EXTMEM_PROBE_AS_SERVER "Spike only (P11 B2 step 1): also package the probe as libMobileGLServer.so, the real server as libMobileGLServerReal.so" 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)
# The PipeInputs strangler (ARCHITECTURE.md 9.2) is finished: since P13 every build pushes, and
# there is neither an option nor a macro for it (W3a removed the option, W3b the `#if` guards and
# the definition). A -DMOBILEGL_PIPE_PUSH=OFF (or the OFF that every pre-P13 build tree holds in its
# cache, the old default) is named and dropped rather than honoured: a warning, not an error, so an
# existing tree reconfigures. The RUNTIME MOBILEGL_PIPE_PUSH environment bitmask is unrelated.
if (DEFINED CACHE{MOBILEGL_PIPE_PUSH} AND NOT "$CACHE{MOBILEGL_PIPE_PUSH}")
message(WARNING "MobileGL: MOBILEGL_PIPE_PUSH=OFF asks for the pull build, which P13 retired; "
"ignored - every build pushes now")
endif()
unset(MOBILEGL_PIPE_PUSH CACHE)
option(MOBILEGL_PIPE_VERIFY "Compile SnapshotFromGLContext() and the G4 per-verb shadow comparator; never shipped" OFF)
# Track H's old-versus-new arm (ARCHITECTURE.md 9.6) - MOBILEGL_PIPE_LEGACY_MEMOS - retired with the
# pull path in P13 W3b: the pre-handle memo arm is no longer compiled anywhere. Named and dropped,
# like MOBILEGL_PIPE_PUSH=OFF above (every pre-P13 tree holds the old default ON in its cache).
if (DEFINED CACHE{MOBILEGL_PIPE_LEGACY_MEMOS} AND "$CACHE{MOBILEGL_PIPE_LEGACY_MEMOS}")
message(WARNING "MobileGL: MOBILEGL_PIPE_LEGACY_MEMOS=ON asks for the pre-handle memo arm, "
"which P13 retired; ignored")
endif()
unset(MOBILEGL_PIPE_LEGACY_MEMOS CACHE)
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")
@@ -111,12 +118,15 @@ if (ANDROID)
"(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)
# A higher level is a separate artifact of the SAME source (gradle -Pmobilegl.androidApi=29:
# native ELF TLS instead of emulated TLS). Any post-${MOBILEGL_ANDROID_API_LEVEL} API use must
# sit behind a compile-time __ANDROID_API__ gate and never replace the API
# ${MOBILEGL_ANDROID_API_LEVEL} path; the minSdk-${MOBILEGL_ANDROID_API_LEVEL} build stays the
# enforcing compile.
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.")
"(> shipping minimum ${MOBILEGL_ANDROID_API_LEVEL}): a separate artifact; "
"post-${MOBILEGL_ANDROID_API_LEVEL} APIs only behind a compile-time __ANDROID_API__ gate.")
endif()
message(STATUS "MobileGL: Android API level ${_mobilegl_android_api}")
@@ -126,13 +136,14 @@ if (ANDROID)
unset(_mobilegl_api_triple)
endif()
option(MOBILEGL_ENABLE_LTO "Build with ThinLTO/IPO" OFF)
option(MOBILEGL_ENABLE_LTO "Build with ThinLTO/IPO and the release optimization flags" OFF)
if ((NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT) AND MOBILEGL_ENABLE_LTO)
# Check if ThinLTO or LTO is suppported
include(CheckIPOSupported)
include(CheckCCompilerFlag)
include(CheckCXXCompilerFlag)
include(CheckLinkerFlag)
check_ipo_supported(RESULT LTOSupported OUTPUT LTOError)
@@ -157,11 +168,49 @@ if ((NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT) AND MO
message(STATUS "IPO not supported: ${LTOError}")
endif()
if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" AND NOT MATCHES "AppleClang")
add_compile_options(-O3 -ffunction-sections -fdata-sections)
add_link_options(-Wl,--gc-sections)
elseif (CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
# add_compile_options(/O2)
# Every flag below is probed first, so a compiler or linker without it simply goes without.
# add_compile_options/add_link_options set directory properties, so they act from a function.
function(mobilegl_add_compile_flag_if_supported flag)
string(MAKE_C_IDENTIFIER "MGL_HAS_C${flag}" c_var)
string(MAKE_C_IDENTIFIER "MGL_HAS_CXX${flag}" cxx_var)
check_c_compiler_flag("${flag}" ${c_var})
check_cxx_compiler_flag("${flag}" ${cxx_var})
if (${c_var} AND ${cxx_var})
add_compile_options("${flag}")
endif()
endfunction()
function(mobilegl_add_link_flag_if_supported flag)
string(MAKE_C_IDENTIFIER "MGL_HAS_LINK${flag}" link_var)
check_linker_flag(CXX "${flag}" ${link_var})
if (${link_var})
add_link_options("${flag}")
endif()
endfunction()
if (MSVC)
# One COMDAT per function (/Gy) and per datum (/Gw), so /OPT:REF can drop the unreferenced
# ones and /OPT:ICF fold the identical ones - the sections/gc/icf trio below, MSVC-spelled.
add_compile_options(/Gy /Gw)
add_link_options(/OPT:REF /OPT:ICF)
elseif (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
add_compile_options(-O3)
mobilegl_add_compile_flag_if_supported(-ffunction-sections)
mobilegl_add_compile_flag_if_supported(-fdata-sections)
# Hidden visibility and -Bsymbolic-functions already keep the library's calls inside it;
# this lets the optimizer rely on that and inline across what it would otherwise have to
# treat as interposable definitions.
mobilegl_add_compile_flag_if_supported(-fno-semantic-interposition)
if (APPLE)
mobilegl_add_link_flag_if_supported(LINKER:-dead_strip)
elseif (NOT WIN32)
mobilegl_add_link_flag_if_supported(LINKER:--gc-sections)
# Calls into libc and the GPU driver load their target from the GOT instead of going
# through a PLT stub. Bionic binds everything at load time anyway.
mobilegl_add_compile_flag_if_supported(-fno-plt)
# Identical code folding (lld, gold). Safe mode never folds a function whose address
# is taken, so function-pointer identity holds.
mobilegl_add_link_flag_if_supported(LINKER:--icf=safe)
endif()
else ()
add_compile_options(-O2)
endif()
@@ -421,6 +470,7 @@ set(SOURCE_FILES
MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp
MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp
MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp
MobileGL/MG_Impl/GLImpl/Program/GL_ShaderObjectsARB.cpp
MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp
MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp
MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp
@@ -545,63 +595,34 @@ if (MOBILEGL_BUILD_DISAGGREGATED AND
set(MOBILEGL_BUILD_DISAGGREGATED_INPROC OFF)
endif()
# MOBILEGL_BUILD_DISAGGREGATED implies MOBILEGL_PIPE_PUSH (P5 c0). The split path IS the
# pushed path: MG_Remote's server decodes records into the MGPipeApply* entry points, which
# live in MG_Pipe/PipeApply.cpp, which the PIPE_PUSH block below is what compiles. Without
# this, `-DMOBILEGL_BUILD_DISAGGREGATED=ON` alone configures and then fails to link the
# applier - and the shape it fails in (MG_Remote compiled, no applier) is indistinguishable
# at the CMake level from a legitimate transport-only build, which is why it is stated here
# rather than left to whoever hits the link error. Same normal-variable form as the two
# implications above.
if (MOBILEGL_BUILD_DISAGGREGATED AND NOT MOBILEGL_PIPE_PUSH)
message(STATUS "MobileGL: MOBILEGL_BUILD_DISAGGREGATED=ON forces MOBILEGL_PIPE_PUSH ON for "
"this configure: the split path decodes into the MGPipe applier, and the "
"applier is what MOBILEGL_PIPE_PUSH compiles")
set(MOBILEGL_PIPE_PUSH ON)
endif()
# P13 W5 (notes/p13/W5-DESIGN.md): the record arm is part of every library - the one without
# MG_Remote (FCL's) runs it too. Its temporary guard, MOBILEGL_BUILD_RECORD_ARM, was folded away at
# S8 (scripts/ci/retired_switches.py keeps it from coming back).
# 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
# P4a's contract: the reflection-archive serializer over ProgramArtifacts.h's
# VisitFields tables. Push-only for the same G1 reason as the three above - in
# monolith the archive never crosses (create_shader_state hands the two structs over
# by pointer beside the record), so the codec is live code only in the VERIFY lane,
# where the applier serialises, deserialises and field-compares before storing.
MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp
# P5's contract (integrator ruling R-17, package c1): the MONOLITH arm of the
# client->wire routing - the thirty-seven adapters that install gMGPipeScreen /
# gMGPipeContext over the MGPipeApply* entry points, and the reply mailbox the four
# acceptance rows answer through. Push-only for the same G1 reason as the five above:
# the two tables are inline variables that are zero in a pull build and nothing there
# can reach a thunk.
MobileGL/MG_Pipe/PipeRoute.cpp
)
endif()
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
# P4a's contract: the reflection-archive serializer over ProgramArtifacts.h's
# VisitFields tables. Push-only for the same G1 reason as the three above - in
# monolith the archive never crosses (create_shader_state hands the two structs over
# by pointer beside the record), so the codec is live code only in the VERIFY lane,
# where the applier serialises, deserialises and field-compares before storing.
MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp
# P5's contract (integrator ruling R-17, package c1): the MONOLITH arm of the
# client->wire routing - the thirty-seven adapters that install gMGPipeScreen /
# gMGPipeContext over the MGPipeApply* entry points, and the reply mailbox the four
# acceptance rows answer through. Push-only for the same G1 reason as the five above:
# the two tables are inline variables that are zero in a pull build and nothing there
# can reach a thunk.
MobileGL/MG_Pipe/PipeRoute.cpp
)
if (MOBILEGL_BUILD_DISAGGREGATED)
message(STATUS "MobileGL: disaggregated transport ON, appending MG_Remote sources")
@@ -670,7 +691,8 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
# MarkGpuWritten producers are on the server's side of the line, and the
# block-granularity persistent-map push that tier T2 makes mandatory.
MobileGL/MG_Remote/Client/GpuWritePending.cpp
MobileGL/MG_Remote/Client/PersistentMapTracker.cpp
# P13 W5: the adoption tier's verdict (the tracker itself moved to the record arm below).
MobileGL/MG_Remote/Client/AdoptTierChoice.cpp
MobileGL/MG_Remote/Server/ServerSession.cpp
MobileGL/MG_Remote/Server/PipeApplier.cpp
MobileGL/MG_Remote/Server/ServerLoop.cpp
@@ -698,14 +720,25 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
MobileGL/MG_Remote/Server/ServerDisplay.cpp
# Package-neutral C ABI for an APK that owns the server and its ANativeWindow.
MobileGL/MG_Remote/Server/EmbeddedServer.cpp
# P7 gate 5 (g5-msprobe): the Magma wire arm's two multisample depth/stencil resolve
# arms and the probe that measures the first against the second, shared by the
# renderer's bring-up and the Vulkan POST. Wire-only - the monolith records neither arm -
# so it stays out of the pull build (gate G1).
MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.cpp
)
endif()
# P13 W5: the record arm's own sources, compiled into every library (MG_Remote or not).
list(APPEND SOURCE_FILES
# P7 gate 5 (g5-msprobe): the Magma wire arm's two multisample depth/stencil resolve
# arms and the probe that measures the first against the second, shared by the
# renderer's bring-up and the Vulkan POST.
MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.cpp
# P5 b1: the block-granularity persistent-map push the record arm makes mandatory (T2).
MobileGL/MG_State/GLState/BufferState/PersistentMapTracker.cpp
# P13 W5: the verb emitters and the monolith verb port, and the client GPU-write set
# they mark before a draw or a dispatch (MG_Record; EmitTables.cpp resolves the session).
MobileGL/MG_Impl/Pipe/Verb/VerbPort.cpp
MobileGL/MG_Impl/Pipe/Verb/GpuWriteSet.cpp
# The record verbs' consumer (ServerVerbSink under a transport is this plus the session's).
MobileGL/MG_Backend/Record/RecordVerbSink.cpp
)
if (APPLE AND NOT MOBILEGL_IOS)
list(APPEND SOURCE_FILES
MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp
@@ -768,15 +801,9 @@ 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}")
@@ -802,13 +829,87 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
# 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)
# The vendored glvnd vendor-ABI headers (3rdparty/glvnd): the entry point a vendor library
# exposes so the system libEGL - which IS glvnd - can dispatch to it.
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/glvnd/include)
endif()
# The vendored glvnd vendor-ABI headers (3rdparty/glvnd): the entry point a vendor library exposes so
# the system libEGL - which IS glvnd - can dispatch to it. Every build compiles the two GlvndVendor.cpp
# files, so the path is every build's (P13 W5: the Android library without MG_Remote - FCL's - did not
# find <glvnd/libeglabi.h>; a Linux host only built because its system has the header).
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/glvnd/include)
# ---- P13 W7 (D12): MODULE TARGETS ------------------------------------------------------------
#
# One flat SOURCE_FILES list used to feed MobileGL and MobileGL_s separately (every file compiled
# twice). It is split here into OBJECT libraries along the link ratchet's partition
# (scripts/link_ratchet.py PARTITION, which is the written form of these boundaries):
#
# mg_util MG_Util, Init / GlobalObjects / ConfigLoader, MG_Remote/FatalFunnel SHARED
# mg_pipe MG_Pipe SERVER
# mg_backend MG_Backend (both backends, the record arm's staged stores) SERVER
# mg_frontend MG_Impl + MG_State FRONTEND
# mg_remote_transport MG_Remote/{Transport,Protocol,Wire}, CapsCodec SERVER
# mg_remote_client MG_Remote/Client FRONTEND
# mg_remote_server MG_Remote/Server SERVER
#
# Both MobileGL and MobileGL_s link the same objects. mgl_build_iface carries every compile-side
# usage requirement (include directories, definitions, the link libraries whose headers the
# sources include), so a property set on it reaches every module and both libraries; a property
# that only matters at link time may still be set on the libraries themselves.
add_library(mgl_build_iface INTERFACE)
target_include_directories(mgl_build_iface INTERFACE ${MOBILEGL_INCLUDE_DIR})
target_link_libraries(mgl_build_iface INTERFACE ${MOBILEGL_LINK_LIBRARIES})
target_compile_definitions(mgl_build_iface INTERFACE
${MOBILEGL_COMPILE_DEF}
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
$<$<BOOL:${MOBILEGL_TRACE_ANGLE_VARIANTS}>:MOBILEGL_TRACE_ANGLE_VARIANTS=1>
)
set(MGL_MODULES util pipe backend frontend remote_transport remote_client remote_server)
foreach(MGL_MODULE IN LISTS MGL_MODULES)
set(MGL_MODULE_SOURCES_${MGL_MODULE})
endforeach()
foreach(MGL_SOURCE IN LISTS SOURCE_FILES)
if (MGL_SOURCE MATCHES "^MobileGL/MG_Remote/Client/")
list(APPEND MGL_MODULE_SOURCES_remote_client ${MGL_SOURCE})
elseif (MGL_SOURCE MATCHES "^MobileGL/MG_Remote/Server/")
list(APPEND MGL_MODULE_SOURCES_remote_server ${MGL_SOURCE})
elseif (MGL_SOURCE MATCHES "^MobileGL/MG_Remote/(Transport|Protocol|Wire)/" OR
MGL_SOURCE STREQUAL "MobileGL/MG_Remote/CapsCodec.cpp")
list(APPEND MGL_MODULE_SOURCES_remote_transport ${MGL_SOURCE})
elseif (MGL_SOURCE MATCHES "^MobileGL/MG_(Impl|State)/")
list(APPEND MGL_MODULE_SOURCES_frontend ${MGL_SOURCE})
elseif (MGL_SOURCE MATCHES "^MobileGL/MG_Backend/")
list(APPEND MGL_MODULE_SOURCES_backend ${MGL_SOURCE})
elseif (MGL_SOURCE MATCHES "^MobileGL/MG_Pipe/")
list(APPEND MGL_MODULE_SOURCES_pipe ${MGL_SOURCE})
else()
list(APPEND MGL_MODULE_SOURCES_util ${MGL_SOURCE})
endif()
endforeach()
set(MGL_MODULE_OBJECTS)
set(MGL_SERVER_MODULE_OBJECTS)
foreach(MGL_MODULE IN LISTS MGL_MODULES)
if (NOT MGL_MODULE_SOURCES_${MGL_MODULE})
continue() # MG_Remote's modules are empty without MOBILEGL_BUILD_DISAGGREGATED
endif()
add_library(mg_${MGL_MODULE} OBJECT ${MGL_MODULE_SOURCES_${MGL_MODULE}})
target_link_libraries(mg_${MGL_MODULE} PUBLIC mgl_build_iface)
set_target_properties(mg_${MGL_MODULE} PROPERTIES POSITION_INDEPENDENT_CODE ON)
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
set_target_properties(mg_${MGL_MODULE} PROPERTIES
C_VISIBILITY_PRESET default CXX_VISIBILITY_PRESET default VISIBILITY_INLINES_HIDDEN OFF)
else()
set_target_properties(mg_${MGL_MODULE} PROPERTIES
C_VISIBILITY_PRESET hidden CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON)
endif()
list(APPEND MGL_MODULE_OBJECTS $<TARGET_OBJECTS:mg_${MGL_MODULE}>)
if (NOT MGL_MODULE STREQUAL "frontend" AND NOT MGL_MODULE STREQUAL "remote_client")
list(APPEND MGL_SERVER_MODULE_OBJECTS $<TARGET_OBJECTS:mg_${MGL_MODULE}>)
endif()
endforeach()
add_library(${CMAKE_PROJECT_NAME} SHARED
${SOURCE_FILES}
${MGL_MODULE_OBJECTS}
)
if (WIN32)
@@ -833,21 +934,7 @@ else()
)
endif()
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC
${MOBILEGL_INCLUDE_DIR}
)
target_link_libraries(${CMAKE_PROJECT_NAME}
PUBLIC
${MOBILEGL_LINK_LIBRARIES}
)
target_compile_definitions(${CMAKE_PROJECT_NAME}
PUBLIC
${MOBILEGL_COMPILE_DEF}
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
$<$<BOOL:${MOBILEGL_TRACE_ANGLE_VARIANTS}>:MOBILEGL_TRACE_ANGLE_VARIANTS=1>
)
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC mgl_build_iface)
if(UNIX AND NOT APPLE AND NOT ANDROID)
foreach(MOBILEGL_LOADER_ALIAS
@@ -874,8 +961,8 @@ if(WIN32)
endif()
if(NOT ANDROID)
add_library(${CMAKE_PROJECT_NAME}_s STATIC
${SOURCE_FILES}
add_library(${CMAKE_PROJECT_NAME}_s STATIC
${MGL_MODULE_OBJECTS}
)
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
@@ -892,27 +979,12 @@ if(NOT ANDROID)
)
endif()
target_include_directories(${CMAKE_PROJECT_NAME}_s PUBLIC
${MOBILEGL_INCLUDE_DIR}
)
target_link_libraries(${CMAKE_PROJECT_NAME}_s
PUBLIC
${MOBILEGL_LINK_LIBRARIES}
)
target_compile_definitions(${CMAKE_PROJECT_NAME}_s
PUBLIC
${MOBILEGL_COMPILE_DEF}
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
)
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC mgl_build_iface)
endif()
if (TRACY_ENABLE)
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC Tracy::TracyClient)
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC Tracy::TracyClient)
target_compile_definitions(${CMAKE_PROJECT_NAME} PUBLIC -DTRACY_ENABLE)
target_compile_definitions(${CMAKE_PROJECT_NAME}_s PUBLIC -DTRACY_ENABLE)
target_link_libraries(mgl_build_iface INTERFACE Tracy::TracyClient)
target_compile_definitions(mgl_build_iface INTERFACE -DTRACY_ENABLE)
endif ()
# GLVND, AND WHY THIS IS A LINKER FLAG RATHER THAN A VISIBILITY ONE.
@@ -937,6 +1009,51 @@ if (MOBILEGL_BUILD_DISAGGREGATED AND UNIX AND NOT APPLE AND NOT ANDROID)
add_subdirectory(MobileGL/MG_Gbm)
endif()
# ---- P13 W7 (D12): THE SERVER-ONLY LINK CHECK ---------------------------------------------------
# The server and shared modules linked WITHOUT mg_frontend / mg_remote_client, with --no-undefined:
# the server half has to link from its own half of the tree. The frontend references it still makes
# are the link ratchet's baseline, each a named exemption that resolves to an aborting stub through
# a generated linker response file (scripts/ci/server_link_check.py refuses, before the link, any
# server reference the baseline does not name). Built with the split shape on Linux hosts; never
# installed or loaded. NOT in a verify build: the G4 comparator reads both halves by design (its
# MGPipeVerifyReadHook and the program-artifact codec are frontend code the server half calls), the
# baseline is the shipped split shape's, and a verify build never ships.
if (MOBILEGL_BUILD_DISAGGREGATED AND NOT MOBILEGL_PIPE_VERIFY AND UNIX AND NOT APPLE AND NOT ANDROID)
find_package(Python3 COMPONENTS Interpreter QUIET)
if (Python3_Interpreter_FOUND)
set(MGL_SERVER_LINK_RSP "${CMAKE_BINARY_DIR}/mgl_server_link_exemptions.rsp")
set(MGL_ALL_MODULE_OBJECT_DEPS)
foreach(MGL_MODULE IN LISTS MGL_MODULES)
if (TARGET mg_${MGL_MODULE})
list(APPEND MGL_ALL_MODULE_OBJECT_DEPS $<TARGET_OBJECTS:mg_${MGL_MODULE}>)
endif()
endforeach()
add_custom_command(
OUTPUT "${MGL_SERVER_LINK_RSP}"
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/scripts/ci/server_link_check.py"
--build-dir "${CMAKE_BINARY_DIR}"
--baseline "${CMAKE_SOURCE_DIR}/scripts/data/link_ratchet_baseline.txt"
--out "${MGL_SERVER_LINK_RSP}"
DEPENDS ${MGL_ALL_MODULE_OBJECT_DEPS}
"${CMAKE_SOURCE_DIR}/scripts/ci/server_link_check.py"
"${CMAKE_SOURCE_DIR}/scripts/link_ratchet.py"
"${CMAKE_SOURCE_DIR}/scripts/data/link_ratchet_baseline.txt"
COMMENT "Server-only link check: naming the frontend references the baseline exempts"
VERBATIM)
add_custom_target(mgl_server_link_exemptions DEPENDS "${MGL_SERVER_LINK_RSP}")
add_library(MobileGL_server_linkcheck SHARED
${MGL_SERVER_MODULE_OBJECTS}
tools/server_link_check/Exempt.cpp)
target_link_libraries(MobileGL_server_linkcheck PRIVATE mgl_build_iface)
target_link_options(MobileGL_server_linkcheck PRIVATE
"LINKER:--no-undefined" "@${MGL_SERVER_LINK_RSP}")
set_property(TARGET MobileGL_server_linkcheck APPEND PROPERTY LINK_DEPENDS "${MGL_SERVER_LINK_RSP}")
add_dependencies(MobileGL_server_linkcheck mgl_server_link_exemptions)
else()
message(STATUS "MobileGL: no Python3 interpreter - the server-only link check target is not configured")
endif()
endif()
if (ANDROID)
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
android
@@ -977,7 +1094,7 @@ if (APPLE AND NOT MOBILEGL_IOS)
endif()
if (APPLE AND MOBILEGL_IOS)
target_compile_definitions(${CMAKE_PROJECT_NAME} PUBLIC MOBILEGL_IOS=1 _LIBCPP_DISABLE_AVAILABILITY)
target_compile_definitions(mgl_build_iface INTERFACE MOBILEGL_IOS=1 _LIBCPP_DISABLE_AVAILABILITY)
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
"-framework CoreGraphics"
"-framework Foundation"
@@ -988,7 +1105,6 @@ if (APPLE AND MOBILEGL_IOS)
endif()
if(TARGET ${CMAKE_PROJECT_NAME}_s)
target_compile_definitions(${CMAKE_PROJECT_NAME}_s PUBLIC MOBILEGL_IOS=1 _LIBCPP_DISABLE_AVAILABILITY)
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC
"-framework CoreGraphics"
"-framework Foundation"
@@ -1003,10 +1119,8 @@ endif()
if (NOT ANDROID AND NOT MOBILEGL_IOS)
find_package(Vulkan)
if (Vulkan_FOUND)
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC Vulkan::Vulkan Vulkan::Headers)
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC Vulkan::Vulkan Vulkan::Headers)
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC ${Vulkan_INCLUDE_DIR})
target_include_directories(${CMAKE_PROJECT_NAME}_s PUBLIC ${Vulkan_INCLUDE_DIR})
target_link_libraries(mgl_build_iface INTERFACE Vulkan::Vulkan Vulkan::Headers)
target_include_directories(mgl_build_iface INTERFACE ${Vulkan_INCLUDE_DIR})
endif ()
endif ()
+113 -56
View File
@@ -120,6 +120,37 @@ namespace MobileGL::MG_Config {
// does whenever local_size_x is a multiple of the native width). ForceOff returns
// to the raw driver builtin.
QuirkOverride MagmaDeriveNumSubgroups = QuirkOverride::Auto;
// MOBILEGL_MAGMA_INPASS_CLEAR: a wire-arm glClear / glClearBuffer* whose targets are the
// attachments of the draw pass for the bound framebuffer is recorded inside that pass
// (vkCmdClearAttachments) rather than in a render pass of its own built for the clear.
// Auto is ON; ForceOff restores the standalone clear for every surface.
QuirkOverride MagmaInPassClear = QuirkOverride::Auto;
// MOBILEGL_MAGMA_INPASS_CLEAR_DEFAULT: the same for the default framebuffer. Auto is OFF
// (an unexplained openra regression on Adreno 750, perf-p15/HANDOFF.md); ForceOn enables it.
QuirkOverride MagmaInPassClearDefault = QuirkOverride::Auto;
// MOBILEGL_MAGMA_PASS_CACHE: wire draw passes (render pass, framebuffer, views) are kept
// across frames while their key's image epochs hold. Auto is ON; ForceOff drops every
// cached pass whose last use completed at each frame begin, as before P15.
QuirkOverride MagmaPassCache = QuirkOverride::Auto;
// MOBILEGL_MAGMA_VERTEX_CONVERSION_CACHE: a wire vertex stream that needs a CPU format
// conversion is converted once per frame slot per (buffer content, layout) and reused by
// every draw that names it, instead of once per draw. Auto is ON; ForceOff converts per draw.
QuirkOverride MagmaVertexConversionCache = QuirkOverride::Auto;
// MOBILEGL_MAGMA_WRITE_RENAME: a glBufferSubData into a small wire store the GPU may still
// be reading gives the store a fresh buffer filled from a host shadow, instead of a staged
// copy that ends the render pass. Auto is ON; ForceOff keeps the staged copy.
QuirkOverride MagmaWriteRename = QuirkOverride::Auto;
// MOBILEGL_ESPRYT_VAO_SHADOW: DirectGLES emits a vertex attribute's enable flag, divisor and
// pointer to the driver VAO only when they differ from what that VAO already holds (a
// shadow on the VAO twin). Auto is ON; ForceOff re-emits the whole configuration per walk.
QuirkOverride EsprytVaoShadow = QuirkOverride::Auto;
// MOBILEGL_PIPE_VERTEX_ELEMENTS_DEDUP: a VAO configuration change whose vertex-elements
// record is byte-identical to the one its handle last published (a glBindVertexBuffer
// that only changed the buffer) re-publishes nothing. Auto is ON; ForceOff re-creates.
QuirkOverride PipeVertexElementsDedup = QuirkOverride::Auto;
// P15: the client reuses a bound framebuffer's built set_framebuffer_state while nothing it
// reads has moved (MGPipeFramebufferEmitter::BuildBoundFramebufferState).
QuirkOverride PipeFramebufferStateMemo = 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
@@ -135,8 +166,22 @@ namespace MobileGL::MG_Config {
// draws refused by name), because a driver that accepts invalid SPIR-V may fault the GPU
// into device loss. For diagnosing a driver that copes with a specific module only.
Bool MagmaAllowInvalidSpirv = false;
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
Uint32 MagmaFramesInFlight = 3;
// MOBILEGL_FRAMES_IN_FLIGHT (P15; MOBILEGL_MAGMA_FRAMESINFLIGHT is its deprecated alias):
// how many frames may be queued ahead of the GPU, default 3, both backends.
// - Magma: its frame contexts (frame S+1 reuses S+1-N's), clamped to the surface.
// - Espryt: after presenting S the backend waits for S+1-N to complete (N = 1 is a full
// CPU/GPU sync; N >= 2 is what Android's own queueBuffer throttle already gives).
// - inproc / spawn / TCP: the present credit, how many presents the GL thread may run
// ahead of the server, is derived from it: max(1, N - 2), so the default 3 keeps the
// shipped credit of 1 and N = 4 is credit 2 (one more frame of latency).
Uint32 FramesInFlight = 3;
// MOBILEGL_MAGMA_EXTRA_SWAPCHAIN_IMAGES: swapchain images Magma asks for beyond the
// surface's minimum (the frames-in-flight count still raises the request when it is
// larger), clamped to the surface's maximum. With more images, the one the display has
// just released is reused later, by when its release has usually signaled, so fewer
// frames start their window pass behind it. Each image costs a full-size colour buffer
// plus the default framebuffer's depth/stencil buffer of the same size.
Uint32 MagmaExtraSwapchainImages = 0;
// MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER: GL draws and dispatches Magma records into
// one command buffer before it submits it and continues on a fresh one. 0 = unbounded.
// Drivers back a command buffer with GPU memory that is only returned when the buffer is
@@ -370,6 +415,10 @@ namespace MobileGL::MG_Config {
// 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.
// P13 W3b (ID-P13-3): bits 0-13 are FIXED ON. Their pre-handle arms are gone, so a mask
// that clears one is refused by name at startup (Fatal{PipeSubsystemsFixedOn},
// ConfigLoader.cpp); bit 63 is the one switch left. Everything above about "refused,
// runs its legacy arm" describes code that is unreachable since then and goes in W6.
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
@@ -377,7 +426,6 @@ namespace MobileGL::MG_Config {
// 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
@@ -396,31 +444,9 @@ namespace MobileGL::MG_Config {
// 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
@@ -469,6 +495,42 @@ namespace MobileGL::MG_Config {
NamedPipe = 4, // P6: Windows named pipe (Endpoint = <name>)
};
// P13 W5: THE RECORD ARM'S OWN KNOBS. They kept their MOBILEGL_IPC_* names (logs, CI lanes and
// notes spell them so), but they tune the record arm - the persistent-map push and Magma's wire
// stores - which runs in a library without a transport too, so they are not IpcTable's.
struct RecordArmTable {
// MOBILEGL_IPC_WIRE_DEFERRED_MB (P7 wave 4 M2, ID-P7-32): the SERVER's budget, in MiB,
// for orphaned wire buffer stores - the old VkBuffer every glBufferData that crosses the
// wire leaves behind - that a GPU command recorded but not yet retired may still name.
// Stores no command names are destroyed at once and stores whose last submission has
// retired are destroyed at the next park; this bounds the REST. When the parked bytes
// exceed it after a sweep, the server flushes what it has recorded and waits for it
// (WaitForWireBufferHostAccess's sync point, mid-frame), which retires every one. It is
// not a frame count because a frame is not bounded: a snapshot-exiting pbuffer replay
// delivers one present for 1.3 M calls. The same sync point also fires above a fixed
// 1024 parked stores (VkBufferManager::kWireDeferredCountCeiling), because small
// orphans never reach a byte budget. 0 IS THE NEGATIVE CONTROL for both, not "unlimited
// by design": no forced sync, so a one-frame respecify-and-draw loop grows without bound
// and MagmaWireReclaimScenario's watermark cases must go red.
Uint32 WireDeferredMb = 64;
// MOBILEGL_IPC_PERSISTENT_BLOCK_KB: block granularity of the persistent-map push.
// 0 IS A NEGATIVE CONTROL, NOT "unlimited": it disables the push, and
// PersistentCoherentMapScenario must go RED under it (exit gate E3(a)).
Uint32 PersistentBlockKb = 64;
// MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS: 1 = the persistent-map push ships only
// blocks whose xxHash64 changed since the last push, instead of the whole mapped
// range every verb. 0 restores the whole-range push (A/B control).
Uint32 PersistentHashSuppress = 1;
// MOBILEGL_PIPE_PERSISTENT_MPROTECT (P13 W5): 1 (default) = the persistent-map push finds
// written pages through mprotect and a chained SIGSEGV handler where the platform has
// one (Linux, Android); 0 = no handler is ever installed and every persistent write map
// takes the hash scan. The switch exists for the hosts whose own SIGSEGV use the
// chained handler has not met yet - FCL is a JVM, whose implicit null checks fault on
// purpose - so a bring-up can rule the handler out without a rebuild.
Uint32 PersistentMprotect = 1;
};
extern RecordArmTable RecordArm;
#if MOBILEGL_BUILD_DISAGGREGATED
// Parsed once by MG_ConfigLoader::Init(). Defaults to Monolith even here: building the
// transport in is not the same as using it, and every existing lane of a build-split
@@ -527,20 +589,6 @@ namespace MobileGL::MG_Config {
// stage chunk budget (MGPipeStageChunkBytes, a quarter of this), and a record type with
// no cut is Fatal{RingOverrun, "SEG_STAGE"} rather than allowed to exceed it.
Uint32 StageMb = 32;
// MOBILEGL_IPC_WIRE_DEFERRED_MB (P7 wave 4 M2, ID-P7-32): the SERVER's budget, in MiB,
// for orphaned wire buffer stores - the old VkBuffer every glBufferData that crosses the
// wire leaves behind - that a GPU command recorded but not yet retired may still name.
// Stores no command names are destroyed at once and stores whose last submission has
// retired are destroyed at the next park; this bounds the REST. When the parked bytes
// exceed it after a sweep, the server flushes what it has recorded and waits for it
// (WaitForWireBufferHostAccess's sync point, mid-frame), which retires every one. It is
// not a frame count because a frame is not bounded: a snapshot-exiting pbuffer replay
// delivers one present for 1.3 M calls. The same sync point also fires above a fixed
// 1024 parked stores (VkBufferManager::kWireDeferredCountCeiling), because small
// orphans never reach a byte budget. 0 IS THE NEGATIVE CONTROL for both, not "unlimited
// by design": no forced sync, so a one-frame respecify-and-draw loop grows without bound
// and MagmaWireReclaimScenario's watermark cases must go red.
Uint32 WireDeferredMb = 64;
// MOBILEGL_IPC_SPIN_US: spin before parking on a doorbell, either direction.
Uint32 SpinUs = 50;
// MOBILEGL_IPC_EVENT_WAIT_MS (PH-6, ID-P7-2): the SERVER's patience, in ms, for ONE
@@ -558,14 +606,6 @@ namespace MobileGL::MG_Config {
// spent twice. Read by the server only (ServerSpawn.cpp passes it to a spawned child);
// the lockstep arm (no kCapRunAheadApply, i.e. Magma) never waits and keeps P5C's Fatal.
Uint32 EventWaitMs = 2000;
// MOBILEGL_IPC_PERSISTENT_BLOCK_KB: block granularity of the persistent-map push.
// 0 IS A NEGATIVE CONTROL, NOT "unlimited": it disables the push, and
// PersistentCoherentMapScenario must go RED under it (exit gate E3(a)).
Uint32 PersistentBlockKb = 64;
// MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS: 1 = the persistent-map push ships only
// blocks whose xxHash64 changed since the last push, instead of the whole mapped
// range every verb. 0 restores the whole-range push (A/B control).
Uint32 PersistentHashSuppress = 1;
// MOBILEGL_IPC_BATCH_WAITS: 1 = value-class records (kCtxState / kCtxCso / kCtxObject
// with no reply slot) are published without waiting for their own apply; the barrier
// is taken at the next pull-reading verb (kCtxVerb syncs, queries, screen rows, and
@@ -677,14 +717,19 @@ namespace MobileGL::MG_Config {
// client-filled gPipeInputs block for every verb, and run-ahead is precisely the
// arm that stops filling it.
Uint32 RunAhead = 1;
// MOBILEGL_IPC_PRESENT_CREDIT (P5e, ruling 4 / ID-92): how many presents the client may
// have in flight before it waits for a swap to come back. 1 = the client publishes
// frame N+1's records while the server applies and swaps frame N - one frame of
// overlap, at most one frame of added latency - and the CREDIT, never the ring's bytes,
// is what paces a run-ahead client. 2 is a device MEASUREMENT arm: it buys no CPU on a
// client that is already CPU-bound and costs a frame of latency, which is why the
// default is 1 and not "as deep as the ring".
// The present credit (P5e, ruling 4 / ID-92): how many presents the client may have in
// flight before it waits for a swap to come back. 1 = the client publishes frame N+1's
// records while the server applies and swaps frame N - one frame of overlap, at most one
// frame of added latency - and the CREDIT, never the ring's bytes, is what paces a
// run-ahead client. Derived from MOBILEGL_FRAMES_IN_FLIGHT (max(1, N - 2)); the
// MOBILEGL_IPC_PRESENT_CREDIT override stays for experiments. Credit 2 is a real trade,
// not a measurement arm: on FCL inproc (cpuhunt, 2026-10-08) it brought Magma 309 -> 320
// and Espryt 299 -> 319 fps, at one frame of added latency.
Uint32 PresentCredit = 1;
// MOBILEGL_IPC_WAIT_STATS (P15, diagnostic): every 600 presents, log how many times per
// frame the run-ahead client waited, by record op (applied/reply), by quiesce reason and
// barriered verb, for ring space and for the present credit. Off by default.
Bool WaitStats = false;
// MOBILEGL_IPC_CONTROL_TIMEOUT_MS (CONTRACT-P6 §5.4 D5b): how long a spawn/tcp client
// waits for the SurfaceReply to one surface-control op (eglCreate*Surface, MakeCurrent,
// ...) once the server's backend is up. Its expiry is NOT fatal: the doorbell's death
@@ -713,8 +758,9 @@ namespace MobileGL::MG_Config {
// selection folds to the single shared block) and forced off by MOBILEGL_PIPE_VERIFY
// (the comparator owns the one fill block it compares against). 0 is not merely the
// default, it is the negative control: the lane's distinctness case must go red
// without it (P5F §6).
Bool RoleSplitState = false;
// without it (P5F §6). P15: the default is ON (unset = on, 0 = off); the negative control
// sets 0 explicitly.
Bool RoleSplitState = true;
// MOBILEGL_IPC_AUDIT: after a record retires, the server fills the SEG_STAGE bytes
// it referenced with 0xDD (R-2.5). This is the ONLY mechanical control that an
// inproc implementation did not quietly keep using a pointer past its lifetime.
@@ -742,10 +788,21 @@ namespace MobileGL::MG_Config {
inline Bool ServerOwnedWindowSurfaces() {
return Transport == TransportMode::Spawn && Ipc.Surface == IpcSurface::Server;
}
#else
// The whole point: in a build without MG_Remote this folds at compile time, so
// `if (MG_Config::Transport != MG_Config::TransportMode::Monolith)` in Init.cpp is a
// discarded statement and the pull build gains no symbol, no branch and no byte.
inline constexpr TransportMode Transport = TransportMode::Monolith;
#endif
// P13: THE DATA-ARM QUESTION, ANSWERED ONCE FOR EVERY BUILD. The backends read RECORDS (the
// applier's twins, the staged stores, the verb's own handles), never frontend objects - over a
// wire and on monolith alike ("monolith is the push variant"). W4 switched monolith family by
// family behind MOBILEGL_PIPE_DATA_ARM; W5 built the record arm into every library; W6 deleted
// the frontend arm and the knob. The predicate stays as a name for the sites that read it.
inline constexpr Bool DataArmIsRecord() { return true; }
// ...and the record arm runs in this process over stores that ALIAS the frontend's shadows
// (monolith) instead of copying them (every wire).
inline Bool RecordArmAliasesFrontend() { return Transport == TransportMode::Monolith; }
} // namespace MobileGL::MG_Config
+106 -36
View File
@@ -10,12 +10,10 @@
#include <sys/system_properties.h>
#endif
#include "Config.h"
#if MOBILEGL_PIPE_PUSH
// For kMGPipeSubsystemsMigratedAtP5e, the push build's PipePush default (the P2, P3a and P4a
// constants beside it are the phase-by-phase controls, not the default). Push-only, so the
// pull build's translation unit is unchanged.
#include <MG_Pipe/MGPipe.h>
#endif
#include <cerrno>
#include <cstdio>
@@ -41,6 +39,7 @@ namespace MobileGL::MG_Config {
Bool SplitTransportRequestedByConfig = false;
IpcTable Ipc;
#endif
RecordArmTable RecordArm;
} // namespace MobileGL::MG_Config
namespace MobileGL::MG_ConfigLoader {
@@ -143,6 +142,28 @@ namespace MobileGL::MG_ConfigLoader {
if (IsServerProcess()) return;
ReadBackendFile(FilePathSetting("MOBILEGL_BACKEND_FILE", kDefaultBackendFile), out);
ReadConfigFile(FilePathSetting("MOBILEGL_CONFIG_FILE", kDefaultConfigFile), out, log);
#if defined(__ANDROID__)
// An app that hosts the library (FCL) owns its process environment, so a developer
// has no way to set a MOBILEGL_* knob in it. `adb shell setprop debug.mobilegl.env
// "MOBILEGL_A=1;MOBILEGL_B=0"` does it, below the environment like the Linux config
// file (PROP_VALUE_MAX bounds it to a few keys).
char property[PROP_VALUE_MAX] = {};
if (__system_property_get("debug.mobilegl.env", property) > 0) {
String all(property);
SizeT start = 0;
while (start < all.size()) {
SizeT end = all.find(';', start);
if (end == String::npos) end = all.size();
const String item = all.substr(start, end - start);
const SizeT eq = item.find('=');
if (eq != String::npos && eq > 0) {
out.emplace(item.substr(0, eq), item.substr(eq + 1));
if (log) MGLOG_I("Config: debug.mobilegl.env sets %s", item.c_str());
}
start = end + 1;
}
}
#endif
}
} // namespace
@@ -346,10 +367,26 @@ namespace MobileGL::MG_ConfigLoader {
QueryEnvQuirkOverride("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
features.MagmaIterationRPFixBarrier = QueryEnvFlag("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER");
features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
features.MagmaInPassClear = QueryEnvQuirkOverride("MOBILEGL_MAGMA_INPASS_CLEAR");
features.MagmaInPassClearDefault = QueryEnvQuirkOverride("MOBILEGL_MAGMA_INPASS_CLEAR_DEFAULT");
features.MagmaPassCache = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PASS_CACHE");
features.MagmaVertexConversionCache = QueryEnvQuirkOverride("MOBILEGL_MAGMA_VERTEX_CONVERSION_CACHE");
features.MagmaWriteRename = QueryEnvQuirkOverride("MOBILEGL_MAGMA_WRITE_RENAME");
features.EsprytVaoShadow = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_VAO_SHADOW");
features.PipeVertexElementsDedup = QueryEnvQuirkOverride("MOBILEGL_PIPE_VERTEX_ELEMENTS_DEDUP");
features.PipeFramebufferStateMemo = QueryEnvQuirkOverride("MOBILEGL_PIPE_FRAMEBUFFER_STATE_MEMO");
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV");
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
// P15: one backend-neutral knob; the Magma-only spelling stays as a deprecated alias.
if (acceptedEnvVariablesMap->count("MOBILEGL_FRAMES_IN_FLIGHT") != 0) {
features.FramesInFlight = QueryEnvUint32("MOBILEGL_FRAMES_IN_FLIGHT", 3, 1, 64);
} else {
features.FramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
if (acceptedEnvVariablesMap->count("MOBILEGL_MAGMA_FRAMESINFLIGHT") != 0)
MGLOG_W("Config: MOBILEGL_MAGMA_FRAMESINFLIGHT is deprecated; use MOBILEGL_FRAMES_IN_FLIGHT");
}
features.MagmaExtraSwapchainImages = QueryEnvUint32("MOBILEGL_MAGMA_EXTRA_SWAPCHAIN_IMAGES", 0, 0, 16);
features.MagmaMaxDrawsPerCommandBuffer =
QueryEnvUint32("MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER", 16384, 0, 1u << 24);
features.MagmaDescriptorTrimFrames = QueryEnvUint32("MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES", 120, 0, 1u << 20);
@@ -386,34 +423,34 @@ namespace MobileGL::MG_ConfigLoader {
// 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
// kMGPipeSubsystemsMigratedAtP3a (0x1ff) is the phase-by-phase control - P4a's four
// subsystems off, everything P3a landed still on.
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP5e);
#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
// P13 W3b (ID-P13-3): bits 0-13 are fixed on. The arms a cleared bit used to select
// (the pre-handle memos, the pull build's frontend reads) are gone, so a cleared bit has
// nothing to run; it is refused here, by name, before any backend is brought up, rather
// than discovered as an armless subsystem at its first draw. Bit 63 (CSO content
// addressing off) stays a switch.
if ((features.PipePush & MG_Pipe::kMGPipeSubsystemsMigratedAtP5e) !=
MG_Pipe::kMGPipeSubsystemsMigratedAtP5e) {
MGLOG_F("MGPipe: Fatal{PipeSubsystemsFixedOn, \"MOBILEGL_PIPE_PUSH=0x%llx clears subsystem "
"bit(s) 0x%llx; bits 0-13 are fixed on since P13 - only bit 63 may be set or "
"cleared\"}",
static_cast<unsigned long long>(features.PipePush),
static_cast<unsigned long long>(MG_Pipe::kMGPipeSubsystemsMigratedAtP5e &
~features.PipePush));
std::abort();
}
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, "");
@@ -459,6 +496,15 @@ namespace MobileGL::MG_ConfigLoader {
inline void InitTransport() {
String value;
QueryEnvVariable("MOBILEGL_TRANSPORT", value, "monolith");
#if defined(__ANDROID__)
// As debug.mobilegl.backend above: a developer picks the transport of an app-hosted
// library (a launcher's game process) without rebuilding it or its launcher.
char property[PROP_VALUE_MAX] = {};
if (__system_property_get("debug.mobilegl.transport", property) > 0) {
MGLOG_I("Config: debug.mobilegl.transport=%s overrides MOBILEGL_TRANSPORT=%s", property, value.c_str());
value = property;
}
#endif
String lowered = value;
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
@@ -478,7 +524,7 @@ namespace MobileGL::MG_ConfigLoader {
"ring to an apply thread");
return;
}
// The three P6 forms. Recognised precisely, so the diagnostic can say "not yet"
// The three P6 forms. Recognised precisely, so the diagnostic can say "rejected"
// rather than "unknown", which are different bugs on the operator's side.
// P6 `sm`/`cp`: spawn is IMPLEMENTED. It launches a server process on a
// rendezvous of its own and connects to it - the two processes are
@@ -502,9 +548,10 @@ namespace MobileGL::MG_ConfigLoader {
return;
}
if (lowered.compare(0, 5, "unix:") == 0 || lowered.compare(0, 5, "pipe:") == 0) {
// unix: and pipe: remain P6+ - connecting to an endpoint SOMEBODY
// ELSE is listening on needs the server lifecycle to be somebody
// else's too, which is P12's. Named, never a silent degrade.
// unix: and pipe: are NOT MOBILEGL_TRANSPORT values and are rejected here. Connecting
// to a server somebody else is listening on is `spawn` plus
// MOBILEGL_IPC_CONTROL=unix:<path>|unix:@<name>|tcp://host:port (InitIpc); there is
// no pipe: form. Named, never a silent degrade.
MGLOG_E("Config: MOBILEGL_TRANSPORT='%s' names a transport P6 does not implement yet; "
"staying on monolith. This run is NOT a split run.",
value.c_str());
@@ -531,20 +578,11 @@ namespace MobileGL::MG_ConfigLoader {
// are sanity, not policy.
ipc.RingMb = QueryEnvUint32("MOBILEGL_IPC_RING_MB", 8, 1, 1024);
ipc.StageMb = QueryEnvUint32("MOBILEGL_IPC_STAGE_MB", 32, 1, 4096);
// P7 wave 4 M2 (Config.h has the semantics). 0 is admitted ON PURPOSE as the negative
// control: no forced sync, and MagmaWireReclaimScenario's watermark case must go red.
ipc.WireDeferredMb = QueryEnvUint32("MOBILEGL_IPC_WIRE_DEFERRED_MB", 64, 0, 65536);
ipc.SpinUs = QueryEnvUint32("MOBILEGL_IPC_SPIN_US", 50, 0, 1000000);
// PH-6 (ID-P7-2; Config.h has the semantics). 0 is NOT admitted: a server with no
// patience at all would forfeit a healthy run-ahead client the first time its ring
// filled between two of the client's drains, which is an ordinary backlog.
ipc.EventWaitMs = QueryEnvUint32("MOBILEGL_IPC_EVENT_WAIT_MS", 2000, 1, 600000);
// 0 is admitted ON PURPOSE and is the negative control of exit gate E3(a): it turns
// the persistent-map push OFF, and PersistentCoherentMapScenario must go red.
ipc.PersistentBlockKb = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_BLOCK_KB", 64, 0, 65536);
// Whole-range push is the 0 arm; with it on (default) only blocks whose
// xxHash64 changed since the last push are shipped.
ipc.PersistentHashSuppress = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS", 1, 0, 1);
ipc.BatchWaits = QueryEnvUint32("MOBILEGL_IPC_BATCH_WAITS", 1, 0, 1);
// The create window. DEFAULT 6, and it works because a link that declares RetainsReplies
// keeps a declared answer until its reader takes it - Config.h carries the three reasons it
@@ -583,19 +621,31 @@ namespace MobileGL::MG_ConfigLoader {
// The present credit (ruling 4). 1 is one frame of overlap; 8 is the ceiling because a
// deeper queue buys nothing on a CPU-bound client and pays for it in latency. 0 is NOT
// admitted: a credit of zero would mean "publish no present at all".
ipc.PresentCredit = QueryEnvUint32("MOBILEGL_IPC_PRESENT_CREDIT", 1, 1, 8);
// P15: derived from MOBILEGL_FRAMES_IN_FLIGHT (Config.h states the mapping); the explicit
// knob stays as an override for experiments.
{
const Uint32 derived = std::max<Uint32>(1u, std::min<Uint32>(8u, MG_Config::Features.FramesInFlight > 2
? MG_Config::Features.FramesInFlight - 2
: 1u));
ipc.PresentCredit = QueryEnvUint32("MOBILEGL_IPC_PRESENT_CREDIT", derived, 1, 8);
}
// CONTRACT-P6 D5b's reply bound and P7's cold-start budget (Config.h has the semantics).
// Both are floored at 100 ms: a zero bound would declare every live server silent.
ipc.ControlTimeoutMs = QueryEnvUint32("MOBILEGL_IPC_CONTROL_TIMEOUT_MS", 5000, 100, 600000);
ipc.ColdStartMs = QueryEnvUint32("MOBILEGL_IPC_COLD_START_MS", 20000, 100, 600000);
ipc.StrictErrors = QueryEnvFlag("MOBILEGL_IPC_STRICT_ERRORS");
ipc.WaitStats = QueryEnvFlag("MOBILEGL_IPC_WAIT_STATS");
ipc.Audit = QueryEnvFlag("MOBILEGL_IPC_AUDIT");
QueryEnvVariable("MOBILEGL_IPC_SERVER_AFFINITY", ipc.ServerAffinity, "auto");
// P5f f1: the dual-block rehearsal (P5F-WIRE-COMPLETENESS.md §4). Forced OFF by the
// verify harness: the comparator's entry compare and compare-at-read hook are built on
// there being ONE filled block (the hook pins itself to &gPipeInputs,
// PipeFill.cpp's MGPipeVerifyReadHook), which is exactly what the rehearsal splits.
ipc.RoleSplitState = QueryEnvFlag("MOBILEGL_IPC_ROLE_SPLIT_STATE");
// P15: ON BY DEFAULT. Every split lane and the on-device retrace gate already ran with it,
// and the dual-block census is empty (no apply reads a client-filled field), so the
// shipped arm now matches what is tested - and with two blocks the client's fill is its
// own and needs no applier quiesce. 0 still turns it off (the negative control does).
ipc.RoleSplitState = QueryEnvQuirkOverride("MOBILEGL_IPC_ROLE_SPLIT_STATE") != MG_Config::QuirkOverride::ForceOff;
if (MG_Config::Features.PipeVerify && ipc.RoleSplitState) {
MGLOG_W("Config: MOBILEGL_IPC_ROLE_SPLIT_STATE=1 is incompatible with "
"MOBILEGL_PIPE_VERIFY (the comparator owns the single fill block); "
@@ -668,8 +718,8 @@ namespace MobileGL::MG_ConfigLoader {
"persistent-block=%uKiB create-window=%u pbo-readback-sync=%u poll-escalate=%u "
"adopt-tier=%s allow-t0=%u verb-barrier=%u run-ahead=%u present-credit=%u control-timeout=%ums "
"cold-start=%ums strict=%d audit=%d role-split-state=%d affinity='%s' surface=%s",
ipc.RingMb, ipc.StageMb, ipc.WireDeferredMb, ipc.SpinUs, ipc.EventWaitMs,
ipc.PersistentBlockKb, ipc.CreateWindow, ipc.PboReadbackSync, ipc.PollEscalate,
ipc.RingMb, ipc.StageMb, MG_Config::RecordArm.WireDeferredMb, ipc.SpinUs, ipc.EventWaitMs,
MG_Config::RecordArm.PersistentBlockKb, ipc.CreateWindow, ipc.PboReadbackSync, ipc.PollEscalate,
ipc.AdoptTier == 0 ? "0" : ipc.AdoptTier == 1 ? "1" : ipc.AdoptTier == 2 ? "2" : "unset(T0)",
ipc.AllowAdoptT0,
ipc.VerbBarrier, ipc.RunAhead, ipc.PresentCredit, ipc.ControlTimeoutMs, ipc.ColdStartMs,
@@ -681,6 +731,26 @@ namespace MobileGL::MG_ConfigLoader {
"expected to fail: the client still pulls 31 of 63 PipeInputs fields from a "
"live GLContext, so an unbarriered queue lets the server read future values");
}
}
#endif
// P13 W5: the record arm's knobs (Config.h RecordArmTable), in every build that has the arm.
inline void InitRecordArm() {
MG_Config::RecordArmTable& ipc = MG_Config::RecordArm;
// P7 wave 4 M2 (Config.h has the semantics). 0 is admitted ON PURPOSE as the negative
// control: no forced sync, and MagmaWireReclaimScenario's watermark case must go red.
ipc.WireDeferredMb = QueryEnvUint32("MOBILEGL_IPC_WIRE_DEFERRED_MB", 64, 0, 65536);
// 0 is admitted ON PURPOSE and is the negative control of exit gate E3(a): it turns
// the persistent-map push OFF, and PersistentCoherentMapScenario must go red.
ipc.PersistentBlockKb = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_BLOCK_KB", 64, 0, 65536);
// Whole-range push is the 0 arm; with it on (default) only blocks whose
// xxHash64 changed since the last push are shipped.
ipc.PersistentHashSuppress = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS", 1, 0, 1);
ipc.PersistentMprotect = QueryEnvUint32("MOBILEGL_PIPE_PERSISTENT_MPROTECT", 1, 0, 1);
if (ipc.PersistentMprotect == 0) {
MGLOG_W("Config: MOBILEGL_PIPE_PERSISTENT_MPROTECT=0 - no SIGSEGV handler is installed "
"for persistent write maps; every one takes the hash scan");
}
if (ipc.WireDeferredMb == 0) {
MGLOG_W("Config: MOBILEGL_IPC_WIRE_DEFERRED_MB=0 is the M2 NEGATIVE CONTROL: the "
"server never forces a sync for orphaned wire buffer stores, so a long frame "
@@ -691,7 +761,6 @@ namespace MobileGL::MG_ConfigLoader {
"the persistent-map push is OFF and a coherent-map scenario must go red");
}
}
#endif
void Init() {
MGLOG_D("Loading configuration from environment variables...");
@@ -699,6 +768,7 @@ namespace MobileGL::MG_ConfigLoader {
InitBackendType();
InitFeatures();
InitRecordArm();
#if MOBILEGL_BUILD_DISAGGREGATED
// After InitFeatures, so the one line InitIpc logs is the last word on this run's
// configuration, and before the accepted-env map is destroyed just below.
+8
View File
@@ -24,6 +24,7 @@
#if MOBILEGL_PIPE_VERIFY
#include <MG_Backend/MGPipe/PipeInputs.h> // MGPipeVerifyFlushSummary, before Debug::Close in DestroyImpl
#endif
#include <MG_Impl/Pipe/Verb/VerbPort.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Remote/Client/ServerProbe.h>
#endif
@@ -182,6 +183,13 @@ namespace MobileGL {
#endif
{
MG_Backend::Init();
// P13 W4: monolith's record arm reads the verb's own state, which only a verb sink
// writes; the port routes the ported verbs through one (MG_Impl/Pipe/Verb/VerbPort.cpp).
// Installed HERE, by the library's role-neutral init, rather than by MG_Backend's: the
// port is client code, and the server image must not need it to link.
if (MG_Config::RecordArmAliasesFrontend() && MG_Backend::pActiveBackendObject) {
MG_Record::InstallMonolithVerbPort(MG_Backend::gBackendFunctionsTable);
}
}
MGLOG_D("MG_Backend initialized");
MG_Impl::Init();
+4 -3
View File
@@ -33,10 +33,11 @@ namespace MobileGL {
} // namespace MG_ConfigLoader
// Whether this process can be served right now. False when MobileGL is configured to reach a
// server that is not there (or a split session already failed to come up), or when the process
// asked for software rendering (LIBGL_ALWAYS_SOFTWARE, desktop Linux), so a
// server that is not there (or a split session already failed to come up), so a
// caller that can choose another implementation - the system's EGL/GLX/GBM loaders - should
// decline rather than hand out something that cannot draw. Cheap: once true it stays true
// decline rather than hand out something that cannot draw. LIBGL_ALWAYS_SOFTWARE does NOT
// make it false: that variable selects the system GL's software renderer and is not a reason
// to disable MobileGL's entry points. Cheap: once true it stays true
// until the library is torn down, and a probe is one non-blocking connect with a short bound.
Bool ImplementationAvailable();
-8
View File
@@ -10,9 +10,7 @@
#include <Includes.h>
#include "MG_State/GLState/TextureState/TextureEnum.h"
#include <MG_Util/Damage/Damage.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Pipe/MGPipeHandles.h>
#endif
namespace MobileGL {
namespace MG_State::GLState {
@@ -37,22 +35,16 @@ namespace MobileGL {
struct CopyImageEndpoint {
SharedPtr<MG_State::GLState::ITextureObject> Texture;
SharedPtr<MG_State::GLState::RenderbufferObject> Renderbuffer;
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeHandle TextureHandle = MG_Pipe::kMGPipeNullHandle;
MG_Pipe::MGPipeHandle RenderbufferHandle = MG_Pipe::kMGPipeNullHandle;
#endif
Bool IsRenderbuffer() const {
#if MOBILEGL_BUILD_DISAGGREGATED
if (!MG_Pipe::MGPipeHandleIsNull(RenderbufferHandle)) return true;
#endif
return Renderbuffer != nullptr;
}
Bool Exists() const {
#if MOBILEGL_BUILD_DISAGGREGATED
if (!MG_Pipe::MGPipeHandleIsNull(TextureHandle)) return true;
if (!MG_Pipe::MGPipeHandleIsNull(RenderbufferHandle)) return true;
#endif
return Texture != nullptr || Renderbuffer != nullptr;
}
};
@@ -32,7 +32,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// ID-54 / ID-67 (v1, under ID-52/ID-59's grant for this file). Which VIRTUAL (dpy, draw,
// read, ctx) the process's one native ES context is currently bound FOR, on the apply
// thread. DirectGLES has one native context and one native surface (g_Context, g_Surface)
@@ -78,7 +77,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
return false;
}
#endif
void ClearGLErrors(const MG_External::GLESFunctionsTable& gl) {
if (!gl.glGetError) return;
@@ -860,7 +858,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
Int maxSamples = 0;
const SizeT formatIndex = static_cast<SizeT>(logicalFormat);
#if MOBILEGL_BUILD_DISAGGREGATED
// C6 / ID-52: read the ROLE's own cache. Under split that is the server's private backend
// (ActiveBackendFormatCaps), not pActiveBackendObject, which holds the client's mirror.
const FormatCapabilityCache* activeCaps = ActiveBackendFormatCaps();
@@ -872,17 +869,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
maxSamples = probedCounts.front();
}
}
#else
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();
}
}
#endif
if (maxSamples <= 0) {
maxSamples = GetGLESFormatMaxSamples(g_GLESCapabilities, logicalFormat, imageFormat);
}
@@ -995,6 +981,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// built).
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities);
UpdateDynamicBackendParameters();
// The colour-attachment cap the framebuffer family checks is known from here on.
DirectGLES::ResolveRecordArmFamilies();
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
PrintFormatCapabilities(GetFormatCapabilities());
return true;
@@ -1085,7 +1073,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
ResetEGLRuntimeState();
}
#if MOBILEGL_BUILD_DISAGGREGATED
// ID-54 / ID-67: as for the window surface - InitPbufferSurface bound natively. The pull
// arm below is the original statement, byte for byte (G1).
const Bool created = BackendObject::CreateEGLPbufferSurface(surface, width, height);
@@ -1098,9 +1085,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
NoteNativeContextGone();
}
return created;
#else
return BackendObject::CreateEGLPbufferSurface(surface, width, height);
#endif
}
Bool BackendObject_DirectGLES::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
@@ -1139,9 +1123,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!DirectGLES::ReleaseCurrent()) {
return false;
}
#if MOBILEGL_BUILD_DISAGGREGATED
NoteNativeContextGone();
#endif
return BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
}
@@ -1212,9 +1194,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!BackendObject::MakeEGLCurrent(dpy, draw, read, ctx)) {
(void)DirectGLES::ReleaseCurrent();
#if MOBILEGL_BUILD_DISAGGREGATED
NoteNativeContextGone();
#endif
return false;
}
return true;
@@ -1267,9 +1247,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BackendObject_DirectGLES::ReleaseEGLResources() {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
DestroyEGLContext();
#if MOBILEGL_BUILD_DISAGGREGATED
NoteNativeContextGone();
#endif
m_suspendedWindowSurfaces.clear();
BackendObject::ReleaseEGLResources();
}
@@ -1295,10 +1273,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
m_suspendedWindowSurfaces.push_back(client);
}
#if MOBILEGL_BUILD_DISAGGREGATED
// The native binding moved onto the placeholder: the next make-current binds for real.
NoteNativeContextGone();
#endif
MGLOG_I("DirectGLES: %zu window surface(s) on server window %p now draw into placeholder pbuffers; the "
"contexts and their objects stay",
onWindow.size(), window);
@@ -1327,9 +1303,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (surface == EGL_NO_SURFACE) return false;
native->second = surface;
}
#if MOBILEGL_BUILD_DISAGGREGATED
NoteNativeContextGone();
#endif
MGLOG_I("DirectGLES: %zu suspended window surface(s) rebuilt on server window %p (%ux%u)", suspended.size(),
window, width, height);
return true;
@@ -1366,9 +1340,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// every object the client had built - which a multi-window client hit on its first closed
// popup.
(void)surface;
#if MOBILEGL_BUILD_DISAGGREGATED
NoteNativeContextGone();
#endif
}
void BackendObject_DirectGLES::OnEGLSurfaceForgotten(EGLSurface surface) {
@@ -1633,7 +1605,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
funcsTable.GL.DrawElements = DrawElements;
funcsTable.GL.DrawElementsBaseVertex = DrawElementsBaseVertex;
funcsTable.GL.MultiDrawArrays = MultiDrawArrays;
funcsTable.GL.MultiDrawElements = MultiDrawElements;
funcsTable.GL.MultiDrawElementsBaseVertex = MultiDrawElementsBaseVertex;
funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
funcsTable.GL.MultiDrawElementsIndirectCount = MultiDrawElementsIndirectCount;
@@ -1661,18 +1632,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
funcsTable.GL.ClearBufferfv = ClearBufferfv;
funcsTable.GL.ClearBufferuiv = ClearBufferuiv;
funcsTable.GL.ClearBufferiv = ClearBufferiv;
funcsTable.GL.ClearNamedFramebufferfv = ClearNamedFramebufferfv;
funcsTable.GL.ClearNamedFramebufferiv = ClearNamedFramebufferiv;
funcsTable.GL.ClearNamedFramebufferuiv = ClearNamedFramebufferuiv;
funcsTable.GL.ClearNamedFramebufferfi = ClearNamedFramebufferfi;
funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
funcsTable.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
funcsTable.GL.CopyTexImage2D = CopyTexImage2D;
funcsTable.GL.CopyTexSubImage2D = CopyTexSubImage2D;
funcsTable.GL.CopyImageSubData = CopyImageSubData;
funcsTable.GL.GenerateMipmap = GenerateMipmap;
funcsTable.GL.ReadPixels = ReadPixels;
funcsTable.GL.GetTexImage = GetTexImage;
funcsTable.GL.FenceSync = FenceSync;
funcsTable.GL.ClientWaitSync = ClientWaitSync;
funcsTable.GL.WaitSync = WaitSync;
File diff suppressed because it is too large Load Diff
+28 -33
View File
@@ -30,7 +30,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool ReadTextureLevelTight(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
TextureInternalFormat logicalFormat, GLint level, const IntVec3& logicalExtent,
Bool sourceUsesImageCarrier, GLenum format, GLenum type, Vector<Uint8>& bytes);
#if MOBILEGL_BUILD_DISAGGREGATED
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& bytes);
// Whether the NATIVE texture currently holds `level` of `uploadTarget` at `logicalExtent`
// (glGetTexLevelParameteriv). False for a level the driver never allocated - one defined after
@@ -38,7 +37,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// refuses answers true, keeping the caller on its readback path.
Bool NativeTextureLevelHasExtent(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
GLint level, const IntVec3& logicalExtent);
#endif
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
@@ -48,8 +46,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
void DrawArrays(GLenum mode, GLint first, GLsizei count);
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex);
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount);
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount);
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex);
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
@@ -73,21 +69,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLuint baseinstance);
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount);
void DrawArraysIndirect(GLenum mode, const void* indirect);
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLuint* value);
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter);
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLbitfield mask, GLenum filter);
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
@@ -100,7 +83,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
void GenerateMipmap(GLenum target);
const GLubyte* GetString(GLenum name);
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 DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
void DispatchComputeIndirect(GLintptr indirect);
void MemoryBarrier(GLbitfield barriers);
@@ -284,7 +266,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
using NativeContextKeyResolver = Bool (*)(Uint64* outSessionKey, Uint64* outContextToken);
void SetNativeContextKeyResolver(NativeContextKeyResolver resolver);
#if MOBILEGL_PIPE_PUSH
// ---------------------------------------------------------------------------------
// P14 S6 (docs/Disaggregated/design/11-state-ownership.md): THE TWIN TABLES' KEY, AND ITS
// SHAPE IS S4's, ONE LEVEL DOWN. A twin is a DRIVER object, so it lives in one native
@@ -323,7 +304,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// The calling thread's key. Answers false for {0, 0} - the process-wide group - which is
// also the answer when no probe has ever been installed.
Bool CurrentTwinKey(TwinKey* outKey);
#endif
// Builds the native context for `contextToken` under the calling thread's session. A
// `shareGroupToken` whose group already has a live native context in this session hands that
@@ -347,7 +327,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
// The calling thread's current native context as a lifetime serial: assigned when the context
// is created and never reused (a recycled EGLContext address is a new serial). 0 when none was
// made current through MakeCurrent.
Uint64 CurrentNativeContextSerial();
//
// P15: it lives in one constant-initialized thread_local with the PerNativeContext lookup
// cache below, so the draw path's "this context's entry" question is one TLS address
// computation, not two calls each paying their own.
struct NativeContextThreadState {
struct CacheEntry {
const void* Owner = nullptr;
Uint64 Serial = 0;
Uint64 Epoch = 0;
void* Entry = nullptr;
};
static constexpr Uint32 kCacheEntries = 8;
Uint64 BoundSerial = 0;
// One entry per PerNativeContext instance slot (instances sharing a slot evict each other).
CacheEntry Cache[kCacheEntries]{};
};
inline constinit thread_local NativeContextThreadState t_nativeContextThread;
inline Uint64 CurrentNativeContextSerial() { return t_nativeContextThread.BoundSerial; }
class PerNativeContextBase {
public:
@@ -360,6 +357,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
protected:
~PerNativeContextBase();
// This instance's entry in NativeContextThreadState::Cache.
const Uint32 m_cacheSlot;
};
// Called where a native context is destroyed.
@@ -374,12 +373,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
class PerNativeContext final : public PerNativeContextBase {
public:
T& Current() {
const Uint64 serial = CurrentNativeContextSerial();
NativeContextThreadState& thread = t_nativeContextThread;
const Uint64 serial = thread.BoundSerial;
// The draw path asks for the same entry over and over from one thread: a one-entry
// thread-local cache, invalidated by any drop anywhere (rare).
thread_local CurrentCache cache;
// thread-local cache per instance, invalidated by any drop anywhere (rare) and by an
// instance's destruction (so a later instance at the same address cannot hit).
NativeContextThreadState::CacheEntry& cache = thread.Cache[m_cacheSlot];
const Uint64 epoch = PerNativeContextEpoch();
if (cache.Owner == this && cache.Serial == serial && cache.Epoch == epoch) return *cache.Entry;
if (cache.Owner == this && cache.Serial == serial && cache.Epoch == epoch) {
return *static_cast<T*>(cache.Entry);
}
const std::lock_guard<std::mutex> lock(m_mutex);
T& entry = m_bySerial[serial]; // node-based: the reference survives other contexts' inserts
cache = {this, serial, epoch, &entry};
@@ -402,12 +405,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
private:
struct CurrentCache {
const PerNativeContext* Owner = nullptr;
Uint64 Serial = 0;
Uint64 Epoch = 0;
T* Entry = nullptr;
};
std::mutex m_mutex;
std::unordered_map<Uint64, T> m_bySerial;
};
@@ -434,12 +431,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// native context already current on this thread IS that one - so the single-context world pays
// nothing for it. Called from the bind_context path on the apply thread.
Bool MakeNativeContextCurrentForBoundToken();
#if MOBILEGL_BUILD_DISAGGREGATED
// P14: the server's backend turn moved to the calling apply thread (ServerLoop.cpp's
// BackendTurnLock). Takes the backend's GL ownership for this thread and drops every
// binding shadow, which still describes the previous holder's native context.
void OnBackendTurnHandoff();
#endif
// How many native contexts this process holds, and how many of them belong to one session. The
// test observables for "a second context did not tear the first one down".
Uint64 NativeContextCount();
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+42 -89
View File
@@ -9,7 +9,7 @@
#include "MultiDraw.h"
#include "Managers.h"
#include <MG_State/GLState/Core.h>
#include <MG_Pipe/PipeInputsSwitch.h>
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <cstdlib>
#include <cstring>
@@ -44,14 +44,14 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// verbatim is already "this batch restarts nowhere".
Uint32 RestartSentinelFor(GLenum type) {
if (ResolveRestartSubstitution(type) != RestartSubstitutionKind::None) {
return MGB_CTX->GetPrimitiveRestartIndex();
return MG_Pipe::gPipeInputs->GetPrimitiveRestartIndex();
}
return MG_Util::FixedRestartIndexForGLType(type);
}
Bool RestartActive() {
return MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
return MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
}
// Vertices per primitive for the modes whose sub-draws may be concatenated into a
@@ -95,7 +95,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// PrepareForDraw leave on GL_ELEMENT_ARRAY_BUFFER (and how big is its store)", and
// "give me its bytes on the CPU" - and until now it asked all three of the FRONTEND
// VAO, on the apply thread, once per indexed multi-draw. That was the last unguarded
// `MGB_CTX->GetBoundVertexArray()` in the backend: package vi retired the ordinary
// `gPipeInputs->GetBoundVertexArray()` in the backend: package vi retired the ordinary
// draw path's copy (DirectGLES.cpp's SyncCurrentVertexAttributeValues / PrepareForDraw)
// and this one survived only because these batches used to die earlier, on the
// framebuffer row fb has since retired.
@@ -125,7 +125,6 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
const Uint8* HostBytes = nullptr; // HostBytes: null when there is no CPU copy
};
#if MOBILEGL_BUILD_DISAGGREGATED
// A missing record on the handle arm is a NAMED refusal and never a quiet fall-back to
// the frontend (TASK-mv requirement 2; the shape is Managers.cpp's
// RefuseNullFrontendTextureOffTheHandleArm). The frontend element slot is not a second
@@ -175,81 +174,45 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
if (!candidate.Live || candidate.Gen != res.Gen) return false;
return candidate.Desc.HasDefinedContent != 0;
}
#endif
BoundIndexBufferView ResolveBoundIndexBuffer(IndexBufferQuestion question, const char* entry) {
BoundIndexBufferView view;
#if MOBILEGL_BUILD_DISAGGREGATED
if (BufferImpl::VertexInputReadsRecords()) {
const auto& st = MG_Pipe::MGPipeApplier();
const MG_Pipe::MGPipeHandle res = BufferImpl::ResolveDrawIndexBufferFromRecord(st).Res;
// A null Res is "no element array buffer bound", exactly as a null frontend slot
// is on the arm below - the batch's indices are a client array, and every caller
// already has an arm for that. It is NOT the arm test; the arm was decided above.
if (MG_Pipe::MGPipeHandleIsNull(res)) return view;
view.Present = true;
if (question == IndexBufferQuestion::Presence) return view;
const auto& st = MG_Pipe::MGPipeApplier();
const MG_Pipe::MGPipeHandle res = BufferImpl::ResolveDrawIndexBufferFromRecord(st).Res;
// A null Res is "no element array buffer bound", exactly as a null frontend slot
// is on the arm below - the batch's indices are a client array, and every caller
// already has an arm for that. It is NOT the arm test; the arm was decided above.
if (MG_Pipe::MGPipeHandleIsNull(res)) return view;
view.Present = true;
if (question == IndexBufferQuestion::Presence) return view;
if (question == IndexBufferQuestion::HostBytes) {
// The applier has already consumed the persistent-map blocks for this
// resource before the draw verb replayed; the one Sync* this arm owes is the
// monolith's SyncGpuWrites, and (P8-C) SplitHostBytesForCpuRead is it: a store
// a shader wrote on this side is read back from this server's own GL buffer,
// because no writeback ever reaches the server's staged copy. That is also why
// this arm cannot be expressed as "find the object and run the monolith body".
auto* resource = BufferImpl::FindBufferResourceForHandle(res);
if (resource == nullptr) RefuseMissingIndexBufferRecord(entry, res, "backend resource");
view.Size = BufferImpl::ResourceWidthForHandle(res);
view.HostBytes = BufferImpl::SplitHostBytesForCpuRead(*resource, res, "multidraw_index_rebase");
if (view.HostBytes != nullptr && IndexBufferRecordHasDefinedContent(st, res)) {
BufferImpl::RequireStagedCoverage(*resource, view.HostBytes, 0, view.Size,
"multidraw_index_rebase");
}
// A null HostBytes is not a refusal: MappedData() on the monolith arm is
// equally allowed to be null (an adopted coherent map keeps its bytes
// elsewhere), and the one caller that asks declines the tier for it.
return view;
}
auto* resource = BufferImpl::EnsureBufferResourceForHandle(nullptr, res);
if (question == IndexBufferQuestion::HostBytes) {
// The applier has already consumed the persistent-map blocks for this
// resource before the draw verb replayed; the one Sync* this arm owes is the
// monolith's SyncGpuWrites, and (P8-C) SplitHostBytesForCpuRead is it: a store
// a shader wrote on this side is read back from this server's own GL buffer,
// because no writeback ever reaches the server's staged copy. That is also why
// this arm cannot be expressed as "find the object and run the monolith body".
auto* resource = BufferImpl::FindBufferResourceForHandle(res);
if (resource == nullptr) RefuseMissingIndexBufferRecord(entry, res, "backend resource");
view.Id = resource->id;
if (question == IndexBufferQuestion::DriverNameAndSize) {
view.Size = BufferImpl::ResourceWidthForHandle(res);
view.Size = BufferImpl::ResourceWidthForHandle(res);
view.HostBytes = BufferImpl::SplitHostBytesForCpuRead(*resource, res, "multidraw_index_rebase");
if (view.HostBytes != nullptr && IndexBufferRecordHasDefinedContent(st, res)) {
BufferImpl::RequireStagedCoverage(*resource, view.HostBytes, 0, view.Size,
"multidraw_index_rebase");
}
// A null HostBytes is not a refusal: MappedData() on the monolith arm is
// equally allowed to be null (an adopted coherent map keeps its bytes
// elsewhere), and the one caller that asks declines the tier for it.
return view;
}
#endif
// MONOLITH GLUE from here down, token for token what each call site did before.
const auto& vao = MGB_CTX->GetBoundVertexArray();
if (!vao) return view;
const auto& ibo = vao->GetIndexBufferBindingSlot().GetBoundObject();
if (!ibo) return view;
view.Present = true;
switch (question) {
case IndexBufferQuestion::Presence:
break;
case IndexBufferQuestion::DriverName: {
const auto* resource = BufferImpl::EnsureBufferResource(ibo);
view.Id = resource ? resource->id : 0;
break;
auto* resource = BufferImpl::EnsureBufferResourceForHandle(nullptr, res);
if (resource == nullptr) RefuseMissingIndexBufferRecord(entry, res, "backend resource");
view.Id = resource->id;
if (question == IndexBufferQuestion::DriverNameAndSize) {
view.Size = BufferImpl::ResourceWidthForHandle(res);
}
case IndexBufferQuestion::DriverNameAndSize: {
const auto* resource = BufferImpl::EnsureBufferResource(ibo);
view.Id = resource ? resource->id : 0;
view.Size = ibo->GetSize();
break;
}
case IndexBufferQuestion::HostBytes:
// The shadow is the source of truth for CPU reads, but a persistent map or
// a shader write may have moved past it since the last sync.
ibo->SyncPersistentMappedRange();
ibo->SyncGpuWrites();
view.HostBytes = ibo->MappedData();
view.Size = ibo->GetSize();
break;
}
(void)entry;
return view;
}
@@ -269,7 +232,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
//
// This was the LAST unguarded frontend read on the multi-draw apply path, and it sat
// twenty lines above the function that retired its neighbour. It asked
// `MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect)` and then
// `gPipeInputs->GetBufferBindingSlot(BufferTarget::DrawIndirect)` and then
// `EnsureBufferResource(<frontend object>)` - a registry lookup keyed by the client's
// identity, which is §4.4's rule and not only the allocator's. Under run-ahead that is
// `Fatal{UnmigratedPipeInput, "GetBufferBindingSlot@DrawArrays"}` on every batch the
@@ -293,23 +256,13 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// an empty slot, and not the arm test (ID-110: the arm was decided by the transport
// above, never inferred from a null).
Uint BoundDrawIndirectBufferId() {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const MG_Pipe::MGPipeHandle res = MG_Pipe::MGPipeApplier().VerbIndirectBuffer;
if (MG_Pipe::MGPipeHandleIsNull(res)) return 0;
auto* resource = BufferImpl::EnsureBufferResourceForHandle(nullptr, res);
if (resource == nullptr) {
RefuseMissingIndirectBufferRecord("BoundDrawIndirectBufferId", res);
}
return resource->id;
const MG_Pipe::MGPipeHandle res = MG_Pipe::MGPipeApplier().VerbIndirectBuffer;
if (MG_Pipe::MGPipeHandleIsNull(res)) return 0;
auto* resource = BufferImpl::EnsureBufferResourceForHandle(nullptr, res);
if (resource == nullptr) {
RefuseMissingIndirectBufferRecord("BoundDrawIndirectBufferId", res);
}
#endif
// MONOLITH GLUE from here down, token for token what this function did before.
const auto& indirect =
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (!indirect) return 0;
const auto* resource = BufferImpl::EnsureBufferResource(indirect);
return resource ? resource->id : 0;
return resource->id;
}
// ---------------------------------------------------------------------------
+42 -331
View File
@@ -17,9 +17,6 @@
// translation units that include it without copying Managers.h's include order.
#include "DirectGLES.h"
#if MOBILEGL_PIPE_PUSH
#include <MG_Impl/Pipe/SlotAllocator.h>
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Pipe/PipeSessionFail.h>
#endif
@@ -45,66 +42,34 @@
// 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;
// from their destructor carrying {kind, lifetimeId, handle}; the backend consumes it in
// Managers.cpp and releases the twin BY THAT HANDLE in every holder of the kind
// (ReleaseTwinByHandle below - the same release the wire's object_death runs on a server), and
// the CLIENT returns the slot after the notice (PipeFill.cpp's NotifyAndFree). So:
// * there is NO draw-path tick, NO creation tick and NO sweep of any kind;
// * 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.)
// length of a test, and a context reset does the same in reverse. Every table therefore links
// itself into a per-table-type list at construction and out at destruction, and one release
// drops the twin in each holder by handle. (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.)
//
// The weak_ptr per entry survives as the MONOLITH-GLUE half of the table's identity: the
// minting GetOrCreate stores it, NoteStateForHandle/StateForHandle read it, and the legacy
// death notice walks it. 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.
// 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).
//
// P5e (id, CONTRACT-P5E §4.1): ForEachLive() NO LONGER HANDS IT OUT. It answers
// fn(MGPipeHandle, const BackendPtr&), because a walk that produced a frontend SharedPtr out
// of server memory was the last place the server could hold one across records - and a caller
// that still needs the object (the detach walk) asks StateForHandle for it BY HANDLE, inside
// the scope that names the debt, so the read is one greppable site rather than a property of
// the iteration.
//
// 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.
//
// P5c (hd, CONTRACT-P5C §3.1) is what gives the debt teeth: with an active transport the
// minting GetOrCreate, HandleOf and OnFrontendObjectDestroyed raise
// Fatal{RoleViolation, "MGPipeSlots"} when reached from the apply thread (the same check the
// allocator's own three entries carry, repeated here so the refusal names this surface), and
// the split paths resolve through GetOrCreate(handle) / ReleaseByHandle instead.
//
// P5f (fr): the frontend-keyed half survives only as MONOLITH GLUE. HandleOf,
// GetOrCreate(StatePtr), NoteStateForHandle and StateForHandle refuse EVERY transport
// apply before touching a frontend object, with or without a barrier or named scope.
// Find(StateObject*) delegates to HandleOf; handle-only iteration never reads stateRef.
// The registry guard is independent of Magma's legacy allocator debt scope.
// P13: THE TABLE IS HANDLE-ONLY. The frontend-keyed half - a GetOrCreate that minted handles
// off a frontend object's lifetime id from inside MG_Backend, the object-keyed Find / HandleOf
// with their lifetime-id memo, the weak per-entry state note and the lifetime-id death walk -
// was monolith glue, unreachable once every family ran the record arm, and is deleted. A handle
// is minted by the CLIENT and arrives in the record; nothing on this side touches the client's
// slot allocator (the link ratchet's P13 exit gate pins that).
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
@@ -122,9 +87,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
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
@@ -140,6 +102,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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.
// This process's verdict, read off MG_Config::Features. Latches nothing and stops nothing.
EsprytSlotArmVerdict CurrentEsprytSlotArmVerdict();
void DiagnoseEsprytSlotArm();
// Reads the config, logs, installs the death-notice consumer, and STOPS when the operator
@@ -151,7 +116,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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
// INLINE on purpose. Every FindByHandle / GetOrCreate / 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
@@ -184,7 +149,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// are two clients, and nothing may be shared with either from the other.
//
// THE HOLDER LIST STAYS PROCESS-WIDE AND THAT IS NOT A COMPROMISE. A notice is about an
// OBJECT, not about a bucket: OnFrontendObjectDestroyed and ReleaseTwinByHandle must reach
// OBJECT, not about a bucket: ReleaseTwinByHandle must reach
// the by-value copies a fixture or a context reset is holding, whichever bucket they sit in,
// and "which bucket holds it" is exactly the question that produced the two-holder leak the
// list exists to fix. Each SHARED holder of the list is ONE bucket; the walk therefore covers
@@ -242,6 +207,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
Table& ForCallingThread() const {
Key key;
CurrentTwinKey(&key);
// The memo hit without the SharedPtr copy Shared() hands out: two atomic reference
// count operations per twin lookup, several lookups per draw (P14). The map keeps the
// table alive; only Drop/DropSession/Reset release it, and they clear the memo.
if (m_memoTable != nullptr && m_memoKey == key) return *m_memoTable;
return *Shared(key);
}
@@ -331,15 +300,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return Shared(key)->GetOrCreate(handle);
}
Uint32 LiveGenAt(Uint32 slot) const { return ForCallingThread().LiveGenAt(slot); }
MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const {
return ForCallingThread().HandleOf(stateObj);
}
void NoteStateForHandle(MG_Pipe::MGPipeHandle handle, const StatePtr& stateObj) {
ForCallingThread().NoteStateForHandle(handle, stateObj);
}
StatePtr StateForHandle(MG_Pipe::MGPipeHandle handle) const {
return ForCallingThread().StateForHandle(handle);
}
template <typename Fn>
void ForEachLive(Fn&& fn) const {
// THE CALLING THREAD'S BUCKET, not every bucket, and that is the semantic answer
@@ -364,9 +324,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
static Bool ReleaseTwinByHandle(MG_Pipe::MGPipeHandle handle) {
return Table::ReleaseTwinByHandle(handle);
}
static Bool OnFrontendObjectDestroyed(Uint64 lifetimeId) {
return Table::OnFrontendObjectDestroyed(lifetimeId);
}
// The instance spelling, and it HANDS THE TWIN OUT rather than answering a Bool: the two
// tables that ARE a bucket registry (buffers, sampler views) have always been called as
// `g_backendBufferResources.ReleaseByHandle(res)` and their callers still have to decide
@@ -408,8 +365,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
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
@@ -439,14 +394,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
struct Entry {
BackendPtr backend;
// THE MONOLITH-GLUE HALF of the entry (P5e, CONTRACT-P5E §4.1): written by the
// minting GetOrCreate and by NoteStateForHandle, read only by StateForHandle and
// the legacy death notice. 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. ForEachLive
// stopped reading it at P5e: a walk that handed a frontend SharedPtr out of server
// memory on every step was the last place the server could hold one across records.
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;
@@ -456,27 +403,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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.
// ENTRIES; the links are the table's own and are never copied.
BackendSlotTable() { LinkHolder(); }
BackendSlotTable(const BackendSlotTable& other):
m_ownerSession(other.m_ownerSession),
m_slots(other.m_slots),
m_band(other.m_band),
m_nullTwin(other.m_nullTwin),
m_memoLifetimeId(other.m_memoLifetimeId),
m_memoHandle(other.m_memoHandle) {
m_nullTwin(other.m_nullTwin) {
LinkHolder();
}
BackendSlotTable(BackendSlotTable&& other) noexcept:
m_ownerSession(other.m_ownerSession),
m_slots(std::move(other.m_slots)),
m_band(std::move(other.m_band)),
m_nullTwin(std::move(other.m_nullTwin)),
m_memoLifetimeId(other.m_memoLifetimeId),
m_memoHandle(other.m_memoHandle) {
m_nullTwin(std::move(other.m_nullTwin)) {
other.m_slots.clear();
other.m_band.clear();
other.ForgetHandle();
LinkHolder();
}
BackendSlotTable& operator=(const BackendSlotTable& other) {
@@ -484,8 +426,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_slots = other.m_slots;
m_band = other.m_band;
m_nullTwin = other.m_nullTwin;
m_memoLifetimeId = other.m_memoLifetimeId;
m_memoHandle = other.m_memoHandle;
}
return *this;
}
@@ -494,11 +434,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_slots = std::move(other.m_slots);
m_band = std::move(other.m_band);
m_nullTwin = std::move(other.m_nullTwin);
m_memoLifetimeId = other.m_memoLifetimeId;
m_memoHandle = other.m_memoHandle;
other.m_slots.clear();
other.m_band.clear();
other.ForgetHandle();
}
return *this;
}
@@ -510,88 +447,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
void SetOwnerSession(Uint64 session) { m_ownerSession = session; }
Uint64 OwnerSession() const { return m_ownerSession; }
// 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();
#if MOBILEGL_BUILD_DISAGGREGATED
// This overload mints from frontend identity and is monolith-only. Refuse
// before either the client object or allocator is touched, even when a
// legacy named scope is open and the current record is barriered.
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("GetOrCreate(StatePtr)");
#endif
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.
// P3a: resolve-or-create BY HANDLE - since P13 the only resolve-or-create there is. The
// handle ARRIVED, in the call's payload, already minted by the client; this never
// touches the allocator. It indexes the slot, notices a generation that no longer
// matches (the slot was recycled, so the twin at it describes driver ids the new
// resource never made) and hands back the twin pointer.
//
// 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.
// Death stays ANNOUNCED: the family's own destroy call or the shared death notice
// releases the twin by handle, and the slot is freed by the CLIENT after that returns.
//
// No StatePtr, therefore no Entry::stateRef unless a caller that holds the object notes
// it (NoteStateForHandle): a handle-keyed entry has no frontend object to weakly hold
// by itself. P5e (§4.1) makes that stop mattering for the iteration - ForEachLive keys
// on Live && backend, so such an entry is VISIBLE to the walk and it is StateForHandle,
// asked by the one caller that needs the object, that answers null for it. Which is the
// same set the old stateRef-locking walk produced, decided at one site instead of in
// the loop.
//
// 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");
@@ -621,15 +485,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif
}
// 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.
// 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,
// THE TWO DIRECTIONS ARE NOT SYMMETRIC. 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
@@ -671,31 +531,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return entry->Live ? entry->Gen : 0;
}
// Remember/read the frontend object last used by the monolith sync path.
// Handles supplied to the server never need this weak state. Neither method
// touches the allocator, so each carries its own unconditional apply guard.
void NoteStateForHandle(MG_Pipe::MGPipeHandle handle, const StatePtr& stateObj) {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("NoteStateForHandle");
#endif
if (MG_Pipe::MGPipeHandleIsNull(handle)) return;
Entry* const entry = EntryOrNull(handle.Slot);
if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return;
entry->stateRef = stateObj;
}
// The frontend object noted for this handle, or null. The handle answers identity;
// this answers only "which object was this twin last synced from".
StatePtr StateForHandle(MG_Pipe::MGPipeHandle handle) const {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("StateForHandle");
#endif
if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr;
const Entry* const entry = EntryOrNull(handle.Slot);
if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return nullptr;
return entry->stateRef.lock();
}
// 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
@@ -711,29 +546,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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();
Entry* const entry = EntryOrNull(handle.Slot);
if (entry == nullptr || !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;
Entry* const entry = EntryOrNull(handle.Slot);
@@ -741,65 +561,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
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;
#if MOBILEGL_BUILD_DISAGGREGATED
// Resolve frontend identity only in the monolith path. A cache hit is
// subject to the same guard as a fresh allocator lookup.
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("HandleOf");
#endif
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) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd): the Find/Free pair below is monolith-only with an active transport -
// a frontend death is announced to the server by the object_death record (ct),
// and a direct call from the apply thread is Fatal{RoleViolation, "MGPipeSlots"}.
MG_Pipe::MGPipeRefuseAllocatorFromApplyThread("OnFrontendObjectDestroyed");
#endif
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;
}
// P5c (ct), CONTRACT-P5C.md §5.2: the handle-keyed half of the above, for a death that
// arrived AS A WIRE RECORD (object_death) rather than as the shared notice. The
// handle IS the resolution - it crossed in the record's payload - so the client's
@@ -851,14 +612,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// it must not have - and replaces P3a's own fn(StatePtr, BackendPtr), which handed a
// frontend SharedPtr OUT OF SERVER MEMORY on every step.
//
// WHAT THE CALLER LOST AND HOW IT GETS IT BACK, because the two are not the same thing.
// The walk no longer locks Entry::stateRef, so a caller that needs the frontend object
// asks StateForHandle(handle) for it - which is the same weak reference, read at one
// named site inside the scope that owns the debt, instead of at every step of an
// iteration. Coverage is UNCHANGED by construction: an entry ForEachLive used to skip
// because its stateRef did not lock is one whose StateForHandle answers null, and the
// caller skips it there instead.
//
// The handle is the entry's own {slot, gen}, band-aware: a composite ShaderCso's slot
// is reported as kMGPipeShaderCsoCompositeSlotBase + its band index, i.e. the slot the
// client minted, never the index into m_band.
@@ -898,7 +651,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
Vector<Entry> slots = std::move(m_slots);
Vector<Entry> band = std::move(m_band);
BackendPtr nullTwin = std::move(m_nullTwin);
ForgetHandle();
slots.clear();
band.clear();
nullTwin.reset();
@@ -934,13 +686,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
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.
// the kind, not to the table, and the client returns it once, after the release.
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();
// 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,
@@ -951,7 +698,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return false;
dead = std::move(entry->backend);
entry->backend.reset();
entry->stateRef.reset();
entry->Live = false;
}
dead.reset();
@@ -986,8 +732,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
return const_cast<BackendSlotTable*>(this)->EntryOrNull(slot);
}
// Grows the right space to hold `slot`. Every caller bounds `slot` first - the minting
// overload because the allocator produced it, the handle overload against
// Grows the right space to hold `slot`. Every caller bounds `slot` first - against
// kMaxHandleSlot - because this is the one place a client-supplied number decides an
// allocation size. A composite slot grows m_band by (slot - base) + 1, so the ordinary
// table never learns the band exists.
@@ -998,15 +743,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return table[index];
}
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
@@ -1044,31 +780,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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
+5 -37
View File
@@ -20,7 +20,7 @@
#include <Config.h>
#include <MG_State/GLState/Core.h>
#include <MG_Pipe/PipeInputsSwitch.h>
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
@@ -37,21 +37,19 @@
#include <regex>
namespace MobileGL::MG_Backend::DirectGLES {
#if MOBILEGL_BUILD_DISAGGREGATED
// read_pixels writes server reply scratch. The client refuses PACK_BUFFER before
// emission, so no frontend pack binding can affect this destination.
static const SharedPtr<MG_State::GLState::BufferObject>& SplitReadbackPackBuffer() {
static const SharedPtr<MG_State::GLState::BufferObject> none;
return none;
}
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
const FormatCapabilityCache* ActiveBackendFormatCaps() {
// Under a live split session the SERVER's private backend is what owns the context on the
// apply thread (and these reads all run there), so its cache is the authoritative one.
// Before the session lands, or under monolith transport in a disaggregated build, fall
// back to the process global exactly as the monolith path always has.
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
if (MG_Backend::BackendObject* server = MG_Remote::Server::ServerLoopInstance().Backend()) {
return &server->GetFormatCapabilities();
@@ -66,9 +64,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
"on the apply thread");
std::abort();
}
#endif
return pActiveBackendObject ? &pActiveBackendObject->GetFormatCapabilities() : nullptr;
}
#endif
namespace {
Flags<PixelFormatNormalizeOptionBit> GetForcedPixelFormatNormalizeOptions() {
@@ -108,26 +106,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
SizeT targetIndex,
Bool caveat,
FormatCapability capability) {
#if MOBILEGL_BUILD_DISAGGREGATED
const FormatCapabilityCache* activeCaps = ActiveBackendFormatCaps();
if (activeCaps == nullptr || targetIndex >= kFormatCapabilityTargetCount) {
return false;
}
#else
if (!pActiveBackendObject || targetIndex >= kFormatCapabilityTargetCount) {
return false;
}
#endif
const SizeT formatIndex = static_cast<SizeT>(internalFormat);
if (formatIndex >= kFormatCapabilityFormatCount) {
return false;
}
#if MOBILEGL_BUILD_DISAGGREGATED
const FormatCapabilityCache& cache = *activeCaps;
#else
const FormatCapabilityCache& cache = pActiveBackendObject->GetFormatCapabilities();
#endif
const FormatCapabilityFlags caps =
caveat ? cache.CaveatCaps[targetIndex][formatIndex] : cache.FullCaps[targetIndex][formatIndex];
return HasFormatCapability(caps, capability);
@@ -171,11 +159,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
using namespace MobileGL::MG_Util::TextureFormatProcessor;
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
Flags<PixelFormatNormalizeOptionBit> options;
#if MOBILEGL_BUILD_DISAGGREGATED
if (ActiveBackendFormatCaps() == nullptr || ShouldUseCaveatFormat(internalFormat, targetIndex)) {
#else
if (!pActiveBackendObject || ShouldUseCaveatFormat(internalFormat, targetIndex)) {
#endif
options = GetRuntimeFallbackNormalizeOptions(
requestedInternalFormat,
TextureImpl::GetRenderTargetNormalizeOptions(g_GLESCapabilities, targetIndex));
@@ -269,15 +253,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// not be resolved yet - a probe run then would latch "cannot tell" as "clean"
// forever. Once the backend exists, the first narrow-format image this process
// creates runs the probe on a live context.
#if MOBILEGL_BUILD_DISAGGREGATED
if (ActiveBackendFormatCaps() == nullptr) {
return false;
}
#else
if (pActiveBackendObject == nullptr) {
return false;
}
#endif
return MG_Util::SelfTest::CopyImageMirrorsPacked16FieldOrder(g_GLESFuncs);
}
@@ -315,15 +293,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!TargetRequiresRenderableFormat(targetIndex)) {
return false;
}
#if MOBILEGL_BUILD_DISAGGREGATED
if (ActiveBackendFormatCaps() != nullptr && !ShouldUseCaveatFormat(internalFormat, targetIndex)) {
return false;
}
#else
if (pActiveBackendObject && !ShouldUseCaveatFormat(internalFormat, targetIndex)) {
return false;
}
#endif
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
const Flags<PixelFormatNormalizeOptionBit> options = GetRuntimeFallbackNormalizeOptions(
requestedInternalFormat, GetRenderTargetNormalizeOptions(g_GLESCapabilities, targetIndex));
@@ -2362,15 +2334,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
static Bool StoreClientRows(SizeT dstPixelBytes, SizeT swapGroupSize, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, void* pixels, Bool applyPackImageParams, FillRow&& fillRow) {
const auto& pixelPackBufferObject =
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::Transport != MG_Config::TransportMode::Monolith
? SplitReadbackPackBuffer() :
#endif
MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
SplitReadbackPackBuffer();
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
// rows are written so skip regions of the destination stay untouched.
const auto packParams = MGB_CTX->GetPixelStoreParameters(false);
const auto packParams = MG_Pipe::gPipeInputs->GetPixelStoreParameters(false);
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
const SizeT imageRows =
-2
View File
@@ -13,7 +13,6 @@
#include <MG_Util/Texture/TextureFormatProcessor.h>
namespace MobileGL::MG_Backend::DirectGLES {
#if MOBILEGL_BUILD_DISAGGREGATED
// C6 / ID-52 / CONTRACT-P5 table 3's pActiveBackendObject row. The format-capability cache of
// THIS ROLE's backend. Under an active transport the server's own private
// BackendObject_DirectGLES owns the context on the apply thread, so a backend-internal format
@@ -24,7 +23,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// (five in Utils.cpp, ClampSamplesToBackendSupport in BackendObject_DirectGLES.cpp) go through
// this instead of dereferencing pActiveBackendObject directly.
const FormatCapabilityCache* ActiveBackendFormatCaps();
#endif
namespace DebugImpl {
class ErrorLopper {
@@ -281,7 +281,6 @@
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P9 W2-a. The store's Defined-ness follows the client's respecifies; the driver's follows the
// twin's last sync. A re-mint asks this before reading a level back, because a level defined
// after that sync (null data, or null data plus a partial upload) is not on the driver yet and
@@ -352,8 +351,8 @@
static Bool ReadWireLevelFromStagedStore(MG_Pipe::MGPipeHandle res, TextureInternalFormat logicalFormat,
TextureUploadTarget uploadTarget, GLint level, const IntVec3& extent,
GLenum format, GLenum type, Vector<Uint8>& bytes) {
auto& store = MG_Remote::Server::ServerStagedTexture();
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(res);
auto& store = MG_Record::ServerStagedTexture();
const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(res);
const auto target = static_cast<Uint16>(uploadTarget);
const auto mip = static_cast<Uint16>(level);
ReadbackChannelMapping mapping{};
@@ -361,7 +360,7 @@
const SizeT texels = static_cast<SizeT>(std::max(extent.x(), 0)) * static_cast<SizeT>(std::max(extent.y(), 0)) *
static_cast<SizeT>(std::max(extent.z(), 0));
const char* declined = nullptr;
if (!store.CopiesIntoServerStorage()) {
if (!store.Holds()) {
declined = "this arm keeps no staged store";
} else if (store.IsDriverWritten(key)) {
declined = "the driver has written the texture since its bytes crossed";
@@ -460,4 +459,3 @@
bytes = Move(result);
return true;
}
#endif // MOBILEGL_BUILD_DISAGGREGATED
@@ -12,7 +12,8 @@
#include "SubgroupSupportPolicy.h"
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
#include "MG_State/GLState/Core.h"
#include <MG_Pipe/PipeInputsSwitch.h>
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Pipe/PipeApply.h>
#include "MG_State/GLState/TextureState/TextureState.h"
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
@@ -32,11 +33,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return MG_Config::Features.MagmaR11G11B10FFallback;
}
Bool IsReleaseCurrentRequest(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
(void)dpy;
return draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT;
}
Bool IsFormatIndexValid(TextureInternalFormat format) {
return format != TextureInternalFormat::Unknown && static_cast<Int>(format) >= 0 &&
static_cast<SizeT>(format) < kFormatCapabilityFormatCount;
@@ -423,17 +419,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
Bool BackendObject_DirectVulkan::LatchIfGpuFaulted() {
#if MOBILEGL_BUILD_DISAGGREGATED
return pVulkanRenderer != nullptr && pVulkanRenderer->LatchIfGpuHung("apply");
#else
return false;
#endif
}
void BackendObject_DirectVulkan::Initialize() {
#if MOBILEGL_BUILD_DISAGGREGATED
VkBufferManager::RegisterWireResourceOps();
#if MOBILEGL_PIPE_PUSH
// P7 wave 2 package C (CONTRACT-P7 §5.5). BESIDE the resource ops and for the same
// reason: this is the one place both Magma server roles pass through - the inproc
// role and the spawn/TCP session child - and ServerLoop::CreateBackend calls it at
@@ -441,7 +432,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// before any frontend object can die unheard. A per-transport install is how one of
// the two roles ends up without it.
InstallStateObjectDeathOps();
#endif
#endif
m_initialized = true;
}
@@ -464,9 +454,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions();
if (MGB_CTX_LIVE) {
MGB_CTX->InvalidateCompileEnv();
}
MG_Pipe::MGPipeNotifyCompileEnvChanged();
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
MutableFormatCapabilities());
PrintFormatCapabilities(GetFormatCapabilities());
@@ -998,7 +986,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
funcsTable.GL.DrawElements = DrawElements;
funcsTable.GL.DrawElementsBaseVertex = DrawElementsBaseVertex;
funcsTable.GL.MultiDrawArrays = MultiDrawArrays;
funcsTable.GL.MultiDrawElements = MultiDrawElements;
funcsTable.GL.MultiDrawElementsBaseVertex = MultiDrawElementsBaseVertex;
funcsTable.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
funcsTable.GL.MultiDrawArraysIndirect = MultiDrawArraysIndirect;
@@ -1019,19 +1006,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
funcsTable.GL.ClearBufferfv = ClearBufferfv;
funcsTable.GL.ClearBufferuiv = ClearBufferuiv;
funcsTable.GL.ClearBufferiv = ClearBufferiv;
funcsTable.GL.ClearNamedFramebufferfv = ClearNamedFramebufferfv;
funcsTable.GL.ClearNamedFramebufferfi = ClearNamedFramebufferfi;
funcsTable.GL.ClearNamedFramebufferiv = ClearNamedFramebufferiv;
funcsTable.GL.ClearNamedFramebufferuiv = ClearNamedFramebufferuiv;
funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
funcsTable.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
funcsTable.GL.CopyTexImage2D = CopyTexImage2D;
funcsTable.GL.CopyTexSubImage2D = CopyTexSubImage2D;
funcsTable.GL.CopyImageSubData = CopyImageSubData;
funcsTable.GL.GenerateMipmap = GenerateMipmap;
funcsTable.GL.ReadPixels = ReadPixels;
funcsTable.GL.GetTexImage = GetTexImage;
funcsTable.GL.GetTextureImage = GetTextureImage;
funcsTable.GL.DispatchCompute = DispatchCompute;
funcsTable.GL.DispatchComputeIndirect = DispatchComputeIndirect;
funcsTable.GL.MemoryBarrier = MemoryBarrier;
@@ -1081,9 +1061,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_vulkanCaps = capabilities;
UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions();
if (MGB_CTX_LIVE) {
MGB_CTX->InvalidateCompileEnv();
}
MG_Pipe::MGPipeNotifyCompileEnvChanged();
MutableFormatCapabilities().Clear();
}
File diff suppressed because it is too large Load Diff
@@ -45,7 +45,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 GetRendererGeneration();
void BumpRendererGeneration();
#if MOBILEGL_BUILD_DISAGGREGATED && MOBILEGL_PIPE_PUSH
// P7 wave 2 package C (CONTRACT-P7 §5.5). Magma's StateObjectDeathOps table, installed
// from BackendObject_DirectVulkan::Initialize() so that GetStateObjectDeathOps() is
// non-null on every Magma role that owns a backend. See the definition for what the one
@@ -55,7 +54,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// True when the notice currently installed is this backend's. Read by MG_Test so the
// install can be asserted without a death having to happen first.
Bool StateObjectDeathOpsInstalled();
#endif
// Drops every cached program-resource reflection entry (CPU-side strings/vectors
// only, no Vulkan handles). Called at EGL teardown next to the renderer reset;
@@ -67,21 +65,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLfloat* value);
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLuint* value);
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, GLfloat depth, GLint stencil);
void Clear(GLbitfield mask);
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices);
void DrawArrays(GLenum mode, GLint first, GLsizei count);
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex);
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount);
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount);
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex);
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride);
@@ -107,11 +95,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DrawArraysIndirect(GLenum mode, const void* indirect);
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter);
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLbitfield mask, GLenum filter);
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
@@ -131,9 +114,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
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 GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,
GLint level, GLenum format, GLenum type, GLsizei bufSize, GLvoid* pixels);
// GL fence sync objects, mapped onto the renderer's frame-serial busy
// tracking: a fence captures the frame serial current at creation and is
// signaled once every command recorded under that serial has completed on
@@ -26,11 +26,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
std::atomic<Uint64> g_nextSessionId{1};
thread_local MagmaSession* t_boundSession = nullptr;
// Leak-at-exit, like the renderer it holds (GlobalObjects.cpp): nothing may tear the
// monolith's renderer down from a static destructor after the driver is gone.
MagmaSession& ProcessSession() {
MagmaSession& ProcessSessionInstance() {
static auto* session = new MagmaSession();
return *session;
}
@@ -42,12 +40,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return static_cast<Uint32>(registry.size() - 1);
}
MagmaSession& Detail::CurrentMagmaSession() {
MagmaSession* bound = t_boundSession;
return bound != nullptr ? *bound : ProcessSession();
}
MagmaSession& Detail::ProcessMagmaSession() { return ProcessSessionInstance(); }
void* Detail::SessionLocalSlot(MagmaSession& session, Uint32 index) {
void* Detail::SessionLocalSlotSlow(MagmaSession& session, Uint32 index) {
if (index >= session.m_slots.size()) session.m_slots.resize(Registry().size(), nullptr);
void*& slot = session.m_slots[index];
if (slot == nullptr) slot = Registry()[index].construct();
@@ -63,25 +58,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
MagmaSession::~MagmaSession() {
MagmaSession* const previous = t_boundSession;
t_boundSession = this;
MagmaSession* const previous = Detail::t_boundMagmaSession;
Detail::t_boundMagmaSession = this;
const auto& registry = Registry();
for (SizeT i = m_slots.size(); i-- > 0;) {
if (m_slots[i] != nullptr) registry[i].destroy(m_slots[i]);
m_slots[i] = nullptr;
}
// Never back to this session: it is gone.
t_boundSession = previous == this ? nullptr : previous;
Detail::t_boundMagmaSession = previous == this ? nullptr : previous;
}
MagmaSessionScope::MagmaSessionScope(MagmaSession* session) : m_previous(t_boundSession) {
t_boundSession = session;
MagmaSessionScope::MagmaSessionScope(MagmaSession* session) : m_previous(Detail::t_boundMagmaSession) {
Detail::t_boundMagmaSession = session;
}
MagmaSessionScope::~MagmaSessionScope() { t_boundSession = m_previous; }
MagmaSessionScope::~MagmaSessionScope() { Detail::t_boundMagmaSession = m_previous; }
void BindMagmaSessionToThisThread(MagmaSession* session) { t_boundSession = session; }
void BindMagmaSessionToThisThread(MagmaSession* session) { Detail::t_boundMagmaSession = session; }
MagmaSession* BoundMagmaSession() { return t_boundSession; }
MagmaSession* BoundMagmaSession() { return Detail::t_boundMagmaSession; }
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -34,9 +34,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
using SessionLocalDestroy = void (*)(void*);
// Registers one SessionLocal<T> and returns its slot. Called from static initialization.
Uint32 RegisterSessionLocal(SessionLocalConstruct construct, SessionLocalDestroy destroy);
// The calling thread's bound session (MagmaSessionScope, BindMagmaSessionToThisThread), null
// when it has none. P15: visible here, and constant-initialized, so the read inlines into
// every SessionLocal access and one function's accesses can share a single TLS address
// computation instead of paying one out-of-line call each.
inline constinit thread_local MagmaSession* t_boundMagmaSession = nullptr;
MagmaSession& ProcessMagmaSession();
// The calling thread's session, or the process-wide default.
MagmaSession& CurrentMagmaSession();
void* SessionLocalSlot(MagmaSession& session, Uint32 index);
inline MagmaSession& CurrentMagmaSession() {
MagmaSession* const bound = t_boundMagmaSession;
return bound != nullptr ? *bound : ProcessMagmaSession();
}
inline void* SessionLocalSlot(MagmaSession& session, Uint32 index);
void* SessionLocalSlotSlow(MagmaSession& session, Uint32 index);
} // namespace Detail
// One served session's copy of every SessionLocal. Slots are built lazily on first use and
@@ -55,12 +65,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
private:
friend void* Detail::SessionLocalSlot(MagmaSession& session, Uint32 index);
friend void* Detail::SessionLocalSlotSlow(MagmaSession& session, Uint32 index);
Vector<void*> m_slots;
Uint64 m_id = 0;
};
// Makes `session` the calling thread's Magma session for the scope's lifetime, restoring the
// previous binding (usually none) afterwards.
// A built slot is returned in line; a missing one is built out of line.
inline void* Detail::SessionLocalSlot(MagmaSession& session, Uint32 index) {
if (index < session.m_slots.size()) {
if (void* const slot = session.m_slots[index]) return slot;
}
return SessionLocalSlotSlow(session, index);
}
class MagmaSessionScope {
public:
explicit MagmaSessionScope(MagmaSession* session);
@@ -7,9 +7,7 @@
// End of Source File Header
#include "BufferArena.h"
#if MOBILEGL_BUILD_DISAGGREGATED
#include <Config.h>
#endif
namespace MobileGL::MG_Backend::DirectVulkan {
Bool BufferArena::Initialize(const BufferArenaDesc& desc) {
@@ -128,14 +126,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
bufferDesc.usage = m_desc.usage;
bufferDesc.memoryUsage = m_desc.memoryUsage;
bufferDesc.allocationFlags = m_desc.allocationFlags;
#if MOBILEGL_BUILD_DISAGGREGATED
// Split staging may also be the destination of a GPU UBO range copy.
// Persistent host writes must be coherent: Upload deliberately writes
// the mapped pointer without a per-range VMA flush.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && m_desc.persistentlyMapped) {
if (m_desc.persistentlyMapped) {
bufferDesc.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
}
#endif
if (!buffer.Create(bufferDesc)) {
return false;
}
@@ -7,9 +7,7 @@
// End of Source File Header
#include "FrameContext.h"
#if MOBILEGL_BUILD_DISAGGREGATED
#include <Config.h>
#endif
namespace MobileGL::MG_Backend::DirectVulkan {
VkResult FrameContext::Initialize(VkDevice device, VkCommandPool commandPool, Uint32 frameCount) {
@@ -110,7 +108,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = flags;
// P15: a frame recording is submitted once and then reset, so the driver may skip whatever
// it keeps for re-submission.
beginInfo.flags = flags | VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
beginInfo.pInheritanceInfo = pInheritanceInfo;
VK_VERIFY(vkBeginCommandBuffer(frame.commandBuffer, &beginInfo), "BeginCommandRecording, vkBeginCommandBuffer");
@@ -139,6 +139,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VK_VERIFY(vkResetCommandBuffer(frame.preCommandBuffer, 0), "BeginPreCommandRecording, vkResetCommandBuffer");
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
VK_VERIFY(vkBeginCommandBuffer(frame.preCommandBuffer, &beginInfo),
"BeginPreCommandRecording, vkBeginCommandBuffer");
frame.isPreCommandRecording = true;
@@ -224,13 +225,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkImageMemoryBarrier presentBarrier{};
presentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
presentBarrier.srcAccessMask = 0;
#if MOBILEGL_BUILD_DISAGGREGATED
// A layout transition writes the image too. Order it after the wire
// render pass's storeOp, clears and copies instead of relying on the old
// per-draw queue-idle path. This applies even when contents are discarded.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith)
presentBarrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
#endif
presentBarrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
presentBarrier.dstAccessMask = 0;
presentBarrier.oldLayout = oldLayout;
presentBarrier.newLayout = presentLayout;
@@ -243,12 +241,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
presentBarrier.subresourceRange.baseArrayLayer = 0;
presentBarrier.subresourceRange.layerCount = 1;
vkCmdPipelineBarrier(commandBuffer,
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::Transport != MG_Config::TransportMode::Monolith
? VK_PIPELINE_STAGE_ALL_COMMANDS_BIT : VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
#else
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
#endif
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
nullptr, 0, nullptr, 1, &presentBarrier);
@@ -309,7 +302,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& frame = GetCurrent();
const VkResult result = vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, frame.imageAvailableSemaphore,
VK_NULL_HANDLE, &outImageIndex);
if (result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR) frame.imageAvailableSemaphoreConsumed = false;
if (result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR) {
frame.imageAvailableSemaphoreConsumed = false;
frame.acquiredImageReferenced = false;
}
return result;
}
@@ -340,6 +336,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
frame.imageAvailableSemaphoreConsumed = false;
frame.acquiredImageReferenced = false;
const VkResult resetResult = vkResetFences(device, 1, &frame.imageInFlightFence);
// Hand the acquire's own code back so the caller can schedule a rebuild.
return resetResult == VK_SUCCESS ? result : resetResult;
@@ -483,6 +480,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
frame.hasCommandBufferRecorded = false;
frame.isCommandRecording = false;
frame.imageAvailableSemaphoreConsumed = false;
frame.acquiredImageReferenced = false;
return VK_SUCCESS;
}
@@ -501,5 +499,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
frame.isCommandRecording = false;
frame.hasCommandBufferRecorded = false;
frame.imageAvailableSemaphoreConsumed = false;
frame.acquiredImageReferenced = false;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -25,8 +25,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
virtual void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) = 0;
};
// The stages the acquire wait blocks. Every first access of an acquired swapchain image is
// a layout transition out of PRESENT_SRC_KHR or UNDEFINED whose source scope includes these
// stages (TransitionWireImage, TransitionToPresent), and that chain is what orders the
// access after the presentation engine's release. Work at other stages, in this and later
// submissions, does not wait for the release.
static constexpr VkPipelineStageFlags kAcquireWaitStages =
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT;
struct SubmitInfoPacket {
VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkPipelineStageFlags waitDstStageMask = kAcquireWaitStages;
VkSemaphore waitSemaphore = VK_NULL_HANDLE;
VkSemaphore signalSemaphore = VK_NULL_HANDLE;
// [0] = pre-pass command buffer (when recorded), then the frame
@@ -67,6 +75,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool isPreCommandRecording = false;
Bool hasPreCommandBufferRecorded = false;
Bool imageAvailableSemaphoreConsumed = false;
// The open recording references the acquired swapchain image. Only a submission that
// does (or the present's) waits on imageAvailableSemaphore: the frame's work recorded
// before its first use of the image goes to the queue without the wait, so it never
// queues behind the release of an image the display still scans out.
Bool acquiredImageReferenced = false;
// Command buffers submitted mid-frame (FlushPendingCommands),
// appended in submit order; freed once their submission is known
// complete (fence wait or completion poll).
@@ -96,7 +96,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
GpuProgressMarkers::Bracket GpuProgressMarkers::Acquire() {
GpuProgressMarkers::Bracket GpuProgressMarkers::Acquire(Uint64 completedSubmitIndex) {
Bracket bracket;
if (m_device == VK_NULL_HANDLE) return bracket;
const std::lock_guard<std::mutex> lock(m_mutex);
@@ -106,7 +106,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (slot.Claimed) continue;
// Retire what finished since the watch last looked: frames come faster than its polls.
if (slot.InFlight && vkGetEventStatus(m_device, slot.Finished) == VK_EVENT_SET) slot.InFlight = false;
if (!slot.InFlight && free == nullptr) free = &slot;
// Free once its submission is complete by fence, not by the event read (see the header).
if (!slot.InFlight && slot.SubmitIndex <= completedSubmitIndex && free == nullptr) free = &slot;
}
if (free == nullptr) {
if (!AddSlot()) return bracket;
@@ -125,7 +126,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return bracket;
}
void GpuProgressMarkers::Submitted(const Bracket& bracket) {
void GpuProgressMarkers::Submitted(const Bracket& bracket, Uint64 submitIndex) {
if (!bracket.Valid()) return;
const std::lock_guard<std::mutex> lock(m_mutex);
if (bracket.Slot >= m_slots.size()) return;
@@ -133,6 +134,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
slot.Claimed = false;
slot.InFlight = true;
slot.Dirty = true;
slot.SubmitIndex = submitIndex;
slot.Id = m_nextId++;
}
@@ -50,9 +50,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// The two buffers to put first and last in one submission (an invalid bracket when off or
// when every slot is in flight: the submission then goes unwatched).
Bracket Acquire();
// The submission carrying `bracket` was accepted by vkQueueSubmit / was not submitted.
void Submitted(const Bracket& bracket);
//
// A slot is reused only once the submission it last bracketed is COMPLETE by its fence
// (`completedSubmitIndex`, the renderer's contiguous completed-submit floor), not merely once
// the host has read its `finished` event as set. A host read of an event gives no device-side
// ordering: nothing orders the bracket's `started` set (TOP_OF_PIPE, first buffer) against
// its `finished` set, so a host vkResetEvent plus a re-set in a later submission could
// overlap a set the earlier submission has not executed yet. The synchronization validator
// reports that as SYNC-vkCmdSetEvent-missingbarrier-set. Event reads still drive the watch.
Bracket Acquire(Uint64 completedSubmitIndex);
// The submission carrying `bracket` (renderer submit index `submitIndex`) was accepted by
// vkQueueSubmit / was not submitted.
void Submitted(const Bracket& bracket, Uint64 submitIndex);
void Abandon(const Bracket& bracket);
// The watch named one of this device's submissions as hanging the GPU.
@@ -74,6 +83,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool InFlight = false; // submitted and not yet seen finished
Bool Claimed = false; // handed out by Acquire, not yet submitted or abandoned
Bool Dirty = false; // its events were set by a past submission: reset before reuse
Uint64 SubmitIndex = 0; // the renderer submit index of the submission it last bracketed
};
static constexpr Uint32 kMaxSlots = 16;
@@ -10,18 +10,17 @@
#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.
// Magma's arm selector for the P2 Track H / render-state re-keys (P2 brief D14). (The {slot, gen}
// mint that used to live here keyed the frontend arm's VAO memos and went with that arm in P13
// W6; the record arm keys every object on the applier's own handles.)
//
// Two switches decide which arm a re-keyed site runs, and they are NOT the same switch:
//
@@ -44,7 +43,6 @@
// 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;
@@ -82,8 +80,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// 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)) {
if (!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 "
@@ -93,18 +90,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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 "
@@ -115,497 +103,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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 still has no death notification, and that is now a narrower statement than
// it was. P7 wave 2 package C gives DirectVulkan a StateObjectDeathOps table
// (DirectVulkan.cpp's g_magmaStateObjectDeathOps, CONTRACT-P7 §5.5), so
// NotifyStateObjectDestroyed DOES have a consumer on this backend - but that consumer's
// one job is to emit the `object_death` RECORD so the server drops its twin, and it
// never touches these per-renderer identity tables: they are the SERVER-side renderer's
// own, the notice is raised on the CLIENT thread, and an Acquire/retire from the wrong
// side is precisely the cross-role read CONTRACT-P5C §3.1 forbids. So a slot here still
// returns to the free list only through OnFrameBoundary's age sweep (kSweepInterval
// 256, kRetireAgeBoundaries 1024, below), and everything the two bullets below derive
// from that is unchanged;
// * 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.
// P4a (BRIEF-P4A.md D-I2, G8): WHICH KINDS THIS ANSWER COVERS, and it is not "all of them".
//
// P4a mints six more client-side kinds - Texture, Renderbuffer, Framebuffer, SamplerCso,
// SamplerViewCso and ShaderCso - and requires the ABA control to defeat "the identity half
// of P4a's memo keys as well", because a control that only defeats the guards a phase
// RETIRED says nothing about the key that phase SHIPS.
//
// On Magma there is no such key to defeat, and that is a fact about the roadmap rather than
// an omission here. MagmaPipeIdentityTables below mints exactly TWO kinds,
// VertexElementsCso and Buffer; a texture, a framebuffer, a sampler, a view and a program
// are all still reached from their frontend objects on this backend, and moving them onto
// handles is P7's work (ROADMAP.md:24 - "Magma anything"; P4a leaves MG_Backend/DirectVulkan
// untouched apart from this file). So the honest statement is per KIND, and it is spelled as
// code rather than as a comment so that a caller cannot read the blanket answer above and
// conclude the knob covers its kind:
//
// * for the two kinds this backend really keys on {slot, gen}, the knob defeats the
// identity exactly as it always has (MagmaPipeClaimSlotMemos);
// * for P4a's six there is nothing here to defeat, so the answer is FALSE - and
// MG_IntegrationTest's HandleRecycleScenario reads that through its own build probe and
// makes those cases' AbaControl arm assert the CORRECT pixels while SAYING that it is
// not controlling anything for that kind. It does not assert a corruption that no code
// on this tree can produce, which would be a permanently red always-on lane.
//
// WHAT MAKES IT TRUE LATER, in one sentence, so the next reader does not have to derive it:
// when a backend grows a Features.PipeHandleAbaControl consumer over its P4a object slot
// tables - one `if` in GetOrCreate / FindByHandle, the shape MagmaPipeClaimSlotMemos already
// has for vertex input - this function's per-kind answer becomes that consumer's, the
// integration probe finds the consumer, and the six cases flip to expecting the corruption.
inline Bool MagmaPipeAbaControlDefeatsIdentity() {
return MG_Config::Features.PipeHandleAbaControl;
}
// WHICH KINDS THIS BACKEND ACTUALLY KEYS ON {slot, gen}, and therefore which kinds the knob
// above has an identity to defeat at all. `kind` is MG_Pipe::MGPipeKind.
//
// EXHAUSTIVE, WITH NO `default:`, for MG_IntegrationTest/Harness/PipeSlotPeek.cpp's reason:
// a kind added to MGPipeKind without a decision here must be a -Wswitch warning in this
// file rather than a row that silently inherits somebody else's answer. Being wrong in the
// "covered" direction is the expensive one - a control asserting a corruption nobody can
// produce is a permanently red always-on lane - so an undecided kind must never read true,
// and with no `default:` there is no arm for it to read true from.
//
// constexpr AND PINNED BY static_assert BELOW, which is what stops it rotting the way a
// predicate with no caller does: MagmaPipeIdentityTables mints exactly two kinds, the
// asserts say so in both directions, and the file no longer compiles if the tables and this
// statement of them ever part company. (Review F-m5: the earlier form had no caller at all
// and could not make anything red or green.)
inline constexpr Bool MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind kind) {
switch (kind) {
// The two MagmaPipeIdentityTables really mints.
case MG_Pipe::MGPipeKind::VertexElementsCso:
case MG_Pipe::MGPipeKind::Buffer:
return true;
// P4a's six object classes: still reached from their frontend objects on this
// backend (Magma's object paths are P7, ROADMAP.md:24), so there is no key here for
// the knob to defeat.
case MG_Pipe::MGPipeKind::Texture:
case MG_Pipe::MGPipeKind::Renderbuffer:
case MG_Pipe::MGPipeKind::Framebuffer:
case MG_Pipe::MGPipeKind::SamplerCso:
case MG_Pipe::MGPipeKind::SamplerViewCso:
case MG_Pipe::MGPipeKind::ShaderCso:
// ...and everything else this backend does not mint a handle for.
case MG_Pipe::MGPipeKind::None:
case MG_Pipe::MGPipeKind::Xfb:
case MG_Pipe::MGPipeKind::RenderStateCso:
case MG_Pipe::MGPipeKind::Fence:
case MG_Pipe::MGPipeKind::Query:
case MG_Pipe::MGPipeKind::Context:
case MG_Pipe::MGPipeKind::KindCount:
return false;
}
return false;
}
static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::VertexElementsCso),
"MagmaPipeIdentityTables mints VertexElementsCso: the knob has an identity to "
"defeat for it");
static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Buffer),
"MagmaPipeIdentityTables mints Buffer: the knob has an identity to defeat for it");
static_assert(!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Texture) &&
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Renderbuffer) &&
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Framebuffer) &&
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerCso) &&
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerViewCso) &&
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::ShaderCso),
"P4a's six object classes are not keyed on {slot, gen} on this backend, so "
"HandleRecycleScenario's six AbaControl arms must NOT expect a corruption here. "
"Wiring one of them is what flips this assert, this predicate and that arm - and "
"MG_IntegrationTest's two-symbol probe over MG_Backend/DirectVulkan is what "
"carries the answer into the lane");
// THERE IS DELIBERATELY NO PER-KIND WRAPPER HERE, and review F-v2-m3 is why. An earlier
// round carried `MagmaPipeAbaControlCoversKind(kind)` - the conjunction of the two
// statements above - and it had no caller anywhere in the tree: the knob's only two
// consumers (VulkanRenderer.cpp's VAO draw memo and VertexInputStateFactory.cpp's pipeline
// key) each hold ONE kind, VertexElementsCso, by construction, so the kind is not a
// variable at either site. A conjunction no build ever evaluates cannot be pinned the way
// the predicate above is pinned - it is not constexpr, because it reads MG_Config::Features,
// so no static_assert can reach it - which makes it exactly the rot F-m5 was raised about,
// one level up: an `&&` whose operands could be inverted or dropped with nothing to say so.
//
// The two pieces stand alone instead, and each is pinned by something that runs:
// MagmaPipeAbaControlKindIsRekeyedHere is constexpr and asserted in BOTH directions by the
// three static_asserts above, which compile in every Magma build; MagmaPipeAbaControlDefeats
// Identity is the knob, and its two consumers are what make it true or false. A call site
// that ever does hold a variable kind writes the `&&` there, where a build will run it.
// 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
@@ -3,7 +3,6 @@
#include <MG_State/GLState/ProgramState/ProgramObject.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Pipe/PipeApply.h>
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
#include "../DirectVulkanResourceState.h"
@@ -12,112 +11,110 @@
// the archive (LinkArtifacts::storageBlocks), so the SPIR-V reflector is off the Magma wire
// draw path entirely. A future edit that re-adds the include is re-adding a per-draw reflect,
// because a MagmaProgramSource is constructed once per draw and can memoise nothing.
#endif
namespace MobileGL::MG_Backend::DirectVulkan {
#if MOBILEGL_BUILD_DISAGGREGATED
// A borrowed view, valid only while its frontend object or server record lives.
// It never constructs a ProgramObject on the server. Post-link mutable values
// come from the record tails, not from the archive's link-time defaults.
class MagmaProgramSource {
public:
using Program = MG_State::GLState::ProgramObject;
MagmaProgramSource(const Program& program) : m_frontend(&program) {}
MagmaProgramSource(MG_Pipe::MGPipeHandle handle, const MG_Pipe::MGPipeShaderCsoRecord& record)
: m_handle(handle), m_record(&record) {
MOBILEGL_ASSERT(record.Archive != nullptr, "Magma program record has no archive");
}
Bool IsWire() const { return m_record != nullptr; }
MG_Pipe::MGPipeHandle Handle() const { return m_handle; }
const Program* Frontend() const { return m_frontend; }
Uint GetExternalIndex() const { return IsWire() ? m_handle.Slot : m_frontend->GetExternalIndex(); }
Uint GetExternalIndex() const { return m_handle.Slot; }
Uint64 GetLifetimeId() const {
return IsWire() ? (Uint64(m_handle.Gen) << 32) | m_handle.Slot : m_frontend->GetLifetimeId();
return (Uint64(m_handle.Gen) << 32) | m_handle.Slot;
}
Bool GetLinkStatus() const { return IsWire() ? m_record->Desc.LinkStatus != 0 : m_frontend->GetLinkStatus(); }
Bool GetSpirvStatus() const { return IsWire() ? m_record->Desc.SpirvStatus != 0 : m_frontend->GetSpirvStatus(); }
Bool PointSizeDemoted() const { return IsWire() ? m_record->Desc.PointSizeDemoted != 0 : m_frontend->PointSizeDemoted(); }
Bool GetLinkStatus() const { return m_record->Desc.LinkStatus != 0; }
Bool GetSpirvStatus() const { return m_record->Desc.SpirvStatus != 0; }
Bool PointSizeDemoted() const { return m_record->Desc.PointSizeDemoted != 0; }
Bool GetSpirvValidationEnabled() const {
return IsWire() ? m_record->Desc.EnableSpirvValidation != 0 : m_frontend->GetSpirvValidationEnabled();
return m_record->Desc.EnableSpirvValidation != 0;
}
const Vector<Vector<Uint>>& GetGeneratedSpirv() const {
return IsWire() ? m_record->Archive->Spirv.generatedSpirv : m_frontend->GetGeneratedSpirv();
return m_record->Archive->Spirv.generatedSpirv;
}
Vector<ShaderStage> GetLinkedShaderStages() const {
if (!IsWire()) return m_frontend->GetLinkedShaderStages();
Vector<ShaderStage> result;
for (const auto stage : m_record->Archive->LinkedStages) result.push_back(static_cast<ShaderStage>(stage));
return result;
}
Bool HasLinkedShaderStage(ShaderStage stage) const {
if (!IsWire()) return m_frontend->HasLinkedShaderStage(stage);
const auto& stages = m_record->Archive->LinkedStages;
return std::find(stages.begin(), stages.end(), static_cast<Uint32>(stage)) != stages.end();
}
// The view has no lifetime of its own, so the memo lives in the server record it views
// (MGPipeShaderCsoRecord::BackendHashMemo), never in the client's program object: an entry
// answers only while the record's Serial and BindingsSerial are the ones it was taken at.
Bool GetBackendHashMemo(Uint flags, Uint64& hash) const {
// A view has no lifetime in which a memo can safely persist. The server
// factory's content cache still owns compiled programs; hash from current
// archive + binding tails rather than borrowing the client's mutable memo.
return !IsWire() && m_frontend->GetBackendHashMemo(flags, hash);
for (const auto& entry : m_record->BackendHashMemo) {
if (entry.Valid && entry.Key == flags && entry.Serial == m_record->Serial &&
entry.BindingsSerial == m_record->BindingsSerial) {
hash = entry.Hash;
return true;
}
}
return false;
}
void SetBackendHashMemo(Uint flags, Uint64 hash) const {
if (!IsWire()) m_frontend->SetBackendHashMemo(flags, hash);
auto& memo = m_record->BackendHashMemo;
auto& entry = memo[m_record->BackendHashMemoNext++ % memo.size()];
entry.Serial = m_record->Serial;
entry.BindingsSerial = m_record->BindingsSerial;
entry.Hash = hash;
entry.Key = flags;
entry.Valid = true;
}
Uint64 GetBackendStateVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetBackendStateVersion(); }
Uint64 GetBlockBindingVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetBlockBindingVersion(); }
Uint64 GetImageUnitVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetImageUnitVersion(); }
Uint64 GetLinkVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetLinkVersion(); }
Uint64 GetBackendStateVersion() const { return m_record->Serial; }
Uint64 GetBlockBindingVersion() const { return m_record->BindingsSerial; }
Uint64 GetImageUnitVersion() const { return m_record->BindingsSerial; }
Uint64 GetLinkVersion() const { return m_record->Serial; }
const void* GetUBOData() const { return IsWire() ? m_record->GlobalConstants.data() : m_frontend->GetUBOData(); }
Uint GetUBOSize() const { return IsWire() ? static_cast<Uint>(m_record->GlobalConstants.size()) : m_frontend->GetUBOSize(); }
Uint32 GetUBOContentVersion() const { return IsWire() ? m_record->GlobalConstantsVersion : m_frontend->GetUBOContentVersion(); }
const void* GetUBOData() const { return m_record->GlobalConstants.data(); }
Uint GetUBOSize() const { return static_cast<Uint>(m_record->GlobalConstants.size()); }
Uint32 GetUBOContentVersion() const { return m_record->GlobalConstantsVersion; }
Int GetActiveUniformBlocksCount() const {
return IsWire() ? static_cast<Int>(Link().glBlockIndexToTProgram.size()) : m_frontend->GetActiveUniformBlocksCount();
return static_cast<Int>(Link().glBlockIndexToTProgram.size());
}
Uint GetUniformBlockIndex(const char* name) const {
if (!IsWire()) return m_frontend->GetUniformBlockIndex(name);
auto found = Link().uniformBlockIndexByName.find(name);
if (found == Link().uniformBlockIndexByName.end()) found = Link().uniformBlockIndexByName.find(String(name) + "[0]");
return found == Link().uniformBlockIndexByName.end() ? GL_INVALID_INDEX : found->second;
}
Uint GetUniformBlockBinding(Uint index) const {
if (!IsWire()) return m_frontend->GetUniformBlockBinding(index);
return index < m_record->BlockBindings.size() ? static_cast<Uint>(m_record->BlockBindings[index]) : 0;
}
const String& GetUniformBlockName(Uint index) const {
if (!IsWire()) return m_frontend->GetUniformBlockName(index);
const auto* block = Block(index);
static const String empty;
return block ? block->name : empty;
}
Uint GetUBOSizeAt(Uint index) const {
if (!IsWire()) return m_frontend->GetUBOSizeAt(index);
const auto* block = Block(index);
return block ? (static_cast<Uint>(block->size) + 15u) & ~15u : 0;
}
Bool IsValidUniformLocation(Int location) const {
return IsWire() ? Program::IsValidUniformLocation(Link(), location) : m_frontend->IsValidUniformLocation(location);
return Program::IsValidUniformLocation(Link(), location);
}
Bool UniformLocationsAliasSameUniform(Int a, Int b) const {
if (!IsWire()) return m_frontend->UniformLocationsAliasSameUniform(a, b);
return IsValidUniformLocation(a) && IsValidUniformLocation(b) &&
Link().uniformIndexInTProgram[a] == Link().uniformIndexInTProgram[b];
}
GLenum GetUniformType(Uint location) const {
if (!IsWire()) return m_frontend->GetUniformType(location);
return IsValidUniformLocation(static_cast<Int>(location))
? Program::UniformAtIn(Link(), Link().uniformIndexInTProgram[location]).glDefineType : 0;
}
Int GetUniformSamplerOrImageUnitIndex(Uint location) const {
if (!IsWire()) return m_frontend->GetUniformSamplerOrImageUnitIndex(location);
const auto& units = m_record->SamplerUnits;
const auto found = std::lower_bound(units.begin(), units.end(), location,
[](const MG_Pipe::MGPProgramSamplerUnit& unit, Uint value) { return unit.Location < value; });
return found != units.end() && found->Location == location ? found->Unit : -1;
}
Int GetUniformLocation(const String& name) const {
if (!IsWire()) return m_frontend->GetUniformLocation(name);
const auto& locations = Link().uniformLocations;
auto found = locations.find(name);
if (found != locations.end()) return static_cast<Int>(found->second);
@@ -145,17 +142,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return UniformLocationsAliasSameUniform(base, location) ? location : -1;
}
SizeT GetTransformFeedbackVaryingCount() const { return IsWire() ? Link().xfbVaryings.size() : m_frontend->GetTransformFeedbackVaryingCount(); }
SizeT GetTransformFeedbackVaryingCount() const { return Link().xfbVaryings.size(); }
const Vector<MG_State::GLState::XfbVarying>& GetTransformFeedbackVaryings() const {
return IsWire() ? Link().xfbVaryings : m_frontend->GetTransformFeedbackVaryings();
return Link().xfbVaryings;
}
SizeT GetTransformFeedbackBufferCount() const { return IsWire() ? Link().xfbStrides.size() : m_frontend->GetTransformFeedbackBufferCount(); }
SizeT GetTransformFeedbackBufferCount() const { return Link().xfbStrides.size(); }
Uint32 GetTransformFeedbackStride(Uint index) const {
if (!IsWire()) return m_frontend->GetTransformFeedbackStride(index);
return index < Link().xfbStrides.size() ? Link().xfbStrides[index] : 0;
}
GLenum GetTransformFeedbackBufferMode() const {
return IsWire() ? Link().xfbBufferMode : m_frontend->GetTransformFeedbackBufferMode();
return Link().xfbBufferMode;
}
// P7 wave 2 package C, OQ-8 (CONTRACT-P7 §5.3): READ OUT OF THE ARCHIVE.
//
@@ -167,7 +163,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// the archive (LinkArtifacts::storageBlocks), which takes the SPIR-V reflector off the
// wire draw path entirely - this header no longer includes spirv_reflect.h at all.
Uint GetShaderStorageBlockIndex(const String& name) const {
if (!IsWire()) return DirectVulkan::GetShaderStorageBlockIndex(*m_frontend, name);
const auto& blocks = Link().storageBlocks;
// The archive carries NORMALISED names (the array subscript stripped), because
// that is the spelling SPIRV-Reflect's type_name gives and the spelling
@@ -188,7 +183,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
: static_cast<Uint>(found - blocks.begin());
}
Uint GetShaderStorageBlockBinding(Uint index) const {
if (!IsWire()) return DirectVulkan::GetShaderStorageBlockBinding(*m_frontend, index);
const auto& blocks = Link().storageBlocks;
if (index >= blocks.size()) return 0;
const auto& block = blocks[index];
@@ -210,7 +204,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return tIndex >= 0 && static_cast<SizeT>(tIndex) < Link().blockReflection.size()
? &Link().blockReflection[tIndex] : nullptr;
}
const Program* m_frontend = nullptr;
MG_Pipe::MGPipeHandle m_handle = MG_Pipe::kMGPipeNullHandle;
const MG_Pipe::MGPipeShaderCsoRecord* m_record = nullptr;
// P7 OQ-8: the two mutables that memoised the per-draw reflect are gone with it. They
@@ -218,7 +211,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// also why they never helped: a view lives for one draw, so the latch was cold every
// time it was read.
};
#else
using MagmaProgramSource = MG_State::GLState::ProgramObject;
#endif
}
@@ -194,69 +194,69 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
PipelineFactory::HashType PipelineFactory::ComputeHash(const PipelineCreatePayload& payload) const {
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.programHash, sizeof(payload.programHash)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.vertexInputHash, sizeof(payload.vertexInputHash)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.pipelineLayout, sizeof(payload.pipelineLayout)));
#if MOBILEGL_BUILD_DISAGGREGATED
// P15: one read of the thread_local per hash (emulated TLS: every read is a call).
XXH64_state_t* const hashState = m_hashState;
XXHASH_VERIFY(XXH64_reset(hashState, m_config.CacheVersion));
XXHASH_VERIFY(XXH64_update(hashState, &payload.programHash, sizeof(payload.programHash)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.vertexInputHash, sizeof(payload.vertexInputHash)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.pipelineLayout, sizeof(payload.pipelineLayout)));
if (payload.wireRenderPassCompatibilityId != 0) {
constexpr Uint64 wireDomain = 0x5749524552504b59ull;
XXHASH_VERIFY(XXH64_update(m_hashState, &wireDomain, sizeof(wireDomain)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.wireRenderPassCompatibilityId,
XXHASH_VERIFY(XXH64_update(hashState, &wireDomain, sizeof(wireDomain)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.wireRenderPassCompatibilityId,
sizeof(payload.wireRenderPassCompatibilityId)));
} else
#endif
{
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.renderPass, sizeof(payload.renderPass)));
}
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.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.topology, sizeof(payload.topology)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.minSampleShading, sizeof(payload.minSampleShading)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.sampleMask, sizeof(payload.sampleMask)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.subpass, sizeof(payload.subpass)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.topology, sizeof(payload.topology)));
XXHASH_VERIFY(
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.passthroughTessControlKey,
XXH64_update(hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
XXHASH_VERIFY(XXH64_update(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.cullMode, sizeof(payload.cullMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.viewportCount, sizeof(payload.viewportCount)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.cullMode, sizeof(payload.cullMode)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.frontFace, sizeof(payload.frontFace)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.provokingVertexMode, sizeof(payload.provokingVertexMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
XXH64_update(hashState, &payload.provokingVertexMode, sizeof(payload.provokingVertexMode)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.rasterizerDiscardEnable, sizeof(payload.rasterizerDiscardEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.logicOpEnable, sizeof(payload.logicOpEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.stencilTestEnable, sizeof(payload.stencilTestEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthCompareOp, sizeof(payload.depthCompareOp)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.logicOp, sizeof(payload.logicOp)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontStencilFailOp, sizeof(payload.frontStencilFailOp)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontStencilPassOp, sizeof(payload.frontStencilPassOp)));
XXH64_update(hashState, &payload.rasterizerDiscardEnable, sizeof(payload.rasterizerDiscardEnable)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.logicOpEnable, sizeof(payload.logicOpEnable)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.stencilTestEnable, sizeof(payload.stencilTestEnable)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.depthCompareOp, sizeof(payload.depthCompareOp)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.logicOp, sizeof(payload.logicOp)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.frontStencilFailOp, sizeof(payload.frontStencilFailOp)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.frontStencilPassOp, sizeof(payload.frontStencilPassOp)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.frontStencilDepthFailOp, sizeof(payload.frontStencilDepthFailOp)));
XXH64_update(hashState, &payload.frontStencilDepthFailOp, sizeof(payload.frontStencilDepthFailOp)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.frontStencilCompareOp, sizeof(payload.frontStencilCompareOp)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.backStencilFailOp, sizeof(payload.backStencilFailOp)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.backStencilPassOp, sizeof(payload.backStencilPassOp)));
XXH64_update(hashState, &payload.frontStencilCompareOp, sizeof(payload.frontStencilCompareOp)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.backStencilFailOp, sizeof(payload.backStencilFailOp)));
XXHASH_VERIFY(XXH64_update(hashState, &payload.backStencilPassOp, sizeof(payload.backStencilPassOp)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
XXH64_update(hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
XXH64_update(hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth)));
XXH64_update(hashState, &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth)));
if (payload.colorAttachmentCount > 0) {
XXHASH_VERIFY(XXH64_update(
m_hashState,
hashState,
payload.colorBlendAttachments.data(),
sizeof(payload.colorBlendAttachments[0]) * payload.colorAttachmentCount));
}
return XXH64_digest(m_hashState);
return XXH64_digest(hashState);
}
VkPipeline PipelineFactory::GetOrCreatePipeline(const PipelineCreatePayload& payload) {
@@ -286,12 +286,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return VK_NULL_HANDLE;
}
m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash,
#if MOBILEGL_BUILD_DISAGGREGATED
// Wire pipelines outlive their creator pass and
// may serve another compatible pass in flight.
// Never evict them by a recycled native handle.
payload.wireRenderPassCompatibilityId != 0 ? VK_NULL_HANDLE :
#endif
payload.renderPass,
m_frameCounter});
return pipeline;
@@ -341,34 +339,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return evicted;
}
Uint32 PipelineFactory::EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses) {
if (renderPasses.empty() || m_cache.empty()) {
return 0;
}
// Sorted-batch membership test keeps a mass eviction (shader-pack switch,
// dimension exit) at one O(cache * log batch) scan instead of one full scan
// per dying pass.
Vector<VkRenderPass> sortedPasses = renderPasses;
std::sort(sortedPasses.begin(), sortedPasses.end());
Uint32 evicted = 0;
for (auto it = m_cache.begin(); it != m_cache.end();) {
if (std::binary_search(sortedPasses.begin(), sortedPasses.end(), it->second.renderPass)) {
if (it->second.pipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
}
it = m_cache.erase(it);
++evicted;
} else {
++it;
}
}
if (evicted > 0) {
MGLOG_D("PipelineFactory::EvictByRenderPasses: evicted %u pipelines for %zu destroyed render passes",
evicted, sortedPasses.size());
}
return evicted;
}
Uint32 PipelineFactory::EvictByProgramHash(HashType programHash) {
Uint32 evicted = 0;
for (auto it = m_cache.begin(); it != m_cache.end();) {
@@ -35,11 +35,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
HashType vertexInputHash = 0;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
VkRenderPass renderPass = VK_NULL_HANDLE;
#if MOBILEGL_BUILD_DISAGGREGATED
// Nonzero is an exact, renderer-lifetime compatibility identity.
// renderPass remains the valid creation handle, not the cache key.
Uint64 wireRenderPassCompatibilityId = 0;
#endif
Uint32 colorAttachmentCount = 1;
VkSampleCountFlagBits rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
// glEnable(GL_SAMPLE_SHADING) + glMinSampleShading, which Vulkan bakes into the
@@ -142,15 +140,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// VkRenderPassManager::OnPresent's sweep. Returns the number of pipelines
// destroyed so the caller can drop any memoized VkPipeline handle.
Uint32 OnFrameBoundary();
// Destroys every cached pipeline hashed on one of `renderPasses`. Only safe
// when the caller guarantees GPU idleness for them - the render-pass manager
// calls this (via the renderer) for passes its own >1024-boundary-idle sweep
// just evicted, and a pipeline hashed on those handles is only ever bound by
// draws that also hit the render-pass entries. Also closes the handle-recycling
// hazard: a recycled VkRenderPass value must never serve a stale pipeline.
// Batched: one cache scan regardless of how many passes died in the sweep.
// Returns the number destroyed (callers invalidate memos when non-zero).
Uint32 EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses);
// Destroys every cached pipeline built from the program with content hash
// `programHash`. Called from the ProgramFactory eviction path, which proves the
// same >1024-boundary idleness (the program's pipelines are only bound by draws
@@ -2235,28 +2235,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
TextureTarget ReflectImageTraitsToTextureTarget(const SpvReflectImageTraits& imageTraits) {
switch (imageTraits.dim) {
case SpvDim1D:
return imageTraits.arrayed != 0 ? TextureTarget::Texture1DArray : TextureTarget::Texture1D;
case SpvDim2D:
if (imageTraits.ms != 0) {
return imageTraits.arrayed != 0 ? TextureTarget::Texture2DMultisampleArray
: TextureTarget::Texture2DMultisample;
}
return imageTraits.arrayed != 0 ? TextureTarget::Texture2DArray : TextureTarget::Texture2D;
case SpvDim3D:
return TextureTarget::Texture3D;
case SpvDimCube:
return imageTraits.arrayed != 0 ? TextureTarget::TextureCubeMapArray : TextureTarget::TextureCubeMap;
case SpvDimBuffer:
return TextureTarget::TextureBuffer;
default:
MOBILEGL_ASSERT(false, "ProgramFactory: unsupported sampler image dim %d", imageTraits.dim);
return TextureTarget::Unknown;
}
}
Bool IsFloatStorageImageUniformType(GLenum uniformType) {
switch (uniformType) {
case GL_IMAGE_1D:
@@ -2597,26 +2575,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ProgramFactory::HashType ProgramFactory::ComputeHash(const MagmaProgramSource& program,
CompileOptionFlags flags) const {
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
// P15: one read of the thread_local per hash (emulated TLS: every read is a call).
XXH64_state_t* const hashState = m_hashState;
XXHASH_VERIFY(XXH64_reset(hashState, m_config.CacheVersion));
// We expect shader stages in program object are sorted
const auto& spirvs = program.GetGeneratedSpirv();
for (const auto& spv : spirvs) {
XXHASH_VERIFY(XXH64_update(m_hashState, spv.data(), spv.size() * sizeof(Uint)));
XXHASH_VERIFY(XXH64_update(hashState, spv.data(), spv.size() * sizeof(Uint)));
}
XXHASH_VERIFY(XXH64_update(m_hashState, &flags, sizeof(CompileOptionFlags)));
XXHASH_VERIFY(XXH64_update(hashState, &flags, sizeof(CompileOptionFlags)));
// Only FragCoordYFlip variants bake the height in, so mixing it unconditionally would
// re-key every program in the cache on a resize for no reason.
if (flags & CompileOptionBit::FragCoordYFlip) {
XXHASH_VERIFY(XXH64_update(m_hashState, &m_defaultFramebufferHeight,
XXHASH_VERIFY(XXH64_update(hashState, &m_defaultFramebufferHeight,
sizeof(m_defaultFramebufferHeight)));
}
// Include UBO block bindings in hash so different binding configurations produce different entries
const Uint32 blockCount = static_cast<Uint32>(program.GetActiveUniformBlocksCount());
XXHASH_VERIFY(XXH64_update(m_hashState, &blockCount, sizeof(blockCount)));
XXHASH_VERIFY(XXH64_update(hashState, &blockCount, sizeof(blockCount)));
for (Uint32 i = 0; i < blockCount; ++i) {
const Uint32 binding = program.GetUniformBlockBinding(i);
XXHASH_VERIFY(XXH64_update(m_hashState, &binding, sizeof(binding)));
XXHASH_VERIFY(XXH64_update(hashState, &binding, sizeof(binding)));
}
// The transform feedback capture layout is baked into the modules by
@@ -2627,18 +2607,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// hashed for a capturing compile, so nothing else changes key.
if (flags & CompileOptionBit::XfbCapture) {
for (const auto& varying : program.GetTransformFeedbackVaryings()) {
XXHASH_VERIFY(XXH64_update(m_hashState, varying.name.data(), varying.name.size()));
XXHASH_VERIFY(XXH64_update(m_hashState, &varying.bufferIndex, sizeof(varying.bufferIndex)));
XXHASH_VERIFY(XXH64_update(m_hashState, &varying.offsetBytes, sizeof(varying.offsetBytes)));
XXHASH_VERIFY(XXH64_update(hashState, varying.name.data(), varying.name.size()));
XXHASH_VERIFY(XXH64_update(hashState, &varying.bufferIndex, sizeof(varying.bufferIndex)));
XXHASH_VERIFY(XXH64_update(hashState, &varying.offsetBytes, sizeof(varying.offsetBytes)));
}
const SizeT bufferCount = program.GetTransformFeedbackBufferCount();
for (SizeT i = 0; i < bufferCount; ++i) {
const Uint32 stride = program.GetTransformFeedbackStride(static_cast<Uint32>(i));
XXHASH_VERIFY(XXH64_update(m_hashState, &stride, sizeof(stride)));
XXHASH_VERIFY(XXH64_update(hashState, &stride, sizeof(stride)));
}
}
HashType hash = XXH64_digest(m_hashState);
HashType hash = XXH64_digest(hashState);
return hash;
}
@@ -3199,11 +3179,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (descriptorKind == DescriptorBindingKind::StorageBuffer) {
const GLuint blockIndex =
#if MOBILEGL_BUILD_DISAGGREGATED
program.GetShaderStorageBlockIndex(uniformName);
#else
GetShaderStorageBlockIndex(program, uniformName);
#endif
if (blockIndex == GL_INVALID_INDEX) {
MGLOG_D("ProgramFactory::ReflectLayout: skipping inactive SSBO '%s' at binding %u",
uniformName.c_str(), binding);
@@ -3495,13 +3471,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
updateAfterBindSampledImages + updateAfterBindStorageImages;
const auto& uab = m_updateAfterBindLimits;
entry.usesUpdateAfterBind =
#if MOBILEGL_BUILD_DISAGGREGATED
// Vulkan forbids ANY update-after-bind binding in a set layout that
// contains a dynamic buffer descriptor (VUID 03001/03011). Wire draws
// retain UniformManager's per-frame, content-versioned descriptor sets;
// this selects the legal ordinary pool, not in-place mutation of a live set.
(!program.IsWire() || entry.dynamicBindings.empty()) &&
#endif
entry.dynamicBindings.empty() &&
uab.enabled && updateAfterBindSamplers <= uab.maxPerStageSamplers &&
updateAfterBindUniformBuffers <= uab.maxPerStageUniformBuffers &&
updateAfterBindStorageBuffers <= uab.maxPerStageStorageBuffers &&
@@ -13,10 +13,8 @@
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
#include "MG_State/GLState/TextureState/TextureObject2D.h"
#if MOBILEGL_PIPE_PUSH
// P5c ev: the surface-changed event's producer callback, installed by the server session.
#include <MG_Pipe/MGPipeCallbacks.h>
#endif
#if defined(__has_include)
#if __has_include(<vulkan/vk_enum_string_helper.h>)
@@ -175,7 +173,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void SwapchainObject::Create(VkDevice device, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface,
Uint32 graphicsQueueFamily, Uint32 presentQueueFamily, Uint32 minImageCountHint,
VkExtent2D desiredExtent, Optional<Int> swapInterval) {
Uint32 extraImageCount, VkExtent2D desiredExtent, Optional<Int> swapInterval) {
const auto swapchainCapabilities = GetSwapchainCapabilities(physicalDevice, surface);
MOBILEGL_ASSERT(swapchainCapabilities.IsComplete(),
"SwapchainObject::Create failed: incomplete swapchain capabilities");
@@ -200,11 +198,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_supportedPresentModes = swapchainCapabilities.presentModes;
const auto& swapchainCaps = swapchainCapabilities.capabilities;
Uint32 targetImageCount = std::max<Uint32>(minImageCountHint, swapchainCaps.minImageCount);
// The extra images count from the surface's own minimum: that is how many the platform
// keeps for itself and the application, so only images beyond it delay a buffer's reuse.
Uint32 targetImageCount =
std::max<Uint32>(minImageCountHint, swapchainCaps.minImageCount + extraImageCount);
if (swapchainCaps.maxImageCount != 0) {
targetImageCount = std::min(targetImageCount, swapchainCaps.maxImageCount);
}
MGLOG_I("Set minImageCount = %u", targetImageCount);
MGLOG_I("Set minImageCount = %u (surface min %u, max %u, extra %u)", targetImageCount,
swapchainCaps.minImageCount, swapchainCaps.maxImageCount, extraImageCount);
MGLOG_I("Swapchain currentTransform = %s",
string_VkSurfaceTransformFlagBitsKHR(swapchainCaps.currentTransform));
@@ -349,7 +351,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
break;
}
#if MOBILEGL_PIPE_PUSH
if (MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged != nullptr) {
// P5c ev (CONTRACT-P5C §4.2): with an active transport the default FBO's
// attachments are CLIENT memory and this thread may not write them - the backend
@@ -367,7 +368,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
info.IsDefault = 1;
MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged(&info);
} else
#endif
{
auto* colorTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->colorAttachment.get());
colorTex->AllocateStorage(
@@ -36,8 +36,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static const char* GetPresentModeName(VkPresentModeKHR presentMode);
void Create(VkDevice device, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, Uint32 graphicsQueueFamily,
Uint32 presentQueueFamily, Uint32 minImageCountHint, VkExtent2D desiredExtent,
Optional<Int> swapInterval);
Uint32 presentQueueFamily, Uint32 minImageCountHint, Uint32 extraImageCount,
VkExtent2D desiredExtent, Optional<Int> swapInterval);
void Shutdown(VkDevice device);
// Tells the frontend (or, under a transport, the client) the default framebuffer's size
// and formats. Create does this; a renderer switching between surfaces repeats it.
File diff suppressed because it is too large Load Diff
@@ -9,6 +9,7 @@
#pragma once
#include "ProgramFactory.h"
#include "RenderPassGuard.h"
#include "MagmaProgramSource.h"
#include "VkBufferManager.h"
#include "VkSamplerManager.h"
@@ -27,16 +28,6 @@ namespace MobileGL::MG_State::GLState {
namespace MobileGL::MG_Backend::DirectVulkan {
class UniformManager {
public:
struct SamplerBindingOverride {
Uint32 binding = 0;
Uint32 element = 0;
MG_State::GLState::ITextureObject* texture = nullptr;
const MG_State::GLState::SamplerObject* sampler = nullptr;
VkImageView imageView = VK_NULL_HANDLE;
VkImageLayout imageLayout = VK_IMAGE_LAYOUT_UNDEFINED;
Bool forceNearestFiltering = false;
};
struct SamplerImageFeedbackBinding {
Uint32 samplerBinding = 0;
Uint32 samplerElement = 0;
@@ -56,7 +47,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void Shutdown();
void BeginFrame(Uint32 frameIndex);
#if MOBILEGL_BUILD_DISAGGREGATED
// A present-less wire frame may run arbitrarily many draws. Before its
// descriptor cursor reaches this budget, the renderer retires the
// submission that last used the sets and rewinds every layout cursor.
@@ -74,10 +64,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// store inside one pass reading zero, nor privacy past kWirePrivateStoragePlaceholderCap
// placeholders (CONTRACT-P7 §12). Called after Initialize.
void SetWireInvalidStorageImageArm(Bool deviceNullDescriptor);
#endif
// A command buffer (re)began recording: descriptor bindings recorded into
// the previous buffer do not carry over, so drop the bind-dedup shadow.
void OnCommandBufferBoundary() { m_lastBindValid = false; }
// The image-write epoch the last full memory barrier made visible (see m_wireImageBarrierEpoch),
// and the note a caller that just recorded that same barrier makes.
Uint64 WireImageBarrierEpoch() const { return m_wireImageBarrierEpoch; }
void NoteWireImageBarrier() const { m_wireImageBarrierEpoch = WireImageWriteEpoch(); }
// A ProgramFactory eviction just destroyed this layout: purge every frame
// slot's cached descriptor sets for it, so a recycled handle value can never
// stale-hit sets written for the dead layout's bindings. The sets are
@@ -102,48 +95,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 textureLifetimeId = 0;
Uint64 samplerLifetimeId = 0;
};
Bool CollectSampledTextures(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures,
Vector<SampledBindingRecord>* outBindingRecords = nullptr);
// Shadow-compare for the SetupDraw fast path: re-runs the CollectSampledTextures
// walk and reports whether every visited binding still resolves to the recorded
// (texture, effective sampler) pair. A texture bind generation bump alone (e.g. a
// redundant glBindSampler, which always bumps it) does not prove the sampled set
// moved; this walk does, without rebuilding the set or falling off the fast path.
Bool SampledBindingsUnchanged(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
const Vector<SampledBindingRecord>& previousRecords) const;
Bool CollectStorageImageTextures(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
Bool CollectSamplerImageFeedback(
const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<SamplerImageFeedbackBinding>& outBindings) const;
static Bool SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel,
GLint imageLevel, GLenum imageAccess);
#if MOBILEGL_BUILD_DISAGGREGATED
// Resolve lazy texture uploads/promotions before the caller captures the
// command buffer passed to BindProgramUniformBuffers. Preparation may
// submit older work, but descriptor recording must never rotate it.
Bool PrepareWireTextureResources(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj);
#endif
// samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that
// every input of every combined-image-sampler resolution is unchanged since the
// previous draw's resolve - same (texture, sampler) per binding, texture params
// sum, sampling-resolution generation (sampler params + texture shape), image
// epochs AND per-resource layout values - so the per-binding cached
// VkDescriptorImageInfo may be reused without re-running the resolve chain.
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj,
Uint32 frameIndex,
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
const SamplerBindingOverride* samplerBindingOverride = nullptr,
Bool samplerDescriptorsUnchangedHint = false,
const Vector<SamplerBindingOverride>* samplerBindingOverrides = nullptr);
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS);
// Pure format-policy helper kept public for host regression tests. Formatted storage
// images use their shader qualifier; transformed float images use glBindImageTexture's
@@ -151,16 +114,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
VkFormat resourceFormat, Bool useBindingFormat);
// True when the program reads at least one sampler and every one of them is bound to a
// texture whose GL level range is a single level. Such a sampler resolves to
// minLod = maxLod = 0 (see VkSamplerManager::GetOrCreateSampler), so an implicit-LOD sample
// and an explicit LOD 0 sample must read the same texel - which is what makes the
// ExplicitLod0Sampling SPIR-V rewrite safe to request. Deliberately conservative: it reads
// only GL state, so a texture that ends up single-level for another reason (one uploaded
// level under a wide level range) merely misses the rewrite.
static Bool ProgramSamplesOnlySingleLevelTextures(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj);
// ---- per-frame descriptor pools ----------------------------------------------------
// Each frame slot owns a list of pools. The first is the base pool (non-update-after-bind,
// setsPerFrame sets) and is never destroyed while the manager lives; every later one was
@@ -215,7 +168,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 cursor = 0;
};
#if MOBILEGL_BUILD_DISAGGREGATED
// ResolveWireImageDescriptor emits no pNext chain. Key every view-create
// value explicitly: hash collisions must never alias different windows,
// formats or swizzles. The root handle and allocation epoch also prevent
@@ -273,7 +225,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Bool storage,
VkDescriptorImageInfo& out, VkFormat boundStorageFormat = VK_FORMAT_UNDEFINED, Uint32 unit = ~0u) const;
void DestroyWirePlaceholderImage(WirePlaceholderImage& image) const;
#endif
// descriptorSetCacheByLayout is the per-frame set cache: BeginFrame rewinds every cursor, so
// a slot re-writes the sets it allocated in earlier frames instead of allocating new ones, and
@@ -283,12 +234,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<DescriptorPoolBucket> descriptorPools;
UnorderedMap<VkDescriptorSetLayout, DescriptorSetCacheEntry> descriptorSetCacheByLayout;
Vector<VkBufferView> texelBufferViews;
#if MOBILEGL_BUILD_DISAGGREGATED
mutable Vector<VkImageView> wireImageViews;
// Own views in the vector above until this slot's fence/idle proof.
// Cache hits preserve the handle, enabling descriptor-content reuse.
mutable UnorderedMap<WireImageViewKey, VkImageView, WireImageViewKeyHash> wireImageViewCache;
#endif
Uint32 activeDescriptorPoolIndex = 0;
Uint32 allocatedSetsThisFrame = 0;
Uint32 peakAllocatedSetsThisFrame = 0;
@@ -298,36 +247,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 quietEpochs = 0;
};
static Bool ResolveSamplerTexture(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
// Shared per-binding resolution for CollectSampledTextures and
// SampledBindingsUnchanged, so membership and comparison can never diverge:
// texture after the fallback substitution (may still be null when no fallback
// exists), effective sampler = unit override else the texture's own sampler.
// 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 MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element,
MG_State::GLState::ITextureObject*& outTexture,
const MG_State::GLState::SamplerObject*& outSampler) const;
// Raw-pointer variant for the per-draw sampled-texture walk (CollectSampledTextures):
// the bound texture stays alive through the draw via GL binding state, so callers that
// only need the pointer skip the SharedPtr copy's atomic refcount churn.
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element);
// `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
@@ -344,32 +263,43 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// 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
// (see BindProgramUniformBuffers' samplerDescriptorsUnchangedHint for the proof
// 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 MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 element, VkDescriptorImageInfo& outImageInfo,
Bool trustUnchangedHint = false) const;
#if MOBILEGL_BUILD_DISAGGREGATED
Bool ResolveWireImageDescriptor(VkCommandBuffer commandBuffer, const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 element, Bool storage, VkDescriptorImageInfo& out) const;
Uint32 element, Bool storage, VkDescriptorImageInfo& out,
VkTextureManager::TextureResource** outResource = nullptr) const;
// P14: THE WIRE DESCRIPTOR REBIND. A draw whose image descriptors cannot have changed since
// the last full resolve of the same program in the same frame - same applier context, same
// unit / sampler / image / buffer-binding serials, same texture shutter, no texture image
// re-created or erased, no sampler destroyed, every sampled image still in GENERAL and no
// image write awaiting its barrier - re-resolves only its buffer descriptors (the UBO
// dynamic offsets move every draw) and, when those name the same buffers and ranges,
// rebinds the set it already has. Anything else, or any program with a texel buffer, storage
// image or placeholder descriptor, takes the full resolve.
struct WireDescriptorRebind {
Bool valid = false;
Uint32 frameIndex = 0;
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
Uint64 programHash = 0;
VkDescriptorSetLayout layout = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
MG_Pipe::MGPipeHandle programHandle = MG_Pipe::kMGPipeNullHandle;
Uint64 programSerial = 0, programBindingsSerial = 0;
Uint64 contextSerial = 0, samplerViewsSerial = 0, samplerStatesSerial = 0, shaderImagesSerial = 0;
Uint64 shaderBuffersSerial = 0, textureShutterSerial = 0;
Uint64 storeDestroyEpoch = 0, textureImageEpoch = 0, resourceEraseEpoch = 0;
Uint64 samplerDestroyEpoch = 0, samplerFrameBoundary = 0;
VkDescriptorSet set = VK_NULL_HANDLE;
Vector<VkTextureManager::TextureResource*> sampledResources;
// The buffer descriptors in walk order: dynamic UBOs with offset 0 (their offset is the
// dynamic one), storage buffers whole.
Vector<VkDescriptorBufferInfo> bufferInfos;
};
WireDescriptorRebind m_wireDescriptorRebind;
Vector<VkDescriptorBufferInfo> m_wireRebindBufferScratch;
Vector<VkTextureManager::TextureResource*> m_wireRebindResourcesScratch;
Bool TryRebindWireDescriptorSet(VkCommandBuffer commandBuffer, const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 frameIndex,
VkPipelineBindPoint bindPoint);
Bool ResolveWireTexelBufferDescriptor(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 frameIndex, Bool storage, VkBufferView& out);
@@ -379,9 +309,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDeviceSize m_wireMaxUniformRange = ~VkDeviceSize{0};
VkDeviceSize m_wireMaxStorageRange = ~VkDeviceSize{0};
Uint32 m_wireMaxTexelElements = ~Uint32{0};
#endif
Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride,
VkDescriptorImageInfo& outImageInfo) const;
Bool ResolveTexelBufferDescriptor(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 frameIndex, VkBufferView& outBufferView);
@@ -415,10 +342,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const void* payload = nullptr; // fallback UploadTransient path
VkDeviceSize payloadSize = 0;
};
#if MOBILEGL_BUILD_DISAGGREGATED
Bool ResolveWireUniformBufferPayload(const MagmaProgramSource& program, Uint32 blockIndex,
Uint32 bindingPoint, UboBindResult& out) const;
#endif
Bool ResolveUniformBufferPayload(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
Uint32 arrayElement, UboBindResult& out) const;
@@ -487,19 +412,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 m_descriptorSlotTrims = 0;
VkTextureManager* m_textureManager = nullptr;
VkSamplerManager* m_samplerManager = nullptr;
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
// See AcquireUnboundTexelBufferView. 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
// capacity) so the descriptor-write path stops allocating on every draw.
@@ -526,49 +446,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
DescriptorReuseEntry m_descriptorReuseMemo[kDescriptorReuseMemoSize];
Uint32 m_descriptorReuseMemoNext = 0;
// Dynamic-offset-only rebind (see BindProgramUniformBuffers): records the
// descriptor set selected by the last cacheable full walk of a program
// whose active bindings are exactly one dynamic UBO (single descriptor)
// plus combined-image samplers. When the next call proves every sampler
// descriptor input unchanged (samplerDescriptorsUnchangedHint) and the
// UBO re-resolves to the SAME VkBuffer+range - only the dynamic offset
// moved, the per-draw glUniform case - the walk collapses to: resolve one
// offset, rebind the recorded set with new pDynamicOffsets (Vulkan allows
// rebinding the same set with different dynamic offsets).
// Invalidation inventory: BeginFrame clears it (the frame's sets are
// recycled) and the frameIndex field guards cross-frame confusion on top;
// OnDescriptorSetLayoutDestroyed clears it (the set may be freed); a
// sampler-override walk clears it (mirrors m_descriptorReuseMemo); a
// program relink bumps the backend state version and thus programObj.hash
// so the key misses; the program lifetime id is never reused, so a
// deleted-and-recreated program misses; a texture/sampler/binding change
// drops the hint upstream; an arena wrap or growth resolves a different
// VkBuffer and misses. AcquireDescriptorSet's per-frame cursor only
// advances, so the recorded set is never re-written within its frame.
// P7 M2 round 2 (ID-P7-43): a WIRE store can die MID-FRAME on the reclaim
// path (VkBufferManager::DeferredWireRelease), and the next mint can hand
// its VkBuffer handle value back - a heap pointer under lavapipe - so "the
// same VkBuffer+range" is no longer proof of the same store. The memo
// therefore records VkBufferManager::GetWireStoreDestroyEpoch() and misses
// once any wire store has been destroyed since; the descriptor-reuse
// signature below folds the same epoch in. On the wire arm this memo is
// never consulted (SetupWireDraw passes no sampler hint), so the field is
// the defensive half; the signature is the half the wire arm reaches.
struct FastRebindMemo {
Bool valid = false;
Uint32 frameIndex = 0;
Uint64 programLifetimeId = 0;
ProgramFactory::HashType programHash = 0;
Uint32 uboBinding = 0;
VkBuffer uboBuffer = VK_NULL_HANDLE;
VkDeviceSize uboRange = 0;
VkDescriptorSet set = VK_NULL_HANDLE;
#if MOBILEGL_BUILD_DISAGGREGATED
Uint64 wireStoreDestroyEpoch = 0;
#endif
};
FastRebindMemo m_fastRebindMemo;
// vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform
// block resolve to the same set AND the same dynamic offsets, so the
// driver call can be skipped outright. Command-buffer-scope state; reset
@@ -660,7 +537,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_samplerResolveMemoHighWater = binding + 1;
}
}
// WireImageWriteEpoch() when ResolveWireImageDescriptor last made image writes visible.
// WireImageWriteEpoch() when ResolveWireImageDescriptor, or a new draw pass's attachment
// barrier (VulkanRenderer::TransitionWireImage), last made image writes visible.
mutable Uint64 m_wireImageBarrierEpoch = 0;
// P14: the wire arm's sampler memo, one entry per texture unit. The sampler a unit resolves
// to is a function of the sampler CSO it names (immutable for a {slot, gen, Serial} within
// one applier - ContextSerial tells appliers apart), the texture's format, the numeric
// domain and the view's level count. A hit skips the parameter hash and the cache map; it
// is taken only while the sampler manager has destroyed nothing (DestroyEpoch) and only
// within the frame boundary of the last real lookup, so every used entry is still stamped
// once per frame and the idle sweep can never retire a sampler this memo hands out.
struct WireSamplerMemo {
Bool valid = false;
Uint32 slot = 0, gen = 0;
Uint64 serial = 0, contextSerial = 0;
Uint32 format = 0, viewLevelCount = 0;
Bool forceNearest = false;
Uint64 frameBoundary = 0, destroyEpoch = 0;
VkSampler sampler = VK_NULL_HANDLE;
};
static constexpr SizeT kWireSamplerMemoUnits = 192; // MG_Pipe::kMGPipeMaxTextureUnits
mutable Array<WireSamplerMemo, kWireSamplerMemoUnits> m_wireSamplerMemo{};
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -7,6 +7,7 @@
// End of Source File Header
#include "VertexInputStateFactory.h"
#include <MG_Backend/Record/ApplyRoleBackend.h>
#include "MagmaPipeArms.h"
#include "MG_Util/Converters/MGToStr/DataTypeConverter.h"
#include <MG_Backend/BackendObjects.h>
@@ -17,428 +18,68 @@
#include <utility>
namespace MobileGL::MG_Backend::DirectVulkan {
VertexInputStateFactory::HashType VertexInputStateFactory::ComputeHash(
const MG_State::GLState::VertexArrayObject& vao) const {
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
namespace {
struct WireVertexInputMemo {
struct Key {
Uint64 Offset;
Uint32 Stride;
Uint32 Divisor;
};
Bool Valid = false;
Uint64 ContentSerial = 0;
Uint32 ActiveMask = 0;
Uint32 Start = 0;
Uint32 Count = 0;
Uint32 KeyCount = 0;
Array<Key, MG_Pipe::kMGPipeMaxVertexAttribs> Keys{};
VertexInputStateFactory::BackendVertexInputState State;
};
} // namespace
for (Int i = 0; i < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++i) {
const auto& attr = vao.GetAttribute(i);
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Enabled, sizeof(attr.Enabled)));
if (!attr.Enabled) {
continue;
}
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Size, sizeof(attr.Size)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Type, sizeof(attr.Type)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Normalized, sizeof(attr.Normalized)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Stride, sizeof(attr.Stride)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Offset, sizeof(attr.Offset)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsInteger, sizeof(attr.IsInteger)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsLong, sizeof(attr.IsLong)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
// The bound buffer's IDENTITY is a component of the key, and it has to be the
// buffer's never-reused lifetime id - NOT its heap address, which this used to
// hash. An address is recycled by the allocator, so a deleted-and-recreated
// buffer reproduces it; combined with a byte-identical attribute layout that
// reproduces the WHOLE content hash, and the hash is what
// TryBindResolvedVertexBindings accepts as proof that a memoised binding still
// reads the buffer it was resolved from. It did not: a destroyed buffer's GPU
// slice was bound for its successor's draw, which is how a transform-feedback
// capture came back holding a dead VAO's vertex data (0,0,0,1 - the previous
// test's positions) instead of its own.
// Zero for client memory (no buffer), which is a distinct identity of its own.
//
// 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)));
const VertexInputStateFactory::BackendVertexInputState* VertexInputStateFactory::ResolveWireVertexInput(
const MG_Pipe::MGPipeVertexElementsRecord& elements, const MG_Pipe::MGPipeApplierState& state,
Uint32 activeMask) const {
auto* memo = static_cast<WireVertexInputMemo*>(elements.BackendMemo.get());
if (memo == nullptr) {
auto created = std::make_shared<WireVertexInputMemo>();
memo = created.get();
elements.BackendMemo = Move(created);
}
return XXH64_digest(m_hashState);
// The window first: a location outside it is BuildWireVertexInput's refusal, and the
// keys below index the window.
Bool hit = memo->Valid && memo->ContentSerial == elements.ContentSerial && memo->ActiveMask == activeMask &&
memo->Start == state.VertexBufferStart && memo->Count == state.VertexBufferCount;
if (hit) {
Uint32 n = 0;
for (Uint32 location = 0; location < elements.AttributeCount && hit; ++location) {
if (!elements.Attributes[location].Enabled || !(activeMask & (1u << location))) continue;
const auto& buffer = state.VertexBuffers[location];
const auto& key = memo->Keys[n++];
hit = key.Offset == buffer.Offset && key.Stride == buffer.Stride && key.Divisor == buffer.Divisor;
}
if (hit) return &memo->State;
}
memo->Valid = false;
if (!BuildWireVertexInput(elements, state, activeMask, memo->State)) return nullptr;
Uint32 n = 0;
for (Uint32 location = 0; location < elements.AttributeCount; ++location) {
if (!elements.Attributes[location].Enabled || !(activeMask & (1u << location))) continue;
const auto& buffer = state.VertexBuffers[location];
memo->Keys[n++] = {buffer.Offset, buffer.Stride, buffer.Divisor};
}
memo->KeyCount = n;
memo->ContentSerial = elements.ContentSerial;
memo->ActiveMask = activeMask;
memo->Start = state.VertexBufferStart;
memo->Count = state.VertexBufferCount;
memo->Valid = true;
return &memo->State;
}
#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(
const MG_State::GLState::VertexArrayObject& vao) const {
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)) {
hash = ComputeHash(vao);
vao.SetBackendHashMemo(hash);
}
#endif
return hash;
}
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
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,
// valid while its config version and the cache's eviction epoch both
// match - no re-hash, no map lookup.
const void* memoState = nullptr;
Uint64 memoEpoch = 0;
if (vao.GetBackendStateMemo(memoState, memoEpoch) && memoEpoch == m_evictionEpoch) {
const auto* entry = static_cast<const BackendVertexInputState*>(memoState);
entry->lastUsedFrameBoundary = m_frameBoundaryCounter;
return *entry;
}
const BackendVertexInputState& entry = GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
vao.SetBackendStateMemo(&entry, m_evictionEpoch);
// Also mirror the layout identity and the two per-draw masks into the VAO's aux
// memo (pure VALUES derived from the VAO configuration, so config-version
// guarding alone is sound). The draw fast path reads them from the VAO object it
// already touched instead of chasing into this entry - see PackVertexInputAuxMemo.
vao.SetBackendAuxMemo(entry.layoutHash,
PackVertexInputAuxMasks(entry.unsupportedAttribMask, entry.attributeLocationMask));
return entry;
#endif // MOBILEGL_PIPE_LEGACY_MEMOS
}
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao, HashType hash) {
auto it = m_cache.find(hash);
if (it != m_cache.end()) {
it->second->lastUsedFrameBoundary = m_frameBoundaryCounter;
return *it->second;
}
VertexInputStateBuilder builder;
Vector<SizeT> bindingBufferKeys;
Vector<SizeT> bindingBaseOffsets;
Vector<Uint32> bindingAttributeLocations;
Vector<Bool> bindingUsesClientMemory;
Vector<VertexStreamConversion> bindingConversions;
Vector<VkVertexInputBindingDivisorDescriptionEXT> bindingDivisors;
Uint32 unsupportedAttribMask = 0;
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
const auto& attr = vao.GetAttribute(location);
if (!attr.Enabled) {
continue;
}
VkFormat sourceVkFormat =
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 =
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.7): with an active transport the answer is the
// SERVER's own backend's - the client caps mirror is client memory (rule E).
MG_Config::Transport != MG_Config::TransportMode::Monolith
? (MG_Remote::Server::ServerLoopInstance().Backend() == nullptr ||
!MG_Remote::Server::ServerLoopInstance().Backend()
->GetDynamicParameters()
.SupportsFloat64VertexAttributes)
:
#endif
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) {
MGLOG_E_ONCE("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
"enabled but cannot be mapped to a VkFormat",
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
unsupportedAttribMask |= (1u << location);
continue;
}
VkFormat vkFormat = sourceVkFormat;
if (conversion == VertexStreamConversion::None && !SupportsVertexBufferFormat(vkFormat)) {
if (IsScaledIntegerVertexFormat(vkFormat)) {
const VkFormat fallbackFormat = ToFloat32VertexFormat(attr.Size);
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
vkFormat = fallbackFormat;
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
MGLOG_W_ONCE("Vertex attribute location=%u format=%d lacks "
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d "
"(type=%s size=%d normalized=%s integer=%s)",
location, static_cast<Int>(sourceVkFormat), static_cast<Int>(vkFormat),
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size,
attr.Normalized ? "true" : "false", attr.IsInteger ? "true" : "false");
}
}
if (conversion == VertexStreamConversion::None) {
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",
location, static_cast<Int>(sourceVkFormat),
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
unsupportedAttribMask |= (1u << location);
continue;
}
}
const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
if (attribByteSize == 0) {
MGLOG_E_ONCE("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
"enabled but cannot be sized",
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str());
unsupportedAttribMask |= (1u << location);
continue;
}
// Verbatim, zero included. The frontend already resolved a pointer call's
// "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 ||
attr.Type == DataType::Uint2101010Rev;
const SizeT requiredAlignment = packedAttribute ? attribByteSize : GetComponentSize(attr.Type);
// For a client-memory array attr.Offset holds the raw client pointer, and the
// draw path re-uploads the data to a 16-aligned transient slice with attribute
// offset 0, so only the stride can violate Vulkan's fetch alignment there.
const Bool clientMemoryAttribute = attr.Buffer == nullptr;
if (conversion == VertexStreamConversion::None && requiredAlignment > 1 &&
((sourceStride % requiredAlignment) != 0 ||
(!clientMemoryAttribute && (attr.Offset % requiredAlignment) != 0))) {
// GL accepts arbitrary byte strides and offsets. Core Vulkan vertex fetches do not
// unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one
// attribute into a tightly packed transient stream without changing its format.
conversion = VertexStreamConversion::Repack;
MGLOG_W_ONCE("Vertex attribute location=%u uses Vulkan-incompatible alignment "
"(offset=%zu stride=%u required=%zu); using a tightly packed stream",
location, attr.Offset, sourceStride, requiredAlignment);
}
Uint32 stride = sourceStride;
// A converted stream is tightly packed, so its stride is the converted element
// size - unless the source stride is zero, which does not describe a packing at
// all but "never advance". That survives the conversion unchanged: the draw path
// 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 =
(attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE;
const SizeT bufferKey = reinterpret_cast<SizeT>(attr.Buffer.get());
const Uint32 binding = static_cast<Uint32>(bindingBufferKeys.size());
bindingBufferKeys.push_back(bufferKey);
bindingBaseOffsets.push_back(attr.Buffer ? attr.Offset : 0);
bindingAttributeLocations.push_back(location);
bindingUsesClientMemory.push_back(attr.Buffer == nullptr);
bindingConversions.push_back(conversion);
builder.AddBinding(binding, stride, inputRate);
builder.AddAttribute(location, binding, vkFormat, 0);
// Divisor 1 is what VK_VERTEX_INPUT_RATE_INSTANCE already means; only anything
// else needs the extension to say it.
if (inputRate == VK_VERTEX_INPUT_RATE_INSTANCE && attr.Divisor != 1) {
bindingDivisors.push_back({binding, static_cast<Uint32>(attr.Divisor)});
}
}
const auto& state = builder.Build();
auto& slot = m_cache[hash];
if (!slot) {
slot = MakeUnique<BackendVertexInputState>();
}
BackendVertexInputState& entry = *slot;
entry.hash = hash;
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
entry.bindingDivisors = Move(bindingDivisors);
entry.bindings = builder.GetBindings();
entry.attributes = builder.GetAttributes();
// See the layoutHash declaration: hash only the resolved layout, never
// buffer identities, so identical layouts across VAOs/buffers agree.
XXHASH_VERIFY(XXH64_reset(m_hashState, 0));
for (const auto& binding : entry.bindings) {
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.binding, sizeof(binding.binding)));
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.stride, sizeof(binding.stride)));
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.inputRate, sizeof(binding.inputRate)));
}
for (const auto& attribute : entry.attributes) {
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.location, sizeof(attribute.location)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.binding, sizeof(attribute.binding)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.offset, sizeof(attribute.offset)));
}
for (const auto& divisor : entry.bindingDivisors) {
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.binding, sizeof(divisor.binding)));
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.divisor, sizeof(divisor.divisor)));
}
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
entry.layoutHash = XXH64_digest(m_hashState);
entry.attributeLocationMask = 0;
for (const auto& attribute : entry.attributes) {
if (attribute.location < 32u) {
entry.attributeLocationMask |= (1u << attribute.location);
}
}
entry.bindingBufferKeys = std::move(bindingBufferKeys);
entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
entry.bindingUsesClientMemory = std::move(bindingUsesClientMemory);
entry.bindingConversions = std::move(bindingConversions);
entry.unsupportedAttribMask = unsupportedAttribMask;
entry.state = state;
entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data();
entry.state.pVertexAttributeDescriptions = entry.attributes.empty() ? nullptr : entry.attributes.data();
if (!entry.bindingDivisors.empty()) {
entry.divisorState.vertexBindingDivisorCount = static_cast<Uint32>(entry.bindingDivisors.size());
entry.divisorState.pVertexBindingDivisors = entry.bindingDivisors.data();
entry.state.pNext = &entry.divisorState;
} else {
entry.state.pNext = nullptr;
}
return entry;
}
#if MOBILEGL_BUILD_DISAGGREGATED
Bool VertexInputStateFactory::BuildWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements,
const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask, BackendVertexInputState& out) const {
out = BackendVertexInputState{};
// The caller's state is a per-draw scratch (WireDraw.inc): reset it without freeing.
out.ResetKeepingCapacity();
for (Uint32 location = 0; location < elements.AttributeCount; ++location) {
const auto& attr = elements.Attributes[location];
if (!attr.Enabled || !(activeMask & (1u << location))) continue;
@@ -506,7 +147,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
attr.IsBgra, attr.IsLong);
auto conversion = VertexStreamConversion::None;
if (format == VK_FORMAT_UNDEFINED && type == DataType::Float64) {
const auto* backend = MG_Remote::Server::ServerLoopInstance().Backend();
const auto* backend = MG_Backend::ApplyRoleBackend();
if (backend && backend->GetDynamicParameters().SupportsFloat64VertexAttributes) {
// Exactly the monolith arm's answer for the same state: with native fp64
// the module KEPT its 64-bit inputs (DemoteFloat64Pass did not run), so
@@ -573,42 +214,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
return true;
}
#endif
void VertexInputStateFactory::OnFrameBoundary() {
++m_frameBoundaryCounter;
// Sweep occasionally; evict entries whose last hit is far in the past.
// Erasure happens only here, never mid-frame: the draw path holds a
// reference into the current entry across its setup, and unordered_map
// erase would invalidate it. Entries are CPU-side only, so no GPU-idle
// proof is needed; an evicted entry that is used again is simply rebuilt
// from the VAO state (same hash, same content).
constexpr Uint64 kSweepInterval = 256;
constexpr Uint64 kRetireAgeBoundaries = 1024;
if ((m_frameBoundaryCounter % kSweepInterval) != 0) {
return;
}
for (auto it = m_cache.begin(); it != m_cache.end();) {
if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) {
it = m_cache.erase(it);
// Invalidate every VAO's state-pointer memo: the erased node's
// 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;
#endif
} else {
++it;
}
}
}
VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger,
Bool isBgra, Bool isLong) {
@@ -847,19 +452,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (m_physicalDevice == VK_NULL_HANDLE || format == VK_FORMAT_UNDEFINED) {
return false;
}
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const auto found = m_wireVertexFormatSupport.find(format);
if (found != m_wireVertexFormatSupport.end()) return found->second;
VkFormatProperties properties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &properties);
const Bool supported = (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0;
m_wireVertexFormatSupport.emplace(format, supported);
return supported;
}
#endif
const auto found = m_wireVertexFormatSupport.find(format);
if (found != m_wireVertexFormatSupport.end()) return found->second;
VkFormatProperties properties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &properties);
return (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0;
const Bool supported = (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0;
m_wireVertexFormatSupport.emplace(format, supported);
return supported;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -10,9 +10,7 @@
// 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>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Pipe/PipeApply.h>
#endif
#include "Config.h"
#include "MagmaPipeArms.h"
@@ -76,66 +74,58 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipelineVertexInputStateCreateInfo state{
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
};
// Back to a default-constructed state while keeping the vectors' storage, so a
// per-draw scratch rebuilds without touching the heap. Every member is reset by the
// default assignment; only the vectors' buffers are carried across it.
void ResetKeepingCapacity() {
const auto keep = [](auto& vector) {
auto storage = Move(vector);
storage.clear();
return storage;
};
auto keptBindings = keep(bindings);
auto keptAttributes = keep(attributes);
auto keptBufferKeys = keep(bindingBufferKeys);
auto keptBaseOffsets = keep(bindingBaseOffsets);
auto keptLocations = keep(bindingAttributeLocations);
auto keptClientMemory = keep(bindingUsesClientMemory);
auto keptConversions = keep(bindingConversions);
auto keptDivisors = keep(bindingDivisors);
*this = BackendVertexInputState{};
bindings = Move(keptBindings);
attributes = Move(keptAttributes);
bindingBufferKeys = Move(keptBufferKeys);
bindingBaseOffsets = Move(keptBaseOffsets);
bindingAttributeLocations = Move(keptLocations);
bindingUsesClientMemory = Move(keptClientMemory);
bindingConversions = Move(keptConversions);
bindingDivisors = Move(keptDivisors);
}
};
#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):
m_config(config), m_physicalDevice(physicalDevice) {}
#endif
~VertexInputStateFactory() = default;
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
// The VAO aux-memo payload GetOrCreateVertexInputState(vao) stamps: aux0 is the
// entry's layoutHash, aux1 packs (unsupportedAttribMask << 32) | attributeLocationMask.
// Readers that find the aux memo valid can use these without resolving the entry.
static Uint64 PackVertexInputAuxMasks(Uint32 unsupportedAttribMask, Uint32 attributeLocationMask) {
return (static_cast<Uint64>(unsupportedAttribMask) << 32) | attributeLocationMask;
}
HashType ComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
// Memoized ComputeHash: reuses the VAO's cached hash while its config version
// is unchanged. Use this on per-draw paths.
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 MG_State::GLState::VertexArrayObject& vao, HashType hash);
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
// Frame boundary hook: ages the cache and evicts entries not hit for many
// frames. The key mixes each bound buffer's never-reused lifetime id, so
// buffer/VAO churn keeps minting fresh keys - and does so by construction,
// not by luck: a recreated buffer can no longer land back on its dead
// predecessor's key. Without eviction the map grows for the whole session.
// Entries hold no Vulkan handles (pipeline creation copies the descriptions)
// and the draw path's entry reference never spans a frame boundary, so
// eviction here needs no GPU-idle proof. Self-gated: one counter bump and
// compare except on sweep boundaries.
void OnFrameBoundary();
static SizeT GetComponentSize(DataType type);
// Tightly-packed byte size of one vertex element for this attribute: componentSize * size for
// normal types, and 4 (one packed word) for the 2_10_10_10 types and GL_BGRA. Returns 0 for
// an unknown/unsupported type.
static SizeT GetAttributeByteSize(DataType type, Int size, Bool isBgra);
#if MOBILEGL_BUILD_DISAGGREGATED
// A wire VAO is a layout record plus a separate per-attribute buffer window.
// No frontend VAO identity, address or memo participates in this layout.
Bool BuildWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements,
const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask,
BackendVertexInputState& out) const;
#endif
// The same layout, kept on the record (MGPipeVertexElementsRecord::BackendMemo) and rebuilt
// only when the record's configuration, the program's active mask, the buffer window or an
// active attribute's buffer offset/stride/divisor moved - everything the build reads that
// can change. Null where BuildWireVertexInput would return false.
const BackendVertexInputState* ResolveWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements,
const MG_Pipe::MGPipeApplierState& state,
Uint32 activeMask) const;
private:
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false,
@@ -144,85 +134,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static VkFormat ToFloat32VertexFormat(Int componentCount);
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;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
#if MOBILEGL_BUILD_DISAGGREGATED
// Physical-device format capabilities never change during this factory's
// lifetime. The wire path rebuilds layouts without the legacy VAO memo.
mutable UnorderedMap<VkFormat, Bool> m_wireVertexFormatSupport;
#endif
// Values are heap-allocated: UnorderedMap is open-addressing, so INSERT
// invalidates references to stored values - and so does ERASE, which shifts
// the rest of the probe cluster into the hole and therefore moves entries
// 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;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameBoundaryCounter = 0;
// Bumped whenever any cache entry is erased. VAOs memo a raw pointer to
// their heap-allocated entry (stable across map insert/rehash by
// construction); a memo is honored only while its recorded epoch
// matches, so an evicted entry can never be dereferenced through a
// 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 std::atomic<Uint64> s_evictionEpochSource{0};
Uint64 m_evictionEpoch = ++s_evictionEpochSource;
#else
Uint64 m_evictionEpoch = 1;
#endif
static inline thread_local XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -13,10 +13,8 @@
#include "../VkIncludes.h"
#include <Includes.h>
#include <vk_mem_alloc.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include "MG_Pipe/MGPipeTypes.h"
#include <unordered_map>
#endif
namespace MobileGL::MG_Backend::DirectVulkan {
enum class BufferKind : Uint8 {
@@ -40,56 +38,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool transformFeedbackUsageEnabled = false;
};
// The DirectVulkan storage behind one frontend buffer (pipe_resource analogue).
// Owned (refcounted) by the frontend BufferObject; the manager holds only weak
// references (for shutdown) plus strong references on deferred-release lists.
class VkBufferResource : public MG_State::GLState::BackendBufferResource {
public:
~VkBufferResource() override = default;
// Resident storage (may be invalid for streaming-only buffers).
VkBufferObject buffer;
VkDeviceSize storageSize = 0;
VkBufferUsageFlags usageFlags = 0;
// Frame serial of the last GPU reference; drives busy tracking.
Uint64 lastUseSerial = 0;
// Set when an immediate op could not be applied; forces a full re-upload
// on the next AcquireResidentSlice.
Bool pendingFullUpload = false;
// Backs a zero-copy coherent persistent map (PipeResource GPU residency): the
// buffer is HOST_VISIBLE+COHERENT, persistently mapped, carries every usage and is
// never orphaned or recreated. Draw-time acquire binds it directly, no re-upload.
Bool persistentMapped = false;
// Bumped from a manager-wide counter every time anything that decides which
// BufferSlice an Acquire*Slice call hands back changes: storage created or
// released, a full re-upload becoming due, a promotion/demotion between
// resident and streamed storage, or a new per-frame arena slice. Callers that
// memoise a resolved slice compare this to prove the memo still describes the
// buffer. The counter is manager-wide (never per-resource) so a freshly
// created resource - including one that replaces a destroyed resource at the
// same address - can never reproduce a value some memo already holds. 0 means
// "no slice has ever been handed out", which no memo can match.
Uint64 sliceEpoch = 0;
// Cached transient (streaming) slice for the current frame.
BufferSlice transientSlice{};
Uint64 transientFrameSerial = 0;
Uint64 transientChangeSerial = 0;
VkDeviceSize transientSize = 0;
// Streaming re-copies the whole store into the per-frame arena on every
// frame, which is right for genuinely per-frame data but pure waste for a
// Dynamic-hinted buffer the app stopped touching. After the content
// survives kStreamedPromotionStreak frame boundaries unchanged it is
// promoted to resident storage (one final upload, then zero per-frame
// cost); the first content change demotes it back to streaming, and the
// streaming path's existing downgrade releases the resident store.
Uint32 unchangedStreak = 0;
Bool promotedResident = false;
Uint64 promotedChangeSerial = 0;
};
// Supplies a command buffer that is recording and outside any render pass,
// for staged buffer-range copies. Implemented by VulkanRenderer.
class IBufferCopyCommandProvider {
@@ -103,7 +51,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool Initialize(const VkBufferManagerInitInfo& initInfo);
void Shutdown();
#if MOBILEGL_BUILD_DISAGGREGATED
// Registered before caps publication; the initialized renderer owns the storage.
static void RegisterWireResourceOps();
// Transport resources are owned by their complete wire handle, never by a
@@ -137,6 +84,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// P8-D: every WaitForWireBufferHostAccess that actually waited, over this manager's life -
// the attribution base a caller reads before and after its own ReadWireBuffer.
Uint64 GetWireHostWaitCount() const { return m_wireHostWaits; }
// P15 S0 (M4): busy writes that renamed their store instead of taking a staged copy.
Uint64 GetWireRenameCount() const { return m_wireRenames; }
// Resource-op entry points. All run on the server apply owner.
void CreateWireBuffer(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc);
@@ -153,9 +102,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// applies unchanged. The T2 store it replaces goes through DeferWireRelease. False = not
// imported (the T2 store is untouched and the client is DECLINED).
Bool ImportWireBuffer(MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size);
// P13 W4: monolith's record arm donates the wire store itself as the client's persistent
// map (see the definition). Null when the store cannot be handed over.
void* DonateWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 size, const void* seedBytes);
// The POST self-test of the sustained-lock pattern on this device (see the definition).
static Bool SelfTestWireImport(char* why, Uint64 whyBytes);
#endif
// Recreate all per-frame transient arenas
Bool RecreateTransientArenas(Uint32 frameCount);
@@ -174,6 +125,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// frameSerial-minus-frameCount inference.
void NotifyFrameSerialComplete(Uint64 serial);
void SetCopyCommandProvider(IBufferCopyCommandProvider* provider);
// The owning renderer's submission counter: AcquireWireSlice stamps the next submission
// from it directly rather than finding the renderer through the session-local slot on
// every bind (a thread-local probe per vertex, index and uniform buffer of every draw).
void SetSubmitCounterSource(const Uint64* submitCounter) { m_submitCounter = submitCounter; }
Bool UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data, VkDeviceSize size,
VkDeviceSize alignment, BufferSlice& outSlice);
@@ -199,59 +154,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// UniformManager::AcquireUnboundTexelBufferView.
BufferSlice AcquireUnboundTexelBufferDescriptor();
// Draw-time acquire for resident (device-storage) buffers: ensures the
// resource exists and is fully uploaded, marks it used this frame.
Bool AcquireResidentSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
BufferSlice& outSlice);
// Draw-time acquire for streamed buffers: uploads the whole shadow into
// the per-frame arena (cached by change serial), releasing any resident
// storage the buffer may still own.
Bool AcquireStreamedSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
BufferSlice& outSlice);
// Zero-copy persistent map (PipeResource GPU residency): create (once) a
// HOST_VISIBLE+COHERENT, persistently mapped resident buffer carrying every usage,
// seed it from the shadow, and return its mapped base for the app to write into
// directly. Idempotent. Returns nullptr on failure (frontend keeps its shadow).
void* AcquirePersistentMap(MG_State::GLState::BufferObject& bufferObject);
// Immediate ops, dispatched from the frontend BufferBackendOps table.
void OnRespecify(MG_State::GLState::BufferObject& bufferObject);
void OnSubData(MG_State::GLState::BufferObject& bufferObject, SizeT offset, SizeT size);
void OnResidentSubData(MG_State::GLState::BufferObject& bufferObject, SizeT offset, DataPtr data);
void OnFlushMappedRange(MG_State::GLState::BufferObject& bufferObject, Range1D range,
Flags<BufferMappingAccessBit> appAccess);
void OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& resource);
Uint64 GetFrameSerial() const { return m_frameSerial; }
// Highest value handed to any VkBufferResource::sliceEpoch. Unchanged since a
// memo was taken means no buffer this manager owns changed which slice it hands
// back, and none was persistently mapped, in between - so a memo of resolved
// slices needs no per-buffer re-check. See AcquirePersistentMap for the mapping half.
Uint64 GetSliceEpochCounter() const { return m_sliceEpochCounter; }
#if MOBILEGL_BUILD_DISAGGREGATED
// Bumped every time this manager destroys a WIRE store's VkBuffer (see
// m_wireStoreDestroyEpoch). Unchanged since a memo was taken means no VkBuffer handle
// that memo names can have been freed and re-minted in between, which is the one
// fact a handle-keyed memo of wire descriptors needs and cannot read off the handle.
Uint64 GetWireStoreDestroyEpoch() const { return m_wireStoreDestroyEpoch; }
#endif
// P15 S0: the store's content state, as a process-wide monotonic serial: it moves on every
// host write, respecify and GPU-write mark, and on every bind a shader may write through
// (storage / texture-buffer slices). False for a store whose bytes the client writes
// directly (an imported or donated store), which no cache may key on.
Bool GetWireBufferContentSerial(MG_Pipe::MGPipeHandle res, Uint64& serial);
// P15 S0: CONVERTED WIRE VERTEX STREAMS, KEPT ACROSS FRAMES (the frontend arm's
// ConvertedVertexStream cache, lost with it, and longer-lived than it: that one was per
// frame). Key: the source store's content serial - process-wide monotonic, so no store or
// handle identity can alias - and the layout. Each entry owns a vertex buffer holding the
// converted elements; a newer conversion of the same store and layout replaces it, and the
// whole set is bounded by bytes (least recently used first). A replaced or evicted buffer
// is released when the frame slot that last used it comes round again.
struct ConvertedStreamKey {
Uint64 contentSerial = 0;
Uint64 baseOffset = 0;
Uint32 stride = 0, type = 0, size = 0, conversion = 0;
Bool normalized = false, isInteger = false, isBgra = false;
Bool operator==(const ConvertedStreamKey&) const = default;
};
Bool LookupConvertedStream(const ConvertedStreamKey& key, SizeT elementCount, BufferSlice& outSlice);
Bool StoreConvertedStream(const ConvertedStreamKey& key, MG_Pipe::MGPipeHandle source, SizeT elementCount,
const void* bytes, SizeT size, BufferSlice& outSlice);
// Highest frame serial whose GPU work is known complete; serials at or
// below it may be considered signaled. Drives IsResourceBusy and the
// backend GL fence objects.
Uint64 GetCompletedSerial() const;
// Busy = potentially referenced by GPU work that has not been fenced yet
// (including commands recorded for the current, unsubmitted frame).
Bool IsResourceBusy(const VkBufferResource& resource) const;
// Hand the manager a buffer to destroy once the frame that recorded commands
// naming it has completed. The renderer's blit path needs a device-local
// scratch store for one recorded operation, which cannot be a stack local:
// the glBlitFramebuffer that records the commands returns long before the
// command buffer is submitted.
void DeferRelease(VkBufferObject&& buffer);
private:
#if MOBILEGL_BUILD_DISAGGREGATED
struct WireBufferResource {
VkBufferObject buffer;
Uint64 size = 0;
@@ -262,6 +197,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// unsubmitted index as incomplete, but the stamp is taken before the draw is
// recorded and a mid-draw flush can submit it without the draw.
Uint64 lastUseSubmitIndex = 0;
// P15 S0: see GetWireBufferContentSerial.
Uint64 contentSerial = 0;
// P15 S0 (M4): the store's bytes on the host, for a small store whose content only
// host writes have made since it was specified - what a busy write renames from (see
// WriteWireBuffer). Empty: no shadow (a large store, the budget was spent, or a GPU
// write, shader-writable bind or client mapping made the host copy unknowable).
Vector<Uint8> shadow;
// P15: buffers this store was renamed away from, kept for the next rename instead of
// allocating one (a UI store is renamed several times a frame). A spare is reused only
// once the submission that can name it has completed - DeferWireRelease's own rule -
// and its handle never died, so no handle-keyed memo can mistake it for another
// buffer. Bounded; the rest go through DeferWireRelease.
struct RenameSpare {
VkBufferObject buffer;
Uint64 submitIndex = 0;
Uint64 lastUseSerial = 0;
};
Vector<RenameSpare> renameSpares;
Bool gpuWritesPending = false;
// Only ranges actually submitted by resource_subdata are covered. No
// shadow is retained: flush cannot replay stale bytes over GPU writes.
@@ -275,6 +228,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
};
// P8-D: see GetWireHostWaitCount.
Uint64 m_wireHostWaits = 0;
Uint64 m_wireRenames = 0;
SizeT m_wireShadowBytes = 0;
void DropWireShadow(WireBufferResource& resource);
void ReleaseRenameSpares(WireBufferResource& resource);
Bool CreateWireStoreBuffer(WireBufferResource& resource);
Bool RenameBusyWireStore(WireBufferResource& resource);
static Uint64 WireBufferKey(MG_Pipe::MGPipeHandle res) {
return (static_cast<Uint64>(res.Gen) << 32) | res.Slot;
}
@@ -332,8 +291,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// while a memo still maps that handle to a descriptor set baked to the dead store's
// memory: silent wrong bytes, no Fatal (ID-P7-43). m_wireStoreDestroyEpoch is the
// fact the memos fold in: every wire-store destroy bumps it, so a memo taken before
// the destroy cannot match after it. Deliberately NOT m_sliceEpochCounter, which every
// WriteWireBuffer bumps and which would defeat the memo on every glBufferSubData.
// the destroy cannot match after it. Deliberately not bumped by WriteWireBuffer, which
// would defeat the memo on every glBufferSubData.
struct DeferredWireRelease {
VkBufferObject buffer;
Uint64 lastUseSerial = 0;
@@ -371,29 +330,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// the stats channel is on; MagmaWireReclaimScenario reads them off the server's line.
void NoteWireStorePeaks();
void PublishWireReclaimGauges();
#endif
Bool InitializeTransientArenas();
static VkBufferUsageFlags GetVkBufferUsage(BufferKind kind);
VkBufferResource* GetOrCreateResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
static VkBufferResource* ResourceOf(MG_State::GLState::BufferObject& bufferObject);
Bool CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size, VkBufferUsageFlags usage,
VkMemoryPropertyFlags requiredFlags = 0);
// Swap storage (conditional orphan) and refill it from the shadow copy.
Bool SwapStorageAndUploadAll(VkBufferResource& resource, MG_State::GLState::BufferObject& bufferObject);
// Record a staging-slice copy into the resident storage, ordered against
// in-flight and already-recorded GPU work.
Bool StagedRangeCopy(VkBufferResource& resource, const void* data,
SizeT offset, SizeT size);
#if MOBILEGL_BUILD_DISAGGREGATED
Bool StagedWireRangeCopy(WireBufferResource& resource, const void* data, SizeT offset, SizeT size);
#endif
void CollectDeferredReleases(Uint32 frameIndex);
void DestroyAllDeferredReleases();
void TrackLiveResource(const SharedPtr<VkBufferResource>& resource);
void ReleaseAllLiveResources();
// See VkBufferResource::sliceEpoch.
void BumpSliceEpoch(VkBufferResource& resource) { resource.sliceEpoch = ++m_sliceEpochCounter; }
VkBufferManagerInitInfo m_initInfo{};
BufferArena m_transientUploadArena;
// See AcquireUnboundStorageDescriptor. Lazily created, never re-created, torn down
@@ -402,11 +342,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// See AcquireUnboundTexelBufferDescriptor. Same lifetime rules.
VkBufferObject m_unboundTexelBuffer;
IBufferCopyCommandProvider* m_copyProvider = nullptr;
const Uint64* m_submitCounter = nullptr;
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
Vector<WeakPtr<VkBufferResource>> m_liveResources;
#if MOBILEGL_BUILD_DISAGGREGATED
std::unordered_map<Uint64, WireBufferResource> m_wireBuffers;
// P14: a slot-indexed front for m_wireBuffers (every draw resolves each vertex, index and
// uniform buffer). An entry is believed only for the generation it was taken at ({slot,
// gen} is never reused), and the erase of that node clears it: a node never moves until
// erased. Slot-indexed rather than direct-mapped because a Minecraft frame draws from a
// few thousand live buffers, which a small hashed table mostly missed.
struct WireBufferLookup {
Uint32 gen = 0;
WireBufferResource* resource = nullptr;
};
static constexpr SizeT kWireBufferLookupMaxSlots = SizeT{1} << 20;
Vector<WireBufferLookup> m_wireBufferLookup;
void ForgetWireBufferLookup(Uint64 key) {
const SizeT slot = static_cast<SizeT>(key & 0xffffffffu);
if (slot < m_wireBufferLookup.size()) m_wireBufferLookup[slot] = {};
}
// See DeferredWireRelease. ONE FLAT LIST rather than the per-frame-slot buckets above:
// the whole point is that these entries do not wait for a frame slot to come round.
Vector<DeferredWireRelease> m_deferredWireReleases;
@@ -421,18 +374,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 m_wireDeferredSyncs = 0;
// See GetWireStoreDestroyEpoch and the DeferredWireRelease comment. Bumped on every
// path that destroys a wire store's VkBuffer, and never reset (not even by Shutdown),
// for m_sliceEpochCounter's reason: a memo taken before a re-initialize must not match
// so that a memo taken before a re-initialize must not match
// a handle minted after it.
Uint64 m_wireStoreDestroyEpoch = 0;
#endif
// Size m_liveResources had just after the last sweep; the next sweep waits for it to double.
SizeT m_liveResourcesLastPruned = 0;
Uint64 m_wireContentSerial = 0;
struct ConvertedStream {
ConvertedStreamKey key;
MG_Pipe::MGPipeHandle source = MG_Pipe::kMGPipeNullHandle;
SizeT elementCount = 0;
VkBufferObject buffer;
Uint64 lastUseFrameSerial = 0;
};
Vector<ConvertedStream> m_convertedStreams;
SizeT m_convertedStreamBytes = 0;
void RetireConvertedStream(SizeT index);
Uint32 m_currentFrameIndex = 0;
Uint64 m_frameSerial = 1;
Uint64 m_completedSerialFloor = 0;
// Never reset (not even by Shutdown): a value handed to a resource must stay
// unique for the process, or a memo taken before a re-initialize could match
// a different resource's state after it.
Uint64 m_sliceEpochCounter = 0;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -13,7 +13,6 @@
#endif
namespace MobileGL::MG_Backend::DirectVulkan {
#if MOBILEGL_BUILD_DISAGGREGATED
void VkBufferObject::MoveExternalFrom(VkBufferObject& other) {
m_device = other.m_device;
m_externalMemory = other.m_externalMemory;
@@ -36,7 +35,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_externalCoherent = coherent;
m_externalAhb = ahb;
}
#endif
VkBufferObject::VkBufferObject(VkBufferObject&& other) noexcept {
m_allocator = other.m_allocator;
@@ -44,9 +42,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_allocation = other.m_allocation;
m_mappedData = other.m_mappedData;
m_size = other.m_size;
#if MOBILEGL_BUILD_DISAGGREGATED
MoveExternalFrom(other);
#endif
other.m_allocator = nullptr;
other.m_buffer = VK_NULL_HANDLE;
@@ -67,9 +63,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_allocation = other.m_allocation;
m_mappedData = other.m_mappedData;
m_size = other.m_size;
#if MOBILEGL_BUILD_DISAGGREGATED
MoveExternalFrom(other);
#endif
other.m_allocator = nullptr;
other.m_buffer = VK_NULL_HANDLE;
@@ -170,10 +164,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void VkBufferObject::Unmap() {
#if MOBILEGL_BUILD_DISAGGREGATED
// An imported store stays mapped for its life (Destroy unmaps it).
if (m_externalMemory != VK_NULL_HANDLE) return;
#endif
if (!IsValid() || m_mappedData == nullptr) {
m_mappedData = nullptr;
return;
@@ -200,7 +192,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
Memcpy(static_cast<Uint8*>(mapped) + offset, data, static_cast<SizeT>(size));
#if MOBILEGL_BUILD_DISAGGREGATED
if (m_externalMemory != VK_NULL_HANDLE) {
if (m_externalCoherent) return true;
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
@@ -209,7 +200,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
range.size = VK_WHOLE_SIZE;
return vkFlushMappedMemoryRanges(m_device, 1, &range) == VK_SUCCESS;
}
#endif
const VkResult flushResult = vmaFlushAllocation(m_allocator, m_allocation, offset, size);
if (flushResult != VK_SUCCESS) {
MGLOG_E_ONCE("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
@@ -234,7 +224,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (resolvedSize == 0) {
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED
if (m_externalMemory != VK_NULL_HANDLE) {
if (m_externalCoherent) return true;
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
@@ -243,7 +232,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
range.size = VK_WHOLE_SIZE;
return vkInvalidateMappedMemoryRanges(m_device, 1, &range) == VK_SUCCESS;
}
#endif
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
if (result != VK_SUCCESS) {
@@ -65,13 +65,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void* GetMappedData() const { return m_mappedData; }
Bool IsMapped() const { return m_mappedData != nullptr; }
Bool IsValid() const {
#if MOBILEGL_BUILD_DISAGGREGATED
if (m_externalMemory != VK_NULL_HANDLE) return m_buffer != VK_NULL_HANDLE;
#endif
return m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P11 B2 (T0): a store whose memory is NOT this allocator's - a client's AHardwareBuffer
// imported with VK_ANDROID_external_memory_android_hardware_buffer into `memory` (a
// dedicated allocation bound to `buffer`, mapped persistently at `mapped`). It behaves
@@ -82,7 +79,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void AdoptExternal(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize size,
void* mapped, Bool coherent, void* ahb);
Bool IsExternal() const { return m_externalMemory != VK_NULL_HANDLE; }
#endif
private:
VmaAllocator m_allocator = nullptr;
@@ -90,12 +86,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VmaAllocation m_allocation = nullptr;
void* m_mappedData = nullptr;
VkDeviceSize m_size = 0;
#if MOBILEGL_BUILD_DISAGGREGATED
VkDevice m_device = VK_NULL_HANDLE;
VkDeviceMemory m_externalMemory = VK_NULL_HANDLE;
void* m_externalAhb = nullptr;
Bool m_externalCoherent = true;
void MoveExternalFrom(VkBufferObject& other);
#endif
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -13,7 +13,7 @@
#include "VkTextureManager.h"
#include "MG_State/GLState/Core.h"
#include <MG_Pipe/PipeInputsSwitch.h>
#include <MG_Backend/MGPipe/PipeInputs.h>
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
@@ -55,7 +55,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (payload.colorEncoding != ClearColorEncoding::Float) return;
// With GL_FRAMEBUFFER_SRGB enabled GL performs the encoding itself, so the driver doing it
// is exactly right and there is nothing to undo.
if (MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)) return;
if (MG_Pipe::gPipeInputs->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)) return;
if (ResolveSrgbAttachmentWriteFormat(destinationFormat, false) == destinationFormat) return;
// sRGB -> linear (GL 4.6 core 8.24), applied to the colour channels only: alpha is stored
@@ -68,43 +68,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
toLinear(payload.color.z()), payload.color.w());
}
void ForceOpaqueClearAlpha(ClearAttachmentPayload& payload) {
switch (payload.colorEncoding) {
case ClearColorEncoding::Int:
payload.colorInt = IntVec4(payload.colorInt.x(), payload.colorInt.y(), payload.colorInt.z(), 1);
break;
case ClearColorEncoding::Uint:
payload.colorUint = UintVec4(payload.colorUint.x(), payload.colorUint.y(), payload.colorUint.z(), 1u);
break;
case ClearColorEncoding::Float:
payload.color = FloatVec4(payload.color.x(), payload.color.y(), payload.color.z(), 1.0f);
break;
}
}
static Bool PendingClearMatchesTextureIdentity(const PendingClearKey& key, const TextureIdentity& identity) {
return key.texture == identity.texture && key.textureLifetimeId == identity.lifetimeId;
}
static Uint32 ResolveAttachmentBaseArrayLayer(TextureUploadTarget target) {
if (!IsCubeMapFaceUploadTarget(target)) {
return 0;
}
return static_cast<Uint32>(target) - static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX);
}
static Uint32 ResolveAttachmentBaseArrayLayer(
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (attachment.IsLayered()) {
return 0;
}
const TextureUploadTarget uploadTarget = attachment.GetTextureUploadTarget();
if (!IsCubeMapFaceUploadTarget(uploadTarget)) {
return static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
}
return ResolveAttachmentBaseArrayLayer(uploadTarget);
}
// ResolveAttachmentLayerCount used to be duplicated here, reading attachment.GetSize().z()
// raw - no ToVulkanLevelExtent remap for a 1D array, no six-faces arm for a cube map. That is
// not a cosmetic difference: the count below is not key-only, it is written straight into
@@ -113,425 +76,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// 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(
const MG_State::GLState::FramebufferObject& drawFbo, FramebufferAttachmentType attachmentType) {
if (attachmentType == FramebufferAttachmentType::None) {
return nullptr;
}
const auto& attachment = drawFbo.GetAttachment(attachmentType);
if (!attachment.IsTexture() || attachment.IsRenderbuffer()) {
return nullptr;
}
return &attachment;
}
// 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) {
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 {
.texture = texture,
.textureLifetimeId = texture ? texture->GetLifetimeId() : 0,
.mipLevel = mipLevel,
.baseArrayLayer = baseArrayLayer,
.layerCount = layerCount,
};
}
PendingClearKey VkClearManager::MakePendingClearKey(
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
MOBILEGL_ASSERT(attachment.IsTexture() && !attachment.IsRenderbuffer(),
"MakePendingClearKey requires a texture framebuffer attachment");
auto* texture = attachment.GetTexture().get();
MOBILEGL_ASSERT(texture != nullptr, "MakePendingClearKey: texture attachment resolved to null");
const Uint32 mipLevel = static_cast<Uint32>(std::max(attachment.GetTextureLevel(), 0));
const Uint32 baseArrayLayer = ResolveAttachmentBaseArrayLayer(attachment);
const Uint32 layerCount = ResolveAttachmentLayerCount(attachment);
return MakePendingClearKey(texture, mipLevel, baseArrayLayer, layerCount);
}
Bool VkClearManager::Initialize() {
return true;
}
void VkClearManager::Shutdown() {
const std::lock_guard<std::mutex> lock(m_mutex);
m_pendingClears.clear();
m_aliveObjects.clear();
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
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 {
.texture = texture,
.lifetimeId = texture ? texture->GetLifetimeId() : 0,
};
}
void VkClearManager::MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src) {
dst.mask |= src.mask;
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.colorEncoding = src.colorEncoding;
dst.colorInt = src.colorInt;
dst.colorUint = src.colorUint;
}
if ((src.mask & GL_DEPTH_BUFFER_BIT) != 0) {
dst.depth = src.depth;
}
if ((src.mask & GL_STENCIL_BUFFER_BIT) != 0) {
dst.stencil = src.stencil;
}
}
void VkClearManager::ErasePendingClearsForTextureLocked(const TextureIdentity& identity) {
Vector<PendingClearKey> keysToErase;
keysToErase.reserve(m_pendingClears.size());
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
if (PendingClearMatchesTextureIdentity(it->first, identity)) {
keysToErase.emplace_back(it->first);
}
}
for (const auto& key : keysToErase) {
m_pendingClears.erase(key);
}
m_aliveObjects.erase(identity);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
Bool VkClearManager::LockTextureIdentityLocked(const TextureIdentity& identity,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
outTexture.reset();
if (identity.texture == nullptr) {
return false;
}
auto aliveIt = m_aliveObjects.find(identity);
if (aliveIt == m_aliveObjects.end()) {
ErasePendingClearsForTextureLocked(identity);
return false;
}
outTexture = aliveIt->second.lock();
if (!outTexture || outTexture.get() != identity.texture || outTexture->GetLifetimeId() != identity.lifetimeId) {
ErasePendingClearsForTextureLocked(identity);
outTexture.reset();
return false;
}
return true;
}
Bool VkClearManager::LockTextureLocked(const PendingClearKey& key,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
return LockTextureIdentityLocked(TextureIdentity{
.texture = key.texture,
.lifetimeId = key.textureLifetimeId,
}, outTexture);
}
void VkClearManager::QueueClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
const MG_State::GLState::FramebufferObject& drawFbo) {
if (mask & GL_COLOR_BUFFER_BIT) {
auto& drawbufs = drawFbo.GetDrawBuffers();
// This should automatically work on default & offscreen FBO
for (auto drawbuf: drawbufs) {
const auto* attachment = GetClearableAttachment(drawFbo, drawbuf);
if (!attachment) {
continue;
}
QueueClear({
.mask = GL_COLOR_BUFFER_BIT,
.color = clearPayload.color
}, *attachment);
MGLOG_D("%s: %s (texture %d) - color = (%.2f, %.2f, %.2f, %.2f)", __func__,
MG_Util::ConvertFramebufferAttachmentTypeToString(drawbuf).c_str(),
attachment->GetTexture()->GetExternalIndex(),
clearPayload.color[0], clearPayload.color[1], clearPayload.color[2], clearPayload.color[3]);
}
}
if (mask & GL_DEPTH_BUFFER_BIT) {
const auto* attachment = GetClearableAttachment(drawFbo, FramebufferAttachmentType::Depth);
if (attachment) {
QueueClear({
.mask = GL_DEPTH_BUFFER_BIT,
.depth = clearPayload.depth,
}, *attachment);
MGLOG_D("%s: Depth (texture %d) - depth = (%.2f)", __func__,
attachment->GetTexture()->GetExternalIndex(), clearPayload.depth);
}
}
if (mask & GL_STENCIL_BUFFER_BIT) {
const auto* attachment = GetClearableAttachment(drawFbo, FramebufferAttachmentType::Stencil);
if (attachment) {
QueueClear({
.mask = GL_STENCIL_BUFFER_BIT,
.stencil = clearPayload.stencil,
}, *attachment);
MGLOG_D("%s: Stencil (texture %d) - stencil = (%u)", __func__,
attachment->GetTexture()->GetExternalIndex(), clearPayload.stencil);
}
}
}
void VkClearManager::QueueClear(const ClearAttachmentPayload& clearPayload,
const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
if (clearPayload.mask == 0 || !texture) {
return;
}
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);
m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
auto& pending = m_pendingClears[key];
MergeClearPayload(pending, clearPayload);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
void VkClearManager::QueueClear(const ClearAttachmentPayload& clearPayload,
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (clearPayload.mask == 0 || !attachment.IsTexture() || attachment.IsRenderbuffer()) {
return;
}
const auto texture = attachment.GetTexture();
if (!texture) {
return;
}
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);
m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
auto& pending = m_pendingClears[key];
MergeClearPayload(pending, clearPayload);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
Bool VkClearManager::HasPendingClear(MG_State::GLState::ITextureObject* texture) {
if (texture == nullptr) {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
texture = ClearStorageTextureOf(texture);
const Uint64 lifetimeId = texture->GetLifetimeId();
const std::lock_guard<std::mutex> lock(m_mutex);
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
if (it->first.texture == texture && it->first.textureLifetimeId == lifetimeId) {
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
return LockTextureLocked(it->first, liveTexture);
}
}
return false;
}
Bool VkClearManager::HasPendingClear(const PendingClearKey& key) {
if (key.texture == nullptr) {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
const std::lock_guard<std::mutex> lock(m_mutex);
if (m_pendingClears.find(key) == m_pendingClears.end()) {
return false;
}
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
return LockTextureLocked(key, liveTexture);
}
Bool VkClearManager::HasPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (!attachment.IsTexture() || attachment.IsRenderbuffer() || !attachment.GetTexture()) {
return false;
}
return HasPendingClear(MakePendingClearKey(attachment));
}
Bool VkClearManager::GetPendingClear(const PendingClearKey& key, ClearAttachmentPayload& outPayload) {
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
return GetPendingClear(key, outPayload, liveTexture);
}
Bool VkClearManager::GetPendingClear(const PendingClearKey& key, ClearAttachmentPayload& outPayload,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
if (key.texture == nullptr) {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
const std::lock_guard<std::mutex> lock(m_mutex);
if (!LockTextureLocked(key, outTexture)) {
return false;
}
auto it = m_pendingClears.find(key);
if (it == m_pendingClears.end()) {
outTexture.reset();
return false;
}
outPayload = it->second;
MGLOG_D("%s: Got pending clear for texture@%p lifetime=%llu, mip=%u layer=%u count=%u mask=0x%x clear value: color = (%.2f, %.2f, %.2f, %.2f), depth = (%.2f), stencil = (%u)", __func__,
static_cast<void*>(key.texture),
static_cast<unsigned long long>(key.textureLifetimeId),
key.mipLevel, key.baseArrayLayer, key.layerCount,
static_cast<Uint32>(outPayload.mask),
outPayload.color[0], outPayload.color[1], outPayload.color[2], outPayload.color[3],
outPayload.depth,
outPayload.stencil);
return true;
}
Bool VkClearManager::GetPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment,
ClearAttachmentPayload& outPayload) {
if (!attachment.IsTexture() || attachment.IsRenderbuffer() || !attachment.GetTexture()) {
MGLOG_D("%s: Failed getting pending clear for non-texture framebuffer attachment", __func__);
return false;
}
return GetPendingClear(MakePendingClearKey(attachment), outPayload);
}
Bool VkClearManager::GetPendingClears(MG_State::GLState::ITextureObject* texture,
Vector<PendingClearEntry>& outEntries) {
outEntries.clear();
if (texture == nullptr) {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
texture = ClearStorageTextureOf(texture);
const Uint64 lifetimeId = texture->GetLifetimeId();
const std::lock_guard<std::mutex> lock(m_mutex);
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
if (!LockTextureIdentityLocked(MakeTextureIdentity(texture), liveTexture)) {
return false;
}
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
if (it->first.texture == texture && it->first.textureLifetimeId == lifetimeId) {
outEntries.emplace_back(PendingClearEntry{.key = it->first, .payload = it->second});
}
}
return !outEntries.empty();
}
void VkClearManager::PopPendingClear(MG_State::GLState::ITextureObject* texture) {
if (texture == nullptr) {
return;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return; // per-draw hot path: nothing pending anywhere
}
const TextureIdentity identity = MakeTextureIdentity(texture);
MGLOG_D("%s: Pop all pending clears for texture %d", __func__, texture->GetExternalIndex());
const std::lock_guard<std::mutex> lock(m_mutex);
ErasePendingClearsForTextureLocked(identity);
}
void VkClearManager::PopPendingClear(const PendingClearKey& key) {
if (key.texture == nullptr) {
return;
}
{
const std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_pendingClears.find(key);
if (it != m_pendingClears.end()) {
m_pendingClears.erase(it);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
}
MGLOG_D("%s: Pop pending clear for texture@%p lifetime=%llu mip=%u layer=%u count=%u", __func__,
static_cast<void*>(key.texture), static_cast<unsigned long long>(key.textureLifetimeId),
key.mipLevel, key.baseArrayLayer, key.layerCount);
}
void VkClearManager::PopPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (!attachment.IsTexture() || attachment.IsRenderbuffer() || !attachment.GetTexture()) {
return;
}
PopPendingClear(MakePendingClearKey(attachment));
}
SizeT VkClearManager::CollectGarbage() {
const std::lock_guard<std::mutex> lock(m_mutex);
m_gcCounter++;
if (m_gcCounter != 0) {
return 0;
}
Vector<TextureIdentity> expiredTextures;
expiredTextures.reserve(m_aliveObjects.size());
for (auto it = m_aliveObjects.begin(); it != m_aliveObjects.end(); ++it) {
if (it->second.expired()) {
expiredTextures.emplace_back(it->first);
}
}
if (expiredTextures.empty()) {
return 0;
}
for (const auto& identity : expiredTextures) {
ErasePendingClearsForTextureLocked(identity);
}
return expiredTextures.size();
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -46,10 +46,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// expressed in whichever type matches (GL 4.6 core 15.2.3).
VkClearColorValue MakeVkClearColorValue(const ClearAttachmentPayload& payload, Bool formatLacksAlpha);
// Applies that same rule in place, for the paths that have to bake it into the payload before
// the destination is known.
void ForceOpaqueClearAlpha(ClearAttachmentPayload& payload);
// vkCmdClearColorImage names the image, so the driver applies the destination format's transfer
// function to whatever value it is handed. Every other write path in this backend goes through
// the UNORM twin view while GL_FRAMEBUFFER_SRGB is off (ResolveSrgbAttachmentWriteFormat) and
@@ -59,111 +55,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// encoding itself.
void PreCompensateSrgbClearColor(ClearAttachmentPayload& payload, VkFormat destinationFormat);
struct PendingClearKey {
MG_State::GLState::ITextureObject* texture = nullptr;
Uint64 textureLifetimeId = 0;
Uint32 mipLevel = 0;
Uint32 baseArrayLayer = 0;
Uint32 layerCount = 1;
Bool operator==(const PendingClearKey& other) const {
return texture == other.texture && textureLifetimeId == other.textureLifetimeId &&
mipLevel == other.mipLevel &&
baseArrayLayer == other.baseArrayLayer && layerCount == other.layerCount;
}
};
struct TextureIdentity {
MG_State::GLState::ITextureObject* texture = nullptr;
Uint64 lifetimeId = 0;
Bool operator==(const TextureIdentity& other) const {
return texture == other.texture && lifetimeId == other.lifetimeId;
}
};
struct PendingClearEntry {
PendingClearKey key{};
ClearAttachmentPayload payload{};
};
struct PendingClearKeyHash {
SizeT operator()(const PendingClearKey& key) const {
const SizeT textureHash = std::hash<MG_State::GLState::ITextureObject*>{}(key.texture);
const SizeT textureLifetimeHash = std::hash<Uint64>{}(key.textureLifetimeId);
const SizeT mipHash = std::hash<Uint32>{}(key.mipLevel);
const SizeT layerHash = std::hash<Uint32>{}(key.baseArrayLayer);
const SizeT layerCountHash = std::hash<Uint32>{}(key.layerCount);
SizeT hash = textureHash;
hash ^= textureLifetimeHash + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= mipHash + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= layerHash + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= layerCountHash + 0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
struct TextureIdentityHash {
SizeT operator()(const TextureIdentity& key) const {
SizeT hash = std::hash<MG_State::GLState::ITextureObject*>{}(key.texture);
hash ^= std::hash<Uint64>{}(key.lifetimeId) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
class VkClearManager {
public:
static PendingClearKey MakePendingClearKey(const MG_State::GLState::FramebufferAttachmentObject& attachment);
// 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);
Bool Initialize();
void Shutdown();
void QueueClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload, const MG_State::GLState::FramebufferObject& drawFbo);
void QueueClear(
const ClearAttachmentPayload& clearPayload,
const SharedPtr<MG_State::GLState::ITextureObject>& texture);
void QueueClear(const ClearAttachmentPayload& clearPayload,
const MG_State::GLState::FramebufferAttachmentObject& attachment);
Bool HasPendingClear(MG_State::GLState::ITextureObject* texture);
Bool HasPendingClear(const PendingClearKey& key);
Bool HasPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment);
Bool GetPendingClear(const PendingClearKey& key, ClearAttachmentPayload& outPayload);
Bool GetPendingClear(const PendingClearKey& key, ClearAttachmentPayload& outPayload,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
Bool GetPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment,
ClearAttachmentPayload& outPayload);
Bool GetPendingClears(MG_State::GLState::ITextureObject* texture, Vector<PendingClearEntry>& outEntries);
void PopPendingClear(MG_State::GLState::ITextureObject* texture);
void PopPendingClear(const PendingClearKey& key);
void PopPendingClear(const MG_State::GLState::FramebufferAttachmentObject& attachment);
SizeT CollectGarbage();
private:
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
static void MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src);
void ErasePendingClearsForTextureLocked(const TextureIdentity& identity);
Bool LockTextureIdentityLocked(const TextureIdentity& identity,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
Bool LockTextureLocked(const PendingClearKey& key,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
Uint8 m_gcCounter = 0;
public:
// Lock-free probe for the consecutive-draw fast path: any pending clear
// forces the full SetupDraw path (which materializes/consumes it).
Bool HasAnyPendingClears() const { return m_pendingCount.load(std::memory_order_relaxed) != 0; }
private:
mutable std::mutex m_mutex;
// Lock-free mirror of m_pendingClears.size(), maintained under m_mutex
// by every mutation. The per-draw probes (HasPendingClear/GetPending*)
// read it before taking the lock: during draw batches the pending set
// is almost always empty, so this turns several locked map probes per
// draw into one relaxed load.
std::atomic<Uint32> m_pendingCount{0};
std::unordered_map<PendingClearKey, ClearAttachmentPayload, PendingClearKeyHash> m_pendingClears;
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -20,40 +20,6 @@
#include <vk_mem_alloc.h>
namespace MobileGL::MG_Backend::DirectVulkan {
enum class TrackedAttachmentTarget : Uint8 {
Texture,
Renderbuffer,
SwapchainColor,
SwapchainDepthStencil
};
struct PendingClearAttachmentInfo {
// Index into the render pass attachment descriptions (VkRenderPassBeginInfo::pClearValues space).
Uint32 attachmentIndex = 0;
// Index into the subpass pColorAttachments (VkClearAttachment::colorAttachment space) — the GL
// draw-buffer slot. Differs from attachmentIndex when earlier slots are GL_NONE/incomplete.
// Only meaningful for color clears.
Uint32 colorAttachmentSlot = 0;
PendingClearKey key{};
MG_State::GLState::RenderbufferObject* renderbuffer = nullptr;
Bool hasInlinePayload = false;
ClearAttachmentPayload inlinePayload{};
};
struct TrackedAttachmentLayoutInfo {
TrackedAttachmentTarget target = TrackedAttachmentTarget::Texture;
WeakPtr<MG_State::GLState::ITextureObject> texture;
// Identity-compare shortcut for the per-draw "does the active pass use
// this sampled texture" probe: comparing this against a LIVE texture's
// address needs no weak_ptr::lock (two refcount atomics per probe).
// May dangle once the texture dies - compare only, never dereference.
MG_State::GLState::ITextureObject* textureRaw = nullptr;
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
Uint32 textureMipLevel = 0;
Uint32 swapchainImageIndex = 0;
VkImageLayout finalLayout = VK_IMAGE_LAYOUT_UNDEFINED;
};
struct DepthStencilAttachmentLoadInfo {
VkAttachmentLoadOp depthLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
VkAttachmentLoadOp stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
@@ -68,13 +34,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
struct RenderPassEntry {
// Per Magma session (MagmaSession.h): each session's renderer is its own device.
static inline SessionLocal<VkDevice> s_device;
static inline thread_local Vector<VkTextureManager::TextureResource*> s_textureResourcesScratch;
Uint64 hash = 0;
VkRenderPass renderPass = VK_NULL_HANDLE;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
Uint64 compatibilityHash = 0;
Vector<PendingClearAttachmentInfo> pendingClearAttachments;
Vector<TrackedAttachmentLayoutInfo> trackedAttachmentLayouts;
Uint32 attachmentCount = 0;
Uint32 colorAttachmentCount = 0;
Bool hasDepthStencilAttachment = false;
@@ -84,6 +47,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 layers = 1;
// Frame counter value of the last GetOrCreateRenderPass hit; drives cache eviction.
Uint64 lastUsedFrame = 0;
// P15 S0 (wire passes only): what the render pass was created from, and the render
// passes that differ from it only in loading some attachments with LOAD_OP_CLEAR. Load ops
// are not part of render-pass compatibility, so a variant begins with this entry's own
// framebuffer and runs every pipeline created against `renderPass`.
Vector<VkAttachmentDescription> wireAttachments;
Vector<VkAttachmentReference> wireColors;
VkAttachmentReference wireDepth{VK_ATTACHMENT_UNUSED, VK_IMAGE_LAYOUT_GENERAL};
Vector<std::pair<Uint64, VkRenderPass>> clearVariants;
RenderPassEntry() = default;
RenderPassEntry(const RenderPassEntry&) = delete;
@@ -92,8 +63,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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);
@@ -101,6 +70,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
std::swap(extent, that.extent);
std::swap(layers, that.layers);
std::swap(lastUsedFrame, that.lastUsedFrame);
std::swap(wireAttachments, that.wireAttachments);
std::swap(wireColors, that.wireColors);
std::swap(wireDepth, that.wireDepth);
std::swap(clearVariants, that.clearVariants);
}
// Move ASSIGNMENT, not just construction. The move constructor above and the
// destructor below each independently suppress the implicit one, which left the
@@ -126,8 +99,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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);
@@ -135,36 +106,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
std::swap(extent, that.extent);
std::swap(layers, that.layers);
std::swap(lastUsedFrame, that.lastUsedFrame);
std::swap(wireAttachments, that.wireAttachments);
std::swap(wireColors, that.wireColors);
std::swap(wireDepth, that.wireDepth);
std::swap(clearVariants, that.clearVariants);
}
return *this;
}
RenderPassEntry(
Uint64 hash,
VkRenderPass renderpass,
VkFramebuffer framebuffer,
Uint64 compatibilityHash,
const Vector<PendingClearAttachmentInfo>& pendingClearAttachments,
const Vector<TrackedAttachmentLayoutInfo>& trackedAttachmentLayouts,
Uint32 attachmentCount,
Uint32 colorAttachmentCount,
Bool hasDepthStencilAttachment,
VkSampleCountFlagBits sampleCount,
IntVec2 extent, Uint32 layers):
hash(hash),
renderPass(renderpass),
framebuffer(framebuffer),
compatibilityHash(compatibilityHash),
pendingClearAttachments(Move(pendingClearAttachments)),
trackedAttachmentLayouts(Move(trackedAttachmentLayouts)),
attachmentCount(attachmentCount),
colorAttachmentCount(colorAttachmentCount),
hasDepthStencilAttachment(hasDepthStencilAttachment),
sampleCount(sampleCount),
extent(extent),
layers(layers)
{}
~RenderPassEntry() {
for (const auto& variant : clearVariants)
if (variant.second != VK_NULL_HANDLE) vkDestroyRenderPass(s_device.Get(), variant.second, nullptr);
if (renderPass != VK_NULL_HANDLE) {
vkDestroyRenderPass(s_device.Get(), renderPass, nullptr);
}
@@ -186,7 +137,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 hash = 0;
Uint64 compatibilityHash = 0;
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
Vector<TrackedAttachmentLayoutInfo> trackedAttachmentLayouts;
IntVec2 extent = {0, 0};
Bool CompatibleWith(const RenderPassEntry& that) const {
@@ -202,85 +152,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
public:
using HashType = Uint64;
// Notified once per OnPresent sweep with every aged-out entry's VkRenderPass
// value: pipelines are hashed on the raw handle, and once destroyed the value
// may be recycled for an incompatible pass, so dependent caches must purge
// everything keyed on them before any new pass can be created (the sweep and
// the notification run back-to-back with no creation in between; observers
// compare the values, never dereference them). Batched so a mass-idle cohort
// (shader-pack switch, dimension exit) costs the observer one pipeline-cache
// scan, not one per dying pass. The wholesale paths
// (Shutdown/RecreateSwapchain) do not notify - their callers already drop
// every pipeline outright.
class IEvictionObserver {
public:
virtual ~IEvictionObserver() = default;
virtual void OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) = 0;
};
VkRenderPassManager(VkDevice device,
VkPhysicalDevice physicalDevice, VmaAllocator allocator, const VulkanRendererConfig& config,
VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject);
VkTextureManager& textureManager, SwapchainObject& swapchainObject);
~VkRenderPassManager();
// Observer may be null (no notifications). Not owned.
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
Bool Initialize();
void Shutdown();
// The surface target the default framebuffer currently names (VulkanRenderer's surface
// targets): a default-framebuffer pass of one surface is not another's.
void SetDefaultFramebufferTarget(Uint64 serial) {
m_defaultFramebufferTarget = serial;
m_rpFastValid = false;
}
// Destroys every cached pass and framebuffer (not the renderbuffers). The device must be
// idle and no pass may be open: a surface target's image views are going away.
void PurgeRenderPasses();
HashType ComputeHash(
const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex,
Bool includePendingClear = true,
Bool includeDefaultFboDepthStencil = true);
// drawUsesDepthStencil: whether the operation about to run inside the pass
// reads or writes the depth/stencil buffer (depth test or stencil test
// enabled, or a depth/stencil clear). Only consulted for the DEFAULT
// framebuffer: EGL undefines its ancillary buffers at every swap, so a
// default-FBO pass whose draws provably never touch depth/stencil is
// created WITHOUT the depth attachment - on a tiler that skips the whole
// depth tile load AND store. The flavor only escalates: once a pass with
// depth is active, later depth-less draws keep using it, and a depth-using
// draw against a depth-less active pass resolves to a new (incompatible)
// 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).
//
// Returns NULLPTR when this framebuffer cannot be represented as a Vulkan render pass at
// 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,
const MG_State::GLState::FramebufferObject& drawFbo);
void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
const MG_State::GLState::FramebufferAttachmentObject& attachment);
void PopPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer);
// Frame boundary hook: ages the render-pass cache and evicts long-unused
// entries (their command buffers retired many frames ago).
void OnPresent();
static Bool BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry);
// P15 S0: begins `renderPassEntry` through `renderPass` (one of its clear-load variants)
// with these clear values; it is active as the entry itself, so a draw continues it.
static Bool BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry,
VkRenderPass renderPass, const VkClearValue* clearValues, Uint32 clearValueCount);
static Bool EndRenderPass(VkCommandBuffer commandBuffer);
static ActiveRenderPassInfo* GetActiveRenderPass();
private:
@@ -288,13 +176,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
VmaAllocator m_allocator = nullptr;
const VulkanRendererConfig& m_config;
VkClearManager& m_clearManager;
VkTextureManager& m_textureManager;
SwapchainObject& m_swapchainObject;
UnorderedMap<Uint64, RenderPassEntry> m_renderPasses;
// Monotonic frame counter (bumped in OnPresent) for render-pass cache aging.
Uint64 m_frameCounter = 0;
IEvictionObserver* m_evictionObserver = nullptr;
// Bumped whenever a renderbuffer VkImage is (re)created; together with the texture
// manager's image epoch this invalidates the render-pass fast path on any attachment
@@ -308,127 +191,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
private:
// Per-draw fast-path memo for GetOrCreateRenderPass (dirty-flag state tracking): when the
// framebuffer state is provably unchanged since the last resolution, the active render pass
// is reused WITHOUT recomputing the expensive per-draw hash. Invalidated by FBO switch /
// version change, swapchain rotation, any attachment image recreation (the two epochs),
// or a pending clear. Portable to Vulkan 1.1 (no dynamic_rendering / imageless FB needed).
Uint64 m_defaultFramebufferTarget = 0;
Bool m_rpFastValid = false;
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;
Uint32 m_rpFastSwapchainIndex = 0;
Uint64 m_rpFastTexEpoch = 0;
Uint64 m_rpFastRbEpoch = 0;
Uint64 m_rpFastRenderPassHash = 0;
// Whether the memoized entry carries a depth/stencil attachment; a
// default-FBO resolution whose effective depth request differs must
// miss the memo (the depth-less/depth-full flavors hash differently).
Bool m_rpFastHadDepthStencil = false;
public:
struct RenderbufferResource {
// deadSinceFrame sentinel: the owning weak reference has not been observed
// expired. Dead resources age past every in-flight frame before Destroy
// (see CollectRenderbufferGarbage); the GPU may still reference the image
// for frames-in-flight frames after the GL object dies.
static constexpr Uint64 kNeverObservedDead = UINT64_MAX;
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
VkImage image = VK_NULL_HANDLE;
VmaAllocation allocation = nullptr;
VkImageView view = VK_NULL_HANDLE;
// UNORM reinterpretation of an sRGB image, used as the attachment view while
// GL_FRAMEBUFFER_SRGB is disabled (raw writes). Null for non-sRGB formats.
VkImageView unormTwinView = VK_NULL_HANDLE;
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
VkExtent2D extent = {0, 0};
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
Int samples = 0;
// m_frameCounter value at which the weak reference was first seen expired.
Uint64 deadSinceFrame = kNeverObservedDead;
void Destroy(VkDevice device, VmaAllocator allocator);
};
// Public so the renderer's blit/copy/readback bindings can source renderbuffer
// attachments the same way texture attachments go through the texture manager.
RenderbufferResource* GetOrCreateRenderbufferResource(
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
Bool GetPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer,
ClearAttachmentPayload& outPayload) const;
private:
struct PendingRenderbufferClear {
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
ClearAttachmentPayload payload{};
};
// A superseded renderbuffer backing (glRenderbufferStorage respecify) parked
// until enough frame boundaries have passed that no in-flight command buffer
// can still reference it; destroyed in OnPresent (see RetireAgeFrames).
struct DeferredRenderbufferRelease {
VkImage image = VK_NULL_HANDLE;
VmaAllocation allocation = nullptr;
VkImageView view = VK_NULL_HANDLE;
VkImageView unormTwinView = VK_NULL_HANDLE;
Uint64 deferredAtFrame = 0;
};
// Node-based std::unordered_map, deliberately NOT the open-addressing UnorderedMap:
// 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
// destination colour bindings (ResolveColorBlitBinding caches &rbResource->layout), then
// materializes the source's pending clear, which looks that same resource up again. Growing
// an open-addressed table relocates every element, so the cached pointer went on to name
// freed storage still holding the pre-clear VK_IMAGE_LAYOUT_UNDEFINED; BlitFramebuffer bailed
// out at "source image layout is 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
// this: the destination resolve still runs after the source pointer is taken. The depth blit,
// 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
// ordering rule. m_textureResources is node-based for the same reason.
//
// 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;
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
// Supported sample counts per attachment format, so per-draw resource lookups
// do not repeat vkGetPhysicalDeviceImageFormatProperties.
UnorderedMap<VkFormat, VkSampleCountFlags> m_attachmentSampleCountsByFormat;
Bool HasPendingRenderbufferClear(
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
void CollectRenderbufferGarbage();
// Frame-boundary margin after which a resource last referenced by a retired
// GL object (or superseded backing) is provably past every in-flight frame.
Uint64 RetireAgeFrames() const;
void DeferRenderbufferBackingRelease(RenderbufferResource& resource);
void CollectDeferredRenderbufferReleases(Bool destroyAll);
static inline thread_local XXH64_state_t* m_hashState = XXH64_createState();
// Per Magma session (MagmaSession.h): one session's open render pass and managers are not
// another's.
static inline SessionLocal<ActiveRenderPassInfo> s_activeRenderPass;
static inline SessionLocal<Bool> s_hasActiveRenderPass;
static inline SessionLocal<VkClearManager*> s_clearManager;
static inline SessionLocal<VkTextureManager*> s_textureManager;
static inline SessionLocal<SwapchainObject*> s_swapchainObject;
static inline SessionLocal<VkRenderPassManager*> s_renderPassManager;
@@ -15,7 +15,6 @@
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
#if MOBILEGL_BUILD_DISAGGREGATED
// An accessor view over the wire's POD values, not a GL object with constructor,
// lifetime ID, allocator hooks or mutation callbacks. Both sources below use
// one sampler policy, including format-sensitive border-color conversion.
@@ -43,9 +42,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
template <class SamplerSource>
Bool UsesBorderColor(const SamplerSource& sampler) {
#else
Bool UsesBorderColor(const MG_State::GLState::SamplerObject& sampler) {
#endif
return sampler.GetWrapS() == SamplerWrapMode::ClampToBorder ||
sampler.GetWrapT() == SamplerWrapMode::ClampToBorder ||
sampler.GetWrapR() == SamplerWrapMode::ClampToBorder;
@@ -221,24 +217,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return std::fabs(lhs - rhs) <= 1e-6f;
}
#if MOBILEGL_BUILD_DISAGGREGATED
template <class SamplerSource>
Float ResolveEffectiveMaxLod(const SamplerSource& sampler) {
#else
Float ResolveEffectiveMaxLod(const MG_State::GLState::SamplerObject& sampler) {
#endif
if (sampler.GetMipmapMode() == SamplerMipmapMode::None) {
return 0.0f;
}
return sampler.GetMaxLod();
}
#if MOBILEGL_BUILD_DISAGGREGATED
template <class SamplerSource>
Float ResolveEffectiveMinLod(const SamplerSource& sampler, Float effectiveMaxLod) {
#else
Float ResolveEffectiveMinLod(const MG_State::GLState::SamplerObject& sampler, Float effectiveMaxLod) {
#endif
return std::min(sampler.GetMinLod(), effectiveMaxLod);
}
@@ -249,12 +237,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// GL's non-mipmapped minification - large enough for lambda to stay positive, small enough
// that a NEAREST mip mode still rounds down to level 0. Clamped rather than assigned, so a
// texture whose GL_TEXTURE_MAX_LOD really is 0 keeps magnifying as GL says it must.
#if MOBILEGL_BUILD_DISAGGREGATED
template <class SamplerSource>
Float ResolveSingleLevelMaxLod(const SamplerSource& sampler, Bool singleLevelView) {
#else
Float ResolveSingleLevelMaxLod(const MG_State::GLState::SamplerObject& sampler, Bool singleLevelView) {
#endif
const Float maxLod = ResolveEffectiveMaxLod(sampler);
return singleLevelView ? std::min(maxLod, 0.25f) : maxLod;
}
@@ -275,12 +259,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED
template <class SamplerSource>
Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const SamplerSource& sampler,
#else
Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler,
#endif
Bool forceNearestFiltering) const {
if (!m_samplerAnisotropySupported) return 1.0f;
if (forceNearestFiltering) return 1.0f;
@@ -294,6 +274,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void VkSamplerManager::Shutdown() {
++m_destroyEpoch;
for (auto& [_, sampler] : m_samplers) {
if (m_device != VK_NULL_HANDLE && sampler.handle != VK_NULL_HANDLE) {
vkDestroySampler(m_device, sampler.handle, nullptr);
@@ -329,6 +310,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (m_device != VK_NULL_HANDLE && entry.handle != VK_NULL_HANDLE) {
vkDestroySampler(m_device, entry.handle, nullptr);
}
++m_destroyEpoch;
if (entry.usesCustomBorderColor && m_customBorderColorSamplerCount > 0) {
--m_customBorderColorSamplerCount;
}
@@ -339,65 +321,58 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
#if MOBILEGL_BUILD_DISAGGREGATED
template <class SamplerSource>
Uint64 VkSamplerManager::BuildSamplerKey(const SamplerSource& sampler,
#else
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
#endif
Bool forceNearestFiltering, Bool singleLevelView,
const ResolvedBorderColor& borderColor) const {
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
// P15: one read of the thread_local per hash (emulated TLS: every read is a call).
XXH64_state_t* const hashState = m_hashState;
XXHASH_VERIFY(XXH64_reset(hashState, m_config->CacheVersion));
XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering)));
XXHASH_VERIFY(XXH64_update(m_hashState, &singleLevelView, sizeof(singleLevelView)));
XXHASH_VERIFY(XXH64_update(hashState, &forceNearestFiltering, sizeof(forceNearestFiltering)));
XXHASH_VERIFY(XXH64_update(hashState, &singleLevelView, sizeof(singleLevelView)));
const auto minFilter = sampler.GetMinFilter();
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
XXHASH_VERIFY(XXH64_update(hashState, &minFilter, sizeof(minFilter)));
const auto magFilter = sampler.GetMagFilter();
XXHASH_VERIFY(XXH64_update(m_hashState, &magFilter, sizeof(magFilter)));
XXHASH_VERIFY(XXH64_update(hashState, &magFilter, sizeof(magFilter)));
const auto mipmapMode = sampler.GetMipmapMode();
XXHASH_VERIFY(XXH64_update(m_hashState, &mipmapMode, sizeof(mipmapMode)));
XXHASH_VERIFY(XXH64_update(hashState, &mipmapMode, sizeof(mipmapMode)));
const auto wrapS = sampler.GetWrapS();
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapS, sizeof(wrapS)));
XXHASH_VERIFY(XXH64_update(hashState, &wrapS, sizeof(wrapS)));
const auto wrapT = sampler.GetWrapT();
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapT, sizeof(wrapT)));
XXHASH_VERIFY(XXH64_update(hashState, &wrapT, sizeof(wrapT)));
const auto wrapR = sampler.GetWrapR();
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapR, sizeof(wrapR)));
XXHASH_VERIFY(XXH64_update(hashState, &wrapR, sizeof(wrapR)));
const auto maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
const auto minLod = ResolveEffectiveMinLod(sampler, maxLod);
XXHASH_VERIFY(XXH64_update(m_hashState, &minLod, sizeof(minLod)));
XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod)));
XXHASH_VERIFY(XXH64_update(hashState, &minLod, sizeof(minLod)));
XXHASH_VERIFY(XXH64_update(hashState, &maxLod, sizeof(maxLod)));
const auto lodBias = sampler.GetLodBias();
XXHASH_VERIFY(XXH64_update(m_hashState, &lodBias, sizeof(lodBias)));
XXHASH_VERIFY(XXH64_update(hashState, &lodBias, sizeof(lodBias)));
// The RESOLVED value, not the GL request: samplers that only differ in an anisotropy Vulkan
// will not apply (NEAREST filtering, or requests past the device limit) must still share one
// VkSampler, while two samplers that really do differ must not collide onto the first one's.
const auto maxAnisotropy = ResolveEffectiveMaxAnisotropy(sampler, forceNearestFiltering);
XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
XXHASH_VERIFY(XXH64_update(hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
const auto compareMode = sampler.GetCompareMode();
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
XXHASH_VERIFY(XXH64_update(hashState, &compareMode, sizeof(compareMode)));
const auto compareFunc = sampler.GetSamplerCompareFunc();
XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc)));
XXHASH_VERIFY(XXH64_update(hashState, &compareFunc, sizeof(compareFunc)));
// The resolved enum AND, when it is one of the *_CUSTOM_EXT values, the sixteen bytes of the
// 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)));
XXHASH_VERIFY(XXH64_update(hashState, &borderColor.color, sizeof(borderColor.color)));
if (borderColor.isCustom) {
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor.customValue, sizeof(borderColor.customValue)));
XXHASH_VERIFY(XXH64_update(hashState, &borderColor.customValue, sizeof(borderColor.customValue)));
}
return XXH64_digest(m_hashState);
return XXH64_digest(hashState);
}
#if MOBILEGL_BUILD_DISAGGREGATED
template <class SamplerSource>
VkSampler VkSamplerManager::GetOrCreateSamplerImpl(const SamplerSource& sampler,
TextureInternalFormat format,
#else
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
#endif
Bool forceNearestFiltering, Uint32 viewLevelCount) {
// A view that exposes a single mip level has no second level to blend with, so GL's
// *_MIPMAP_* minification filters degenerate to plain filtering on the base level -
@@ -409,11 +384,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// the default-framebuffer blit shader had to work around with an explicit-LOD sample.
const Bool singleLevelView = viewLevelCount == 1;
// Resolved once and used for both the key and the create-info; see ResolvedBorderColor.
#if MOBILEGL_BUILD_DISAGGREGATED
const ResolvedBorderColor borderColor = ResolveBorderColor(sampler, format);
#else
const ResolvedBorderColor borderColor = ResolveBorderColor(sampler, texture);
#endif
const Uint64 key = BuildSamplerKey(sampler, forceNearestFiltering, singleLevelView, borderColor);
auto it = m_samplers.find(key);
if (it != m_samplers.end()) {
@@ -473,19 +444,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return vkSampler;
}
#if MOBILEGL_BUILD_DISAGGREGATED
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering, Uint32 viewLevelCount) {
return GetOrCreateSamplerImpl(sampler, texture.GetFormat(), forceNearestFiltering, viewLevelCount);
}
VkSampler VkSamplerManager::GetOrCreateSamplerFromParameters(const SamplerParameters& parameters,
TextureInternalFormat format, Bool forceNearestFiltering, Uint32 viewLevelCount) {
return GetOrCreateSamplerImpl(SamplerParametersSource{parameters}, format,
forceNearestFiltering, viewLevelCount);
}
#endif
VkFilter VkSamplerManager::ToVkFilter(SamplerFilterMode mode) {
return mode == SamplerFilterMode::Nearest ? VK_FILTER_NEAREST : VK_FILTER_LINEAR;
@@ -542,14 +505,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
#if MOBILEGL_BUILD_DISAGGREGATED
template <class SamplerSource>
VkSamplerManager::ResolvedBorderColor VkSamplerManager::ResolveBorderColor(
const SamplerSource& sampler, TextureInternalFormat format) const {
#else
VkSamplerManager::ResolvedBorderColor VkSamplerManager::ResolveBorderColor(
const MG_State::GLState::SamplerObject& sampler, const MG_State::GLState::ITextureObject& texture) const {
#endif
ResolvedBorderColor resolved{};
if (!UsesBorderColor(sampler)) {
return resolved; // FLOAT_TRANSPARENT_BLACK, never sampled
@@ -557,9 +515,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// 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.
#if !MOBILEGL_BUILD_DISAGGREGATED
const auto format = texture.GetFormat();
#endif
const auto domain = ResolveBorderColorDomain(format);
const Bool canUseCustom = m_customBorderColorSupported && m_maxCustomBorderColorSamplers > 0 &&
m_customBorderColorSamplerCount < m_maxCustomBorderColorSamplers;
@@ -40,18 +40,10 @@ public:
Bool Initialize(const InitInfo& initInfo);
void Shutdown();
// viewLevelCount is the mip-level count of the image view this sampler will be paired
// with; 0 means "unknown, do not narrow". See GetOrCreateSampler for why it matters.
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering = false,
Uint32 viewLevelCount = 0);
#if MOBILEGL_BUILD_DISAGGREGATED
// The applier owns these values: effective bound/builtin sampler parameters and
// the sampled texture view's format/range. No frontend object is constructed.
VkSampler GetOrCreateSamplerFromParameters(const SamplerParameters& parameters,
TextureInternalFormat format, Bool forceNearestFiltering = false, Uint32 viewLevelCount = 0);
#endif
// Frame boundary hook: ages the sampler cache and destroys samplers not used
// for many frames. The key hashes continuous float state (lodBias, LOD clamps,
// anisotropy), so an app animating those would otherwise mint an unbounded
@@ -64,6 +56,12 @@ public:
// fence wait. Self-gated: one counter bump and compare except on sweep
// boundaries.
void OnFrameBoundary();
// For a caller that memoises a returned VkSampler (UniformManager's wire sampler memo): the
// frame-boundary count a GetOrCreate call stamps, and a counter that moves whenever any cached
// sampler is destroyed. A memo may reuse a handle only while the destroy epoch is unchanged,
// and must still call GetOrCreate once per frame boundary so the entry stays stamped as used.
Uint64 FrameBoundaryCount() const { return m_frameBoundaryCounter; }
Uint64 DestroyEpoch() const { return m_destroyEpoch; }
// 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`
@@ -92,37 +90,24 @@ private:
Bool usesCustomBorderColor = false;
};
#if MOBILEGL_BUILD_DISAGGREGATED
template <class SamplerSource>
VkSampler GetOrCreateSamplerImpl(const SamplerSource& sampler, TextureInternalFormat format,
Bool forceNearestFiltering, Uint32 viewLevelCount);
template <class SamplerSource>
Uint64 BuildSamplerKey(const SamplerSource& sampler, Bool forceNearestFiltering,
#else
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler, Bool forceNearestFiltering,
#endif
Bool singleLevelView, const ResolvedBorderColor& borderColor) const;
static VkFilter ToVkFilter(SamplerFilterMode mode);
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
static VkCompareOp ToVkCompareOp(SamplerCompareFunc func);
#if MOBILEGL_BUILD_DISAGGREGATED
template <class SamplerSource>
ResolvedBorderColor ResolveBorderColor(const SamplerSource& sampler, TextureInternalFormat format) const;
#else
ResolvedBorderColor ResolveBorderColor(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture) const;
#endif
// 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.
// GL happily carries GL_TEXTURE_MAX_ANISOTROPY on a NEAREST sampler (Blaze3D's blocks do exactly
// that) while Vulkan forbids anisotropyEnable there, so the GL value must never be forwarded raw.
#if MOBILEGL_BUILD_DISAGGREGATED
template <class SamplerSource>
Float ResolveEffectiveMaxAnisotropy(const SamplerSource& sampler,
#else
Float ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler,
#endif
Bool forceNearestFiltering) const;
VkDevice m_device = VK_NULL_HANDLE;
@@ -138,6 +123,8 @@ private:
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameBoundaryCounter = 0;
// Moves on every vkDestroySampler of a cached entry (sweep and Shutdown).
Uint64 m_destroyEpoch = 0;
static inline thread_local XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -19,20 +19,16 @@
#include <unordered_set>
#include <Config.h>
#if MOBILEGL_BUILD_DISAGGREGATED
// The handle-keyed arm's key type. Header-only POD, disagg-only, exactly the shape
// MagmaPipeArms.h already gives this directory.
#include <MG_Pipe/MGPipeHandles.h>
#endif
namespace MobileGL::MG_State::GLState {
class ITextureObject;
}
#if MOBILEGL_BUILD_DISAGGREGATED
namespace MobileGL::MG_Pipe {
struct MGPipeResourceRecord;
}
#endif
namespace MobileGL::MG_Backend::DirectVulkan {
enum class SamplerNumericDomain : Uint8;
@@ -112,54 +108,36 @@ inline Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAt
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 {
public:
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
// manager keys its per-draw fast path on this so an attachment's image recreation
// invalidates the cached render pass (dirty-flag tracking; portable to Vulkan 1.1).
Uint64 GetTextureImageEpoch() const { return m_textureImageEpoch; }
// P15 (wire deferred clears): while one of these lives, SyncTextureResourceByHandle leaves an
// image's pending clears pending - its caller is resolving the image as an ATTACHMENT, and
// either folds the clears into the pass it begins (LOAD_OP_CLEAR) or replaces them with a
// newer clear. Every other caller (sampling, blits, copies, readbacks, uploads, mip
// generation) materializes them first, so a use that forgets the hold is merely slower.
class PendingClearHold {
public:
explicit PendingClearHold(VkTextureManager& manager) : m_manager(manager) { ++m_manager.m_pendingClearHold; }
~PendingClearHold() { --m_manager.m_pendingClearHold; }
PendingClearHold(const PendingClearHold&) = delete;
PendingClearHold& operator=(const PendingClearHold&) = delete;
private:
VkTextureManager& m_manager;
};
// A whole-subresource clear that was recorded but has not run yet (VulkanRenderer::DeferWireClear).
struct WirePendingClear {
Uint32 level = 0, layer = 0, layers = 1;
VkImageAspectFlags aspects = 0;
VkClearValue value{};
};
// Bumped whenever any tracked texture resource is erased; cached
// TextureResource pointers are valid only while this is unchanged.
Uint64 GetResourceEraseEpoch() const { return m_resourceEraseEpoch; }
struct TextureIdentity {
MG_State::GLState::ITextureObject* texture = nullptr;
Uint64 lifetimeId = 0;
Bool operator==(const TextureIdentity& other) const {
return texture == other.texture && lifetimeId == other.lifetimeId;
}
};
struct TextureIdentityHash {
SizeT operator()(const TextureIdentity& key) const {
SizeT hash = std::hash<MG_State::GLState::ITextureObject*>{}(key.texture);
hash ^= std::hash<Uint64>{}(key.lifetimeId) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
struct InitInfo {
VkDevice device = VK_NULL_HANDLE;
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
@@ -355,7 +333,6 @@ public:
// Serial at the last successful sync. 0 also means "this resource has only ever been
// synced by the frontend arm" - a wire record's Serial starts at 0 on a create and moves
// on every mutation, so the first handle sync of a fresh image always re-checks.
#if MOBILEGL_BUILD_DISAGGREGATED
Uint64 syncedWireSerial = 0;
// SHARED IMAGES: level 0 is a server shared image (an imported AHardwareBuffer), not a
// VMA allocation. The resource owns the image and its dedicated imported memory, and
@@ -381,7 +358,9 @@ public:
SharedPtr<const void> yuvOwner;
SharedPtr<void> yuvSource;
Uint64 yuvConvertedFrame = 0;
#endif
// P15: clears of this image still to run (WirePendingClear). Part of the image, so a
// respecify (which moves a new image in, below) drops them with the old one.
Vector<WirePendingClear> pendingClears;
TextureResource() = default;
TextureResource(const TextureResource&) = delete;
@@ -415,7 +394,6 @@ public:
std::swap(this->syncedContentVersion, that.syncedContentVersion);
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
std::swap(this->syncedShapeVersion, that.syncedShapeVersion);
#if MOBILEGL_BUILD_DISAGGREGATED
std::swap(this->syncedWireSerial, that.syncedWireSerial);
std::swap(this->importedMemory, that.importedMemory);
std::swap(this->sharedImageId, that.sharedImageId);
@@ -428,7 +406,7 @@ public:
std::swap(this->yuvOwner, that.yuvOwner);
std::swap(this->yuvSource, that.yuvSource);
std::swap(this->yuvConvertedFrame, that.yuvConvertedFrame);
#endif
std::swap(this->pendingClears, that.pendingClears);
}
void Reset() {
@@ -466,7 +444,6 @@ public:
if (image != VK_NULL_HANDLE && allocation != nullptr) {
vmaDestroyImage(s_allocator.Get(), image, allocation);
}
#if MOBILEGL_BUILD_DISAGGREGATED
if (importedMemory != VK_NULL_HANDLE) {
if (image != VK_NULL_HANDLE) vkDestroyImage(s_device.Get(), image, nullptr);
vkFreeMemory(s_device.Get(), importedMemory, nullptr);
@@ -482,7 +459,6 @@ public:
yuvOwner.reset();
yuvSource.reset();
yuvConvertedFrame = 0;
#endif
fullView = VK_NULL_HANDLE;
sampledView = VK_NULL_HANDLE;
perMipViews.clear();
@@ -510,9 +486,7 @@ public:
syncedContentVersion = 0;
syncedMipLevelCount = 0;
syncedShapeVersion = 0;
#if MOBILEGL_BUILD_DISAGGREGATED
syncedWireSerial = 0;
#endif
}
~TextureResource() {
@@ -539,28 +513,15 @@ public:
// preserve-on-recreate copy are the existing callers. No-op when the
// batch is empty.
void FlushPendingUploads();
#if MOBILEGL_BUILD_DISAGGREGATED
// Non-blocking idle proof for wire-object retirement. Independent texture
// submissions are not represented by VulkanRenderer's submit counter.
Bool WireUploadsAreIdle();
#endif
// Drains every frame slot's deferred image/view releases. Only valid when
// the caller has proven every queue submission complete; used by the
// present-less frame-boundary drain.
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
@@ -579,32 +540,6 @@ public:
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(
MG_State::GLState::ITextureObject& texture);
#if MOBILEGL_BUILD_DISAGGREGATED
// P5f (fm): THE HANDLE-KEYED ARM of SyncTextureAndGetDescriptor. Under an active transport
// the apply thread may not name the client's ITextureObject (rule E), so the clear / blit /
// readback / mipmap verbs resolve their attachment textures from the wire handle the
@@ -615,8 +550,8 @@ public:
// loudly, which is the honest answer a missing record deserves.
//
// The resource lives in m_wireTextureResources keyed by StagedTextureStore::KeyForHandle -
// a NAMESPACE THE FRONTEND-KEYED m_textureResources NEVER MEETS, so a recycled {slot, gen}
// can never inherit its predecessor's image. Texture views resolve the storage owner from
// keyed by the handle's {slot, gen}, so a recycled slot can never inherit its
// predecessor's image. Texture views resolve the storage owner from
// Desc.ViewOf and the window from ViewCso; buffer-backed textures remain P7's buffer arm.
Bool ReadUnbackedWireLevel(MG_Pipe::MGPipeHandle handle, TextureUploadTarget target, Uint32 level,
const IntVec3& extent, VkFormat& format, Vector<Uint8>& bytes);
@@ -679,30 +614,6 @@ public:
const auto it = m_wireTextureResources.find(key);
return it != m_wireTextureResources.end() && it->second.image != VK_NULL_HANDLE ? &it->second : nullptr;
}
#endif
VkImageView GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
VkImageView GetOrCreateAttachmentViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
Uint32 baseArrayLayer, Uint32 layerCount,
VkImageViewType viewType);
VkImageView GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
VkImageView GetOrCreateSampledImageView(MG_State::GLState::ITextureObject& texture, VkFormat format);
VkImageView GetOrCreateStorageImageView(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
VkFormat format, Bool layered, Int32 layer);
void UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout);
void UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject* texture,
Uint32 writtenMipLevel,
VkImageLayout newLayout);
Bool TransitionTextureForSampling(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
// generation advances every time the frame command buffer (re)begins
// recording; a resource whose stamp does not match was not referenced by
@@ -712,8 +623,6 @@ public:
void StampResourceRecordingUse(TextureResource& resource) const {
resource.lastRecordingGeneration = m_recordingGeneration;
}
// Map-lookup variant for callers that only hold the GL texture object.
void StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture);
Bool WasTouchedThisRecording(const TextureResource& resource) const {
return resource.lastRecordingGeneration == m_recordingGeneration;
}
@@ -722,42 +631,6 @@ public:
resource.lastRecordingGeneration = m_recordingGeneration;
resource.lastRecordingWriteGeneration = m_recordingGeneration;
}
void StampTextureRecordingWrite(MG_State::GLState::ITextureObject* texture);
// Upload-ordering queries (VulkanRenderer::OrderPendingUploadAfterRecording). None of them
// syncs, so nothing pending is uploaded by asking. All answer for the STORAGE texture.
//
// Uploads are recorded into a batch submitted AHEAD of the frame's recording
// (FlushPendingUploads), so an upload into an image the open recording already WROTE lands
// before that write, which then overwrites the newer texels. (An image the recording only
// read keeps the old trade: its earlier reads see the newer texels.)
Bool HasLiveImage(MG_State::GLState::ITextureObject& textureOrView) const;
// The next sync would record client texels into the image that is live now: same shape (a
// shape change mints a new image instead) and content moved since the last sync.
Bool HasPendingTexelUpload(MG_State::GLState::ITextureObject& textureOrView) const;
Bool WasTextureWrittenThisRecording(MG_State::GLState::ITextureObject& textureOrView) const;
// The live resource, or null.
TextureResource* FindTextureResource(MG_State::GLState::ITextureObject& textureOrView);
// Records that this texture is bound to a GL image unit, so its image must carry
// VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and
// therefore before the render pass is committed: an image that has to be upgraded is
// recreated, which is illegal inside a render pass. Sticky for the texture's lifetime -
// GL lets an image binding come and go, and re-creating the image every time it does
// would cost far more than the compression it wins back.
void MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture);
// True when this texture is marked but its live image predates the mark, i.e. the next sync
// will recreate it with STORAGE usage and copy the old contents forward. Callers use this to
// submit their pending recording first, so that copy cannot read pre-flush content.
Bool NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const;
// The same ordering question for the other recreate-and-preserve trigger: true when this
// texture's live image carries a shorter mip chain than a full one, so defining the missing
// levels recreates it and copies the old contents forward.
Bool NeedsMipChainGrowth(MG_State::GLState::ITextureObject& texture) const;
// Non-mutating probe for the per-draw storage-image fast path: true when preparing this
// texture as a storage image may need work that is illegal inside a render pass (resource
// creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
// true - a false positive merely ends the render pass, a false negative would skip a barrier.
Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const;
// `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.
@@ -765,8 +638,6 @@ public:
GLenum depthStencilTextureMode = GL_DEPTH_COMPONENT);
static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain);
static Bool AreSampledImageViewFormatsCompatible(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
@@ -788,63 +659,16 @@ public:
SizeT CollectGarbage();
// Per-draw sync memo. Within a single SetupDraw the same sampled texture is
// resolved ~3x (SetupDraw's layout-probe loop, its post-transition loop, and
// again inside ResolveSamplerDescriptor). No GL texture mutation can happen
// mid-SetupDraw, and layout is tracked on the TextureResource independently of
// SyncTexture, so after the first successful sync of a texture in a draw the
// heavy SyncTexture work (mip-completeness/resource/view resync + dirty scan)
// is pure redundancy. BeginDrawSyncScope opens a window in which repeat
// SyncTextureAndGetDescriptor calls short-circuit to the already-synced
// resource; EndDrawSyncScope closes it. Use the RAII DrawSyncScope guard.
void BeginDrawSyncScope();
void EndDrawSyncScope();
// RAII guard that opens/closes a per-draw sync memo window (see above).
class DrawSyncScope {
public:
explicit DrawSyncScope(VkTextureManager& manager) : m_manager(manager) { m_manager.BeginDrawSyncScope(); }
~DrawSyncScope() { m_manager.EndDrawSyncScope(); }
DrawSyncScope(const DrawSyncScope&) = delete;
DrawSyncScope& operator=(const DrawSyncScope&) = delete;
private:
VkTextureManager& m_manager;
};
private:
// Bumped in SyncTextureResource right after vmaCreateImage(texture). See GetTextureImageEpoch().
Uint64 m_textureImageEpoch = 1;
Uint32 m_pendingClearHold = 0; // PendingClearHold's depth
// See AdvanceRecordingGeneration. Starts above every resource's default
// stamp of 0 so a fresh resource counts as untouched.
Uint64 m_recordingGeneration = 1;
Bool SyncTexture(MG_State::GLState::ITextureObject &texture,
TextureResource &outResource);
Bool SyncTextureResource(const MG_State::GLState::ITextureObject &texture,
TextureUploadTarget uploadTarget,
const IntVec3 &texelSize, SizeT byteSize, Uint32 mipLevels,
TextureResource &resource);
Bool SyncTextureViews(const MG_State::GLState::ITextureObject& texture, TextureResource& resource);
VkImageView CreateImageView(VkImage image, VkFormat format, VkImageAspectFlags aspect,
VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount,
Uint32 baseArrayLayer,
Uint32 layerCount,
const VkComponentMapping* components = nullptr,
VkImageUsageFlags viewUsage = 0) const;
Bool UploadDirtyMipLevels(MG_State::GLState::TextureObjectMipmap &mipmapTexture,
TextureUploadTarget uploadTarget,
TextureResource &outResource);
static Bool CheckMipmapCompleteness(const MG_State::GLState::ITextureObject& texture,
TextureUploadTarget& outTarget,
IntVec3& outTexelSize,
SizeT& outByteSize,
Uint32& outMipLevelCount);
static Uint32 GetUploadMipLevelCount(const MG_State::GLState::TextureObjectMipmap& texture, TextureUploadTarget target);
static void ResolveViewMipRange(const MG_State::GLState::ITextureObject& texture, Uint32 mipLevels,
Uint32& outBaseMipLevel, Uint32& outLevelCount);
static VkImageAspectFlags GetAspectMaskForFormat(VkFormat format);
void DeferResourceRelease(TextureResource&& resource);
void DeferViewRelease(VkImageView view);
void CollectDeferredReleases(Uint32 frameIndex);
void DestroyDeferredReleases();
// Frees the fence/command buffer/staging buffer of every in-flight texture
@@ -852,11 +676,7 @@ private:
// the single queue, so the scan stops at the first still-pending entry).
// waitAll blocks on every entry - Shutdown's drain.
void ReclaimCompletedUploads(Bool waitAll = false);
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
void EraseTrackedTexture(const TextureIdentity& identity);
void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture);
SizeT PruneDeadTextures();
#if MOBILEGL_BUILD_DISAGGREGATED
// The two halves of SyncTextureResourceByHandle: shape from the resource record's
// descriptor (create / recreate / compatibility), then the pending-upload walk against the
// server's staged store. UploadPendingWireLevels mutates the applier's record (consumed
@@ -879,7 +699,6 @@ private:
Vector<Uint64> m_sharedImageUses;
// Handles are per-kind; a renderbuffer and texture may have identical slot/gen.
std::unordered_map<Uint64, TextureResource> m_wireRenderbufferResources;
#endif
VkDevice m_device = VK_NULL_HANDLE;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
@@ -896,37 +715,20 @@ private:
// Frame-boundary GC gate: counts BeginFrame calls, not draws, so texture churn
// through non-draw paths (FBO clears, readbacks) still reaches the prune.
Uint32 m_gcFrameCounter = 0;
// Active only between BeginDrawSyncScope/EndDrawSyncScope; identities of
// textures already fully synced in the current draw (small N -> flat scan).
Bool m_drawSyncScopeActive = false;
// SyncWireTextureShape defining the RGBA8 storage a YUV shared image is converted into.
Bool m_syncingYuvTextureStorage = false;
// Per-draw sync memo: the identity plus the resolved resource pointer. The pointer is stable
// across rehash in the node-based m_textureResources and stays valid for the draw (a texture
// synced this draw is alive and is not erased mid-draw), so a repeat sync of the same texture
// returns the resource without re-hashing the identity into m_textureResources.
struct DrawSyncedTexture {
TextureIdentity identity;
TextureResource* resource = nullptr;
};
Vector<DrawSyncedTexture> m_drawSyncedThisDraw;
// Cross-draw sampled-texture memo: the same few textures (atlas, lightmap)
// are resolved on every draw, so cache their resource pointers and skip the
// alive/resource map lookups. Node-based std::unordered_map keeps the
// pointees stable across inserts; erases bump m_resourceEraseEpoch, which
// every memo entry must match. SyncTexture still runs on memo hits, so
// content/param freshness is unaffected. A dead-then-reused texture address
// cannot false-hit: the new object carries a new lifetime id.
struct SyncedTextureMemoEntry {
const MG_State::GLState::ITextureObject* texture = nullptr;
Uint64 lifetimeId = 0;
Uint64 m_resourceEraseEpoch = 1;
// P14: a direct-mapped front for the two wire resource maps, keyed on the handle key
// ({slot, gen}, never reused) and believed only at the erase epoch it was taken at - an
// unordered_map node never moves until it is erased, and every erase moves the epoch.
struct WireResourceLookup {
Uint64 key = 0;
Uint64 eraseEpoch = 0;
TextureResource* resource = nullptr;
};
static constexpr Uint32 kSyncedTextureMemoSize = 8;
SyncedTextureMemoEntry m_syncedTextureMemo[kSyncedTextureMemoSize];
Uint32 m_syncedTextureMemoNext = 0;
Uint64 m_resourceEraseEpoch = 1;
static constexpr SizeT kWireResourceLookupEntries = 256;
Array<WireResourceLookup, kWireResourceLookupEntries> m_wireTextureLookup{};
Array<WireResourceLookup, kWireResourceLookupEntries> m_wireRenderbufferLookup{};
// Formats whose mutable-image probe failed on this device; their images are created
// without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch.
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
@@ -934,23 +736,6 @@ private:
// exactly like the mutable-format verdict above, so it is answered at image creation and
// remembered rather than probed once globally.
std::unordered_set<VkFormat> m_2dArrayCompatibleUnsupported;
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
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
// re-query vkGetPhysicalDeviceImageFormatProperties.
std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,5 @@
// Included by VulkanRenderer.cpp inside the backend namespace, after WireFramebuffer.inc.
// The native blit owns no frontend object and never invokes the GL shader compiler.
#if MOBILEGL_BUILD_DISAGGREGATED
#include "WireColorBlitSpirv.h"
struct VulkanRenderer::WireColorBlitResources {
@@ -15,17 +14,80 @@
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
VkSampler nearest = VK_NULL_HANDLE, linear = VK_NULL_HANDLE;
Vector<Target> targets;
// P15 S0: one descriptor pool per frame slot for the blit's set, reset when the slot's
// previous recording is known complete (ClearWireDrawPassCache). A pool created and
// destroyed per blit was ~0.16 ms a blit on Adreno 750. A full pool falls back to that.
static constexpr Uint32 kFramePoolSets = 64;
Vector<VkDescriptorPool> framePools;
// P15: the blit's two views and its framebuffer, kept across frames. MC blits its main
// target to the window every frame, and creating and retiring two VkImageViews and a
// VkFramebuffer per blit was ~0.03 ms/frame on the apply thread. The key carries the
// image epochs (any texture image recreated or resource erased invalidates every entry,
// the same rule as the draw-pass cache) and the swapchain image for the default
// framebuffer; ClearAllWireDrawPassCaches drops the lot at the idle points.
struct CachedTarget {
VkImage srcImage = VK_NULL_HANDLE, dstImage = VK_NULL_HANDLE;
Uint32 srcLevel = 0, srcLayer = 0, dstLevel = 0, dstLayer = 0;
VkFormat srcFormat = VK_FORMAT_UNDEFINED, dstFormat = VK_FORMAT_UNDEFINED;
VkExtent2D dstExtent{};
VkRenderPass renderPass = VK_NULL_HANDLE;
Uint64 textureImageEpoch = 0, resourceEraseEpoch = 0;
VkSwapchainKHR swapchain = VK_NULL_HANDLE;
Bool dstIsDefault = false;
VkImageView srcView = VK_NULL_HANDLE, dstView = VK_NULL_HANDLE;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
};
static constexpr SizeT kCachedTargets = 8;
Vector<CachedTarget> cachedTargets;
};
// Retires cached blit targets through the wire's deferred destruction (they may still be
// named by an in-flight submission). `all` false keeps the entries whose epochs still hold.
void VulkanRenderer::RetireWireColorBlitTargets(Bool all) {
if (!m_wireColorBlitResources) return;
auto& cached = m_wireColorBlitResources->cachedTargets;
const Uint64 imageEpoch = m_textureManager ? m_textureManager->GetTextureImageEpoch() : 0;
const Uint64 eraseEpoch = m_textureManager ? m_textureManager->GetResourceEraseEpoch() : 0;
WireRetiredObjects retired{};
SizeT kept = 0;
for (SizeT i = 0; i < cached.size(); ++i) {
auto& entry = cached[i];
if (!all && entry.textureImageEpoch == imageEpoch && entry.resourceEraseEpoch == eraseEpoch) {
cached[kept++] = entry;
continue;
}
retired.framebuffers.push_back(entry.framebuffer);
retired.imageViews.push_back(entry.srcView);
retired.imageViews.push_back(entry.dstView);
}
cached.resize(kept);
if (!retired.framebuffers.empty()) RetireWireObjects(Move(retired));
}
void VulkanRenderer::ResetWireColorBlitFramePool(Uint32 frameIndex) {
auto& pools = m_wireColorBlitResources->framePools;
if (frameIndex < pools.size() && pools[frameIndex] != VK_NULL_HANDLE)
vkResetDescriptorPool(m_device, pools[frameIndex], 0);
}
void VulkanRenderer::DestroyWireColorBlitResources() {
if (!m_wireColorBlitResources) return;
// Shutdown calls this after vkDeviceWaitIdle and after all per-submit
// descriptor pools/framebuffers/views have been collected.
auto& cache = *m_wireColorBlitResources;
// Device idle here: cached blit targets no submission names any more.
for (const auto& entry : cache.cachedTargets) {
vkDestroyFramebuffer(m_device, entry.framebuffer, nullptr);
vkDestroyImageView(m_device, entry.srcView, nullptr);
vkDestroyImageView(m_device, entry.dstView, nullptr);
}
cache.cachedTargets.clear();
for (const auto& target : cache.targets) {
vkDestroyPipeline(m_device, target.pipeline, nullptr);
vkDestroyRenderPass(m_device, target.renderPass, nullptr);
}
for (const auto pool : cache.framePools)
if (pool != VK_NULL_HANDLE) vkDestroyDescriptorPool(m_device, pool, nullptr);
vkDestroySampler(m_device, cache.nearest, nullptr);
vkDestroySampler(m_device, cache.linear, nullptr);
vkDestroyPipelineLayout(m_device, cache.pipelineLayout, nullptr);
@@ -176,7 +238,7 @@
// Mip generation samples and writes the texture's own color space;
// GL_FRAMEBUFFER_SRGB only controls framebuffer operations.
if (!mipmap) destination.format = ResolveSrgbAttachmentWriteFormat(destination.format,
MGB_CTX->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb));
MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb));
VkFormatProperties destinationProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, destination.format, &destinationProperties);
if (!(destinationProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT)) return false;
@@ -315,31 +377,96 @@
VK_VERIFY(vkCreateImageView(m_device, &view, nullptr, &handle));
return handle;
};
const VkImageView sourceView = makeView(source), destinationView = makeView(destination);
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
framebufferInfo.renderPass = target->renderPass;
framebufferInfo.attachmentCount = 1;
framebufferInfo.pAttachments = &destinationView;
framebufferInfo.width = destination.extent.width;
framebufferInfo.height = destination.extent.height;
framebufferInfo.layers = 1;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &framebuffer));
// P15: the views and framebuffer come from the cross-frame cache (see CachedTarget).
RetireWireColorBlitTargets(/*all=*/false);
WireColorBlitResources::CachedTarget wanted{};
wanted.srcImage = source.image;
wanted.srcLevel = source.level;
wanted.srcLayer = source.layer;
wanted.srcFormat = source.format;
wanted.dstImage = destination.image;
wanted.dstLevel = destination.level;
wanted.dstLayer = destination.layer;
wanted.dstFormat = destination.format;
wanted.dstExtent = destination.extent;
wanted.renderPass = target->renderPass;
wanted.textureImageEpoch = m_textureManager ? m_textureManager->GetTextureImageEpoch() : 0;
wanted.resourceEraseEpoch = m_textureManager ? m_textureManager->GetResourceEraseEpoch() : 0;
wanted.dstIsDefault = destination.isDefault;
wanted.swapchain = destination.isDefault ? m_swapchainObject.GetHandle() : VK_NULL_HANDLE;
const auto sameTarget = [&](const WireColorBlitResources::CachedTarget& entry) {
return entry.srcImage == wanted.srcImage && entry.srcLevel == wanted.srcLevel &&
entry.srcLayer == wanted.srcLayer && entry.srcFormat == wanted.srcFormat &&
entry.dstImage == wanted.dstImage && entry.dstLevel == wanted.dstLevel &&
entry.dstLayer == wanted.dstLayer && entry.dstFormat == wanted.dstFormat &&
entry.dstExtent.width == wanted.dstExtent.width &&
entry.dstExtent.height == wanted.dstExtent.height && entry.renderPass == wanted.renderPass &&
entry.textureImageEpoch == wanted.textureImageEpoch &&
entry.resourceEraseEpoch == wanted.resourceEraseEpoch && entry.swapchain == wanted.swapchain &&
entry.dstIsDefault == wanted.dstIsDefault;
};
auto& cachedTargets = cache.cachedTargets;
WireColorBlitResources::CachedTarget* hit = nullptr;
for (auto& entry : cachedTargets)
if (sameTarget(entry)) { hit = &entry; break; }
if (hit == nullptr) {
if (cachedTargets.size() >= WireColorBlitResources::kCachedTargets) {
WireRetiredObjects evicted{};
evicted.framebuffers.push_back(cachedTargets.front().framebuffer);
evicted.imageViews.push_back(cachedTargets.front().srcView);
evicted.imageViews.push_back(cachedTargets.front().dstView);
RetireWireObjects(Move(evicted));
cachedTargets.erase(cachedTargets.begin());
}
wanted.srcView = makeView(source);
wanted.dstView = makeView(destination);
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
framebufferInfo.renderPass = target->renderPass;
framebufferInfo.attachmentCount = 1;
framebufferInfo.pAttachments = &wanted.dstView;
framebufferInfo.width = destination.extent.width;
framebufferInfo.height = destination.extent.height;
framebufferInfo.layers = 1;
VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &wanted.framebuffer));
cachedTargets.push_back(wanted);
hit = &cachedTargets.back();
}
const VkImageView sourceView = hit->srcView;
const VkFramebuffer framebuffer = hit->framebuffer;
// Each in-flight blit owns its descriptor set. Updating a cached set
// would rewrite the source view of an older queued blit.
VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1};
VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
pool.maxSets = 1;
pool.poolSizeCount = 1;
pool.pPoolSizes = &poolSize;
VK_VERIFY(vkCreateDescriptorPool(m_device, &pool, nullptr, &descriptorPool));
VkDescriptorSetAllocateInfo alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
alloc.descriptorPool = descriptorPool;
alloc.descriptorSetCount = 1;
alloc.pSetLayouts = &cache.descriptorLayout;
VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet));
{
// The frame slot's pool (P15 S0): its sets live until the slot's next frame begins,
// which is after this recording completed.
const Uint32 frameIndex = m_frameContext.GetCurrentFrameIndex();
if (cache.framePools.size() <= frameIndex) cache.framePools.resize(frameIndex + 1, VK_NULL_HANDLE);
if (cache.framePools[frameIndex] == VK_NULL_HANDLE) {
const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
WireColorBlitResources::kFramePoolSets};
VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
pool.maxSets = WireColorBlitResources::kFramePoolSets;
pool.poolSizeCount = 1;
pool.pPoolSizes = &poolSize;
VK_VERIFY(vkCreateDescriptorPool(m_device, &pool, nullptr, &cache.framePools[frameIndex]));
}
alloc.descriptorPool = cache.framePools[frameIndex];
if (vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet) != VK_SUCCESS) descriptorSet = VK_NULL_HANDLE;
}
if (descriptorSet == VK_NULL_HANDLE) {
const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1};
VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
pool.maxSets = 1;
pool.poolSizeCount = 1;
pool.pPoolSizes = &poolSize;
VK_VERIFY(vkCreateDescriptorPool(m_device, &pool, nullptr, &descriptorPool));
alloc.descriptorPool = descriptorPool;
VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet));
}
// GL_LINEAR filters only a STRETCHED blit; an unscaled one (mirrored or not, on any of the
// four transforms) is a copy and samples NEAREST, which is exact where linear filtering
// at a quantised coordinate is not (WireColorBlitFilter.h). The format check above still
@@ -397,12 +524,11 @@
// This native pass binds its own set/layout without going through the
// application's descriptor binder. Its next fast path must rebind.
if (m_uniformManager) m_uniformManager->OnCommandBufferBoundary();
WireRetiredObjects retired{};
retired.framebuffers.push_back(framebuffer);
retired.imageViews = {sourceView, destinationView};
retired.descriptorPools.push_back(descriptorPool);
RetireWireObjects(Move(retired));
if (descriptorPool != VK_NULL_HANDLE) {
WireRetiredObjects retired{};
retired.descriptorPools.push_back(descriptorPool);
RetireWireObjects(Move(retired));
}
if (destination.isDefault) m_swapchainObject.SetImageContentDefined(destination.swapchainImageIndex, true);
return true;
}
#endif
@@ -35,7 +35,6 @@
// GL's own (logical) rectangles for identity and all three rotations. BlitWireColorImage declines
// any other transform before it chooses a sampler; for one, this answers with the filter's own
// mapping, as before this package.
#if MOBILEGL_BUILD_DISAGGREGATED
namespace MobileGL::MG_Backend::DirectVulkan {
// A glBlitFramebuffer rectangle as GL names it: two corners, either order.
struct WireBlitRect {
@@ -63,4 +62,3 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return VK_FILTER_LINEAR;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
#endif
@@ -9,7 +9,6 @@
#pragma once
#include <Config.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <Includes.h>
#include <MG_Util/Debug/Log.h>
@@ -169,4 +168,3 @@ private:
} // namespace MobileGL::MG_Backend::DirectVulkan
#endif // MOBILEGL_BUILD_DISAGGREGATED
@@ -15,7 +15,6 @@
// frontend objects and a GL compiler on the server's apply thread. The baked program below is
// the same shader with the same arithmetic, driven by a push constant and its own descriptor
// set, which is what lets the server generate a depth mip while owning nothing of the client.
#if MOBILEGL_BUILD_DISAGGREGATED
#include "WireDepthMipmapSpirv.h"
struct VulkanRenderer::WireDepthMipmapResources {
@@ -326,4 +325,3 @@
RetireWireObjects(Move(retired));
return true;
}
#endif
@@ -21,7 +21,6 @@
#include <cstring>
#include <vulkan/vulkan_core.h>
#if MOBILEGL_BUILD_DISAGGREGATED
namespace MobileGL::MG_Backend::DirectVulkan {
#include "WireMultisampleResolveSpirv.h"
@@ -993,4 +992,3 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return measurement;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
#endif
@@ -27,7 +27,6 @@
//
// Disaggregated builds only: the monolith never records either arm, and the pull build's image
// must not change (P7 gate G1).
#if MOBILEGL_BUILD_DISAGGREGATED
namespace MobileGL::MG_Backend::DirectVulkan {
// Every aspect a wire depth/stencil format has.
VkImageAspectFlags WireDepthStencilFormatAspects(VkFormat format);
@@ -118,4 +117,3 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 graphicsQueueFamilyIndex, const Vector<VkExtensionProperties>& deviceExtensions,
Bool& renderPassArmAvailable, Bool& shaderStencilExport);
} // namespace MobileGL::MG_Backend::DirectVulkan
#endif
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,5 @@
// P5f fm: included by VulkanRenderer.cpp inside the backend namespace. All inputs here
// belong to the applier; no frontend object, lifetime-id lookup or allocator is used.
#if MOBILEGL_BUILD_DISAGGREGATED
// P7 wave 0. THE MESSAGE IS UNCHANGED; where it goes is not. This funnel is how eleven of
// the fifteen P7-marked refusals die (the other four are WireDescriptorFatal's), and until now
// every one of them raised its own abort: no SessionFault frame for the peer, no
@@ -134,6 +133,9 @@
out.swapchainImageIndex = isWriteTarget ? DefaultFramebufferWriteIndex(true)
: DefaultFramebufferReadIndex(true);
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) {
// Every use of the acquired image is reached through here, ahead of its commands:
// the work recorded before it goes out without the acquire wait.
if (out.swapchainImageIndex == m_imageIndexAcquired) SubmitAheadOfAcquiredImage();
out.image = m_swapchainObject.GetImage(out.swapchainImageIndex);
out.format = m_swapchainObject.GetSurfaceFormat().format;
out.layout = m_swapchainObject.GetImageLayout(out.swapchainImageIndex);
@@ -173,6 +175,7 @@
std::max(resource->extent.height >> level, 1u)};
out.layout = resource->layout;
out.trackedLayout = &resource->layout;
out.resource = resource;
out.samples = resource->sampleCount;
// P7 wave 2-B2: the shader multisample resolve SAMPLES this image, which is a question
// about the image and not about the format - VkTextureManager asks for SAMPLED on every
@@ -194,13 +197,19 @@
return out;
}
void VulkanRenderer::TransitionWireImage(WireImage& image, VkImageLayout layout) {
void VulkanRenderer::TransitionWireImage(WireImage& image, VkImageLayout layout, Bool coalesceWithLastFullBarrier) {
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
if (VkRenderPassManager::GetActiveRenderPass()) VkRenderPassManager::EndRenderPass(frame.commandBuffer);
VkPipelineStageFlags stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags access = 0;
GetImageTransitionSourceState(image.layout, stage, access);
// A swapchain image leaves PRESENT_SRC_KHR (or UNDEFINED, new) only after it was acquired:
// the transition is ordered after the presentation engine's release through the stages
// the acquire wait blocks, which an empty (TOP_OF_PIPE) source scope would not chain to.
if (image.isDefault && image.aspect == VK_IMAGE_ASPECT_COLOR_BIT &&
(image.layout == VK_IMAGE_LAYOUT_PRESENT_SRC_KHR || image.layout == VK_IMAGE_LAYOUT_UNDEFINED))
stage |= FrameContext::kAcquireWaitStages;
// One tracked layout per image: move the complete mip chain, never just the surface.
VkImageAspectFlags barrierAspect = image.aspect;
// Every packed format, D16_UNORM_S8_UINT included (g5-msrbo review round): the layout is
@@ -208,15 +217,31 @@
// tracked layout - and CopyWireDepthStencilBlit moves the two aspects in two legs.
if (WireFormatIsPackedDepthStencil(image.format))
barrierAspect = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
// P15: A DRAW PASS'S ATTACHMENTS SHARE ONE FULL BARRIER, and the sampled-image
// barrier shares it too. The memory barrier below is global (ALL_COMMANDS, every write
// made visible), the same one ResolveWireImageDescriptor records, so a second one with no
// image written since the last (WireImageWriteEpoch unchanged) orders nothing. Only the
// new-pass caller (PrepareWireDrawPass) opts in: its attachments are written inside the
// pass it is about to begin, and that pass's end moves the epoch. A transfer caller
// writes right after this call with the epoch already moved, so it must neither skip nor
// note - noting there would let a later barrier be skipped past its write.
// FCL Magma: 27.5 barriers and 12.6 render passes per frame against dev's 3.4 and 4.1,
// most of them a second barrier per attachment and a pass closed and reopened at the
// first sampled texture after every pass begin.
if (image.layout == layout) {
// GENERAL->GENERAL is not a layout transition, but the next load,
// sample or blit still depends on earlier attachment/shader writes.
// The old per-verb queue idle must not be its only ordering edge.
VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memory, 0, nullptr, 0, nullptr);
const Bool covered = coalesceWithLastFullBarrier && m_uniformManager &&
WireImageWriteEpoch() == m_uniformManager->WireImageBarrierEpoch();
if (!covered) {
VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memory, 0, nullptr, 0, nullptr);
if (coalesceWithLastFullBarrier && m_uniformManager) m_uniformManager->NoteWireImageBarrier();
}
}
if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer, image.image, image.layout, layout,
stage, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, access,
@@ -230,8 +255,341 @@
}
}
// ---- P15: WIRE DEFERRED CLEARS -------------------------------------------------------------
//
// Minecraft (1.21.5) clears its targets through framebuffers of their own - a depth-only one
// for the shared depth texture, a colour+depth one per target - and then draws through the
// main framebuffer. A clear that begins a LOAD_OP_CLEAR pass of its own made every such clear a
// pass that did nothing but clear, closed by the very next draw: 11.5 passes a frame against
// dev's 4.1 (whose clear manager folded the same clears into the next pass's load op), each
// one an extra full load/store of the attachments on a tiler.
//
// So a clear of an image the open pass holds is recorded inside that pass, and a whole-image
// clear of any other image WAITS ON THE IMAGE (WirePendingClear, kept on the image's own
// resource with its level and layers - never keyed by a GL name). The next draw pass that has
// it as an attachment begins with LOAD_OP_CLEAR for it (PrepareWireDrawPass /
// BeginWireDrawPassFolding); any other use reaches the image through
// SyncTextureResourceByHandle, which runs it first (MaterializeWirePendingClears). The default
// framebuffer never defers: its in-pass clear is a known Adreno problem (see
// ClearWireFramebufferInPass), and it is presented, not sampled.
// Whether `image` can carry a deferred clear: a plain single-sample 2D(-array) image of this
// session with a float colour or a depth/stencil format, clearable by a transfer command.
Bool VulkanRenderer::WireImageCanDeferClear(const WireImage& image) {
const auto* resource = image.resource;
if (resource == nullptr || image.isDefault || image.is3D || image.samples != VK_SAMPLE_COUNT_1_BIT) return false;
if (resource->sharedImageId != 0 || resource->yuvImageId != 0) return false;
if ((resource->usageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) == 0) return false;
if (image.aspect == VK_IMAGE_ASPECT_COLOR_BIT &&
(vkuFormatIsSRGB(image.format) || vkuFormatIsSINT(image.format) || vkuFormatIsUINT(image.format) ||
vkuFormatIsCompressed(image.format)))
return false;
return true;
}
void VulkanRenderer::MaterializeWirePendingClears(VkTextureManager::TextureResource& resource) {
if (resource.pendingClears.empty()) return;
if (resource.image == VK_NULL_HANDLE) {
resource.pendingClears.clear();
return;
}
auto& frame = m_frameContext.GetCurrent();
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
EndActiveRenderPassOn(frame.commandBuffer);
// After every earlier access to the image (the clears were issued after them).
if (resource.layout != VK_IMAGE_LAYOUT_GENERAL) {
if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer, resource.image, resource.layout,
VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
resource.layout == VK_IMAGE_LAYOUT_UNDEFINED ? 0 : VK_ACCESS_MEMORY_WRITE_BIT,
VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, resource.aspect, 0, resource.mipLevels))
MagmaWireFatal("pending-clear-transition");
} else {
VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memory, 0, nullptr, 0, nullptr);
}
for (const auto& pending : resource.pendingClears) {
const VkImageSubresourceRange range{pending.aspects, pending.level, 1, pending.layer, pending.layers};
if (pending.aspects & VK_IMAGE_ASPECT_COLOR_BIT)
vkCmdClearColorImage(frame.commandBuffer, resource.image, VK_IMAGE_LAYOUT_GENERAL,
&pending.value.color, 1, &range);
else
vkCmdClearDepthStencilImage(frame.commandBuffer, resource.image, VK_IMAGE_LAYOUT_GENERAL,
&pending.value.depthStencil, 1, &range);
}
resource.pendingClears.clear();
// Recorded writes: the next sampled-image or attachment barrier must cover them.
BumpWireImageWriteEpoch();
}
// Records `aspects` of `image` cleared to `value` as pending, replacing whatever an earlier
// pending clear of the same subresource said for those aspects.
void VulkanRenderer::NoteWirePendingClear(const WireImage& image, VkImageAspectFlags aspects,
const VkClearValue& value) {
auto& pending = image.resource->pendingClears;
for (SizeT i = 0; i < pending.size();) {
auto& entry = pending[i];
if (entry.level == image.level && entry.layer == image.layer && entry.layers == image.layers) {
entry.aspects &= ~aspects;
if (entry.aspects == 0) {
pending.erase(pending.begin() + static_cast<std::ptrdiff_t>(i));
continue;
}
}
++i;
}
VkTextureManager::WirePendingClear entry{};
entry.level = image.level;
entry.layer = image.layer;
entry.layers = image.layers;
entry.aspects = aspects;
entry.value = value;
pending.push_back(entry);
}
// The open wire draw pass's attachment that IS `image` (same image, level and layers), or
// VK_ATTACHMENT_UNUSED.
Uint32 VulkanRenderer::OpenPassAttachmentOf(const WireImage& image,
const Vector<WireDrawAttachmentKey>& attachments) {
for (Uint32 a = 0; a < attachments.size(); ++a) {
const auto& key = attachments[a];
if (key.image == image.image && key.level == image.level && key.layer == image.layer &&
key.layers == image.layers)
return a;
}
return VK_ATTACHMENT_UNUSED;
}
// A clear whose images the OPEN draw pass holds - through another framebuffer than the
// pass's own (the depth-only clear framebuffer) - is recorded inside that pass. Returns the
// buffers it did not take.
GLbitfield VulkanRenderer::ClearWireAttachmentsOfOpenPass(const MG_Pipe::MGPFramebufferState& fbo,
const ClearAttachmentPayload& payload, GLint drawbuffer) {
GLbitfield remaining = payload.mask & (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
if (!remaining || fbo.IsDefault || m_wirePreparationDepth != 0 || !m_wireDrawPass) return remaining;
auto& frame = m_frameContext.GetCurrent();
const auto* active = VkRenderPassManager::GetActiveRenderPass();
if (active == nullptr || active->commandBuffer != frame.commandBuffer || active->hash != m_wireDrawPass->hash)
return remaining;
const auto& key = m_wireDrawPassKey;
const RenderPassEntry* pass = m_wireDrawPass.get();
const VkCommandBuffer passBuffer = active->commandBuffer;
const Uint64 passHash = active->hash;
VkRect2D area{{0, 0}, {key.width, key.height}};
if (GLScissorTestClips()) {
const auto& box = MG_Pipe::gPipeInputs->GetScissorBox();
const Int32 x0 = std::max(box.x(), 0), y0 = std::max(box.y(), 0);
const Int32 x1 = std::min(box.x() + box.z(), static_cast<Int32>(key.width));
const Int32 y1 = std::min(box.y() + box.w(), static_cast<Int32>(key.height));
if (x0 >= x1 || y0 >= y1) return remaining; // the standalone path answers an empty box
area = {{x0, y0}, {static_cast<Uint32>(x1 - x0), static_cast<Uint32>(y1 - y0)}};
}
// The image must be exactly the pass's extent, or the pass's render area is not its area.
const auto fits = [&](const WireImage& image) {
return image.extent.width == key.width && image.extent.height == key.height;
};
VkClearAttachment clears[MG_Pipe::kMGPipeMaxColorAttachments + 1]{};
Uint32 count = 0;
GLbitfield handled = 0;
VkTextureManager::PendingClearHold hold(*m_textureManager);
if (remaining & GL_COLOR_BUFFER_BIT) {
Bool all = true;
Uint32 colorCount = 0;
VkClearAttachment colorClears[MG_Pipe::kMGPipeMaxColorAttachments]{};
for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments && all; ++i) {
if (drawbuffer >= 0 && static_cast<Uint32>(drawbuffer) != i) continue;
const Int32 slot = fbo.DrawBuffers[i];
if (slot < 0 || slot >= static_cast<Int32>(MG_Pipe::kMGPipeMaxColorAttachments)) continue;
if (fbo.Color[slot].Kind == MG_Pipe::kMGPipeSurfaceKindNone) continue;
const auto mask = MG_Pipe::gPipeInputs->GetColorMaskIndexed(i);
if (!mask.r() && !mask.g() && !mask.b() && !mask.a()) continue;
if (!mask.r() || !mask.g() || !mask.b() || !mask.a() || payload.colorEncoding != ClearColorEncoding::Float) {
all = false;
break;
}
const WireImage image = ResolveWireImage(fbo, fbo.Color[slot], VK_IMAGE_ASPECT_COLOR_BIT, true);
const Uint32 a = image.image ? OpenPassAttachmentOf(image, key.attachments) : VK_ATTACHMENT_UNUSED;
Uint32 reference = VK_ATTACHMENT_UNUSED;
for (Uint32 c = 0; a != VK_ATTACHMENT_UNUSED && c < key.colorReferences.size(); ++c)
if (key.colorReferences[c] == a) reference = c;
if (reference == VK_ATTACHMENT_UNUSED || !fits(image) || vkuFormatIsSRGB(image.format) ||
vkuFormatIsSRGB(key.attachments[a].format) || vkuFormatIsSINT(image.format) ||
vkuFormatIsUINT(image.format)) {
all = false;
break;
}
auto& clear = colorClears[colorCount++];
clear.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
clear.colorAttachment = reference;
clear.clearValue.color = MakeVkClearColorValue(payload, false);
}
if (all) {
for (Uint32 c = 0; c < colorCount; ++c) clears[count++] = colorClears[c];
handled |= GL_COLOR_BUFFER_BIT;
}
}
VkImageAspectFlags depthStencilAspects = 0;
const auto depthStencilHeld = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) {
const WireImage image = ResolveWireImage(fbo, surface, aspect, true);
return image.image && fits(image) && key.depthReference != VK_ATTACHMENT_UNUSED &&
OpenPassAttachmentOf(image, key.attachments) == key.depthReference &&
(key.attachments[key.depthReference].aspect & aspect) != 0;
};
if (remaining & GL_DEPTH_BUFFER_BIT) {
if (!MG_Pipe::gPipeInputs->GetDepthMask() || fbo.Depth.Kind == MG_Pipe::kMGPipeSurfaceKindNone) {
handled |= GL_DEPTH_BUFFER_BIT;
} else if (depthStencilHeld(fbo.Depth, VK_IMAGE_ASPECT_DEPTH_BIT)) {
depthStencilAspects |= VK_IMAGE_ASPECT_DEPTH_BIT;
handled |= GL_DEPTH_BUFFER_BIT;
}
}
if (remaining & GL_STENCIL_BUFFER_BIT) {
const Uint32 mask = MG_Pipe::gPipeInputs->GetStencilState(StencilFace::Front).WriteMask & 255u;
if (mask == 0 || fbo.Stencil.Kind == MG_Pipe::kMGPipeSurfaceKindNone) {
handled |= GL_STENCIL_BUFFER_BIT;
} else if (mask == 255u && depthStencilHeld(fbo.Stencil, VK_IMAGE_ASPECT_STENCIL_BIT)) {
depthStencilAspects |= VK_IMAGE_ASPECT_STENCIL_BIT;
handled |= GL_STENCIL_BUFFER_BIT;
}
}
if (depthStencilAspects) {
auto& clear = clears[count++];
clear.aspectMask = depthStencilAspects;
clear.clearValue.depthStencil = {payload.depth, payload.stencil};
}
// A resolve above may have submitted (a lazy upload) or run another image's deferred clear,
// closing the pass: then nothing is recorded and nothing is taken.
active = VkRenderPassManager::GetActiveRenderPass();
if (active == nullptr || active->commandBuffer != passBuffer || active->hash != passHash ||
m_wireDrawPass.get() != pass)
return remaining;
if (count > 0) {
const VkClearRect rect{area, 0, key.layers};
vkCmdClearAttachments(frame.commandBuffer, count, clears, 1, &rect);
}
return remaining & ~handled;
}
// A whole-image clear of images the open pass does not hold waits on the images (see the
// section comment). Returns the buffers it did not take.
GLbitfield VulkanRenderer::DeferWireClear(const MG_Pipe::MGPFramebufferState& fbo,
const ClearAttachmentPayload& payload, GLint drawbuffer) {
GLbitfield remaining = payload.mask & (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
if (!remaining || fbo.IsDefault || drawbuffer >= 0 || m_wirePreparationDepth != 0) return remaining;
const Bool scissor = GLScissorTestClips();
const auto wholeImage = [&](const WireImage& image) {
if (!scissor) return true;
const auto& box = MG_Pipe::gPipeInputs->GetScissorBox();
return box.x() <= 0 && box.y() <= 0 && box.x() + box.z() >= static_cast<Int32>(image.extent.width) &&
box.y() + box.w() >= static_cast<Int32>(image.extent.height);
};
const Bool passOpen = VkRenderPassManager::GetActiveRenderPass() != nullptr && m_wireDrawPass;
const auto heldByOpenPass = [&](const WireImage& image) {
return passOpen && OpenPassAttachmentOf(image, m_wireDrawPassKey.attachments) != VK_ATTACHMENT_UNUSED;
};
struct Target {
WireImage image;
VkImageAspectFlags aspects;
VkClearValue value;
Bool texture;
};
Target targets[MG_Pipe::kMGPipeMaxColorAttachments + 2]{};
Uint32 targetCount = 0;
GLbitfield handled = 0;
VkTextureManager::PendingClearHold hold(*m_textureManager);
if ((remaining & GL_COLOR_BUFFER_BIT) && payload.colorEncoding == ClearColorEncoding::Float) {
Bool all = true;
const Uint32 first = targetCount;
for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments && all; ++i) {
const Int32 slot = fbo.DrawBuffers[i];
if (slot < 0 || slot >= static_cast<Int32>(MG_Pipe::kMGPipeMaxColorAttachments)) continue;
const auto& surface = fbo.Color[slot];
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone) continue;
const auto mask = MG_Pipe::gPipeInputs->GetColorMaskIndexed(i);
if (!mask.r() && !mask.g() && !mask.b() && !mask.a()) continue;
if (!mask.r() || !mask.g() || !mask.b() || !mask.a()) {
all = false;
break;
}
const WireImage image = ResolveWireImage(fbo, surface, VK_IMAGE_ASPECT_COLOR_BIT, true);
if (!image.image || !WireImageCanDeferClear(image) || !wholeImage(image) || heldByOpenPass(image)) {
all = false;
break;
}
auto& target = targets[targetCount++];
target.image = image;
target.aspects = VK_IMAGE_ASPECT_COLOR_BIT;
target.value.color = MakeVkClearColorValue(payload, false);
target.texture = surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture;
}
if (all) handled |= GL_COLOR_BUFFER_BIT;
else targetCount = first;
}
const auto depthStencil = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) -> Bool {
const WireImage image = ResolveWireImage(fbo, surface, aspect, true);
if (!image.image || !WireImageCanDeferClear(image) || !wholeImage(image) || heldByOpenPass(image)) return false;
for (Uint32 t = 0; t < targetCount; ++t) {
auto& target = targets[t];
if (target.image.image == image.image && target.image.level == image.level &&
target.image.layer == image.layer && target.image.layers == image.layers &&
!(target.aspects & VK_IMAGE_ASPECT_COLOR_BIT)) {
target.aspects |= aspect; // a combined depth/stencil image: one entry, both aspects
return true;
}
}
auto& target = targets[targetCount++];
target.image = image;
target.aspects = aspect;
target.value.depthStencil = {payload.depth, payload.stencil};
target.texture = surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture;
return true;
};
if (remaining & GL_DEPTH_BUFFER_BIT) {
if (!MG_Pipe::gPipeInputs->GetDepthMask() || fbo.Depth.Kind == MG_Pipe::kMGPipeSurfaceKindNone)
handled |= GL_DEPTH_BUFFER_BIT;
else if (depthStencil(fbo.Depth, VK_IMAGE_ASPECT_DEPTH_BIT))
handled |= GL_DEPTH_BUFFER_BIT;
}
if (remaining & GL_STENCIL_BUFFER_BIT) {
const Uint32 mask = MG_Pipe::gPipeInputs->GetStencilState(StencilFace::Front).WriteMask & 255u;
if (mask == 0 || fbo.Stencil.Kind == MG_Pipe::kMGPipeSurfaceKindNone)
handled |= GL_STENCIL_BUFFER_BIT;
else if (mask == 255u && depthStencil(fbo.Stencil, VK_IMAGE_ASPECT_STENCIL_BIT))
handled |= GL_STENCIL_BUFFER_BIT;
}
m_textureManager->FlushPendingUploads();
for (Uint32 t = 0; t < targetCount; ++t) {
const auto& target = targets[t];
NoteWirePendingClear(target.image, target.aspects, target.value);
// A GPU write as far as the CPU shadow is concerned, as a recorded clear is.
if (target.texture)
m_textureManager->MarkWireTextureGpuWritten(target.image.storage, target.image.level,
target.image.layer, target.image.layers);
}
return remaining & ~handled;
}
void VulkanRenderer::ClearWireFramebuffer(const MG_Pipe::MGPFramebufferState& fbo,
const ClearAttachmentPayload& payload, GLint drawbuffer) {
const ClearAttachmentPayload& clearRequest, GLint drawbuffer) {
if (MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) return;
// A clear of the window: the frame's work so far goes out ahead of the acquire wait, before
// any of the clear's paths (in-pass, deferred, standalone) records or resolves anything.
if (fbo.IsDefault) SubmitAheadOfAcquiredImage();
// P15 S0: what the draw pass can take is cleared inside it (ClearWireFramebufferInPass,
// WireDraw.inc); only the buffers it hands back take the standalone pass below.
// P15: in the open pass when it is this framebuffer's (the in-pass clear) or merely holds
// its images (another framebuffer's), deferred on the images when it covers them whole,
// and only then a pass of its own (the in-pass clear again, which begins one) or the
// standalone path.
ClearAttachmentPayload payload = clearRequest;
if (m_wirePreparationDepth == 0 && WirePassMemoHolds(fbo, m_frameContext.GetCurrent()))
payload.mask = ClearWireFramebufferInPass(fbo, payload, drawbuffer);
if (payload.mask) payload.mask = ClearWireAttachmentsOfOpenPass(fbo, payload, drawbuffer);
if (payload.mask) payload.mask = DeferWireClear(fbo, payload, drawbuffer);
if (payload.mask) payload.mask = ClearWireFramebufferInPass(fbo, payload, drawbuffer);
if (!payload.mask) return;
RetireWireDrawPass();
// A stream that neither presents nor finishes (an off-screen loop) never submits, so its
// recording and the per-clear objects retired below would grow until host memory runs
@@ -240,12 +598,12 @@
if (m_wireRetiredObjects.size() >= kRetiredObjectsBeforeSubmit && m_wirePreparationDepth == 0 &&
HasPendingRecordedWork())
(void)FlushPendingCommands();
if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) return;
if (MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) return;
const auto clearSurface = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect,
Uint32 colorIndex) {
if (!fbo.IsDefault && surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone) return;
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) {
const auto mask = MGB_CTX->GetColorMaskIndexed(colorIndex);
const auto mask = MG_Pipe::gPipeInputs->GetColorMaskIndexed(colorIndex);
if (!mask.r() && !mask.g() && !mask.b() && !mask.a()) return;
if (!mask.r() || !mask.g() || !mask.b() || !mask.a()) {
MGLOG_W_ONCE("DirectVulkan: glClear with a partial color mask is not supported");
@@ -253,10 +611,10 @@
}
}
if (aspect & VK_IMAGE_ASPECT_DEPTH_BIT) {
if (!MGB_CTX->GetDepthMask()) aspect &= ~VK_IMAGE_ASPECT_DEPTH_BIT;
if (!MG_Pipe::gPipeInputs->GetDepthMask()) aspect &= ~VK_IMAGE_ASPECT_DEPTH_BIT;
}
if (aspect & VK_IMAGE_ASPECT_STENCIL_BIT) {
const Uint32 mask = MGB_CTX->GetStencilState(StencilFace::Front).WriteMask & 255u;
const Uint32 mask = MG_Pipe::gPipeInputs->GetStencilState(StencilFace::Front).WriteMask & 255u;
if (!mask) aspect &= ~VK_IMAGE_ASPECT_STENCIL_BIT;
else if (mask != 255u) {
MGLOG_W_ONCE("DirectVulkan: glClear with a partial stencil write mask is not supported");
@@ -268,8 +626,8 @@
if (!image.image) return;
m_textureManager->FlushPendingUploads();
VkRect2D area{{0, 0}, image.extent};
if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
const auto& box = MGB_CTX->GetScissorBox();
if (GLScissorTestClips()) {
const auto& box = MG_Pipe::gPipeInputs->GetScissorBox();
const Int32 x0 = std::max(box.x(), 0), y0 = std::max(box.y(), 0);
const Int32 x1 = std::min(box.x() + box.z(), static_cast<Int32>(image.extent.width));
const Int32 y1 = std::min(box.y() + box.w(), static_cast<Int32>(image.extent.height));
@@ -917,8 +1275,8 @@
RetireWireDrawPass();
// The scissor test clips blit writes: intersect the destination rectangle with
// the scissor box and shrink the source proportionally.
if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
const IntVec4& scissor = MGB_CTX->GetScissorBox();
if (GLScissorTestClips()) {
const IntVec4& scissor = MG_Pipe::gPipeInputs->GetScissorBox();
const auto clipAxis = [](GLint& d0, GLint& d1, GLint& s0, GLint& s1, GLint clipLo, GLint clipHi) -> Bool {
const Bool dstFlipped = d1 < d0;
GLint lo = dstFlipped ? d1 : d0;
@@ -1700,8 +2058,8 @@
auto& record = state.TextureResources[handle.Slot];
record.Desc.Levels = std::max<Uint16>(record.Desc.Levels,static_cast<Uint16>(base+count));
resource->syncedWireSerial = ++record.Serial;
auto& store = MG_Remote::Server::ServerStagedTexture();
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(handle);
auto& store = MG_Record::ServerStagedTexture();
const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(handle);
const auto target = static_cast<MG_Pipe::MGPipeResourceTarget>(record.Desc.Target);
// The resource descriptor determines the upload-target set; no texture unit or
// client upload-target vector participates in defining the generated shadow.
@@ -1719,7 +2077,7 @@
default: break;
}
for (Uint32 level=base+1;level<base+count;++level) {
const IntVec3 extent = MG_Remote::Server::StagedTextureMipExtent(
const IntVec3 extent = MG_Record::StagedTextureMipExtent(
record.Desc.Target, record.Desc.Width, record.Desc.Height, record.Desc.Depth, level);
// PH-4: the record's own target and format, not NoteLevelDefined's Tex2D/RGBA8
// defaults - a 2D-array or 3D level has depth > 1, which the Tex2D device-limit rule
@@ -1730,4 +2088,3 @@
}
}
}
#endif
@@ -15,7 +15,6 @@
// shader arm fetches sample 0 - not an average. Two arms that answer differently on two devices
// would make "the wire arm is observably the monolith's" untestable, and GL 4.6 core 18.3.1
// requires a SELECTED sample for stencil anyway.
#if MOBILEGL_BUILD_DISAGGREGATED
// The pass's modules, layouts, render passes and pipeline, and the draw it records, are
// WireDepthResolveProbe.cpp's (g5-msprobe): the multisample depth/stencil resolve probe runs
// this exact arm as its CONTROL, so it is defined once for both.
@@ -164,4 +163,3 @@
RetireWireObjects(Move(retired));
return true;
}
#endif
@@ -1,6 +1,5 @@
// UniformManager-owned native descriptors for wire bindings with no image.
// No GL object is constructed or inserted into the client's resource namespace.
#if MOBILEGL_BUILD_DISAGGREGATED
void UniformManager::DestroyWirePlaceholderImage(WirePlaceholderImage& image) const {
if (m_device != VK_NULL_HANDLE) {
if (image.clearFramebuffer) vkDestroyFramebuffer(m_device, image.clearFramebuffer, nullptr);
@@ -245,4 +244,3 @@
out = {image.sampler, image.view, VK_IMAGE_LAYOUT_GENERAL};
return true;
}
#endif
@@ -12,7 +12,6 @@
// The pure half of UniformManager's wire placeholder cache (WirePlaceholderImages.inc): WHICH
// placeholder a binding with no usable image takes. A unit test pins it
// (MG_Test/Pipeline/PipelineQuirkTest.cpp); the Vulkan half that makes the image stays in the .inc.
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Pipe/MGPipeTypes.h>
namespace MobileGL::MG_Backend::DirectVulkan {
@@ -59,4 +58,3 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return key;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
#endif
@@ -2,7 +2,6 @@
#include <Includes.h>
#include "../VkIncludes.h"
#if MOBILEGL_BUILD_DISAGGREGATED
namespace MobileGL::MG_Backend::DirectVulkan {
// The wire draw pass has one graphics subpass, no input attachments,
// multiview or extensions. Its Vulkan pipeline compatibility is formats,
@@ -46,4 +45,3 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 m_nextId = 1; // zero selects the native-handle pipeline domain
};
}
#endif
@@ -1,6 +1,5 @@
// Server-owned texture readback. Included after WireFramebuffer.inc so image
// layout tracking and frame retirement are the same as every other wire verb.
#if MOBILEGL_BUILD_DISAGGREGATED
static Bool PackWireColorTight(const Uint8* source, SizeT sourceBytes, VkFormat format,
TextureInternalFormat logicalFormat, const MG_Pipe::MGPReadbackInfo& info,
Vector<Uint8>& result) {
@@ -211,4 +210,3 @@
ownedBytes = Move(result);
return true;
}
#endif
+21 -12
View File
@@ -16,10 +16,8 @@
#include <cstdint>
#include <cstring>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <Config.h>
#include <MG_Util/Metrics/PipeStats.h>
#endif
namespace MobileGL::MG_Pipe {
const char* MGPipeVerbName(MGPipeVerb verb) {
@@ -47,7 +45,9 @@ namespace MobileGL::MG_Pipe {
return std::nullopt;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// The stamp is the poison's (m_filled, the generations): a build without MOBILEGL_PIPE_POISON
// compiles no read check that could ask any of this. Every transport build has the poison.
#if MOBILEGL_PIPE_POISON
// ================================================================================
// P5: the server's verb stamp, the residual-pull counter, and the four-way read verdict
// ================================================================================
@@ -228,9 +228,11 @@ namespace MobileGL::MG_Pipe {
// D10: rides MGPipeBlocksAreDistinct() - under spawn the two blocks are distinct
// whatever the rehearsal knob says, and a barrier pull across a real process
// boundary is exactly the thing this Fatal exists to name.
#if MOBILEGL_BUILD_DISAGGREGATED
if (MGPipeBlocksAreDistinct()) {
StrictBarrierPullFatal(field, verb, "MOBILEGL_IPC_ROLE_SPLIT_STATE=1");
}
#endif
if (!MGPipeApplierCurrentRecordIsBarriered()) {
StrictBarrierPullFatal(field, verb, "UNBARRIERED, the client did not fill it");
}
@@ -238,6 +240,9 @@ namespace MobileGL::MG_Pipe {
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ResidualPulls, 1);
}
#if MOBILEGL_BUILD_DISAGGREGATED
// The strict knob is the transport's (MOBILEGL_IPC_STRICT_ERRORS); without one an
// admitted pull is counted and the unbarriered one above is still Fatal.
if (MG_Config::Ipc.StrictErrors) {
// THE THIRD DISJUNCT (P5e gl, ID-128), and it has to be asked at RUNTIME because
// it is a fact about this record's payload rather than about its opcode. The
@@ -255,6 +260,7 @@ namespace MobileGL::MG_Pipe {
}
AdmittedBarrierPullOnce(field, verb, !statically);
}
#endif
}
// The verb's OWN may-read table (FillPoints.def, kMGPipeClassFieldMask). The stamp
@@ -348,12 +354,15 @@ namespace MobileGL::MG_Pipe {
// MGPipeRoleSplitRehearsalActive is deliberately NOT latched: it is two global loads, asked once per
// verb on the fill side and once per verb boundary on the stamp side, and a latch is a
// second thing a test that flips the knob mid-process would have to know about.
#if MOBILEGL_BUILD_DISAGGREGATED
// P13 W5: the role split, the two blocks and the context-live flag are the transport's; the
// stamp and the read verdicts around them are the record arm's (PipeInputs.h's dual block
// gives a build without a transport the one-block answers inline).
Bool MGPipeRoleSplitRehearsalActive() {
return MG_Config::Ipc.RoleSplitState &&
MG_Config::Transport != MG_Config::TransportMode::Monolith;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// ---- D1c's predicates (CONTRACT-P6 3.2) -------------------------------------------------
namespace {
// Set once by ServerMain, before any GL work. A spawn server is the server for the whole
@@ -389,7 +398,6 @@ namespace MobileGL::MG_Pipe {
return MGPipeRoleSplitRehearsalActive() ||
MG_Config::Transport == MG_Config::TransportMode::Spawn;
}
#endif // MOBILEGL_BUILD_DISAGGREGATED
PipeInputs& MGPipeClientInputs() {
return MGPipeRoleSplitRehearsalActive() ? gPipeInputsClientBlock : gPipeInputs;
@@ -407,7 +415,6 @@ namespace MobileGL::MG_Pipe {
Bool MGPipeServerContextIsLive() { return MGPipeStampAccess::ServerContextLive(gPipeInputs); }
#if MOBILEGL_BUILD_DISAGGREGATED
namespace { const void* g_serverOwnedWindow = nullptr; }
void MGPipeServerSetOwnedWindow(const void* window) { g_serverOwnedWindow = window; }
@@ -430,13 +437,12 @@ namespace MobileGL::MG_Pipe {
if (height != nullptr) *height = g_serverOwnedWindowHeight;
return true;
}
#endif
void MGPipeServerBlockNoteIdentity() {
// D10: THE WIDER PREDICATE, and the narrow one was a latent crash rather than a missing
// optimisation. Gated on the REHEARSAL, this early-returned for the whole life of a
// spawn server - because that knob is off by default and nothing in the spawn shape
// turns it on - so the server's gPipeInputs.ContextIdentity() stayed nullptr forever.
// turns it on - so the server's gPipeInputs->ContextIdentity() stayed nullptr forever.
// DirectGLES' fb-slot memo compares that identity FIRST and with no generation, so a
// nullptr against its own nullptr initialiser reads as a CACHE HIT and hands out a slot
// that was never filled. `st` lands the named Fatal at that site; this is the gate that
@@ -455,6 +461,7 @@ namespace MobileGL::MG_Pipe {
reinterpret_cast<const void*>(static_cast<std::uintptr_t>(
(serial << 1) | Uint64{1})));
}
#endif // MOBILEGL_BUILD_DISAGGREGATED
void MGPipeServerStampVerbBoundary(MGPipeVerb verb) {
PipeInputs& inputs = gPipeInputs;
@@ -509,7 +516,9 @@ namespace MobileGL::MG_Pipe {
// "SetIdentity moves to ApplyOne" - the stamp is the per-verb half of ApplyOne, and
// refreshing here rather than once at Attach keeps the token in step with
// MGPipeApplierContextSerial across a context switch). A no-op with the rehearsal off.
#if MOBILEGL_BUILD_DISAGGREGATED
MGPipeServerBlockNoteIdentity();
#endif
}
void MGPipeServerClearVerbBoundary() { MGPipeStampAccess::SetServerStamped(gPipeInputs, false); }
@@ -587,14 +596,14 @@ namespace MobileGL::MG_Pipe {
// The seven carry no MGP_INPUT_CHECK at all (the declared exception argued at
// PipeInputs.h's F-class block), so freshness can never reach them and neither can the
// stamp's withdrawal. This is the only thing that puts them in `rsp`.
if (!gPipeInputs.ServerStampedVerb()) return;
if (!gPipeInputs->ServerStampedVerb()) return;
const auto ownership = MGPipeFieldOwnershipOf(field);
if (ownership == MGPipeFieldOwnership::kFatal)
MGPipeInputPoisonFatalForVerb(field, gPipeInputs.CurrentVerb());
MGPipeInputPoisonFatalForVerb(field, gPipeInputs->CurrentVerb());
if (ownership == MGPipeFieldOwnership::kBarrierPulled)
CountBarrierPull(field, gPipeInputs.CurrentVerb());
CountBarrierPull(field, gPipeInputs->CurrentVerb());
}
#endif // MOBILEGL_BUILD_DISAGGREGATED
#endif // MOBILEGL_BUILD_RECORD_ARM && MOBILEGL_PIPE_POISON
#if MOBILEGL_PIPE_VERIFY
namespace {
+46 -44
View File
@@ -13,22 +13,19 @@
// the live context happens on the client side, in MG_Impl/Pipe/PipeFill.cpp.
#include <MG_State/GLState/Core.h>
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (rv, CONTRACT-P5C.md §5.3): the three texture shutters' server-side answer lives in the
// applier - MGPipeApplierTextureShutterSerial() / MGPipeApplierContextSerial(), declared here
// so the accessors below can answer with them under a server-stamped verb. MG_Pipe is below
// MG_Backend, so this direction is the layering's, and PipeApply.h forward-declares
// PipeInputs rather than including this header, so there is no cycle.
#include <MG_Pipe/PipeApply.h>
#endif
// 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)
#if (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
@@ -38,9 +35,9 @@ namespace MobileGL::MG_Pipe {
// TABLE 2 (CONTRACT-P5.md section 3, R-7): the four ownership classes, one per field, plus
// the seven sticky forwards' own rows. Included HERE rather than from MG_Pipe/MGPipe.h with
// gen_pipe.py's seven outputs, deliberately: MGPipe.h is in the PULL build's include closure
// and G1 admits no symbol motion there, while this header is reached only through
// PipeInputsSwitch.h's MOBILEGL_PIPE_PUSH arm. It is also exactly the header the poison check
// below and the server's verb stamp both already see.
// and G1 admitted no symbol motion there (until P13 retired the pull build), while this header is
// what the backends include directly. It is also exactly the header the poison check below and the
// server's verb stamp both already see.
#include <MG_Pipe/generated/PipeFieldOwnership.inc>
// PipeInputs.cpp. The poison Fatal with the verb's name ("<none>" before the first
@@ -54,7 +51,6 @@ namespace MobileGL::MG_Pipe {
Optional<MGPipeInputField> MGPipeFindInputField(const char* name);
Optional<MGPipeVerb> MGPipeFindVerb(const char* name);
#if MOBILEGL_BUILD_DISAGGREGATED
// ---- P5: the split arm of the read check (R-7.2, R-7.3) ------------------------------
//
// A stale read stops being one answer and becomes FOUR, keyed on the field's table-2 class
@@ -88,13 +84,11 @@ namespace MobileGL::MG_Pipe {
// from either counting the same read twice or - worse, because the field's own class would
// not be BARRIER-PULLED - aborting a read the argument row had just declared legal.
Bool MGPipeInputArgumentRead(MGPipeInputField field, Uint32 arg0, MGPipeVerb verb, Bool serverStamped);
#endif
// 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
#if MOBILEGL_BUILD_DISAGGREGATED
#define MGP_INPUT_CHECK(Field) \
do { \
if ((m_serverStampedVerb && ::MobileGL::MG_Pipe::MGPipeFieldOwnershipOf(Field) == \
@@ -111,15 +105,6 @@ namespace MobileGL::MG_Pipe {
} \
} while (0)
#else
#define MGP_INPUT_CHECK(Field) \
do { \
if (!::MobileGL::MG_Pipe::MGPipeInputFieldIsFresh(m_filled, (Field))) { \
::MobileGL::MG_Pipe::MGPipeInputPoisonFatalForVerb((Field), m_currentVerb); \
} \
} while (0)
#define MGP_INPUT_CHECK_ARG(Field, Arg0) MGP_INPUT_CHECK(Field)
#endif
#else
#define MGP_INPUT_CHECK(Field) ((void)0)
#define MGP_INPUT_CHECK_ARG(Field, Arg0) ((void)0)
#endif
@@ -256,18 +241,17 @@ namespace MobileGL::MG_Pipe {
};
// ---- identity / liveness (not fields) ----
// Whether a live GLContext exists. Forwarded (PipeFill.cpp): under push MGB_CTX_LIVE
// Whether a live GLContext exists. Forwarded (PipeFill.cpp): under push gPipeInputs
// 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.
// The live GLContext's address at the last fill; serves gPipeInputs.
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
#if MOBILEGL_BUILD_DISAGGREGATED
// TRUE between the server's verb-boundary stamp and whoever clears it. It is the
// arming condition of the whole split read path: only inside a server-stamped verb is
// a BARRIER-PULLED read counted rather than Fatal, and only there is a sticky forward
@@ -286,7 +270,6 @@ namespace MobileGL::MG_Pipe {
// case the sticky exemption was written for, would be counted and, under strict, would
// abort. So: PipeApplier clears on leaving the applier. Not optional.
Bool ServerStampedVerb() const { return m_serverStampedVerb; }
#endif
// ---- V: values ----
Int GetActiveTextureUnit() const {
@@ -443,7 +426,7 @@ namespace MobileGL::MG_Pipe {
// BufferTarget, and Coverage.def:62-69 already rules that such a field stays ONE row.
// Only [0] (pack) has a carrier - set_pixel_pack_state, which the applier writes
// (PipeApply.cpp:1373) - so the field is APPLIER-DERIVED and the UNPACK half is FATAL:
// every MGB_CTX->GetPixelStoreParameters site in the tree passes false
// every gPipeInputs->GetPixelStoreParameters site in the tree passes false
// (DirectGLES.cpp:7924, :9399, :10893, :11272, Utils.cpp:2302,
// VulkanRenderer.cpp:10980), and PipeFill.cpp's EmitPixelPackState says the same from
// the other side: "nothing on the far side of the boundary reads unpack state".
@@ -492,9 +475,7 @@ namespace MobileGL::MG_Pipe {
Uint64 GetSamplingResolutionGeneration() const {
MGP_INPUT_CHECK(MGPipeInputField::GetSamplingResolutionGeneration);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetSamplingResolutionGeneration, 0, 0);
#if MOBILEGL_BUILD_DISAGGREGATED
if (m_serverStampedVerb) return MGPipeApplierTextureShutterSerial();
#endif
return m_samplingResolutionGeneration;
}
const IntVec4& GetScissorBox() const {
@@ -510,21 +491,17 @@ namespace MobileGL::MG_Pipe {
Uint64 GetTextureBindGeneration() const {
MGP_INPUT_CHECK(MGPipeInputField::GetTextureBindGeneration);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTextureBindGeneration, 0, 0);
#if MOBILEGL_BUILD_DISAGGREGATED
// See GetSamplingResolutionGeneration: the server answers from its own Serial.
if (m_serverStampedVerb) return MGPipeApplierTextureShutterSerial();
#endif
return m_textureBindGeneration;
}
Uint64 GetTextureContextId() const {
MGP_INPUT_CHECK(MGPipeInputField::GetTextureContextId);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetTextureContextId, 0, 0);
#if MOBILEGL_BUILD_DISAGGREGATED
// A context IDENTITY rather than a generation: stable within the served context,
// moved by every MGPipeApplierReset - which is all the backends' per-context memo
// keys ask of it.
if (m_serverStampedVerb) return MGPipeApplierContextSerial();
#endif
return m_textureContextId;
}
Uint64 GetTransformFeedbackCapturedVertices() const {
@@ -758,9 +735,7 @@ namespace MobileGL::MG_Pipe {
#if MOBILEGL_PIPE_POISON
MGPipeFilledState m_filled{};
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
Bool m_serverStampedVerb = false;
#endif
// ---- V ----
Int m_activeTextureUnit = 0;
@@ -823,7 +798,7 @@ namespace MobileGL::MG_Pipe {
TextureUnit* m_textureUnitBase = nullptr;
};
// The single global the backends read through MGB_CTX (ARCHITECTURE.md 9.2). An inline
// The single global the backends read through gPipeInputs (ARCHITECTURE.md 9.2). An inline
// variable: no .cpp is needed for the definition.
//
// P14 S2: IT IS THE PROCESS-WIDE BLOCK, and `gPipeInputs` below is the name every reader
@@ -851,18 +826,24 @@ namespace MobileGL::MG_Pipe {
// ============================================================================
//
// ONE PROCESS, SEVERAL SERVED SESSIONS. The applier writes this block and the backend reads it
// through MGB_CTX, both on the SAME apply thread - the block IS "the state the record in hand
// through gPipeInputs, both on the SAME apply thread - the block IS "the state the record in hand
// carries". Two sessions applying concurrently on two apply threads through one block would
// read each other's render state, which is the cross-talk a multi-session server must not
// have. Nothing about the block's contents changed; what changed is that the name resolves to
// the SESSION'S block on a thread that belongs to one.
//
// WHY A THREAD-LOCAL REFERENCE, AND NOT SOMETHING CLEVERER. MGB_CTX is
// `(&::MobileGL::MG_Pipe::gPipeInputs)` (MG_Pipe/PipeInputsSwitch.h) and is spelled at ~380
// sites in MG_Backend/MG_Impl; a reference whose TARGET varies with the calling thread is the
// only shape that keeps all of them compiling and all of them correct. The cost is one TLS
// address computation per access site - not one per field, and with no guard: the reference is
// bound once per thread on its first use.
// WHY A THREAD-LOCAL SLOT, AND NOT SOMETHING CLEVERER. gPipeInputs is spelled at ~380
// sites in MG_Backend/MG_Impl (they read `MG_Pipe::gPipeInputs->` directly since P13 retired the
// pull/push switch macro); a name whose TARGET varies with the calling thread is the only
// shape that keeps all of them compiling and all of them correct.
//
// P15: A POINTER SLOT, NOT A REFERENCE. It used to be a `thread_local PipeInputs&` bound by a
// dynamic initializer, and a thread_local with a dynamic initializer is reached through its
// TLS init function, which tests two more thread_local guards on every read: three TLS
// address calls per access in a dlopen'ed library, at every API level. The slot below is
// constant-initialized and trivially destructible, so an access is one TLS address
// computation, a load and a null test; the binding itself is unchanged - once per thread, at
// its first use, through the same resolver.
//
// THE RESOLVER IS A HOOK because this header may not know about sessions (MG_Remote/Server
// owns them and includes THIS header, not the other way round). It is installed once by the
@@ -885,7 +866,27 @@ namespace MobileGL::MG_Pipe {
return gPipeInputsDefault;
}
inline thread_local PipeInputs& gPipeInputs = MGPipeServerInputsForCallingThread();
class PipeInputsThreadSlot {
public:
constexpr PipeInputsThreadSlot() = default;
PipeInputsThreadSlot(const PipeInputsThreadSlot&) = delete;
PipeInputsThreadSlot& operator=(const PipeInputsThreadSlot&) = delete;
// The block's address is `&*gPipeInputs`; the slot's own address names nothing.
void operator&() const = delete;
PipeInputs& Get() {
PipeInputs* bound = m_bound;
if (bound == nullptr) [[unlikely]] bound = m_bound = &MGPipeServerInputsForCallingThread();
return *bound;
}
PipeInputs* operator->() { return &Get(); }
PipeInputs& operator*() { return Get(); }
operator PipeInputs&() { return Get(); }
private:
PipeInputs* m_bound = nullptr;
};
inline constinit thread_local PipeInputsThreadSlot gPipeInputs;
#if MOBILEGL_BUILD_DISAGGREGATED
// ============================================================================
@@ -903,7 +904,7 @@ namespace MobileGL::MG_Pipe {
// dual-block arm, PipeInputs.cpp) instead of a silent cross-role answer.
//
// THE READ SIDE'S SPELLING DOES NOT MOVE. gPipeInputs remains the server-role block, so the
// 379 MGB_CTX sites and PipeApply.cpp's applier writes are untouched; the only new spelling
// 379 gPipeInputs sites and PipeApply.cpp's applier writes are untouched; the only new spelling
// is on the fill side (MG_Impl/Pipe/PipeFill.cpp), which asks MGPipeClientInputs(). Under
// monolith transport - every unit and integration-gpu lane of a split build - the selection
// folds back to the single shared block and behaviour is byte-for-byte the old one.
@@ -1007,6 +1008,9 @@ namespace MobileGL::MG_Pipe {
// inline that no caller in such a build ever has a reason to call twice - the disaggregated
// arm above is the real one.
inline PipeInputs& MGPipeClientInputs() { return gPipeInputs; }
// P13 W5: one block, no client-role stamp to withdraw - and no server role to be on.
inline void MGPipeClientClearVerbBoundary() {}
inline constexpr Bool MGPipeServerArm() { return false; }
#endif
// Every field has storage or is forwarded, and nothing else.
@@ -1017,7 +1021,6 @@ namespace MobileGL::MG_Pipe {
// The docs budget ~20 KB; the block is a few KB.
static_assert(sizeof(PipeInputs) < 20 * 1024, "PipeInputs outgrew its budget");
#if MOBILEGL_BUILD_DISAGGREGATED
// ============================================================================
// P5: the server-side verb stamp, and the counter that sizes what it leaves behind
// ============================================================================
@@ -1092,7 +1095,6 @@ namespace MobileGL::MG_Pipe {
// object or write into the frontend. This is what puts them in `rsp` and, under
// MOBILEGL_IPC_STRICT_ERRORS=1, makes them Fatal like any other BARRIER-PULLED row.
void MGPipeStickyForwardPull(MGPipeInputField field);
#endif
#if MOBILEGL_PIPE_VERIFY
// PipeInputs.cpp. Per-field equality for the entry compare (P1 brief D8): V by value
@@ -0,0 +1,27 @@
// MobileGL - MobileGL/MG_Backend/Record/ApplyRoleBackend.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
// P13 W5: WHOSE BACKEND ANSWERS A CAPABILITY QUESTION ON THE APPLY SIDE. Under a transport the
// server's own backend (ServerLoop's) is the authority for the records it applies; the record arm
// asked MG_Remote for it by name, which a library without MG_Remote cannot. The answer now comes
// through this hook, which ServerLoop.cpp installs at static init. Without it - a library without
// a transport - the answer is null, which is exactly what a monolith process got before (a
// monolith has no server loop, so its server backend was always null).
#pragma once
namespace MobileGL::MG_Backend {
class BackendObject;
using ApplyRoleBackendHook = BackendObject* (*)();
inline constinit ApplyRoleBackendHook gApplyRoleBackendHook = nullptr;
inline BackendObject* ApplyRoleBackend() {
return gApplyRoleBackendHook != nullptr ? gApplyRoleBackendHook() : nullptr;
}
} // namespace MobileGL::MG_Backend
File diff suppressed because it is too large Load Diff
+197
View File
@@ -0,0 +1,197 @@
// MobileGL - MobileGL/MG_Backend/Record/RecordVerbSink.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
// P13 W5: THE RECORD VERBS' CONSUMER. The verbs no MGPipeApply* entry point takes (clear, blit, the
// readbacks, the draws, dispatch, copies, image binds, XFB, mipmaps) end in the backend's own call
// through the private GlobalBackendFunctionsTable this sink holds. Under a transport the server's
// ServerVerbSink (MG_Remote/Server/PipeApplier.h) is this plus the session's verbs; the record
// arm's in-process port drives this class directly, which is what lets a library without a
// transport run the record arm.
#pragma once
#include <Includes.h>
#include <MG_Backend/BackendObject.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/PipeVerbSink.h>
namespace MobileGL::MG_Record {
class RecordVerbSink : public MG_Pipe::MGPipeVerbSink {
public:
// The server's private backend. Null until ServerLoop::CreateBackend has run, and a
// verb that arrives before then declines by name rather than dereferencing.
void SetBackend(MG_Backend::BackendObject* backend);
MG_Backend::BackendObject* Backend() const { return m_backend; }
void SetMaxReplyBytes(Uint64 bytes) { m_maxReplyBytes = bytes; }
Bool OnClear(const MG_Pipe::MGPClear& clear) override;
Bool OnBlit(const MG_Pipe::MGPBlit& blit) override;
Bool OnReadPixels(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq,
MG_Pipe::MGPipeReplySink* replies) override;
Bool OnGetTextureImage(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq,
MG_Pipe::MGPipeReplySink* replies) override;
// P9 (CONTRACT-P9.md §1): the pack-buffer halves. Nothing is answered; the tight pixels
// land in record.Dst through the applier's buffer-write gate.
Bool OnReadPixelsToBuffer(const MG_Pipe::MGPReadbackToBuffer& record) override;
Bool OnGetTextureImageToBuffer(const MG_Pipe::MGPReadbackToBuffer& record) override;
Bool OnDrawVbo(const MG_Pipe::MGPDrawInfo& info, const MG_Pipe::MGPDrawRange* ranges,
const MG_Pipe::MGHostSpan* userIndices,
const MG_Pipe::MGPDrawIndirect* indirect) override;
// ---- P5b (MG_Remote/CONTRACT-P5B.md): one override per row a migration package owns.
// At the contract commit EVERY BODY BELOW IS A STUB that dies
// Fatal{UnmigratedVerb, "<GL slot>"} by the slot's own name - the same line the client's
// class-C table raises and the census greps - so a client flipped ahead of its server
// half aborts by name rather than rendering nothing, and the census on this head is
// unchanged (the client refuses first). The owning package replaces the body.
//
// i1 OnLaunchGrid ("DispatchCompute"), OnMemoryBarrier, OnResourceCopyRegion
// ("CopyImageSubData"), OnBindShaderImage ("BindImageTexture"),
// OnSetStorageBlockBinding ("ShaderStorageBlockBinding")
// t2 LANDED. OnBeginStreamOutput / OnEndStreamOutput / OnPauseStreamOutput /
// OnResumeStreamOutput / OnBindStreamOutput / OnPatchParameter are real bodies
// now: the backend call the contract names, the null-slot DECLINE, a tally.
// f1 OnGenerateMipmap, OnCopyFramebufferToTexture ("CopyTexImage2D" /
// "CopyTexSubImage2D"), and OnClear's four non-Whole kinds (live already)
// d1 OnDrawVbo above: the indirect tail, the user-index span, NumDraws > 1 and the
// instanced arms - LIVE since d1 v1 (every shape the client's nineteen draw slots
// produce dispatches to the backend slot CONTRACT-P5B.md §2 d1 names)
Bool OnLaunchGrid(const MG_Pipe::MGPGridInfo& grid) override;
Bool OnMemoryBarrier(const MG_Pipe::MGPMemoryBarrier& barrier) override;
Bool OnResourceCopyRegion(const MG_Pipe::MGPCopyRegion& copy) override;
Bool OnBindShaderImage(const MG_Pipe::MGPImageBind& bind) override;
Bool OnSetStorageBlockBinding(const MG_Pipe::MGPStorageBlockBinding& binding,
const char* name) override;
Bool OnBeginStreamOutput(const MG_Pipe::MGPStreamOutputBegin& begin) override;
Bool OnEndStreamOutput(const MG_Pipe::MGPXfbAccounting& accounting) override;
Bool OnPauseStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) override;
Bool OnResumeStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) override;
Bool OnBindStreamOutput(const MG_Pipe::MGPStreamOutputBind& bind) override;
Bool OnDeleteStreamOutput(const MG_Pipe::MGPStreamOutputBind& object) override;
Bool OnPatchParameter(const MG_Pipe::MGPPatchParameter& patch) override;
Bool OnGenerateMipmap(const MG_Pipe::MGPMipPlan& plan) override;
Bool OnCopyFramebufferToTexture(const MG_Pipe::MGPCopyFromFramebuffer& copy) override;
// Per-verb tallies. The lane asserts these moved, because "the scenario passed" on a
// split build is also what a scenario that ran entirely on the monolith path looks
// like (R-16: a probe may not arm against a stub).
Uint64 Clears() const { return m_clears; }
Uint64 Draws() const { return m_draws; }
Uint64 Readbacks() const { return m_readbacks; }
Uint64 Blits() const { return m_blits; }
Uint64 ReadbackBytes() const { return m_readbackBytes; }
// P9: pack-buffer readbacks that landed in their buffer (both ops), and the buffer-write
// records they took. Readbacks() above counts only the reply-slot form.
Uint64 BufferReadbacks() const { return m_bufferReadbacks; }
Uint64 BufferReadbackWrites() const { return m_bufferReadbackWrites; }
// ---- P5b package i1's tallies. R-16: a probe may not arm against a stub, and on a
// split build "the scenario passed" is also what a scenario that never left the
// monolith path looks like - so the lane asserts the number that only this sink can
// move. One per wire ROW, not per GL slot: launch_grid carries both dispatch entry
// points and memory_barrier both barrier ones, and the sink is where they separate.
Uint64 ImageBinds() const { return m_imageBinds; }
Uint64 Dispatches() const { return m_dispatches; }
Uint64 MemoryBarriers() const { return m_memoryBarriers; }
Uint64 ImageCopies() const { return m_imageCopies; }
Uint64 StorageBlockBindings() const { return m_storageBlockBindings; }
// ID-49's tight-size control reads this: the scratch a read_pixels grew to. It must equal
// the tight w*h*bpp extent of the read, never the client's DstSize - a scratch sized from
// DstSize is exactly the heap overflow codex 1 found, one field over.
Uint64 ReadbackScratchBytes() const { return static_cast<Uint64>(m_readbackScratch.size()); }
// ---- P5b d1: what the LAST draw_vbo record carried, as the sink saw it ---------------
//
// Recorded BEFORE the backend is consulted, so a process with no backend object (every
// unit case) can still assert the wire's fields rather than only that a draw "was
// declined": the record's head, its first range, its indirect block, and whether a
// user-index span rode with it and how many bytes it named. Nothing here outlives the
// call except these copies (rule C: the pointers the sink was handed are not kept).
struct LastDrawRecord {
MG_Pipe::MGPDrawInfo Info{};
MG_Pipe::MGPDrawRange FirstRange{};
MG_Pipe::MGPDrawIndirect Indirect{};
Uint64 UserIndexBytes = 0;
Bool HadUserIndices = false;
Bool HadIndirect = false;
};
const LastDrawRecord& LastDraw() const { return m_lastDraw; }
// draw_vbo records seen, applied or declined; Draws() above counts only the applied.
Uint64 DrawRecords() const { return m_drawRecords; }
// P5b t2's tallies, for the same reason the five above exist (R-16): under split "the
// scenario passed" is also what a scenario that ran entirely on the monolith path looks
// like, so a lane that wants to say the XFB spans CROSSED has to read a counter the
// server moved. Spans counts Begin and End together - they are one span and a lane that
// saw only one of them has a bug the two-counter version would have hidden behind a
// sum; controls counts Pause and Resume; binds and patch parameters count their own.
Uint64 StreamOutputSpans() const { return m_streamOutputSpans; }
Uint64 StreamOutputControls() const { return m_streamOutputControls; }
Uint64 StreamOutputBinds() const { return m_streamOutputBinds; }
Uint64 PatchParameters() const { return m_patchParameters; }
protected:
// A backend change: the subclass lets go of what it held on the old one.
virtual void OnBackendChanged() {}
// The wire's user-index span (a draw whose indices rode in a stage segment). The record
// arm's port never hands one over - its indices are in the client's own buffer - so only
// the wire's subclass, which can check a span against its segments, answers true.
virtual Bool CheckUserIndices(const MG_Pipe::MGPDrawInfo&, const MG_Pipe::MGPDrawRange*,
const MG_Pipe::MGHostSpan&) {
return false;
}
const MG_Backend::GlobalBackendFunctionsTable* Table(const char* verb) const;
// read_pixels' read, shared by the reply form and the pack-buffer form: the bound read
// framebuffer, NEUTRAL pack state (ID-49), `tight` bytes into m_readbackScratch. False
// when this server has no GL.ReadPixels - the caller owns what that means.
Bool ReadBoundFramebufferTight(const MG_Pipe::MGPReadbackInfo& info, Uint64 tight);
// get_texture_image's read, shared the same way: the whole level into `bytes`.
Bool ReadTextureImageTight(const MG_Pipe::MGPReadbackInfo& image, Vector<Uint8>& bytes);
// P9: `tight` holds Src's pixels, tightly packed; land them in record.Dst at the
// record's layout through MGPipeApplyResourceSubData, one write per contiguous run. The
// layout was checked before the read (PipeApplier.cpp ReadbackToBufferLayoutFault).
Bool LandReadbackInBuffer(const MG_Pipe::MGPReadbackToBuffer& record, Uint8* tight,
Uint64 bytesPerPixel);
MG_Backend::BackendObject* m_backend = nullptr;
Uint64 m_clears = 0;
Uint64 m_draws = 0;
Uint64 m_readbacks = 0;
Uint64 m_blits = 0;
Uint64 m_readbackBytes = 0;
Uint64 m_bufferReadbacks = 0;
Uint64 m_bufferReadbackWrites = 0;
Uint64 m_imageBinds = 0;
Uint64 m_dispatches = 0;
Uint64 m_memoryBarriers = 0;
Uint64 m_imageCopies = 0;
Uint64 m_storageBlockBindings = 0;
Uint64 m_streamOutputSpans = 0;
Uint64 m_streamOutputControls = 0;
Uint64 m_streamOutputBinds = 0;
Uint64 m_patchParameters = 0;
// ReadPixels' destination. The pixels go into the reply slot, but GLFunctionsTable::
// ReadPixels writes into a caller buffer, so one staging vector per session sits
// between them. Grown, never shrunk, and never handed out past the call.
Vector<Uint8> m_readbackScratch;
// Server-declared LinkTerms.maxReplyBytes, copied from the attached server link.
Uint64 m_maxReplyBytes = 0;
// P5b d1: the multi-draw arrays the glMultiDraw* slots take, rebuilt from the ranges
// per record (rule C: bounded by NumDraws, owned here, never handed out past the call),
// and the last-record witness above.
Vector<GLsizei> m_multiCounts;
Vector<GLint> m_multiFirsts;
Vector<const void*> m_multiOffsets;
Vector<GLint> m_multiBaseVertices;
LastDrawRecord m_lastDraw{};
Uint64 m_drawRecords = 0;
};
} // namespace MobileGL::MG_Record
@@ -1,4 +1,4 @@
// MobileGL - MobileGL/MG_Remote/Server/StagedShadow.h
// MobileGL - MobileGL/MG_Backend/Record/StagedShadow.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
@@ -38,9 +38,9 @@
#include <Includes.h>
// P7 wave 0 / Ph slice (2): RequireCovered's death goes through Session::Fail like every other
// one. Not an out-of-line helper - this header is already inside MG_Remote, so it may name
// MG_Remote's own funnel, and check_include_closure.py's four probes do not reach it.
#include <MG_Remote/FatalFunnel.h>
// one. P13 W5: the store left MG_Remote (the record arm runs without it), so it dies through
// MG_Pipe's record fail seam, which FatalFunnel.cpp points at SessionFail wherever MG_Remote is linked.
#include <MG_Pipe/PipeSessionFail.h>
#include <MG_Util/Debug/Log.h>
#include <MG_Util/Math/VectorTypes.h>
@@ -50,7 +50,7 @@
#include <cstring>
#include <mutex>
namespace MobileGL::MG_Remote::Server {
namespace MobileGL::MG_Record {
// P14: WHOSE STAGED BYTES ARE THESE? The two server stores below are one per process, but the
// in-process display server serves several sessions in that process at once, and a session's
@@ -120,6 +120,22 @@ namespace MobileGL::MG_Remote::Server {
return shadow.Bytes.data();
}
// P13 W4a: a COPY even in a non-copying store - the fallback for a GPU-written store on
// monolith's record arm that never recorded the shadow base it aliases (Managers.cpp's
// RefreshAliasedStoreFromGpu). Whole-store, covered, and dropped with the key.
const Uint8* AdoptPrivateCopy(const void* key, SizeT width, const void* bytes, SizeT size) {
const std::lock_guard<std::mutex> lock(m_mutex);
Shadow& shadow = m_shadows[key];
shadow.SessionKey = CurrentStagedBucket().SessionKey;
if (shadow.Bytes.size() < std::max(width, size)) shadow.Bytes.resize(std::max(width, size), 0);
if (size != 0 && bytes != nullptr) {
std::memcpy(shadow.Bytes.data(), bytes, size);
CoverageAdd(shadow.Covered, 0, size);
}
m_any.store(true, std::memory_order_release);
return shadow.Bytes.data();
}
void Drop(const void* key) {
if (!m_any.load(std::memory_order_acquire)) return;
const std::lock_guard<std::mutex> lock(m_mutex);
@@ -156,7 +172,7 @@ namespace MobileGL::MG_Remote::Server {
if (it == m_shadows.end() || it->second.Bytes.data() != hostBase) return;
if (CoverageHas(it->second.Covered, start, end)) return;
// Verbatim what the MGLOG_F said, through the funnel that publishes it (P7 wave 0).
SessionFail(MGFatalFamily::StageSnapshotTooNarrow,
MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::StageSnapshotTooNarrow,
"MGPipe: Fatal{StageSnapshotTooNarrow, \"%s\"} - the server's ladder wants "
"[%zu, %zu) of a buffer whose staged coverage does not include it. Under "
"split the authoritative shadow is SERVER-OWNED (rule C) and "
@@ -246,4 +262,4 @@ namespace MobileGL::MG_Remote::Server {
std::atomic<Bool> m_any{false};
};
} // namespace MobileGL::MG_Remote::Server
} // namespace MobileGL::MG_Record
@@ -1,4 +1,4 @@
// MobileGL - MobileGL/MG_Remote/Server/StagedTextureStore.h
// MobileGL - MobileGL/MG_Backend/Record/StagedTextureStore.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
@@ -100,10 +100,10 @@
#include <MG_Pipe/MGPipeTypes.h>
// StagedShadowStore::CoverageAdd / CoverageHas: ONE spelling of the covered set, so the texture
// half's runs and the buffer half's ranges cannot drift apart on what "covered" means.
#include <MG_Remote/Server/StagedShadow.h>
#include <MG_Backend/Record/StagedShadow.h>
// P7 wave 0 / Ph slice (2): RequireLevelBytes' death goes through Session::Fail. Named here
// rather than left to StagedShadow.h's copy of the same include.
#include <MG_Remote/FatalFunnel.h>
#include <MG_Pipe/PipeSessionFail.h>
#include <MG_Util/Debug/Log.h>
#include <MG_Util/Metrics/TextureMetrics.h>
#include <MG_Util/Math/VectorTypes.h>
@@ -114,11 +114,9 @@
#include <mutex>
#include <limits>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <Config.h>
#endif
namespace MobileGL::MG_Remote::Server {
namespace MobileGL::MG_Record {
// §1's server-side per-level extent (CONTRACT-P5C table 0): max(1, base_extent >> level)
// per SHRINKING axis, with an array texture's layer count fixed - it is not a dimension of
@@ -298,6 +296,20 @@ namespace MobileGL::MG_Remote::Server {
explicit StagedTextureStore(Bool copies) : m_copies(copies) {}
Bool CopiesIntoServerStorage() const { return m_copies; }
// P13 W4b: THE ALIASING MODE - monolith's record arm. The store keeps every piece of
// bookkeeping it keeps under split (definitions, extents, the exact covered runs, the
// driver-written and GPU-dirty marks), but a level's bytes are the FRONTEND's level shadow
// in place: a run records the address it arrived at (minus its offset in the level) and
// nothing is copied. The readers then ask exactly what they ask under split - is the
// level covered, what are its bytes - and get the same answers from memory that is
// already there. This store never writes frontend memory: a copy it follows
// (FollowCopy) first takes the destination level into a server-owned copy, which is then
// what AdoptClientBoxes lands the client's later boxes on - exactly the split store's
// shape for that level - and a copy it cannot follow marks the destination
// driver-written, which sends its readers to the driver.
Bool Aliases() const { return !m_copies && MG_Config::RecordArmAliasesFrontend(); }
// Does this store answer at all: a copying store, or an aliasing one.
Bool Holds() const { return m_copies || Aliases(); }
void SetDeviceLimits(Int maxTextureSize, Int max3DTextureSize, Int maxCubeMapTextureSize,
Int maxArrayTextureLayers, Int maxTextureBufferSize) {
const std::lock_guard<std::mutex> lock(m_mutex);
@@ -330,14 +342,17 @@ namespace MobileGL::MG_Remote::Server {
// reallocates it, which is StagedShadowStore::Adopt's note one level down. Nothing needs
// the base while a level is still being assembled - the readers run at the barrier, after
// the last piece - so callers here ignore it.
// `frontendShadow`: the bytes ARE the frontend's level shadow (a client record's run) - the
// one source an aliasing store may keep by address. Every other adoption (the server's own
// readbacks, regenerations and promotions) is server memory that dies with the call, and is
// copied even by an aliasing store.
const Uint8* AdoptRun(Uint64 key, Uint16 uploadTarget, Uint16 level, const IntVec3& extent,
Uint64 imageOffset, const void* bytes, SizeT byteSize) {
if (!m_copies) return nullptr;
Uint64 imageOffset, const void* bytes, SizeT byteSize, Bool frontendShadow = false) {
if (!Holds()) return nullptr;
const ShadowKey bucketed = Bucketed(key);
const std::lock_guard<std::mutex> lock(m_mutex);
LevelShadow& shadow = m_shadows[bucketed].Levels[PackLevel(uploadTarget, level)];
RequireDeclaredLevel(shadow, key, uploadTarget, level, extent);
#if MOBILEGL_BUILD_DISAGGREGATED
// A run can be the first piece of a redefined level. Old coverage
// belongs to the previous coordinate system even when the new run
// happens to touch its old range; retaining it invents bytes that
@@ -348,9 +363,9 @@ namespace MobileGL::MG_Remote::Server {
// belt that keeps B4's rule if that refusal is ever relaxed.
if (!shadow.Defined || shadow.Extent != extent) {
shadow.Bytes.clear();
shadow.Alias = nullptr;
shadow.Covered.clear();
}
#endif
if (shadow.HoldsFollowedCopy) {
// P8-E: a run landed whole over a followed copy (AdoptClientBoxes is the spelling
// that keeps it), so the bytes no longer hold what the driver does.
@@ -358,7 +373,8 @@ namespace MobileGL::MG_Remote::Server {
m_driverWritten[bucketed] = true;
}
shadow.GpuDirty = false;
return CopyRunInto(shadow, key, uploadTarget, level, imageOffset, bytes, byteSize);
m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
return CopyRunInto(shadow, key, uploadTarget, level, imageOffset, bytes, byteSize, frontendShadow);
}
// THE WHOLE-LEVEL SPELLING: the run is the level's COMPLETE current content, beginning at
@@ -368,18 +384,20 @@ namespace MobileGL::MG_Remote::Server {
// into; under monolith both do nothing and return nullptr, and the caller
// (Ops_H_TextureSubData) has already returned before reaching either.
const Uint8* Adopt(Uint64 key, Uint16 uploadTarget, Uint16 level, const IntVec3& extent,
const void* bytes, SizeT byteSize) {
if (!m_copies) return nullptr;
const void* bytes, SizeT byteSize, Bool frontendShadow = false) {
if (!Holds()) return nullptr;
const ShadowKey bucketed = Bucketed(key);
const std::lock_guard<std::mutex> lock(m_mutex);
LevelShadow& shadow = m_shadows[bucketed].Levels[PackLevel(uploadTarget, level)];
RequireDeclaredLevel(shadow, key, uploadTarget, level, extent);
shadow.GpuDirty = false;
m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
// P8-E: the caller states these bytes ARE the level, which a followed copy is part of.
shadow.HoldsFollowedCopy = false;
shadow.Bytes.clear();
shadow.Alias = nullptr;
shadow.Covered.clear();
return CopyRunInto(shadow, key, uploadTarget, level, 0, bytes, byteSize);
return CopyRunInto(shadow, key, uploadTarget, level, 0, bytes, byteSize, frontendShadow);
}
// A storage-defining respecify named this level: it EXISTS from here on, at the exact
@@ -408,7 +426,7 @@ namespace MobileGL::MG_Remote::Server {
void NoteLevelDefined(Uint64 key, Uint16 uploadTarget, Uint16 level, const IntVec3& extent,
Uint8 resourceTarget = static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D),
Uint32 internalFormat = static_cast<Uint32>(TextureInternalFormat::RGBA8)) {
if (!m_copies) return;
if (!Holds()) return;
const ShadowKey bucketed = Bucketed(key);
const std::lock_guard<std::mutex> lock(m_mutex);
Uint64 byteBound = 0;
@@ -420,7 +438,7 @@ namespace MobileGL::MG_Remote::Server {
if (!validExtent ||
(supportsSubData && sizedFormat && !emptyExtent && !StagedTextureDeclaredLevelByteBound(
resourceTarget, internalFormat, extent, m_deviceLimits, &byteBound))) {
SessionFail(MGFatalFamily::ProtocolCorruption,
MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.NoteLevelDefined\"} - "
"the accepted respecify has no valid server-bounded texture level "
"(key=%llu, target=%u, level=%u, extent=%d,%d,%d, internalFormat=%u)",
@@ -431,11 +449,14 @@ namespace MobileGL::MG_Remote::Server {
if (!shadow.Defined || shadow.Extent != extent || shadow.InternalFormat != internalFormat ||
shadow.ResourceTarget != resourceTarget || shadow.DeclaredByteBound != byteBound) {
shadow.Bytes.clear();
// An aliased level's base is the frontend allocation this redefinition replaced.
shadow.Alias = nullptr;
// The covered set is a statement about THIS coordinate system, so it goes with
// the bytes it described.
shadow.Covered.clear();
shadow.GpuDirty = false;
shadow.HoldsFollowedCopy = false;
m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
}
shadow.Extent = extent;
shadow.InternalFormat = internalFormat;
@@ -453,6 +474,7 @@ namespace MobileGL::MG_Remote::Server {
const ShadowKey bucketed = Bucketed(key);
const std::lock_guard<std::mutex> lock(m_mutex);
m_shadows.erase(bucketed);
m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
}
// T5's dirty mark: a GPU-side generation made this level's texels newer than any
@@ -460,18 +482,26 @@ namespace MobileGL::MG_Remote::Server {
// never crossed - and the mark is what answers "dirty region, level has no pending
// upload" without asking the client (§2.2's last row).
void MarkLevelGpuDirty(Uint64 key, Uint16 uploadTarget, Uint16 level, Bool dirty) {
if (!m_copies) return;
if (!Holds()) return;
const ShadowKey bucketed = Bucketed(key);
const std::lock_guard<std::mutex> lock(m_mutex);
m_shadows[bucketed].Levels[PackLevel(uploadTarget, level)].GpuDirty = dirty;
if (!dirty) m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
m_any.store(true, std::memory_order_release);
}
// P14: moves whenever a level's GPU-dirty mark may have gone false - a CPU run or adoption
// over it, a redefinition, an explicit clear, or a level/shadow dropped. A caller that
// re-marks the same levels every draw (Magma's attachments) may skip the repeat while it
// has not moved: marking is idempotent until something clears it.
Uint64 GpuDirtyClearGeneration() const { return m_gpuDirtyClears.load(std::memory_order_relaxed); }
void Drop(Uint64 key) {
if (!m_any.load(std::memory_order_acquire)) return;
const ShadowKey bucketed = Bucketed(key);
const std::lock_guard<std::mutex> lock(m_mutex);
m_shadows.erase(bucketed);
m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
m_driverWritten.erase(bucketed);
}
@@ -485,10 +515,12 @@ namespace MobileGL::MG_Remote::Server {
const std::lock_guard<std::mutex> lock(m_mutex);
if (session == 0) {
m_shadows.clear();
m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
m_driverWritten.clear();
return;
}
EraseSession(m_shadows, session);
m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
EraseSession(m_driverWritten, session);
}
@@ -510,7 +542,7 @@ namespace MobileGL::MG_Remote::Server {
// next run for it is the CLIENT's shadow, which never saw the copy: AdoptClientBoxes
// lands only the texels that run says the client wrote.
void MarkDriverWritten(Uint64 key) {
if (!m_copies) return;
if (!Holds()) return;
const std::lock_guard<std::mutex> lock(m_mutex);
m_driverWritten[Bucketed(key)] = true;
m_any.store(true, std::memory_order_release);
@@ -535,7 +567,7 @@ namespace MobileGL::MG_Remote::Server {
Bool FollowCopy(Uint64 srcKey, Uint16 srcTarget, Uint16 srcLevel, const IntVec3& srcOrigin,
Uint64 dstKey, Uint16 dstTarget, Uint16 dstLevel, const IntVec3& dstOrigin,
const IntVec3& size) {
if (!m_copies) return false;
if (!Holds()) return false;
const std::lock_guard<std::mutex> lock(m_mutex);
if (m_driverWritten.find(Bucketed(srcKey)) != m_driverWritten.end()) return false;
const LevelShadow* src = FindLevel(srcKey, srcTarget, srcLevel);
@@ -549,14 +581,22 @@ namespace MobileGL::MG_Remote::Server {
// Gathered first, so a copy within one level cannot read rows it has already written.
const SizeT rowBytes = static_cast<SizeT>(size.x()) * texel;
Vector<Uint8> rows(rowBytes * static_cast<SizeT>(size.y()) * static_cast<SizeT>(size.z()));
const Uint8* srcBytes = LevelData(*src);
for (Int z = 0; z < size.z(); ++z) {
for (Int y = 0; y < size.y(); ++y) {
std::memcpy(rows.data() + (static_cast<SizeT>(z) * size.y() + y) * rowBytes,
src->Bytes.data() + TexelOffset(src->Extent, srcOrigin.x(), srcOrigin.y() + y,
srcOrigin.z() + z, texel),
srcBytes + TexelOffset(src->Extent, srcOrigin.x(), srcOrigin.y() + y,
srcOrigin.z() + z, texel),
rowBytes);
}
}
if (dst->Alias != nullptr) {
// P13 W4b, the aliasing mode: the destination's bytes are the frontend's level
// shadow, which never sees the copy and which this store does not write. The level
// becomes server-owned here, from the bytes it stood for, as under split.
dst->Bytes.assign(dst->Alias, dst->Alias + static_cast<SizeT>(dst->DeclaredByteBound));
dst->Alias = nullptr;
}
if (!LevelComplete(*dst)) {
dst->Bytes.assign(static_cast<SizeT>(dst->DeclaredByteBound), Uint8{0});
dst->Covered.clear();
@@ -589,10 +629,13 @@ namespace MobileGL::MG_Remote::Server {
Bool AdoptClientBoxes(Uint64 key, Uint16 uploadTarget, Uint16 level, const IntVec3& extent,
Uint64 imageOffset, const void* bytes, SizeT byteSize,
const ClientTexelBox* boxes, SizeT boxCount) {
if (!m_copies || bytes == nullptr || (boxCount != 0 && boxes == nullptr)) return false;
if (!Holds() || bytes == nullptr || (boxCount != 0 && boxes == nullptr)) return false;
const std::lock_guard<std::mutex> lock(m_mutex);
LevelShadow* shadow = FindLevelMutable(key, uploadTarget, level);
if (shadow == nullptr || !shadow->HoldsFollowedCopy || !LevelComplete(*shadow) || shadow->Extent != extent) {
// Alias is null on a level holding a followed copy (FollowCopy took it server-owned);
// checked anyway, because the boxes below are written into Bytes.
if (shadow == nullptr || !shadow->HoldsFollowedCopy || shadow->Alias != nullptr || !LevelComplete(*shadow) ||
shadow->Extent != extent) {
return false;
}
const SizeT texel = LevelTexelBytes(*shadow);
@@ -645,17 +688,12 @@ namespace MobileGL::MG_Remote::Server {
// reads the level image WHOLE (LevelByteSize / the sync's texel base), so a level with
// one piece missing is as unreadable as one with none.
const Uint8* RequireLevelBytes(Uint64 key, Uint16 uploadTarget, Uint16 level, const char* site) const {
if (!m_copies) return nullptr;
if (!Holds()) return nullptr;
const std::lock_guard<std::mutex> lock(m_mutex);
const LevelShadow* shadow = FindLevel(key, uploadTarget, level);
if (shadow != nullptr && shadow->Defined && shadow->DeclaredByteBound != 0 &&
shadow->Bytes.size() == shadow->DeclaredByteBound &&
StagedShadowStore::CoverageHas(shadow->Covered, 0,
static_cast<SizeT>(shadow->DeclaredByteBound))) {
return shadow->Bytes.data();
}
if (shadow != nullptr && LevelComplete(*shadow)) return LevelData(*shadow);
// Verbatim what the MGLOG_F said, through the funnel that publishes it (P7 wave 0).
SessionFail(MGFatalFamily::StageSnapshotTooNarrow,
MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::StageSnapshotTooNarrow,
"MGPipe: Fatal{StageSnapshotTooNarrow, \"%s\"} - the texture sync wants the "
"bytes of (uploadTarget=%u, level=%u) and the server's staged shadow has no "
"COMPLETE covered run for it. Under split the authoritative shadow is "
@@ -675,7 +713,7 @@ namespace MobileGL::MG_Remote::Server {
// covered level answers, and nothing is fatal: an uncovered box is the caller's to skip.
const Uint8* CoveredBoxBytes(Uint64 key, Uint16 uploadTarget, Uint16 level, Int x, Int y, Uint32 w, Uint32 h,
SizeT texel, Int* rowTexels) const {
if (!m_copies) return nullptr;
if (!Holds()) return nullptr;
const std::lock_guard<std::mutex> lock(m_mutex);
const LevelShadow* shadow = FindLevel(key, uploadTarget, level);
if (shadow == nullptr || !shadow->Defined || x < 0 || y < 0 || w == 0 || h == 0 ||
@@ -684,11 +722,11 @@ namespace MobileGL::MG_Remote::Server {
for (Uint32 row = 0; row < h; ++row) {
const SizeT begin = TexelOffset(shadow->Extent, x, y + static_cast<Int>(row), 0, texel);
const SizeT end = begin + static_cast<SizeT>(w) * texel;
if (end > shadow->Bytes.size() || !StagedShadowStore::CoverageHas(shadow->Covered, begin, end))
if (end > LevelBytesHeld(*shadow) || !StagedShadowStore::CoverageHas(shadow->Covered, begin, end))
return nullptr;
}
*rowTexels = shadow->Extent.x();
return shadow->Bytes.data();
return LevelData(*shadow);
}
// Diagnostics the sync path and the unit cases read, so that a check can assert WHAT
@@ -697,7 +735,7 @@ namespace MobileGL::MG_Remote::Server {
const std::lock_guard<std::mutex> lock(m_mutex);
const LevelShadow* shadow = FindLevel(key, uploadTarget, level);
if (shadow == nullptr || !shadow->Defined || shadow->DeclaredByteBound == 0 ||
shadow->Bytes.size() != shadow->DeclaredByteBound) return false;
LevelBytesHeld(*shadow) != shadow->DeclaredByteBound) return false;
// THE WHOLE LEVEL, not merely the currently assembled prefix: all runs must cover
// the server-derived bound before a reader can treat the level as complete.
return StagedShadowStore::CoverageHas(
@@ -737,7 +775,7 @@ namespace MobileGL::MG_Remote::Server {
const std::lock_guard<std::mutex> lock(m_mutex);
const LevelShadow* shadow = FindLevel(key, uploadTarget, level);
if (shadow == nullptr || !shadow->Defined) return 0;
return shadow->Bytes.size();
return LevelBytesHeld(*shadow);
}
Uint64 LevelDeclaredByteBound(Uint64 key, Uint16 uploadTarget, Uint16 level) const {
if (!m_any.load(std::memory_order_acquire)) return 0;
@@ -820,7 +858,20 @@ namespace MobileGL::MG_Remote::Server {
Bool GpuDirty = false;
// P8-E: FollowCopy wrote server-side texels into Bytes that the client's shadow lacks.
Bool HoldsFollowedCopy = false;
// P13 W4b, the aliasing mode: the level's first byte in the FRONTEND's level shadow,
// learned from the first run that arrived (its address minus its offset in the level).
// Null until then, and nulled with the coordinate system it belongs to. Bytes stays
// empty in this mode; LevelData / LevelBytesHeld are the two readers' spellings.
const Uint8* Alias = nullptr;
};
static const Uint8* LevelData(const LevelShadow& shadow) {
return shadow.Alias != nullptr ? shadow.Alias : shadow.Bytes.data();
}
// How many bytes of the level image this store can hand out: an aliased level is the
// frontend's whole level allocation, which is exactly its declared bound.
static SizeT LevelBytesHeld(const LevelShadow& shadow) {
return shadow.Alias != nullptr ? static_cast<SizeT>(shadow.DeclaredByteBound) : shadow.Bytes.size();
}
struct TextureShadow {
ska::flat_hash_map<Uint32, LevelShadow> Levels;
};
@@ -835,14 +886,14 @@ namespace MobileGL::MG_Remote::Server {
// `InternalFormat == 0`, but 0 is TextureInternalFormat::R8, so every R8 upload read
// as "arrived before the level was declared".
if (!shadow.Defined) {
SessionFail(MGFatalFamily::ProtocolCorruption,
MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.LevelExtent\"} - "
"resource_subdata arrived before the server declared texture level "
"(key=%llu, uploadTarget=%u, level=%u)",
static_cast<unsigned long long>(key), uploadTarget, level);
}
if (!StagedTextureTargetSupportsSubData(shadow.ResourceTarget)) {
SessionFail(MGFatalFamily::ProtocolCorruption,
MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.Target\"} - "
"resource_subdata is not legal for the server-declared texture target "
"(key=%llu, uploadTarget=%u, level=%u, target=%u)",
@@ -850,7 +901,7 @@ namespace MobileGL::MG_Remote::Server {
static_cast<Uint32>(shadow.ResourceTarget));
}
if (shadow.DeclaredByteBound == 0) {
SessionFail(MGFatalFamily::ProtocolCorruption,
MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.LevelBound\"} - "
"resource_subdata into a byteless declared level: its extent is empty or "
"its format has no uncompressed size on this server "
@@ -860,7 +911,7 @@ namespace MobileGL::MG_Remote::Server {
shadow.InternalFormat);
}
if (shadow.Extent == extent) return;
SessionFail(MGFatalFamily::ProtocolCorruption,
MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.LevelExtent\"} - "
"resource_subdata extent disagrees with the server-declared texture level "
"(key=%llu, uploadTarget=%u, level=%u, declared=%d,%d,%d, record=%d,%d,%d)",
@@ -872,12 +923,12 @@ namespace MobileGL::MG_Remote::Server {
// Check the run's exclusive end against the extent recorded by the server's prior
// resource_respecify before any cast, coverage edit, or vector growth.
const Uint8* CopyRunInto(LevelShadow& shadow, Uint64 key, Uint16 uploadTarget, Uint16 level,
Uint64 imageOffset, const void* bytes, SizeT byteSize) {
Uint64 imageOffset, const void* bytes, SizeT byteSize, Bool frontendShadow) {
if (bytes != nullptr && byteSize != 0) {
constexpr Uint64 max = std::numeric_limits<Uint64>::max();
const Uint64 runBytes = static_cast<Uint64>(byteSize);
if (imageOffset > max - runBytes) {
SessionFail(MGFatalFamily::ProtocolCorruption,
MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.CopyRunInto\"} - "
"texture run end overflows (handle=%llu, uploadTarget=%u, level=%u, "
"offset=%llu, size=%llu)",
@@ -888,7 +939,7 @@ namespace MobileGL::MG_Remote::Server {
const Uint64 end = imageOffset + runBytes;
if (end > shadow.DeclaredByteBound ||
end > static_cast<Uint64>(std::numeric_limits<SizeT>::max())) {
SessionFail(MGFatalFamily::ProtocolCorruption,
MG_Pipe::MGPipeRecordFail(MG_Pipe::MGFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.CopyRunInto\"} - "
"texture run exceeds the server-declared level byte bound "
"(handle=%llu, uploadTarget=%u, level=%u, offset=%llu, size=%llu, "
@@ -901,11 +952,35 @@ namespace MobileGL::MG_Remote::Server {
const auto* raw = static_cast<const Uint8*>(bytes);
const SizeT begin = static_cast<SizeT>(imageOffset);
const SizeT needed = static_cast<SizeT>(end);
if (!m_copies && frontendShadow) {
// The aliasing mode: the run IS the frontend level shadow at `begin`, so the
// level's base is one subtraction away and nothing is copied. Every run of one
// level comes from one allocation; a run from another is a level redefined
// without a respecify (or one the server had been holding a copy of), which
// would leave earlier coverage describing other memory - so it starts the
// coverage over.
const Uint8* base = raw - begin;
if (shadow.Alias != base) {
shadow.Covered.clear();
shadow.Bytes.clear();
shadow.Alias = base;
}
StagedShadowStore::CoverageAdd(shadow.Covered, begin, needed);
m_any.store(true, std::memory_order_release);
return shadow.Alias;
}
if (shadow.Alias != nullptr) {
// A server copy landing on an aliased level: the level becomes server-owned, so
// the bytes it already stands for are taken into it first.
shadow.Bytes.assign(shadow.Alias, shadow.Alias + static_cast<SizeT>(shadow.DeclaredByteBound));
shadow.Alias = nullptr;
}
if (shadow.Bytes.size() < needed) shadow.Bytes.resize(needed, 0);
std::memcpy(shadow.Bytes.data() + begin, raw, byteSize);
StagedShadowStore::CoverageAdd(shadow.Covered, begin, needed);
}
m_any.store(true, std::memory_order_release);
if (shadow.Alias != nullptr) return shadow.Alias;
return shadow.Bytes.empty() ? nullptr : shadow.Bytes.data();
}
@@ -927,7 +1002,7 @@ namespace MobileGL::MG_Remote::Server {
// that does not divide evenly is a level this cannot address texel by texel.
static Bool LevelComplete(const LevelShadow& shadow) {
return shadow.Defined && shadow.DeclaredByteBound != 0 &&
shadow.Bytes.size() == shadow.DeclaredByteBound &&
LevelBytesHeld(shadow) == shadow.DeclaredByteBound &&
StagedShadowStore::CoverageHas(shadow.Covered, 0, static_cast<SizeT>(shadow.DeclaredByteBound));
}
static SizeT LevelTexelBytes(const LevelShadow& shadow) {
@@ -963,9 +1038,9 @@ namespace MobileGL::MG_Remote::Server {
// Read on every IsLevelGpuDirty / LevelExtentOrUndefined, so the monolith cost is one
// acquire load of a never-written flag rather than a mutex and two hash lookups.
std::atomic<Bool> m_any{false};
std::atomic<Uint64> m_gpuDirtyClears{0};
};
#if MOBILEGL_BUILD_DISAGGREGATED
// ONE PER PROCESS, and its copying arm is decided ONCE at first use - StagedShadow's
// ServerStaged() ruling verbatim: the two arms hold the authoritative bytes in DIFFERENT
// places, so an answer that changed mid-run would strand every level already staged.
@@ -978,6 +1053,5 @@ namespace MobileGL::MG_Remote::Server {
*new StagedTextureStore(MG_Config::Transport != MG_Config::TransportMode::Monolith);
return store;
}
#endif
} // namespace MobileGL::MG_Remote::Server
} // namespace MobileGL::MG_Record
+57 -1
View File
@@ -46,12 +46,64 @@
#include <dlfcn.h>
#endif
#include <algorithm>
#include <chrono>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#if defined(__ANDROID__)
#include <sys/system_properties.h>
#endif
#if defined(__unix__) || defined(__linux__) || defined(__ANDROID__)
#include <time.h>
#endif
namespace MobileGL::MG_Impl::EGLImpl {
namespace {
// MOBILEGL_FPS_LOG=1 (on Android also the property debug.mobilegl.fps_log=1): one INFO line
// about every two seconds with the frames this process presented, their mean frame time and
// the presenting thread's CPU per frame. A frame rate for an app-hosted game that shows none
// of its own (a launcher's Minecraft with the debug overlay closed, which is itself a cost).
// Off, a swap pays one load and branch.
Bool FrameRateLogEnabled() {
static const Bool enabled = [] {
const char* value = std::getenv("MOBILEGL_FPS_LOG");
#if defined(__ANDROID__)
char property[PROP_VALUE_MAX] = {};
if (value == nullptr && __system_property_get("debug.mobilegl.fps_log", property) > 0) value = property;
#endif
return value != nullptr && value[0] == '1';
}();
return enabled;
}
void NoteSwapForFrameRateLog() {
if (!FrameRateLogEnabled()) return;
using Clock = std::chrono::steady_clock;
static Clock::time_point windowStart = Clock::now();
static Uint32 frames = 0;
static Double cpuStartMs = -1.0;
const auto threadCpuMs = []() -> Double {
#if defined(__unix__) || defined(__linux__) || defined(__ANDROID__)
struct timespec now {};
if (clock_gettime(CLOCK_THREAD_CPUTIME_ID, &now) == 0)
return static_cast<Double>(now.tv_sec) * 1000.0 + static_cast<Double>(now.tv_nsec) / 1e6;
#endif
return -1.0;
};
if (cpuStartMs < 0.0) cpuStartMs = threadCpuMs();
++frames;
const auto now = Clock::now();
const Double elapsedMs = std::chrono::duration<Double, std::milli>(now - windowStart).count();
if (elapsedMs < 2000.0) return;
const Double cpuNow = threadCpuMs();
MGLOG_I("MobileGL fps: %.1f over %u frames (%.2f ms/frame, presenting thread CPU %.2f ms/frame)",
1000.0 * frames / elapsedMs, frames, elapsedMs / frames,
cpuNow >= 0.0 && cpuStartMs >= 0.0 ? (cpuNow - cpuStartMs) / frames : -1.0);
windowStart = now;
frames = 0;
cpuStartMs = cpuNow;
}
// SetDispatcherCurrentApiHook.
std::atomic<EGLenum (*)()> g_dispatcherCurrentApi{nullptr};
using EGLStateContext = MG_State::EGLState::EGLContext;
@@ -766,6 +818,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
// A resize made between frames takes effect for the next one.
ApplyWaylandResize(state, dpy, draw);
#endif
NoteSwapForFrameRateLog();
return EGL_TRUE;
}
@@ -1606,7 +1659,10 @@ namespace MobileGL::MG_Impl::EGLImpl {
#if MOBILEGL_BUILD_DISAGGREGATED
// A split client's backend is the remote one, which forwards the shared-image verbs to
// the server; the server's own apply thread and a monolith have no allocator behind them.
return StreamGateActive() && MG_Backend::pActiveBackendObject != nullptr;
// Nor has inproc: shared images carry memory between processes, and an inproc server is
// this process (its server refuses them too - ServerVerbSink::OnSharedImage).
return StreamGateActive() && MG_Config::Transport != MG_Config::TransportMode::InProcess &&
MG_Backend::pActiveBackendObject != nullptr;
#else
return false;
#endif
@@ -1497,7 +1497,6 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, pointIndex);
#if MOBILEGL_PIPE_PUSH
// P5e (sb, CONTRACT-P5E.md §5.6). THE BIND-POINT GENERATION, and it is bumped HERE -
// before the writes below and after every refusal above - rather than inside
// BindingSlotRange1D, for two reasons. The slot has no back-pointer to the state
@@ -1510,7 +1509,6 @@ namespace MobileGL::MG_Impl::GLImpl {
// The dirty bits this feeds are 15/16/17 (Tracker.h); before P5e they shuttered on the
// buffer CONTENT aggregate, which no bind has ever moved.
MG_State::pGLContext->NoteBufferBindPointChanged(bufferTarget);
#endif
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, pointIndex);
SharedPtr<MG_State::GLState::BufferObject> bufferObject;
@@ -1634,13 +1632,11 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, index);
#if MOBILEGL_PIPE_PUSH
// P5e (sb): the other half of BindBufferBase_State's bump - same generation, same
// placement, same reason. A glBindBufferRange moves the point's EXTENT as well as what
// is bound to it, and the record carries the resolved Offset/Size, so a range change
// with no object change still has to reach the emitter.
MG_State::pGLContext->NoteBufferBindPointChanged(bufferTarget);
#endif
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, index);
SharedPtr<MG_State::GLState::BufferObject> bufferObject;
@@ -12,9 +12,7 @@
#include <MG_State/EGLState/Core.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_Impl/Pipe/PipeFill.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Remote/Client/GpuWritePending.h>
#endif
#include <MG_Impl/Pipe/Verb/GpuWriteSet.h>
// CONTRACT-P5.md §7 / ID-14: a null check on a GLFunctionsTable slot may not survive into the
// client under split - it becomes a caps-mirror read. SlotCaps.h carries the rule and the test
// that decides which of its two spellings a site takes; in a pull build both expand to exactly
@@ -1332,7 +1330,6 @@ namespace MobileGL::MG_Impl::GLImpl {
static_cast<Uint>(bufferIndex));
const auto& buffer = bindingPoint.GetBoundObject();
if (buffer == nullptr) continue;
#if MOBILEGL_BUILD_DISAGGREGATED
// This fixup READS the captured bytes back through the shadow, so it is the one
// consumer that cannot simply inherit the deferral EndTransformFeedback's dropped
// fence introduces. Under split it pays the reconciliation itself, which is the
@@ -1344,8 +1341,7 @@ namespace MobileGL::MG_Impl::GLImpl {
// BeginXfbCaptureForDraw does mark the capture targets - where the fence at the
// caller still runs, so the readback it emits is pure new work on the monolith
// path and integration-gpu cannot see it.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) buffer->SyncGpuWrites();
#endif
buffer->SyncGpuWrites();
const Range1D range = bindingPoint.GetRange();
const Uint8* mapped = buffer->MappedData();
if (mapped == nullptr) continue;
@@ -1386,12 +1382,10 @@ namespace MobileGL::MG_Impl::GLImpl {
MGP_FILL(EndTransformFeedback);
endXfb();
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5 (b1), the second producer the client-side GPU-write set ADDS, and it has to be
// taken HERE - before GLContext::EndTransformFeedback clears the live bindings, since
// a mark taken after it marks nothing.
MG_Remote::Client::MarkEndTransformFeedbackCaptureTargets();
#endif
MG_Record::MarkEndTransformFeedbackCaptureTargets();
MG_State::pGLContext->EndTransformFeedback();
// Captured results must be visible to MapBuffer/GetBufferSubData after
// End; the capture targets are host-coherent GPU memory, so completing
+215 -40
View File
@@ -1307,17 +1307,6 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnMinmaxARB, GLenum target, GLboolean rese
DECLARE_GL_FUNCTION_STUB_HEAD(void, FramebufferSampleLocationsfvARB, GLenum target, GLuint start, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, FramebufferSampleLocationsfvARB, target, start, count, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, NamedFramebufferSampleLocationsfvARB, GLuint framebuffer, GLuint start, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, NamedFramebufferSampleLocationsfvARB, framebuffer, start, count, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, EvaluateDepthValuesARB, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EvaluateDepthValuesARB, )
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteObjectARB, GLhandleARB obj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteObjectARB, obj)
DECLARE_GL_FUNCTION_STUB_HEAD(GLhandleARB, GetHandleARB, GLenum pname) DECLARE_GL_FUNCTION_STUB_END(GLhandleARB, GetHandleARB, pname)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DetachObjectARB, GLhandleARB containerObj, GLhandleARB attachedObj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DetachObjectARB, containerObj, attachedObj)
DECLARE_GL_FUNCTION_STUB_HEAD(GLhandleARB, CreateShaderObjectARB, GLenum shaderType) DECLARE_GL_FUNCTION_STUB_END(GLhandleARB, CreateShaderObjectARB, shaderType)
DECLARE_GL_FUNCTION_STUB_HEAD(GLhandleARB, CreateProgramObjectARB, void) DECLARE_GL_FUNCTION_STUB_END(GLhandleARB, CreateProgramObjectARB, )
DECLARE_GL_FUNCTION_STUB_HEAD(void, AttachObjectARB, GLhandleARB containerObj, GLhandleARB obj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, AttachObjectARB, containerObj, obj)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UseProgramObjectARB, GLhandleARB programObj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UseProgramObjectARB, programObj)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetObjectParameterfvARB, GLhandleARB obj, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetObjectParameterfvARB, obj, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetObjectParameterivARB, GLhandleARB obj, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetObjectParameterivARB, obj, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetInfoLogARB, GLhandleARB obj, GLsizei maxLength, GLsizei* length, GLcharARB* infoLog) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetInfoLogARB, obj, maxLength, length, infoLog)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetAttachedObjectsARB, GLhandleARB containerObj, GLsizei maxCount, GLsizei* count, GLhandleARB* obj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetAttachedObjectsARB, containerObj, maxCount, count, obj)
DECLARE_GL_FUNCTION_STUB_HEAD(void, NamedStringARB, GLenum type, GLint namelen, const GLchar* name, GLint strinen, const GLchar* string) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, NamedStringARB, type, namelen, name, strinen, string)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteNamedStringARB, GLint namelen, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteNamedStringARB, namelen, name)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompileShaderIncludeARB, GLuint shader, GLsizei count, const GLchar* const* path, const GLint* length) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompileShaderIncludeARB, shader, count, path, length)
@@ -1346,35 +1335,6 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, WeightuivARB, GLint size, const GLuint* weig
DECLARE_GL_FUNCTION_STUB_HEAD(void, WeightPointerARB, GLint size, GLenum type, GLsizei stride, const void* pointer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WeightPointerARB, size, type, stride, pointer)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexBlendARB, GLint count) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexBlendARB, count)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetBufferSubDataARB, GLenum target, GLintptrARB offset, GLsizeiptrARB size, void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetBufferSubDataARB, target, offset, size, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib1dARB, GLuint index, GLdouble x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib1dARB, index, x)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib1dvARB, GLuint index, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib1dvARB, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib1sARB, GLuint index, GLshort x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib1sARB, index, x)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib1svARB, GLuint index, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib1svARB, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib2dARB, GLuint index, GLdouble x, GLdouble y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib2dARB, index, x, y)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib2dvARB, GLuint index, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib2dvARB, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib2sARB, GLuint index, GLshort x, GLshort y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib2sARB, index, x, y)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib2svARB, GLuint index, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib2svARB, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib3dARB, GLuint index, GLdouble x, GLdouble y, GLdouble z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib3dARB, index, x, y, z)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib3dvARB, GLuint index, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib3dvARB, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib3sARB, GLuint index, GLshort x, GLshort y, GLshort z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib3sARB, index, x, y, z)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib3svARB, GLuint index, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib3svARB, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4NbvARB, GLuint index, const GLbyte* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4NbvARB, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4NivARB, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4NivARB, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4NsvARB, GLuint index, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4NsvARB, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4NubARB, GLuint index, GLubyte x, GLubyte y, GLubyte z, GLubyte w) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4NubARB, index, x, y, z, w)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4NubvARB, GLuint index, const GLubyte* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4NubvARB, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4NuivARB, GLuint index, const GLuint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4NuivARB, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4NusvARB, GLuint index, const GLushort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4NusvARB, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4bvARB, GLuint index, const GLbyte* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4bvARB, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4dARB, GLuint index, GLdouble x, GLdouble y, GLdouble z, GLdouble w) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4dARB, index, x, y, z, w)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4dvARB, GLuint index, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4dvARB, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4ivARB, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4ivARB, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4sARB, GLuint index, GLshort x, GLshort y, GLshort z, GLshort w) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4sARB, index, x, y, z, w)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4svARB, GLuint index, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4svARB, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4ubvARB, GLuint index, const GLubyte* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4ubvARB, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4uivARB, GLuint index, const GLuint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4uivARB, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttrib4usvARB, GLuint index, const GLushort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttrib4usvARB, index, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexAttribdvARB, GLuint index, GLenum pname, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexAttribdvARB, index, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeArraydvNV, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeArraydvNV, first, count, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeIndexeddNV, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeIndexeddNV, index, n, f)
DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos2dARB, GLdouble x, GLdouble y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos2dARB, x, y)
@@ -2996,6 +2956,221 @@ MOBILEGL_GL_API void glGetnUniformdvARB(GLuint program, GLint location, GLsizei
glGetnUniformdv(program, location, bufSize, params);
}
// ---- GL_ARB_shader_objects --------------------------------------------------------------------
// A handle is a name of the core's one shared shader/program name space: CreateShaderObjectARB and
// CreateProgramObjectARB hand out exactly what glCreateShader / glCreateProgram would, so an ARB
// handle works with every core call and a core name with every ARB call. The calls whose meaning
// is the core one under another name forward to the core entry point; the ones that ask which
// kind of object a handle names (DeleteObjectARB, GetHandleARB, GetObjectParameter*ARB,
// GetInfoLogARB) are GLImpl's (Program/GL_ShaderObjectsARB.cpp). GLhandleARB is GLuint everywhere
// but Apple, where the header makes it a pointer - hence the two conversions.
namespace {
inline GLuint ArbName(GLhandleARB handle) {
#if defined(__APPLE__)
return static_cast<GLuint>(reinterpret_cast<uintptr_t>(handle));
#else
return handle;
#endif
}
inline GLhandleARB ArbHandle(GLuint name) {
#if defined(__APPLE__)
return reinterpret_cast<GLhandleARB>(static_cast<uintptr_t>(name));
#else
return name;
#endif
}
} // namespace
MOBILEGL_GL_API GLhandleARB glCreateShaderObjectARB(GLenum shaderType) { return ArbHandle(glCreateShader(shaderType)); }
MOBILEGL_GL_API GLhandleARB glCreateProgramObjectARB(void) { return ArbHandle(glCreateProgram()); }
MOBILEGL_GL_API void glDeleteObjectARB(GLhandleARB obj) {
const MobileGL::MG_Impl::EGLImpl::GLStreamScope mglStreamScope;
MobileGL::MG_Impl::GLImpl::DeleteObjectARB(ArbName(obj));
}
MOBILEGL_GL_API GLhandleARB glGetHandleARB(GLenum pname) {
const MobileGL::MG_Impl::EGLImpl::GLStreamScope mglStreamScope;
return ArbHandle(MobileGL::MG_Impl::GLImpl::GetHandleARB(pname));
}
MOBILEGL_GL_API void glDetachObjectARB(GLhandleARB containerObj, GLhandleARB attachedObj) {
glDetachShader(ArbName(containerObj), ArbName(attachedObj));
}
MOBILEGL_GL_API void glAttachObjectARB(GLhandleARB containerObj, GLhandleARB obj) {
glAttachShader(ArbName(containerObj), ArbName(obj));
}
MOBILEGL_GL_API void glShaderSourceARB(GLhandleARB shaderObj, GLsizei count, const GLcharARB** string,
const GLint* length) {
glShaderSource(ArbName(shaderObj), count, string, length);
}
MOBILEGL_GL_API void glCompileShaderARB(GLhandleARB shaderObj) { glCompileShader(ArbName(shaderObj)); }
MOBILEGL_GL_API void glLinkProgramARB(GLhandleARB programObj) { glLinkProgram(ArbName(programObj)); }
MOBILEGL_GL_API void glUseProgramObjectARB(GLhandleARB programObj) { glUseProgram(ArbName(programObj)); }
MOBILEGL_GL_API void glValidateProgramARB(GLhandleARB programObj) { glValidateProgram(ArbName(programObj)); }
MOBILEGL_GL_API void glGetObjectParameterfvARB(GLhandleARB obj, GLenum pname, GLfloat* params) {
const MobileGL::MG_Impl::EGLImpl::GLStreamScope mglStreamScope;
MobileGL::MG_Impl::GLImpl::GetObjectParameterfvARB(ArbName(obj), pname, params);
}
MOBILEGL_GL_API void glGetObjectParameterivARB(GLhandleARB obj, GLenum pname, GLint* params) {
const MobileGL::MG_Impl::EGLImpl::GLStreamScope mglStreamScope;
MobileGL::MG_Impl::GLImpl::GetObjectParameterivARB(ArbName(obj), pname, params);
}
MOBILEGL_GL_API void glGetInfoLogARB(GLhandleARB obj, GLsizei maxLength, GLsizei* length, GLcharARB* infoLog) {
const MobileGL::MG_Impl::EGLImpl::GLStreamScope mglStreamScope;
MobileGL::MG_Impl::GLImpl::GetInfoLogARB(ArbName(obj), maxLength, length, infoLog);
}
MOBILEGL_GL_API void glGetAttachedObjectsARB(GLhandleARB containerObj, GLsizei maxCount, GLsizei* count,
GLhandleARB* obj) {
#if defined(__APPLE__)
Vector<GLuint> names(maxCount > 0 ? static_cast<SizeT>(maxCount) : 0);
GLsizei written = 0;
glGetAttachedShaders(ArbName(containerObj), maxCount, &written, names.empty() ? nullptr : names.data());
if (obj != nullptr) {
for (GLsizei i = 0; i < written; ++i) obj[i] = ArbHandle(names[static_cast<SizeT>(i)]);
}
if (count != nullptr) *count = written;
#else
glGetAttachedShaders(ArbName(containerObj), maxCount, count, obj);
#endif
}
MOBILEGL_GL_API GLint glGetUniformLocationARB(GLhandleARB programObj, const GLcharARB* name) {
return glGetUniformLocation(ArbName(programObj), name);
}
MOBILEGL_GL_API void glGetActiveUniformARB(GLhandleARB programObj, GLuint index, GLsizei maxLength, GLsizei* length,
GLint* size, GLenum* type, GLcharARB* name) {
glGetActiveUniform(ArbName(programObj), index, maxLength, length, size, type, name);
}
MOBILEGL_GL_API void glGetUniformfvARB(GLhandleARB programObj, GLint location, GLfloat* params) {
glGetUniformfv(ArbName(programObj), location, params);
}
MOBILEGL_GL_API void glGetUniformivARB(GLhandleARB programObj, GLint location, GLint* params) {
glGetUniformiv(ArbName(programObj), location, params);
}
MOBILEGL_GL_API void glGetShaderSourceARB(GLhandleARB obj, GLsizei maxLength, GLsizei* length, GLcharARB* source) {
glGetShaderSource(ArbName(obj), maxLength, length, source);
}
MOBILEGL_GL_API void glUniform1fARB(GLint location, GLfloat v0) { glUniform1f(location, v0); }
MOBILEGL_GL_API void glUniform2fARB(GLint location, GLfloat v0, GLfloat v1) { glUniform2f(location, v0, v1); }
MOBILEGL_GL_API void glUniform3fARB(GLint location, GLfloat v0, GLfloat v1, GLfloat v2) {
glUniform3f(location, v0, v1, v2);
}
MOBILEGL_GL_API void glUniform4fARB(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3) {
glUniform4f(location, v0, v1, v2, v3);
}
MOBILEGL_GL_API void glUniform1iARB(GLint location, GLint v0) { glUniform1i(location, v0); }
MOBILEGL_GL_API void glUniform2iARB(GLint location, GLint v0, GLint v1) { glUniform2i(location, v0, v1); }
MOBILEGL_GL_API void glUniform3iARB(GLint location, GLint v0, GLint v1, GLint v2) { glUniform3i(location, v0, v1, v2); }
MOBILEGL_GL_API void glUniform4iARB(GLint location, GLint v0, GLint v1, GLint v2, GLint v3) {
glUniform4i(location, v0, v1, v2, v3);
}
MOBILEGL_GL_API void glUniform1fvARB(GLint location, GLsizei count, const GLfloat* value) {
glUniform1fv(location, count, value);
}
MOBILEGL_GL_API void glUniform2fvARB(GLint location, GLsizei count, const GLfloat* value) {
glUniform2fv(location, count, value);
}
MOBILEGL_GL_API void glUniform3fvARB(GLint location, GLsizei count, const GLfloat* value) {
glUniform3fv(location, count, value);
}
MOBILEGL_GL_API void glUniform4fvARB(GLint location, GLsizei count, const GLfloat* value) {
glUniform4fv(location, count, value);
}
MOBILEGL_GL_API void glUniform1ivARB(GLint location, GLsizei count, const GLint* value) {
glUniform1iv(location, count, value);
}
MOBILEGL_GL_API void glUniform2ivARB(GLint location, GLsizei count, const GLint* value) {
glUniform2iv(location, count, value);
}
MOBILEGL_GL_API void glUniform3ivARB(GLint location, GLsizei count, const GLint* value) {
glUniform3iv(location, count, value);
}
MOBILEGL_GL_API void glUniform4ivARB(GLint location, GLsizei count, const GLint* value) {
glUniform4iv(location, count, value);
}
MOBILEGL_GL_API void glUniformMatrix2fvARB(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
glUniformMatrix2fv(location, count, transpose, value);
}
MOBILEGL_GL_API void glUniformMatrix3fvARB(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
glUniformMatrix3fv(location, count, transpose, value);
}
MOBILEGL_GL_API void glUniformMatrix4fvARB(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
glUniformMatrix4fv(location, count, transpose, value);
}
// ---- GL_ARB_vertex_shader -----------------------------------------------------------------------
// Advertised beside ARB_shader_objects (see BackendObject_*'s extension lists); every entry point is
// the core one under the ARB name, and the three that take a program take an ARB handle.
MOBILEGL_GL_API void glVertexAttrib1sARB(GLuint index, GLshort x) { glVertexAttrib1s(index, x); }
MOBILEGL_GL_API void glVertexAttrib1svARB(GLuint index, const GLshort* v) { glVertexAttrib1sv(index, v); }
MOBILEGL_GL_API void glVertexAttrib1fARB(GLuint index, GLfloat x) { glVertexAttrib1f(index, x); }
MOBILEGL_GL_API void glVertexAttrib1fvARB(GLuint index, const GLfloat* v) { glVertexAttrib1fv(index, v); }
MOBILEGL_GL_API void glVertexAttrib1dARB(GLuint index, GLdouble x) { glVertexAttrib1d(index, x); }
MOBILEGL_GL_API void glVertexAttrib1dvARB(GLuint index, const GLdouble* v) { glVertexAttrib1dv(index, v); }
MOBILEGL_GL_API void glVertexAttrib2sARB(GLuint index, GLshort x, GLshort y) { glVertexAttrib2s(index, x, y); }
MOBILEGL_GL_API void glVertexAttrib2svARB(GLuint index, const GLshort* v) { glVertexAttrib2sv(index, v); }
MOBILEGL_GL_API void glVertexAttrib2fARB(GLuint index, GLfloat x, GLfloat y) { glVertexAttrib2f(index, x, y); }
MOBILEGL_GL_API void glVertexAttrib2fvARB(GLuint index, const GLfloat* v) { glVertexAttrib2fv(index, v); }
MOBILEGL_GL_API void glVertexAttrib2dARB(GLuint index, GLdouble x, GLdouble y) { glVertexAttrib2d(index, x, y); }
MOBILEGL_GL_API void glVertexAttrib2dvARB(GLuint index, const GLdouble* v) { glVertexAttrib2dv(index, v); }
MOBILEGL_GL_API void glVertexAttrib3sARB(GLuint index, GLshort x, GLshort y, GLshort z) { glVertexAttrib3s(index, x, y, z); }
MOBILEGL_GL_API void glVertexAttrib3svARB(GLuint index, const GLshort* v) { glVertexAttrib3sv(index, v); }
MOBILEGL_GL_API void glVertexAttrib3fARB(GLuint index, GLfloat x, GLfloat y, GLfloat z) { glVertexAttrib3f(index, x, y, z); }
MOBILEGL_GL_API void glVertexAttrib3fvARB(GLuint index, const GLfloat* v) { glVertexAttrib3fv(index, v); }
MOBILEGL_GL_API void glVertexAttrib3dARB(GLuint index, GLdouble x, GLdouble y, GLdouble z) { glVertexAttrib3d(index, x, y, z); }
MOBILEGL_GL_API void glVertexAttrib3dvARB(GLuint index, const GLdouble* v) { glVertexAttrib3dv(index, v); }
MOBILEGL_GL_API void glVertexAttrib4sARB(GLuint index, GLshort x, GLshort y, GLshort z, GLshort w) { glVertexAttrib4s(index, x, y, z, w); }
MOBILEGL_GL_API void glVertexAttrib4svARB(GLuint index, const GLshort* v) { glVertexAttrib4sv(index, v); }
MOBILEGL_GL_API void glVertexAttrib4fARB(GLuint index, GLfloat x, GLfloat y, GLfloat z, GLfloat w) { glVertexAttrib4f(index, x, y, z, w); }
MOBILEGL_GL_API void glVertexAttrib4fvARB(GLuint index, const GLfloat* v) { glVertexAttrib4fv(index, v); }
MOBILEGL_GL_API void glVertexAttrib4dARB(GLuint index, GLdouble x, GLdouble y, GLdouble z, GLdouble w) { glVertexAttrib4d(index, x, y, z, w); }
MOBILEGL_GL_API void glVertexAttrib4dvARB(GLuint index, const GLdouble* v) { glVertexAttrib4dv(index, v); }
MOBILEGL_GL_API void glVertexAttrib4NubARB(GLuint index, GLubyte x, GLubyte y, GLubyte z, GLubyte w) { glVertexAttrib4Nub(index, x, y, z, w); }
MOBILEGL_GL_API void glVertexAttrib4bvARB(GLuint index, const GLbyte* v) { glVertexAttrib4bv(index, v); }
MOBILEGL_GL_API void glVertexAttrib4ivARB(GLuint index, const GLint* v) { glVertexAttrib4iv(index, v); }
MOBILEGL_GL_API void glVertexAttrib4ubvARB(GLuint index, const GLubyte* v) { glVertexAttrib4ubv(index, v); }
MOBILEGL_GL_API void glVertexAttrib4usvARB(GLuint index, const GLushort* v) { glVertexAttrib4usv(index, v); }
MOBILEGL_GL_API void glVertexAttrib4uivARB(GLuint index, const GLuint* v) { glVertexAttrib4uiv(index, v); }
MOBILEGL_GL_API void glVertexAttrib4NbvARB(GLuint index, const GLbyte* v) { glVertexAttrib4Nbv(index, v); }
MOBILEGL_GL_API void glVertexAttrib4NsvARB(GLuint index, const GLshort* v) { glVertexAttrib4Nsv(index, v); }
MOBILEGL_GL_API void glVertexAttrib4NivARB(GLuint index, const GLint* v) { glVertexAttrib4Niv(index, v); }
MOBILEGL_GL_API void glVertexAttrib4NubvARB(GLuint index, const GLubyte* v) { glVertexAttrib4Nubv(index, v); }
MOBILEGL_GL_API void glVertexAttrib4NusvARB(GLuint index, const GLushort* v) { glVertexAttrib4Nusv(index, v); }
MOBILEGL_GL_API void glVertexAttrib4NuivARB(GLuint index, const GLuint* v) { glVertexAttrib4Nuiv(index, v); }
MOBILEGL_GL_API void glVertexAttribPointerARB(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void* pointer) { glVertexAttribPointer(index, size, type, normalized, stride, pointer); }
MOBILEGL_GL_API void glEnableVertexAttribArrayARB(GLuint index) { glEnableVertexAttribArray(index); }
MOBILEGL_GL_API void glDisableVertexAttribArrayARB(GLuint index) { glDisableVertexAttribArray(index); }
MOBILEGL_GL_API void glGetVertexAttribdvARB(GLuint index, GLenum pname, GLdouble* params) { glGetVertexAttribdv(index, pname, params); }
MOBILEGL_GL_API void glGetVertexAttribfvARB(GLuint index, GLenum pname, GLfloat* params) { glGetVertexAttribfv(index, pname, params); }
MOBILEGL_GL_API void glGetVertexAttribivARB(GLuint index, GLenum pname, GLint* params) { glGetVertexAttribiv(index, pname, params); }
MOBILEGL_GL_API void glGetVertexAttribPointervARB(GLuint index, GLenum pname, void** pointer) { glGetVertexAttribPointerv(index, pname, pointer); }
MOBILEGL_GL_API void glBindAttribLocationARB(GLhandleARB programObj, GLuint index, const GLcharARB* name) {
glBindAttribLocation(ArbName(programObj), index, name);
}
MOBILEGL_GL_API void glGetActiveAttribARB(GLhandleARB programObj, GLuint index, GLsizei maxLength, GLsizei* length, GLint* size, GLenum* type, GLcharARB* name) {
glGetActiveAttrib(ArbName(programObj), index, maxLength, length, size, type, name);
}
MOBILEGL_GL_API GLint glGetAttribLocationARB(GLhandleARB programObj, const GLcharARB* name) {
return glGetAttribLocation(ArbName(programObj), name);
}
// KHR_robustness and EXT_robustness name the same entry points with their own suffixes (an ES
// application, ANGLE's GL backend among them, looks them up by those names).
MOBILEGL_GL_API GLenum glGetGraphicsResetStatusKHR(void) {
@@ -19,14 +19,12 @@
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Remote/Client/GpuWritePending.h>
#endif
#if MOBILEGL_PIPE_PUSH
// P4a, ID-19(c). This file is the ONLY place every DSA framebuffer entry point lives, and the
// emitter it reaches is this package's own header rather than a declaration in one of the
// contract's: MG_Pipe/PipeMutation.h is the door MG_State has into the client and carries no
// framebuffer row, and MG_Impl/GLImpl and MG_Impl/Pipe are the same layer (this file already
// includes MG_Impl/Pipe/PipeFill.h for MGP_FILL).
#include <MG_Impl/Pipe/FramebufferEmit.h>
#endif
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
@@ -624,7 +622,6 @@ namespace MobileGL::MG_Impl::GLImpl {
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, 0, layered);
}
#if MOBILEGL_PIPE_PUSH
// P4a, ID-19(c): ANY FRAMEBUFFER THE SERVER IS ABOUT TO RECEIVE BY NAME HAS A RECORD.
//
// The applier keeps framebuffer records PER OBJECT, keyed by the handle - but before
@@ -652,7 +649,6 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!fbo) return;
MG_Pipe::MGPipeFramebufferEmitterInstance().EmitFramebufferByName(*fbo);
}
#endif
} // namespace
void BlitFramebuffer_Backend(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
@@ -671,10 +667,8 @@ namespace MobileGL::MG_Impl::GLImpl {
MGLOG_E_ONCE("glBlitNamedFramebuffer skipped: backend does not implement explicit framebuffer blit.");
return;
}
#if MOBILEGL_PIPE_PUSH
PipePublishFramebufferByName(readFramebuffer);
PipePublishFramebufferByName(drawFramebuffer);
#endif
MGP_FILL(BlitNamedFramebuffer);
blitNamedFramebuffer(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
dstY1, mask, filter);
@@ -687,9 +681,7 @@ namespace MobileGL::MG_Impl::GLImpl {
MGLOG_E_ONCE("glClearNamedFramebufferfv skipped: backend does not implement explicit framebuffer clear.");
return;
}
#if MOBILEGL_PIPE_PUSH
PipePublishFramebufferByName(framebuffer);
#endif
MGP_FILL(ClearNamedFramebufferfv);
clearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
}
@@ -701,9 +693,7 @@ namespace MobileGL::MG_Impl::GLImpl {
MGLOG_E_ONCE("glClearNamedFramebufferfi skipped: backend does not implement explicit framebuffer clear.");
return;
}
#if MOBILEGL_PIPE_PUSH
PipePublishFramebufferByName(framebuffer);
#endif
MGP_FILL(ClearNamedFramebufferfi);
clearNamedFramebufferfi(framebuffer, buffer, drawbuffer, depth, stencil);
}
@@ -715,9 +705,7 @@ namespace MobileGL::MG_Impl::GLImpl {
MGLOG_E_ONCE("glClearNamedFramebufferiv skipped: backend does not implement explicit framebuffer clear.");
return;
}
#if MOBILEGL_PIPE_PUSH
PipePublishFramebufferByName(framebuffer);
#endif
MGP_FILL(ClearNamedFramebufferiv);
clearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
}
@@ -729,9 +717,7 @@ namespace MobileGL::MG_Impl::GLImpl {
MGLOG_E_ONCE("glClearNamedFramebufferuiv skipped: backend does not implement explicit framebuffer clear.");
return;
}
#if MOBILEGL_PIPE_PUSH
PipePublishFramebufferByName(framebuffer);
#endif
MGP_FILL(ClearNamedFramebufferuiv);
clearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
}
@@ -1460,9 +1446,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (texture == 0) {
framebufferObject->Detach(attachmentType);
#if MOBILEGL_PIPE_PUSH
PipePublishFramebufferByName(framebufferObject);
#endif
return;
}
@@ -1489,9 +1473,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, 0, layered);
#if MOBILEGL_PIPE_PUSH
PipePublishFramebufferByName(framebufferObject);
#endif
}
void NamedFramebufferTextureWithUploadTarget_State(const char* functionName, GLuint framebuffer, GLenum attachment,
@@ -1515,9 +1497,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (texture == 0) {
framebufferObject->Detach(attachmentType);
#if MOBILEGL_PIPE_PUSH
PipePublishFramebufferByName(framebufferObject);
#endif
return;
}
@@ -1545,9 +1525,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level);
#if MOBILEGL_PIPE_PUSH
PipePublishFramebufferByName(framebufferObject);
#endif
}
void NamedFramebufferTexture1D_State(GLuint framebuffer, GLenum attachment, GLenum textarget, GLuint texture,
@@ -1602,9 +1580,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (texture == 0) {
framebufferObject->Detach(attachmentType);
#if MOBILEGL_PIPE_PUSH
PipePublishFramebufferByName(framebufferObject);
#endif
return;
}
@@ -1707,9 +1683,7 @@ namespace MobileGL::MG_Impl::GLImpl {
framebufferObject->AttachTexture(attachmentType, textureObject, textureUploadTarget, level, layer,
/*layered=*/false);
#if MOBILEGL_PIPE_PUSH
PipePublishFramebufferByName(framebufferObject);
#endif
}
void FramebufferRenderbuffer_State(GLenum target, GLenum attachment, GLenum renderbuffertarget,
@@ -1779,9 +1753,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (renderbuffer == 0) {
framebufferObject->Detach(attachmentType);
#if MOBILEGL_PIPE_PUSH
PipePublishFramebufferByName(framebufferObject);
#endif
return;
}
@@ -1791,9 +1763,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!renderbufferObject) return;
framebufferObject->AttachRenderbuffer(attachmentType, renderbufferObject);
#if MOBILEGL_PIPE_PUSH
PipePublishFramebufferByName(framebufferObject);
#endif
}
void DrawBuffersForFramebuffer_State(const SharedPtr<MG_State::GLState::FramebufferObject>& fbo, Bool isDefaultFBO,
@@ -1993,9 +1963,7 @@ namespace MobileGL::MG_Impl::GLImpl {
: GetNamedFramebufferObject_State(framebuffer, "NamedFramebufferDrawBuffers_State");
if (!framebufferObject) return;
DrawBuffersForFramebuffer_State(framebufferObject, framebuffer == 0, n, bufs, false);
#if MOBILEGL_PIPE_PUSH
PipePublishFramebufferByName(framebufferObject);
#endif
}
void NamedFramebufferDrawBuffer_State(GLuint framebuffer, GLenum buf) {
@@ -2010,9 +1978,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const GLenum bufs[] = {buf};
DrawBuffersForFramebuffer_State(framebufferObject, framebuffer == 0, 1, bufs, true);
}
#if MOBILEGL_PIPE_PUSH
PipePublishFramebufferByName(framebufferObject);
#endif
}
void NamedFramebufferReadBuffer_State(GLuint framebuffer, GLenum src) {
@@ -2022,9 +1988,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!framebufferObject) return;
ReadBufferForFramebuffer_State(framebufferObject, framebuffer == 0, src,
"NamedFramebufferReadBuffer_State");
#if MOBILEGL_PIPE_PUSH
PipePublishFramebufferByName(framebufferObject);
#endif
}
SharedPtr<MG_State::GLState::FramebufferObject> GetFramebufferObjectForNamedClear(GLuint framebuffer,
@@ -833,7 +833,8 @@ namespace MobileGL::MG_Impl::GLImpl {
}
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
const auto& s = programObject->GetAttachedShaders();
// GL-visible: a shader detached since the last link is no longer attached (GL 4.6 §7.3).
const auto s = programObject->GetGLVisibleAttachedShaders();
GLsizei c = std::min((GLsizei)s.size(), maxCount);
if (count) *count = c;
for (GLsizei i = 0; i < c; ++i) {
@@ -873,7 +874,7 @@ namespace MobileGL::MG_Impl::GLImpl {
break;
}
case GL_ATTACHED_SHADERS: {
const auto& attachedShaders = programObject->GetAttachedShaders();
const auto attachedShaders = programObject->GetGLVisibleAttachedShaders();
*params = (GLint)attachedShaders.size();
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
break;
@@ -1571,7 +1572,8 @@ namespace MobileGL::MG_Impl::GLImpl {
// version.
if (std::memcmp(pUBO + offset + byteOffsetInsideUniform, value, writeSize) == 0) return;
Memcpy(pUBO + offset + byteOffsetInsideUniform, value, writeSize);
programObject.MarkUBOContentDirty();
programObject.MarkUBOContentDirtyRange(static_cast<Uint>(offset + byteOffsetInsideUniform),
static_cast<Uint>(writeSize));
} else {
const auto& ttype = programObject.GetUniformTypeFacts(location);
if (!ttype.isTexture && !ttype.isImage) return;
@@ -197,4 +197,12 @@ namespace MobileGL::MG_Impl::GLImpl {
void TransformFeedbackVaryings(GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode);
void GetTransformFeedbackVarying(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size,
GLenum* type, GLchar* name);
// GL_ARB_shader_objects (GL_ShaderObjectsARB.cpp): the entry points that ask which kind of object
// a handle names. The rest of the extension is the core calls under ARB names (Definitions.cpp).
void DeleteObjectARB(GLuint obj);
GLuint GetHandleARB(GLenum pname);
void GetObjectParameterivARB(GLuint obj, GLenum pname, GLint* params);
void GetObjectParameterfvARB(GLuint obj, GLenum pname, GLfloat* params);
void GetInfoLogARB(GLuint obj, GLsizei maxLength, GLsizei* length, GLchar* infoLog);
} // namespace MobileGL::MG_Impl::GLImpl
@@ -0,0 +1,138 @@
// MobileGL - MobileGL/MG_Impl/GLImpl/Program/GL_ShaderObjectsARB.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
// GL_ARB_shader_objects: the entry points whose meaning is not just "the core call under another
// name". A handle IS a shader or program name of the core's one shared shader/program name space
// (GL 4.6 §7.1 / §7.3): glCreateShaderObjectARB and glCreateProgramObjectARB hand out exactly what
// glCreateShader / glCreateProgram would, so every core call takes an ARB handle and every ARB call
// takes a core name. What this file adds is the part of the extension that asks "which kind of
// object is this handle" - deleting, querying and reading the info log of either kind through one
// entry point - and the one query of the current program by handle.
#include "GL_Program.h"
#include "../Getter/GL_Getter.h"
#include <MG_State/GLState/Core.h>
#include <string>
namespace MobileGL::MG_Impl::GLImpl {
namespace {
// The ARB tokens that have no core spelling. The rest of GetObjectParameter's pnames ARE the
// core values (OBJECT_DELETE_STATUS_ARB == GL_DELETE_STATUS ... 0x8B80-0x8B88, and
// ARB_vertex_shader's two attribute counts 0x8B89 / 0x8B8A).
constexpr GLenum kProgramObjectArb = 0x8B40;
constexpr GLenum kShaderObjectArb = 0x8B48;
constexpr GLenum kObjectTypeArb = 0x8B4E;
constexpr GLenum kObjectSubtypeArb = 0x8B4F;
enum class ObjectKind { None, Program, Shader };
ObjectKind KindOf(GLuint obj) {
if (obj == 0) return ObjectKind::None;
if (IsProgram(obj)) return ObjectKind::Program;
if (IsShader(obj)) return ObjectKind::Shader;
return ObjectKind::None;
}
void Fail(ErrorCode code, const char* func, const std::string& what) {
MG_State::pGLContext->RecordError(code, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", func, what));
}
} // namespace
// DeleteObjectARB deletes whichever kind the handle names, with the core's deferred-deletion
// rules (a program in use, a shader still attached). Zero is ignored, as for glDeleteShader /
// glDeleteProgram; any other name that is not a shader or program object is INVALID_VALUE.
void DeleteObjectARB(GLuint obj) {
if (obj == 0) return;
switch (KindOf(obj)) {
case ObjectKind::Program: DeleteProgram(obj); return;
case ObjectKind::Shader: DeleteShader(obj); return;
case ObjectKind::None: break;
}
Fail(ErrorCode::InvalidValue, __func__, std::to_string(obj) + " is not a shader or program object.");
}
// GetHandleARB(PROGRAM_OBJECT_ARB) is the program object in use - GL_CURRENT_PROGRAM, 0 when
// none. It is the only pname the extension defines.
GLuint GetHandleARB(GLenum pname) {
if (pname != kProgramObjectArb) {
Fail(ErrorCode::InvalidEnum, __func__, "pname must be GL_PROGRAM_OBJECT_ARB.");
return 0;
}
GLint current = 0;
GetIntegerv(GL_CURRENT_PROGRAM, &current);
return static_cast<GLuint>(current);
}
namespace {
// The ARB spec's table of pnames, per kind. An unknown pname is INVALID_ENUM; a known one the
// object's kind does not have (COMPILE_STATUS of a program, LINK_STATUS of a shader, SUBTYPE
// of a program) is INVALID_OPERATION; a handle that names no shader or program is
// INVALID_VALUE. True when `*out` holds the answer (every pname here is a single value).
bool QueryObjectParameter(GLuint obj, GLenum pname, GLint* out, const char* func) {
bool programParam = false, shaderParam = false;
switch (pname) {
case kObjectTypeArb:
case GL_DELETE_STATUS:
case GL_INFO_LOG_LENGTH: programParam = shaderParam = true; break;
case kObjectSubtypeArb:
case GL_COMPILE_STATUS:
case GL_SHADER_SOURCE_LENGTH: shaderParam = true; break;
case GL_LINK_STATUS:
case GL_VALIDATE_STATUS:
case GL_ATTACHED_SHADERS:
case GL_ACTIVE_UNIFORMS:
case GL_ACTIVE_UNIFORM_MAX_LENGTH:
case GL_ACTIVE_ATTRIBUTES:
case GL_ACTIVE_ATTRIBUTE_MAX_LENGTH: programParam = true; break;
default: Fail(ErrorCode::InvalidEnum, func, "pname is not a GetObjectParameterARB parameter."); return false;
}
const ObjectKind kind = KindOf(obj);
if (kind == ObjectKind::None) {
Fail(ErrorCode::InvalidValue, func, std::to_string(obj) + " is not a shader or program object.");
return false;
}
if ((kind == ObjectKind::Program && !programParam) || (kind == ObjectKind::Shader && !shaderParam)) {
Fail(ErrorCode::InvalidOperation, func, "pname is not a parameter of this kind of object.");
return false;
}
if (pname == kObjectTypeArb) {
*out = static_cast<GLint>(kind == ObjectKind::Program ? kProgramObjectArb : kShaderObjectArb);
} else if (pname == kObjectSubtypeArb) {
GetShaderiv(obj, GL_SHADER_TYPE, out);
} else if (kind == ObjectKind::Program) {
GetProgramiv(obj, pname, out);
} else {
GetShaderiv(obj, pname, out);
}
return true;
}
} // namespace
void GetObjectParameterivARB(GLuint obj, GLenum pname, GLint* params) {
GLint value = 0;
if (QueryObjectParameter(obj, pname, &value, __func__) && params != nullptr) *params = value;
}
void GetObjectParameterfvARB(GLuint obj, GLenum pname, GLfloat* params) {
GLint value = 0;
if (QueryObjectParameter(obj, pname, &value, __func__) && params != nullptr) *params = static_cast<GLfloat>(value);
}
void GetInfoLogARB(GLuint obj, GLsizei maxLength, GLsizei* length, GLchar* infoLog) {
switch (KindOf(obj)) {
case ObjectKind::Program: GetProgramInfoLog(obj, maxLength, length, infoLog); return;
case ObjectKind::Shader: GetShaderInfoLog(obj, maxLength, length, infoLog); return;
case ObjectKind::None: break;
}
Fail(ErrorCode::InvalidValue, __func__, std::to_string(obj) + " is not a shader or program object.");
}
} // namespace MobileGL::MG_Impl::GLImpl
+9 -54
View File
@@ -49,11 +49,7 @@
// SyncGpuWrites, exactly as glMapBufferRange and glGetBufferSubData already do. Monolith keeps its
// read as it was: its ReadPixels maps the driver PBO back into the shadow inside the call, and the
// pull build compiles this to nothing (G1).
#if MOBILEGL_BUILD_DISAGGREGATED
#define MGL_SYNC_PIXEL_UNPACK_SOURCE(buffer) do { if (MG_Config::Transport != MG_Config::TransportMode::Monolith) (buffer)->SyncGpuWrites(); } while (0)
#else
#define MGL_SYNC_PIXEL_UNPACK_SOURCE(buffer) ((void)0)
#endif
#define MGL_SYNC_PIXEL_UNPACK_SOURCE(buffer) do { (buffer)->SyncGpuWrites(); } while (0)
namespace MobileGL::MG_Impl::GLImpl {
static SharedPtr<MG_State::GLState::ITextureObject> nullTextureObject;
@@ -1376,7 +1372,6 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
return;
}
#if MOBILEGL_PIPE_PUSH
// P4a, ID-18 M2 - THE THIRTEENTH MGP_NOTE_AGGREGATE(TextureParams) SITE, and the one
// no publisher reached. Nine of the thirteen are TextureObject.cpp's own mutators and
// publish through TextureObjectBase::PipePublishParams; the tenth is
@@ -1399,7 +1394,6 @@ namespace MobileGL::MG_Impl::GLImpl {
// free: the emitter's version-first skip reads GetTextureParamsVersion() AND
// SamplerObject::GetVersion() and returns without hashing anything when neither moved.
MobileGL::MG_Pipe::MGPipeEmitTextureParams(*textureObject);
#endif
}
void TextureParameterObjectf_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, GLenum pname,
@@ -1478,7 +1472,6 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
return;
}
#if MOBILEGL_PIPE_PUSH
// P4a, ID-18 M2 - THE THIRTEENTH MGP_NOTE_AGGREGATE(TextureParams) SITE, and the one
// no publisher reached. Nine of the thirteen are TextureObject.cpp's own mutators and
// publish through TextureObjectBase::PipePublishParams; the tenth is
@@ -1501,7 +1494,6 @@ namespace MobileGL::MG_Impl::GLImpl {
// free: the emitter's version-first skip reads GetTextureParamsVersion() AND
// SamplerObject::GetVersion() and returns without hashing anything when neither moved.
MobileGL::MG_Pipe::MGPipeEmitTextureParams(*textureObject);
#endif
}
void GetTextureParameterObjectiv_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
@@ -2197,7 +2189,6 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
return;
}
#if MOBILEGL_PIPE_PUSH
// P4a, ID-18 M2 - THE THIRTEENTH MGP_NOTE_AGGREGATE(TextureParams) SITE, and the one
// no publisher reached. Nine of the thirteen are TextureObject.cpp's own mutators and
// publish through TextureObjectBase::PipePublishParams; the tenth is
@@ -2220,7 +2211,6 @@ namespace MobileGL::MG_Impl::GLImpl {
// free: the emitter's version-first skip reads GetTextureParamsVersion() AND
// SamplerObject::GetVersion() and returns without hashing anything when neither moved.
MobileGL::MG_Pipe::MGPipeEmitTextureParams(*textureObject);
#endif
}
void TexParameteri_State(GLenum target, GLenum pname, GLint param) {
@@ -6127,10 +6117,7 @@ namespace MobileGL::MG_Impl::GLImpl {
viewObject->SetInternalFormat(viewInternalFormat);
viewObject->SetSamples(storageOwner->GetSamples());
viewObject->SetFixedSampleLocations(storageOwner->HasFixedSampleLocations());
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith)
MG_Pipe::MGPipeEmitSamplerViewCreate(*viewObject);
#endif
MG_Pipe::MGPipeEmitSamplerViewCreate(*viewObject);
}
void TexStorage1D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width) {
@@ -6592,23 +6579,10 @@ namespace MobileGL::MG_Impl::GLImpl {
static void GetTextureImageForUploadTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
TextureUploadTarget uploadTarget, GLint level, GLenum format,
GLenum type, GLsizei bufSize, void* pixels, const char* caller) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
MGP_FILL(GetTextureImage);
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
bufSize, pixels);
return;
}
#endif
if (MG_Backend::pActiveBackendObject != nullptr &&
MG_Backend::pActiveBackendObject->GetBackendType() == BackendType::DirectVulkan &&
MGL_BACKEND_SLOT_LOCAL(GetTextureImage)) {
MGP_FILL(GetTextureImage);
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
bufSize, pixels);
return;
}
CopyTextureImageToClientOrPBO_State(textureObject, uploadTarget, level, format, type, bufSize, pixels, caller);
MGP_FILL(GetTextureImage);
MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
bufSize, pixels);
return;
}
void GetTextureImage(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels) {
@@ -6620,12 +6594,9 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
auto uploadTarget = GetPrimaryUploadTarget(textureObject);
#if MOBILEGL_BUILD_DISAGGREGATED
// Unlike glGetTexImage's face target, this DSA call reads all six faces.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
textureObject->GetTarget() == TextureTarget::TextureCubeMap)
if (textureObject->GetTarget() == TextureTarget::TextureCubeMap)
uploadTarget = TextureUploadTarget::Unknown;
#endif
GetTextureImageForUploadTarget(textureObject, uploadTarget, level, format, type,
bufSize, pixels, __func__);
}
@@ -6815,7 +6786,6 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit))
.Bind(textureObject, level, layered, layer, access, format);
MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit));
#if MOBILEGL_PIPE_PUSH
// AND THE IMAGE-UNIT MARK BESIDE IT. The line above moves the TEXTURE-unit high-water
// mark, which is a different array: a reader that needs "the highest image unit ever
// bound" cannot take it from there without either over-walking (a texture bind at unit
@@ -6824,7 +6794,6 @@ namespace MobileGL::MG_Impl::GLImpl {
// (MG_Remote/Client/GpuWritePending.cpp). Push builds only, so the pull build's bytes do
// not move (G1).
MG_State::pGLContext->NoteImageUnitTouched(static_cast<Int>(unit));
#endif
MGP_FILL(BindImageTexture);
bindImageTexture(unit, texture, level, layered, layer, access, format);
}
@@ -6888,22 +6857,8 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
if (!GetTexImage_State(target, level, format, type, pixels)) return;
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
GetTexImage_Backend(target, level, format, type, pixels);
return;
}
#endif
if (MGL_BACKEND_SLOT_LOCAL(GetTexImage)) {
GetTexImage_Backend(target, level, format, type, pixels);
return;
}
TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
const auto& textureObject = activeUnit.GetBindingSlot(textureTarget).GetBoundObject();
CopyTextureImageToClientOrPBO_State(textureObject, textureUploadTarget, level, format, type, -1, pixels,
__func__);
GetTexImage_Backend(target, level, format, type, pixels);
return;
}
void GetInternalformativ(GLenum target, GLenum internalformat, GLenum pname, GLsizei bufSize, GLint* params) {
+87
View File
@@ -824,6 +824,93 @@ namespace MobileGL::MG_Impl {
GETPROC(glMultiDrawArraysIndirectCount, name);
GETPROC(glMultiDrawElementsIndirectCount, name);
GETPROC(glPolygonOffsetClamp, name);
// GL_ARB_shader_objects (Definitions.cpp): advertised, so every entry point resolves.
GETPROC(glDeleteObjectARB, name);
GETPROC(glGetHandleARB, name);
GETPROC(glDetachObjectARB, name);
GETPROC(glCreateShaderObjectARB, name);
GETPROC(glShaderSourceARB, name);
GETPROC(glCompileShaderARB, name);
GETPROC(glCreateProgramObjectARB, name);
GETPROC(glAttachObjectARB, name);
GETPROC(glLinkProgramARB, name);
GETPROC(glUseProgramObjectARB, name);
GETPROC(glValidateProgramARB, name);
GETPROC(glUniform1fARB, name);
GETPROC(glUniform2fARB, name);
GETPROC(glUniform3fARB, name);
GETPROC(glUniform4fARB, name);
GETPROC(glUniform1iARB, name);
GETPROC(glUniform2iARB, name);
GETPROC(glUniform3iARB, name);
GETPROC(glUniform4iARB, name);
GETPROC(glUniform1fvARB, name);
GETPROC(glUniform2fvARB, name);
GETPROC(glUniform3fvARB, name);
GETPROC(glUniform4fvARB, name);
GETPROC(glUniform1ivARB, name);
GETPROC(glUniform2ivARB, name);
GETPROC(glUniform3ivARB, name);
GETPROC(glUniform4ivARB, name);
GETPROC(glUniformMatrix2fvARB, name);
GETPROC(glUniformMatrix3fvARB, name);
GETPROC(glUniformMatrix4fvARB, name);
GETPROC(glGetObjectParameterfvARB, name);
GETPROC(glGetObjectParameterivARB, name);
GETPROC(glGetInfoLogARB, name);
GETPROC(glGetAttachedObjectsARB, name);
GETPROC(glGetUniformLocationARB, name);
GETPROC(glGetActiveUniformARB, name);
GETPROC(glGetUniformfvARB, name);
GETPROC(glGetUniformivARB, name);
GETPROC(glGetShaderSourceARB, name);
// GL_ARB_vertex_shader (Definitions.cpp): advertised, so every entry point resolves.
GETPROC(glVertexAttrib1sARB, name);
GETPROC(glVertexAttrib1svARB, name);
GETPROC(glVertexAttrib1fARB, name);
GETPROC(glVertexAttrib1fvARB, name);
GETPROC(glVertexAttrib1dARB, name);
GETPROC(glVertexAttrib1dvARB, name);
GETPROC(glVertexAttrib2sARB, name);
GETPROC(glVertexAttrib2svARB, name);
GETPROC(glVertexAttrib2fARB, name);
GETPROC(glVertexAttrib2fvARB, name);
GETPROC(glVertexAttrib2dARB, name);
GETPROC(glVertexAttrib2dvARB, name);
GETPROC(glVertexAttrib3sARB, name);
GETPROC(glVertexAttrib3svARB, name);
GETPROC(glVertexAttrib3fARB, name);
GETPROC(glVertexAttrib3fvARB, name);
GETPROC(glVertexAttrib3dARB, name);
GETPROC(glVertexAttrib3dvARB, name);
GETPROC(glVertexAttrib4sARB, name);
GETPROC(glVertexAttrib4svARB, name);
GETPROC(glVertexAttrib4fARB, name);
GETPROC(glVertexAttrib4fvARB, name);
GETPROC(glVertexAttrib4dARB, name);
GETPROC(glVertexAttrib4dvARB, name);
GETPROC(glVertexAttrib4NubARB, name);
GETPROC(glVertexAttrib4bvARB, name);
GETPROC(glVertexAttrib4ivARB, name);
GETPROC(glVertexAttrib4ubvARB, name);
GETPROC(glVertexAttrib4usvARB, name);
GETPROC(glVertexAttrib4uivARB, name);
GETPROC(glVertexAttrib4NbvARB, name);
GETPROC(glVertexAttrib4NsvARB, name);
GETPROC(glVertexAttrib4NivARB, name);
GETPROC(glVertexAttrib4NubvARB, name);
GETPROC(glVertexAttrib4NusvARB, name);
GETPROC(glVertexAttrib4NuivARB, name);
GETPROC(glVertexAttribPointerARB, name);
GETPROC(glEnableVertexAttribArrayARB, name);
GETPROC(glDisableVertexAttribArrayARB, name);
GETPROC(glGetVertexAttribdvARB, name);
GETPROC(glGetVertexAttribfvARB, name);
GETPROC(glGetVertexAttribivARB, name);
GETPROC(glGetVertexAttribPointervARB, name);
GETPROC(glBindAttribLocationARB, name);
GETPROC(glGetActiveAttribARB, name);
GETPROC(glGetAttribLocationARB, name);
GETPROC(glPrimitiveBoundingBoxARB, name);
GETPROC(glGetTextureHandleARB, name);
GETPROC(glGetTextureSamplerHandleARB, name);
+1 -3
View File
@@ -54,7 +54,7 @@
// THE CONTEXT ID IS GLContext::GetTextureContextId() AND NOTHING ELSE - the tree's existing
// never-reused per-context id (TextureState::AllocateContextId; PipeInputs carries it as
// m_textureContextId at seven fill points and the backends' own per-context memos key on it).
// Deliberately NOT the GLContext ADDRESS that MGB_CTX_IDENTITY and MGPipeTracker::m_context
// Deliberately NOT the GLContext ADDRESS that gPipeInputs and MGPipeTracker::m_context
// compare, because Core.h states the reason that id exists at all: a context freed and remade
// lands on the old heap address, which would put this same defect back one context recreation
// later.
@@ -81,7 +81,6 @@
// one and this one depends on nothing of the family's. The reverse arrangement would make the
// resolver reachable only from a translation unit that had already decided to use it, i.e.
// dead in the build that matters and live only in the tests.
#if MOBILEGL_PIPE_PUSH
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/PipeMutation.h>
@@ -267,4 +266,3 @@ namespace MobileGL::MG_Pipe {
return *resolver;
}
} // namespace MobileGL::MG_Pipe
#endif // MOBILEGL_PIPE_PUSH
-2
View File
@@ -38,7 +38,6 @@
//
// Header-only for the same ownership reason as Tracker.h: the root CMakeLists.txt that
// would name a new .cpp is package A's and is frozen behind the p2/contract tag.
#if MOBILEGL_PIPE_PUSH
#include <Config.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Pipe/MGPipe.h>
@@ -203,4 +202,3 @@ namespace MobileGL::MG_Pipe {
return *cache;
}
} // namespace MobileGL::MG_Pipe
#endif // MOBILEGL_PIPE_PUSH
+130 -5
View File
@@ -25,7 +25,6 @@
// HEADER-ONLY, for the ownership reason Tracker.h and ResourceTracker.h both state: the root
// CMakeLists.txt that would name a new .cpp is the contract package's and is frozen behind the
// tag. MG_Impl/Pipe/PipeFill.cpp is the one translation unit that includes it in the library.
#if MOBILEGL_PIPE_PUSH
#include <MG_Impl/Pipe/SetHashSuppressor.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Impl/Pipe/TextureEmit.h>
@@ -276,13 +275,13 @@ namespace MobileGL::MG_Pipe {
Bool drawOk = false;
Bool readOk = false;
if (shared) {
drawOk = BuildFramebufferState(*drawFbo, MGPipeFramebufferTarget::Both, drawState);
drawOk = BuildBoundFramebufferState(*drawFbo, MGPipeFramebufferTarget::Both, drawState);
} else {
if (drawFbo) {
drawOk = BuildFramebufferState(*drawFbo, MGPipeFramebufferTarget::Draw, drawState);
drawOk = BuildBoundFramebufferState(*drawFbo, MGPipeFramebufferTarget::Draw, drawState);
}
if (readFbo) {
readOk = BuildFramebufferState(*readFbo, MGPipeFramebufferTarget::Read, readState);
readOk = BuildBoundFramebufferState(*readFbo, MGPipeFramebufferTarget::Read, readState);
}
}
if (!drawOk && !readOk) return 0;
@@ -415,6 +414,8 @@ namespace MobileGL::MG_Pipe {
const MGPFramebufferState& LastNamed() const { return m_lastNamed; }
Uint64 EmissionCount() const { return m_emissions; }
Uint64 RefusedCount() const { return m_refusals; }
// P15: bound-target builds answered from the memo (BuildBoundFramebufferState).
Uint64 MemoHitCount() const { return m_memoHits; }
// A fresh context: what the server has is no longer what this emitter last sent. Only
// LATCHES reset here - MGPipeApplierReset clears the applier's DrawFramebuffer and
@@ -439,6 +440,7 @@ namespace MobileGL::MG_Pipe {
void ResetForTest() {
Reset();
ResetCounters();
for (auto& memo : m_buildMemo) memo.Valid = false;
m_lastDraw = MGPFramebufferState{};
m_lastRead = MGPFramebufferState{};
m_lastNamed = MGPFramebufferState{};
@@ -564,6 +566,40 @@ namespace MobileGL::MG_Pipe {
return true;
}
// BuildFramebufferState for a BOUND target, reusing the last record built for the same
// framebuffer and target while nothing the build reads has moved. The build runs per draw
// and its answer is a function of:
// - the framebuffer: lifetime id (identity) and object version (attachments, draw and
// read buffers, no-attachment defaults all move it);
// - each attached texture: lifetime id, shape version (sizes, level set, internal
// format: completeness and geometry) and its samples / fixed-locations pair, which
// move the params version rather than the shape;
// - each attached renderbuffer: lifetime id plus its size, format and samples, read
// directly (its storage has no version);
// - the handles it minted: the slot allocator's reset epoch, and the texture emitter's
// table epoch for the sticky bind bits SurfaceOf notes (a dropped table must be
// re-noted).
// Not for the default framebuffer, whose geometry follows the current surface.
// Kill switch MOBILEGL_PIPE_FRAMEBUFFER_STATE_MEMO=0.
Bool BuildBoundFramebufferState(const FramebufferObject& fbo, MGPipeFramebufferTarget target,
MGPFramebufferState& out) {
const Bool memoOn = MG_Config::Features.PipeFramebufferStateMemo != MG_Config::QuirkOverride::ForceOff &&
!fbo.IsDefaultFramebuffer();
// Direct-mapped on (framebuffer, target): the build only runs when the bound framebuffer
// changed, so a per-target single entry would miss on every A/B/A switch between targets.
const Uint64 key = fbo.GetLifetimeId() * 4u + static_cast<Uint64>(target);
BuildMemo& memo = m_buildMemo[static_cast<SizeT>((key * 0x9E3779B97F4A7C15ull) >> 60) % m_buildMemo.size()];
if (memoOn && memo.Valid && MemoHolds(memo, fbo, target)) {
out = memo.State;
++m_memoHits;
return true;
}
memo.Valid = false;
if (!BuildFramebufferState(fbo, target, out)) return false;
if (memoOn) FillMemo(memo, fbo, target, out);
return true;
}
static Bool IsBoundTo(const FramebufferObject& fbo, MobileGL::FramebufferTarget target) {
if (MG_State::pGLContext == nullptr) return false;
const auto& bound = MG_State::pGLContext->GetFramebufferBindingSlot(target).GetBoundObject();
@@ -643,6 +679,96 @@ namespace MobileGL::MG_Pipe {
out.FixedSampleLocations = fbo.GetDefaultFixedSampleLocations() ? 1 : 0;
}
struct MemoAttachment {
Uint32 Point = 0;
Bool IsTexture = false;
Uint64 LifetimeId = 0;
Uint64 ShapeVersion = 0;
Int Width = 0, Height = 0, Samples = 0;
Uint32 Format = 0;
Bool FixedSampleLocations = false;
};
struct BuildMemo {
Bool Valid = false;
Uint8 Target = 0;
Uint16 ObjectVersion = 0;
Uint64 FboLifetimeId = 0;
Uint64 AllocatorEpoch = 0;
Uint64 TextureTableEpoch = 0;
Uint32 AttachmentCount = 0;
Array<MemoAttachment, static_cast<SizeT>(FramebufferAttachmentType::FramebufferAttachmentTypeCount)>
Attachments{};
MGPFramebufferState State{};
};
static Bool AttachmentMatches(const MemoAttachment& entry,
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (attachment.IsEmpty() || attachment.IsTexture() != entry.IsTexture) return false;
if (entry.IsTexture) {
const auto& texture = attachment.GetTexture();
return texture->GetLifetimeId() == entry.LifetimeId &&
texture->GetShapeVersion() == entry.ShapeVersion &&
texture->GetSamples() == entry.Samples &&
texture->HasFixedSampleLocations() == entry.FixedSampleLocations;
}
const auto& renderbuffer = attachment.GetRenderbuffer();
return renderbuffer->GetLifetimeId() == entry.LifetimeId && renderbuffer->GetWidth() == entry.Width &&
renderbuffer->GetHeight() == entry.Height && renderbuffer->GetSamples() == entry.Samples &&
static_cast<Uint32>(renderbuffer->GetInternalFormat()) == entry.Format;
}
Bool MemoHolds(const BuildMemo& memo, const FramebufferObject& fbo, MGPipeFramebufferTarget target) const {
if (memo.Target != static_cast<Uint8>(target) || memo.FboLifetimeId != fbo.GetLifetimeId() ||
memo.ObjectVersion != fbo.GetObjectVersion() || memo.AllocatorEpoch != MGPipeSlots().ResetEpoch() ||
memo.TextureTableEpoch != MGPipeTextureEmitterInstance().TableEpoch()) {
return false;
}
const auto& all = fbo.GetAllAttachmentObjects();
for (Uint32 i = 0; i < memo.AttachmentCount; ++i) {
const MemoAttachment& entry = memo.Attachments[i];
if (!AttachmentMatches(entry, all[entry.Point])) return false;
}
return true;
}
void FillMemo(BuildMemo& memo, const FramebufferObject& fbo, MGPipeFramebufferTarget target,
const MGPFramebufferState& state) {
memo.Target = static_cast<Uint8>(target);
memo.FboLifetimeId = fbo.GetLifetimeId();
memo.ObjectVersion = fbo.GetObjectVersion();
memo.AllocatorEpoch = MGPipeSlots().ResetEpoch();
memo.TextureTableEpoch = MGPipeTextureEmitterInstance().TableEpoch();
memo.AttachmentCount = 0;
const auto& all = fbo.GetAllAttachmentObjects();
for (SizeT point = 0; point < all.size(); ++point) {
const auto& attachment = all[point];
if (attachment.IsEmpty()) continue;
MemoAttachment& entry = memo.Attachments[memo.AttachmentCount++];
entry = MemoAttachment{};
entry.Point = static_cast<Uint32>(point);
entry.IsTexture = attachment.IsTexture();
if (entry.IsTexture) {
const auto& texture = attachment.GetTexture();
entry.LifetimeId = texture->GetLifetimeId();
entry.ShapeVersion = texture->GetShapeVersion();
entry.Samples = texture->GetSamples();
entry.FixedSampleLocations = texture->HasFixedSampleLocations();
} else {
const auto& renderbuffer = attachment.GetRenderbuffer();
entry.LifetimeId = renderbuffer->GetLifetimeId();
entry.Width = renderbuffer->GetWidth();
entry.Height = renderbuffer->GetHeight();
entry.Samples = renderbuffer->GetSamples();
entry.Format = static_cast<Uint32>(renderbuffer->GetInternalFormat());
}
}
memo.State = state;
memo.Valid = true;
}
// Direct-mapped on (framebuffer lifetime id, target); Named never reaches it.
Array<BuildMemo, 16> m_buildMemo{};
Uint64 m_memoHits = 0;
Array<Uint64, 2> m_lastEmitted{};
// The per-FRAMEBUFFER suppressor for Named records, slot-indexed with the generation
// checked, exactly as the applier's own table is. A framebuffer has no wire lifetime
@@ -663,4 +789,3 @@ namespace MobileGL::MG_Pipe {
return *emitter;
}
} // namespace MobileGL::MG_Pipe
#endif // MOBILEGL_PIPE_PUSH
-2
View File
@@ -32,7 +32,6 @@
//
// THIS FILE IS CREATED BY THE CONTRACT COMMIT AND FILLED BY THE PACKAGE THAT OWNS IT - see
// FramebufferEmit.h for why, in full.
#if MOBILEGL_PIPE_PUSH
#include <MG_Impl/Pipe/SamplerEmit.h>
#include <MG_Impl/Pipe/SetHashSuppressor.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
@@ -195,4 +194,3 @@ namespace MobileGL::MG_Pipe {
return *emitter;
}
} // namespace MobileGL::MG_Pipe
#endif // MOBILEGL_PIPE_PUSH
+1 -2
View File
@@ -9,7 +9,6 @@
// Client memory becomes ordinary owned buffer resources before a draw.
#pragma once
#if MOBILEGL_BUILD_DISAGGREGATED && MOBILEGL_PIPE_PUSH
#include <MG_Impl/Pipe/ClientFetchPlan.h>
#include <MG_Impl/Pipe/VertexInputEmit.h>
#include <MG_State/GLState/BufferState/BufferObject.h>
@@ -60,6 +59,7 @@ namespace MobileGL::MG_Pipe {
binding.Res = info.IndexResource;
binding.IndexSize = info.IndexSize;
MGPipeRouteSetIndexBuffer(binding);
MGPipeVertexInputEmitterInstance().NoteIndexBufferRoutedElsewhere(m_context);
m_indexBindingChanged = true;
}
@@ -207,4 +207,3 @@ namespace MobileGL::MG_Pipe {
Bool m_indexBindingChanged = false;
};
} // namespace MobileGL::MG_Pipe
#endif
+250 -227
View File
@@ -39,7 +39,12 @@
#include <MG_Pipe/PipeApply.h>
#include <MG_Pipe/PipeRoute.h>
#include <MG_Pipe/PipeMutation.h>
#include <MG_Pipe/VerbRecordOps.h>
#include <Config.h>
#if MOBILEGL_PIPE_VERIFY
#include <MG_Impl/Pipe/Verb/VerbPort.h>
#include <MG_Impl/Pipe/Verb/GpuWriteSet.h>
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// R-8 (c1): the client's liveness gates read the caps mirror, never MGPipeGetResourceOps().
@@ -93,7 +98,6 @@ namespace MobileGL::MG_Pipe {
return inputs.m_currentVertexAttribute;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (ra, CONTRACT-P5E §2.3). THE FOUR O-CLASS ROWS, AND ONLY THEY: these are the only
// members of this block that own a frontend object rather than point into one, so they
// are the only ones through which the apply thread can become a last owner. The raw
@@ -105,7 +109,6 @@ namespace MobileGL::MG_Pipe {
inputs.m_programForDraw.reset();
inputs.m_transformFeedbackProgram.reset();
}
#endif
static void CopyField(PipeInputs& dst, GLContext& ctx, MGPipeInputField field) {
using F = MGPipeInputField;
@@ -132,8 +135,11 @@ namespace MobileGL::MG_Pipe {
case F::GetBoundTransformFeedbackName:
dst.m_boundTransformFeedbackName = ctx.GetBoundTransformFeedbackName();
break;
// The four object pins: assigned only when the object moved, so the residual fill of
// every draw does not pay a reference-count increment and decrement for a pointer it
// already holds (P14: one of these moves per Minecraft draw, the other three rarely).
case F::GetBoundVertexArray:
dst.m_boundVertexArray = ctx.GetBoundVertexArray();
if (dst.m_boundVertexArray != ctx.GetBoundVertexArray()) dst.m_boundVertexArray = ctx.GetBoundVertexArray();
break;
case F::GetBufferBindingSlot:
// Every global target has a slot; Index stays null - GLContext resolves it
@@ -246,10 +252,11 @@ namespace MobileGL::MG_Pipe {
dst.m_primitiveRestartIndex = ctx.GetPrimitiveRestartIndex();
break;
case F::GetProgramForDispatch:
dst.m_programForDispatch = ctx.GetProgramForDispatch();
if (dst.m_programForDispatch != ctx.GetProgramForDispatch())
dst.m_programForDispatch = ctx.GetProgramForDispatch();
break;
case F::GetProgramForDraw:
dst.m_programForDraw = ctx.GetProgramForDraw();
if (dst.m_programForDraw != ctx.GetProgramForDraw()) dst.m_programForDraw = ctx.GetProgramForDraw();
break;
case F::GetProvokingVertexMode:
dst.m_provokingVertexMode = ctx.GetProvokingVertexMode();
@@ -290,7 +297,8 @@ namespace MobileGL::MG_Pipe {
dst.m_transformFeedbackPausedPrimitiveCounter = ctx.GetTransformFeedbackPausedPrimitiveCounter();
break;
case F::GetTransformFeedbackProgram:
dst.m_transformFeedbackProgram = ctx.GetTransformFeedbackProgram();
if (dst.m_transformFeedbackProgram != ctx.GetTransformFeedbackProgram())
dst.m_transformFeedbackProgram = ctx.GetTransformFeedbackProgram();
break;
case F::GetViewport:
dst.m_viewport = ctx.GetViewport();
@@ -352,6 +360,10 @@ namespace MobileGL::MG_Pipe {
namespace {
GLContext* LiveContext() { return MG_State::pGLContext.get(); }
#if !MOBILEGL_BUILD_DISAGGREGATED
// P13 W5: no transport, no run-ahead client.
Bool ClientRunsAhead() { return false; }
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// ---- P5e (ra): the fill decision (CONTRACT-P5E §3.1) -----------------------------
//
@@ -365,85 +377,57 @@ namespace MobileGL::MG_Pipe {
return session != nullptr && session->RunAheadArmed();
}
// THE WIRE OP A VERB BOUNDARY BECOMES, which is the join MGPipeVerbForWireOp draws in
// the other direction. Built by walking the op space once at compile time rather than
// written out, because a second hand-written table is a second thing to forget a row
// in - and the generator already refuses a verb-shaped op with no row.
constexpr MGPWireOp WireOpForVerb(MGPipeVerb verb) {
for (SizeT i = 0; i < static_cast<SizeT>(MGPWireOp::kOpCount); ++i) {
const auto op = static_cast<MGPWireOp>(i);
if (MGPipeVerbForWireOp(op) == verb) return op;
}
return MGPWireOp::kOpCount;
}
// CONTRACT-P5E §2.1's predicate, ASKED AT THE VALIDATE POINT - which is before the
// record exists, so it is asked of the verb and the live context rather than of the
// payload. The two halves must agree with MGPipeBarriered(op, payload, applierState),
// which is what the server computes, so each clause is the same clause:
//
// 1. the static WaitClass column - the same generated table, same op;
// 1. the static WaitClass column - of the op the verb's emitter publishes,
// named by MGP_VERB_RECORD_OP_LIST (MG_Pipe/VerbRecordOps.h);
// 2. kCtxVerb inside an open XFB span - ctx.IsTransformFeedbackActive() here,
// the applied MGPContextValues mirror there, and set_context_values precedes the
// verb on the ring, so the two read the same value (§2.1);
// 3. a draw carrying kDrawClientArrays - NOT asked here, because under run-ahead
// such a draw never reaches a record at all: vi refuses it on this very thread in
// EmitDrawRecord's array arm (§5.1, ruling 2). Asking it here would be a second
// spelling of vi's "does any enabled attribute lack a buffer" walk, and two
// spellings of an escalation is precisely what ruling 3 replaced.
// EmitDrawRecord's array arm (§5.1, ruling 2).
//
// AN OP WITH NO VERB ROW ANSWERS BARRIERED. A verb the join does not know is one this
// file cannot reason about, and the safe answer - fill it, wait for it - is also the
// pre-P5e answer.
// `ctx` MAY BE NULL, and that is not a convenience: the validate point asks this
// question BEFORE it has decided whether there is anything to fill, so that the answer
// is about the RECORD and not about the state of the block. A null context has no open
// transform-feedback span, so clause 2 is false for it - and the verb is a no-op below
// either way.
// P15: THERE IS NO FALLBACK. The answer used to come from inverting MGP_VERB_OP_LIST
// (op -> verb, many-to-one), and a verb the inverse missed answered BARRIERED - which
// the draw family and then the blit family each paid for until a special case named
// them (the blit: one applier quiesce per frame on FCL). The verb -> record table has a
// row for every verb by static_assert, so async is the default and a verb syncs only
// because the record it publishes says so.
//
// `ctx` MAY BE NULL: the validate point asks this question BEFORE it has decided
// whether there is anything to fill. A null context has no open transform-feedback
// span, so clause 2 is false for it - and the verb is a no-op below either way.
// MGPipeValidateForVerbBarriered's switch: the next ClientVerbIsBarriered answers true.
Bool g_forceBarrieredFill = false;
// P15: A PACK-BUFFER READBACK IS FIRE-AND-FORGET, decided per call. With a pack buffer
// bound the readback emitters publish read_pixels_to_buffer / get_texture_image_to_buffer
// (kWaitNone) instead of the reply form, and nothing is owed to the client at the call;
// the predicate mirrors PackBufferReadbackTarget's conditions. The one condition it
// cannot see (the byte range past 2 GiB) sends the emitter to the reply form, which then
// re-validates barriered (MGPipeValidateForVerbBarriered) before it publishes.
Bool PackReadbackGoesToBuffer(GLContext* ctx) {
if (ctx == nullptr) return false;
const auto& pbo = ctx->GetBufferBindingSlot(::MobileGL::BufferTarget::PixelPack).GetBoundObject();
if (!pbo) return false;
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Ipc.PboReadbackSync != 0) return false;
#endif
if (pbo->IsMapped() || !MG_Record::BufferWritebackIsReachable(*pbo)) return false;
return !MGPipeHandleIsNull(MGPipeSlots().FindByLifetimeId(MGPipeKind::Buffer, pbo->GetLifetimeId()));
}
Bool ClientVerbIsBarriered(MGPipeVerb verb, GLContext* ctx) {
MGPWireOp op = WireOpForVerb(verb);
// ---- P5e (ra2): THE JOIN IS MANY-TO-ONE ON THE DRAW FAMILY, SO THE INVERSE IS NOT
// A FUNCTION, AND THE DEFAULT BELOW WAS ANSWERING FOR NINETEEN VERBS -----------------
//
// MGP_VERB_OP_LIST carries ONE row for the whole draw family - `DrawVbo ->
// DrawArrays` - because that is the direction the SERVER needs: a draw_vbo record
// stamps a verb boundary and DrawArrays is the name it prints. Inverting it
// verb-first therefore answers kOpCount for DrawElements, DrawElementsBaseVertex,
// DrawElementsIndirect, MultiDrawElementsBaseVertex and every other indexed /
// instanced / multi / indirect verb - all of which emit exactly that same draw_vbo.
//
// The conservative default below reads "a verb the join does not know answers
// BARRIERED: fill it, wait for it". THE FILL HAPPENS AND THE WAIT DOES NOT. The wait
// is not decided here - it is decided per RECORD, by MGPipeBarriered(op, payload,
// st) at the publish (§2.1) - and the record is a draw_vbo, whose wait class is
// kWaitNone. So every indexed draw filled gPipeInputs while telling
// RefusePipeInputsTouchWhileApplierOwnsIt, through isBarrieredFill, that this thread
// was about to park behind it, and then ran on without parking. The apply thread was
// measured inside a record at that instant, and the abort it produced named the
// CLIENT's verb - a verb the applier cannot stamp, which is what identifies the
// writer (report §2).
//
// So the family's one row is applied to the family. Every other unknown verb keeps
// the conservative answer, which is now honest rather than hoped for: the fill site
// establishes quiescence before it writes (MGPipeValidateForVerb below), so a
// barriered fill no longer rests on a park that may never come.
static_assert(MGPipeVerbForWireOp(MGPWireOp::DrawVbo) == MGPipeVerb::DrawArrays,
"the draw family's representative row moved; the fallback below names "
"draw_vbo because that is the record every kDraw verb emits");
// P5e (ra2): THE CLIENT'S HALF OF ESCALATION (iii) WAS HERE AND WENT WITH IT
// (ID-133, withdrawn by ID-136). It named MultiDrawArrays / MultiDrawElements /
// MultiDrawElementsBaseVertex so the client would FILL for the records the server
// was escalating - a fill the server then read as a barriered pull. Retiring that
// pull outright (MultiDraw.cpp's BoundDrawIndirectBufferId takes a handle arm) left
// nothing for the fill to serve, so a plain multi-draw is an ordinary kDraw verb
// again and takes the draw_vbo answer below. The pair is the phase's own lesson
// written twice: a wait added to make a lane green is paid by an arm, and here the
// arm was the default tier the phone ships.
if (op == MGPWireOp::kOpCount &&
kMGPipeVerbClass[static_cast<SizeT>(verb)] == MGPipeVerbClass::kDraw) {
op = MGPWireOp::DrawVbo;
}
if (op == MGPWireOp::kOpCount) return true;
if (g_forceBarrieredFill) return true;
MGPWireOp op = MGPipeRecordOpForVerb(verb);
if (op == MGPWireOp::ReadPixels && PackReadbackGoesToBuffer(ctx)) op = MGPWireOp::ReadPixelsToBuffer;
if (op == MGPWireOp::GetTextureImage && PackReadbackGoesToBuffer(ctx)) op = MGPWireOp::GetTextureImageToBuffer;
// No record (answered from the caps mirror): no apply can read a fill for it.
if (op == kMGPipeNoRecord) return false;
if (MGPipeWaitClassFor(op) != kWaitNone) return true;
if (MGPipeCallClassFor(op) == kCtxVerb && ctx != nullptr &&
ctx->IsTransformFeedbackActive()) {
@@ -481,6 +465,12 @@ namespace MobileGL::MG_Pipe {
// call that returns, and the lockstep client's behaviour is unchanged byte for byte
// (G1) - under lockstep appliedSeq is already at LastPublishedSeq by construction.
void QuiesceApplierBeforeFill(const char* surface) {
// P15: WITH TWO BLOCKS THE FILL IS THE CLIENT'S OWN. The applier reads the server
// block, so a GL-thread write to the client block races nothing and needs no wait -
// which is what makes one barriered verb cost one wait (its record's) instead of a
// quiesce before the fill, another before the residual half and one per frontend
// mutation after it (FCL BSL: 22 of them per frame).
if (MGPipeBlocksAreDistinct()) return;
MG_Remote::Client::ClientSession* session = MG_Remote::Client::ClientSession::Active();
if (session == nullptr) return; // no session: this process has no applier to outrun
session->WaitForApplyToCatchUp(surface);
@@ -681,11 +671,13 @@ namespace MobileGL::MG_Pipe {
}
void MGPipeVerifyReadHook(const PipeInputs& self, MGPipeInputField field, Uint index0, Uint index1) {
if (&self != &gPipeInputs || !g_verify.Enabled) return;
if (&self != &*gPipeInputs || !g_verify.Enabled) return;
const auto index = static_cast<SizeT>(field);
if (kMGPipeInputFieldSticky[index]) return;
#if MOBILEGL_BUILD_DISAGGREGATED
if (self.ServerStampedVerb() && field == MGPipeInputField::GetPixelStoreParameters) {
// P13 W5: the monolith verb port applies records without a server stamp, and its readback
// forces the neutral pack around the backend's read exactly as the server's does.
if ((self.ServerStampedVerb() || MG_Record::MonolithPortApplyingForVerify()) &&
field == MGPipeInputField::GetPixelStoreParameters) {
// Compare the backend's pack field against the last payload applied to this
// context, including the neutral-pack override during readback. Never obtain
// the reference by reading the field being checked, or consult the client's
@@ -701,7 +693,6 @@ namespace MobileGL::MG_Pipe {
}
return;
}
#endif
if (g_verify.InHook) return;
auto* ctx = LiveContext();
if (ctx == nullptr) return;
@@ -984,8 +975,13 @@ namespace MobileGL::MG_Pipe {
// InitialBytesNotCarried self-check aborted by name on the first
// glBufferData(target, 0, NULL, usage) of the bsl-esc-menu trace. `GetSize() > 0` is
// what makes the answer mean "there are bytes here" rather than "the store is defined".
const void* initialBytes =
(desc.HasDefinedContent != 0 && buffer.GetSize() > 0) ? buffer.MappedData() : nullptr;
//
// P13 W4a: ON MONOLITH'S RECORD ARM THE BASE RIDES AN ORPHANING RESPECIFY TOO. That arm's
// twin aliases this shadow and has no object to re-read it from, and an orphan is exactly
// the call that reallocates it; the backend still uploads nothing for it, because its
// upload is gated on the descriptor's HasDefinedContent and not on the pointer.
const Bool carryBase = desc.HasDefinedContent != 0 || MG_Config::RecordArmAliasesFrontend();
const void* initialBytes = (carryBase && buffer.GetSize() > 0) ? buffer.MappedData() : nullptr;
// kNeedsAck rides on the CALL and MGPipeResourceRespecifyNeedsAck(desc) decides per
// record: only an immutable store (a glBufferStorage*) is a real synchronous
// allocation and only it is allowed one. In monolith the acknowledgement is
@@ -1091,7 +1087,6 @@ namespace MobileGL::MG_Pipe {
// one was emitted, so this cannot be false - but a zeroed record (null
// handle, size 0) is not the answer if that pre-pass is ever relaxed.
if (!MGPipeBuildSubDataRecord(handle, at, length, record, /*verbatimShadow=*/true)) return;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5 (b1): the live-host-writes bit rides the content record, because
// "someone may be writing these bytes without telling you" is a fact about the
// CONTENT and not about the storage. It is set from the object's PUBLISHED
@@ -1099,7 +1094,6 @@ namespace MobileGL::MG_Pipe {
// edge that announced it can never disagree. MGPipeBuildSubDataRecord does not
// take the object, which is why it is set here and not in the builder.
record.HasLiveHostWrites = buffer.HasLiveHostWritesForWire() ? 1 : 0;
#endif
// `length` is this chunk's byte count, which the record also declares
// (MGPipeBuildSubDataRecord writes it into the destination range) - passed
// rather than re-read so the staged run and the record's own claim come from
@@ -1125,7 +1119,6 @@ namespace MobileGL::MG_Pipe {
// store, or the pattern FillSubData expanded locally, and neither is this
// client's untransformed shadow of the level.
if (!MGPipeBuildSubDataRecord(handle, at, length, record, /*verbatimShadow=*/false)) return;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5 (b1): THE SECOND CONTENT EMITTER, and it has to speak for the same reason
// the first does. ApplyBufferWrite ASSIGNS the bit - a content record emitted
// while nothing maps the buffer is how the state goes back to false - so a
@@ -1133,7 +1126,6 @@ namespace MobileGL::MG_Pipe {
// map. `glBufferSubData` against a persistently mapped arena is legal and is
// the ordinary Flywheel/Create shape, so that is not a corner.
record.HasLiveHostWrites = buffer.HasLiveHostWritesForWire() ? 1 : 0;
#endif
// The application's STAGING store, valid for the duration of the call only.
MGPipeRouteBufferSubDataResident(record, base + (at - offset), length);
// P7 wave 2 package C, OQ-10: `rsd=`, counted HERE rather than at the call
@@ -1205,6 +1197,9 @@ namespace MobileGL::MG_Pipe {
record.Res = handle;
record.Offset = offset;
record.Size = size;
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ResourceReadbacks, 1);
}
// The answer travels back through MGPipeClientOnBufferWriteback, and the server's
// epoch bump happens AFTER that writeback, never before.
MGPipeRouteResourceReadback(record);
@@ -1407,6 +1402,7 @@ namespace MobileGL::MG_Pipe {
// refusal, and the refusal asserts (PipeApply.cpp's ResolveVertexElements), i.e. it
// stops a verify build.
MGPipeVertexInputEmitter& emitter = MGPipeVertexInputEmitterInstance();
emitter.ForgetVao(lifetimeId);
const Bool published = emitter.RecordIsPublished(handle);
if (published) {
MGPHandleOnly only{};
@@ -1430,7 +1426,7 @@ namespace MobileGL::MG_Pipe {
// are the same call on the same handle and the second is a no-op, because Free
// bumps no generation (the bump rides the next handout) and refuses a slot that is
// no longer live at this generation.
MG_State::GLState::NotifyStateObjectDestroyed(MGPipeKind::VertexElementsCso, lifetimeId);
MG_State::GLState::NotifyStateObjectDestroyed(MGPipeKind::VertexElementsCso, lifetimeId, handle);
if (!MGPipeHandleIsNull(handle)) MGPipeSlots().Free(MGPipeKind::VertexElementsCso, handle);
return published;
}
@@ -1969,7 +1965,7 @@ namespace MobileGL::MG_Pipe {
// P4a: whether a slot exists is this client's business, and a consumer that records
// notices must not stop seeing a class announce itself.
void NotifyAndFree(MGPipeKind kind, Uint64 lifetimeId, MGPipeHandle handle) {
MG_State::GLState::NotifyStateObjectDestroyed(kind, lifetimeId);
MG_State::GLState::NotifyStateObjectDestroyed(kind, lifetimeId, handle);
if (!MGPipeHandleIsNull(handle)) MGPipeSlots().Free(kind, handle);
}
@@ -2252,15 +2248,18 @@ namespace MobileGL::MG_Pipe {
}
Bool PipeInputs::HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
return lifetimeId != 0 && MGPipeApplier().StreamOutputSpans.count(lifetimeId) != 0;
}
#endif
return lifetimeId != 0 && MGPipeApplier().StreamOutputSpans.count(lifetimeId) != 0;
}
MGP_STICKY_FORWARD_PULL(HasOpenTransformFeedbackSpan);
const auto* ctx = LiveContext();
return ctx != nullptr && ctx->HasOpenTransformFeedbackSpan(lifetimeId);
namespace {
// The frontend half of MGPipeNotifyCompileEnvChanged (PipeApply.h): registered once, at
// static initialisation, by the translation unit that owns the forward.
const Bool kCompileEnvHookRegistered = [] {
MGPipeSetCompileEnvChangedHook([] {
if (gPipeInputs->IsLive()) gPipeInputs->InvalidateCompileEnv();
});
return true;
}();
}
void PipeInputs::InvalidateCompileEnv() {
@@ -2351,9 +2350,7 @@ namespace MobileGL::MG_Pipe {
// stamped, so every stamp this verb made falls behind the serial.
++MGPipeFillAccess::Filled(inputs).CurrentVerbSerial;
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
MGPipeClientClearVerbBoundary();
#endif
MGPipeFillAccess::SetVerb(inputs, MGPipeVerb::kVerbCount);
// The pending base instance belongs to the verb that was about to run, so leaving
// one drops it.
@@ -2643,6 +2640,44 @@ namespace MobileGL::MG_Pipe {
// consumer mask gains bit 13 in the same
// commit, or R-8 withholds the whole family.
kMGPipeWiredBufferBindingSubsystem;
// P15: the dirty bits whose family this build, push mask and consumer emit - every
// condition of the per-bit gate in MGPipeValidateForVerb except the dirty bit itself.
Uint32 ComputeLiveDirtyBits(Uint64 pushMask) {
Uint32 live = 0;
for (Uint32 b = 0; b < kMGPipeDirtyCount; ++b) {
const Uint64 subsystem = MGPipeSubsystemForDirty(static_cast<MGPipeDirty>(b));
if (subsystem != 0 && (pushMask & subsystem) != 0 && (kMGPipeWiredSubsystems & subsystem) != 0 &&
P4aFamilyHasItsConsumer(subsystem) && P4aFamilyDependenciesAreSet(subsystem, pushMask) &&
P5eFamilyIsLive(subsystem, pushMask)) {
live |= MGPipeDirtyBit(static_cast<MGPipeDirty>(b));
}
}
return live;
}
// Per verb, these conditions used to be re-asked for every bit (~15 consumer-registration
// and dependency-table walks per draw). In monolith their only inputs are the push mask and
// the registered consumer, so the answer is kept until either moves. Under a transport the
// caps mirror answers and can move with a new session, so it is recomputed every verb.
Uint32 LiveDirtyBits(Uint64 pushMask) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return ComputeLiveDirtyBits(pushMask);
#endif
static Bool valid = false;
static Uint64 cachedPushMask = 0;
static const MGPipeResourceOps* cachedConsumer = nullptr;
static Uint32 cachedLive = 0;
const MGPipeResourceOps* consumer = MGPipeGetResourceOps();
if (!valid || cachedPushMask != pushMask || cachedConsumer != consumer) {
cachedLive = ComputeLiveDirtyBits(pushMask);
cachedPushMask = pushMask;
cachedConsumer = consumer;
valid = true;
}
return cachedLive;
}
// Each family constant is either 0 or its own subsystem bit and nothing else. Without
// this a header that set the wrong constant - the sampler bit in the program header,
// say - would switch the wrong family on and every gate would still pass.
@@ -2798,7 +2833,7 @@ namespace MobileGL::MG_Pipe {
// and the static_assert below is that fact's trip wire rather than a claim in prose.
//
// NOT THREAD-LOCAL, AND THAT IS THIS FILE'S EXISTING RULE RATHER THAN A NEW ONE:
// g_residualDue, g_omission and the verify latches beside it are file-scope too, and what
// g_omission and the verify latches beside it are file-scope too, and what
// keeps a single writer on them is the verb barrier (ROADMAP.md's G1 row states it for
// gPipeInputs itself). A validate that runs on the apply thread is inside that barrier by
// construction.
@@ -2899,13 +2934,26 @@ namespace MobileGL::MG_Pipe {
Bool ApplierDerives = false;
Bool ContextValuesWireLive = false;
Bool P4aConsumer = false;
#if MOBILEGL_BUILD_DISAGGREGATED
Uint64 CapsGeneration = 0;
#endif
// One bit per FIELD - not per verb class. The class mask is applied at the walk
// exactly as it always was, so "does this verb read this field" stays the walk's
// business and this stays a statement about EMISSION alone.
MGPipeFieldMask Supplied{};
// P14: the walk itself, per verb class, built with Supplied: the non-sticky fields the
// class reads (bit 7 set = copy it, clear = the emitted call supplies it), and the
// sticky ones. The walk then visits only the ~47 fields a draw reads instead of testing
// all 63 against three masks per verb.
static constexpr Uint8 kCopy = 0x80;
static_assert(kMGPipeInputFieldCount < kCopy, "field indices must leave the copy bit free");
static constexpr SizeT kClasses = static_cast<SizeT>(MGPipeVerbClass::kClassCount);
Array<Array<Uint8, kMGPipeInputFieldCount>, kClasses> Walk{};
Array<Uint8, kClasses> WalkCount{};
Array<Array<Uint8, kMGPipeInputFieldCount>, kClasses> Sticky{};
Array<Uint8, kClasses> StickyCount{};
// The walk's copy entries alone, in walk order (~11 of a draw's ~47): the copy loop
// visits only these, and the stamp loop runs apart over the whole walk.
Array<Array<Uint8, kMGPipeInputFieldCount>, kClasses> Copy{};
Array<Uint8, kClasses> CopyCount{};
};
ResidualFillPlan g_fillPlan;
@@ -2978,6 +3026,26 @@ namespace MobileGL::MG_Pipe {
MG_Remote::Client::CapsMirrorInstance().Generation() : 0;
#endif
g_fillPlan.Supplied = built;
for (SizeT c = 0; c < ResidualFillPlan::kClasses; ++c) {
const MGPipeFieldMask& classMask = kMGPipeClassFieldMask[c];
Uint8 walk = 0;
Uint8 sticky = 0;
Uint8 copy = 0;
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
const auto field = static_cast<MGPipeInputField>(i);
if (!MGPipeFieldMaskHas(classMask, field)) continue;
if (kMGPipeInputFieldSticky[i]) {
g_fillPlan.Sticky[c][sticky++] = static_cast<Uint8>(i);
continue;
}
const Bool copied = !MGPipeFieldMaskHas(built, field);
g_fillPlan.Walk[c][walk++] = static_cast<Uint8>(i | (copied ? ResidualFillPlan::kCopy : 0));
if (copied) g_fillPlan.Copy[c][copy++] = static_cast<Uint8>(i);
}
g_fillPlan.WalkCount[c] = walk;
g_fillPlan.CopyCount[c] = copy;
g_fillPlan.StickyCount[c] = sticky;
}
return built;
}
@@ -3066,7 +3134,7 @@ namespace MobileGL::MG_Pipe {
// GLContext's m_currentVertexAttributes hold exactly those, so the diff below is
// EMPTY on the one walk that must publish everything. The server's mirror is not
// default: MGPipeApplierReset() clears the CSO store and the residual block and
// leaves gPipeInputs.m_currentVertexAttribute holding the PREVIOUS context's
// leaves gPipeInputs->m_currentVertexAttribute holding the PREVIOUS context's
// defaults. So the InvalidateAll() a fresh context does to the set-hash
// suppressor would have been cancelled two lines later by this diff, and the one
// call P2 fully owns would publish nothing across a context change - exactly the
@@ -3107,9 +3175,7 @@ namespace MobileGL::MG_Pipe {
// `g_attribDefaultLastHeader`, which is what it just published; there is nothing
// the mirror could add that the wire does not already carry. A build that ever
// needs the repair arm again has to earn it with a barriered row.
#if MOBILEGL_BUILD_DISAGGREGATED
if (ClientRunsAhead()) return sizeof(MGPVertexAttribDefaults) + header.Count * sizeof(MGPAttribValue);
#endif
const auto* mirror = MGPipeFillAccess::VertexAttribDefaultsOf(MGPipeClientInputs());
Bool reproduced = true;
for (SizeT i = 0; i < kAttribs && reproduced; ++i) {
@@ -3126,7 +3192,6 @@ namespace MobileGL::MG_Pipe {
return sizeof(MGPVertexAttribDefaults) + header.Count * sizeof(MGPAttribValue);
}
#if MOBILEGL_BUILD_DISAGGREGATED
// set_context_values (P5c rv, CONTRACT-P5C.md §5.3): the residual-value record. One
// POD carrying every value-class field no other set_* supplies - the two texture-unit
// counters, the 15 per-target touched-buffer-binding counts and the five XFB values -
@@ -3141,6 +3206,18 @@ namespace MobileGL::MG_Pipe {
// server-role-only fixture) - nothing is emitted and the fields keep being pulled, byte
// for byte as before (G1).
Uint64 EmitContextValues(GLContext& ctx) {
// P14: nothing the record reads has moved since the record whose hash the suppressor
// still holds was built (same context, same input generation): it would be dropped.
static MGPContextValues s_lastValues{};
static Uint64 s_lastHash = 0;
static Uint64 s_lastContextId = 0;
static Uint64 s_lastGeneration = 0;
auto& suppressor = MGPipeSetHashSuppressorInstance();
const Uint64 contextId = ctx.GetTextureContextId();
const Uint64 generation = ctx.GetContextValuesGeneration();
if (s_lastHash != 0 && contextId == s_lastContextId && generation == s_lastGeneration &&
suppressor.LastEmitted(MGPipeSuppressorSlot::SetContextValues) == s_lastHash)
return 0;
MGPContextValues values{};
values.ActiveTextureUnit = static_cast<Uint32>(ctx.GetActiveTextureUnit());
values.MaxTouchedTextureUnit = static_cast<Uint32>(ctx.GetMaxTouchedTextureUnit());
@@ -3159,11 +3236,21 @@ namespace MobileGL::MG_Pipe {
values.TransformFeedbackGeneration = ctx.GetTransformFeedbackGeneration();
values.BoundTransformFeedbackLifetimeId = ctx.GetBoundTransformFeedbackLifetimeId();
values.TransformFeedbackCapturedVertices = ctx.GetTransformFeedbackCapturedVertices();
// P14: the record is rebuilt every draw and nearly always equal to the last one. When it
// is byte-equal to the record whose hash the suppressor still holds, the suppressor
// would drop it: skip the hash. (An InvalidateAll changes what the suppressor holds,
// so a reset still re-emits.)
s_lastContextId = contextId;
s_lastGeneration = generation;
if (s_lastHash != 0 && std::memcmp(&values, &s_lastValues, sizeof(values)) == 0 &&
suppressor.LastEmitted(MGPipeSuppressorSlot::SetContextValues) == s_lastHash)
return 0;
// The whole record is the hash input, padding included - `values{}` zeroes it, so
// the pad bytes are defined and the hash is stable.
const Uint64 contentHash = XXH64(&values, sizeof(values), 0);
if (!MGPipeSetHashSuppressorInstance().ShouldEmit(MGPipeSuppressorSlot::SetContextValues,
contentHash)) {
s_lastValues = values;
s_lastHash = contentHash == 0 ? 1 : contentHash; // the suppressor's own latch value
if (!suppressor.ShouldEmit(MGPipeSuppressorSlot::SetContextValues, contentHash)) {
return 0;
}
MGPipeRouteSetContextValues(values);
@@ -3183,70 +3270,8 @@ namespace MobileGL::MG_Pipe {
MGPipeFieldMaskHas(mask, MGPipeInputField::GetBoundTransformFeedbackLifetimeId) ||
MGPipeFieldMaskHas(mask, MGPipeInputField::GetTransformFeedbackCapturedVertices);
}
#endif
// set_residual_value_state (P2 brief D9, ARCHITECTURE.md 9.4).
//
// Since P2 the block is one Uint64 of capability bits, and every one of the 35 is
// ALSO answerable from the assembled working block now that the contract closed the
// FramebufferSrgb / DepthClamp / TextureCubeMapSeamless storage holes. That
// redundancy is the whole point: the bits are read HERE from the frontend, and the
// applier compares them against the assembled answer, so the day a later call takes
// a capability over and forgets to carry it the block says so on the next draw
// (Fatal{PipeResidualDiverged, "<Cap>"}).
//
// Building the carried bits from the ASSEMBLED block instead would make the trip
// wire a tautology, which is exactly the failure P1's entry compare had and P2 is
// paying to remove.
//
// ON THIS BRANCH IT IS STILL HALF A TAUTOLOGY, and saying so is part of the honesty
// the trip wire is for: the applier compares these bits against gPipeInputs'
// capability mirror, and while MGPipeDeriveRenderStateFields is a stub that mirror is
// filled by the residual fill from the SAME IsCapabilityEnabled accessor a few lines
// below. It becomes an independent oracle the moment package A's c1 lands and the
// fill stops copying those fields. What it proves already is that the block is
// emitted, sized and suppressed - the resid= byte class and the one divergence it
// caught during development (GL_DITHER) are that evidence.
//
// Emitted once per context and again whenever the capability set may have moved
// (D9). THE SHUTTER FOR THAT IS NEW_RENDER_STATE, NOT NEW_PIPELINE_STATE, and the
// difference is a hole rather than a nicety: SetCapability's ClipDistance0..7 arms
// are deliberately NOT BumpVersions() (RenderState.cpp says so in as many words), so
// glEnable(GL_CLIP_DISTANCE0) moves m_version alone - and ClipDistance0..7 are 8 of
// the 35 CapabilityInputs this block carries. Arming on the pipeline version would
// leave the trip wire disarmed for those eight for an unbounded window, which is the
// under-firing direction ARCHITECTURE.md 13.2 names as the dangerous one, and no gate
// could see it: a block that is never emitted cannot diverge.
//
// So the arming is the coarsest always-true shutter - either render-state counter
// moved - which is the same answer DirtySurface.def's derivation gives SetCapability.
// It over-fires (a glViewport re-sends 8 bytes and re-runs the compare) and that is
// the intended trade: over-firing costs one 35-bit loop on a verb that already moved
// render state, under-firing renders stale.
Uint64 EmitResidualValueState(GLContext& ctx) {
ResidualValueBlock block{};
constexpr SizeT kCapabilityCount = static_cast<SizeT>(CapabilityInput::CapabilityInputCount);
static_assert(kCapabilityCount <= 64, "CapabilityBits is a Uint64");
for (SizeT i = 0; i < kCapabilityCount; ++i) {
if (ctx.IsCapabilityEnabled(static_cast<CapabilityInput>(i))) {
block.CapabilityBits |= Uint64{1} << i;
}
}
MGPipeRouteSetResidualValueState(block);
if (MG_Util::PipeStats::Enabled()) {
// ByteClass::ResidualValueBlock has been a placeholder that "stays at 0
// until P2" since P0. This is what makes it non-zero.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::ResidualValueBlock,
sizeof(ResidualValueBlock));
}
return sizeof(MGPResidualValueState) + sizeof(ResidualValueBlock);
}
// Set when the capability set may have moved, cleared when the block goes out. It is
// not part of the tracker because it is emission state, not a shutter: the shutter
// (the render-state counter) has already been consumed by the time this is read.
Bool g_residualDue = true;
// The residual block is the ONE emission whose gate names a subsystem constant
// directly instead of going through MGPipeSubsystemForDirty, and the reason is that
@@ -3440,13 +3465,23 @@ namespace MobileGL::MG_Pipe {
field);
}
void MGPipeValidateForVerbBarriered(MGPipeVerb verb) {
#if MOBILEGL_BUILD_DISAGGREGATED
g_forceBarrieredFill = true;
MGPipeValidateForVerb(verb);
g_forceBarrieredFill = false;
#else
// Without a transport every fill is barriered already (fillOwed is constant true).
MGPipeValidateForVerb(verb);
#endif
}
// ---- the validate point (P2 brief D1) ----
void MGPipeValidateForVerb(MGPipeVerb verb) {
// P5f (f1): the fill side's one new spelling. With MOBILEGL_IPC_ROLE_SPLIT_STATE=1 this
// is the CLIENT-role block and gPipeInputs is the server's alone; off, or under
// monolith transport, it folds back onto gPipeInputs and nothing below changes.
PipeInputs& inputs = MGPipeClientInputs();
#if MOBILEGL_BUILD_DISAGGREGATED
// ---- P5e (ra), CONTRACT-P5E §3: WHO OWNS gPipeInputs FOR THIS VERB ----------------
//
// Under run-ahead the block is SERVER-ROLE MEMORY for an unbarriered record: the
@@ -3461,7 +3496,6 @@ namespace MobileGL::MG_Pipe {
// every verb) and the guard below then fires Fatal{RoleViolation, "gPipeInputs"} on
// the first unbarriered verb, by name.
const Bool runAhead = ClientRunsAhead();
#endif
ParsePoisonOmissionKnob();
#if MOBILEGL_PIPE_VERIFY
ArmVerify();
@@ -3508,6 +3542,7 @@ namespace MobileGL::MG_Pipe {
// delete it and the guard below aborts with Fatal{RoleViolation, "gPipeInputs"}
// naming MGPipeValidateForVerb, because the guard now tests the fact rather than
// taking the claim.
if (runAhead) MG_Remote::Client::ClientSession::NoteBarrieredVerbForStats(kMGPipeVerbNames[static_cast<SizeT>(verb)]);
QuiesceApplierBeforeFill("MGPipeValidateForVerb");
MG_Remote::Client::ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt(
"MGPipeValidateForVerb", /*isBarrieredFill=*/barriered);
@@ -3522,7 +3557,6 @@ namespace MobileGL::MG_Pipe {
// Fatal{UnmigratedPipeInput, "<Field>@<none>"} rather than default storage.
++MGPipeFillAccess::Filled(inputs).CurrentVerbSerial;
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// The client is filling, so whatever the server stamped at its last verb boundary
// is withdrawn: the stamps below are the CLIENT's again and a stale read is a
// defect, not a residual pull. Disarming here rather than at the end of the
@@ -3542,7 +3576,6 @@ namespace MobileGL::MG_Pipe {
// (PipeApplier::LeaveApplier) - which is CONTRACT-P5E §3.2's per-role stamp
// ownership, landed as the dual block rather than as moved fields.
MGPipeClientClearVerbBoundary();
#endif
MGPipeFillAccess::SetVerb(inputs, verb);
}
#if MOBILEGL_PIPE_POISON
@@ -3606,25 +3639,28 @@ namespace MobileGL::MG_Pipe {
// supplies a field: with no live session (a monolith transport, the bring-up window, a
// server-role-only fixture) nothing is emitted and the fields keep being pulled, byte
// for byte as before (G1).
//
// P13 W4d: AND ON MONOLITH'S RECORD ARM, where the route is the in-process applier
// (Mono_SetContextValues) and the backend reads what the record put in gPipeInputs - the
// same supplier as a wire, one call away.
const Bool contextValuesWireLive =
MG_Config::Transport != MG_Config::TransportMode::Monolith &&
MG_Remote::Client::ContextValuesWireLive();
MG_Config::Transport != MG_Config::TransportMode::Monolith
? MG_Remote::Client::ContextValuesWireLive()
: MG_Config::RecordArmAliasesFrontend();
#else
constexpr Bool contextValuesWireLive = false;
// P13 W8: a library without a transport is ALWAYS monolith's record arm, so it takes the
// eight fields from the record exactly as the split build's monolith does. This branch
// used to answer a constant false - the pre-W5 pull shape - and left the shipped (FCL)
// build pulling them by residual fill while the split build's monolith carried them; the
// recorder goldens (MG_Test/Pipe/RecorderGoldenTest.cpp) found the two streams differ.
const Bool contextValuesWireLive = MG_Config::RecordArmAliasesFrontend();
#endif
// AND THE SEVENTH IS P5eFamilyIsLive (ID-106), which is the same sentence for the
// binding-point family and asks bit 13's OWN consumer bit rather than the resource
// family's. It answers true for every subsystem but bit 13, so nothing that emitted
// before P5e changes.
const auto wants = [&](MGPipeDirty bit) {
const Uint64 subsystem = MGPipeSubsystemForDirty(bit);
return subsystem != 0 && (pushMask & subsystem) != 0 &&
(kMGPipeWiredSubsystems & subsystem) != 0 &&
P4aFamilyHasItsConsumer(subsystem) &&
P4aFamilyDependenciesAreSet(subsystem, pushMask) &&
P5eFamilyIsLive(subsystem, pushMask) &&
(dirty & MGPipeDirtyBit(bit)) != 0;
};
const Uint32 liveDirty = LiveDirtyBits(pushMask) & dirty;
const auto wants = [&](MGPipeDirty bit) { return (liveDirty & MGPipeDirtyBit(bit)) != 0; };
Uint64 payloadBytes = 0;
// A fresh context is a fresh server, and that is true of EVERY subsystem, so it is
@@ -3701,7 +3737,6 @@ namespace MobileGL::MG_Pipe {
// would be suppressed as unchanged and the server would draw against a window it
// had just been told to empty.
MGPipeShaderBufferEmitterInstance().Reset();
g_residualDue = true;
}
// P4a's segment, in ARCHITECTURE.md 5.4's RECOMMENDED order - framebuffer, then
@@ -3781,7 +3816,6 @@ namespace MobileGL::MG_Pipe {
payloadBytes += EmitVertexAttribDefaults(*ctx, tracker.FreshlyPrimed());
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c rv (CONTRACT-P5C.md §5.3): the residual-value record, emitted when any covered
// value moved. THE GATE IS THE SUBSYSTEM BIT PLUS THE WIRE BEING LIVE - the family has
// no dirty bit (NoDirtyBitOwnsTheResidualSubsystem) and no P4a consumer predicate (it
@@ -3792,7 +3826,6 @@ namespace MobileGL::MG_Pipe {
VerbMaskReadsContextValues(mask)) {
payloadBytes += EmitContextValues(*ctx);
}
#endif
// P3a's vertex segment, in the order the design fixes: vertex elements, then the
// vertex buffers that fill them, then the index binding. All three are LIVE now (m1):
@@ -3848,51 +3881,42 @@ namespace MobileGL::MG_Pipe {
const Bool applierDerives = ApplierDerivesRenderStateFields();
// BY VALUE, NOT BY REFERENCE: the memo's storage is rebuilt in place when the key moves,
// and the walk below holds this across 63 iterations.
const MGPipeFieldMask supplied =
SuppliedFieldMask(pushMask, applierDerives, contextValuesWireLive);
for (SizeT i = 0; fillOwed && i < kMGPipeInputFieldCount; ++i) {
const auto field = static_cast<MGPipeInputField>(i);
if (!MGPipeFieldMaskHas(mask, field)) continue;
(void)SuppliedFieldMask(pushMask, applierDerives, contextValuesWireLive);
// The plan the call above just validated (or rebuilt) for this key: the class's fields in
// index order, sticky ones apart - the same visits, in the same order, as testing all 63.
const SizeT planClass = static_cast<SizeT>(verbClass);
(void)mask;
#if MOBILEGL_PIPE_POISON
if (kMGPipeInputFieldSticky[i]) {
// Stamped once by the first fill that sees a live context; fresh through the
// Sticky -> FilledGen != 0 branch of MGPipeInputFieldIsFresh from then on.
if (filled.FilledGen[i] == 0) filled.FilledGen[i] = 1;
continue;
}
#else
if (kMGPipeInputFieldSticky[i]) continue;
for (SizeT k = 0; fillOwed && k < g_fillPlan.StickyCount[planClass]; ++k) {
// Stamped once by the first fill that sees a live context; fresh through the
// Sticky -> FilledGen != 0 branch of MGPipeInputFieldIsFresh from then on.
const SizeT i = g_fillPlan.Sticky[planClass][k];
if (filled.FilledGen[i] == 0) filled.FilledGen[i] = 1;
}
#endif
if (!MGPipeFieldMaskHas(supplied, field)) MGPipeFillAccess::CopyField(inputs, *ctx, field);
if (fillOwed) {
// The copies first, then the stamps: no copy reads a stamp, so splitting the one walk
// into two loops changes nothing but the cost (the stamp loop no longer reloads its
// bounds and the omission pair around an out-of-line CopyField per entry).
const Uint8* const copies = g_fillPlan.Copy[planClass].data();
const SizeT copyCount = g_fillPlan.CopyCount[planClass];
for (SizeT k = 0; k < copyCount; ++k) {
MGPipeFillAccess::CopyField(inputs, *ctx, static_cast<MGPipeInputField>(copies[k]));
}
#if MOBILEGL_PIPE_POISON
// The value is copied either way; only the stamp is withheld for the omitted pair.
if (!IsOmitted(verb, field)) filled.FilledGen[i] = filled.CurrentVerbSerial;
#endif
}
// ---- step 4b: the residual value block, and it goes out HERE ----
// ARMED OUTSIDE THE SUBSYSTEM GATE: whether the capability set may have moved is a
// fact about the frontend, not about which subsystems this build pushes, and a
// per-subsystem A/B that turns the block off must not also lose the record that one
// is owed.
if ((dirty & (MGPipeDirtyBit(MGPipeDirty::NewRenderState) |
MGPipeDirtyBit(MGPipeDirty::NewPipelineState))) != 0) {
g_residualDue = true;
}
// Its trip wire compares the carried bits against the ASSEMBLED capability mirror,
// and that mirror is written either by the applier's derivation or by the fill loop
// above - so the block is only meaningful once step 4 has run. Emitting it with the
// other calls would compare against the previous verb's answer.
if ((pushMask & kMGPipeSubsystemResidualValues) != 0) {
// The trip wire compares against the ASSEMBLED capability mirror, so it can only
// run at a verb whose class actually carries that mirror - IsCapabilityEnabled is
// in seven of the nine class masks and kQuery and kXfbSpan do not read it, so at
// those verbs the mirror is whatever the last verb that did read it left behind.
// The change is HELD rather than dropped: dropping it would silently disarm the
// wire for a capability that moved between two queries.
if (g_residualDue && MGPipeFieldMaskHas(mask, MGPipeInputField::IsCapabilityEnabled)) {
payloadBytes += EmitResidualValueState(*ctx);
g_residualDue = false;
// The value is copied either way; only the stamp is withheld for the omitted pair,
// which is put back to what it held after the branch-free stamp of the whole walk.
const Uint8* const walk = g_fillPlan.Walk[planClass].data();
const SizeT walkCount = g_fillPlan.WalkCount[planClass];
const Uint64 serial = filled.CurrentVerbSerial;
const Bool omit = g_omission.Armed && g_omission.Verb == verb;
const SizeT omitted = omit ? static_cast<SizeT>(g_omission.Field) : 0;
const Uint64 omittedGen = omit ? filled.FilledGen[omitted] : 0;
for (SizeT k = 0; k < walkCount; ++k) {
filled.FilledGen[walk[k] & ~ResidualFillPlan::kCopy] = serial;
}
if (omit) filled.FilledGen[omitted] = omittedGen;
#endif
}
if (payloadBytes != 0 && MG_Util::PipeStats::Enabled()) {
// PipeStats::RecordDrawPayloadBytes has been implemented and unit-tested since
@@ -3945,7 +3969,6 @@ namespace MobileGL::MG_Pipe {
MGPipeImageEmitterInstance().Reset();
MGPipeProgramEmitterInstance().Reset();
MGPipeShaderBufferEmitterInstance().Reset();
g_residualDue = true;
g_attribDefaultLastHeader = MGPVertexAttribDefaults{};
g_lastFillWasBarriered = true;
}
+5 -5
View File
@@ -13,7 +13,6 @@
// frontend fills the PipeInputs block for exactly the verbs that reach a backend. In the
// pull build the macro is ((void)0) and the pull build is byte-identical to a tree without
// it.
#if MOBILEGL_PIPE_PUSH
#include <MG_Pipe/MGPipe.h>
namespace MobileGL::MG_Pipe {
struct PipeInputs;
@@ -34,6 +33,11 @@ namespace MobileGL::MG_Pipe {
// entry points with one call site per verb instead of nine.
void MGPipeValidateForVerb(MGPipeVerb verb);
// P15: the validate point again for a verb whose first validate answered UNBARRIERED for a
// per-call reason that did not hold at emission (a pack-buffer readback that falls back to
// the reply form). Runs the barriered fill: quiesce, then fill, exactly as a barriered verb.
void MGPipeValidateForVerbBarriered(MGPipeVerb verb);
// Ends the verb in flight without starting another: bumps the serial, so every field the
// verb stamped goes stale, and puts the current verb back to "none", so a read made after
// it aborts as Fatal{UnmigratedPipeInput, "<Field>@<none>"} - which is what such a read
@@ -176,7 +180,3 @@ namespace MobileGL::MG_Pipe {
// neither declared nor defined.
#define MGP_SET_BASE_INSTANCE(BaseInstance) \
::MobileGL::MG_Pipe::MGPipeSetPendingBaseInstance(static_cast<::MobileGL::Uint32>(BaseInstance))
#else
#define MGP_FILL(Verb) ((void)0)
#define MGP_SET_BASE_INSTANCE(BaseInstance) ((void)0)
#endif
+74 -18
View File
@@ -35,7 +35,6 @@
//
// THIS FILE IS CREATED BY THE CONTRACT COMMIT AND FILLED BY THE PACKAGE THAT OWNS IT - see
// FramebufferEmit.h for why, in full.
#if MOBILEGL_PIPE_PUSH
#include <MG_Impl/Pipe/CompositeResolver.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Pipe/MGPipe.h>
@@ -211,29 +210,70 @@ namespace MobileGL::MG_Pipe {
Uint64 bytes = 0;
const MGPipeHandle cso = AcquireShaderCso(*program, bytes);
// (ShaderCso, Version) IS the key, so the latch is the key: an unchanged pair means
// the server already holds these bytes and re-sending them would move the record's
// serial for nothing.
if (cso == m_constantsCso && version == m_constantsVersion) return bytes;
// THE KEY IS PER HANDLE, because the block is: the applier keeps one image per shader
// CSO record, so a program drawn again after another one ran in between still holds
// what it was last sent. An unchanged (ShaderCso, Version) pair means the server
// already holds these bytes and re-sending them would move the record's serial for
// nothing. The latch lives beside the record latch, so a recycled slot or a death
// retires it with that one; a re-issued create and Reset() retire it explicitly.
Latch* latch = MGPipeHandleIsNull(cso) ? nullptr : &LatchFor(cso);
const Bool held = latch != nullptr && latch->ConstantsLive && latch->ConstantsEpoch == m_constantsEpoch;
if (held && latch->ConstantsVersion == version) return bytes;
// Taken on every send, span or whole, so the range always describes the bytes
// written since the image the server holds for this program.
const auto dirty = program->TakeUBODirtyRange();
m_lastConstants = MGPGlobalConstants{};
m_lastConstants.ShaderCso = cso;
m_lastConstants.Version = version;
m_constantsCso = cso;
m_constantsVersion = version;
// P14: the server holds this program's previous image while its latch is held (every
// path that could drop or replace the applier's block retires the latch), so only the
// span the frontend wrote since then goes out - when it is small enough to be worth it.
const Uint8* image = static_cast<const Uint8*>(program->GetUBOData());
if (held && latch->ConstantsSize == size && size <= 0xffffu) {
Uint32 first = std::min<Uint32>(dirty.Begin, size);
Uint32 last = std::min<Uint32>(dirty.End, size);
// Nothing written but the version moved: one byte carries the new version.
if (first >= last) {
first = 0;
last = 1;
}
const Uint32 length = last - first;
if (length * 2 <= size) {
m_lastConstants.Range = (first << 16) | length;
m_lastConstants.Blob.Seg = kMGHostSpanSegNone;
m_lastConstants.Blob.Offset = reinterpret_cast<Uint64>(image + first);
m_lastConstants.Blob.Size = 0;
MGPipeRouteSetGlobalConstants(m_lastConstants, image + first, static_cast<Uint64>(length));
latch->ConstantsVersion = version;
++m_constantSets;
++m_constantSpans;
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::CsoBlobBytes, length);
}
return bytes + sizeof(MGPGlobalConstants) + length;
}
}
// THE ONE BLOB RULE: Size 0 means "this record does not declare its blob" - which
// is what a monolith emission is - and the bytes ride beside it as a companion
// pointer. Offset carries the staging address for diagnostics only; nothing reads
// it as a length.
m_lastConstants.Blob.Seg = kMGHostSpanSegNone;
m_lastConstants.Blob.Offset = reinterpret_cast<Uint64>(program->GetUBOData());
m_lastConstants.Blob.Offset = reinterpret_cast<Uint64>(image);
m_lastConstants.Blob.Size = 0;
// `size` is GetUBOSize(), and it is passed because the record declares 0 - the
// monolith convention (the bytes ride beside the record) that CONTRACT-P5 table 1
// rule A cannot keep under split. It is the row's largest and least bounded blob,
// per program per frame, so it is also the one R-10's max-record counter watches.
MGPipeRouteSetGlobalConstants(m_lastConstants, program->GetUBOData(),
static_cast<Uint64>(size));
m_constantsCso = cso;
m_constantsVersion = version;
MGPipeRouteSetGlobalConstants(m_lastConstants, image, static_cast<Uint64>(size));
if (latch != nullptr) {
latch->ConstantsLive = true;
latch->ConstantsEpoch = m_constantsEpoch;
latch->ConstantsVersion = version;
latch->ConstantsSize = size;
}
++m_constantSets;
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::CsoBlobBytes, size);
@@ -468,7 +508,8 @@ namespace MobileGL::MG_Pipe {
// not happen to move the content version, of which a recycled slot is one.
// Invalidated rather than re-emitted here, because this function has no business
// deciding when the constants go out: the next EmitGlobalConstants sees an
// unlatched key and sends them.
// unlatched key and sends them whole.
latch.ConstantsLive = false;
if (m_constantsCso == handle) {
m_constantsCso = kMGPipeNullHandle;
m_constantsVersion = kMGPipeGlobalConstantsNeverUploaded;
@@ -566,12 +607,13 @@ namespace MobileGL::MG_Pipe {
// The RECORD half stays, and that is the rule rather than an oversight: the applier
// keeps its shader-CSO records across a make-current because a program lives in a share
// group, and re-publishing one would move its Serial for nothing. The global-constants
// key goes with the working state because its record's bytes are per (Cso, Version) and
// a fresh server has not been told them.
// keys go with the working state because its record's bytes are per (Cso, Version) and
// a fresh server has not been told them: every per-handle key retires at once by epoch.
void Reset() {
m_boundCso = kMGPipeNullHandle;
m_drawCso = kMGPipeNullHandle;
m_dispatchCso = kMGPipeNullHandle;
++m_constantsEpoch;
m_constantsCso = kMGPipeNullHandle;
m_constantsVersion = kMGPipeGlobalConstantsNeverUploaded;
// The composite memo's freshness goes with them - and only its freshness. Its
@@ -589,9 +631,10 @@ namespace MobileGL::MG_Pipe {
// ---- what a unit case reads ----
const MGPProgramDesc& LastProgramDesc() const { return m_lastDesc; }
const MGPGlobalConstants& LastGlobalConstants() const { return m_lastConstants; }
// THE (Cso, Version) KEY set_global_constants is suppressed against. Exposed so a case
// can pin that a re-issued create_shader_state invalidates it - the applier clears the
// block on the re-issue (wire W6), so a latch that survived it would never re-send.
// THE LAST (Cso, Version) set_global_constants sent. Exposed so a case can pin that a
// re-issued create_shader_state invalidates it - the applier clears the block on the
// re-issue (wire W6), so a latch that survived it would never re-send. The per-handle
// key the suppression reads retires at the same three places.
MGPipeHandle GlobalConstantsCso() const { return m_constantsCso; }
Uint32 GlobalConstantsVersion() const { return m_constantsVersion; }
// D-J3's counted refusal: programs whose linked snapshot carried more modules than
@@ -605,6 +648,7 @@ namespace MobileGL::MG_Pipe {
Uint64 DrawProgramSetCount() const { return m_drawSets; }
Uint64 DispatchProgramSetCount() const { return m_dispatchSets; }
Uint64 GlobalConstantsSetCount() const { return m_constantSets; }
Uint64 GlobalConstantsSpanCount() const { return m_constantSpans; }
// P5e (pg). The record a case reads back, and the two counters D-J3's rule asks for:
// how many set_program_bindings went out, and how many programs were REFUSED because
// one of their three sets is larger than the record can name. A refusal leaves the
@@ -623,7 +667,8 @@ namespace MobileGL::MG_Pipe {
// composite.
MGPipeHandle AcquireShaderCsoHandle(const ProgramObject& program) {
const Uint64 lifetimeId = program.GetLifetimeId();
const MGPipeHandle existing = MGPipeSlots().FindByLifetimeId(MGPipeKind::ShaderCso, lifetimeId);
const MGPipeHandle existing =
MGPipeSlots().FindByLifetimeIdHinted(MGPipeKind::ShaderCso, lifetimeId, program.PipeHandleHint());
if (!MGPipeHandleIsNull(existing)) return existing;
// A composite is minted off ITS OWN lifetime id, out of the reserved band, and is
// an ordinary ShaderCso handle in every other respect - the same kind, the same
@@ -650,6 +695,13 @@ namespace MobileGL::MG_Pipe {
Uint32 BindingsGen = 0;
Uint32 BindingsBackendStateVersion = 0;
Uint32 BindingsBlockBindingVersion = 0;
// set_global_constants' key, here for the bindings' reason: what the applier's record
// at this handle holds, as (Version, Size), valid only while ConstantsLive and the
// epoch is the emitter's current one.
Bool ConstantsLive = false;
Uint32 ConstantsVersion = 0;
Uint32 ConstantsSize = 0;
Uint64 ConstantsEpoch = 0;
};
// TWO TABLES, NOT A WIDER ONE, and it is the allocator's own reason repeated where it
@@ -704,8 +756,13 @@ namespace MobileGL::MG_Pipe {
MGPipeHandle m_boundCso = kMGPipeNullHandle;
MGPipeHandle m_drawCso = kMGPipeNullHandle;
MGPipeHandle m_dispatchCso = kMGPipeNullHandle;
// The last (Cso, Version) set_global_constants sent, for the unit cases; the suppression
// itself reads the per-handle Latch keys.
MGPipeHandle m_constantsCso = kMGPipeNullHandle;
Uint32 m_constantsVersion = kMGPipeGlobalConstantsNeverUploaded;
// Moved by Reset(): a Latch's constants key holds only at the epoch it was taken in.
Uint64 m_constantsEpoch = 1;
Uint64 m_constantSpans = 0;
Uint64 m_creates = 0;
Uint64 m_binds = 0;
@@ -726,4 +783,3 @@ namespace MobileGL::MG_Pipe {
return *emitter;
}
} // namespace MobileGL::MG_Pipe
#endif // MOBILEGL_PIPE_PUSH
+4 -4
View File
@@ -38,7 +38,6 @@
// upload; resource_subdata, the hot one, is observed through the pure builders below
// instead (MGPipeBuildSubDataRecord / MGPipeForEachSubDataRecordRange), which is also what
// lets a test drive the splitter at both of its bounds without a 4 GiB buffer.
#if MOBILEGL_PIPE_PUSH
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/PipeApply.h>
@@ -282,7 +281,8 @@ namespace MobileGL::MG_Pipe {
// gating it would make the vertex-input subsystem emit null handles whenever the
// resource subsystem is off. Only the CALLS are gated (D-A1).
MGPipeHandle Acquire(BufferObject& buffer) {
const MGPipeHandle handle = MGPipeSlots().Acquire(MGPipeKind::Buffer, buffer.GetLifetimeId());
const MGPipeHandle handle =
MGPipeSlots().AcquireHinted(MGPipeKind::Buffer, buffer.GetLifetimeId(), buffer.PipeHandleHint());
const SizeT slot = handle.Slot;
if (slot >= m_bySlot.size()) m_bySlot.resize(slot + 1);
m_bySlot[slot].Object = &buffer;
@@ -293,7 +293,8 @@ namespace MobileGL::MG_Pipe {
// The handle a buffer already has, or the null handle. Never mints - the emission
// path calls Acquire, the query paths call this.
MGPipeHandle Find(const BufferObject& buffer) const {
return MGPipeSlots().FindByLifetimeId(MGPipeKind::Buffer, buffer.GetLifetimeId());
return MGPipeSlots().FindByLifetimeIdHinted(MGPipeKind::Buffer, buffer.GetLifetimeId(),
buffer.PipeHandleHint());
}
// D-D's inverse, and a RAW pointer is exact here: the entry exists only between the
@@ -732,4 +733,3 @@ namespace MobileGL::MG_Pipe {
gMGPipeCallbacks.OnGpuWritten(res, 1, &whole);
}
} // namespace MobileGL::MG_Pipe
#endif // MOBILEGL_PIPE_PUSH
+5 -6
View File
@@ -37,10 +37,7 @@
// detection. Neither is reproduced here and neither may be.
//
// HEADER-ONLY, for the ownership reason Tracker.h and ResourceTracker.h both state.
#if MOBILEGL_PIPE_PUSH
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Impl/EGLImpl/EGLImpl.h>
#endif
#include <MG_Impl/Pipe/SetHashSuppressor.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Impl/Pipe/Tracker.h>
@@ -862,7 +859,9 @@ namespace MobileGL::MG_Pipe {
unitSampler ? unitSampler.get() : texture->GetSamplerObject().get();
if (MG_State::GLState::SamplesAsIncompleteTexture(texture.get(), effective)) continue;
entry.Texture = MGPipeSlots().Acquire(MGPipeKind::Texture, texture->GetLifetimeId());
entry.Texture = MGPipeSlots().AcquireHinted(
MGPipeKind::Texture, texture->GetLifetimeId(),
texture->PipeHandleHint(ITextureObject::PipeHintKind::Texture));
// D-A4: a texture the sampler-view resolution names in an emitted MGPBoundView
// is SAMPLER-bound from then on (sticky; the texture emitter's contract door,
// since this header is included BY TextureEmit.h). One early-out per unit per
@@ -979,7 +978,8 @@ namespace MobileGL::MG_Pipe {
MGPipeHandle AcquireSamplerView(const ITextureObject& texture, MGPipeHandle textureHandle,
Uint64& payloadBytes) {
const MGPipeHandle handle =
MGPipeSlots().Acquire(MGPipeKind::SamplerViewCso, texture.GetLifetimeId());
MGPipeSlots().AcquireHinted(MGPipeKind::SamplerViewCso, texture.GetLifetimeId(),
texture.PipeHandleHint(ITextureObject::PipeHintKind::SamplerView));
const SizeT slot = handle.Slot;
if (slot >= m_viewLatch.size()) m_viewLatch.resize(slot + 1);
ViewLatch& latch = m_viewLatch[slot];
@@ -1178,4 +1178,3 @@ namespace MobileGL::MG_Pipe {
return *emitter;
}
} // namespace MobileGL::MG_Pipe
#endif // MOBILEGL_PIPE_PUSH
+7 -2
View File
@@ -37,7 +37,6 @@
// Header-only for the same ownership reason as Tracker.h and CsoCache.h: the root
// CMakeLists.txt that would name a new .cpp belongs to package A and is frozen behind the
// p2/contract tag.
#if MOBILEGL_PIPE_PUSH
#include <MG_Pipe/MGPipe.h>
namespace MobileGL::MG_Pipe {
@@ -104,6 +103,13 @@ namespace MobileGL::MG_Pipe {
return true;
}
// The server now holds the set `contentHash` names without this slot having emitted it
// (P14: bind_vertex_elements re-applies the array's kept set). Same 0 -> 1 remap as
// ShouldEmit, so the next ShouldEmit of the same hash answers false.
void Latch(MGPipeSuppressorSlot slot, Uint64 contentHash) {
m_lastEmitted[static_cast<SizeT>(slot)] = contentHash == 0 ? 1 : contentHash;
}
// A context change or a server reset: what the server has is no longer what this
// slot last emitted, so the next resolved set must go out whatever it hashes to.
void Invalidate(MGPipeSuppressorSlot slot) { m_lastEmitted[static_cast<SizeT>(slot)] = 0; }
@@ -131,4 +137,3 @@ namespace MobileGL::MG_Pipe {
return *suppressor;
}
} // namespace MobileGL::MG_Pipe
#endif // MOBILEGL_PIPE_PUSH
-2
View File
@@ -48,7 +48,6 @@
// HEADER-ONLY, and the wired constant below is what switches the family on, for the ownership
// reason PipeFill.cpp states in full: that file belongs to the contract package for the whole
// phase, so the bit that turns an emitter on is a constant in the emitter's OWN header.
#if MOBILEGL_PIPE_PUSH
#include <MG_Impl/Pipe/ResourceTracker.h>
#include <MG_Impl/Pipe/SetHashSuppressor.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
@@ -274,4 +273,3 @@ namespace MobileGL::MG_Pipe {
return *emitter;
}
} // namespace MobileGL::MG_Pipe
#endif // MOBILEGL_PIPE_PUSH

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