Compare commits

...
Author SHA1 Message Date
swung0x48 652e7cd3fc [Merge] (Anland): take fix-d1100-mali - the damage-swap fallback and extent-and-format shared-image naming on kernels with one dma-buf identity 2026-10-14 20:40:21 -04:00
swung0x48 fdb93e9e65 [Fix] (CI): treat a process that exits mid-read of its /proc stat as gone in the TCP server fixture 2026-10-14 18:59:27 -04:00
swung0x48 d1b3a3325f [Test] (ServerLoop): arm the guilty-reset drain hold by the healthy record's seq so the previous drain cannot take it 2026-10-14 14:59:47 -04:00
swung0x48 28f937d539 [Fix] (Espryt): keep the invalid-image placeholder on ANGLE instead of probing an empty unit it faults on 2026-10-14 10:30:17 -04:00
swung0x48 2afbcaaf4f [CI] (Census): retire the renamed subgroup sanity case and Magma's split T0 fallback, admit the split-only writeback skip 2026-10-13 12:47:31 -04:00
swung0x48 e8dafacb1c [Fix] (Magma): leave the fragment stage out of rasterizer-discard pipelines 2026-10-13 12:47:30 -04:00
swung0x48 6336943b87 [Test] (Coverage): register ClipControlScenario on the gated split arms 2026-10-13 10:28:38 -04:00
swung0x48 a31eabd12b [Test] (IterationRP): accept the driver's dot() rounding within one ulp in the sampled first-reduction probe 2026-10-13 10:28:37 -04:00
swung0x48 b9f940ac63 [Test] (Pipe): re-pin the pipeline member count and the clear-blit recorder golden 2026-10-13 08:02:37 -04:00
swung0x48 5980349b33 [Fix] (CI): the parity gate sanctions Magma's spawn-only AdoptTier0 fallback case 2026-10-13 08:02:36 -04:00
swung0x48 fdb747b06d [Fix] (Remote): stop the adoption's falling edge from overwriting writes made through the adopted map 2026-10-13 08:02:35 -04:00
swung0x48 8ecab66567 [Fix] (Espryt): probe empty image units and keep invalid units empty on drivers that read them as zero 2026-10-13 08:02:34 -04:00
swung0x48 d6b0eba8fe [Feat] (Subgroups): use native subgroups or emulate them on both backends 2026-10-13 06:03:57 -04:00
swung0x48 cfbaffd757 [Fix] (Espryt): defer geometry emits behind memory writes on drivers that drop them 2026-10-12 23:57:48 -04:00
swung0x48 735b705397 [Fix] (Espryt): invalid image units read zero through a shaped placeholder 2026-10-12 23:13:18 -04:00
swung0x48 cd9f67763e [Fix] (Magma): re-emit dynamic state after rasterizer-discard draws 2026-10-12 22:32:28 -04:00
swung0x48 8c2d5019c2 [Fix] (Magma): convert 32-bit int and packed 2_10_10_10 float attributes on the CPU 2026-10-12 21:13:30 -04:00
swung0x48 4e0989b99a [Fix] (Magma): read unbacked depth levels back from the CPU shadow 2026-10-12 21:03:54 -04:00
swung0x48 4eb0dbc8a9 [Fix] (Magma): probe and work around Adreno E5 transfer reads and cross-format slice copies 2026-10-12 21:00:18 -04:00
swung0x48 065d1582ba [Fix] (Magma): ignore shader gl_SampleMask without multisample fragment ops 2026-10-12 19:37:25 -04:00
swung0x48 05d58a566a [Fix] (Espryt): sample cube-array shadow textures at LOD 0 with texture() outside the fragment stage 2026-10-12 19:05:33 -04:00
swung0x48 702f937ca9 [Fix] (Pipe): avalanche the composite shutter mix so dirty bits stop colliding 2026-10-12 18:55:51 -04:00
swung0x48 6129d6e463 [Fix] (Magma): zero gl_Layer in draws into a non-layered framebuffer 2026-10-12 18:28:13 -04:00
swung0x48 53f3b7a9af [Fix] (GL): leave the injected default fragment stage out of program pipeline validation 2026-10-12 18:28:03 -04:00
swung0x48 47601cbbb8 [Fix] (Buffer): refresh persistent read maps at a client-mapped buffer barrier 2026-10-12 18:00:16 -04:00
swung0x48 cd26f508df [Fix] (Pipe): skip the location tables a failed relink cleared when emitting program bindings 2026-10-12 15:30:33 -04:00
swung0x48 da6b172d08 [Fix] (Magma): lower pixel_center_integer gl_FragCoord so Vulkan validation stops refusing the program 2026-10-12 15:06:42 -04:00
swung0x48 171eed9ac0 [Fix] (CI): re-baseline the link ratchet for the seq-ordered buffer writeback signature 2026-10-12 08:40:52 -04:00
swung0x48 09b315197b [Fix] (Espryt): bound usampler2DMS fetches by the bound texture's real sample count 2026-10-12 08:39:26 -04:00
swung0x48 c9bf43b710 [Fix] (Texture): decode ETC2/EAC uploads into the texel shadow 2026-10-12 08:32:53 -04:00
swung0x48 1962e0c603 [Fix] (Buffer): refresh persistent read maps at glFinish and glClientWaitSync 2026-10-12 08:32:52 -04:00
swung0x48 acad001b34 [Fix] (Pipe): bind incomplete image units as no image 2026-10-12 08:32:51 -04:00
swung0x48 8fff9363f7 [Fix] (Backend): apply framebuffer no-attachment defaults on both backends 2026-10-12 08:32:51 -04:00
swung0x48 3b0e6270b7 [Fix] (Backend): apply the polygon offset clamp on both backends 2026-10-12 08:32:50 -04:00
swung0x48 1469022b53 [Fix] (Shader): keep ESSL 3.10 block-array indexing and image-uniform rules for sources that declared an es version 2026-10-12 08:32:49 -04:00
swung0x48 e98c5b4a6a [Fix] (GL): validate program pipelines by the spec rules in glValidateProgramPipeline and on every pipeline draw 2026-10-12 08:32:49 -04:00
swung0x48 e0193f0b3d [Fix] (GL): fail the link of a non-separable program that has a tessellation or geometry stage but no vertex stage 2026-10-12 08:32:48 -04:00
swung0x48 35077242c3 [Fix] (GL): derive per-stage atomic-counter limits from the backend's storage-block budget and fail the link past them 2026-10-12 08:32:47 -04:00
swung0x48 cc23ed3c7e [Fix] (Shader): reject a location qualifier on an atomic counter instead of dropping it with a warning 2026-10-12 08:32:47 -04:00
swung0x48 4cb65bdfdc [Fix] (Shader): place relaxed-rule atomic counters on their binding's offset cursor and reject unsized, unbound and overlapping counters at compile 2026-10-12 08:32:46 -04:00
swung0x48 c2dbcbee19 [Fix] (GL): report GL_SAMPLE_POSITION from the backend's sample locations and check the index against the draw framebuffer's GL_SAMPLES 2026-10-12 08:32:44 -04:00
swung0x48 45f5bae8b3 [Fix] (GL): answer one for the name-length program queries of a program linked from SPIR-V 2026-10-12 08:32:44 -04:00
swung0x48 b4da0e1a87 [Fix] (GL): raise INVALID_VALUE/INVALID_ENUM from glGetStringi for an index past the list or an unindexed name 2026-10-12 08:32:43 -04:00
swung0x48 049c8a421c [Fix] (GL): implement glGetCompressedTextureSubImage over the compressed block shadow 2026-10-12 08:22:18 -04:00
swung0x48 60ba52bfe4 [Fix] (GL): validate the glGetTextureSubImage region with INVALID_VALUE before refusing a partial read 2026-10-12 08:22:17 -04:00
swung0x48 b7d42faac0 [Fix] (GL): answer GL_TEXTURE_BUFFER_DATA_STORE_BINDING in glGetTexLevelParameter 2026-10-12 08:22:16 -04:00
swung0x48 2f312262ea [Fix] (GL): keep TEXTURE_BASE_LEVEL/MAX_LEVEL as set on immutable textures and clamp only the effective range 2026-10-12 08:22:16 -04:00
swung0x48 4b5c315ea5 [Fix] (GL): report FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS and treat an attached layer past the last one as an incomplete attachment 2026-10-12 08:22:15 -04:00
swung0x48 d4501e0c23 [Fix] (GL): raise INVALID_OPERATION from glTextureParameter* for sampler state on a multisample texture 2026-10-12 08:22:14 -04:00
swung0x48 6e43ff8ccc [Feat] (GL): apply glClipControl on both backends and advertise ARB_clip_control 2026-10-12 08:11:18 -04:00
swung0x48 82ef0053ea [Fix] (DirectVulkan): express polygon facing as the pipeline front face 2026-10-12 08:10:50 -04:00
swung0x48 c83d27a668 [Feat] (GL): advertise ARB_cull_distance where the backend has cull distances 2026-10-12 08:10:50 -04:00
swung0x48 454083bf57 [Fix] (DirectGLES): redeclare input gl_PerVertex with the producer's clip/cull sizes 2026-10-12 08:10:04 -04:00
swung0x48 b22c37f763 [Fix] (GL): resolve attribute array elements in glGetAttribLocation 2026-10-12 08:10:04 -04:00
swung0x48 4c9ce56124 [Feat] (GL): advertise the 4.x-core extension batch and ARB_tessellation_shader where the device has tessellation 2026-10-12 08:09:24 -04:00
swung0x48 2eae028938 [Fix] (GL): report tessellation-stage image uniform limits from the backend and enforce them at link 2026-10-12 08:08:58 -04:00
swung0x48 c881bc2396 [Feat] (Magma): pipeline and primitive fragment shading rate via VK_KHR_fragment_shading_rate 2026-10-12 08:03:31 -04:00
swung0x48 db23b9d551 [Feat] (Espryt): forward fragment shading rate to the driver extension 2026-10-12 08:03:29 -04:00
swung0x48 6d27499f3c [Feat] (ShaderTranspiler): carry shading-rate built-ins through the ESSL leg 2026-10-12 08:03:28 -04:00
swung0x48 7e544c2b78 [Feat] (Frontend): add GL_EXT_fragment_shading_rate state, entry points and getters 2026-10-12 08:03:27 -04:00
swung0x48 ba6db60c09 [Fix] (GL): fail the link when a stage exceeds its uniform or storage block limits 2026-10-12 06:46:05 -04:00
swung0x48 106190e2ff [Fix] (Shader): make demoted double varyings flat on every stage so both sides of the interface agree 2026-10-12 06:46:04 -04:00
swung0x48 bc3c4fbb6c [Fix] (Remote): order buffer writebacks by producing seq so a stale capture writeback cannot retire a newer GPU-write mark 2026-10-12 06:46:04 -04:00
swung0x48 9b5e15dfc8 [Fix] (Espryt): swizzle GL_BGRA vertex arrays into RGBA streams instead of disabling them 2026-10-12 05:59:29 -04:00
swung0x48 363fce247f [Fix] (Espryt): order a dispatch after unbarriered storage-buffer writes so adjacent components survive 2026-10-12 05:59:28 -04:00
swung0x48 c639e17514 [Fix] (Espryt): barrier re-uploads into image-accessed textures ahead of the dispatch that reads them 2026-10-12 05:59:27 -04:00
swung0x48 84b0c20512 [Test] (Espryt): upload into an image-only texture lands before the draw that stores over it 2026-10-12 04:29:44 -04:00
swung0x48 14617a57df [Fix] (Espryt): re-sync image units when a bound image texture's content serial moves 2026-10-12 04:29:43 -04:00
swung0x48 8800c6b068 [Fix] (Shader): keep subroutine type declarations as prototypes so reserved type names are rejected 2026-10-12 03:55:51 -04:00
swung0x48 7abc581551 [Test] (GL): image_external names stay off strict desktop contexts 2026-10-12 03:55:50 -04:00
swung0x48 a566342eed [Test] (Texture): unsized depth formats resolve to sized storage 2026-10-12 03:55:49 -04:00
swung0x48 ef3178a98f [Fix] (GL): keep ES-only image_external and ARB_robustness names off the wrong context kinds 2026-10-12 03:55:49 -04:00
swung0x48 03aa1528a8 [Fix] (GL): accept GL_SRC_ALPHA_SATURATE as a source and destination blend factor 2026-10-12 03:55:48 -04:00
swung0x48 e7cd3a3290 [Fix] (Texture): size unsized GL_DEPTH_COMPONENT/GL_DEPTH_STENCIL textures so depth readback can stage them 2026-10-12 03:55:48 -04:00
swung0x48 53877cfb6b [Docs] (cts): ReadBuffer waiver names GL_INVALID_OPERATION, not GL_INVALID_ENUM 2026-10-12 03:55:47 -04:00
swung0x48 2a1920adce [Fix] (Espryt): redeclare clip/cull distance outputs inside gl_PerVertex for ESSL 2026-10-12 03:55:46 -04:00
swung0x48 b4f35cd67d [Fix] (cts): run_cts_local.py times out on qpa log idleness, not chunk wall time 2026-10-12 03:32:57 -04:00
swung0x48 cef575916e [Feat] (Video): add the paper-cutout intro and hype reel sources 2026-10-11 12:18:32 -04:00
swung0x48 2afeec95cd [Fix] (Magma): readback before the first acquire no longer submits a signaled slot fence 2026-10-11 07:48:04 -04:00
swung0x48 a2801f9da4 [Fix] (Build): restore the MSVC Windows build of the opengl32 drop-in 2026-10-11 07:48:03 -04:00
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
BZLZHH 8302dd30bf [Docs] (Anland): the lock screen behind the black desktop, and where the Magma text gap is not 2026-10-03 16:34:25 +08:00
BZLZHH a6d788191f [Docs] (Anland): record the Magma verification and its missing glyphs 2026-10-03 16:07:53 +08:00
BZLZHH 0d89db36dd [Docs] (Anland): the D1100 + Mali changes and why each one was needed 2026-10-03 15:57:11 +08:00
BZLZHH b7a6e6c538 [Docs] (Anland): the D1100 + Mali, kernel 4.14 bring-up and its three fixes 2026-10-03 15:55:53 +08:00
BZLZHH f44888ca01 [Fix] (Espryt): present through eglSwapBuffers when the driver refuses the damage entry point
A vendor EGL can export eglSwapBuffersWithDamageEXT and then answer EGL_FALSE on
a window surface without setting an error (measured on an Android 4.14 vendor
kernel with a Mali driver; the surface is current, the context is current, the
ANativeWindow reports its full extent, and the plain entry point presents the
same surface happily). Trusting that pointer kept the frame: the window's buffer
queue never saw a buffer, the compositor's own window stayed a solid colour on
screen, and every client of the session fell back to wl_shm because the
compositor could not sample what it could not present.

A refused damage swap now falls back to eglSwapBuffers and says so once. A
driver without the extension presents through the plain call as before.
2026-10-03 15:54:55 +08:00
BZLZHH 8245221755 [Fix] (Remote): name a shared image by extent and format when the kernel gives descriptors one identity
(st_dev, st_ino) is how an incoming dma-buf descriptor names the image it was
exported from, and it is the only identity the registry had. Some kernels do not
keep those apart: on a 4.14 vendor kernel every descriptor of every image reports
dev 12 ino 10658 with st_size 0, and /proc/self/fdinfo carries nothing else, so
the registry correctly detected the ambiguity and then refused every import -
which is every zero-copy window a compositor ever tries to sample, leaving the
whole stack on the wl_shm fallback.

The identity stays the kernel's to give. When it is ambiguous, the import's own
extent and format select among the live images exported under that identity, in
allocation order and each claimed once, which is the order a producer allocates
and hands them over in. Identify() takes the extent and the fourcc the import
record already carries; nothing else changes.
2026-10-03 15:54:55 +08:00
582 changed files with 73576 additions and 42405 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
+2 -2
View File
@@ -44,9 +44,9 @@ require 'key:MOBILEGL_BACKEND_TYPE' "$plugin_resource_text" 'V2 backend variable
require 'defaultValue:DirectGLES' "$plugin_resource_text" 'V2 DirectGLES default'
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_SUBGROUP_EMULATION' "$plugin_resource_text" 'V2 subgroup emulation setting'
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'
+50 -10
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
@@ -472,9 +512,7 @@ jobs:
MOBILEGL_ESPRYT_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }}
MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_ITRP_FIX: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && 'ForceOn' || 'Auto' }}
run: |
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
test -f "${apk_file}"
@@ -492,10 +530,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 +774,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" \
+796 -520
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
+257 -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()
@@ -360,6 +409,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenFloat64StorageBlockPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeferGeometryEmitsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/UniquifyIoBlockNamesPass.cpp
@@ -367,6 +417,9 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerPixelCenterIntegerPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroLayerOutputPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FullCoverageSampleMaskPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.cpp
@@ -399,6 +452,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
MobileGL/MG_Util/Texture/EtcDecoder.cpp
MobileGL/MG_Impl/GLXImpl/Exporting/Definitions.cpp
MobileGL/MG_Impl/GLXImpl/Exporting/GlvndVendor.cpp
@@ -421,6 +475,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 +600,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 +696,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 +725,28 @@ 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
MobileGL/MG_Backend/DirectVulkan/Renderer/WireE5RawCopy.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/WireCopySliceProbe.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.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 +809,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 +837,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 +942,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 +969,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 +987,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 +1017,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 +1102,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 +1113,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 +1127,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 ()
+180 -91
View File
@@ -85,41 +85,57 @@ namespace MobileGL::MG_Config {
// MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash.
String TraceAngleVariant;
#endif
// MOBILEGL_MAGMA_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support,
// including the opt-in emulated compute path below.
Bool MagmaDisableSubgroup = false;
// MOBILEGL_MAGMA_EMULATE_SUBGROUP: implement GL_KHR_shader_subgroup's compute
// stage on a 32-lane VIRTUAL subgroup lowered to workgroup-shared memory
// (ShaderTranspiler::EmulateSubgroupsPass). Strictly a last resort: it only ever
// engages when this flag is set AND the device has no native subgroup support at
// all - a device with real subgroup operations always uses them natively,
// whatever their width (the known iterationRP defect is patched by
// MagmaFixIterationRPSubgroupScratch below instead). Off by default.
Bool MagmaEmulateSubgroup = false;
// MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: patch iterationRP's own bug - the
// pack declares `shared vec2 prefixSumCache[32]` for a 512-invocation exposure
// reduction and indexes it by gl_SubgroupID, so any device with sub-16-lane
// subgroups (8-lane lavapipe -> 64 subgroups) writes shared memory out of
// bounds. The pass grows that one array to what the device's topology needs and
// touches nothing else; it only rewrites modules positively matching the pack's
// reduction fingerprint (ShaderTranspiler::FixIterationRPSubgroupScratchPass),
// so every other shader passes through byte-identical - as does iterationRP
// itself on >= 16-lane devices. Auto is ON; ForceOff replays the pack's bug
// verbatim.
QuirkOverride MagmaFixIterationRPSubgroupScratch = QuirkOverride::Auto;
// MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup
// rendezvous between its two reductions over prefixSumCache. Off by default and
// fingerprint-gated by FixIterationRPBarrierPass when enabled.
Bool MagmaIterationRPFixBarrier = false;
// MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
// ceil(workgroup invocations / gl_SubgroupSize) on the NATIVE subgroup path
// (ShaderTranspiler::DeriveNumSubgroupsPass). Auto is ON: GL requires
// gl_SubgroupID < gl_NumSubgroups, Adreno's builtin reports 1 while the same
// dispatch emits IDs 0..7, and the derived value is the one Vulkan guarantees
// whenever the pipeline can request REQUIRE_FULL_SUBGROUPS (which the renderer
// does whenever local_size_x is a multiple of the native width). ForceOff returns
// to the raw driver builtin.
QuirkOverride MagmaDeriveNumSubgroups = QuirkOverride::Auto;
// MOBILEGL_SUBGROUP_EMULATION: how either backend implements GL_KHR_shader_subgroup
// (MG_Backend/SubgroupImplementation.h). Auto (the default) uses the driver's own subgroup
// operations natively whenever it has them, at whatever width (8/16/32/64/128 lanes), and
// otherwise emulates the compute stage on a 32-lane VIRTUAL subgroup in workgroup-shared
// memory (ShaderTranspiler::EmulateSubgroupsPass). ForceOn emulates even over native
// support, so the emulation can be exercised on any device; ForceOff never emulates,
// leaving a driver without subgroups without the extension.
QuirkOverride SubgroupEmulation = QuirkOverride::Auto;
// MOBILEGL_ITRP_FIX: the iterationRP shaderpack's two compute-shader bugs, repaired by
// fingerprint-gated passes that leave every other module byte-identical
// (MG_Backend/SubgroupImplementation.h):
// * FixIterationRPSubgroupScratchPass - the pack declares `shared vec2
// prefixSumCache[32]` for a 512-invocation reduction indexed by gl_SubgroupID, so a
// sub-16-lane device (8-lane lavapipe -> 64 subgroups) writes shared memory out of
// bounds; the array grows to what the native width needs (inert at >= 16 lanes, and
// under the 32-lane emulation);
// * FixIterationRPBarrierPass - Program 203's second reduction overwrites that scratch
// without a workgroup rendezvous after the first one's readers.
// Auto and ForceOn apply both; ForceOff replays the pack's bugs verbatim.
QuirkOverride IterationRPFix = 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 +151,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
@@ -169,6 +199,31 @@ namespace MobileGL::MG_Config {
// lavapipe carry a located block correctly and would otherwise never run this code -
// and ForceOff is the negative control. See StripIoBlockLocationsPass.
QuirkOverride EsprytUnlocatedIoBlocks = QuirkOverride::Auto;
// MOBILEGL_ESPRYT_DEFER_GEOMETRY_EMITS: DirectGLES moves a geometry shader's
// EmitVertex()/EndPrimitive() behind its memory writes (DeferGeometryEmitsPass). The
// Adreno ES driver silently drops storage writes a geometry invocation makes after
// emitting; the driver POST measures that and arms this by itself, so Auto is right
// everywhere. ForceOn exercises the rewrite on a healthy driver; ForceOff is the
// negative control that replays the lost writes.
QuirkOverride EsprytDeferGeometryEmits = QuirkOverride::Auto;
// MOBILEGL_ESPRYT_ATOMIC_SUBGROUP_SCRATCH: DirectGLES's subgroup emulation reads its
// workgroup scratch back through atomic loads (EmulateSubgroupsPass). The Adreno ES
// compiler treats ordinary shared-memory loads as pure and merges or reorders them
// across stores and barrier(), so an emulated subgroup operation would see the lanes of
// an earlier one; the driver POST measures that and arms this by itself, so Auto is
// right everywhere. ForceOn exercises the atomic reads on a healthy driver; ForceOff is
// the negative control.
QuirkOverride EsprytAtomicSubgroupScratch = QuirkOverride::Auto;
// MOBILEGL_ESPRYT_INVALID_IMAGE_PLACEHOLDER: DirectGLES binds an image unit GL 4.6 8.26
// calls invalid (a level the texture does not have) to a zeroed 1x1 texture of the bind's
// shape instead of leaving it empty. The Adreno ES driver answers a load through an EMPTY
// unit with alpha 1 where GL says 0, and ANGLE faults on any use of an empty unit; the
// driver POST measures the first and Auto arms this on ANGLE without probing, so Auto is
// right everywhere. The placeholder's one texel is real, so a store
// through such a unit can be read back through the same unit in the same pass - which is
// why a healthy driver keeps the empty unit. ForceOn exercises the placeholder on a
// healthy driver; ForceOff is the negative control.
QuirkOverride EsprytInvalidImagePlaceholder = QuirkOverride::Auto;
// MOBILEGL_POINT_SIZE_DEMOTION: demote gl_PointSize out of tessellation/geometry
// stages into an ordinary varying (ShaderCompiler::
// DemoteTessellationGeometryPointSizeForProgram) instead of declining such programs
@@ -336,6 +391,28 @@ namespace MobileGL::MG_Config {
// immune to the probe's verdict moving), and ForceOff is the negative control that
// replays the driver's silence.
QuirkOverride MagmaPrimGenQueryReroute = QuirkOverride::Auto;
// MOBILEGL_MAGMA_E5_RAW_READS: DirectVulkan's raw read route for E5B9G9R9 textures
// (WireE5RawCopy.h). GL moves RGB9_E5 texel blocks bit-exactly (glGetTexImage with
// UNSIGNED_INT_5_9_9_9_REV, glCopyImageSubData), but Adreno 750's transfer reads of
// an E5B9G9R9 image larger than ~16x16 decode and re-encode, canonicalizing every
// non-canonical shared exponent. Auto defers to a device probe at renderer bring-up
// (RunWireE5TransferProbe: plain transfer read vs. a shader read through an R32_UINT
// view) and arms the route only where the plain read changed words; ForceOn arms it
// on any device (the host lanes exercise the route with it); ForceOff is the negative
// control that replays the canonicalization.
QuirkOverride MagmaE5RawReads = QuirkOverride::Auto;
// MOBILEGL_MAGMA_SPLIT_SLICE_COPIES: DirectVulkan's per-slice split of a cross-format
// glCopyImageSubData between a layered texture and a 3D one (WireCopySliceProbe.h).
// Adreno 750 lands only the first slice of such a vkCmdCopyImage region. Auto defers to
// a device probe at renderer bring-up (one region vs. per-slice regions); ForceOn splits
// on any device; ForceOff is the negative control that replays the dropped slices.
QuirkOverride MagmaSplitSliceCopies = QuirkOverride::Auto;
// MOBILEGL_MAGMA_REEMIT_STATE_AFTER_DISCARD: DirectVulkan sets the dynamic state again after
// every rasterizer-discard draw (WireDiscardStateProbe.h). On Adreno 750 a draw with a
// rasterizerDiscardEnable pipeline drops the dynamic viewport/scissor, and later draws that
// rely on it draw nothing. Auto defers to a device probe at renderer bring-up; ForceOn
// re-emits on any device; ForceOff is the negative control that replays the lost state.
QuirkOverride MagmaReemitStateAfterDiscard = QuirkOverride::Auto;
// --- MGPipe (the disaggregation plan's explicit frontend/backend boundary) ---
// MOBILEGL_PIPE_PUSH: per-subsystem bitmask selecting which state the frontend
// PUSHES over MGPipe instead of leaving the backend to pull it out of GLContext.
@@ -370,6 +447,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 +458,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 +476,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 +527,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 +621,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 +638,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 +749,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 +790,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 +820,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
+122 -42
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
@@ -228,11 +249,19 @@ namespace MobileGL::MG_ConfigLoader {
// Quirk overrides are tri-state: an unset variable keeps device auto-detection, a truthy
// value forces the quirk on, anything else set ("0", "false", "") forces it off.
// Unset, empty or "auto" keeps Auto; "forceon"/"on" and "forceoff"/"off"/"no" name the two
// overrides; anything else is read as a flag (1/true -> ForceOn, 0/false -> ForceOff).
inline MG_Config::QuirkOverride QueryEnvQuirkOverride(const String& key) {
auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) {
return MG_Config::QuirkOverride::Auto;
}
String lowered = it->second;
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (lowered.empty() || lowered == "auto") return MG_Config::QuirkOverride::Auto;
if (lowered == "forceon" || lowered == "on") return MG_Config::QuirkOverride::ForceOn;
if (lowered == "forceoff" || lowered == "off" || lowered == "no") return MG_Config::QuirkOverride::ForceOff;
return IsTruthyValue(it->second) ? MG_Config::QuirkOverride::ForceOn
: MG_Config::QuirkOverride::ForceOff;
}
@@ -340,16 +369,28 @@ namespace MobileGL::MG_ConfigLoader {
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
#endif
features.MagmaDisableSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_SUBGROUP");
features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
features.MagmaFixIterationRPSubgroupScratch =
QueryEnvQuirkOverride("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
features.MagmaIterationRPFixBarrier = QueryEnvFlag("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER");
features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
features.SubgroupEmulation = QueryEnvQuirkOverride("MOBILEGL_SUBGROUP_EMULATION");
features.IterationRPFix = QueryEnvQuirkOverride("MOBILEGL_ITRP_FIX");
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);
@@ -357,6 +398,9 @@ namespace MobileGL::MG_ConfigLoader {
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
features.EsprytUnlocatedIoBlocks = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS");
features.EsprytDeferGeometryEmits = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_DEFER_GEOMETRY_EMITS");
features.EsprytAtomicSubgroupScratch = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_ATOMIC_SUBGROUP_SCRATCH");
features.EsprytInvalidImagePlaceholder = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_INVALID_IMAGE_PLACEHOLDER");
features.PointSizeDemotion = QueryEnvQuirkOverride("MOBILEGL_POINT_SIZE_DEMOTION");
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
@@ -383,37 +427,40 @@ namespace MobileGL::MG_ConfigLoader {
features.EsprytWidenPacked16Storage =
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE");
features.MagmaPrimGenQueryReroute = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE");
features.MagmaE5RawReads = QueryEnvQuirkOverride("MOBILEGL_MAGMA_E5_RAW_READS");
features.MagmaSplitSliceCopies = QueryEnvQuirkOverride("MOBILEGL_MAGMA_SPLIT_SLICE_COPIES");
features.MagmaReemitStateAfterDiscard = QueryEnvQuirkOverride("MOBILEGL_MAGMA_REEMIT_STATE_AFTER_DISCARD");
// 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 +506,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 +534,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 +558,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 +588,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 +631,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 +728,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 +741,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 +771,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 +778,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();
+80 -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;
}
};
@@ -317,6 +309,22 @@ namespace MobileGL {
void (*SetSwapInterval)(Int interval);
};
// Device features that decide whether an extension string may be advertised, beyond the
// per-backend positional flags of BuildAdvertisedExtensions. Each is the PROBED driver
// capability (an ES version / extension on DirectGLES, a VkPhysicalDeviceFeatures bit on
// DirectVulkan), never a vendor guess. Defaults describe the no-device case.
struct AdvertisedFeatureSupport {
// ES 3.2 or GL_EXT_tessellation_shader; VkPhysicalDeviceFeatures::tessellationShader.
Bool TessellationShader = false;
// At least one cull distance: GL_EXT_clip_cull_distance with a non-zero
// GL_MAX_CULL_DISTANCES; VkPhysicalDeviceFeatures::shaderCullDistance with a non-zero
// maxCullDistances.
Bool CullDistance = false;
// GL_EXT_clip_control on DirectGLES. DirectVulkan applies clip control in its own
// position fixup and does not read this.
Bool ClipControl = false;
};
// Coarse GPU vendor identity for gating device-specific quirks. Detected from the
// Vulkan physical-device vendorID or the GLES GL_VENDOR/GL_RENDERER strings; stays
// Unknown when detection is inconclusive, in which case auto-gated quirks stay off.
@@ -419,6 +427,8 @@ namespace MobileGL {
Int MaxImageUnits = 8;
Int MaxCombinedImageUniforms = 8;
Int MaxVertexImageUniforms = 0;
Int MaxTessControlImageUniforms = 0;
Int MaxTessEvaluationImageUniforms = 0;
Int MaxGeometryImageUniforms = 0;
Int MaxFragmentImageUniforms = 8;
Int MaxComputeImageUniforms = 8;
@@ -563,8 +573,70 @@ namespace MobileGL {
Uint32 SubgroupSize = 0;
Uint32 SubgroupSupportedStages = 0;
Uint32 SubgroupSupportedFeatures = 0;
// GL_SAMPLE_POSITION (GL 4.6 core 14.3.1.1) as the device actually places the samples
// of a 2-, 4-, 8- and 16-sample pixel: (x, y) pairs in [0, 1], the N-sample row starting
// at pair N - 2. Bit log2(N) of SamplePositionCountMask says row N was filled in; a row
// the backend could not determine stays unset rather than being guessed. Read and
// written through Get/SetSamplePositions only.
static constexpr Int kMaxSamplePositionCount = 16;
Float SamplePositions[60] = {}; // 2 * (2 + 4 + 8 + 16)
Uint32 SamplePositionCountMask = 0;
Bool SubgroupQuadOperationsInAllStages = false;
GpuVendorKind GpuVendor = GpuVendorKind::Unknown;
// GL_EXT_fragment_shading_rate and its two companions. Each is a statement about the
// device, so all of them default to "absent" and the extension strings are advertised
// only from these: DirectGLES fills them from the driver's own
// EXT_fragment_shading_rate(_primitive, _attachment), DirectVulkan from
// VK_KHR_fragment_shading_rate's pipeline / primitive / attachment features.
// SupportsFragmentShadingRate glShadingRateEXT and gl_ShadingRateEXT
// SupportsPrimitiveFragmentShadingRate gl_PrimitiveShadingRateEXT, combiner op 0
// SupportsAttachmentFragmentShadingRate the R8UI rate attachment, combiner op 1
Bool SupportsFragmentShadingRate = false;
Bool SupportsPrimitiveFragmentShadingRate = false;
Bool SupportsAttachmentFragmentShadingRate = false;
// The four GL_FRAGMENT_SHADING_RATE_*_SUPPORTED_EXT booleans, as the device answers them.
Bool FragmentShadingRateNonTrivialCombiners = false;
Bool FragmentShadingRateWithShaderDepthStencilWrites = false;
Bool FragmentShadingRateWithSampleMask = false;
Bool FragmentShadingRatePrimitiveWithMultiViewport = false;
Bool FragmentShadingRateAttachmentWithDefaultFramebuffer = false;
// What glGetFragmentShadingRatesEXT answers, one mask per sample count 1, 2, 4 .. 64
// (index = log2(samples)): bit (log2(w) << 2 | log2(h)) is set when that rate is
// honoured exactly at that sample count, i.e. gl_ShadingRateEXT reads it back
// unclamped. Zero masks mean no rate at all, which is what a sample count the device
// cannot shade coarsely answers.
Uint16 FragmentShadingRateMasks[7] = {};
// The GL_{MIN,MAX}_FRAGMENT_SHADING_RATE_ATTACHMENT_TEXEL_{WIDTH,HEIGHT}_EXT,
// _ASPECT_RATIO_EXT and _LAYERS_EXT limits. Meaningful only with the attachment.
Int MinFragmentShadingRateAttachmentTexelWidth = 0;
Int MinFragmentShadingRateAttachmentTexelHeight = 0;
Int MaxFragmentShadingRateAttachmentTexelWidth = 0;
Int MaxFragmentShadingRateAttachmentTexelHeight = 0;
Int MaxFragmentShadingRateAttachmentTexelAspectRatio = 0;
Int MaxFragmentShadingRateAttachmentLayers = 0;
static constexpr Bool IsSamplePositionCount(Int samples) {
return samples >= 2 && samples <= kMaxSamplePositionCount && (samples & (samples - 1)) == 0;
}
static constexpr Uint32 SamplePositionCountBit(Int samples) {
return 1u << static_cast<Uint32>(std::countr_zero(static_cast<Uint32>(samples)));
}
// `xy` holds `samples` (x, y) pairs.
void SetSamplePositions(Int samples, const Float* xy) {
if (!IsSamplePositionCount(samples)) return;
for (Int i = 0; i < 2 * samples; ++i) SamplePositions[2 * (samples - 2) + i] = xy[i];
SamplePositionCountMask |= SamplePositionCountBit(samples);
}
Bool GetSamplePosition(Int samples, Int index, Float* outXY) const {
if (!IsSamplePositionCount(samples) || index < 0 || index >= samples ||
(SamplePositionCountMask & SamplePositionCountBit(samples)) == 0) {
return false;
}
outXY[0] = SamplePositions[2 * (samples - 2 + index)];
outXY[1] = SamplePositions[2 * (samples - 2 + index) + 1];
return true;
}
};
enum class WindowBackend {
@@ -12,6 +12,7 @@
#include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectGLES/Utils.h>
#include <MG_Backend/DirectGLES/SubgroupPolicy.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Classifiers/TextureEnumClassifier.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
@@ -32,7 +33,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 +78,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
return false;
}
#endif
void ClearGLErrors(const MG_External::GLESFunctionsTable& gl) {
if (!gl.glGetError) return;
@@ -455,6 +454,59 @@ namespace MobileGL::MG_Backend::DirectGLES {
return supported;
}
// GL_SAMPLE_POSITION is a property of the multisample framebuffer the driver builds, so it
// is asked once per power-of-two sample count, of a throwaway RGBA8 renderbuffer framebuffer,
// through the driver's own glGetMultisamplefv (ES 3.1 core). A count the driver rounds up
// reports the first positions of the pattern it really uses - the samples gl_SampleID
// 0..count-1 address in a shader. Counts that cannot be built are left unset.
void ProbeSamplePositions(const MG_External::GLESFunctionsTable& gl,
const MG_External::GLESCapabilities& capabilities,
DynamicBackendParameters& dynamicParameters) {
const auto& version = capabilities.GLESVersion;
if (version.Major < 3 || (version.Major == 3 && version.Minor < 1)) return;
if (!gl.glGetMultisamplefv || !gl.glGenFramebuffers || !gl.glBindFramebuffer ||
!gl.glFramebufferRenderbuffer || !gl.glCheckFramebufferStatus || !gl.glDeleteFramebuffers ||
!gl.glGenRenderbuffers || !gl.glBindRenderbuffer || !gl.glRenderbufferStorageMultisample ||
!gl.glDeleteRenderbuffers || !gl.glGetIntegerv) {
return;
}
GLint prevFramebuffer = 0, prevRenderbuffer = 0;
gl.glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFramebuffer);
gl.glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRenderbuffer);
for (Int samples = 2; samples <= DynamicBackendParameters::kMaxSamplePositionCount &&
samples <= capabilities.MaxSamples;
samples *= 2) {
GLuint framebuffer = 0, renderbuffer = 0;
gl.glGenFramebuffers(1, &framebuffer);
gl.glGenRenderbuffers(1, &renderbuffer);
gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
gl.glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_RGBA8, 4, 4);
gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
GLint actualSamples = 0;
if (gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) {
gl.glGetIntegerv(GL_SAMPLES, &actualSamples);
}
if (actualSamples >= samples) {
Float positions[2 * DynamicBackendParameters::kMaxSamplePositionCount] = {};
for (Int index = 0; index < samples; ++index) {
gl.glGetMultisamplefv(GL_SAMPLE_POSITION, static_cast<GLuint>(index), &positions[2 * index]);
}
dynamicParameters.SetSamplePositions(samples, positions);
}
gl.glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(prevFramebuffer));
gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(prevRenderbuffer));
gl.glDeleteFramebuffers(1, &framebuffer);
gl.glDeleteRenderbuffers(1, &renderbuffer);
}
// A count the driver refused leaves its error behind; it must not reach the application.
if (gl.glGetError) {
for (Int drained = 0; drained < 16 && gl.glGetError() != GL_NO_ERROR; ++drained) {
}
}
}
Bool ProbeFramebufferCompletenessForRenderbuffer(const MG_External::GLESFunctionsTable& gl, GLuint renderbuffer,
TextureInternalFormat format) {
GLuint framebuffer = 0;
@@ -843,7 +895,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy,
capabilities.SupportsDrawIndirect,
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance,
capabilities.SupportsTextureView, capabilities.SupportsTextureCubeMapArray);
capabilities.SupportsTextureView, capabilities.SupportsTextureCubeMapArray,
AdvertisedFeatureSupport{.TessellationShader = capabilities.SupportsTessellationShader,
.CullDistance = capabilities.MaxCullDistances > 0,
.ClipControl = capabilities.SupportsClipControl});
// Exactly what InitCapabilities forwards into the dynamic parameters: the base
// extension and _primitive when the driver has them, _attachment not yet.
auto& extensions = MutableRendererInfo().RendererGLInfo.Extensions;
if (capabilities.SupportsFragmentShadingRate) {
extensions.push_back(E_GL_EXT_fragment_shading_rate);
if (capabilities.SupportsFragmentShadingRatePrimitive) {
extensions.push_back(E_GL_EXT_fragment_shading_rate_primitive);
}
}
// Native where the ES driver has subgroups, emulated in the compute stage where it
// has none (SubgroupPolicy.h); the dynamic parameters below say which.
if (ResolveEsprytSubgroupImplementation(capabilities) != SubgroupImplementation::None) {
extensions.push_back(E_GL_KHR_shader_subgroup);
}
}
} // namespace
@@ -860,7 +929,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 +940,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 +1052,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 +1144,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 +1156,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 +1194,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 +1265,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 +1318,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 +1344,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 +1374,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 +1411,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) {
@@ -1402,7 +1445,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
Bool drawIndirectSupported,
Bool nonZeroIndirectBaseInstanceSupported,
Bool textureViewSupported, Bool cubeMapArraySupported) {
Bool textureViewSupported, Bool cubeMapArraySupported,
const AdvertisedFeatureSupport& features) {
Vector<GLExtension> extensions = {
// The version tokens have to reach the version the backend actually claims:
// TargetGLVersion is {4,6,0}, and a list that stopped at OpenGL40 told an
@@ -1513,10 +1557,42 @@ namespace MobileGL::MG_Backend::DirectGLES {
// glDepthRangeArrayv / glDepthRangeIndexed / glGetFloati_v / glGetDoublei_v, over the
// 16 viewports GL_MAX_VIEWPORTS reports and the per-viewport routing emulation.
E_GL_ARB_viewport_array,
// Core in the 4.x versions this backend already claims, and named so a context of an
// older requested version reaches the same, already-implemented surface (the conformance
// suite gates its GL 3.x runs of these on the string alone):
// - transform feedback objects, pause/resume and glDrawTransformFeedback (4.0);
E_GL_ARB_transform_feedback2,
// - multiple capture buffers per stream, gl_SkipComponents / gl_NextBuffer, the
// indexed queries and glDrawTransformFeedbackStream (4.0);
E_GL_ARB_transform_feedback3,
// - GL_RGB10_A2UI (3.3), GL_RGB9_E5 (3.0) and the integer colour formats (3.0);
E_GL_ARB_texture_rgb10_a2ui, E_GL_EXT_texture_shared_exponent, E_GL_EXT_texture_integer,
// - the eleven pipeline-statistics query targets (4.6). Accepted by glBeginQuery and
// answered with GL_QUERY_COUNTER_BITS = 0, the spec's own "this counter is not
// instrumented" signal (see IsPipelineStatisticsQueryTarget in GL_Query.cpp);
E_GL_ARB_pipeline_statistics_query,
// - mix() over integer and boolean vectors in GLSL below 4.50. The frontend compiles it
// to OpSelect, which the backend prints back as a core boolean mix.
E_GL_EXT_shader_integer_mix,
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
// extension explicitly permits. It is also the only thing that
// exposes glProgramParameteri before GL 4.1.
E_GL_ARB_get_program_binary};
// Tessellation is core from GL 4.0, but on the host it is ES 3.2 or
// GL_EXT_tessellation_shader, so the string follows the driver. GL_Query also reads it to
// open the two tessellation pipeline-statistics targets.
if (features.TessellationShader) {
extensions.push_back(E_GL_ARB_tessellation_shader);
}
// gl_CullDistance goes through GL_EXT_clip_cull_distance, whose cull half the loader only
// reports where the driver answers a non-zero GL_MAX_CULL_DISTANCES.
if (features.CullDistance) {
extensions.push_back(E_GL_ARB_cull_distance);
}
// glClipControl is forwarded to the driver's glClipControlEXT.
if (features.ClipControl) {
extensions.push_back(E_GL_ARB_clip_control);
}
// Minecraft 26.3 checks this prerequisite before it even considers
// GL_ARB_multi_draw_indirect. ES 3.1 supplies both single-draw entry points; the loader
// folds the version and pointer checks into SupportsDrawIndirect.
@@ -1633,7 +1709,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 +1736,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;
@@ -1846,6 +1915,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_dynamicParameters.MaxCombinedImageUniforms});
};
m_dynamicParameters.MaxVertexImageUniforms = clampStageImageUniforms(m_GLESCapabilities.MaxVertexImageUniforms);
m_dynamicParameters.MaxTessControlImageUniforms =
clampStageImageUniforms(m_GLESCapabilities.MaxTessControlImageUniforms);
m_dynamicParameters.MaxTessEvaluationImageUniforms =
clampStageImageUniforms(m_GLESCapabilities.MaxTessEvaluationImageUniforms);
m_dynamicParameters.MaxGeometryImageUniforms =
clampStageImageUniforms(m_GLESCapabilities.MaxGeometryImageUniforms);
m_dynamicParameters.MaxFragmentImageUniforms =
@@ -1854,6 +1927,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
clampStageImageUniforms(m_GLESCapabilities.MaxComputeImageUniforms);
m_dynamicParameters.SupportsDistinctDepthStencilAttachments =
ProbeDistinctDepthStencilAttachments(DirectGLES::g_GLESFuncs);
ProbeSamplePositions(DirectGLES::g_GLESFuncs, m_GLESCapabilities, m_dynamicParameters);
// SyncAttachmentObject routes a layered upload target to glFramebufferTextureLayer with the
// attachment's layer passed through, so this backend really does render to the layer it was
// given - provided the driver resolved the entry point at all.
@@ -1881,6 +1955,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
// mathematically mandatory here, on every device, forever - which is why this stays false
// regardless of what the driver underneath happens to support.
m_dynamicParameters.SupportsShaderFloat64 = false;
{
const EsprytSubgroupParameters subgroups = ResolveEsprytSubgroupParameters(m_GLESCapabilities);
m_dynamicParameters.SubgroupSize = subgroups.size;
m_dynamicParameters.SubgroupSupportedStages = subgroups.stages;
m_dynamicParameters.SubgroupSupportedFeatures = subgroups.features;
m_dynamicParameters.SubgroupQuadOperationsInAllStages = subgroups.quadInAllStages;
MGLOG_I("DirectGLES: GL_KHR_shader_subgroup %s (size %u, stages 0x%x, features 0x%x)",
SubgroupImplementationName(ResolveEsprytSubgroupImplementation(m_GLESCapabilities)),
subgroups.size, subgroups.stages, subgroups.features);
}
// Follows the line above, and must: OpenGL ES has no double-precision vertex format and no
// fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to land here.
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
@@ -1930,6 +2014,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
// describe behaviour this backend does not implement.
m_dynamicParameters.LayerProvokingVertex = m_GLESCapabilities.LayerProvokingVertex;
m_dynamicParameters.ViewportIndexProvokingVertex = m_GLESCapabilities.ViewportIndexProvokingVertex;
// GL_EXT_fragment_shading_rate is a straight pass-through: the pipeline rate and the
// combiner ops are forwarded per draw (SyncRenderState), gl_ShadingRateEXT and
// gl_PrimitiveShadingRateEXT are emitted into the ESSL as the driver's own built-ins, and
// the device answers every query. The attachment is not forwarded yet, so it is not
// claimed, whatever the driver offers.
m_dynamicParameters.SupportsFragmentShadingRate = m_GLESCapabilities.SupportsFragmentShadingRate;
m_dynamicParameters.SupportsPrimitiveFragmentShadingRate =
m_GLESCapabilities.SupportsFragmentShadingRatePrimitive;
m_dynamicParameters.SupportsAttachmentFragmentShadingRate = false;
m_dynamicParameters.FragmentShadingRateNonTrivialCombiners =
m_GLESCapabilities.FragmentShadingRateNonTrivialCombiners;
m_dynamicParameters.FragmentShadingRateWithShaderDepthStencilWrites =
m_GLESCapabilities.FragmentShadingRateWithShaderDepthStencilWrites;
m_dynamicParameters.FragmentShadingRateWithSampleMask = m_GLESCapabilities.FragmentShadingRateWithSampleMask;
m_dynamicParameters.FragmentShadingRatePrimitiveWithMultiViewport =
m_GLESCapabilities.FragmentShadingRatePrimitiveWithMultiViewport;
for (SizeT i = 0; i < std::size(m_dynamicParameters.FragmentShadingRateMasks); ++i) {
m_dynamicParameters.FragmentShadingRateMasks[i] = m_GLESCapabilities.FragmentShadingRateMasks[i];
}
m_dynamicParameters.MaxViewportWidth = m_GLESCapabilities.MaxViewportWidth;
m_dynamicParameters.MaxViewportHeight = m_GLESCapabilities.MaxViewportHeight;
m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
@@ -114,12 +114,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
// for a device whose timer queries / anisotropic filtering / native indirect draws /
// non-zero indirect baseInstance semantics / EXT-OES texture views are (or are not) usable.
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
// non-zero indirect baseInstance semantics / EXT-OES texture views are (or are not) usable,
// plus the feature set in `features`. The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied
// inside.
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
Bool drawIndirectSupported,
Bool nonZeroIndirectBaseInstanceSupported,
Bool textureViewSupported, Bool cubeMapArraySupported);
Bool textureViewSupported, Bool cubeMapArraySupported,
const AdvertisedFeatureSupport& features = {});
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
// initialized backend returns from GetBackendAPIVersionString (and that ends up
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
@@ -0,0 +1,55 @@
// MobileGL - MobileGL/MG_Backend/DirectGLES/SubgroupPolicy.h
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include <MG_Backend/SubgroupImplementation.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
namespace MobileGL::MG_Backend::DirectGLES {
// How DirectGLES implements GL_KHR_shader_subgroup (MG_Backend/SubgroupImplementation.h),
// shared by the extension list, the dynamic parameters and the ESSL transpile.
//
// NATIVE where the ES driver itself has GL_KHR_shader_subgroup: the stage is transpiled
// with its GroupNonUniform operations intact and SPIRV-Cross spells them as the
// GL_KHR_shader_subgroup_* built-ins. Without Vulkan semantics SPIRV-Cross can only print
// the basic, vote and ballot operations (plus add/mul reductions, which do not make up the
// arithmetic feature), so those three features are all the native path advertises.
//
// EMULATED where it has none (Adreno's ES driver among them): the compute stage is lowered
// onto a 32-lane virtual subgroup in workgroup-shared memory before the transpile
// (EmulateSubgroupsPass), which needs nothing from the driver beyond barrier() and shared
// memory, and advertises every feature.
inline SubgroupImplementation ResolveEsprytSubgroupImplementation(const MG_External::GLESCapabilities& caps) {
return ResolveSubgroupImplementation(caps.SupportsShaderSubgroup);
}
inline constexpr Uint32 kEsslPrintableSubgroupFeatures =
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_VOTE_BIT_KHR | GL_SUBGROUP_FEATURE_BALLOT_BIT_KHR;
struct EsprytSubgroupParameters {
Uint32 size = 0;
Uint32 stages = 0;
Uint32 features = 0;
Bool quadInAllStages = false;
};
inline EsprytSubgroupParameters ResolveEsprytSubgroupParameters(const MG_External::GLESCapabilities& caps) {
switch (ResolveEsprytSubgroupImplementation(caps)) {
case SubgroupImplementation::Native:
return {caps.SubgroupSize, caps.SubgroupSupportedStages,
caps.SubgroupSupportedFeatures & kEsslPrintableSubgroupFeatures, false};
case SubgroupImplementation::Emulated:
return {kEmulatedSubgroupSize, kEmulatedSubgroupStages, kEmulatedSubgroupFeatures, false};
case SubgroupImplementation::None:
break;
}
return {};
}
} // namespace MobileGL::MG_Backend::DirectGLES
+287 -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));
@@ -984,6 +956,170 @@ namespace MobileGL::MG_Backend::DirectGLES {
return std::nullopt;
}
String RedeclareClipCullDistancesInPerVertexBlock(String glslCode, const GLenum shaderType) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (shaderType != GL_VERTEX_SHADER && shaderType != GL_TESS_EVALUATION_SHADER &&
shaderType != GL_GEOMETRY_SHADER) {
return glslCode;
}
if (glslCode.find("gl_ClipDistance[") == String::npos &&
glslCode.find("gl_CullDistance[") == String::npos) {
return glslCode;
}
// Whole lines only: the declarations SPIRV-Cross prints are one statement per line,
// and anchoring on the line keeps a use of the array in the body from ever matching.
static const std::regex standaloneRegex(
R"(^\s*out\s+(?:highp\s+)?float\s+(gl_ClipDistance|gl_CullDistance)\s*\[\s*([0-9]+)\s*\]\s*;\s*$)");
static const std::regex invariantPositionRegex(R"(^\s*invariant\s+gl_Position\s*;\s*$)");
Vector<String> lines;
SizeT lineStart = 0;
while (lineStart <= glslCode.size()) {
const SizeT lineEnd = glslCode.find('\n', lineStart);
if (lineEnd == String::npos) {
lines.push_back(glslCode.substr(lineStart));
break;
}
lines.push_back(glslCode.substr(lineStart, lineEnd - lineStart));
lineStart = lineEnd + 1;
}
String clipSize;
String cullSize;
SizeT insertAt = lines.size();
SizeT invariantLine = lines.size();
for (SizeT i = 0; i < lines.size(); ++i) {
std::smatch match;
if (std::regex_match(lines[i], match, standaloneRegex)) {
(match[1] == "gl_ClipDistance" ? clipSize : cullSize) = match[2].str();
if (insertAt == lines.size()) insertAt = i;
} else if (invariantLine == lines.size() && std::regex_match(lines[i], invariantPositionRegex)) {
invariantLine = i;
}
}
if (insertAt == lines.size()) {
return glslCode;
}
// A WRITE of gl_PointSize is a bare identifier; a read of the previous stage's value
// is a member access on gl_in, which is not this block's business.
const auto isIdentifierChar = [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '_';
};
Bool writesPointSize = false;
static constexpr const char* kPointSize = "gl_PointSize";
const SizeT pointSizeLength = std::strlen(kPointSize);
for (SizeT pos = glslCode.find(kPointSize); pos != String::npos && !writesPointSize;
pos = glslCode.find(kPointSize, pos + 1)) {
if (pos + pointSizeLength < glslCode.size() && isIdentifierChar(glslCode[pos + pointSizeLength])) {
continue;
}
SizeT before = pos;
while (before > 0 && std::isspace(static_cast<unsigned char>(glslCode[before - 1]))) --before;
if (before > 0 && (glslCode[before - 1] == '.' || isIdentifierChar(glslCode[before - 1]))) {
continue;
}
writesPointSize = true;
}
const Bool invariantPosition = invariantLine != lines.size();
String block = "out gl_PerVertex\n{\n";
block += invariantPosition ? " invariant highp vec4 gl_Position;\n" : " highp vec4 gl_Position;\n";
if (writesPointSize) block += " highp float gl_PointSize;\n";
if (!clipSize.empty()) block += " highp float gl_ClipDistance[" + clipSize + "];\n";
if (!cullSize.empty()) block += " highp float gl_CullDistance[" + cullSize + "];\n";
block += "};";
String result;
result.reserve(glslCode.size() + block.size());
for (SizeT i = 0; i < lines.size(); ++i) {
if (i == insertAt) {
result += block;
} else if (i == invariantLine || std::regex_match(lines[i], standaloneRegex)) {
continue;
} else {
result += lines[i];
}
if (i + 1 < lines.size()) result += '\n';
}
return result;
}
String RedeclareClipCullDistanceInputsInPerVertexBlock(String glslCode, const GLenum shaderType,
const String& producerOutMembers) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const Bool tessellation = shaderType == GL_TESS_CONTROL_SHADER || shaderType == GL_TESS_EVALUATION_SHADER;
if (!tessellation && shaderType != GL_GEOMETRY_SHADER) return glslCode;
if (producerOutMembers.find("gl_ClipDistance") == String::npos &&
producerOutMembers.find("gl_CullDistance") == String::npos) {
return glslCode;
}
if (ExtractPerVertexBlockMembers(glslCode, /*input=*/true).has_value()) return glslCode;
// Every gl_in member the stage reads has to be in the redeclared block, or the stage
// stops compiling; one the producer does not write cannot be added honestly.
static const std::regex memberRegex(R"(gl_in\s*\[[^\]]*\]\s*\.\s*(gl_[A-Za-z]+))");
Bool readsDistances = false;
for (auto it = std::sregex_iterator(glslCode.begin(), glslCode.end(), memberRegex);
it != std::sregex_iterator(); ++it) {
const String member = (*it)[1].str();
if (member == "gl_ClipDistance" || member == "gl_CullDistance") readsDistances = true;
if (producerOutMembers.find(member) == String::npos) return glslCode;
}
if (!readsDistances) return glslCode;
// `invariant` qualifies outputs only.
String members = producerOutMembers;
for (SizeT pos = members.find("invariant "); pos != String::npos; pos = members.find("invariant ")) {
members.erase(pos, std::strlen("invariant "));
}
while (!members.empty() && std::isspace(static_cast<unsigned char>(members.back()))) members.pop_back();
while (!members.empty() && (members.front() == '\n' || members.front() == '\r')) members.erase(0, 1);
String block = "in gl_PerVertex\n{\n" + members + "\n} gl_in[";
block += tessellation ? "gl_MaxPatchVertices" : "";
block += "];\n";
// After the header (#directives, precision statements and the stage's layout-only
// in/out declarations, which a geometry stage's unsized gl_in[] depends on).
static const std::regex headerRegex(
R"(^\s*(#.*|precision\s.*|layout\s*\(.*\)\s*(in|out)\s*;\s*)?$)");
SizeT insertAt = 0;
SizeT lineStart = 0;
while (lineStart < glslCode.size()) {
SizeT lineEnd = glslCode.find('\n', lineStart);
if (lineEnd == String::npos) lineEnd = glslCode.size();
const String line = glslCode.substr(lineStart, lineEnd - lineStart);
if (!std::regex_match(line, headerRegex)) break;
insertAt = std::min(lineEnd + 1, glslCode.size());
lineStart = lineEnd + 1;
}
glslCode.insert(insertAt, block);
return glslCode;
}
String RequestClipCullDistanceExtension(String glslCode) {
static constexpr const char* kDirective = "#extension GL_EXT_clip_cull_distance : require\n";
if (glslCode.find("GL_EXT_clip_cull_distance") != String::npos ||
(glslCode.find("gl_ClipDistance") == String::npos && glslCode.find("gl_CullDistance") == String::npos)) {
return glslCode;
}
// Right after the #version line, like every other directive this backend adds.
const SizeT versionPos = glslCode.find("#version");
if (versionPos == String::npos) {
return kDirective + glslCode;
}
const SizeT lineEnd = glslCode.find('\n', versionPos);
if (lineEnd == String::npos) {
return glslCode + "\n" + kDirective;
}
glslCode.insert(lineEnd + 1, kDirective);
return glslCode;
}
String BuildPassthroughTessControlEssl(const Uint esslVersion, const Uint patchVertices,
const String& inPerVertexMembers,
const String& outPerVertexMembers,
@@ -1921,6 +2057,124 @@ namespace MobileGL::MG_Backend::DirectGLES {
return result;
}
String LowerZeroGradCubeArrayShadowLookups(const String& glslCode, GLenum shaderType) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (shaderType == GL_FRAGMENT_SHADER || glslCode.find("samplerCubeArrayShadow") == String::npos ||
glslCode.find("textureGrad") == String::npos) {
return glslCode;
}
// Every name declared with the type: uniforms, and the parameters of functions the
// sampler is passed to.
static const std::regex declRegex(R"(samplerCubeArrayShadow\s+([A-Za-z_][A-Za-z0-9_]*))");
UnorderedMap<String, Bool> names;
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), declRegex), end; it != end; ++it) {
names.emplace((*it)[1].str(), true);
}
if (names.empty()) return glslCode;
static constexpr const char* kName = "textureGrad";
static constexpr SizeT kNameLength = 11;
String result = glslCode;
Bool changed = false;
// Right to left, so an earlier call's offsets stay valid.
for (SizeT scan = result.rfind(kName); scan != String::npos;
scan = scan == 0 ? String::npos : result.rfind(kName, scan - 1)) {
if (scan > 0 && IsIdentifierChar(result[scan - 1])) continue;
const SizeT openParen = result.find_first_not_of(" \t", scan + kNameLength);
if (openParen == String::npos || result[openParen] != '(') continue;
const Vector<SizeT> marks = SplitCallArguments(result, openParen);
// sampler, P, compare, dPdx, dPdy
if (marks.size() != 5) continue;
SizeT nameStart = result.find_first_not_of(" \t", openParen + 1);
SizeT nameEnd = nameStart;
while (nameEnd < marks[0] && IsIdentifierChar(result[nameEnd])) ++nameEnd;
if (names.find(result.substr(nameStart, nameEnd - nameStart)) == names.end()) continue;
const auto argument = [&](SizeT index) {
const SizeT start = marks[index - 1] + 1;
String text = result.substr(start, marks[index] - start);
text.erase(std::remove_if(text.begin(), text.end(),
[](char c) { return c == ' ' || c == '\t'; }),
text.end());
return text;
};
if (argument(3) != "vec3(0.0)" || argument(4) != "vec3(0.0)") continue;
result.erase(marks[2], marks[4] - marks[2]);
result.replace(scan, kNameLength, "texture");
changed = true;
}
return changed ? result : glslCode;
}
String ClampUnsignedMultisampleFetches(const String& glslCode) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (glslCode.find("usampler2DMS") == String::npos || glslCode.find("texelFetch") == String::npos) {
return glslCode;
}
// The non-arrayed unsigned multisample samplers - the ones ClampMultisampleFetchPass
// deferred, and the only ones a lookup can name directly as its first argument.
static const std::regex declRegex(
R"(uniform\s+(?:(?:highp|mediump|lowp)\s+)?usampler2DMS(?:Array)?\s+([A-Za-z_][A-Za-z0-9_]*)\s*;)");
UnorderedMap<String, String> clampNames; // sampler name -> sample-max uniform name
Vector<std::pair<SizeT, String>> declEnds; // where each declaration ends, and its uniform
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), declRegex), end; it != end; ++it) {
const String name = (*it)[1].str();
const String clampName = String(MS_SAMPLE_MAX_UNIFORM_PREFIX) + name;
if (clampNames.emplace(name, clampName).second) {
declEnds.emplace_back(static_cast<SizeT>(it->position(0) + it->length(0)), clampName);
}
}
if (clampNames.empty()) {
return glslCode;
}
// Right-to-left, so an earlier call's offsets survive the insertions behind it.
constexpr const char* kFetch = "texelFetch";
const SizeT fetchLength = std::strlen(kFetch);
String result = glslCode;
Vector<String> usedClamps;
for (SizeT scan = result.rfind(kFetch); scan != String::npos;
scan = scan == 0 ? String::npos : result.rfind(kFetch, scan - 1)) {
if (scan > 0 && IsIdentifierChar(result[scan - 1])) continue;
const SizeT openParen = result.find_first_not_of(" \t", scan + fetchLength);
if (openParen == String::npos || result[openParen] != '(') continue; // texelFetchOffset etc.
const Vector<SizeT> marks = SplitCallArguments(result, openParen);
if (marks.size() != 3) continue; // (sampler, coord, sample) is the multisample form
const SizeT firstStart = result.find_first_not_of(" \t", openParen + 1);
SizeT firstEnd = marks[0];
while (firstEnd > firstStart && (result[firstEnd - 1] == ' ' || result[firstEnd - 1] == '\t')) {
--firstEnd;
}
if (firstStart == String::npos || firstEnd <= firstStart) continue;
const auto found = clampNames.find(result.substr(firstStart, firstEnd - firstStart));
if (found == clampNames.end()) continue;
const SizeT sampleStart = result.find_first_not_of(" \t\n", marks[1] + 1);
if (sampleStart == String::npos || sampleStart >= marks[2]) continue;
const SizeT sampleEnd = marks[2];
result.insert(sampleEnd, String(", ") + found->second + ")");
result.insert(sampleStart, "min(");
if (std::find(usedClamps.begin(), usedClamps.end(), found->second) == usedClamps.end()) {
usedClamps.push_back(found->second);
}
}
if (usedClamps.empty()) {
return glslCode;
}
// Declared right after the sampler they bound, back to front so the recorded
// offsets (taken from the unedited text) stay valid: every fetch sits after the
// declarations in the emitted ESSL, so no insertion above has moved them.
for (auto it = declEnds.rbegin(); it != declEnds.rend(); ++it) {
if (std::find(usedClamps.begin(), usedClamps.end(), it->second) == usedClamps.end()) continue;
result.insert(it->first, String("\nuniform highp int ") + it->second + ";");
}
return result;
}
} // namespace PrgramImpl
namespace Utils {
@@ -2362,15 +2616,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 =
+55 -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 {
@@ -388,6 +386,37 @@ namespace MobileGL::MG_Backend::DirectGLES {
// that redeclares a different shape than its neighbours is an ES link error against a
// program that has no other problem.
std::optional<String> ExtractPerVertexBlockMembers(const String& essl, Bool input);
// Moves the clip/cull distance OUTPUTS of a vertex, tessellation evaluation or geometry
// stage into a redeclared `out gl_PerVertex { ... };` block. SPIRV-Cross sizes the two
// arrays with a standalone `out float gl_ClipDistance[N];` - legal desktop GLSL, but an
// ES compiler may refuse it outright ("reserved built-in name"), and leaving the
// declaration out instead compiles but fails the link on an array-size mismatch against
// the fragment stage. A member of the redeclared block is the spelling ESSL accepts.
// gl_Position is always a member (every pre-rasterization stage has it), gl_PointSize
// only when the stage writes it, so the block never names a built-in the stage would
// otherwise not need. A stage-level `invariant gl_Position;` moves onto the member,
// because the bare re-qualification of a block member is not something ESSL allows.
// Fragment inputs and the control stage (whose block SPIRV-Cross already emits) are
// left alone; a stage without the standalone declarations comes back unchanged.
String RedeclareClipCullDistancesInPerVertexBlock(String glslCode, GLenum shaderType);
// The INPUT half of the same problem, for a tessellation control, tessellation evaluation
// or geometry stage that reads gl_in[i].gl_ClipDistance / gl_CullDistance. SPIRV-Cross
// never redeclares an ES input block, so the stage reads the implicitly sized built-in
// arrays - and a driver that packs cull distances after clip distances then places the
// cull array at the implicit clip size rather than at the size the previous stage wrote,
// reading the wrong component. Redeclares `in gl_PerVertex { ... } gl_in[]` with exactly
// the members of the previous stage's output block (`producerOutMembers`, from
// ExtractPerVertexBlockMembers), which is also the shape ES interface matching wants.
// Unchanged when the stage does not read either array, already declares its input block,
// reads a gl_in member the producer's block does not carry, or the producer has no block.
String RedeclareClipCullDistanceInputsInPerVertexBlock(String glslCode, GLenum shaderType,
const String& producerOutMembers);
// Adds `#extension GL_EXT_clip_cull_distance : require` when `glslCode` names
// gl_ClipDistance or gl_CullDistance and does not request it yet. For the synthesized
// pass-through control stage, whose gl_in/gl_out member lists are mirrored from stages
// that may carry the clip/cull arrays: SPIRV-Cross requests the extension for the stages
// it emits, but nothing does for a stage built by hand.
String RequestClipCullDistanceExtension(String glslCode);
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes: "the input
// patch is passed through unmodified", the output patch has PATCH_VERTICES vertices, and
// the levels come from the PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
@@ -547,6 +576,30 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Off by default and only ever set on ANGLE + llvmpipe, where injecting the uniform
// into a constant LOD crashes the driver (MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS).
String EmulateTextureLodBias(const String& glslCode, Bool avoidExplicitLodBias = false);
// A depth-compare lookup at an explicit LOD of 0 on a samplerCubeArrayShadow - what
// texture() is outside the fragment stage - has no GLSL ES spelling: there is no
// textureLod or textureGrad overload for that sampler. SPIRV-Cross prints it as
// textureGrad(s, P, compare, vec3(0.0), vec3(0.0)) anyway; Adreno compiles that and
// returns wrong comparisons (KHR-GL4x.texture_cube_map_array.sampling, DEPTH32F, vertex/
// tessellation control/geometry/compute). Outside the fragment stage texture(s, P,
// compare) samples level 0 exactly (GLSL ES 3.20 8.9), so those calls are rewritten to
// it. The fragment stage is left alone: there texture() would pick its own level.
String LowerZeroGradCubeArrayShadowLookups(const String& glslCode, GLenum shaderType);
// Prefix of the per-sampler int uniform that carries the bound multisample texture's
// last real sample index into the shader (see ClampUnsignedMultisampleFetches); the
// suffix is the sampler's own name.
constexpr const char* MS_SAMPLE_MAX_UNIFORM_PREFIX = "mg_msSampleMax_";
// The runtime half of ClampMultisampleFetchPass's deferDirectUnsignedFetches. A
// usampler2DMS[Array] reads integer colour textures, which the backend may allocate with
// fewer samples than MobileGL advertises, AND the stencil aspect of multisample
// depth/stencil textures, which it allocates under the (larger) depth ceiling - so no
// single compile-time bound is right for both. Declares one
// `uniform highp int mg_msSampleMax_<sampler>;` per non-arrayed unsigned multisample
// sampler and wraps the sample argument of every texelFetch that names it in
// min(sample, that uniform). Draws push the bound texture's real sample count - 1 into
// it (0, the program's initial value, when nothing multisampled is bound). Returns the
// source unchanged when there is nothing to rewrite.
String ClampUnsignedMultisampleFetches(const String& glslCode);
} // namespace PrgramImpl
namespace Utils {
@@ -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());
@@ -804,7 +792,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
Bool anisotropicFilteringSupported,
Bool nonZeroIndirectBaseInstanceSupported,
Bool cubeMapArraySupported) {
Bool cubeMapArraySupported,
const AdvertisedFeatureSupport& features) {
Vector<GLExtension> extensions = {
// The version tokens have to reach the version the backend actually claims:
// TargetGLVersion is {4,6,0}, and a list that stopped at OpenGL40 told an
@@ -910,10 +899,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// glDepthRangeArrayv / glDepthRangeIndexed / glGetFloati_v / glGetDoublei_v, over the
// 16 viewports GL_MAX_VIEWPORTS reports.
E_GL_ARB_viewport_array,
// Core in the 4.x versions this backend already claims, and named so a context of an
// older requested version reaches the same, already-implemented surface (the conformance
// suite gates its GL 3.x runs of these on the string alone):
// - transform feedback objects, pause/resume and glDrawTransformFeedback (4.0);
E_GL_ARB_transform_feedback2,
// - multiple capture buffers per stream, gl_SkipComponents / gl_NextBuffer, the
// indexed queries and glDrawTransformFeedbackStream (4.0);
E_GL_ARB_transform_feedback3,
// - GL_RGB10_A2UI (3.3), GL_RGB9_E5 (3.0) and the integer colour formats (3.0);
E_GL_ARB_texture_rgb10_a2ui, E_GL_EXT_texture_shared_exponent, E_GL_EXT_texture_integer,
// - the eleven pipeline-statistics query targets (4.6). Accepted by glBeginQuery and
// answered with GL_QUERY_COUNTER_BITS = 0, the spec's own "this counter is not
// instrumented" signal (see IsPipelineStatisticsQueryTarget in GL_Query.cpp);
E_GL_ARB_pipeline_statistics_query,
// - mix() over integer and boolean vectors in GLSL below 4.50. The frontend compiles it
// to OpSelect, which SPIR-V consumes directly.
E_GL_EXT_shader_integer_mix,
// The origin and depth mode select the Y negation and the [-1, 1] -> [0, 1] depth
// remap of the position fixup every program already gets, so no device feature is
// involved (ApplyClipControlToTransformFlags).
E_GL_ARB_clip_control,
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
// extension explicitly permits. It is also the only thing that
// exposes glProgramParameteri before GL 4.1.
E_GL_ARB_get_program_binary};
// Follows the tessellationShader device feature; GL_Query also reads the string to open
// the two tessellation pipeline-statistics targets.
if (features.TessellationShader) {
extensions.push_back(E_GL_ARB_tessellation_shader);
}
// Follows the shaderCullDistance device feature, the same gate GL_MAX_CULL_DISTANCES uses.
if (features.CullDistance) {
extensions.push_back(E_GL_ARB_cull_distance);
}
// Vulkan's drawIndirectFirstInstance feature is optional. Direct base-instance calls work
// without it, but ARB_base_instance also promises non-zero firstInstance in GPU indirect
// commands; the renderer supplies true only when that word is legal and gl_InstanceID can
@@ -921,7 +940,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (nonZeroIndirectBaseInstanceSupported) {
extensions.push_back(E_GL_ARB_base_instance);
}
if (shaderSubgroupSupported && !MG_Config::Features.MagmaDisableSubgroup) {
if (shaderSubgroupSupported) {
extensions.push_back(E_GL_KHR_shader_subgroup);
}
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the Vulkan
@@ -998,7 +1017,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 +1037,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 +1092,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_vulkanCaps = capabilities;
UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions();
if (MGB_CTX_LIVE) {
MGB_CTX->InvalidateCompileEnv();
}
MG_Pipe::MGPipeNotifyCompileEnvChanged();
MutableFormatCapabilities().Clear();
}
@@ -1094,17 +1103,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// real device timestamp support. ApplyVulkanCapabilitiesForTesting may
// run without a renderer; no timer query is advertised then. Rebuilding
// the whole list keeps re-runs idempotent.
// The opt-in emulated compute path (SubgroupSupportPolicy.h) carries the
// extension by itself on devices with no native subgroup support at all; a
// device with native subgroups always advertises - and uses - those.
// The emulated compute path (SubgroupSupportPolicy.h) carries the extension by
// itself on devices with no native subgroup support at all; a device with native
// subgroups advertises - and uses - those unless MOBILEGL_SUBGROUP_EMULATION forces
// the emulation.
const Bool subgroupSupportAdvertised =
m_vulkanCaps.SupportsShaderSubgroup ||
ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup);
ResolveVulkanSubgroupImplementation(m_vulkanCaps.SupportsShaderSubgroup) !=
MG_Backend::SubgroupImplementation::None;
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
subgroupSupportAdvertised, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported(),
m_vulkanCaps.SupportsImageCubeArray);
m_vulkanCaps.SupportsImageCubeArray,
AdvertisedFeatureSupport{.TessellationShader = m_vulkanCaps.SupportsTessellationShader,
.CullDistance = m_vulkanCaps.SupportsShaderCullDistance &&
m_vulkanCaps.MaxCullDistances > 0});
// GL_EXT_fragment_shading_rate rides on VK_KHR_fragment_shading_rate's pipeline rate, and
// its _primitive companion on the primitive rate; both only when the renderer enabled them.
// _attachment is not advertised: no framebuffer carries a rate attachment to this backend.
if (pVulkanRenderer != nullptr && pVulkanRenderer->GetFragmentShadingRateSupport().Pipeline) {
m_rendererInfo.RendererGLInfo.Extensions.push_back(E_GL_EXT_fragment_shading_rate);
if (pVulkanRenderer->GetFragmentShadingRateSupport().Primitive) {
m_rendererInfo.RendererGLInfo.Extensions.push_back(E_GL_EXT_fragment_shading_rate_primitive);
}
}
}
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
@@ -1180,6 +1202,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.MaxIntegerSamples = m_vulkanCaps.MaxIntegerSamples;
m_dynamicParameters.MaxSamples = m_vulkanCaps.MaxSamples;
m_dynamicParameters.MaxSampleMaskWords = m_vulkanCaps.MaxSampleMaskWords;
// GL_SAMPLE_POSITION. A device reporting standardSampleLocations places the samples exactly
// at the Vulkan specification's table (Multisampling, "Standard sample locations"), which
// is also what gl_SamplePosition reads back in a shader. Vulkan's sample y grows with the
// framebuffer row, and a framebuffer object's row 0 is GL's y = 0, so the table needs no
// flip to be GL's answer. Without the flag the positions are unknown and stay unset.
if (m_vulkanCaps.StandardSampleLocations) {
static constexpr Float kStandard2[] = {0.75f, 0.75f, 0.25f, 0.25f};
static constexpr Float kStandard4[] = {0.375f, 0.125f, 0.875f, 0.375f, 0.125f, 0.625f, 0.625f, 0.875f};
static constexpr Float kStandard8[] = {0.5625f, 0.3125f, 0.4375f, 0.6875f, 0.8125f, 0.5625f,
0.3125f, 0.1875f, 0.1875f, 0.8125f, 0.0625f, 0.4375f,
0.6875f, 0.9375f, 0.9375f, 0.0625f};
static constexpr Float kStandard16[] = {
0.5625f, 0.5625f, 0.4375f, 0.3125f, 0.3125f, 0.625f, 0.75f, 0.4375f, 0.1875f, 0.375f, 0.625f,
0.8125f, 0.8125f, 0.6875f, 0.6875f, 0.1875f, 0.375f, 0.875f, 0.5f, 0.0625f, 0.25f, 0.125f,
0.125f, 0.75f, 0.0f, 0.5f, 0.9375f, 0.25f, 0.875f, 0.9375f, 0.0625f, 0.0f};
const Float* const tables[] = {kStandard2, kStandard4, kStandard8, kStandard16};
for (Int samples = 2, row = 0; samples <= decltype(m_dynamicParameters)::kMaxSamplePositionCount &&
samples <= m_vulkanCaps.MaxSamples;
samples *= 2, ++row) {
m_dynamicParameters.SetSamplePositions(samples, tables[row]);
}
}
const Int maxSupportedTextureUnits = static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
// GL_MAX_TEXTURE_IMAGE_UNITS is a *per-stage* sampler limit. Adreno/Qualcomm report a huge
// maxPerStageDescriptorSampledImages (descriptor-indexing scale), so clamping it only to our
@@ -1294,6 +1338,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// device reports it, so these are the exact limits the logical device can compile and run.
m_dynamicParameters.MaxVertexImageUniforms =
m_vulkanCaps.SupportsVertexPipelineStoresAndAtomics ? maxPerStageImageUniforms : 0;
// The tessellation stages are vertex-pipeline stages too, so the same feature gates them.
m_dynamicParameters.MaxTessControlImageUniforms = m_dynamicParameters.MaxVertexImageUniforms;
m_dynamicParameters.MaxTessEvaluationImageUniforms = m_dynamicParameters.MaxVertexImageUniforms;
m_dynamicParameters.MaxGeometryImageUniforms =
m_vulkanCaps.SupportsVertexPipelineStoresAndAtomics && m_vulkanCaps.SupportsGeometryShader
? maxPerStageImageUniforms
@@ -1434,14 +1481,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
m_dynamicParameters.MaxShaderStorageBlockSize =
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
if (m_vulkanCaps.SupportsShaderSubgroup) {
const MG_Backend::SubgroupImplementation subgroupImplementation =
ResolveVulkanSubgroupImplementation(m_vulkanCaps.SupportsShaderSubgroup);
if (subgroupImplementation == MG_Backend::SubgroupImplementation::Native) {
m_dynamicParameters.SubgroupSize = m_vulkanCaps.SubgroupSize;
m_dynamicParameters.SubgroupSupportedStages = mapShaderStages(m_vulkanCaps.SubgroupSupportedStages);
m_dynamicParameters.SubgroupSupportedFeatures =
mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
m_dynamicParameters.SubgroupQuadOperationsInAllStages = m_vulkanCaps.SubgroupQuadOperationsInAllStages;
} else if (ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup)) {
// MOBILEGL_MAGMA_EMULATE_SUBGROUP on a device with no native subgroups: the
} else if (subgroupImplementation == MG_Backend::SubgroupImplementation::Emulated) {
// A device with no native subgroups (or MOBILEGL_SUBGROUP_EMULATION forcing it): the
// advertised values describe the 32-lane virtual subgroup the compute
// lowering implements (SubgroupSupportPolicy.h / EmulateSubgroupsPass).
// GL requires the advertisement and the execution to agree, and on this
@@ -1450,8 +1499,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.SubgroupSupportedStages = kEmulatedSubgroupStages;
m_dynamicParameters.SubgroupSupportedFeatures = kEmulatedSubgroupFeatures;
m_dynamicParameters.SubgroupQuadOperationsInAllStages = false;
MGLOG_I("DirectVulkan: emulating 32-lane compute subgroups "
"(MOBILEGL_MAGMA_EMULATE_SUBGROUP, no native subgroup support)");
MGLOG_I("DirectVulkan: emulating 32-lane compute subgroups (%s)",
m_vulkanCaps.SupportsShaderSubgroup ? "forced by MOBILEGL_SUBGROUP_EMULATION"
: "no native subgroup support");
} else {
m_dynamicParameters.SubgroupSize = 0;
m_dynamicParameters.SubgroupSupportedStages = 0;
@@ -1489,5 +1539,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.GpuVendor = GpuVendorKind::Unknown;
break;
}
// GL_EXT_fragment_shading_rate: what the renderer enabled of VK_KHR_fragment_shading_rate.
// The attachment rate stays off (see UpdateAdvertisedExtensions), and with it every limit
// that only describes the attachment.
{
const FragmentShadingRateSupport fsr =
pVulkanRenderer != nullptr ? pVulkanRenderer->GetFragmentShadingRateSupport() : FragmentShadingRateSupport{};
m_dynamicParameters.SupportsFragmentShadingRate = fsr.Pipeline;
m_dynamicParameters.SupportsPrimitiveFragmentShadingRate = fsr.Pipeline && fsr.Primitive;
m_dynamicParameters.SupportsAttachmentFragmentShadingRate = false;
m_dynamicParameters.FragmentShadingRateNonTrivialCombiners = fsr.NonTrivialCombiners;
m_dynamicParameters.FragmentShadingRateWithShaderDepthStencilWrites = fsr.WithShaderDepthStencilWrites;
m_dynamicParameters.FragmentShadingRateWithSampleMask = fsr.WithSampleMask;
m_dynamicParameters.FragmentShadingRatePrimitiveWithMultiViewport = fsr.PrimitiveWithMultipleViewports;
m_dynamicParameters.FragmentShadingRateAttachmentWithDefaultFramebuffer = false;
for (SizeT i = 0; i < std::size(m_dynamicParameters.FragmentShadingRateMasks); ++i) {
m_dynamicParameters.FragmentShadingRateMasks[i] = fsr.Pipeline ? fsr.RateMasks[i] : 0;
}
m_dynamicParameters.MinFragmentShadingRateAttachmentTexelWidth = 0;
m_dynamicParameters.MinFragmentShadingRateAttachmentTexelHeight = 0;
m_dynamicParameters.MaxFragmentShadingRateAttachmentTexelWidth = 0;
m_dynamicParameters.MaxFragmentShadingRateAttachmentTexelHeight = 0;
m_dynamicParameters.MaxFragmentShadingRateAttachmentTexelAspectRatio = 0;
m_dynamicParameters.MaxFragmentShadingRateAttachmentLayers = 0;
}
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -109,13 +109,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const RendererInfo& GetRendererIdentity();
// The full OpenGL extension list Magma advertises (glGetString(GL_EXTENSIONS)) for
// a device with the given raw capabilities. The MOBILEGL_MAGMA_DISABLE_SUBGROUP and
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
// the detected device support (passing an already-gated value is harmless).
// a device with the given raw capabilities. The MOBILEGL_DISABLE_TIMERQUERY escape hatch
// is applied inside, so callers pass the detected device support (passing an
// already-gated value is harmless). `shaderSubgroupSupported` is whether
// GL_KHR_shader_subgroup is offered at all, natively or emulated.
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
Bool anisotropicFilteringSupported,
Bool nonZeroIndirectBaseInstanceSupported,
Bool cubeMapArraySupported);
Bool cubeMapArraySupported,
const AdvertisedFeatureSupport& features = {});
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
// string an initialized backend returns from GetBackendAPIVersionString (and that
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,72 @@ 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.nonLayeredStages, sizeof(payload.nonLayeredStages)));
const Bool fullCoverageSampleMask = payload.fullCoverageFragmentModule != VK_NULL_HANDLE;
XXHASH_VERIFY(XXH64_update(hashState, &fullCoverageSampleMask, sizeof(fullCoverageSampleMask)));
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 +289,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 +342,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();) {
@@ -412,6 +385,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
payload.colorAttachmentCount,
payload.subpass);
// The fragment shading rate goes last so the common prefix is the eight-state list every
// pipeline shares; it is only present when the device enabled the pipeline rate.
static constexpr VkDynamicState kDynamicStates[] = {
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR,
@@ -420,12 +395,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VK_DYNAMIC_STATE_LINE_WIDTH,
VK_DYNAMIC_STATE_STENCIL_COMPARE_MASK,
VK_DYNAMIC_STATE_STENCIL_WRITE_MASK,
VK_DYNAMIC_STATE_STENCIL_REFERENCE
VK_DYNAMIC_STATE_STENCIL_REFERENCE,
VK_DYNAMIC_STATE_FRAGMENT_SHADING_RATE_KHR
};
VkPipelineDynamicStateCreateInfo dynamicState{};
dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamicState.dynamicStateCount = static_cast<uint32_t>(std::size(kDynamicStates));
dynamicState.dynamicStateCount =
static_cast<uint32_t>(std::size(kDynamicStates)) - (payload.fragmentShadingRateDynamic ? 0u : 1u);
dynamicState.pDynamicStates = kDynamicStates;
VkPipelineInputAssemblyStateCreateInfo ia{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
@@ -532,11 +509,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// wrong). Returning VK_NULL_HANDLE routes this through the same path a driver rejection
// takes: the draw is skipped, nothing is memoised, and the process survives.
const Vector<VkPipelineShaderStageCreateInfo>* effectiveStages = payload.stages;
Vector<VkPipelineShaderStageCreateInfo> stagesWithPassthrough;
if (payload.passthroughTessControlStage.module != VK_NULL_HANDLE) {
stagesWithPassthrough = *payload.stages;
stagesWithPassthrough.push_back(payload.passthroughTessControlStage);
effectiveStages = &stagesWithPassthrough;
Vector<VkPipelineShaderStageCreateInfo> adjustedStages;
if (payload.passthroughTessControlStage.module != VK_NULL_HANDLE ||
payload.fullCoverageFragmentModule != VK_NULL_HANDLE) {
adjustedStages = *payload.stages;
if (payload.passthroughTessControlStage.module != VK_NULL_HANDLE) {
adjustedStages.push_back(payload.passthroughTessControlStage);
}
if (payload.fullCoverageFragmentModule != VK_NULL_HANDLE) {
for (auto& stageInfo : adjustedStages) {
if (stageInfo.stage == VK_SHADER_STAGE_FRAGMENT_BIT) {
stageInfo.module = payload.fullCoverageFragmentModule;
}
}
}
effectiveStages = &adjustedStages;
}
// A static rasterizer discard leaves the pipeline without fragment shader state, and then
// pStages may not name a fragment stage (VUID-VkGraphicsPipelineCreateInfo-pStages-06894).
// Nothing would run it anyway.
if (payload.rasterizerDiscardEnable) {
if (effectiveStages != &adjustedStages) adjustedStages = *effectiveStages;
std::erase_if(adjustedStages, [](const VkPipelineShaderStageCreateInfo& stageInfo) {
return stageInfo.stage == VK_SHADER_STAGE_FRAGMENT_BIT;
});
effectiveStages = &adjustedStages;
}
{
VkShaderStageFlags stagesPresent = 0;
@@ -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
@@ -83,6 +81,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// therefore hashed; the DYNAMIC viewport/scissor arrays the draw pushes must have
// exactly this many elements (VUID-vkCmdDraw-viewportCount-03417/-03418).
Uint32 viewportCount = 1;
// `stages` is the program's nonLayeredStages: the gl_Layer-zeroed modules a draw into
// a non-layered framebuffer uses (ZeroLayerOutputPass). Hashed - the two stage sets
// are different pipelines; the program hash alone names both.
Bool nonLayeredStages = false;
// Non-null: the fragment stage in `stages` is replaced by this module, the program's
// full-coverage gl_SampleMask variant (VkProgramObject::fullCoverageFragmentModule),
// because this draw has no multisample fragment operations. Hashed as presence only -
// the module is a pure function of the program, which programHash already names.
VkShaderModule fullCoverageFragmentModule = VK_NULL_HANDLE;
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
@@ -110,6 +117,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// The fragment module writes gl_FragDepth (SPIR-V DepthReplacing); exempts the
// pipeline from the blended depth-write quirk (see ShouldSuppressDepthWrite).
Bool fragmentReplacesDepth = false;
// The device enabled VK_KHR_fragment_shading_rate's pipeline rate, so the pipeline takes
// VK_DYNAMIC_STATE_FRAGMENT_SHADING_RATE_KHR and every draw sets glShadingRateEXT's rate
// and combiner ops on the command buffer. NOT hashed: it is a device constant, the same
// for every pipeline this renderer builds.
Bool fragmentShadingRateDynamic = false;
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
// The tessellation control stage this renderer synthesized for a program that has
@@ -142,15 +154,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 &&
@@ -3701,6 +3675,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
// Vulkan forbids the PixelCenterInteger execution mode that
// `layout(pixel_center_integer) in vec4 gl_FragCoord;` compiles to, so a module
// carrying it would fail validation and every draw of the program be refused. This
// runs before the FragCoordYFlip variant: the flip then rewrites the unshifted builtin
// read and the -0.5 lands on the flipped value, as GL defines it.
if (stages[i] == ShaderStage::Fragment) {
Vector<Uint> centeredSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerPixelCenterIntegerForVulkan(
moduleSpirvs[i], centeredSpirv, enableSpirvValidation) &&
!centeredSpirv.empty()) {
moduleSpirvs[i] = Move(centeredSpirv);
}
}
if ((flags & ProgramFactory::CompileOptionBit::FragCoordYFlip) && stages[i] == ShaderStage::Fragment) {
Vector<Uint> fragCoordSpirv;
if (TransformSpirvForFragCoordYFlip(moduleSpirvs[i], fragCoordSpirv, m_defaultFramebufferHeight)) {
@@ -3710,8 +3698,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// GL_KHR_shader_subgroup handling (SubgroupSupportPolicy.h). Native subgroup
// operations execute natively; module repairs keep the GL contract intact
// around them. The opt-in emulation path replaces them only on devices with no
// subgroup support at all (MOBILEGL_MAGMA_EMULATE_SUBGROUP).
// around them. The emulation path replaces them on devices with no subgroup
// support at all, or when MOBILEGL_SUBGROUP_EMULATION forces it.
if (stages[i] == ShaderStage::Compute) {
// Program 203 broadcasts the first reduction through
// prefixSumCache[0], then lets the second reduction overwrite that
@@ -4010,6 +3998,51 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.stageSpirvDigests.push_back(ShaderStageSpirvDigest{
static_cast<Uint32>(stage.stage), static_cast<Uint32>(moduleSpv.size()),
XXH64(moduleSpv.data(), moduleSpv.size() * sizeof(Uint), 0)});
// GL ignores gl_Layer when the framebuffer is not layered; Vulkan leaves a Layer at or
// past the framebuffer's layer count undefined. A stage that stores it gets a second
// module storing 0 for draws into a non-layered framebuffer (ZeroLayerOutputPass).
VkPipelineShaderStageCreateInfo nonLayeredStage = stage;
if (shaderStage == ShaderStage::Vertex || shaderStage == ShaderStage::TessEval ||
shaderStage == ShaderStage::Geometry) {
Vector<Uint> zeroLayerSpv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::ZeroLayerOutputForVulkan(moduleSpv, zeroLayerSpv,
enableSpirvValidation) &&
!zeroLayerSpv.empty() && zeroLayerSpv != moduleSpv) {
VkShaderModuleCreateInfo zeroLayerInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
zeroLayerInfo.codeSize = zeroLayerSpv.size() * sizeof(Uint);
zeroLayerInfo.pCode = zeroLayerSpv.data();
VkShaderModule zeroLayerModule = VK_NULL_HANDLE;
VK_VERIFY(vkCreateShaderModule(m_device, &zeroLayerInfo, nullptr, &zeroLayerModule),
"vkCreateShaderModule(zero gl_Layer)");
entry.modules.push_back(zeroLayerModule);
nonLayeredStage.module = zeroLayerModule;
// Back-fill: every stage before this one is shared with `stages`.
if (entry.nonLayeredStages.empty()) {
entry.nonLayeredStages.assign(entry.stages.begin(), entry.stages.end() - 1);
}
}
}
if (!entry.nonLayeredStages.empty()) entry.nonLayeredStages.push_back(nonLayeredStage);
// GL ignores gl_SampleMask without multisample fragment operations; Vulkan discards
// a fragment whose one-sample mask has bit 0 clear. A fragment stage that stores it
// gets a second module storing all-ones for those draws (FullCoverageSampleMaskPass).
if (shaderStage == ShaderStage::Fragment) {
Vector<Uint> fullCoverageSpv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::FullCoverageSampleMaskForVulkan(
moduleSpv, fullCoverageSpv, enableSpirvValidation) &&
!fullCoverageSpv.empty() && fullCoverageSpv != moduleSpv) {
VkShaderModuleCreateInfo fullCoverageInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
fullCoverageInfo.codeSize = fullCoverageSpv.size() * sizeof(Uint);
fullCoverageInfo.pCode = fullCoverageSpv.data();
VkShaderModule fullCoverageModule = VK_NULL_HANDLE;
VK_VERIFY(vkCreateShaderModule(m_device, &fullCoverageInfo, nullptr, &fullCoverageModule),
"vkCreateShaderModule(full-coverage gl_SampleMask)");
entry.modules.push_back(fullCoverageModule);
entry.fullCoverageFragmentModule = fullCoverageModule;
}
}
}
// Reflect and create layout as part of the program object
@@ -75,6 +75,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ZeroBaseVertex = 1 << 8,
};
using CompileOptionFlags = Flags<CompileOptionBit>;
// glClipControl folded into the position fixup's two bits. An upper-left origin negates
// clip-space Y on top of whatever the framebuffer needs (the fixup flips before it rotates,
// so this stays in the logical orientation), and inverts the cull sense with it, which
// keeps facing a property of the clip-space winding as the extension requires. A
// [0, 1] depth mode is Vulkan's own clip volume, so the remap is dropped. gl_FragCoord is
// a window coordinate, which clip control does not move.
static constexpr Uint32 ApplyClipControlToTransformFlags(Uint32 flags, GLenum origin, GLenum depthMode) {
if (origin == GL_UPPER_LEFT) flags ^= static_cast<Uint32>(CompileOptionBit::PositionYFlip);
if (depthMode == GL_ZERO_TO_ONE) flags &= ~static_cast<Uint32>(CompileOptionBit::PositionZRemap);
return flags;
}
using HashType = Uint64;
// The gl_PerVertex members a pass-through tessellation control stage may have to carry,
@@ -116,6 +128,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
HashType hash = 0;
Vector<VkPipelineShaderStageCreateInfo> stages;
// `stages` for a draw into a NON-LAYERED framebuffer: the same list, except that every
// module storing gl_Layer is replaced by its ZeroLayerOutputPass variant (GL ignores
// gl_Layer there; Vulkan offsets the primitive out of the attachment). Empty for every
// program that never writes gl_Layer, which then uses `stages` for both. The variant
// modules are owned by `modules` like the others.
Vector<VkPipelineShaderStageCreateInfo> nonLayeredStages;
// The fragment module a draw without multisample fragment operations uses (a
// single-sample framebuffer, or MULTISAMPLE disabled): every gl_SampleMask store
// writes all-ones, since GL ignores the shader's mask there and Vulkan would discard
// on a clear bit 0 (FullCoverageSampleMaskPass). VK_NULL_HANDLE for every program
// whose fragment stage never writes gl_SampleMask. Owned by `modules`.
VkShaderModule fullCoverageFragmentModule = VK_NULL_HANDLE;
Vector<VkShaderModule> modules;
// Parallel to stages; identifies the exact module bytes handed to the driver when a
// pipeline creation fails. Sixteen bytes per stage instead of keeping the SPIR-V.
@@ -265,6 +289,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkProgramObject(VkProgramObject&& other) noexcept {
hash = other.hash;
stages = std::move(other.stages);
nonLayeredStages = std::move(other.nonLayeredStages);
fullCoverageFragmentModule = other.fullCoverageFragmentModule;
other.fullCoverageFragmentModule = VK_NULL_HANDLE;
modules = std::move(other.modules);
// Must travel with `modules`: these digests name the SPIR-V those exact
// shader modules were built from, and the pipeline-failure diagnostics
@@ -344,6 +371,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Destroy();
hash = other.hash;
stages = std::move(other.stages);
nonLayeredStages = std::move(other.nonLayeredStages);
fullCoverageFragmentModule = other.fullCoverageFragmentModule;
other.fullCoverageFragmentModule = VK_NULL_HANDLE;
modules = std::move(other.modules);
stageSpirvDigests = std::move(other.stageSpirvDigests); // travels with `modules` - see the move ctor
descriptorSetLayout = other.descriptorSetLayout;
@@ -434,6 +464,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
modules.clear();
stages.clear();
nonLayeredStages.clear();
fullCoverageFragmentModule = VK_NULL_HANDLE;
stageSpirvDigests.clear(); // the modules they describe are gone
}
};
@@ -457,7 +489,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// repair passes patch modules AROUND them, and the emulation only replaces them
// on opted-in devices with no subgroup support at all.
struct SubgroupLoweringPolicy {
Bool emulateSubgroups = false; // MOBILEGL_MAGMA_EMULATE_SUBGROUP, no-native-support devices
Bool emulateSubgroups = false; // no-native-support devices, or MOBILEGL_SUBGROUP_EMULATION
Bool fixIterationRPSubgroupScratch = false; // patch iterationRP's under-declared scratch
Bool fixIterationRPBarrier = false; // repair Program 203's shared-scratch race
Bool deriveNumSubgroups = false; // repair the NumSubgroups builtin
@@ -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
@@ -519,6 +160,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
format = ToFloat32VertexFormat(attr.Size);
conversion = VertexStreamConversion::Float64ToFloat32;
}
// GL_INT / GL_UNSIGNED_INT read as float (glVertexAttribFormat without the I suffix):
// Vulkan has no 32-bit SCALED or NORM vertex format, so the stream is converted to
// float32 on the CPU, normalized when the attribute asks for it.
if (format == VK_FORMAT_UNDEFINED && !attr.IsInteger && !attr.IsLong && !attr.IsBgra &&
(type == DataType::Int32 || type == DataType::Uint32)) {
format = ToFloat32VertexFormat(attr.Size);
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
}
if (format == VK_FORMAT_UNDEFINED) { maskOut("format-map"); continue; }
if (!SupportsVertexBufferFormat(format)) {
if (!IsScaledIntegerVertexFormat(format)) { maskOut("native-format-feature", format); continue; }
@@ -573,42 +222,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) {
@@ -827,6 +440,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case VK_FORMAT_R16G16B16_SSCALED:
case VK_FORMAT_R16G16B16A16_USCALED:
case VK_FORMAT_R16G16B16A16_SSCALED:
// Packed 2_10_10_10 fetches are optional too (Adreno has neither SCALED form); the
// converter decodes the fields itself, normalized or not.
case VK_FORMAT_A2B10G10R10_USCALED_PACK32:
case VK_FORMAT_A2B10G10R10_SSCALED_PACK32:
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
case VK_FORMAT_A2B10G10R10_SNORM_PACK32:
return true;
default:
return false;
@@ -847,19 +466,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,17 @@
#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
#include "WireE5RawCopy.h"
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 +109,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 +334,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 +359,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 +395,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 +407,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 +445,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 +460,6 @@ public:
yuvOwner.reset();
yuvSource.reset();
yuvConvertedFrame = 0;
#endif
fullView = VK_NULL_HANDLE;
sampledView = VK_NULL_HANDLE;
perMipViews.clear();
@@ -510,9 +487,7 @@ public:
syncedContentVersion = 0;
syncedMipLevelCount = 0;
syncedShapeVersion = 0;
#if MOBILEGL_BUILD_DISAGGREGATED
syncedWireSerial = 0;
#endif
}
~TextureResource() {
@@ -539,28 +514,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 +541,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 +551,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);
@@ -640,6 +576,18 @@ public:
// stays as it was).
Bool GrowWireTextureMipChain(MG_Pipe::MGPipeHandle handle, Uint32 requiredMipLevels,
VkCommandBuffer commandBuffer);
// THE RAW E5B9G9R9 READ ROUTE (WireE5RawCopy.h). The renderer hands its kit over once
// ArmWireE5RawReads has armed the route; nullptr keeps every transfer read plain.
void SetWireE5RawCopyKit(const WireE5RawCopyKit* kit) { m_wireE5RawCopyKit = kit; }
// True when a transfer read of `resource` must go through the route: armed, and a
// single-sample E5B9G9R9 MUTABLE_FORMAT image that is not 1D.
Bool WireE5RawReadApplies(const TextureResource& resource) const;
// Records `region` of such an image (in GENERAL or SHADER_READ_ONLY_OPTIMAL) into a fresh
// R32_UINT twin, left in TRANSFER_SRC_OPTIMAL. On success `twin` owns the image and `objects`
// the views and the pool, and both must outlive the recording's submission. On failure
// nothing was recorded and everything created was destroyed.
Bool RecordWireE5RawTwin(VkCommandBuffer commandBuffer, const WireE5RawCopyRegion& region,
VkImageLayout sourceLayout, TextureResource& twin, WireE5RawCopyObjects& objects);
// Records that GPU writes superseded the staged CPU snapshot of this level. Layer
// ranges select cube faces; array and 3D levels share one staged level shadow.
void MarkWireTextureGpuWritten(MG_Pipe::MGPipeHandle handle, Uint32 mipLevel,
@@ -679,30 +627,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 +636,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 +644,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 +651,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 +672,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 +689,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 +712,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;
@@ -890,43 +722,27 @@ private:
VkCommandPool m_uploadCommandPool = VK_NULL_HANDLE;
VkQueue m_graphicsQueue = VK_NULL_HANDLE;
Bool m_imageFormatListSupported = false;
const WireE5RawCopyKit* m_wireE5RawCopyKit = nullptr;
Uint32 m_currentFrameIndex = 0;
Uint8 m_gcCounter = 0;
// 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,28 +750,13 @@ 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;
Vector<Vector<TextureResource>> m_deferredReleases;
Vector<Vector<VkImageView>> m_deferredViewReleases;
// The raw E5B9G9R9 route's views and descriptor pools, on the same ring as the images they name.
Vector<Vector<WireE5RawCopyObjects>> m_deferredE5Releases;
// --- Batched upload machinery ---
// Uploads within a frame are recorded into ONE shared command buffer and
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
@@ -0,0 +1,356 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireCopySliceProbe.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// See WireCopySliceProbe.h. Plumbing as in WireDepthResolveProbe.cpp: one throwaway command
// buffer, a bounded fence wait that leaks rather than idle-waits a hung GPU.
#include "WireCopySliceProbe.h"
#include <vulkan/vulkan_core.h>
namespace MobileGL::MG_Backend::DirectVulkan {
String DescribeWireCopySliceProbe(const WireCopySliceProbeMeasurement& measurement) {
if (!measurement.ran)
return format("not run ({})", measurement.failureReason.empty() ? "?" : measurement.failureReason);
return format("one-region 2D-array A2B10G10R10_UINT -> 3D A2B10G10R10_UNORM copy: {}/{} texels wrong, "
"{}/{} slices exact; per-slice regions: {} wrong",
measurement.subjectMismatches, measurement.texels, measurement.subjectExactSlices,
measurement.slices, measurement.controlMismatches);
}
namespace {
constexpr Uint32 kExtent = 7;
constexpr Uint32 kSlices = 8;
constexpr Uint32 kSliceTexels = kExtent * kExtent;
constexpr Uint32 kTexels = kSliceTexels * kSlices;
// The destination is larger than the region, as in the copy_image cases that drop slices.
constexpr Uint32 kDestinationExtent = 30;
constexpr Uint32 kDestinationDepth = 24;
constexpr Uint32 kDestinationTexels = kDestinationExtent * kDestinationExtent * kDestinationDepth;
constexpr VkFormat kSourceFormat = VK_FORMAT_A2B10G10R10_UINT_PACK32;
constexpr VkFormat kDestinationFormat = VK_FORMAT_A2B10G10R10_UNORM_PACK32;
constexpr Uint64 kFenceTimeoutNs = 5ull * 1000ull * 1000ull * 1000ull;
Uint32 Word(Uint32 index, Uint32 salt) {
Uint32 state = (index + salt * 0x10001u) * 2654435761u + 0x9E3779B9u;
state ^= state >> 15;
state *= 2246822519u;
state ^= state >> 13;
return state;
}
struct ProbeObjects {
VkDevice device = VK_NULL_HANDLE;
Vector<VkImage> images;
Vector<VkBuffer> buffers;
Vector<VkDeviceMemory> memories;
VkCommandPool commandPool = VK_NULL_HANDLE;
VkFence fence = VK_NULL_HANDLE;
void Destroy() {
for (VkImage image : images) vkDestroyImage(device, image, nullptr);
for (VkBuffer buffer : buffers) vkDestroyBuffer(device, buffer, nullptr);
for (VkDeviceMemory memory : memories) vkFreeMemory(device, memory, nullptr);
vkDestroyCommandPool(device, commandPool, nullptr);
vkDestroyFence(device, fence, nullptr);
}
};
Optional<Uint32> PickMemoryType(const VkPhysicalDeviceMemoryProperties& properties, Uint32 allowed,
VkMemoryPropertyFlags wanted, VkMemoryPropertyFlags required) {
Optional<Uint32> fallback;
for (Uint32 i = 0; i < properties.memoryTypeCount; ++i) {
if (!(allowed & (1u << i))) continue;
const VkMemoryPropertyFlags flags = properties.memoryTypes[i].propertyFlags;
if (flags & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT) continue;
if ((flags & required) != required) continue;
if ((flags & wanted) == wanted) return i;
if (!fallback) fallback = i;
}
return fallback;
}
void ImageBarrier(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout oldLayout,
VkImageLayout newLayout, VkAccessFlags srcAccess, VkAccessFlags dstAccess) {
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
barrier.srcAccessMask = srcAccess;
barrier.dstAccessMask = dstAccess;
barrier.oldLayout = oldLayout;
barrier.newLayout = newLayout;
barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image;
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, VK_REMAINING_ARRAY_LAYERS};
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
}
} // namespace
WireCopySliceProbeMeasurement RunWireCopySliceProbe(const WireCopySliceProbeContext& context) {
WireCopySliceProbeMeasurement measurement;
const VkDevice device = context.device;
if (device == VK_NULL_HANDLE || context.queue == VK_NULL_HANDLE) {
measurement.failureReason = "no device or queue";
return measurement;
}
constexpr VkFormatFeatureFlags kRequired = VK_FORMAT_FEATURE_TRANSFER_SRC_BIT | VK_FORMAT_FEATURE_TRANSFER_DST_BIT;
for (const VkFormat format : {kSourceFormat, kDestinationFormat}) {
VkFormatProperties properties{};
vkGetPhysicalDeviceFormatProperties(context.physicalDevice, format, &properties);
if ((properties.optimalTilingFeatures & kRequired) != kRequired) {
measurement.failureReason = "A2B10G10R10 UINT/UNORM are not transfer formats here";
return measurement;
}
}
ProbeObjects objects;
objects.device = device;
Bool leak = false;
struct Teardown {
ProbeObjects& objects;
Bool& leak;
~Teardown() {
if (!leak) objects.Destroy();
}
} teardown{objects, leak};
const auto fail = [&](String reason) {
measurement.ran = false;
measurement.failureReason = Move(reason);
return measurement;
};
VkPhysicalDeviceMemoryProperties memoryProperties{};
vkGetPhysicalDeviceMemoryProperties(context.physicalDevice, &memoryProperties);
// The wire's images (VkTextureManager): MUTABLE_FORMAT (a storage-compatible colour class),
// SAMPLED | TRANSFER_SRC | TRANSFER_DST | COLOR_ATTACHMENT, a 3D one 2D_ARRAY_COMPATIBLE.
const auto makeImage = [&](Bool volume, VkFormat format, VkImage& out) -> Bool {
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(context.physicalDevice, format, &formatProperties);
const Bool attachable =
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0;
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
info.flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | (volume ? VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT : 0);
info.imageType = volume ? VK_IMAGE_TYPE_3D : VK_IMAGE_TYPE_2D;
info.format = format;
info.extent = volume ? VkExtent3D{kDestinationExtent, kDestinationExtent, kDestinationDepth}
: VkExtent3D{kExtent, kExtent, 1};
info.mipLevels = 1;
info.arrayLayers = volume ? 1u : kSlices;
info.samples = VK_SAMPLE_COUNT_1_BIT;
info.tiling = VK_IMAGE_TILING_OPTIMAL;
info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
(attachable ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0u);
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
if (vkCreateImage(device, &info, nullptr, &out) != VK_SUCCESS) return false;
objects.images.push_back(out);
VkMemoryRequirements requirements{};
vkGetImageMemoryRequirements(device, out, &requirements);
const Optional<Uint32> type = PickMemoryType(memoryProperties, requirements.memoryTypeBits,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0);
if (!type) return false;
VkMemoryAllocateInfo allocate{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocate.allocationSize = requirements.size;
allocate.memoryTypeIndex = *type;
VkDeviceMemory memory = VK_NULL_HANDLE;
if (vkAllocateMemory(device, &allocate, nullptr, &memory) != VK_SUCCESS) return false;
objects.memories.push_back(memory);
return vkBindImageMemory(device, out, memory, 0) == VK_SUCCESS;
};
struct HostBuffer {
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
Bool coherent = false;
};
const auto makeBuffer = [&](VkDeviceSize bytes, VkBufferUsageFlags usage, HostBuffer& out) -> Bool {
VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
info.size = bytes;
info.usage = usage;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (vkCreateBuffer(device, &info, nullptr, &out.buffer) != VK_SUCCESS) return false;
objects.buffers.push_back(out.buffer);
VkMemoryRequirements requirements{};
vkGetBufferMemoryRequirements(device, out.buffer, &requirements);
const Optional<Uint32> type =
PickMemoryType(memoryProperties, requirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT);
if (!type) return false;
out.coherent =
(memoryProperties.memoryTypes[*type].propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) != 0;
VkMemoryAllocateInfo allocate{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocate.allocationSize = requirements.size;
allocate.memoryTypeIndex = *type;
if (vkAllocateMemory(device, &allocate, nullptr, &out.memory) != VK_SUCCESS) return false;
objects.memories.push_back(out.memory);
return vkBindBufferMemory(device, out.buffer, out.memory, 0) == VK_SUCCESS;
};
const auto access = [&](const HostBuffer& buffer, Bool write, auto&& body) -> Bool {
void* mapped = nullptr;
if (vkMapMemory(device, buffer.memory, 0, VK_WHOLE_SIZE, 0, &mapped) != VK_SUCCESS || mapped == nullptr)
return false;
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
range.memory = buffer.memory;
range.size = VK_WHOLE_SIZE;
if (!write && !buffer.coherent) vkInvalidateMappedMemoryRanges(device, 1, &range);
body(static_cast<Uint32*>(mapped));
if (write && !buffer.coherent) vkFlushMappedMemoryRanges(device, 1, &range);
vkUnmapMemory(device, buffer.memory);
return true;
};
VkImage source = VK_NULL_HANDLE, subject = VK_NULL_HANDLE, control = VK_NULL_HANDLE;
if (!makeImage(false, kSourceFormat, source) || !makeImage(true, kDestinationFormat, subject) ||
!makeImage(true, kDestinationFormat, control))
return fail("image creation failed");
HostBuffer sourceWords, sentinelWords, subjectOut, controlOut;
if (!makeBuffer(kTexels * 4, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, sourceWords) ||
!makeBuffer(kDestinationTexels * 4, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, sentinelWords) ||
!makeBuffer(kTexels * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT, subjectOut) ||
!makeBuffer(kTexels * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT, controlOut))
return fail("buffer creation failed");
if (!access(sourceWords, true, [](Uint32* words) { for (Uint32 i = 0; i < kTexels; ++i) words[i] = Word(i, 1); }) ||
!access(sentinelWords, true, [](Uint32* words) { for (Uint32 i = 0; i < kDestinationTexels; ++i) words[i] = Word(i, 2); }))
return fail("vkMapMemory (upload) failed");
VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
poolInfo.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT;
poolInfo.queueFamilyIndex = context.queueFamilyIndex;
if (vkCreateCommandPool(device, &poolInfo, nullptr, &objects.commandPool) != VK_SUCCESS)
return fail("vkCreateCommandPool failed");
VkCommandBufferAllocateInfo commandInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
commandInfo.commandPool = objects.commandPool;
commandInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
commandInfo.commandBufferCount = 1;
VkCommandBuffer cmd = VK_NULL_HANDLE;
if (vkAllocateCommandBuffers(device, &commandInfo, &cmd) != VK_SUCCESS)
return fail("vkAllocateCommandBuffers failed");
VkCommandBufferBeginInfo beginInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
if (vkBeginCommandBuffer(cmd, &beginInfo) != VK_SUCCESS) return fail("vkBeginCommandBuffer failed");
// (1) The source's words, and a sentinel in both destinations so an untouched slice shows.
for (const VkImage image : {source, subject, control})
ImageBarrier(cmd, image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0,
VK_ACCESS_TRANSFER_WRITE_BIT);
VkBufferImageCopy layered{};
layered.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, kSlices};
layered.imageExtent = {kExtent, kExtent, 1};
VkBufferImageCopy volume{};
volume.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
volume.imageExtent = {kExtent, kExtent, kSlices};
VkBufferImageCopy wholeVolume = volume;
wholeVolume.imageExtent = {kDestinationExtent, kDestinationExtent, kDestinationDepth};
vkCmdCopyBufferToImage(cmd, sourceWords.buffer, source, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &layered);
vkCmdCopyBufferToImage(cmd, sentinelWords.buffer, subject, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &wholeVolume);
vkCmdCopyBufferToImage(cmd, sentinelWords.buffer, control, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &wholeVolume);
ImageBarrier(cmd, source, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
for (const VkImage image : {subject, control})
ImageBarrier(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT);
// (2) SUBJECT: the one region CopyImageSubData records. CONTROL: one region per slice.
VkImageCopy whole{};
whole.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, kSlices};
whole.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
whole.extent = {kExtent, kExtent, kSlices};
vkCmdCopyImage(cmd, source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, subject, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, &whole);
VkImageCopy perSlice[kSlices]{};
for (Uint32 slice = 0; slice < kSlices; ++slice) {
perSlice[slice].srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, slice, 1};
perSlice[slice].dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
perSlice[slice].dstOffset = {0, 0, static_cast<Int32>(slice)};
perSlice[slice].extent = {kExtent, kExtent, 1};
}
vkCmdCopyImage(cmd, source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, control, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
kSlices, perSlice);
// (3) Both destinations out: same-format reads of a 3D image, which the defect does not touch.
for (const VkImage image : {subject, control})
ImageBarrier(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
vkCmdCopyImageToBuffer(cmd, subject, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, subjectOut.buffer, 1, &volume);
vkCmdCopyImageToBuffer(cmd, control, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, controlOut.buffer, 1, &volume);
VkMemoryBarrier hostBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
hostBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
hostBarrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &hostBarrier, 0,
nullptr, 0, nullptr);
if (vkEndCommandBuffer(cmd) != VK_SUCCESS) return fail("vkEndCommandBuffer failed");
VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
if (vkCreateFence(device, &fenceInfo, nullptr, &objects.fence) != VK_SUCCESS)
return fail("vkCreateFence failed");
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit.commandBufferCount = 1;
submit.pCommandBuffers = &cmd;
if (const VkResult result = vkQueueSubmit(context.queue, 1, &submit, objects.fence); result != VK_SUCCESS)
return fail(format("vkQueueSubmit failed (VkResult {})", static_cast<Int>(result)));
const VkResult waited = vkWaitForFences(device, 1, &objects.fence, VK_TRUE, kFenceTimeoutNs);
if (waited == VK_TIMEOUT) {
leak = true;
measurement.fenceWaitTimedOut = true;
return fail("the probe's fence did not signal within 5 s");
}
if (waited != VK_SUCCESS) return fail(format("vkWaitForFences failed (VkResult {})", static_cast<Int>(waited)));
const auto tally = [&](const Uint32* words, Uint32& mismatches, Uint32* exactSlices) {
for (Uint32 slice = 0; slice < kSlices; ++slice) {
Uint32 sliceMismatches = 0;
for (Uint32 i = slice * kSliceTexels; i < (slice + 1) * kSliceTexels; ++i)
if (words[i] != Word(i, 1)) ++sliceMismatches;
mismatches += sliceMismatches;
if (exactSlices && sliceMismatches == 0) ++*exactSlices;
}
};
if (!access(subjectOut, false, [&](Uint32* words) {
tally(words, measurement.subjectMismatches, &measurement.subjectExactSlices);
}) ||
!access(controlOut, false, [&](Uint32* words) { tally(words, measurement.controlMismatches, nullptr); }))
return fail("vkMapMemory (readback) failed");
measurement.texels = kTexels;
measurement.slices = kSlices;
measurement.ran = true;
return measurement;
}
WireCopySliceProbeMeasurement RunWireCopySliceProbeOnThrowawayDevice(PFN_vkGetInstanceProcAddr getInstanceProcAddr,
VkInstance instance,
VkPhysicalDevice physicalDevice,
Uint32 graphicsQueueFamilyIndex) {
WireCopySliceProbeMeasurement measurement;
const auto createDevice =
reinterpret_cast<PFN_vkCreateDevice>(getInstanceProcAddr(instance, "vkCreateDevice"));
if (createDevice == nullptr) {
measurement.failureReason = "no vkCreateDevice";
return measurement;
}
const Float priority = 1.0f;
VkDeviceQueueCreateInfo queueInfo{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
queueInfo.queueFamilyIndex = graphicsQueueFamilyIndex;
queueInfo.queueCount = 1;
queueInfo.pQueuePriorities = &priority;
VkDeviceCreateInfo deviceInfo{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
deviceInfo.queueCreateInfoCount = 1;
deviceInfo.pQueueCreateInfos = &queueInfo;
VkDevice device = VK_NULL_HANDLE;
if (const VkResult result = createDevice(physicalDevice, &deviceInfo, nullptr, &device);
result != VK_SUCCESS || device == VK_NULL_HANDLE) {
measurement.failureReason = format("vkCreateDevice failed (VkResult {})", static_cast<Int>(result));
return measurement;
}
WireCopySliceProbeContext context;
context.physicalDevice = physicalDevice;
context.device = device;
vkGetDeviceQueue(device, graphicsQueueFamilyIndex, 0, &context.queue);
context.queueFamilyIndex = graphicsQueueFamilyIndex;
measurement = RunWireCopySliceProbe(context);
if (!measurement.fenceWaitTimedOut) vkDestroyDevice(device, nullptr);
return measurement;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,93 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireCopySliceProbe.h
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Config.h>
#include <Includes.h>
#include <vulkan/vulkan_core.h>
// CROSS-FORMAT SLICE COPIES BETWEEN A LAYERED IMAGE AND A 3D ONE, and the probe that decides
// whether a device needs them split.
//
// glCopyImageSubData between a 2D array and a 3D texture maps layers onto depth slices (GL 4.6
// core 18.3.2), and Vulkan expresses the whole move as ONE VkImageCopy: layerCount on the layered
// side, extent.depth on the 3D side (VUID-VkImageCopy-srcImage-01790/01791). Adreno 750 performs
// that region correctly when the two formats are the same, but when they differ (size-compatible,
// e.g. A2B10G10R10_UINT into A2B10G10R10_UNORM - KHR-GL4x.copy_image's cross-format cases) it
// lands only the FIRST slice and leaves the rest of the destination untouched. The same copy
// recorded as one single-slice region per slice is exact. The probe uses the wire's image shape
// (MUTABLE_FORMAT, attachment usage, a 30x30x24 destination): a non-mutable 7x7x8 pair without
// attachment usage copied cleanly on the same device.
//
// VulkanRenderer::CopyImageSubData therefore splits such a copy per slice where the probe below
// measured the defect (VulkanRenderer::ArmWireCopySliceSplit at bring-up, the POST's "Known Driver
// Bugs" section in MG_Util/SelfTest/DriverBugProbes.cpp); MOBILEGL_MAGMA_SPLIT_SLICE_COPIES forces it.
namespace MobileGL::MG_Backend::DirectVulkan {
struct WireCopySliceProbeMeasurement {
// Recorded, submitted and read back.
Bool ran = false;
// The bounded fence wait expired: every object was deliberately leaked.
Bool fenceWaitTimedOut = false;
String failureReason;
// SUBJECT: one region, A2B10G10R10_UINT 2D array -> A2B10G10R10_UNORM 3D, every slice.
Uint32 texels = 0;
Uint32 slices = 0;
Uint32 subjectMismatches = 0;
// The slices the subject got entirely right (the defect leaves exactly the first).
Uint32 subjectExactSlices = 0;
// CONTROL: the same copy recorded one slice per region. Must be exact, or nothing is concluded.
Uint32 controlMismatches = 0;
};
enum class WireCopySliceProbeVerdict : Uint8 {
NotRun,
Inconclusive,
Clean,
// The defect: the single region missed texels the per-slice regions moved exactly.
CrossFormatSlicesDropped,
};
inline const char* WireCopySliceProbeVerdictName(WireCopySliceProbeVerdict verdict) {
switch (verdict) {
case WireCopySliceProbeVerdict::NotRun: return "not-run";
case WireCopySliceProbeVerdict::Inconclusive: return "inconclusive";
case WireCopySliceProbeVerdict::Clean: return "clean";
case WireCopySliceProbeVerdict::CrossFormatSlicesDropped: return "cross-format-slices-dropped";
}
return "?";
}
inline WireCopySliceProbeVerdict EvaluateWireCopySliceProbe(const WireCopySliceProbeMeasurement& measurement) {
if (!measurement.ran || measurement.fenceWaitTimedOut || measurement.texels == 0)
return WireCopySliceProbeVerdict::NotRun;
if (measurement.controlMismatches != 0) return WireCopySliceProbeVerdict::Inconclusive;
return measurement.subjectMismatches != 0 ? WireCopySliceProbeVerdict::CrossFormatSlicesDropped
: WireCopySliceProbeVerdict::Clean;
}
String DescribeWireCopySliceProbe(const WireCopySliceProbeMeasurement& measurement);
struct WireCopySliceProbeContext {
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
VkDevice device = VK_NULL_HANDLE;
// Must be otherwise idle for the probe's duration.
VkQueue queue = VK_NULL_HANDLE;
Uint32 queueFamilyIndex = 0;
};
// One submission on its own command pool, a bounded fence wait (on timeout every object is
// leaked and the measurement says so), teardown on every other path.
WireCopySliceProbeMeasurement RunWireCopySliceProbe(const WireCopySliceProbeContext& context);
// The POST's entry: a throwaway device with one graphics queue.
WireCopySliceProbeMeasurement RunWireCopySliceProbeOnThrowawayDevice(PFN_vkGetInstanceProcAddr getInstanceProcAddr,
VkInstance instance,
VkPhysicalDevice physicalDevice,
Uint32 graphicsQueueFamilyIndex);
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -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
@@ -0,0 +1,410 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// See WireDiscardStateProbe.h. Plumbing as in WireDepthResolveProbe.cpp: one throwaway command
// buffer, a bounded fence wait that leaks rather than idle-waits a hung GPU.
#include "WireDiscardStateProbe.h"
#include <vulkan/vulkan_core.h>
namespace MobileGL::MG_Backend::DirectVulkan {
#include "WireDiscardStateProbeSpirv.h"
String DescribeWireDiscardStateProbe(const WireDiscardStateProbeMeasurement& measurement) {
if (!measurement.ran)
return format("not run ({})", measurement.failureReason.empty() ? "?" : measurement.failureReason);
return format("a draw inheriting viewport/scissor after a discard draw: {}/{} pixels; after a clipped "
"rasterizing draw: {}/{}",
measurement.subjectGreen, measurement.pixels, measurement.controlGreen, measurement.pixels);
}
namespace {
constexpr Uint32 kExtent = 16;
constexpr Uint32 kPixels = kExtent * kExtent;
constexpr VkFormat kColorFormat = VK_FORMAT_R8G8B8A8_UNORM;
constexpr Uint64 kFenceTimeoutNs = 5ull * 1000ull * 1000ull * 1000ull;
struct ProbeObjects {
VkDevice device = VK_NULL_HANDLE;
Vector<VkImage> images;
Vector<VkImageView> views;
Vector<VkBuffer> buffers;
Vector<VkDeviceMemory> memories;
Vector<VkFramebuffer> framebuffers;
Vector<VkPipeline> pipelines;
Vector<VkShaderModule> modules;
VkRenderPass pass = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
VkCommandPool commandPool = VK_NULL_HANDLE;
VkFence fence = VK_NULL_HANDLE;
void Destroy() {
for (VkPipeline pipeline : pipelines) vkDestroyPipeline(device, pipeline, nullptr);
for (VkShaderModule module : modules) vkDestroyShaderModule(device, module, nullptr);
vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
for (VkFramebuffer framebuffer : framebuffers) vkDestroyFramebuffer(device, framebuffer, nullptr);
vkDestroyRenderPass(device, pass, nullptr);
for (VkImageView view : views) vkDestroyImageView(device, view, nullptr);
for (VkImage image : images) vkDestroyImage(device, image, nullptr);
for (VkBuffer buffer : buffers) vkDestroyBuffer(device, buffer, nullptr);
for (VkDeviceMemory memory : memories) vkFreeMemory(device, memory, nullptr);
vkDestroyCommandPool(device, commandPool, nullptr);
vkDestroyFence(device, fence, nullptr);
}
};
Optional<Uint32> PickMemoryType(const VkPhysicalDeviceMemoryProperties& properties, Uint32 allowed,
VkMemoryPropertyFlags wanted, VkMemoryPropertyFlags required) {
Optional<Uint32> fallback;
for (Uint32 i = 0; i < properties.memoryTypeCount; ++i) {
if (!(allowed & (1u << i))) continue;
const VkMemoryPropertyFlags flags = properties.memoryTypes[i].propertyFlags;
if (flags & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT) continue;
if ((flags & required) != required) continue;
if ((flags & wanted) == wanted) return i;
if (!fallback) fallback = i;
}
return fallback;
}
} // namespace
WireDiscardStateProbeMeasurement RunWireDiscardStateProbe(const WireDiscardStateProbeContext& context) {
WireDiscardStateProbeMeasurement measurement;
const VkDevice device = context.device;
if (device == VK_NULL_HANDLE || context.queue == VK_NULL_HANDLE) {
measurement.failureReason = "no device or queue";
return measurement;
}
ProbeObjects objects;
objects.device = device;
Bool leak = false;
struct Teardown {
ProbeObjects& objects;
Bool& leak;
~Teardown() {
if (!leak) objects.Destroy();
}
} teardown{objects, leak};
const auto fail = [&](String reason) {
measurement.ran = false;
measurement.failureReason = Move(reason);
return measurement;
};
VkPhysicalDeviceMemoryProperties memoryProperties{};
vkGetPhysicalDeviceMemoryProperties(context.physicalDevice, &memoryProperties);
const auto allocate = [&](const VkMemoryRequirements& requirements, VkMemoryPropertyFlags wanted,
VkMemoryPropertyFlags required, VkDeviceMemory& out, Bool* coherent) -> Bool {
const Optional<Uint32> type =
PickMemoryType(memoryProperties, requirements.memoryTypeBits, wanted, required);
if (!type) return false;
if (coherent)
*coherent = (memoryProperties.memoryTypes[*type].propertyFlags &
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) != 0;
VkMemoryAllocateInfo info{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
info.allocationSize = requirements.size;
info.memoryTypeIndex = *type;
if (vkAllocateMemory(device, &info, nullptr, &out) != VK_SUCCESS) return false;
objects.memories.push_back(out);
return true;
};
// One render pass for both lanes: clear, draw twice, hand the target to the readback copy.
{
VkAttachmentDescription attachment{};
attachment.format = kColorFormat;
attachment.samples = VK_SAMPLE_COUNT_1_BIT;
attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachment.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
const VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &reference;
// The readback copy waits on the pass's attachment writes.
VkSubpassDependency dependency{};
dependency.srcSubpass = 0;
dependency.dstSubpass = VK_SUBPASS_EXTERNAL;
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.dstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT;
dependency.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
dependency.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
info.attachmentCount = 1;
info.pAttachments = &attachment;
info.subpassCount = 1;
info.pSubpasses = &subpass;
info.dependencyCount = 1;
info.pDependencies = &dependency;
if (vkCreateRenderPass(device, &info, nullptr, &objects.pass) != VK_SUCCESS)
return fail("vkCreateRenderPass failed");
}
// Two lanes, SUBJECT (the first draw discarded) and CONTROL (the first draw clipped away):
// each has its own target, framebuffer and readback.
struct Lane {
VkImage target = VK_NULL_HANDLE;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
VkBuffer readback = VK_NULL_HANDLE;
VkDeviceMemory readbackMemory = VK_NULL_HANDLE;
Bool readbackCoherent = false;
VkPipeline first = VK_NULL_HANDLE;
} lanes[2];
for (Lane& lane : lanes) {
VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.format = kColorFormat;
imageInfo.extent = {kExtent, kExtent, 1};
imageInfo.mipLevels = imageInfo.arrayLayers = 1;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
if (vkCreateImage(device, &imageInfo, nullptr, &lane.target) != VK_SUCCESS)
return fail("vkCreateImage failed");
objects.images.push_back(lane.target);
VkMemoryRequirements requirements{};
vkGetImageMemoryRequirements(device, lane.target, &requirements);
VkDeviceMemory imageMemory = VK_NULL_HANDLE;
if (!allocate(requirements, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0, imageMemory, nullptr) ||
vkBindImageMemory(device, lane.target, imageMemory, 0) != VK_SUCCESS)
return fail("image memory failed");
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
viewInfo.image = lane.target;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = kColorFormat;
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
VkImageView view = VK_NULL_HANDLE;
if (vkCreateImageView(device, &viewInfo, nullptr, &view) != VK_SUCCESS)
return fail("vkCreateImageView failed");
objects.views.push_back(view);
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
framebufferInfo.renderPass = objects.pass;
framebufferInfo.attachmentCount = 1;
framebufferInfo.pAttachments = &view;
framebufferInfo.width = framebufferInfo.height = kExtent;
framebufferInfo.layers = 1;
if (vkCreateFramebuffer(device, &framebufferInfo, nullptr, &lane.framebuffer) != VK_SUCCESS)
return fail("vkCreateFramebuffer failed");
objects.framebuffers.push_back(lane.framebuffer);
VkBufferCreateInfo bufferInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
bufferInfo.size = kPixels * 4;
bufferInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (vkCreateBuffer(device, &bufferInfo, nullptr, &lane.readback) != VK_SUCCESS)
return fail("vkCreateBuffer failed");
objects.buffers.push_back(lane.readback);
vkGetBufferMemoryRequirements(device, lane.readback, &requirements);
if (!allocate(requirements, VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
lane.readbackMemory, &lane.readbackCoherent) ||
vkBindBufferMemory(device, lane.readback, lane.readbackMemory, 0) != VK_SUCCESS)
return fail("readback memory failed");
}
VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
if (vkCreatePipelineLayout(device, &layoutInfo, nullptr, &objects.pipelineLayout) != VK_SUCCESS)
return fail("vkCreatePipelineLayout failed");
const auto shader = [&](const Uint32* code, SizeT bytes, VkShaderModule& out) -> Bool {
VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
info.codeSize = bytes;
info.pCode = code;
if (vkCreateShaderModule(device, &info, nullptr, &out) != VK_SUCCESS) return false;
objects.modules.push_back(out);
return true;
};
VkShaderModule vertex = VK_NULL_HANDLE, fragment = VK_NULL_HANDLE;
if (!shader(kWireDiscardStateProbeVertexSpirv, sizeof(kWireDiscardStateProbeVertexSpirv), vertex) ||
!shader(kWireDiscardStateProbeFragmentSpirv, sizeof(kWireDiscardStateProbeFragmentSpirv), fragment))
return fail("vkCreateShaderModule failed");
// Every pipeline declares viewport and scissor dynamic; only `discard` differs.
const auto makePipeline = [&](Bool discard, VkPipeline& out) -> Bool {
VkPipelineShaderStageCreateInfo stages[2]{};
for (auto& stage : stages) {
stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stage.pName = "main";
}
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
stages[0].module = vertex;
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
stages[1].module = fragment;
VkPipelineVertexInputStateCreateInfo vertexInput{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;
VkPipelineViewportStateCreateInfo viewportState{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
viewportState.viewportCount = viewportState.scissorCount = 1;
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
raster.rasterizerDiscardEnable = discard ? VK_TRUE : VK_FALSE;
raster.polygonMode = VK_POLYGON_MODE_FILL;
raster.cullMode = VK_CULL_MODE_NONE;
raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
raster.lineWidth = 1.0f;
VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
VkPipelineColorBlendAttachmentState blendAttachment{};
blendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
blend.attachmentCount = 1;
blend.pAttachments = &blendAttachment;
constexpr VkDynamicState kDynamic[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
dynamic.dynamicStateCount = 2;
dynamic.pDynamicStates = kDynamic;
VkGraphicsPipelineCreateInfo info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
// As PipelineFactory builds it: a static discard has no fragment shader state, so no
// fragment stage (VUID-VkGraphicsPipelineCreateInfo-pStages-06894).
info.stageCount = discard ? 1u : 2u;
info.pStages = stages;
info.pVertexInputState = &vertexInput;
info.pInputAssemblyState = &assembly;
info.pViewportState = &viewportState;
info.pRasterizationState = &raster;
info.pMultisampleState = &samples;
info.pColorBlendState = &blend;
info.pDynamicState = &dynamic;
info.layout = objects.pipelineLayout;
info.renderPass = objects.pass;
if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &info, nullptr, &out) != VK_SUCCESS) return false;
objects.pipelines.push_back(out);
return true;
};
VkPipeline second = VK_NULL_HANDLE;
if (!makePipeline(true, lanes[0].first) || !makePipeline(false, lanes[1].first) || !makePipeline(false, second))
return fail("pipeline creation failed");
VkCommandPoolCreateInfo commandPoolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
commandPoolInfo.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT;
commandPoolInfo.queueFamilyIndex = context.queueFamilyIndex;
if (vkCreateCommandPool(device, &commandPoolInfo, nullptr, &objects.commandPool) != VK_SUCCESS)
return fail("vkCreateCommandPool failed");
VkCommandBufferAllocateInfo commandInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
commandInfo.commandPool = objects.commandPool;
commandInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
commandInfo.commandBufferCount = 1;
VkCommandBuffer cmd = VK_NULL_HANDLE;
if (vkAllocateCommandBuffers(device, &commandInfo, &cmd) != VK_SUCCESS)
return fail("vkAllocateCommandBuffers failed");
VkCommandBufferBeginInfo beginInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
if (vkBeginCommandBuffer(cmd, &beginInfo) != VK_SUCCESS) return fail("vkBeginCommandBuffer failed");
const VkClearValue black{{{0.0f, 0.0f, 0.0f, 1.0f}}};
const VkViewport viewport{0.0f, 0.0f, static_cast<Float>(kExtent), static_cast<Float>(kExtent), 0.0f, 1.0f};
const VkRect2D scissor{{0, 0}, {kExtent, kExtent}};
for (Uint32 i = 0; i < 2; ++i) {
const Lane& lane = lanes[i];
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
begin.renderPass = objects.pass;
begin.framebuffer = lane.framebuffer;
begin.renderArea = scissor;
begin.clearValueCount = 1;
begin.pClearValues = &black;
vkCmdBeginRenderPass(cmd, &begin, VK_SUBPASS_CONTENTS_INLINE);
// The first draw sets the state: discarded (SUBJECT) or a strip the vertex shader puts
// outside the clip volume (CONTROL, vertices 4-7).
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, lane.first);
vkCmdSetViewport(cmd, 0, 1, &viewport);
vkCmdSetScissor(cmd, 0, 1, &scissor);
vkCmdDraw(cmd, 4, 1, i == 0 ? 0 : 4, 0);
// The second draw covers the target with what it inherits.
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, second);
vkCmdDraw(cmd, 4, 1, 0, 0);
vkCmdEndRenderPass(cmd);
VkBufferImageCopy copy{};
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
copy.imageExtent = {kExtent, kExtent, 1};
vkCmdCopyImageToBuffer(cmd, lane.target, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, lane.readback, 1, &copy);
}
VkMemoryBarrier hostBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
hostBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
hostBarrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &hostBarrier, 0,
nullptr, 0, nullptr);
if (vkEndCommandBuffer(cmd) != VK_SUCCESS) return fail("vkEndCommandBuffer failed");
VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
if (vkCreateFence(device, &fenceInfo, nullptr, &objects.fence) != VK_SUCCESS)
return fail("vkCreateFence failed");
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit.commandBufferCount = 1;
submit.pCommandBuffers = &cmd;
if (const VkResult result = vkQueueSubmit(context.queue, 1, &submit, objects.fence); result != VK_SUCCESS)
return fail(format("vkQueueSubmit failed (VkResult {})", static_cast<Int>(result)));
const VkResult waited = vkWaitForFences(device, 1, &objects.fence, VK_TRUE, kFenceTimeoutNs);
if (waited == VK_TIMEOUT) {
leak = true;
measurement.fenceWaitTimedOut = true;
return fail("the probe's fence did not signal within 5 s");
}
if (waited != VK_SUCCESS) return fail(format("vkWaitForFences failed (VkResult {})", static_cast<Int>(waited)));
Uint32* green[2] = {&measurement.subjectGreen, &measurement.controlGreen};
for (Uint32 i = 0; i < 2; ++i) {
const Lane& lane = lanes[i];
void* mapped = nullptr;
if (vkMapMemory(device, lane.readbackMemory, 0, VK_WHOLE_SIZE, 0, &mapped) != VK_SUCCESS || !mapped)
return fail("vkMapMemory failed");
if (!lane.readbackCoherent) {
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
range.memory = lane.readbackMemory;
range.size = VK_WHOLE_SIZE;
vkInvalidateMappedMemoryRanges(device, 1, &range);
}
const Uint8* pixels = static_cast<const Uint8*>(mapped);
for (Uint32 p = 0; p < kPixels; ++p)
if (pixels[p * 4] < 16 && pixels[p * 4 + 1] > 240) ++*green[i];
vkUnmapMemory(device, lane.readbackMemory);
}
measurement.pixels = kPixels;
measurement.ran = true;
return measurement;
}
WireDiscardStateProbeMeasurement RunWireDiscardStateProbeOnThrowawayDevice(
PFN_vkGetInstanceProcAddr getInstanceProcAddr, VkInstance instance, VkPhysicalDevice physicalDevice,
Uint32 graphicsQueueFamilyIndex) {
WireDiscardStateProbeMeasurement measurement;
const auto createDevice =
reinterpret_cast<PFN_vkCreateDevice>(getInstanceProcAddr(instance, "vkCreateDevice"));
if (createDevice == nullptr) {
measurement.failureReason = "no vkCreateDevice";
return measurement;
}
const Float priority = 1.0f;
VkDeviceQueueCreateInfo queueInfo{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
queueInfo.queueFamilyIndex = graphicsQueueFamilyIndex;
queueInfo.queueCount = 1;
queueInfo.pQueuePriorities = &priority;
VkDeviceCreateInfo deviceInfo{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
deviceInfo.queueCreateInfoCount = 1;
deviceInfo.pQueueCreateInfos = &queueInfo;
VkDevice device = VK_NULL_HANDLE;
if (const VkResult result = createDevice(physicalDevice, &deviceInfo, nullptr, &device);
result != VK_SUCCESS || device == VK_NULL_HANDLE) {
measurement.failureReason = format("vkCreateDevice failed (VkResult {})", static_cast<Int>(result));
return measurement;
}
WireDiscardStateProbeContext context;
context.physicalDevice = physicalDevice;
context.device = device;
vkGetDeviceQueue(device, graphicsQueueFamilyIndex, 0, &context.queue);
context.queueFamilyIndex = graphicsQueueFamilyIndex;
measurement = RunWireDiscardStateProbe(context);
if (!measurement.fenceWaitTimedOut) vkDestroyDevice(device, nullptr);
return measurement;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,6 @@
#version 450
layout(location = 0) out vec4 color;
void main() {
color = vec4(0.0, 1.0, 0.0, 1.0);
}
@@ -0,0 +1,94 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.h
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Config.h>
#include <Includes.h>
#include <vulkan/vulkan_core.h>
// DYNAMIC STATE ACROSS A RASTERIZER-DISCARD DRAW, and the probe that decides whether a device needs it
// set again.
//
// Dynamic viewport and scissor are command-buffer state: set once, they hold for every later draw
// whose pipeline declares them dynamic, across pipeline binds and render passes. Magma's draw path
// relies on that - its dynamic-state shadow skips a vkCmdSetViewport/vkCmdSetScissor whose value the
// command buffer already holds. On Adreno 750 a DRAW with a rasterizerDiscardEnable pipeline drops
// that state: the next draw that inherits it instead of setting it again draws nothing, in the same
// render pass or a later one. Binding the discard pipeline and setting the state without drawing
// with it is harmless, and so is the same draw with a rasterizing pipeline.
//
// The GL pattern is a vertex shader that writes with imageStore under GL_RASTERIZER_DISCARD, then a
// draw that reads what it wrote (KHR-GL4x.shader_image_load_store.advanced-sync-imageAccess and
// -vertexArray): the second draw has the same viewport and scissor, so the shadow sets neither, and
// it is lost.
//
// Where the probe below measured the defect, the draw path forgets the dynamic-state shadow after
// every discard draw (VulkanRenderer::ForgetDynamicStateAfterDiscardDraw), so the next draw sets its
// own. Armed by VulkanRenderer::ArmWireDiscardStateReemit at bring-up and reported in the POST's
// "Known Driver Bugs" (MG_Util/SelfTest/DriverBugProbes.cpp); MOBILEGL_MAGMA_REEMIT_STATE_AFTER_DISCARD
// forces it.
namespace MobileGL::MG_Backend::DirectVulkan {
struct WireDiscardStateProbeMeasurement {
Bool ran = false;
Bool fenceWaitTimedOut = false;
String failureReason;
// Pixels of each lane's 16x16 target.
Uint32 pixels = 0;
// SUBJECT: a discard draw, then a full-target draw that inherits its viewport and scissor.
// Green pixels (all of them, when correct).
Uint32 subjectGreen = 0;
// CONTROL: the same with the first draw rasterizing (clipped away). Must be all green.
Uint32 controlGreen = 0;
};
enum class WireDiscardStateProbeVerdict : Uint8 {
NotRun,
Inconclusive,
Clean,
// The defect: the draw after the discard draw lost the state, the one after the clipped draw
// did not.
DiscardDrawDropsState,
};
inline const char* WireDiscardStateProbeVerdictName(WireDiscardStateProbeVerdict verdict) {
switch (verdict) {
case WireDiscardStateProbeVerdict::NotRun: return "not-run";
case WireDiscardStateProbeVerdict::Inconclusive: return "inconclusive";
case WireDiscardStateProbeVerdict::Clean: return "clean";
case WireDiscardStateProbeVerdict::DiscardDrawDropsState: return "discard-draw-drops-state";
}
return "?";
}
inline WireDiscardStateProbeVerdict EvaluateWireDiscardStateProbe(const WireDiscardStateProbeMeasurement& m) {
if (!m.ran || m.fenceWaitTimedOut || m.pixels == 0) return WireDiscardStateProbeVerdict::NotRun;
if (m.controlGreen != m.pixels) return WireDiscardStateProbeVerdict::Inconclusive;
return m.subjectGreen != m.pixels ? WireDiscardStateProbeVerdict::DiscardDrawDropsState
: WireDiscardStateProbeVerdict::Clean;
}
String DescribeWireDiscardStateProbe(const WireDiscardStateProbeMeasurement& measurement);
struct WireDiscardStateProbeContext {
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
VkDevice device = VK_NULL_HANDLE;
// Must be otherwise idle for the probe's duration.
VkQueue queue = VK_NULL_HANDLE;
Uint32 queueFamilyIndex = 0;
};
// One submission on its own command pool, a bounded fence wait (on timeout every object is
// leaked and the measurement says so), teardown on every other path.
WireDiscardStateProbeMeasurement RunWireDiscardStateProbe(const WireDiscardStateProbeContext& context);
// The POST's entry: a throwaway device with one graphics queue and no features.
WireDiscardStateProbeMeasurement RunWireDiscardStateProbeOnThrowawayDevice(
PFN_vkGetInstanceProcAddr getInstanceProcAddr, VkInstance instance, VkPhysicalDevice physicalDevice,
Uint32 graphicsQueueFamilyIndex);
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,8 @@
#version 450
// The discard state probe's draws (WireDiscardStateProbe.h): vertices 0-3 are a full-viewport strip,
// vertices 4-7 the same strip far outside the clip volume (the control's first draw).
void main() {
const vec2 corners[4] = vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0));
const vec2 offset = gl_VertexIndex >= 4 ? vec2(8.0) : vec2(0.0);
gl_Position = vec4(corners[gl_VertexIndex & 3] + offset, 0.0, 1.0);
}
@@ -0,0 +1,447 @@
// Generated from WireDiscardStateProbe.vert / WireDiscardStateProbe.frag (Vulkan 1.0 / SPIR-V 1.0) by
// scripts/bake_internal_shaders.py, which runs MG_Test's BakedInternalShadersTest with the IN-TREE
// glslang and splices the words it emits into the arrays below. Do not edit the words by hand:
// MOBILEGL_BAKED_INTERNAL_SHADERS (the same test, run as a gate) recompiles every source in its table
// and reds on the first word that disagrees.
#pragma once
static constexpr Uint32 kWireDiscardStateProbeVertexSpirv[] = {
0x07230203u,
0x00010000u,
0x0008000bu,
0x00000037u,
0x00000000u,
0x00020011u,
0x00000001u,
0x0006000bu,
0x00000001u,
0x4c534c47u,
0x6474732eu,
0x3035342eu,
0x00000000u,
0x0003000eu,
0x00000000u,
0x00000001u,
0x0007000fu,
0x00000000u,
0x00000004u,
0x6e69616du,
0x00000000u,
0x0000000cu,
0x0000001eu,
0x00030003u,
0x00000002u,
0x000001c2u,
0x00040005u,
0x00000004u,
0x6e69616du,
0x00000000u,
0x00040005u,
0x00000009u,
0x7366666fu,
0x00007465u,
0x00060005u,
0x0000000cu,
0x565f6c67u,
0x65747265u,
0x646e4978u,
0x00007865u,
0x00060005u,
0x0000001cu,
0x505f6c67u,
0x65567265u,
0x78657472u,
0x00000000u,
0x00060006u,
0x0000001cu,
0x00000000u,
0x505f6c67u,
0x7469736fu,
0x006e6f69u,
0x00070006u,
0x0000001cu,
0x00000001u,
0x505f6c67u,
0x746e696fu,
0x657a6953u,
0x00000000u,
0x00070006u,
0x0000001cu,
0x00000002u,
0x435f6c67u,
0x4470696cu,
0x61747369u,
0x0065636eu,
0x00070006u,
0x0000001cu,
0x00000003u,
0x435f6c67u,
0x446c6c75u,
0x61747369u,
0x0065636eu,
0x00030005u,
0x0000001eu,
0x00000000u,
0x00050005u,
0x0000002du,
0x65646e69u,
0x6c626178u,
0x00000065u,
0x00040047u,
0x0000000cu,
0x0000000bu,
0x0000002au,
0x00030047u,
0x0000001cu,
0x00000002u,
0x00050048u,
0x0000001cu,
0x00000000u,
0x0000000bu,
0x00000000u,
0x00050048u,
0x0000001cu,
0x00000001u,
0x0000000bu,
0x00000001u,
0x00050048u,
0x0000001cu,
0x00000002u,
0x0000000bu,
0x00000003u,
0x00050048u,
0x0000001cu,
0x00000003u,
0x0000000bu,
0x00000004u,
0x00020013u,
0x00000002u,
0x00030021u,
0x00000003u,
0x00000002u,
0x00030016u,
0x00000006u,
0x00000020u,
0x00040017u,
0x00000007u,
0x00000006u,
0x00000002u,
0x00040020u,
0x00000008u,
0x00000007u,
0x00000007u,
0x00040015u,
0x0000000au,
0x00000020u,
0x00000001u,
0x00040020u,
0x0000000bu,
0x00000001u,
0x0000000au,
0x0004003bu,
0x0000000bu,
0x0000000cu,
0x00000001u,
0x0004002bu,
0x0000000au,
0x0000000eu,
0x00000004u,
0x00020014u,
0x0000000fu,
0x0004002bu,
0x00000006u,
0x00000011u,
0x41000000u,
0x0005002cu,
0x00000007u,
0x00000012u,
0x00000011u,
0x00000011u,
0x0004002bu,
0x00000006u,
0x00000013u,
0x00000000u,
0x0005002cu,
0x00000007u,
0x00000014u,
0x00000013u,
0x00000013u,
0x00040017u,
0x00000015u,
0x0000000fu,
0x00000002u,
0x00040017u,
0x00000018u,
0x00000006u,
0x00000004u,
0x00040015u,
0x00000019u,
0x00000020u,
0x00000000u,
0x0004002bu,
0x00000019u,
0x0000001au,
0x00000001u,
0x0004001cu,
0x0000001bu,
0x00000006u,
0x0000001au,
0x0006001eu,
0x0000001cu,
0x00000018u,
0x00000006u,
0x0000001bu,
0x0000001bu,
0x00040020u,
0x0000001du,
0x00000003u,
0x0000001cu,
0x0004003bu,
0x0000001du,
0x0000001eu,
0x00000003u,
0x0004002bu,
0x0000000au,
0x0000001fu,
0x00000000u,
0x0004002bu,
0x00000019u,
0x00000020u,
0x00000004u,
0x0004001cu,
0x00000021u,
0x00000007u,
0x00000020u,
0x0004002bu,
0x00000006u,
0x00000022u,
0xbf800000u,
0x0005002cu,
0x00000007u,
0x00000023u,
0x00000022u,
0x00000022u,
0x0004002bu,
0x00000006u,
0x00000024u,
0x3f800000u,
0x0005002cu,
0x00000007u,
0x00000025u,
0x00000024u,
0x00000022u,
0x0005002cu,
0x00000007u,
0x00000026u,
0x00000022u,
0x00000024u,
0x0005002cu,
0x00000007u,
0x00000027u,
0x00000024u,
0x00000024u,
0x0007002cu,
0x00000021u,
0x00000028u,
0x00000023u,
0x00000025u,
0x00000026u,
0x00000027u,
0x0004002bu,
0x0000000au,
0x0000002au,
0x00000003u,
0x00040020u,
0x0000002cu,
0x00000007u,
0x00000021u,
0x00040020u,
0x00000035u,
0x00000003u,
0x00000018u,
0x00050036u,
0x00000002u,
0x00000004u,
0x00000000u,
0x00000003u,
0x000200f8u,
0x00000005u,
0x0004003bu,
0x00000008u,
0x00000009u,
0x00000007u,
0x0004003bu,
0x0000002cu,
0x0000002du,
0x00000007u,
0x0004003du,
0x0000000au,
0x0000000du,
0x0000000cu,
0x000500afu,
0x0000000fu,
0x00000010u,
0x0000000du,
0x0000000eu,
0x00050050u,
0x00000015u,
0x00000016u,
0x00000010u,
0x00000010u,
0x000600a9u,
0x00000007u,
0x00000017u,
0x00000016u,
0x00000012u,
0x00000014u,
0x0003003eu,
0x00000009u,
0x00000017u,
0x0004003du,
0x0000000au,
0x00000029u,
0x0000000cu,
0x000500c7u,
0x0000000au,
0x0000002bu,
0x00000029u,
0x0000002au,
0x0003003eu,
0x0000002du,
0x00000028u,
0x00050041u,
0x00000008u,
0x0000002eu,
0x0000002du,
0x0000002bu,
0x0004003du,
0x00000007u,
0x0000002fu,
0x0000002eu,
0x0004003du,
0x00000007u,
0x00000030u,
0x00000009u,
0x00050081u,
0x00000007u,
0x00000031u,
0x0000002fu,
0x00000030u,
0x00050051u,
0x00000006u,
0x00000032u,
0x00000031u,
0x00000000u,
0x00050051u,
0x00000006u,
0x00000033u,
0x00000031u,
0x00000001u,
0x00070050u,
0x00000018u,
0x00000034u,
0x00000032u,
0x00000033u,
0x00000013u,
0x00000024u,
0x00050041u,
0x00000035u,
0x00000036u,
0x0000001eu,
0x0000001fu,
0x0003003eu,
0x00000036u,
0x00000034u,
0x000100fdu,
0x00010038u,
};
static constexpr Uint32 kWireDiscardStateProbeFragmentSpirv[] = {
0x07230203u,
0x00010000u,
0x0008000bu,
0x0000000du,
0x00000000u,
0x00020011u,
0x00000001u,
0x0006000bu,
0x00000001u,
0x4c534c47u,
0x6474732eu,
0x3035342eu,
0x00000000u,
0x0003000eu,
0x00000000u,
0x00000001u,
0x0006000fu,
0x00000004u,
0x00000004u,
0x6e69616du,
0x00000000u,
0x00000009u,
0x00030010u,
0x00000004u,
0x00000007u,
0x00030003u,
0x00000002u,
0x000001c2u,
0x00040005u,
0x00000004u,
0x6e69616du,
0x00000000u,
0x00040005u,
0x00000009u,
0x6f6c6f63u,
0x00000072u,
0x00040047u,
0x00000009u,
0x0000001eu,
0x00000000u,
0x00020013u,
0x00000002u,
0x00030021u,
0x00000003u,
0x00000002u,
0x00030016u,
0x00000006u,
0x00000020u,
0x00040017u,
0x00000007u,
0x00000006u,
0x00000004u,
0x00040020u,
0x00000008u,
0x00000003u,
0x00000007u,
0x0004003bu,
0x00000008u,
0x00000009u,
0x00000003u,
0x0004002bu,
0x00000006u,
0x0000000au,
0x00000000u,
0x0004002bu,
0x00000006u,
0x0000000bu,
0x3f800000u,
0x0007002cu,
0x00000007u,
0x0000000cu,
0x0000000au,
0x0000000bu,
0x0000000au,
0x0000000bu,
0x00050036u,
0x00000002u,
0x00000004u,
0x00000000u,
0x00000003u,
0x000200f8u,
0x00000005u,
0x0003003eu,
0x00000009u,
0x0000000cu,
0x000100fdu,
0x00010038u,
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
#version 450
// Raw texel-block copy out of an E5B9G9R9 image (WireE5RawCopy.h). The source is read through an
// R32_UINT view, so its words are never decoded, and stored into an R32_UINT twin whose transfer
// reads move bits rather than values. Layered and plain 2D sources; WireE5RawCopy3D.comp is the
// volume twin of this shader.
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
layout(set = 0, binding = 0) uniform usampler2DArray sourceImage;
layout(set = 0, binding = 1, r32ui) uniform writeonly uimage2DArray twinImage;
layout(push_constant) uniform Region {
// xy: the region's texel origin in the source level; z unused (the view starts at the
// region's first layer).
ivec4 sourceOffset;
// xyz: texels by layers to copy.
ivec4 extent;
} region;
void main() {
ivec3 id = ivec3(gl_GlobalInvocationID);
if (any(greaterThanEqual(id, region.extent.xyz))) return;
uint word = texelFetch(sourceImage, ivec3(region.sourceOffset.xy + id.xy, id.z), 0).r;
imageStore(twinImage, id, uvec4(word, 0u, 0u, 0u));
}
@@ -0,0 +1,588 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireE5RawCopy.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// See WireE5RawCopy.h. The probe's plumbing is WireDepthResolveProbe.cpp's: one throwaway command
// buffer, a bounded fence wait that leaks rather than idle-waits a hung GPU, teardown on every
// other path.
#include "WireE5RawCopy.h"
#include <MG_Util/Debug/Log.h>
#include <cstring>
#include <vulkan/vulkan_core.h>
namespace MobileGL::MG_Backend::DirectVulkan {
#include "WireE5RawCopySpirv.h"
namespace {
// The shaders' push-constant block: ivec4 sourceOffset, ivec4 extent.
struct RegionPush {
Int32 sourceOffset[4];
Int32 extent[4];
};
constexpr Uint32 kGroupSize = 8;
} // namespace
VkResult CreateWireE5RawCopyKit(VkDevice device, WireE5RawCopyKit& kit) {
const auto shader = [&](const Uint32* code, SizeT bytes, VkShaderModule& module) {
VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
info.codeSize = bytes;
info.pCode = code;
return vkCreateShaderModule(device, &info, nullptr, &module);
};
VkResult result = shader(kWireE5RawCopySpirv, sizeof(kWireE5RawCopySpirv), kit.arrayModule);
if (result != VK_SUCCESS) return result;
result = shader(kWireE5RawCopy3DSpirv, sizeof(kWireE5RawCopy3DSpirv), kit.volumeModule);
if (result != VK_SUCCESS) return result;
const VkDescriptorSetLayoutBinding bindings[2]{
{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr},
{1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr},
};
VkDescriptorSetLayoutCreateInfo layout{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
layout.bindingCount = 2;
layout.pBindings = bindings;
result = vkCreateDescriptorSetLayout(device, &layout, nullptr, &kit.descriptorLayout);
if (result != VK_SUCCESS) return result;
const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(RegionPush)};
VkPipelineLayoutCreateInfo pipelineLayout{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
pipelineLayout.setLayoutCount = 1;
pipelineLayout.pSetLayouts = &kit.descriptorLayout;
pipelineLayout.pushConstantRangeCount = 1;
pipelineLayout.pPushConstantRanges = &push;
result = vkCreatePipelineLayout(device, &pipelineLayout, nullptr, &kit.pipelineLayout);
if (result != VK_SUCCESS) return result;
const auto pipeline = [&](VkShaderModule module, VkPipeline& out) {
VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
info.stage.module = module;
info.stage.pName = "main";
info.layout = kit.pipelineLayout;
return vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &info, nullptr, &out);
};
result = pipeline(kit.arrayModule, kit.arrayPipeline);
if (result != VK_SUCCESS) return result;
result = pipeline(kit.volumeModule, kit.volumePipeline);
if (result != VK_SUCCESS) return result;
// texelFetch ignores the filter; the sampler exists only because a GLSL usampler is a
// COMBINED image sampler. An integer format must not be linearly filtered anyway.
VkSamplerCreateInfo sampler{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
sampler.addressModeU = sampler.addressModeV = sampler.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler.magFilter = sampler.minFilter = VK_FILTER_NEAREST;
sampler.maxLod = 0.0f;
return vkCreateSampler(device, &sampler, nullptr, &kit.nearest);
}
void DestroyWireE5RawCopyKit(VkDevice device, WireE5RawCopyKit& kit) {
vkDestroySampler(device, kit.nearest, nullptr);
vkDestroyPipeline(device, kit.arrayPipeline, nullptr);
vkDestroyPipeline(device, kit.volumePipeline, nullptr);
vkDestroyPipelineLayout(device, kit.pipelineLayout, nullptr);
vkDestroyDescriptorSetLayout(device, kit.descriptorLayout, nullptr);
vkDestroyShaderModule(device, kit.arrayModule, nullptr);
vkDestroyShaderModule(device, kit.volumeModule, nullptr);
kit = {};
}
Bool WireE5RawCopySupported(VkPhysicalDevice physicalDevice) {
VkFormatProperties properties{};
vkGetPhysicalDeviceFormatProperties(physicalDevice, VK_FORMAT_R32_UINT, &properties);
constexpr VkFormatFeatureFlags kRequired = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT |
VK_FORMAT_FEATURE_TRANSFER_SRC_BIT;
return (properties.optimalTilingFeatures & kRequired) == kRequired;
}
VkImageCreateInfo WireE5RawCopyTwinInfo(const WireE5RawCopyRegion& region) {
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
info.imageType = region.volume ? VK_IMAGE_TYPE_3D : VK_IMAGE_TYPE_2D;
info.format = VK_FORMAT_R32_UINT;
info.extent = {region.extent.width, region.extent.height, region.volume ? region.extent.depth : 1u};
info.mipLevels = 1;
info.arrayLayers = region.volume ? 1u : region.layerCount;
info.samples = VK_SAMPLE_COUNT_1_BIT;
info.tiling = VK_IMAGE_TILING_OPTIMAL;
info.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
return info;
}
void DestroyWireE5RawCopyObjects(VkDevice device, WireE5RawCopyObjects& objects) {
vkDestroyImageView(device, objects.sourceView, nullptr);
vkDestroyImageView(device, objects.twinView, nullptr);
vkDestroyDescriptorPool(device, objects.descriptorPool, nullptr);
objects = {};
}
VkResult RecordWireE5RawCopy(VkDevice device, VkCommandBuffer commandBuffer, const WireE5RawCopyKit& kit,
const WireE5RawCopyRegion& region, VkImageLayout sourceLayout, VkImage twin,
WireE5RawCopyObjects& objects) {
if (kit.arrayPipeline == VK_NULL_HANDLE || kit.volumePipeline == VK_NULL_HANDLE)
return VK_ERROR_INITIALIZATION_FAILED;
const Uint32 slices = region.volume ? region.extent.depth : region.layerCount;
if (region.extent.width == 0 || region.extent.height == 0 || slices == 0) return VK_SUCCESS;
// The source through R32_UINT: same 32-bit compatibility class, so a plain view of a
// MUTABLE_FORMAT image. A 3D view covers every slice; the shader offsets into it.
VkImageViewCreateInfo sourceInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
sourceInfo.image = region.source;
sourceInfo.viewType = region.volume ? VK_IMAGE_VIEW_TYPE_3D : VK_IMAGE_VIEW_TYPE_2D_ARRAY;
sourceInfo.format = VK_FORMAT_R32_UINT;
sourceInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, region.level, 1,
region.volume ? 0u : region.baseLayer, region.volume ? 1u : region.layerCount};
VkResult result = vkCreateImageView(device, &sourceInfo, nullptr, &objects.sourceView);
if (result != VK_SUCCESS) return result;
VkImageViewCreateInfo twinInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
twinInfo.image = twin;
twinInfo.viewType = region.volume ? VK_IMAGE_VIEW_TYPE_3D : VK_IMAGE_VIEW_TYPE_2D_ARRAY;
twinInfo.format = VK_FORMAT_R32_UINT;
twinInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, region.volume ? 1u : region.layerCount};
result = vkCreateImageView(device, &twinInfo, nullptr, &objects.twinView);
if (result != VK_SUCCESS) return result;
const VkDescriptorPoolSize poolSizes[2]{{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1},
{VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1}};
VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
pool.maxSets = 1;
pool.poolSizeCount = 2;
pool.pPoolSizes = poolSizes;
result = vkCreateDescriptorPool(device, &pool, nullptr, &objects.descriptorPool);
if (result != VK_SUCCESS) return result;
VkDescriptorSetAllocateInfo alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
alloc.descriptorPool = objects.descriptorPool;
alloc.descriptorSetCount = 1;
alloc.pSetLayouts = &kit.descriptorLayout;
VkDescriptorSet set = VK_NULL_HANDLE;
result = vkAllocateDescriptorSets(device, &alloc, &set);
if (result != VK_SUCCESS) return result;
const VkDescriptorImageInfo sampled{kit.nearest, objects.sourceView, sourceLayout};
const VkDescriptorImageInfo storage{VK_NULL_HANDLE, objects.twinView, VK_IMAGE_LAYOUT_GENERAL};
VkWriteDescriptorSet writes[2]{};
for (Uint32 i = 0; i < 2; ++i) {
writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[i].dstSet = set;
writes[i].dstBinding = i;
writes[i].descriptorCount = 1;
}
writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
writes[0].pImageInfo = &sampled;
writes[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
writes[1].pImageInfo = &storage;
vkUpdateDescriptorSets(device, 2, writes, 0, nullptr);
// Every earlier write - transfer, attachment or shader - visible to the fetch, and the
// fresh twin ready for storage writes.
VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
memory.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
VkImageMemoryBarrier twinBarrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
twinBarrier.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
twinBarrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
twinBarrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
twinBarrier.srcQueueFamilyIndex = twinBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
twinBarrier.image = twin;
twinBarrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, VK_REMAINING_ARRAY_LAYERS};
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
0, 1, &memory, 0, nullptr, 1, &twinBarrier);
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE,
region.volume ? kit.volumePipeline : kit.arrayPipeline);
vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, kit.pipelineLayout, 0, 1, &set, 0,
nullptr);
const RegionPush push{{region.offset.x, region.offset.y, region.volume ? region.offset.z : 0, 0},
{static_cast<Int32>(region.extent.width), static_cast<Int32>(region.extent.height),
static_cast<Int32>(slices), 0}};
vkCmdPushConstants(commandBuffer, kit.pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(push), &push);
vkCmdDispatch(commandBuffer, (region.extent.width + kGroupSize - 1) / kGroupSize,
(region.extent.height + kGroupSize - 1) / kGroupSize, slices);
twinBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
twinBarrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
twinBarrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL;
twinBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0,
nullptr, 0, nullptr, 1, &twinBarrier);
return VK_SUCCESS;
}
String DescribeWireE5TransferProbe(const WireE5TransferProbeMeasurement& measurement) {
if (!measurement.ran)
return format("not run ({})", measurement.failureReason.empty() ? "?" : measurement.failureReason);
return format("30x30 transfer read: {}/{} words changed (first {:#010x} -> {:#010x}); shader read: {} "
"changed; 16x16 transfer read: {}/{} changed",
measurement.subjectMismatches, measurement.texels, measurement.firstExpected,
measurement.firstObserved, measurement.controlMismatches, measurement.smallMismatches,
measurement.smallTexels);
}
namespace {
constexpr Uint32 kSubjectExtent = 30;
constexpr Uint32 kSmallExtent = 16;
constexpr Uint64 kFenceTimeoutNs = 5ull * 1000ull * 1000ull * 1000ull;
// A non-canonical RGB9_E5 word: exponent 1..31 and every mantissa below 256, so the same
// value also encodes with the exponent one lower and every mantissa doubled. A path that
// decodes and re-encodes changes it; a bit move does not.
Uint32 NonCanonicalWord(Uint32 index) {
Uint32 state = index * 2654435761u + 0x9E3779B9u;
state ^= state >> 15;
state *= 2246822519u;
state ^= state >> 13;
const Uint32 r = state & 0xFFu;
const Uint32 g = (state >> 8) & 0xFFu;
const Uint32 b = ((state >> 16) & 0xFFu) | ((r | g) == 0 ? 1u : 0u);
const Uint32 exponent = 1u + ((state >> 24) % 31u);
return r | (g << 9) | (b << 18) | (exponent << 27);
}
struct ProbeObjects {
VkDevice device = VK_NULL_HANDLE;
Vector<VkImage> images;
Vector<VkDeviceMemory> memories;
Vector<VkBuffer> buffers;
VkCommandPool commandPool = VK_NULL_HANDLE;
VkFence fence = VK_NULL_HANDLE;
WireE5RawCopyKit kit;
WireE5RawCopyObjects copy;
void Destroy() {
DestroyWireE5RawCopyObjects(device, copy);
for (VkImage image : images) vkDestroyImage(device, image, nullptr);
for (VkBuffer buffer : buffers) vkDestroyBuffer(device, buffer, nullptr);
for (VkDeviceMemory memory : memories) vkFreeMemory(device, memory, nullptr);
vkDestroyCommandPool(device, commandPool, nullptr);
vkDestroyFence(device, fence, nullptr);
DestroyWireE5RawCopyKit(device, kit);
}
};
Optional<Uint32> PickMemoryType(const VkPhysicalDeviceMemoryProperties& properties, Uint32 allowed,
VkMemoryPropertyFlags wanted, VkMemoryPropertyFlags required) {
Optional<Uint32> fallback;
for (Uint32 i = 0; i < properties.memoryTypeCount; ++i) {
if (!(allowed & (1u << i))) continue;
const VkMemoryPropertyFlags flags = properties.memoryTypes[i].propertyFlags;
if (flags & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT) continue;
if ((flags & required) != required) continue;
if ((flags & wanted) == wanted) return i;
if (!fallback) fallback = i;
}
return fallback;
}
void ImageBarrier(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout oldLayout,
VkImageLayout newLayout, VkPipelineStageFlags srcStage, VkAccessFlags srcAccess,
VkPipelineStageFlags dstStage, VkAccessFlags dstAccess) {
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
barrier.srcAccessMask = srcAccess;
barrier.dstAccessMask = dstAccess;
barrier.oldLayout = oldLayout;
barrier.newLayout = newLayout;
barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image;
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
vkCmdPipelineBarrier(commandBuffer, srcStage, dstStage, 0, 0, nullptr, 0, nullptr, 1, &barrier);
}
} // namespace
WireE5TransferProbeMeasurement RunWireE5TransferProbe(const WireE5TransferProbeContext& context) {
WireE5TransferProbeMeasurement measurement;
const VkDevice device = context.device;
if (device == VK_NULL_HANDLE || context.queue == VK_NULL_HANDLE) {
measurement.failureReason = "no device or queue";
return measurement;
}
if (!WireE5RawCopySupported(context.physicalDevice)) {
measurement.failureReason = "R32_UINT is not a sampled + storage + transfer-source format here";
return measurement;
}
// The usage production gives a wire E5B9G9R9 texture: SAMPLED | TRANSFER_SRC | TRANSFER_DST,
// plus COLOR_ATTACHMENT where the format can be one.
VkFormatProperties e5Properties{};
vkGetPhysicalDeviceFormatProperties(context.physicalDevice, VK_FORMAT_E5B9G9R9_UFLOAT_PACK32, &e5Properties);
constexpr VkFormatFeatureFlags kE5Required = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
VK_FORMAT_FEATURE_TRANSFER_SRC_BIT |
VK_FORMAT_FEATURE_TRANSFER_DST_BIT;
if ((e5Properties.optimalTilingFeatures & kE5Required) != kE5Required) {
measurement.failureReason = "E5B9G9R9 is not a sampled transfer format here";
return measurement;
}
VkImageUsageFlags e5Usage =
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
if (e5Properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT)
e5Usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
ProbeObjects objects;
objects.device = device;
Bool leak = false;
struct Teardown {
ProbeObjects& objects;
Bool& leak;
~Teardown() {
if (!leak) objects.Destroy();
}
} teardown{objects, leak};
const auto fail = [&](String reason) {
measurement.ran = false;
measurement.failureReason = Move(reason);
return measurement;
};
VkPhysicalDeviceMemoryProperties memoryProperties{};
vkGetPhysicalDeviceMemoryProperties(context.physicalDevice, &memoryProperties);
const auto bindImage = [&](VkImage image) -> Bool {
VkMemoryRequirements requirements{};
vkGetImageMemoryRequirements(device, image, &requirements);
const Optional<Uint32> type = PickMemoryType(memoryProperties, requirements.memoryTypeBits,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0);
if (!type) return false;
VkMemoryAllocateInfo allocate{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocate.allocationSize = requirements.size;
allocate.memoryTypeIndex = *type;
VkDeviceMemory memory = VK_NULL_HANDLE;
if (vkAllocateMemory(device, &allocate, nullptr, &memory) != VK_SUCCESS) return false;
objects.memories.push_back(memory);
return vkBindImageMemory(device, image, memory, 0) == VK_SUCCESS;
};
const auto makeImage = [&](const VkImageCreateInfo& info, VkImage& out) -> Bool {
if (vkCreateImage(device, &info, nullptr, &out) != VK_SUCCESS) return false;
objects.images.push_back(out);
return bindImage(out);
};
const auto makeE5 = [&](Uint32 extent, VkImage& out) {
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
info.flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
info.imageType = VK_IMAGE_TYPE_2D;
info.format = VK_FORMAT_E5B9G9R9_UFLOAT_PACK32;
info.extent = {extent, extent, 1};
info.mipLevels = info.arrayLayers = 1;
info.samples = VK_SAMPLE_COUNT_1_BIT;
info.tiling = VK_IMAGE_TILING_OPTIMAL;
info.usage = e5Usage;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
return makeImage(info, out);
};
// Host-visible buffers: the upload source and the three readbacks, one allocation each.
struct HostBuffer {
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
Bool coherent = false;
};
const auto makeBuffer = [&](VkDeviceSize bytes, VkBufferUsageFlags usage, HostBuffer& out) -> Bool {
VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
info.size = bytes;
info.usage = usage;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (vkCreateBuffer(device, &info, nullptr, &out.buffer) != VK_SUCCESS) return false;
objects.buffers.push_back(out.buffer);
VkMemoryRequirements requirements{};
vkGetBufferMemoryRequirements(device, out.buffer, &requirements);
const Optional<Uint32> type =
PickMemoryType(memoryProperties, requirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT);
if (!type) return false;
out.coherent =
(memoryProperties.memoryTypes[*type].propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) != 0;
VkMemoryAllocateInfo allocate{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocate.allocationSize = requirements.size;
allocate.memoryTypeIndex = *type;
if (vkAllocateMemory(device, &allocate, nullptr, &out.memory) != VK_SUCCESS) return false;
objects.memories.push_back(out.memory);
return vkBindBufferMemory(device, out.buffer, out.memory, 0) == VK_SUCCESS;
};
constexpr Uint32 kTexels = kSubjectExtent * kSubjectExtent;
constexpr Uint32 kSmallTexels = kSmallExtent * kSmallExtent;
constexpr VkDeviceSize kBytes = kTexels * sizeof(Uint32);
constexpr VkDeviceSize kSmallBytes = kSmallTexels * sizeof(Uint32);
VkImage subject = VK_NULL_HANDLE, small = VK_NULL_HANDLE, twin = VK_NULL_HANDLE;
if (!makeE5(kSubjectExtent, subject) || !makeE5(kSmallExtent, small))
return fail("E5B9G9R9 image creation failed");
WireE5RawCopyRegion region;
region.source = subject;
region.extent = {kSubjectExtent, kSubjectExtent, 1};
if (!makeImage(WireE5RawCopyTwinInfo(region), twin)) return fail("R32_UINT twin creation failed");
HostBuffer upload, subjectOut, controlOut, smallOut;
if (!makeBuffer(kBytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, upload) ||
!makeBuffer(kBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT, subjectOut) ||
!makeBuffer(kBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT, controlOut) ||
!makeBuffer(kSmallBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT, smallOut))
return fail("buffer creation failed");
if (const VkResult result = CreateWireE5RawCopyKit(device, objects.kit); result != VK_SUCCESS)
return fail(format("raw copy kit creation failed (VkResult {})", static_cast<Int>(result)));
{
void* mapped = nullptr;
if (vkMapMemory(device, upload.memory, 0, VK_WHOLE_SIZE, 0, &mapped) != VK_SUCCESS || mapped == nullptr)
return fail("vkMapMemory (upload) failed");
Uint32* words = static_cast<Uint32*>(mapped);
for (Uint32 i = 0; i < kTexels; ++i) words[i] = NonCanonicalWord(i);
if (!upload.coherent) {
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
range.memory = upload.memory;
range.size = VK_WHOLE_SIZE;
vkFlushMappedMemoryRanges(device, 1, &range);
}
vkUnmapMemory(device, upload.memory);
}
VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
poolInfo.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT;
poolInfo.queueFamilyIndex = context.queueFamilyIndex;
if (vkCreateCommandPool(device, &poolInfo, nullptr, &objects.commandPool) != VK_SUCCESS)
return fail("vkCreateCommandPool failed");
VkCommandBufferAllocateInfo commandInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
commandInfo.commandPool = objects.commandPool;
commandInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
commandInfo.commandBufferCount = 1;
VkCommandBuffer cmd = VK_NULL_HANDLE;
if (vkAllocateCommandBuffers(device, &commandInfo, &cmd) != VK_SUCCESS)
return fail("vkAllocateCommandBuffers failed");
VkCommandBufferBeginInfo beginInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
if (vkBeginCommandBuffer(cmd, &beginInfo) != VK_SUCCESS) return fail("vkBeginCommandBuffer failed");
// (1) Upload: transfer writes into E5B9G9R9 move bits (measured), so both images hold
// exactly the generated words; the small one gets the first 8x8 of them.
for (const VkImage image : {subject, small}) {
ImageBarrier(cmd, image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_ACCESS_TRANSFER_WRITE_BIT);
}
VkBufferImageCopy copy{};
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
copy.imageExtent = {kSubjectExtent, kSubjectExtent, 1};
vkCmdCopyBufferToImage(cmd, upload.buffer, subject, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copy);
VkBufferImageCopy smallCopy = copy;
smallCopy.bufferRowLength = kSubjectExtent;
smallCopy.imageExtent = {kSmallExtent, kSmallExtent, 1};
vkCmdCopyBufferToImage(cmd, upload.buffer, small, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &smallCopy);
// (2) SUBJECT and the small reading: plain transfer reads.
for (const VkImage image : {subject, small}) {
ImageBarrier(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_ACCESS_TRANSFER_READ_BIT);
}
vkCmdCopyImageToBuffer(cmd, subject, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, subjectOut.buffer, 1, &copy);
VkBufferImageCopy smallOutCopy = copy;
smallOutCopy.imageExtent = {kSmallExtent, kSmallExtent, 1};
vkCmdCopyImageToBuffer(cmd, small, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, smallOut.buffer, 1, &smallOutCopy);
// (3) CONTROL: the route, exactly as production records it.
ImageBarrier(cmd, subject, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
VK_ACCESS_SHADER_READ_BIT);
if (const VkResult result =
RecordWireE5RawCopy(device, cmd, objects.kit, region, VK_IMAGE_LAYOUT_GENERAL, twin, objects.copy);
result != VK_SUCCESS)
return fail(format("recording the raw copy failed (VkResult {})", static_cast<Int>(result)));
vkCmdCopyImageToBuffer(cmd, twin, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, controlOut.buffer, 1, &copy);
VkMemoryBarrier hostBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
hostBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
hostBarrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &hostBarrier, 0,
nullptr, 0, nullptr);
if (vkEndCommandBuffer(cmd) != VK_SUCCESS) return fail("vkEndCommandBuffer failed");
VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
if (vkCreateFence(device, &fenceInfo, nullptr, &objects.fence) != VK_SUCCESS)
return fail("vkCreateFence failed");
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit.commandBufferCount = 1;
submit.pCommandBuffers = &cmd;
if (const VkResult result = vkQueueSubmit(context.queue, 1, &submit, objects.fence); result != VK_SUCCESS)
return fail(format("vkQueueSubmit failed (VkResult {})", static_cast<Int>(result)));
const VkResult waited = vkWaitForFences(device, 1, &objects.fence, VK_TRUE, kFenceTimeoutNs);
if (waited == VK_TIMEOUT) {
leak = true;
measurement.fenceWaitTimedOut = true;
return fail("the probe's fence did not signal within 5 s");
}
if (waited != VK_SUCCESS) return fail(format("vkWaitForFences failed (VkResult {})", static_cast<Int>(waited)));
const auto read = [&](const HostBuffer& buffer, const Uint32*& words) -> Bool {
void* mapped = nullptr;
if (vkMapMemory(device, buffer.memory, 0, VK_WHOLE_SIZE, 0, &mapped) != VK_SUCCESS || mapped == nullptr)
return false;
if (!buffer.coherent) {
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
range.memory = buffer.memory;
range.size = VK_WHOLE_SIZE;
vkInvalidateMappedMemoryRanges(device, 1, &range);
}
words = static_cast<const Uint32*>(mapped);
return true;
};
const Uint32 *subjectWords = nullptr, *controlWords = nullptr, *smallWords = nullptr;
if (!read(subjectOut, subjectWords) || !read(controlOut, controlWords) || !read(smallOut, smallWords))
return fail("vkMapMemory (readback) failed");
measurement.texels = kTexels;
for (Uint32 i = 0; i < kTexels; ++i) {
const Uint32 expected = NonCanonicalWord(i);
if (subjectWords[i] != expected) {
if (measurement.subjectMismatches++ == 0) {
measurement.firstExpected = expected;
measurement.firstObserved = subjectWords[i];
}
}
if (controlWords[i] != expected) ++measurement.controlMismatches;
}
measurement.smallTexels = kSmallTexels;
for (Uint32 y = 0; y < kSmallExtent; ++y)
for (Uint32 x = 0; x < kSmallExtent; ++x)
if (smallWords[y * kSmallExtent + x] != NonCanonicalWord(y * kSubjectExtent + x))
++measurement.smallMismatches;
vkUnmapMemory(device, subjectOut.memory);
vkUnmapMemory(device, controlOut.memory);
vkUnmapMemory(device, smallOut.memory);
measurement.ran = true;
return measurement;
}
WireE5TransferProbeMeasurement RunWireE5TransferProbeOnThrowawayDevice(PFN_vkGetInstanceProcAddr getInstanceProcAddr,
VkInstance instance,
VkPhysicalDevice physicalDevice,
Uint32 graphicsQueueFamilyIndex) {
WireE5TransferProbeMeasurement measurement;
const auto createDevice =
reinterpret_cast<PFN_vkCreateDevice>(getInstanceProcAddr(instance, "vkCreateDevice"));
if (createDevice == nullptr) {
measurement.failureReason = "no vkCreateDevice";
return measurement;
}
const Float priority = 1.0f;
VkDeviceQueueCreateInfo queueInfo{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
queueInfo.queueFamilyIndex = graphicsQueueFamilyIndex;
queueInfo.queueCount = 1;
queueInfo.pQueuePriorities = &priority;
VkDeviceCreateInfo deviceInfo{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
deviceInfo.queueCreateInfoCount = 1;
deviceInfo.pQueueCreateInfos = &queueInfo;
VkDevice device = VK_NULL_HANDLE;
if (const VkResult result = createDevice(physicalDevice, &deviceInfo, nullptr, &device);
result != VK_SUCCESS || device == VK_NULL_HANDLE) {
measurement.failureReason = format("vkCreateDevice failed (VkResult {})", static_cast<Int>(result));
return measurement;
}
WireE5TransferProbeContext context;
context.physicalDevice = physicalDevice;
context.device = device;
vkGetDeviceQueue(device, graphicsQueueFamilyIndex, 0, &context.queue);
context.queueFamilyIndex = graphicsQueueFamilyIndex;
measurement = RunWireE5TransferProbe(context);
if (!measurement.fenceWaitTimedOut) vkDestroyDevice(device, nullptr);
return measurement;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,168 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireE5RawCopy.h
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Config.h>
#include <Includes.h>
#include <vulkan/vulkan_core.h>
// THE RAW E5B9G9R9 READ ROUTE, and the probe that decides whether a device needs it.
//
// GL makes RGB9_E5 a bit-exact format wherever it moves texel blocks: glGetTexImage with
// GL_UNSIGNED_INT_5_9_9_9_REV hands back the words the application stored, and
// glCopyImageSubData is a raw block move (GL 4.6 core 18.3.2). RGB9_E5 has redundant encodings -
// a shared exponent one higher with every mantissa halved is the same value - so a path that
// decodes and re-encodes keeps every value and still changes words.
//
// Adreno 750 does exactly that on TRANSFER READS of a VK_FORMAT_E5B9G9R9_UFLOAT_PACK32 image once
// the level is not a power-of-two size: vkCmdCopyImageToBuffer and vkCmdCopyImage out of a 30x30
// level return every non-canonical word canonicalized, while a 16x16 or 64x64 level reads back verbatim.
// The stored words are intact - a shader reading the same image through an R32_UINT view sees
// every one of them - and so are transfer WRITES into it. Measured with format lists, without
// the colour-attachment usage and with storage usage: none of them changes the answer, and the
// device has no VK_EXT_image_compression_control to ask with.
//
// The route therefore reads through a shader: a compute pass fetches the source through an
// R32_UINT view (the wire's E5B9G9R9 textures are MUTABLE_FORMAT from birth, see
// VkTextureManager) and stores the words into an R32_UINT twin, whose transfer reads move bits.
// The caller then reads the twin where it would have read the source.
//
// Whether a device needs it is MEASURED (RunWireE5TransferProbe below), at Magma bring-up
// (VulkanRenderer::ArmWireE5RawReads) and in the POST's "Known Driver Bugs" section
// (MG_Util/SelfTest/DriverBugProbes.cpp); a device that reads E5B9G9R9 back verbatim keeps its
// plain transfers.
namespace MobileGL::MG_Backend::DirectVulkan {
// Device objects that do not depend on any image.
struct WireE5RawCopyKit {
VkShaderModule arrayModule = VK_NULL_HANDLE;
VkShaderModule volumeModule = VK_NULL_HANDLE;
VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
VkPipeline arrayPipeline = VK_NULL_HANDLE;
VkPipeline volumePipeline = VK_NULL_HANDLE;
VkSampler nearest = VK_NULL_HANDLE;
};
// Creates every member; on failure the members made so far stay set, for DestroyWireE5RawCopyKit.
VkResult CreateWireE5RawCopyKit(VkDevice device, WireE5RawCopyKit& kit);
void DestroyWireE5RawCopyKit(VkDevice device, WireE5RawCopyKit& kit);
// Whether the device can run the route at all: R32_UINT sampled and storage on optimal tiling.
Bool WireE5RawCopySupported(VkPhysicalDevice physicalDevice);
// One region of one level of the source. `volume` for a VK_IMAGE_TYPE_3D source, whose slices
// are `offset.z` .. `offset.z + extent.depth`; any other source is a 2D (possibly layered or
// cube-compatible) image whose layers are `baseLayer` .. `baseLayer + layerCount` and whose
// `offset.z` / `extent.depth` are 0 / 1. 1D images are not supported.
struct WireE5RawCopyRegion {
VkImage source = VK_NULL_HANDLE;
Bool volume = false;
Uint32 level = 0;
Uint32 baseLayer = 0;
Uint32 layerCount = 1;
VkOffset3D offset{0, 0, 0};
VkExtent3D extent{1, 1, 1};
};
// The twin the caller creates for `region`: R32_UINT, one level, the region's extent at its
// origin - a 3D image for a volume region, a 2D image of `layerCount` layers otherwise -
// with STORAGE | TRANSFER_SRC usage. RecordWireE5RawCopy leaves it in TRANSFER_SRC_OPTIMAL.
VkImageCreateInfo WireE5RawCopyTwinInfo(const WireE5RawCopyRegion& region);
// What a recorded copy holds until its submission completes.
struct WireE5RawCopyObjects {
VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
VkImageView sourceView = VK_NULL_HANDLE;
VkImageView twinView = VK_NULL_HANDLE;
};
void DestroyWireE5RawCopyObjects(VkDevice device, WireE5RawCopyObjects& objects);
// Records the copy of `region` into `twin` (fresh, created from WireE5RawCopyTwinInfo).
// The source must have been created MUTABLE_FORMAT with an E5B9G9R9 format, and be in
// `sourceLayout` (GENERAL or SHADER_READ_ONLY_OPTIMAL); the recording starts with a global
// barrier that makes every earlier write visible to the compute stage and ends with the twin
// in TRANSFER_SRC_OPTIMAL, its writes visible to transfer reads. On failure nothing has been
// recorded and `objects` holds whatever was created, for DestroyWireE5RawCopyObjects.
VkResult RecordWireE5RawCopy(VkDevice device, VkCommandBuffer commandBuffer, const WireE5RawCopyKit& kit,
const WireE5RawCopyRegion& region, VkImageLayout sourceLayout, VkImage twin,
WireE5RawCopyObjects& objects);
// ---------------------------------------------------------------------------------------
// THE PROBE.
struct WireE5TransferProbeMeasurement {
// Recorded, submitted and read back.
Bool ran = false;
// The bounded fence wait expired: every object was deliberately leaked.
Bool fenceWaitTimedOut = false;
String failureReason;
// SUBJECT: a 30x30 E5B9G9R9 level of non-canonical words read back by vkCmdCopyImageToBuffer.
Uint32 texels = 0;
Uint32 subjectMismatches = 0;
Uint32 firstExpected = 0;
Uint32 firstObserved = 0;
// CONTROL: the same image read through the route. Must be exact, or the upload, the
// comparison or the route itself is what failed, and nothing is concluded.
Uint32 controlMismatches = 0;
// Reported, not judged: the same words in a 16x16 level read back by plain transfer. Clean
// on the device this was characterised on (power-of-two extents read back verbatim, 28, 30
// and 31 do not), which is why the subject is 30x30.
Uint32 smallTexels = 0;
Uint32 smallMismatches = 0;
};
enum class WireE5TransferProbeVerdict : Uint8 {
// Nothing was measured (the route is unavailable, or setup failed): plain transfers stay.
NotRun,
// Measured, but the route's own read was not exact: nothing is concluded.
Inconclusive,
// Plain transfer reads return the stored words.
Clean,
// The defect: plain transfer reads changed words the route read back exactly.
TransferReadNormalizes,
};
inline const char* WireE5TransferProbeVerdictName(WireE5TransferProbeVerdict verdict) {
switch (verdict) {
case WireE5TransferProbeVerdict::NotRun: return "not-run";
case WireE5TransferProbeVerdict::Inconclusive: return "inconclusive";
case WireE5TransferProbeVerdict::Clean: return "clean";
case WireE5TransferProbeVerdict::TransferReadNormalizes: return "transfer-read-normalizes";
}
return "?";
}
inline WireE5TransferProbeVerdict EvaluateWireE5TransferProbe(const WireE5TransferProbeMeasurement& measurement) {
if (!measurement.ran || measurement.fenceWaitTimedOut || measurement.texels == 0)
return WireE5TransferProbeVerdict::NotRun;
if (measurement.controlMismatches != 0) return WireE5TransferProbeVerdict::Inconclusive;
return measurement.subjectMismatches != 0 ? WireE5TransferProbeVerdict::TransferReadNormalizes
: WireE5TransferProbeVerdict::Clean;
}
// One line with every reading, for the bring-up log and the POST row.
String DescribeWireE5TransferProbe(const WireE5TransferProbeMeasurement& measurement);
struct WireE5TransferProbeContext {
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
VkDevice device = VK_NULL_HANDLE;
// Must be otherwise idle for the probe's duration.
VkQueue queue = VK_NULL_HANDLE;
Uint32 queueFamilyIndex = 0;
};
// One submission on its own command pool, a bounded fence wait (on timeout every object is
// leaked and the measurement says so - the caller must then not destroy or idle-wait the
// device), teardown on every other path. Never touches renderer state.
WireE5TransferProbeMeasurement RunWireE5TransferProbe(const WireE5TransferProbeContext& context);
// The POST's entry: a throwaway device with one graphics queue, the probe, the device
// destroyed (leaked on a fence timeout).
WireE5TransferProbeMeasurement RunWireE5TransferProbeOnThrowawayDevice(
PFN_vkGetInstanceProcAddr getInstanceProcAddr, VkInstance instance, VkPhysicalDevice physicalDevice,
Uint32 graphicsQueueFamilyIndex);
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,19 @@
#version 450
// The volume twin of WireE5RawCopy.comp: the source level is a 3D image and the region's slices
// are its z range.
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
layout(set = 0, binding = 0) uniform usampler3D sourceImage;
layout(set = 0, binding = 1, r32ui) uniform writeonly uimage3D twinImage;
layout(push_constant) uniform Region {
// xyz: the region's texel origin in the source level.
ivec4 sourceOffset;
// xyz: texels to copy.
ivec4 extent;
} region;
void main() {
ivec3 id = ivec3(gl_GlobalInvocationID);
if (any(greaterThanEqual(id, region.extent.xyz))) return;
uint word = texelFetch(sourceImage, region.sourceOffset.xyz + id, 0).r;
imageStore(twinImage, id, uvec4(word, 0u, 0u, 0u));
}
@@ -0,0 +1,839 @@
// Generated from WireE5RawCopy.comp / WireE5RawCopy3D.comp (Vulkan 1.0 / SPIR-V 1.0) by
// scripts/bake_internal_shaders.py, which runs MG_Test's BakedInternalShadersTest with the
// IN-TREE glslang and splices the words it emits into the arrays below. Do not edit the words
// by hand: MOBILEGL_BAKED_INTERNAL_SHADERS (the same test, run as a gate) recompiles every
// source in its table and reds on the first word that disagrees.
#pragma once
static constexpr Uint32 kWireE5RawCopySpirv[] = {
0x07230203u,
0x00010000u,
0x0008000bu,
0x00000046u,
0x00000000u,
0x00020011u,
0x00000001u,
0x0006000bu,
0x00000001u,
0x4c534c47u,
0x6474732eu,
0x3035342eu,
0x00000000u,
0x0003000eu,
0x00000000u,
0x00000001u,
0x0006000fu,
0x00000005u,
0x00000004u,
0x6e69616du,
0x00000000u,
0x0000000du,
0x00060010u,
0x00000004u,
0x00000011u,
0x00000008u,
0x00000008u,
0x00000001u,
0x00030003u,
0x00000002u,
0x000001c2u,
0x00040005u,
0x00000004u,
0x6e69616du,
0x00000000u,
0x00030005u,
0x00000009u,
0x00006469u,
0x00080005u,
0x0000000du,
0x475f6c67u,
0x61626f6cu,
0x766e496cu,
0x7461636fu,
0x496e6f69u,
0x00000044u,
0x00040005u,
0x00000012u,
0x69676552u,
0x00006e6fu,
0x00070006u,
0x00000012u,
0x00000000u,
0x72756f73u,
0x664f6563u,
0x74657366u,
0x00000000u,
0x00050006u,
0x00000012u,
0x00000001u,
0x65747865u,
0x0000746eu,
0x00040005u,
0x00000014u,
0x69676572u,
0x00006e6fu,
0x00040005u,
0x00000022u,
0x64726f77u,
0x00000000u,
0x00050005u,
0x00000026u,
0x72756f73u,
0x6d496563u,
0x00656761u,
0x00050005u,
0x0000003eu,
0x6e697774u,
0x67616d49u,
0x00000065u,
0x00040047u,
0x0000000du,
0x0000000bu,
0x0000001cu,
0x00030047u,
0x00000012u,
0x00000002u,
0x00050048u,
0x00000012u,
0x00000000u,
0x00000023u,
0x00000000u,
0x00050048u,
0x00000012u,
0x00000001u,
0x00000023u,
0x00000010u,
0x00040047u,
0x00000026u,
0x00000021u,
0x00000000u,
0x00040047u,
0x00000026u,
0x00000022u,
0x00000000u,
0x00030047u,
0x0000003eu,
0x00000019u,
0x00040047u,
0x0000003eu,
0x00000021u,
0x00000001u,
0x00040047u,
0x0000003eu,
0x00000022u,
0x00000000u,
0x00040047u,
0x00000045u,
0x0000000bu,
0x00000019u,
0x00020013u,
0x00000002u,
0x00030021u,
0x00000003u,
0x00000002u,
0x00040015u,
0x00000006u,
0x00000020u,
0x00000001u,
0x00040017u,
0x00000007u,
0x00000006u,
0x00000003u,
0x00040020u,
0x00000008u,
0x00000007u,
0x00000007u,
0x00040015u,
0x0000000au,
0x00000020u,
0x00000000u,
0x00040017u,
0x0000000bu,
0x0000000au,
0x00000003u,
0x00040020u,
0x0000000cu,
0x00000001u,
0x0000000bu,
0x0004003bu,
0x0000000cu,
0x0000000du,
0x00000001u,
0x00040017u,
0x00000011u,
0x00000006u,
0x00000004u,
0x0004001eu,
0x00000012u,
0x00000011u,
0x00000011u,
0x00040020u,
0x00000013u,
0x00000009u,
0x00000012u,
0x0004003bu,
0x00000013u,
0x00000014u,
0x00000009u,
0x0004002bu,
0x00000006u,
0x00000015u,
0x00000001u,
0x00040020u,
0x00000016u,
0x00000009u,
0x00000011u,
0x00020014u,
0x0000001au,
0x00040017u,
0x0000001bu,
0x0000001au,
0x00000003u,
0x00040020u,
0x00000021u,
0x00000007u,
0x0000000au,
0x00090019u,
0x00000023u,
0x0000000au,
0x00000001u,
0x00000000u,
0x00000001u,
0x00000000u,
0x00000001u,
0x00000000u,
0x0003001bu,
0x00000024u,
0x00000023u,
0x00040020u,
0x00000025u,
0x00000000u,
0x00000024u,
0x0004003bu,
0x00000025u,
0x00000026u,
0x00000000u,
0x0004002bu,
0x00000006u,
0x00000028u,
0x00000000u,
0x00040017u,
0x00000029u,
0x00000006u,
0x00000002u,
0x0004002bu,
0x0000000au,
0x00000030u,
0x00000002u,
0x00040020u,
0x00000031u,
0x00000007u,
0x00000006u,
0x00040017u,
0x00000038u,
0x0000000au,
0x00000004u,
0x0004002bu,
0x0000000au,
0x0000003au,
0x00000000u,
0x00090019u,
0x0000003cu,
0x0000000au,
0x00000001u,
0x00000000u,
0x00000001u,
0x00000000u,
0x00000002u,
0x00000021u,
0x00040020u,
0x0000003du,
0x00000000u,
0x0000003cu,
0x0004003bu,
0x0000003du,
0x0000003eu,
0x00000000u,
0x0004002bu,
0x0000000au,
0x00000043u,
0x00000008u,
0x0004002bu,
0x0000000au,
0x00000044u,
0x00000001u,
0x0006002cu,
0x0000000bu,
0x00000045u,
0x00000043u,
0x00000043u,
0x00000044u,
0x00050036u,
0x00000002u,
0x00000004u,
0x00000000u,
0x00000003u,
0x000200f8u,
0x00000005u,
0x0004003bu,
0x00000008u,
0x00000009u,
0x00000007u,
0x0004003bu,
0x00000021u,
0x00000022u,
0x00000007u,
0x0004003du,
0x0000000bu,
0x0000000eu,
0x0000000du,
0x0004007cu,
0x00000007u,
0x0000000fu,
0x0000000eu,
0x0003003eu,
0x00000009u,
0x0000000fu,
0x0004003du,
0x00000007u,
0x00000010u,
0x00000009u,
0x00050041u,
0x00000016u,
0x00000017u,
0x00000014u,
0x00000015u,
0x0004003du,
0x00000011u,
0x00000018u,
0x00000017u,
0x0008004fu,
0x00000007u,
0x00000019u,
0x00000018u,
0x00000018u,
0x00000000u,
0x00000001u,
0x00000002u,
0x000500afu,
0x0000001bu,
0x0000001cu,
0x00000010u,
0x00000019u,
0x0004009au,
0x0000001au,
0x0000001du,
0x0000001cu,
0x000300f7u,
0x0000001fu,
0x00000000u,
0x000400fau,
0x0000001du,
0x0000001eu,
0x0000001fu,
0x000200f8u,
0x0000001eu,
0x000100fdu,
0x000200f8u,
0x0000001fu,
0x0004003du,
0x00000024u,
0x00000027u,
0x00000026u,
0x00050041u,
0x00000016u,
0x0000002au,
0x00000014u,
0x00000028u,
0x0004003du,
0x00000011u,
0x0000002bu,
0x0000002au,
0x0007004fu,
0x00000029u,
0x0000002cu,
0x0000002bu,
0x0000002bu,
0x00000000u,
0x00000001u,
0x0004003du,
0x00000007u,
0x0000002du,
0x00000009u,
0x0007004fu,
0x00000029u,
0x0000002eu,
0x0000002du,
0x0000002du,
0x00000000u,
0x00000001u,
0x00050080u,
0x00000029u,
0x0000002fu,
0x0000002cu,
0x0000002eu,
0x00050041u,
0x00000031u,
0x00000032u,
0x00000009u,
0x00000030u,
0x0004003du,
0x00000006u,
0x00000033u,
0x00000032u,
0x00050051u,
0x00000006u,
0x00000034u,
0x0000002fu,
0x00000000u,
0x00050051u,
0x00000006u,
0x00000035u,
0x0000002fu,
0x00000001u,
0x00060050u,
0x00000007u,
0x00000036u,
0x00000034u,
0x00000035u,
0x00000033u,
0x00040064u,
0x00000023u,
0x00000037u,
0x00000027u,
0x0007005fu,
0x00000038u,
0x00000039u,
0x00000037u,
0x00000036u,
0x00000002u,
0x00000028u,
0x00050051u,
0x0000000au,
0x0000003bu,
0x00000039u,
0x00000000u,
0x0003003eu,
0x00000022u,
0x0000003bu,
0x0004003du,
0x0000003cu,
0x0000003fu,
0x0000003eu,
0x0004003du,
0x00000007u,
0x00000040u,
0x00000009u,
0x0004003du,
0x0000000au,
0x00000041u,
0x00000022u,
0x00070050u,
0x00000038u,
0x00000042u,
0x00000041u,
0x0000003au,
0x0000003au,
0x0000003au,
0x00040063u,
0x0000003fu,
0x00000040u,
0x00000042u,
0x000100fdu,
0x00010038u,
};
static constexpr Uint32 kWireE5RawCopy3DSpirv[] = {
0x07230203u,
0x00010000u,
0x0008000bu,
0x0000003du,
0x00000000u,
0x00020011u,
0x00000001u,
0x0006000bu,
0x00000001u,
0x4c534c47u,
0x6474732eu,
0x3035342eu,
0x00000000u,
0x0003000eu,
0x00000000u,
0x00000001u,
0x0006000fu,
0x00000005u,
0x00000004u,
0x6e69616du,
0x00000000u,
0x0000000du,
0x00060010u,
0x00000004u,
0x00000011u,
0x00000008u,
0x00000008u,
0x00000001u,
0x00030003u,
0x00000002u,
0x000001c2u,
0x00040005u,
0x00000004u,
0x6e69616du,
0x00000000u,
0x00030005u,
0x00000009u,
0x00006469u,
0x00080005u,
0x0000000du,
0x475f6c67u,
0x61626f6cu,
0x766e496cu,
0x7461636fu,
0x496e6f69u,
0x00000044u,
0x00040005u,
0x00000012u,
0x69676552u,
0x00006e6fu,
0x00070006u,
0x00000012u,
0x00000000u,
0x72756f73u,
0x664f6563u,
0x74657366u,
0x00000000u,
0x00050006u,
0x00000012u,
0x00000001u,
0x65747865u,
0x0000746eu,
0x00040005u,
0x00000014u,
0x69676572u,
0x00006e6fu,
0x00040005u,
0x00000022u,
0x64726f77u,
0x00000000u,
0x00050005u,
0x00000026u,
0x72756f73u,
0x6d496563u,
0x00656761u,
0x00050005u,
0x00000035u,
0x6e697774u,
0x67616d49u,
0x00000065u,
0x00040047u,
0x0000000du,
0x0000000bu,
0x0000001cu,
0x00030047u,
0x00000012u,
0x00000002u,
0x00050048u,
0x00000012u,
0x00000000u,
0x00000023u,
0x00000000u,
0x00050048u,
0x00000012u,
0x00000001u,
0x00000023u,
0x00000010u,
0x00040047u,
0x00000026u,
0x00000021u,
0x00000000u,
0x00040047u,
0x00000026u,
0x00000022u,
0x00000000u,
0x00030047u,
0x00000035u,
0x00000019u,
0x00040047u,
0x00000035u,
0x00000021u,
0x00000001u,
0x00040047u,
0x00000035u,
0x00000022u,
0x00000000u,
0x00040047u,
0x0000003cu,
0x0000000bu,
0x00000019u,
0x00020013u,
0x00000002u,
0x00030021u,
0x00000003u,
0x00000002u,
0x00040015u,
0x00000006u,
0x00000020u,
0x00000001u,
0x00040017u,
0x00000007u,
0x00000006u,
0x00000003u,
0x00040020u,
0x00000008u,
0x00000007u,
0x00000007u,
0x00040015u,
0x0000000au,
0x00000020u,
0x00000000u,
0x00040017u,
0x0000000bu,
0x0000000au,
0x00000003u,
0x00040020u,
0x0000000cu,
0x00000001u,
0x0000000bu,
0x0004003bu,
0x0000000cu,
0x0000000du,
0x00000001u,
0x00040017u,
0x00000011u,
0x00000006u,
0x00000004u,
0x0004001eu,
0x00000012u,
0x00000011u,
0x00000011u,
0x00040020u,
0x00000013u,
0x00000009u,
0x00000012u,
0x0004003bu,
0x00000013u,
0x00000014u,
0x00000009u,
0x0004002bu,
0x00000006u,
0x00000015u,
0x00000001u,
0x00040020u,
0x00000016u,
0x00000009u,
0x00000011u,
0x00020014u,
0x0000001au,
0x00040017u,
0x0000001bu,
0x0000001au,
0x00000003u,
0x00040020u,
0x00000021u,
0x00000007u,
0x0000000au,
0x00090019u,
0x00000023u,
0x0000000au,
0x00000002u,
0x00000000u,
0x00000000u,
0x00000000u,
0x00000001u,
0x00000000u,
0x0003001bu,
0x00000024u,
0x00000023u,
0x00040020u,
0x00000025u,
0x00000000u,
0x00000024u,
0x0004003bu,
0x00000025u,
0x00000026u,
0x00000000u,
0x0004002bu,
0x00000006u,
0x00000028u,
0x00000000u,
0x00040017u,
0x0000002fu,
0x0000000au,
0x00000004u,
0x0004002bu,
0x0000000au,
0x00000031u,
0x00000000u,
0x00090019u,
0x00000033u,
0x0000000au,
0x00000002u,
0x00000000u,
0x00000000u,
0x00000000u,
0x00000002u,
0x00000021u,
0x00040020u,
0x00000034u,
0x00000000u,
0x00000033u,
0x0004003bu,
0x00000034u,
0x00000035u,
0x00000000u,
0x0004002bu,
0x0000000au,
0x0000003au,
0x00000008u,
0x0004002bu,
0x0000000au,
0x0000003bu,
0x00000001u,
0x0006002cu,
0x0000000bu,
0x0000003cu,
0x0000003au,
0x0000003au,
0x0000003bu,
0x00050036u,
0x00000002u,
0x00000004u,
0x00000000u,
0x00000003u,
0x000200f8u,
0x00000005u,
0x0004003bu,
0x00000008u,
0x00000009u,
0x00000007u,
0x0004003bu,
0x00000021u,
0x00000022u,
0x00000007u,
0x0004003du,
0x0000000bu,
0x0000000eu,
0x0000000du,
0x0004007cu,
0x00000007u,
0x0000000fu,
0x0000000eu,
0x0003003eu,
0x00000009u,
0x0000000fu,
0x0004003du,
0x00000007u,
0x00000010u,
0x00000009u,
0x00050041u,
0x00000016u,
0x00000017u,
0x00000014u,
0x00000015u,
0x0004003du,
0x00000011u,
0x00000018u,
0x00000017u,
0x0008004fu,
0x00000007u,
0x00000019u,
0x00000018u,
0x00000018u,
0x00000000u,
0x00000001u,
0x00000002u,
0x000500afu,
0x0000001bu,
0x0000001cu,
0x00000010u,
0x00000019u,
0x0004009au,
0x0000001au,
0x0000001du,
0x0000001cu,
0x000300f7u,
0x0000001fu,
0x00000000u,
0x000400fau,
0x0000001du,
0x0000001eu,
0x0000001fu,
0x000200f8u,
0x0000001eu,
0x000100fdu,
0x000200f8u,
0x0000001fu,
0x0004003du,
0x00000024u,
0x00000027u,
0x00000026u,
0x00050041u,
0x00000016u,
0x00000029u,
0x00000014u,
0x00000028u,
0x0004003du,
0x00000011u,
0x0000002au,
0x00000029u,
0x0008004fu,
0x00000007u,
0x0000002bu,
0x0000002au,
0x0000002au,
0x00000000u,
0x00000001u,
0x00000002u,
0x0004003du,
0x00000007u,
0x0000002cu,
0x00000009u,
0x00050080u,
0x00000007u,
0x0000002du,
0x0000002bu,
0x0000002cu,
0x00040064u,
0x00000023u,
0x0000002eu,
0x00000027u,
0x0007005fu,
0x0000002fu,
0x00000030u,
0x0000002eu,
0x0000002du,
0x00000002u,
0x00000028u,
0x00050051u,
0x0000000au,
0x00000032u,
0x00000030u,
0x00000000u,
0x0003003eu,
0x00000022u,
0x00000032u,
0x0004003du,
0x00000033u,
0x00000036u,
0x00000035u,
0x0004003du,
0x00000007u,
0x00000037u,
0x00000009u,
0x0004003du,
0x0000000au,
0x00000038u,
0x00000022u,
0x00070050u,
0x0000002fu,
0x00000039u,
0x00000038u,
0x00000031u,
0x00000031u,
0x00000031u,
0x00040063u,
0x00000036u,
0x00000037u,
0x00000039u,
0x000100fdu,
0x00010038u,
};
@@ -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) {
@@ -99,6 +98,20 @@
MG_Util::ConvertGLEnumToTextureUploadTarget(info.Target), info.Level,
IntVec3{static_cast<Int>(box.W), static_cast<Int>(box.H), static_cast<Int>(box.D)},
nativeFormat, raw)) return false;
// A depth-only level's shadow holds the format's own depth words, which the
// depth packer reads exactly as it reads them off a Vulkan copy.
if (nativeFormat == VK_FORMAT_D16_UNORM || nativeFormat == VK_FORMAT_D32_SFLOAT ||
nativeFormat == VK_FORMAT_X8_D24_UNORM_PACK32) {
if (info.Format != GL_DEPTH_COMPONENT) return false;
const SizeT texels = static_cast<SizeT>(Uint64(box.W) * box.H * box.D);
const SizeT wordBytes = nativeFormat == VK_FORMAT_D16_UNORM ? 2 : 4;
if (raw.size() < texels * wordBytes) return false;
Vector<Uint8> packed;
if (!PackDepthStencilTight(raw.data(), nullptr, nativeFormat, texels, info.Format, info.Type, packed) ||
packed.size() != info.DstSize) return false;
ownedBytes = Move(packed);
return true;
}
return PackWireColorTight(raw.data(), raw.size(), nativeFormat,
static_cast<TextureInternalFormat>(record.Desc.InternalFormat), info, ownedBytes);
};
@@ -148,7 +161,7 @@
image.image = resource->image; image.format = resource->format; image.aspect = resource->aspect;
image.layout = resource->layout; image.trackedLayout = &resource->layout; image.levels = resource->mipLevels;
const auto restore = image.layout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_IMAGE_LAYOUT_GENERAL : image.layout;
TransitionWireImage(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
auto& frame = m_frameContext.GetCurrent();
VkBufferImageCopy regions[2]{};
Uint32 regionCount = 0;
const auto addCopy = [&](VkImageAspectFlags aspect, VkDeviceSize offset) {
@@ -161,8 +174,35 @@
if (!depthStencil) addCopy(VK_IMAGE_ASPECT_COLOR_BIT, 0);
if (depth) addCopy(VK_IMAGE_ASPECT_DEPTH_BIT, 0);
if (stencil) addCopy(VK_IMAGE_ASPECT_STENCIL_BIT, stencilOffset);
auto& frame = m_frameContext.GetCurrent();
vkCmdCopyImageToBuffer(frame.commandBuffer, image.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
// E5B9G9R9 on a device whose transfer reads canonicalize it: the words leave through a
// shader into a twin (WireE5RawCopy.h), which is what the buffer copy then reads.
VkImage copySource = image.image;
if (!depthStencil && m_textureManager->WireE5RawReadApplies(*resource)) {
TransitionWireImage(image, VK_IMAGE_LAYOUT_GENERAL);
WireE5RawCopyRegion region;
region.source = image.image;
region.volume = nativeVolume;
region.level = level;
region.baseLayer = nativeVolume ? 0u : layer;
region.layerCount = nativeVolume ? 1u : layerCount;
region.offset = regions[0].imageOffset;
region.extent = extent;
auto twin = MakeUnique<VkTextureManager::TextureResource>();
WireE5RawCopyObjects objects;
if (m_textureManager->RecordWireE5RawTwin(frame.commandBuffer, region, VK_IMAGE_LAYOUT_GENERAL, *twin,
objects)) {
copySource = twin->image;
regions[0].imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, region.layerCount};
regions[0].imageOffset = {0, 0, 0};
WireRetiredObjects retired{};
retired.textures.push_back(Move(twin));
retired.imageViews = {objects.sourceView, objects.twinView};
retired.descriptorPools = {objects.descriptorPool};
RetireWireObjects(Move(retired));
}
}
if (copySource == image.image) TransitionWireImage(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
vkCmdCopyImageToBuffer(frame.commandBuffer, copySource, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
readback.GetHandle(), regionCount, regions);
TransitionWireImage(image, restore);
VkMemoryBarrier host{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
@@ -211,4 +251,3 @@
ownedBytes = Move(result);
return true;
}
#endif
@@ -10,6 +10,7 @@
#include <Config.h>
#include <Includes.h>
#include <MG_Backend/SubgroupImplementation.h>
namespace MobileGL::MG_Backend::DirectVulkan {
// The single decision point for how DirectVulkan implements GL_KHR_shader_subgroup,
@@ -22,42 +23,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// - FixIterationRPSubgroupScratchPass patches the one known pack bug: iterationRP's
// prefixSumCache[32], under-declared for sub-16-lane devices (8-lane lavapipe);
// - FixIterationRPBarrierPass repairs Program 203's race between two reductions
// reusing that scratch, when explicitly enabled;
// reusing that scratch (both iterationRP repairs: MOBILEGL_ITRP_FIX);
// - DeriveNumSubgroupsPass replaces the one builtin drivers get wrong
// (gl_NumSubgroups) with the value the rest of the topology implies.
// The 32-lane shared-memory emulation (EmulateSubgroupsPass) is a LAST RESORT for
// devices with no subgroup support at all, and only when the user opts in with
// MOBILEGL_MAGMA_EMULATE_SUBGROUP=1; it never replaces available native operations.
// The 32-lane shared-memory emulation (EmulateSubgroupsPass) is what a device with no
// subgroup support at all gets (MG_Backend::ResolveSubgroupImplementation; forced with
// MOBILEGL_SUBGROUP_EMULATION); it never replaces available native operations by default.
inline constexpr Uint32 kEmulatedSubgroupSize = 32u;
inline constexpr Uint32 kEmulatedSubgroupStages = GL_COMPUTE_SHADER_BIT;
inline constexpr Uint32 kEmulatedSubgroupFeatures =
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_VOTE_BIT_KHR |
GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR | GL_SUBGROUP_FEATURE_BALLOT_BIT_KHR |
GL_SUBGROUP_FEATURE_SHUFFLE_BIT_KHR | GL_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT_KHR |
GL_SUBGROUP_FEATURE_CLUSTERED_BIT_KHR | GL_SUBGROUP_FEATURE_QUAD_BIT_KHR;
using MG_Backend::kEmulatedSubgroupFeatures;
using MG_Backend::kEmulatedSubgroupSize;
using MG_Backend::kEmulatedSubgroupStages;
inline MG_Backend::SubgroupImplementation ResolveVulkanSubgroupImplementation(const Bool nativeSubgroupSupported) {
return MG_Backend::ResolveSubgroupImplementation(nativeSubgroupSupported);
}
inline Bool ShouldEmulateSubgroups(const Bool nativeSubgroupSupported) {
return MG_Config::Features.MagmaEmulateSubgroup && !nativeSubgroupSupported &&
!MG_Config::Features.MagmaDisableSubgroup;
return ResolveVulkanSubgroupImplementation(nativeSubgroupSupported) ==
MG_Backend::SubgroupImplementation::Emulated;
}
inline Bool ShouldFixIterationRPSubgroupScratch() {
// Auto is ON: the patch is fingerprint-gated to iterationRP's reduction and
// grows one under-declared array; every other module passes through untouched.
return MG_Config::Features.MagmaFixIterationRPSubgroupScratch !=
MG_Config::QuirkOverride::ForceOff;
// Native subgroup operations in use - the device has them and they are not overridden.
// gl_NumSubgroups is derived on this path unconditionally (DeriveNumSubgroupsPass): GL
// requires gl_SubgroupID < gl_NumSubgroups, Adreno's builtin reports 1 while the same
// dispatch emits IDs 0..7, and the derived ceil() value is the one Vulkan guarantees
// whenever the pipeline can request REQUIRE_FULL_SUBGROUPS.
inline Bool UsesNativeSubgroups(const Bool nativeSubgroupSupported) {
return ResolveVulkanSubgroupImplementation(nativeSubgroupSupported) ==
MG_Backend::SubgroupImplementation::Native;
}
inline Bool ShouldFixIterationRPBarrier() {
return MG_Config::Features.MagmaIterationRPFixBarrier;
}
inline Bool ShouldDeriveNumSubgroups() {
// Auto is ON: gl_NumSubgroups must agree with the gl_SubgroupID range for the GL
// contract to hold, and the derived ceil() value is the one the renderer can pin
// with REQUIRE_FULL_SUBGROUPS - the driver builtin is the value with no
// cross-driver guarantee (Adreno returns 1 for an 8-subgroup dispatch).
return MG_Config::Features.MagmaDeriveNumSubgroups != MG_Config::QuirkOverride::ForceOff;
}
using MG_Backend::ShouldApplyIterationRPFixes;
} // namespace MobileGL::MG_Backend::DirectVulkan
+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
@@ -0,0 +1,63 @@
// MobileGL - MobileGL/MG_Backend/SubgroupImplementation.h
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Config.h>
#include <Includes.h>
namespace MobileGL::MG_Backend {
// How a backend implements GL_KHR_shader_subgroup - decided once, from what its driver
// offers, and shared by capability advertisement and module lowering so the two can never
// disagree.
// Native - the driver's own subgroup operations, at whatever width it runs (8, 16, 32,
// 64, 128 lanes); MobileGL advertises the driver's size, stages and features.
// Emulated - a 32-lane VIRTUAL subgroup in workgroup-shared memory
// (ShaderTranspiler::EmulateSubgroupsPass): compute stage only, every feature
// bit. What a driver with no subgroup operations at all gets.
// None - GL_KHR_shader_subgroup is not advertised.
// MOBILEGL_SUBGROUP_EMULATION: Auto emulates exactly where nothing native exists; ForceOn
// emulates even over native support (to exercise the emulation on any device); ForceOff
// never emulates.
enum class SubgroupImplementation : Uint8 { None, Native, Emulated };
inline constexpr Uint32 kEmulatedSubgroupSize = 32u;
inline constexpr Uint32 kEmulatedSubgroupStages = GL_COMPUTE_SHADER_BIT;
inline constexpr Uint32 kEmulatedSubgroupFeatures =
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_VOTE_BIT_KHR |
GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR | GL_SUBGROUP_FEATURE_BALLOT_BIT_KHR |
GL_SUBGROUP_FEATURE_SHUFFLE_BIT_KHR | GL_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT_KHR |
GL_SUBGROUP_FEATURE_CLUSTERED_BIT_KHR | GL_SUBGROUP_FEATURE_QUAD_BIT_KHR;
inline SubgroupImplementation ResolveSubgroupImplementation(const Bool nativeSupported) {
switch (MG_Config::Features.SubgroupEmulation) {
case MG_Config::QuirkOverride::ForceOn:
return SubgroupImplementation::Emulated;
case MG_Config::QuirkOverride::ForceOff:
return nativeSupported ? SubgroupImplementation::Native : SubgroupImplementation::None;
case MG_Config::QuirkOverride::Auto:
default:
return nativeSupported ? SubgroupImplementation::Native : SubgroupImplementation::Emulated;
}
}
// MOBILEGL_ITRP_FIX: whether iterationRP's two compute-shader repairs run (Config.h). Both
// passes are fingerprint-gated, so Auto applies them; only ForceOff replays the pack's bugs.
inline Bool ShouldApplyIterationRPFixes() {
return MG_Config::Features.IterationRPFix != MG_Config::QuirkOverride::ForceOff;
}
inline const char* SubgroupImplementationName(const SubgroupImplementation implementation) {
switch (implementation) {
case SubgroupImplementation::Native: return "native";
case SubgroupImplementation::Emulated: return "emulated";
case SubgroupImplementation::None: break;
}
return "none";
}
} // namespace MobileGL::MG_Backend
+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
@@ -348,8 +348,16 @@ namespace {
void* EGLAPIENTRY VendorGetDispatchAddress(const char* procName) { return LookupEntryPoint(procName); }
} // namespace
extern "C" MOBILEGL_EGL_API EGLBoolean __egl_Main(uint32_t version, const __EGLapiExports* exports,
__EGLvendorInfo* vendor, __EGLapiImports* imports) {
// libeglabi.h has already declared this prototype without an export attribute, and MSVC refuses a
// redeclaration that adds dllexport. Windows has no glvnd to look the symbol up, so it is a plain
// external definition there.
#ifdef _WIN32
#define MOBILEGL_GLVND_EGL_MAIN extern "C"
#else
#define MOBILEGL_GLVND_EGL_MAIN MOBILEGL_EGL_API
#endif
MOBILEGL_GLVND_EGL_MAIN EGLBoolean __egl_Main(uint32_t version, const __EGLapiExports* exports,
__EGLvendorInfo* vendor, __EGLapiImports* imports) {
(void)vendor;
if (EGL_VENDOR_ABI_GET_MAJOR_VERSION(version) != EGL_VENDOR_ABI_MAJOR_VERSION) {
// A vendor that does not know the ABI it is being handed must say so, not guess: the
@@ -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;

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