Compare commits

...
Author SHA1 Message Date
swung0x48 ad7340921e [Fix] (MG_Impl/GLImpl, MG_State/GLState, MG_Test/Texture, scripts/data): keep GENERATE_MIPMAP on the texture, not keyed by GL name
- A process-wide map keyed by GL name gave a deleted texture's flag to the
  next texture created under the recycled name.
- The flag is now texture object state, so it dies with its texture.
2026-10-16 16:52:59 -04:00
swung0x48 095cf2643b [CI] (scripts/data): expect the new F1 scenarios to skip on the monolith lanes 2026-10-16 16:04:48 -04:00
swung0x48 230a90a439 [Fix] (MG_Impl/GLImpl, MG_IntegrationTest, MG_IntegrationTest/Scenarios, MG_Remote/Client, MG_Test/Texture, scripts/data): make by-name texture calls act on the named texture
- The 19 calls that bound their texture first now share object cores with the target forms
- GL 4.6 rules the binding hid: cube maps in TextureSubImage3D and level queries, named target errors
2026-10-16 14:47:50 -04:00
swung0x48 28bf0c1653 [Refactor] (MG_Backend, MG_Impl/Pipe, MG_IntegrationTest/Harness, MG_Pipe, MG_Pipe/generated, MG_Remote/Client, MG_Test/Pipe, MG_Test/Wire, scripts/data): add by-name mipmap and copy slots that name their texture
- GenerateTextureMipmap and CopyTextureSubImage2D emit the records their target slots emit
- RemoteEmitTable.TheThreeClassesPartitionAllSeventyOneSlots is renamed ...PartitionEverySlot
2026-10-16 14:47:40 -04:00
swung0x48 7dbe0e5c78 [Docs] (.git-blame-ignore-revs, CONTRIBUTING.md): list the reformat for git blame and update the format rules 2026-10-16 06:30:59 -04:00
swung0x48 f4994ee36e [Chore] (MG_Backend, MG_Backend/DirectGLES, MG_Backend/DirectVulkan, MG_Backend/MGPipe, MG_Backend/Record, MG_Benchmark/Buffer, MG_Benchmark/Container, MG_Benchmark/Driver, MG_Benchmark/Program, MG_Benchmark/ShaderCache, MG_Benchmark/Transpile, MG_Gbm, MG_Impl, MG_Impl/CGLImpl, MG_Impl/DyldInterpose, MG_Impl/EGLImpl, MG_Impl/GLImpl, MG_Impl/GLXImpl, MG_Impl/NSOpenGLImpl, MG_Impl/Pipe, MG_Impl/WGLImpl, MG_IntegrationTest, MG_IntegrationTest/Harness, MG_IntegrationTest/Scenarios, MG_Pipe, MG_Remote, MG_Remote/Client, MG_Remote/Protocol, MG_Remote/Server, MG_Remote/Transport, MG_Remote/Wire, MG_State/EGLState, MG_State/GLState, MG_Test, MG_Test/Backend, MG_Test/BackendLoader, MG_Test/Buffer, MG_Test/EGLState, MG_Test/Framebuffer, MG_Test/Gbm, MG_Test/Pipe, MG_Test/Pipeline, MG_Test/Program, MG_Test/Query, MG_Test/SelfTest, MG_Test/ShaderTranspiler, MG_Test/State, MG_Test/Swapchain, MG_Test/Texture, MG_Test/Util, MG_Test/VertexArray, MG_Test/Wire, MG_Util/Async, MG_Util/BackendLoaders, MG_Util/Converters, MG_Util/Damage, MG_Util/Debug, MG_Util/Math, MG_Util/Metrics, MG_Util/SelfTest, MG_Util/ShaderTranspiler, MG_Util/Texture, MG_Util/X11, MobileGL, scripts/data, tools/cts, tools/device_bench, tools/spikes, tools/trace_replay, tools/wgl-smoke): clang-format every C and C++ source
- Layout only: token streams match up to split literals, continuations and using order
- The size totals fall, so the structure baseline is lowered
2026-10-16 06:30:38 -04:00
swung0x48 76b10cc35b [CI] (scripts): read the dirty-bit name table in any line layout 2026-10-16 06:26:09 -04:00
swung0x48 58c727f74e [Tools] (scripts): format exactly the files the format check covers
- Skip MG_Impl/GLImpl/Exporting/Definitions.cpp, whose table clang-format cannot wrap
2026-10-16 06:26:07 -04:00
swung0x48 351bb35a96 [CI] (scripts/data): lower the baselines after the BackendCaps.h move 2026-10-16 05:47:58 -04:00
swung0x48 f7e065aedb [Docs] (MG_Backend/Record, MG_Impl/GLImpl, MG_Impl/Pipe, MG_Remote, MG_Remote/Client): cite BackendObject.h symbols, not lines 2026-10-16 05:47:43 -04:00
swung0x48 778014c013 [Refactor] (MG_Backend, MG_Backend/DirectVulkan, MG_Pipe, MG_Remote/Client, MG_State/GLState, MG_Test/Pipe, MG_Util/SelfTest, MG_Util/ShaderTranspiler, MobileGL, scripts): move the backend capability types into BackendCaps.h
- Config.h includes only BackendCaps.h, no longer both backends; pActiveBackendObject is declared with the interface
2026-10-16 05:47:42 -04:00
swung0x48 cf1b7f6ed8 [CI] (scripts/ci, scripts/data): lower the baselines after the DirectGLES split 2026-10-16 05:17:25 -04:00
swung0x48 91294a19f8 [Docs] (MG_Backend/DirectGLES, MG_Backend/DirectVulkan, MG_Backend/Record, MG_Impl/Pipe, MG_IntegrationTest/Scenarios, MG_Pipe, MG_Remote/Client, MG_Remote/Server, MG_State/GLState, MG_Test, MG_Test/Backend, MG_Test/Buffer, MG_Test/Pipe, MG_Test/Wire, scripts, scripts/ci): cite symbols, not lines, in the split backend files 2026-10-16 05:16:51 -04:00
swung0x48 d1f220c1a2 [Refactor] (CMakeLists.txt, MG_Backend/DirectGLES): split Managers.cpp by object kind
- helpers and state several files share lose `static`; ManagersInternal.h declares them
2026-10-16 05:10:03 -04:00
swung0x48 b0f73e3795 [Refactor] (CMakeLists.txt, MG_Backend/DirectGLES): split DirectGLES.cpp by area
- helpers and state several files share lose `static`; DirectGLESInternal.h declares them
2026-10-16 05:09:31 -04:00
swung0x48 1ec54d876f [CI] (scripts/ci): read every DirectGLES source in the memo purity gate 2026-10-16 04:47:34 -04:00
swung0x48 5973bf353b [CI] (scripts/data): lower the hygiene and structure baselines after the renderer split 2026-10-16 04:30:51 -04:00
swung0x48 16994dc994 [Docs] (MG_Backend/Record, MG_IntegrationTest/Scenarios): cite symbols the split left past their files' ends 2026-10-16 04:30:26 -04:00
swung0x48 7f7c596213 [Docs] (docs/Disaggregated, docs/MobileVA, docs/Performance, MG_Impl/Pipe, MG_IntegrationTest/Scenarios, MG_State/GLState, MG_Test/Buffer, MG_Test/Wire, scripts/ci): name the renderer's new files 2026-10-16 04:28:53 -04:00
swung0x48 98c8e8833c [Refactor] (CMakeLists.txt, MG_Backend/DirectVulkan): split VulkanRenderer.cpp and its fragments by area
- code moves unchanged; helpers several files use are in VulkanRendererInternal.h
2026-10-16 04:28:47 -04:00
swung0x48 b0e229f2a3 [Refactor] (MG_Backend/DirectVulkan): make the renderer's two file-scope singletons inline 2026-10-16 04:20:26 -04:00
swung0x48 01f80b0a0a [Refactor] (CMakeLists.txt, MG_Backend, MG_Backend/DirectGLES, MG_Backend/DirectVulkan, MG_Backend/MGPipe, MG_Test/Framebuffer, MG_Test/Texture): move the client readback packing out of DirectGLES 2026-10-16 04:20:19 -04:00
swung0x48 deae0fc4c1 [Refactor] (MG_Backend/DirectGLES): drop the readback stores' pack-buffer branch, which never runs 2026-10-16 04:05:43 -04:00
swung0x48 f90537c3cb [CI] (.github/workflows, CONTRIBUTING.md, scripts, scripts/data): add the layering and size ratchets 2026-10-16 04:01:29 -04:00
swung0x48 26a30ecd02 [CI] (CONTRIBUTING.md, scripts): skip moved lines in the changed-lines format check 2026-10-16 04:01:27 -04:00
swung0x48 37cb446bda [CI] (scripts/data): lower the text hygiene baseline after the comment cleanup 2026-10-15 23:16:52 -04:00
swung0x48 7bb5a9ccbe [Docs] (.github/workflows, docs/Disaggregated, MG_IntegrationTest): shorten the long comments in the integration lanes and test workflow 2026-10-15 23:16:45 -04:00
swung0x48 1161d489e2 [Build] (MG_IntegrationTest): register each integration lane with one mgl_itest_lane call
- The generated test registrations are identical in the monolith, verify and split builds.
2026-10-15 22:41:35 -04:00
swung0x48 92f1b9b231 [CI] (scripts/data): lower the text hygiene baseline after the comment cleanup 2026-10-15 22:29:04 -04:00
swung0x48 265966a15d [Docs] (MG_Backend/DirectGLES, MG_Backend/MGPipe, MG_Backend/Record, MG_Impl/Pipe, MG_IntegrationTest, MG_IntegrationTest/Harness, MG_IntegrationTest/Scenarios, MG_Pipe, MG_Pipe/generated, MG_Remote, MG_Remote/Client, MG_Remote/Server, MG_Remote/Transport, MG_Remote/Wire, MG_State/GLState, MG_Test/Pipe, MG_Test/Wire, MG_Util/Metrics, MobileGL, scripts, scripts/ci): cite symbols, not line numbers, in the shortened files 2026-10-15 22:27:15 -04:00
swung0x48 23ee8bdb49 [Docs] (docs/Disaggregated, MG_Remote, MG_Remote/Client, MG_Remote/Protocol, MG_Remote/Server, MG_Remote/Transport, MG_Remote/Wire): shorten long comments and move their design arguments into docs 2026-10-15 21:53:39 -04:00
swung0x48 37ecba200b [Docs] (docs/Disaggregated, MG_Impl/Pipe, MG_Remote/Client): shorten long comments and move their design arguments into docs 2026-10-15 21:18:49 -04:00
swung0x48 7ec1448f03 [Docs] (docs/Disaggregated, MG_Backend/MGPipe, MG_IntegrationTest/Harness, MG_Pipe): shorten long comments and move their design arguments into docs 2026-10-15 20:48:58 -04:00
swung0x48 211ca08b0c [Docs] (docs/Disaggregated): point the indexes' prose at docs/archive 2026-10-15 20:06:45 -04:00
swung0x48 aabca3c171 [Docs] (.claude/skills, .github/workflows, CMakeLists.txt, docs/archive, docs/Disaggregated, docs/MobileVA, docs/Performance, MG_Backend, MG_Backend/DirectGLES, MG_Backend/DirectVulkan, MG_Backend/Record, MG_Gbm, MG_Impl, MG_Impl/EGLImpl, MG_Impl/GLImpl, MG_Impl/GLXImpl, MG_Impl/Pipe, MG_IntegrationTest, MG_IntegrationTest/Harness, MG_IntegrationTest/Scenarios, MG_Pipe, MG_Pipe/generated, MG_Remote, MG_Remote/Client, MG_Remote/Server, MG_Remote/Transport, MG_Remote/Wire, MG_State/GLState, MG_Test/Pipe, MG_Test/Util, MG_Test/Wire, MobileGL, scripts, scripts/data, tools/device_bench, userguide): archive the planning notes and agent contracts under docs/archive
- DEBTS.md and OPEN-QUESTIONS.md stay maintained, now in docs/Disaggregated.
2026-10-15 20:01:45 -04:00
swung0x48 0e3e2b7761 [CI, Docs] (CONTRIBUTING.md, scripts): freeze docs/archive and treat moved files as moves 2026-10-15 19:55:42 -04:00
swung0x48 174a428e81 [Test] (MG_Test/Util): give each log level test process its own log file 2026-10-15 12:22:36 -04:00
swung0x48 d3c10f15c3 [Fix, Test] (MG_Test, MG_Test/Backend, MG_Test/BackendLoader, MG_Test/Buffer, MG_Test/EGLState, MG_Test/Framebuffer, MG_Test/Gbm, MG_Test/Pipe, MG_Test/Pipeline, MG_Test/Program, MG_Test/Query, MG_Test/SelfTest, MG_Test/ShaderTranspiler, MG_Test/State, MG_Test/Swapchain, MG_Test/Texture, MG_Test/Util, MG_Test/VertexArray, MG_Test/Wire, MG_Util/Async): drain shader compile workers before a test's main returns
- On MSVC the atexit drain deadlocked with a worker registering a static's
  destructor, hanging the shader tests on Windows.
2026-10-15 12:12:24 -04:00
swung0x48 66ef4076c8 [Fix] (scripts, scripts/data): count comment lines the same on CRLF checkouts 2026-10-15 10:24:19 -04:00
swung0x48 67c4864ea1 [CI] (.github/workflows, scripts, scripts/data): check commit messages, comment text and formatting 2026-10-15 10:17:32 -04:00
swung0x48 b1f36a71b5 [Docs] (MG_Backend, MG_Backend/DirectGLES, MG_Impl/Pipe, MG_IntegrationTest/Scenarios, MG_State/GLState, MG_Test/Buffer, MG_Test/Util, MG_Util/Metrics): correct comments about retired switches and moved code 2026-10-15 10:15:41 -04:00
swung0x48 725a73e2b5 [Docs] (.claude/skills): point the anland skill's commit rule at CONTRIBUTING.md 2026-10-15 10:15:41 -04:00
swung0x48 73d4511bc1 [Chore] (MG_Backend/DirectVulkan, MG_Benchmark, MG_Benchmark/Buffer, MG_Benchmark/Container, MG_Benchmark/Driver, MG_Benchmark/Program, MG_Benchmark/ShaderCache, MG_Benchmark/Transpile, MG_Gbm, MG_IntegrationTest, MG_IntegrationTest/Harness, MG_Test, MG_Test/Backend, MG_Test/BackendLoader, MG_Test/Buffer, MG_Test/EGLState, MG_Test/Framebuffer, MG_Test/Gbm, MG_Test/Pipe, MG_Test/Pipeline, MG_Test/Program, MG_Test/Purity, MG_Test/Query, MG_Test/SelfTest, MG_Test/ShaderTranspiler, MG_Test/State, MG_Test/Swapchain, MG_Test/Texture, MG_Test/Util, MG_Test/VertexArray, MG_Test/Wire, scripts, scripts/ci, tools/cts, tools/device_bench, tools/spikes, tools/trace_replay): extend license headers to shell, CMake, Java and GLSL 2026-10-15 10:15:40 -04:00
swung0x48 b808b68fd2 [Chore] (MG_Backend/DirectVulkan): remove an empty source file nothing builds 2026-10-15 10:15:39 -04:00
swung0x48 eb0830dfeb [Fix] (MG_IntegrationTest/Scenarios): re-encode a GBK section sign as UTF-8 2026-10-15 10:15:38 -04:00
swung0x48 f9c5e0adbc [Docs] (docs/Disaggregated, docs/Performance, MG_Pipe, MG_Remote/Client, MobileGL): correct a stale default, stub banners, a call count and a status line
- the create-window measurements move from Config.h to docs/Performance
2026-10-15 10:15:38 -04:00
swung0x48 5370d917d0 [Docs] (.github/workflows, MG_Backend/DirectGLES, MG_Backend/DirectVulkan, MG_Benchmark/Driver, MG_Impl/EGLImpl, MG_Impl/Pipe, MG_IntegrationTest, MG_IntegrationTest/Harness, MG_IntegrationTest/Scenarios, MG_Pipe, MG_State/GLState, MG_Test, MG_Test/Pipe, MG_Test/Program, MG_Test/SelfTest, MG_Test/ShaderTranspiler, MG_Test/Swapchain, MG_Test/Wire, MG_Util/Async, MG_Util/BackendLoaders, MG_Util/SelfTest, MG_Util/ShaderTranspiler, MobileGL, scripts/ci, tools/spikes, tools/trace_replay): name drivers and techniques instead of external projects 2026-10-15 10:15:37 -04:00
swung0x48 506ab1541d [Docs] (.github/workflows, CMakeLists.txt, MG_Backend, MG_Backend/DirectGLES, MG_Backend/DirectVulkan, MG_Backend/MGPipe, MG_Backend/Record, MG_Impl/Pipe, MG_IntegrationTest, MG_IntegrationTest/Harness, MG_IntegrationTest/Scenarios, MG_Pipe, MG_Remote, MG_Remote/Client, MG_Remote/Server, MG_Remote/Transport, MG_Remote/Wire, MG_State/GLState, MG_Test, MG_Test/Buffer, MG_Test/Purity, MG_Test/Util, MG_Test/Wire, MG_Util/Metrics, MobileGL, scripts, tools/trace_replay): replace stale line-number citations with sections and symbols 2026-10-15 10:15:35 -04:00
swung0x48 c4fb9397cb [Chore] (MG_Backend/DirectGLES, MG_Backend/DirectVulkan, MG_Impl/EGLImpl, MG_Impl/Pipe, MG_IntegrationTest/Harness, MG_IntegrationTest/Scenarios, MG_Pipe, MG_Remote, MG_Remote/Client, MG_Remote/Server, MG_Remote/Transport, MG_Test/Backend, MG_Test/EGLState, MG_Test/Wire, scripts, scripts/ci, tools/cts, tools/device_bench, tools/spikes, tools/trace_replay, tools/wgl-smoke): add the license header where it is missing 2026-10-15 10:15:34 -04:00
swung0x48 35c7b589f4 [Chore] (scripts): add license headers only where missing, covering .inc and Python 2026-10-15 10:15:32 -04:00
swung0x48 f0cd9b2c84 [Chore] (MG_Client, MG_Protocol, MG_Protocol/gen): remove generated wire code nothing builds or includes 2026-10-15 10:15:30 -04:00
swung0x48 bc38b0f735 [Docs] (AGENTS.md, CLAUDE.md, CONTRIBUTING.md): add rules for commits, comments and files 2026-10-15 10:15:30 -04:00
swung0x48 37d07cd38c [CI] (.github/workflows): run CI on cleanup as on feat/disaggregated 2026-10-15 08:28:43 -04:00
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 5331106e90 [CI] Use uname for Arch runner hostname 2026-10-05 08:57:17 -04:00
swung0x48 ca981a8fc1 [CI] Add Arch WSL GPU runner smoke workflow and configuration 2026-10-05 08:56:27 -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
swung0x48 c90383db16 [Fix] (MG_Remote): bound socket buffers in log flood tests 2026-10-05 07:13:39 -04:00
swung0x48 698f04574b [Fix] (MG_Remote): wait for records emitted while draining return events 2026-10-05 06:21:42 -04:00
swung0x48 f497ff9741 [Fix] (DirectVulkan): detach ended render passes before attachment layout reconciliation 2026-10-05 05:12:42 -04:00
swung0x48 946e63d810 [Fix] (MG_IntegrationTest): check pre-finish work completion in present credit tests 2026-10-05 04:22:18 -04:00
swung0x48 918d40dd4f [Fix] (Init): allow MobileGL initialization with LIBGL_ALWAYS_SOFTWARE 2026-10-05 02:55:35 -04:00
swung0x48 f5b8fe48aa [Fix] (MGPipe, MG_Remote): verify server pack state and reject records after guilty GPU resets 2026-10-05 02:51:19 -04:00
swung0x48 c5d25868cf [Fix] (ShaderTranspiler): preserve block identity when deduplicating image types 2026-10-05 01:27:50 -04:00
swung0x48 c464947bdd [Fix] (TraceReplay): share Android window surfaces and release current bindings before resize 2026-10-05 00:47:56 -04:00
swung0x48 4fb75f4e00 [Fix] (DirectVulkan): prepare image copy recording after uploads and share texture usage checks 2026-10-05 00:19:21 -04:00
swung0x48 4d6904184d [Fix] (ShaderTranspiler): deduplicate image types after Vulkan image rewrites 2026-10-05 00:08:32 -04:00
swung0x48 2f1a673513 [Fix] (BackendLoader, DirectGLES): check EGL extensions before resolving damage swap entry points 2026-10-05 00:08:31 -04:00
swung0x48 88bc35b711 [Refactor] (BackendLoader): share extension name matching between GLES and EGL probes 2026-10-04 20:54:28 -04:00
swung0x48 880be57bb6 [CI] (Watchdog): disable GPU hang detection in all CI runs 2026-10-04 10:39:39 -04:00
swung0x48 70e4691b3a [Docs] (Anland): refresh the handoff to the 2026-10-04 state, with the perf, CI, YUV and VA-API streams 2026-10-04 07:25:23 -04:00
Swung0x48 2d42414667 [CI] (Test): annotate failed ctest entries with their failure lines, since the run logs need a signed-in reader 2026-10-04 07:20:04 -04:00
Swung0x48 32d27b1491 [Fix] (DirectGLES): a session's backend registration no longer trips the strict apply-thread pin while another session is applying 2026-10-04 07:20:04 -04:00
Swung0x48 78c5f8dae7 [CI] (Census): record Refuse{Recover} as a local refusal; the client declines to redial and there is no peer to tell 2026-10-04 07:20:04 -04:00
swung0x48 96bd540ba1 [Docs] (Performance): record the device state at the glmark2 handoff 2026-10-04 06:46:51 -04:00
swung0x48 710beefd02 [Docs] (Performance): glmark2 optimization handoff with the landed fixes, current numbers, remaining work and the bench scripts 2026-10-04 06:44:49 -04:00
Swung0x48 c1e957cb98 [CI] (Gates): retire the P3a/P4a untouched-region gates; those phases closed and later phases legitimately changed the regions 2026-10-04 06:30:24 -04:00
Swung0x48 eb71b669b3 [Fix] (Build): make the pull and verify builds compile again 2026-10-04 06:30:24 -04:00
swung0x48 bb31f374a3 [Perf] (DirectVulkan): keep the wire draw's render pass open across draws to the same attachments, closing it only for work that cannot sit inside one 2026-10-04 06:18:35 -04:00
swung0x48 e5ee31b022 [Build] (Anland): build the anland server for android-30, the APK's minSdk, so thread_local uses native TLS 2026-10-04 06:02:45 -04:00
swung0x48 915882b293 [Perf] (Server): time records only when the per-frame statistics are on 2026-10-04 06:02:44 -04:00
swung0x48 963f4d8177 [Perf] (DirectVulkan): reuse completed command buffers instead of freeing and allocating one per submission 2026-10-04 06:02:43 -04:00
swung0x48 d627fca387 [Perf] (Transport): the server rings a parked client only once the sequence it waits for is applied 2026-10-04 05:40:14 -04:00
swung0x48 2de0d6dd25 [Perf] (Pipe): reuse client-array buffers instead of a blocking create per draw, and copy contiguous ranges in one go 2026-10-04 04:54:54 -04:00
swung0x48 66b47a101b [Fix] (Remote): glFinish waits for the GPU, Magma bounds unsubmitted clear objects, ES contexts advertise OES_mapbuffer/rgb8_rgba8/depth24 2026-10-04 04:54:53 -04:00
swung0x48 4722fa38bb [Docs] (Performance): glmark2 baseline of Espryt and Magma against the stock kgsl stack on anland, with profiles and ranked bottlenecks 2026-10-04 04:33:24 -04:00
swung0x48 3a289222af [Docs] (MobileVA): plan a vendor-agnostic VA-API driver on Android MediaCodec, H.264 first 2026-10-03 23:46:13 -04:00
swung0x48 18a5dba0e4 [Docs] (Anland): Chrome decodes video in hardware through the YUV shared images, with probes to check it 2026-10-03 22:46:01 -04:00
swung0x48 7218bacb49 [Feature] (SharedImage): sample NV12/P010 dma-bufs through GL_OES_EGL_image_external on both backends, copying foreign buffers into server YUV images 2026-10-03 22:46:00 -04:00
swung0x48 4633ca223c [Fix] (Server): log the per-frame P65ServerFrame line only when MOBILEGL_SERVER_FRAME_STATS asks for it 2026-10-03 14:20:11 -04:00
swung0x48 0ef13ab0a6 [Docs] (Anland): Chrome decodes video in software because MobileGL cannot import the container VA-API driver's NV12 frames. 2026-10-03 14:16:04 -04:00
swung0x48 287e1df8dc [Fix] (Server, Magma, Espryt): end the session whose GPU work hangs the GPU so it cannot stall the desktop again. 2026-10-03 12:28:39 -04:00
swung0x48 4f6f4125ac [Fix] (EGL, SharedImage): real EGL native fences publish a dma-buf producer's writes, so Chrome's GPU process frames reach the compositor. 2026-10-03 10:59:02 -04:00
swung0x48 2a1889b817 Discover gtest cases at ctest start so parallel links stop sharing one discovery JSON file. 2026-10-03 09:38:51 -04:00
swung0x48 07e7730085 [Docs] (Anland): kwin-recover.sh reads the latch from the server log file, not the too-small logcat buffer. 2026-10-03 09:05:55 -04:00
swung0x48 03648c2783 [Docs] (Anland): add a device test that loses KWin's own session and checks the desktop keeps compositing. 2026-10-03 09:05:54 -04:00
swung0x48 f22ae625cb [Fix] (Server): a lost session's window lease is waited for by the fresh session, and a window wait holds no backend turn. 2026-10-03 09:05:54 -04:00
swung0x48 f52a546707 [Fix] (EGL, Remote): a server-owned surface is re-created at the size it asked for, and a lost session's image reference is never released on the fresh one. 2026-10-03 09:05:53 -04:00
swung0x48 77c604061e [Docs] (Anland): glxgears is fixed-function, so it measures presentation only 2026-10-03 07:53:11 -04:00
swung0x48 36190b7cc0 [Fix] (EGL, Wayland): a swap that runs into a device loss reports it and still commits the window, instead of showing an empty frame. 2026-10-03 07:17:08 -04:00
swung0x48 cf763b4081 [Fix] (EGL, Remote): a thread that released a lost context keeps reporting the reset until it binds another. 2026-10-03 07:17:07 -04:00
swung0x48 2acc6c41bb [Fix] (Pipe): an evicted sampler CSO is deleted from every share group of its session. 2026-10-03 05:37:53 -04:00
swung0x48 a84104a40f [Fix] (Pipe): the default framebuffer's record names no placeholder surface that only one share group holds. 2026-10-03 05:37:53 -04:00
swung0x48 1dd888cb21 [Fix] (EGL, DirectGLES): keep a lost context's release off the fresh session, bind no surfaceless context natively, and keep the display while surfaces remain. 2026-10-03 05:37:52 -04:00
swung0x48 393002a46e [Fix] (Pipe): give each context's applier its own serial range so backend memos never match another context. 2026-10-03 05:37:51 -04:00
swung0x48 25a76c1b20 [Fix] (Pipe): drain a texture's pending texels only under a context of the share group that wrote them. 2026-10-03 05:37:51 -04:00
swung0x48 a9f22ebb63 [Fix] (EGL): create and end a context's objects under its own binding and the stream lock, and defer destroying a current context. 2026-10-03 05:37:50 -04:00
swung0x48 2fe011adf6 [Test] (Wire): enable the concurrent-context repro and add release, sibling, deferred-destroy and read-back-race shapes. 2026-10-03 05:37:49 -04:00
swung0x48 11668b02f2 [Docs] (Anland): Chrome runs its GPU in its own process by default and recovers from a device loss. 2026-10-03 05:21:53 -04:00
swung0x48 30977c181f [Feature] (EGL+GL): an opt-in OpenGL ES identity for ES contexts with BGRA textures, and contexts take the dispatcher's API, so Chrome's GPU process runs on ANGLE. 2026-10-03 05:21:52 -04:00
swung0x48 e5f097f9e1 [Docs] (Anland): add a Chrome device-loss recovery test and the GPU-process launch mode. 2026-10-03 04:37:34 -04:00
swung0x48 90e63c9e2a [Feature] (EGL+GL): advertise EGL_EXT_create_context_robustness and GL robustness, with per-context reset notification and a pid-targeted device-loss knob. 2026-10-03 04:37:33 -04:00
swung0x48 af2b9f4c1c [Docs] (Anland): root cause of the X11 stale-region issue 2026-10-03 04:36:24 -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
swung0x48 cd58f048b6 [Fix] (Shader): an inactive fragment output no longer takes location 0 from the active one 2026-10-03 04:16:41 -04: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
swung0x48 b0dad54cf1 [Docs] (Anland): device numbers and the open stale-region issue for X11 on the GPU 2026-10-03 03:20:12 -04:00
swung0x48 95aa618d22 [Docs] (Anland): x11-verify measures 1280x720 glxgears and runs Qt apps on xcb 2026-10-03 02:58:03 -04:00
swung0x48 5de6ecd4e1 [Fix] (SharedImage): uploads into a texture bound to a shared image are written into the image, and a producer's flush sends them first 2026-10-03 02:58:02 -04:00
swung0x48 7fd965ceda [Fix] (SharedImage): a session that writes images publishes every frame boundary as a write 2026-10-03 01:51:25 -04:00
swung0x48 9e7305b885 [Docs] (Anland): plan and device scripts for X11 apps on the GPU 2026-10-03 01:51:24 -04:00
swung0x48 bd4c5b5770 [Test] (Gbm): drive mobilegl_gbm through the system loader the way Xwayland's glamor does 2026-10-03 01:51:23 -04:00
swung0x48 198d6feaf3 [Feature] (GLX): present windows as shared images through DRI3 and Present, with MIT-SHM readback as the fallback 2026-10-03 01:51:22 -04:00
swung0x48 08f23543aa [Feature] (SharedImage): a glFlush publishes an image producer's accesses, so Xwayland's glamor gets implicit sync 2026-10-03 01:51:20 -04:00
swung0x48 316ffe02d0 [Fix] (X11): never open an X connection to a display this process serves 2026-10-03 01:50:58 -04:00
swung0x48 fdf9252b53 [Test] (Wire): add a disabled repro for read-backs lost when threads with their own contexts tear down. 2026-10-03 00:42:35 -04:00
swung0x48 8024b645cb [Fix] (EGL): contexts recreated naming a lost share context share one new group instead of none. 2026-10-03 00:42:34 -04:00
swung0x48 9f30fc500f [Fix] (Magma): log how many SPIR-V modules the driver screen validated at renderer shutdown. 2026-10-03 00:10:58 -04:00
swung0x48 e3120af6a9 [Fix] (EGL): name a lost context's refused bind and a refused share in the client log once. 2026-10-03 00:08:49 -04:00
swung0x48 c60fcc17c5 [Fix] (Pipe): the device-loss debug knob re-arms when set to a new value. 2026-10-02 23:59:55 -04:00
swung0x48 b06c2a9bf2 [Fix] (Remote): a surfaceless make-current's hidden pbuffer no longer latches the session offscreen. 2026-10-02 23:59:54 -04:00
swung0x48 8b5295401f [Docs] (Anland): reproduce.md brings the desktop up by opening the app. 2026-10-02 23:41:20 -04:00
swung0x48 22266c47cf Merge client-recover: open a fresh server session after a device loss. 2026-10-02 23:38:56 -04:00
swung0x48 4a4e53d9a9 Merge magma-validate: Magma SPIR-V validation declines and surfaceless EGL. 2026-10-02 23:38:19 -04:00
swung0x48 b7cc56dd44 [Docs] (Anland): record the app-driven desktop's timings, idle numbers and frame pacing 2026-10-02 23:36:47 -04:00
swung0x48 7823bcd987 [Fix] (Client): recover a fresh server session after a device loss so applications can recreate their contexts 2026-10-02 23:34:21 -04:00
swung0x48 caa00995e7 [Fix] (EGL): slow an unpaced Wayland window to one frame a second while the compositor shows none of it 2026-10-02 23:26:03 -04:00
swung0x48 1ff7ca5331 [Fix] (EGL): pace Wayland window swaps by frame callbacks so clients idle while the compositor shows nothing 2026-10-02 23:10:33 -04:00
swung0x48 6125f941d1 [Fix] (EGL): accept surfaceless make-current and config-less contexts so eglinfo prints the GL renderer lines 2026-10-02 23:09:59 -04:00
swung0x48 bfa7987ca6 [Fix] (Magma): decline programs whose final SPIR-V fails validation instead of handing the module to the driver 2026-10-02 23:09:58 -04:00
swung0x48 b1405e17ad [Docs] (Anland): the desktop starts from the app and idles while hidden, with no manual bring-up 2026-10-02 22:46:04 -04:00
swung0x48 b64c2de3c8 [Docs] (Anland): point the reproduce guide at the anland fork 2026-10-02 22:26:24 -04:00
swung0x48 2b95a9697a [Docs] (Anland): reproduce the MobileGL Plasma container setup from source 2026-10-02 22:26:11 -04:00
swung0x48 228bd24c67 [Fix] (Espryt): turn window buffer age on by default 2026-10-02 21:00:20 -04:00
swung0x48 c822a9fef7 [Fix] (Espryt): copy only the damaged rectangles into shared images again 2026-10-02 21:00:10 -04:00
swung0x48 815217cc07 [Fix] (Espryt): turn the scissor off around backend copies by what the driver has, not another session's render-state shadow 2026-10-02 20:57:18 -04:00
swung0x48 7a5b0755b3 [Docs] (Anland): install MobileGL as the container's system-wide vendor and document the fallback 2026-10-02 20:13:33 -04:00
swung0x48 3d69c37504 [Fix] (EGL): keep a process the vendor does not serve from opening its log 2026-10-02 20:07:15 -04:00
swung0x48 4f679d669d [Fix] (EGL): answer client queries and proc lookups without bringing a session up 2026-10-02 20:03:45 -04:00
swung0x48 4095624f87 [Fix] (Client): step aside for processes that ask for software rendering 2026-10-02 19:57:49 -04:00
swung0x48 321e61ca0b [Docs] (README): the target is OpenGL 4.6 Core, with the current modules and platforms 2026-10-02 19:53:29 -04:00
swung0x48 552dd3d39b [Fix] (EGL): decline another backend's GBM device and ask X for a visual only on X displays 2026-10-02 19:49:32 -04:00
swung0x48 6c742eba8a [Build] (Config): bump the version to 26.10 2026-10-02 19:43:47 -04:00
swung0x48 69e96f5b8d [Feature] (Client): read a system config file and decline EGL/GLX/GBM when no server answers 2026-10-02 19:36:38 -04:00
swung0x48 d24abe9287 [Fix] (Espryt): make window buffer age opt-in 2026-10-02 18:34:23 -04:00
swung0x48 d6d75c9951 [Fix] (Espryt): copy whole frames into shared images 2026-10-02 17:38:55 -04:00
swung0x48 bb4e3e0ec6 [Fix] (Espryt): query buffer age only after a damage region is declared 2026-10-02 17:11:31 -04:00
swung0x48 233e2f8ced [Fix] (Espryt): swap a pbuffer without damage rectangles 2026-10-02 16:27:16 -04:00
swung0x48 74919a3bc1 [Feature] (Present): buffer age and swap damage end to end on Espryt and Magma 2026-10-02 15:19:39 -04:00
swung0x48 7090e31dee [Feature] (Damage): damage-region and buffer-age bookkeeping with host tests 2026-10-02 14:51:48 -04:00
swung0x48 5accb0985c [Fix] (glslang): keep samplers of anonymous uniform structs out of the default uniform block, which made Adreno page-fault into device loss on Shadertoy 2026-10-02 14:20:36 -04:00
swung0x48 58685bc116 [Fix] (Server): wait out an apply thread that is busy in a GPU stall at Stop() instead of aborting every session 2026-10-02 13:54:26 -04:00
swung0x48 34090097a1 [Fix] (Client): make a readback on a lost device a no-op instead of a Fatal that kills the client 2026-10-02 13:43:51 -04:00
swung0x48 58c3d9bde5 [Fix] (P14): latch a lost GPU device per session on Magma and Espryt instead of aborting the whole server 2026-10-02 13:37:58 -04:00
swung0x48 d03fe4c9dd [Fix] (Client): drop the late reply of a timed-out surface op instead of reading it as the next op's 2026-10-02 13:34:17 -04:00
swung0x48 239fc228fc [Fix] (DirectVulkan): retype gl_FragCoord access chains to Private when the y-flip pass redirects them to its copy 2026-10-02 13:34:16 -04:00
swung0x48 8da082f805 [Fix] (State): start the clear colour at GL's (0, 0, 0, 0), not opaque black 2026-10-02 12:50:36 -04:00
swung0x48 258b946086 [Fix] (P12): publish the server window's extent from the display on every resize, at once, and let eglQuerySurface take it in 2026-10-02 12:03:52 -04:00
swung0x48 16b07cf4b5 [Fix] (DirectVulkan): build offscreen targets of a window-created renderer through AImageReader instead of a null headless entry point 2026-10-02 11:42:14 -04:00
swung0x48 51c37bcbc9 [Fix] (Sampler): convert sampler parameters set through the other scalar type 2026-10-02 11:22:00 -04:00
swung0x48 5a7b23f661 [Feature] (P12): suspend the on-screen session when the server window goes, resume it on the next, and republish a resized window's extent 2026-10-02 11:14:27 -04:00
swung0x48 15f9b27a42 [Fix] (P14): publish a cached sampler CSO into each share group that names it 2026-10-02 10:35:44 -04:00
swung0x48 82d96a63c9 [Docs] (Anland): runbook and skill for bringing up MobileGL with anland and KDE Plasma 2026-10-02 10:31:35 -04:00
swung0x48 7520e53b5e [Feature] (SharedImage): GPU-side fences between sessions on Espryt and Magma, no CPU wait at present 2026-10-02 10:31:34 -04:00
swung0x48 b5cab44100 [Feature] (SharedImage): sync_file write/read fences per image, so sessions order their access without CPU waits 2026-10-02 09:40:12 -04:00
swung0x48 672261764e [Fix] (SharedImage): allocate and import ARGB8888/XRGB8888 images so KWin's offscreen Quick views get buffers 2026-10-02 09:37:17 -04:00
swung0x48 0e314f645e Remove a stray build log 2026-10-02 09:18:20 -04:00
swung0x48 350fc699a1 [Fix] (SharedImage): create dma-buf wl_buffers without create_immed, drain the ring before teardown ops, and decline Magma records after the renderer is gone 2026-10-02 09:18:05 -04:00
swung0x48 3d17e8c10b [Feature] (SharedImage): zero-copy Wayland windows via linux-dmabuf, EGL dma-buf import, GBM backend, and Espryt/Magma AHB present and texture binding 2026-10-02 08:39:59 -04:00
swung0x48 38842985f8 [Feature] (SharedImage): server-allocated AHB images over a shared_image pipe verb, with dma-buf fds on the aux socket 2026-10-02 07:52:25 -04:00
swung0x48 e06aabe812 [Docs] (Anland): plan for zero-copy windows via server-allocated AHB dma-bufs 2026-10-02 07:34:45 -04:00
swung0x48 67a97065b8 [Fix] (DirectVulkan): transpose quarter-turn viewports and scissors, rebuild a resized pbuffer's target, and leave out-of-range default-framebuffer reads unfilled 2026-10-02 07:15:47 -04:00
swung0x48 ec084959fa [Docs] (Anland): record the Magma bring-up to Espryt parity 2026-10-02 06:36:20 -04:00
swung0x48 4b388950e3 [Fix] (DirectVulkan): decline draws from storeless buffers and sample with defaults when a texture's sampler CSO is gone, as DirectGLES does 2026-10-02 06:33:53 -04:00
swung0x48 7e76a98d36 [Fix] (DirectVulkan): keep one renderer per session across its surfaces, follow bind_context to each context's surface, and keep the wire resource ops registered while any session lives 2026-10-02 06:24:13 -04:00
swung0x48 15dbe78ce6 [Fix] (DirectVulkan): give each served session its own renderer and Magma state 2026-10-02 05:52:38 -04:00
swung0x48 168808d1a7 [Fix] (DirectVulkan): advertise the GLSL 1.00-era ARB strings KWin gates on, and let a debug property pick the Android backend 2026-10-02 05:42:14 -04:00
swung0x48 75e1da4d1b [Docs] (Anland): record the lock-screen corruption root cause 2026-10-02 05:07:59 -04:00
swung0x48 cd610934d7 [Fix] (DirectGLES): scope object deaths to their session, and keep container objects and staging rings per native context 2026-10-02 05:07:59 -04:00
swung0x48 335637af38 [Docs] (Anland): record the GLX, Chrome and server-config fixes 2026-10-02 03:09:18 -04:00
swung0x48 4236b42a57 [Fix] (Server+Transpiler): configure the server once per process, and enable blend_func_extended for index-1 outputs 2026-10-02 03:09:17 -04:00
swung0x48 0b6842ac8b [Fix] (EGL+GL): let ANGLE's GL backend run on MobileGL (GBM displays, foreign wl_display, precision query, ES-free GL_VERSION) 2026-10-02 02:58:55 -04:00
swung0x48 47db68d557 [Feature] (GLX): serve as a glvnd GLX vendor and present remote X windows 2026-10-02 02:11:36 -04:00
swung0x48 ce11c9accd [Docs] (Anland): record the Chrome/dma-buf feedback fix 2026-10-02 01:45:03 -04:00
swung0x48 7bb9d9829f [Docs] (Anland): record the Plasma desktop milestone and the remaining issues 2026-10-02 01:38:57 -04:00
swung0x48 ccf8eb6571 [Fix] (Client): serialize multi-threaded GL streams, follow Wayland window resizes, export the KHR image aliases 2026-10-02 01:38:21 -04:00
swung0x48 0b314ddae7 [Fix] (P14): serialize sessions on one backend turn and give each client surface its own native surface 2026-10-02 01:38:20 -04:00
swung0x48 d5acece005 [Docs] (Anland): record the Plasma bring-up state after the P14 server-state fixes 2026-10-02 00:04:01 -04:00
swung0x48 30fef23d52 [Fix] (P14): scope the server staged stores and the context-live flag to the calling session 2026-10-02 00:04:00 -04:00
swung0x48 cbb87b968a [Docs] (Anland): drop the stale-glslang note and record d554c87c and the landed tooling 2026-10-01 23:36:27 -04:00
swung0x48 d554c87c45 [Fix] (Loader+Texture): tolerate drivers without EXT swap-with-damage, and pair stencil-only storage correctly
- eglSwapBuffersWithDamageEXT is an extension entry point that Android's
  framework libEGL does not export through dlsym; requiring it was a
  bring-up FATAL on TB321FU. Resolve EXT, fall back to the KHR spelling
  (same function, interchangeable signature), then to plain
  eglSwapBuffers (a zero-rectangle damage call is defined to be exactly
  that). The GLES dlsym fallback gains a self-reference guard for
  MobileGL installed under a GLES soname, where dlsym would otherwise
  hand the backend the frontend's own entry points and recurse forever.
- NormalizePixelFormat maps GL_STENCIL_INDEX8/GL_STENCIL_INDEX to
  GL_STENCIL_INDEX/GL_UNSIGNED_BYTE instead of falling through to
  RGBA/UBYTE, which no driver can pair with stencil-only storage.
- Tests pin the EXT->KHR->none preference order and the stencil pair.
- The anland handoff is rewritten to the post-P14 landed state: device
  evidence (win-smoke green screen, Konsole on screen, eglinfo/glxinfo),
  the remote-only recovery procedure, and the known gaps (Plasma session
  bring-up, cursor scale, the no-CPU-blit requirement, resize/surface
  loss, AVC audit).
2026-10-01 23:16:33 -04:00
swung0x48 7a48521d37 [Fix] (DirectGLES): destroy stale window surfaces before creating a new one
A session that dies without its teardown (crash, SIGKILLed client) leaks
its native window surface; the server-owned display window is one
ANativeWindow for the process's lifetime, and Adreno refuses a second
eglCreateWindowSurface on it with EGL_BAD_NATIVE_WINDOW - on TB321FU this
made every session after the first die with 'MobileGL could not acquire
the app Surface 300b'. InitWindowSurface now finds and destroys any
surface still bound to the window it is about to use, and the session
registry records the window with each surface to make that possible.
2026-10-01 22:32:53 -04:00
swung0x48 6946ff3a5c [Docs] (Anland): refresh the handoff to the post-P14 landed state
The document still described the state before the state-ownership work
landed: it called the unified server design unfinished, listed the
KernelSU grant as unsolved, said the directory bind had not taken effect,
and presented MobileGLAppsService plus the per-app fork worker as pending
drafts. None of that is true now.

Rewritten against the current tree and the device:

- header: architecture landed per the user's correction, acceptance still
  open; the rejected fork-worker/AppsService path is removed and the
  unified server lives in the MobileGL library (design 11-state-ownership,
  commits d52c031c / 4ed1c9ce / 5c6399e2 / ff212061 / 5a0f13fd, plus the
  landed 6fb880cd / ebc552a0 that used to be listed as uncommitted)
- MobileGL: HEAD moved to 5a0f13fd; the state-ownership section now records
  what S1-S6 landed and the debts that remain (process-level unit shadows,
  g_backendContextGeneration, per-record ContextSlot, the glIsBuffer probe,
  the window half of pbuffer->window)
- anland: 2ffa1f5 and 33e496c pushed; the draft inventory is now a landed
  inventory plus the three uncommitted KWin-acceptance fixes
- device: KernelSU grant resolved (reinstall changed the uid to 10270), the
  clone restarted with the directory bind effective, the 5a0f13fd APK
  serving @anland-mobilegl with six completed client sessions, and the
  client library installed with a stale build stamp
- new: concurrency warning (parallel sessions have overwritten the dist and
  the source tar), the current desktop-session.service failure state, and a
  rewritten continuation order

Verified: doc citations resolve (scripts/check_doc_citations.py), device
facts checked against HA27Q3LQ at refresh time.
2026-10-01 20:41:54 -04:00
swung0x48 5a0f13fdfa [Fix] (DirectGLES): advertise the GLSL 1.00-era ARB strings KWin gates on
GL_ARB_shader_objects/vertex_shader/fragment_shader and
GL_ARB_texture_non_power_of_two are core since GL 2.0 and trivially true
for the 4.6 this backend claims, but KWin 6's EglContext::checkSupported()
requires exactly these strings before it brings up compositing. Found on
TB321FU: kwin_wayland --anland failed with 'Could not initialize rendering
context' after a successful context create + makeCurrent on the
server-owned window surface.
2026-10-01 20:21:35 -04:00
swung0x48 ff2120616d [Feature] (P14): key the backend twin tables by session and share group
The last piece of state ownership this phase owed: the eight DirectGLES twin
tables resolved a handle process-wide, so two contexts of one session that do
NOT share a share group - which have independent name spaces, so the same
{slot, gen} in both is legal - were handed the SAME twin, driver ids,
persistent map and pooled store included. Two contexts that DO share a group
were served by whichever of them twinned first. Design and per-slice shape:
docs/Disaggregated/design/11-state-ownership.md (S6).

Key: DirectGLES.h's TwinKey{SessionKey, ShareGroupKey} - the same two words
the applier's object records are filed under, with the group derived by
MGPipeApplierShareGroupKeyFor so a record and the twin describing it can never
land in different buckets. Session is in the key because a twin names a driver
id and no driver id crosses a connection.

Resolution: SessionRuntime.cpp's ResolveThreadTwinKey, installed by
InstallSessionRuntimeHooks beside the S2 PipeInputs probe, the S4 native
tuple key and the S5 applier key. It reads the ThreadSessionScope's session
and CurrentContextToken() - apply-thread state only, never MG_State, so the
frontend-keyed-lookup prohibition on the apply thread still holds. A thread
that belongs to no session answers {0, 0}, which is the process singleton this
replaces; the monolith and every unit case are unchanged by construction.

Shape: SlotTables.h's SlotTableRegistry holds one SharedPtr bucket per key,
with a thread-local last-key memo. It forwards every handle-keyed entry -
GetOrCreate, FindByHandle, LiveGenAt, HandleOf, StateForHandle,
NoteStateForHandle, ReleaseByHandle, ForEachLive, the counters - to the
CALLING THREAD's bucket, so the ~150 call sites in Managers.cpp and
DirectGLES.cpp are literally unchanged. The holder list stays process-wide: a
death notice is about an object, not a bucket, and each shared holder is one
bucket. ForEachLive now walks the calling thread's bucket (what a context can
see is its own group); the death and diagnostic walks take the new
ForEachBucket.

DropEveryTwinForEndedServerSession is session-scoped: it runs on the apply
thread of the session that is ending, while S2 lets neighbour sessions keep
rendering in the same process. No session on the thread keeps the old
process-wide drop verbatim. Managers.h's "a texture is owned by exactly one
context (share groups are not implemented)" is corrected: a texture belongs to
a share group and its sibling contexts legitimately mutate it.

Tests: MG_Test/Wire/TwinOwnershipTest.cpp, five cases driving the real twin
tables with no EGL and no wire - the same handle in two nonshared groups is
two twins and neither overwrites the other; two contexts of one group resolve
ONE twin; a per-session drop takes only that session's; a thread with no
session still gets the process-wide table; and the six-registry template
repeats the first three. Negative control run and reverted: making the
registry answer a single {0, 0} bucket turns four of the five red.

Full host ctest against a stashed baseline, compared by test name: 0 new
failures. The one name in the difference set
(LogForwardChannel.APeerThatStopsReadingCostsWarnLinesNotTheCallersTime) is a
pre-existing flake, failing on the baseline too with the identical assertion.
The 103-case acceptance set plus the five new cases is 108/108 green.

Remaining debts (documented): the unit shadows (g_activeTextureUnit,
g_boundTexturesCache, g_boundSamplersCache) are still process-global; the
driver-level glIsBuffer probe is still not usable on this host, so sharing is
proven at the twin layer (the twin IS the object holding the driver id); the
backend context/sync generations are still process-wide; the per-record
ContextSlot is still S1's edge declaration.
2026-10-01 20:11:53 -04:00
swung0x48 5c6399e240 [Docs] (Design): record S5 landed shape and remaining twin-key debt 2026-10-01 19:23:57 -04:00
swung0x48 4ed1c9cef2 [Feature] (P14): state ownership - multi-session, multi-context, share groups
Breaks the one process = one server = one session = one context equation
that the whole stack's correctness used to rest on. Design and per-slice
shape: docs/Disaggregated/design/11-state-ownership.md.

S1 wire: bind_context ring record (opcode 84) announces the current
context at the make-current edge; CreateContext/DestroyContext join the
SurfaceOp control frames; control revision 5 -> 6; the server keeps a
per-session token table and refuses unknown tokens by name.

S2 server: SessionRuntime per session with thread-scoped resolution
(singletons become fallbacks), per-thread gPipeInputs, per-session latch
domains, per-thread segment resolvers; the in-process unix supervisor
serves up to MOBILEGL_IPC_INPROC_MAX_SESSIONS (16) concurrent sessions;
surface mode is declared per session and the display lease refuses a
second on-screen session by name.

S3 frontend: ShareGroupState owns the shared object stores and name
generators (buffer/texture/renderbuffer/sampler/program), GLContext keeps
per-context bindings/errors/container objects; pGLContext becomes
thread-local with a process default context preserving the single-context
world verbatim; eglCreateContext builds a real GLContext and joins the
sharing context's group.

S4 backend: native EGL tuples keyed by (session, context token);
InitDisplayAndContext's unconditional teardown is gone - creating a
second context or surface no longer destroys the first; eglCreateContext
sharing is wired to the native driver; bind_context is the native switch
point.

S5 applier: MGPipeApplierState splits into MGPipeObjectRecords (per
share group) and MGPipeWorkingState (per context), resolved through a
{session, token} registry with a thread-local cached fast path.

Tests: MultiSessionTest (incl. two sessions x two contexts interleaved
with real clear/readback witnesses), ApplierOwnershipTest (with a
null-resolver negative control), ContextStateOwnershipTest,
ServerLoopEglTest native-context liveness; 99/99 of the acceptance set
green on the host build.

Known debts (documented in the design doc): twin registries still keyed
by handle alone, no per-record ContextSlot, process-global backend
generations, MakeCurrent dedup per current binding.
2026-10-01 19:16:41 -04:00
swung0x48 ebc552a048 [Fix] (WSI): report Wayland present failure and keep GLX drawables off XIDs
Wayland Present() failure now returns EGL_BAD_SURFACE instead of being
dropped. In spawn mode a GLX X drawable is backed by a remote pbuffer so
the XID is never mistaken for an Android ANativeWindow; X11 pixel present
and remote resize are still unimplemented.
2026-10-01 13:45:52 -04:00
swung0x48 6fb880cdf2 [Build] (Linux): alias libGL.so and libGL.so.1 to libMobileGL
GLX clients link against libGL; give the Linux build the same alias
treatment libEGL already had so glvnd-less clients can load MobileGL.
2026-10-01 13:45:51 -04:00
swung0x48 d52c031c50 [Docs] (Design): state ownership for multi-session/context/share-group (P14)
The whole stack assumes one process = one server = one session = one
context = one share group. The anland unified server needs that equation
broken in the library instead of fork-per-app workers. This document fixes
the four ownership layers (session, context, share group, thread current),
the wire changes (per-record context slot, CreateContext/DestroyContext
SurfaceOps, context-tagged reverse events), and the staged plan P14a-d
with acceptance tests.
2026-10-01 13:45:41 -04:00
swung0x48 4156ca6e36 [Docs] (Anland): hand off unified runtime work and record corrected state ownership requirements 2026-10-01 13:18:23 -04:00
swung0x48 2f64ede8c6 [Feature] (Remote): serve concurrent shared-memory clients in isolated workers 2026-10-01 12:53:27 -04:00
swung0x48 a5b463a5e8 [Feature] (Remote): expose a package-neutral embedded display server API 2026-10-01 12:31:57 -04:00
BZLZHH 32c79f4135 [Feature] (EGL): Wayland window surfaces, presented through wl_shm
A Wayland client's window is its own wl_surface, and what the compositor shows is the wl_buffer
attached to it - but the pixels of a MobileGL frame are drawn where the backend is, on a split
build in another process on another OS.  So far a wl_egl_window was taken for an X11 window and
its client-side address sent to the Android server (EGL_BAD_NATIVE_WINDOW at best), and under
glvnd a Wayland display never reached this vendor at all.

A window on a Wayland display - asked for as EGL_PLATFORM_WAYLAND, or a wl_display recognised by
its first word being wl_display_interface - is now a window surface in the EGL state at the
wl_egl_window's size with a pbuffer of that size behind it in the backend, whatever
MOBILEGL_IPC_SURFACE says.  At every swap the frame is read back out of the default framebuffer
through this library's own glReadPixels (the application's read framebuffer, pack buffer and pack
state put back as they were), its rows flipped and swizzled into a wl_shm buffer, and attached,
damaged and committed on the application's wl_surface.  Three buffers rotate on their release
events, dispatched on an event queue of the surface's own.  libwayland-client is loaded at run
time, so neither the build nor the library depends on it.  EGL_KHR_platform_wayland and
EGL_EXT_platform_wayland are announced on Linux.

Measured: glmark2-wayland runs its build scene at 13 FPS (400x300) and the window shows the frame
under kwin.  A window resize is not followed yet; the surface keeps its creation size.
2026-10-01 12:25:20 -04:00
gold f458924360 [Fix] (EGL): a second display is served, and glvnd gets its answers from one table
Two defects, both found by asking the client directly rather than by reading its log.

The backend refused to initialize any EGLDisplay but the first - "already bound to a different
EGLDisplay" - and the field it compared against has no reader anywhere else in the tree: the domain
rule was the whole of the rule.  What it cost was a compositor, which asks for the display it will
draw into AFTER probing another one.  The probe succeeded, the real display was refused, and the
caller reported the refusal one layer up as "no usable DRM render device" - a message that names
where it stopped, not why.

The second: glvnd asks a vendor for entry points from two places.  getProcAddress is how it builds
its own static EGL table, and getDispatchAddress is how it answers eglGetProcAddress for every name
it does not know itself - which is every extension entry point, the device queries among them.  This
side answered the second from a GL-only table, so an application asking for eglQueryDeviceStringEXT
got a null for a function the vendor implements.
2026-10-01 12:22:55 -04:00
gold 0dad3fcaa1 [Feature] (EGL): MobileGL serves EGL through libglvnd
On a system with libglvnd the libEGL.so.1 an application links is a dispatcher, not an
implementation, and the supported way to be the implementation it talks to is to be a vendor
library it loads and routes to.  On this machine it is also the ONLY way that reaches a caller which
does not link EGL directly: kwin goes through libepoxy, which dlopen()s libEGL.so.1 and dlsyms its
entry points at startup, so a library injected with LD_PRELOAD is invisible to it however loudly it
is named - measured, with the client in kwin's maps and not one call into it.

So this is the vendor half of that ABI: __egl_Main answers glvnd with the entry points by name, the
platforms this implementation serves, and the device it is associated with - the render node of the
machine, because that is what lets a compositor match its own node to the display it is about to
draw into, and because glvnd drops a vendor that does not answer at all.

Two details are the difference between a vendor that is loaded and a vendor that is used.  glvnd
requires setDispatchIndex to exist even when a vendor has nothing to record, which this one has -
its entry points are the implementation, they never re-enter glvnd.  And a vendor must hand over ITS
OWN entry points: with default visibility a reference inside this library resolves against the whole
process, where another libEGL that loaded first wins.  That is what -Bsymbolic-functions is for;
declaring the entry points protected says the same thing but does not compile against the platform
headers, which declare them default.
2026-10-01 12:22:55 -04:00
gold 75f83b20fc [Feature] (EGL): the platform and device extensions are one list, spelled once
A vendor string and an eglQueryString answer have to agree, and they are read by two different
parties: the system's libglvnd reads the first to decide which vendor a platform request should be
routed to, and an application reads the second before it decides to make that request at all.  Kept
apart they drift, and the drift has a signature - a library that is loaded, initialized, and never
called, while the caller reports that it found no provider for a function it never reached.

The list gains the device family for the same reason: a compositor asks which devices exist and
reads the DRM node off one before it asks for a display on it, so an implementation that answers
"no devices" is one it cannot use no matter how well the implementation draws.
2026-10-01 12:22:54 -04:00
gold 57781688ce [Chore] (glvnd): vendor the EGL vendor ABI headers
Two MIT headers from libglvnd, verbatim, because MobileGL is about to implement the vendor half of
glvnd's EGL ABI and the distribution ships the runtime without them.
2026-10-01 12:22:54 -04:00
swung0x48 7308668488 [Docs] (Anland): kgsl-as-server.md - read with the kernel as the server, routes A and B are about who owns the buffer (6.x + Mesa is A, 5.x is B), plus the split-level analogy to virtio-gpu native context vs virgl / venus; linked from the README 2026-09-30 13:00:11 -04:00
swung0x48 134791b4b9 [Docs] (Anland): compare today's container Mesa (freedreno / turnip over kgsl) with routes A and B - it is route A's no-change dma-buf tier with the renderer in the app process, Anland's dma-buf ingress was shaped around it, and 5.x's private protocol sat between A and B 2026-09-30 12:45:16 -04:00
swung0x48 c24926cafe [Docs] (Anland): add route B - Anland hands the window's Surface to the MobileGL server (the FCL shape: SC mode leaves the parent Surface's queue free, waylandbridge itself receives it over binder), with its MobileGL items B1-B4, the Anland extension, its limits, and the phasing reordered around it 2026-09-30 12:24:24 -04:00
swung0x48 c5fa722be7 [Docs] (Anland): correct the window model - a toplevel attaches on demand to at most one Android window and several can be attached at once (popups and subsurfaces composite into the parent); P12 does not fit because the Android window and its input belong to Anland, not because of the window count 2026-09-30 12:17:56 -04:00
swung0x48 b7aa8d2011 [Docs] (Anland): research note - what MobileGL and Anland would each need for MobileGL to render Anland's Linux apps (glibc split client in the container, bionic render server, native AHB ingress into waylandbridge), with the two source-audit reports, their briefs and the notes index entry 2026-09-30 11:57:57 -04:00
swung0x48 378020adf3 [Fix] (MG_Remote, P11 T0): the T0 POST also checks what the CPU writes through the held lock after the import and its first GPU use, in both directions - Mali-G1-Ultra passed the old single round (the import cleans the CPU cache) while its GPU saw 0/1024 post-import words, so T0 was granted and create-indirect-in-world-align1024 x Magma lost every Flywheel object on inproc/spawn (0.867/0.848); it now fails there and runs T2 (0.999984), and the Redmi passes both rounds and keeps T0 2026-09-30 10:21:46 -04:00
swung0x48 028cf4eb62 [Merge] (trace): take dev's create-indirect-in-world-align1024 fixture (a derived trace replacing the original create-indirect capture) and register its documentation-only derived_from key 2026-09-30 10:21:46 -04:00
swung0x48 08124c99f1 [Test] (trace): replace the create-indirect fixture with minecraft-1.21.1-neoforge-create-indirect-in-world-align1024, a derived trace and not the original capture - the desktop capture bound Flywheel's scatter lists at 16-byte SSBO offsets that Adreno 830 and Mali's Vulkan driver (64) reject, and fixture_tools/realign_buffer_ranges.cpp moves the 1,270 binds off a 1024-byte boundary onto 1024-aligned slots (3,812 calls inserted, target call 504631 -> 508443, host SSIM unchanged to 9 digits on both backends) 2026-09-30 10:00:26 -04:00
swung0x48 648800a2da [Docs] (P8): close the phase - ruling ID-P8-19 (the exit device check and the P12 cross-host rerun), the P8 README and PLAN, the roadmap row and header, MEASUREMENTS row 19, the status page and the last debt 2026-09-30 08:25:38 -04:00
swung0x48 b4bee9dd38 [Docs] (P8): CONTRACT-P8 (the rules P8 landed, each with its code anchor and its test), rulings ID-P8-14..18 (MD + MF and the create-indirect fixture's alignment, SE, SV, the dev merge, the wave 2 integration gate) and the debts wave 2 hands on 2026-09-30 07:55:50 -04:00
swung0x48 494305381e [Merge] (P8): integrate wave 2 package MF - the push flush ladder gets dev's Adreno 830 queued-ring-copy guard (FlushPendingRangesFrom, create-instancing x Espryt x monolith 0.870 -> 0.99998 on the Redmi), and both G5 scripts pin the two reviewed ladder bodies under ID-P8-14 2026-09-30 07:12:50 -04:00
swung0x48 fb106a682e [Docs] (P8-MF): the package note - the push-ladder mirror of dev f973008c, create-instancing Espryt monolith 0.870 -> 0.99998 on the Redmi with spawn unchanged, why spawn was unaffected (the same two partial maps behind queued ring copies, the copy already submitted by the server's intervening whole-buffer maps), the red-onces and the gate 2026-09-30 07:09:28 -04:00
swung0x48 c8c4397475 [Merge] (P8): integrate wave 2 package SV - Magma's 3D native mip blit uses one layer, the depth/stencil mip decline gets its tally row, 1D depth mips decline by name, and the wire arm issues glDispatchComputeIndirect natively
# Conflicts:
#	MobileGL/MG_IntegrationTest/CMakeLists.txt
2026-09-30 06:11:04 -04:00
swung0x48 9c24a14d2b [CI] (scripts, P8-MF): pin G5's tenth row as well and re-pin both flush ladders on the reviewed post-fix bodies (ID-P8-14) - dev f973008c moved FlushPendingRangesNow past every base ref CI passes (the Adreno 830 queued ring copy) and ac9ab078 mirrors it into FlushPendingRangesFrom; both scripts' self-tests now run the pin-precedence and one-token controls on both ladders 2026-09-30 05:45:10 -04:00
swung0x48 ac9ab078cc [Fix] (Espryt, P8-MF): the push arm's flush ladder keeps a partial range-invalidating map behind an upload-ring copy still queued on the store, as dev f973008c's pull arm does - FlushPendingRangesFrom's ring tier stamps ringCopyRetireSerial and InvalidateFlushAccessFor refuses tier 1 for a partial range while that copy is unretired (the whole-buffer orphan-map stays exempt); Adreno 830 loses the queued copy and create-instancing on the monolith arm drew terrain sections empty 2026-09-30 05:43:55 -04:00
swung0x48 377d90d9f1 [Merge] (P8): integrate wave 2 package SE - the spawn/tcp default framebuffer publishes its surface to the client, the CPU mip arm declines on a driver-written base, D's case 3 runs on the Espryt split arms, the unmigrated-emulation name list is linted against its call sites, and the P8 notes' citations point at their content again 2026-09-30 05:24:11 -04:00
swung0x48 4cfb29a553 [Docs] (P8-SV): the note's gate table (G1 0/0, wire-declines 61/66, split_coverage, every lane, retrace 230/236) and the dirty-surface scan finding 2026-09-30 05:02:37 -04:00
swung0x48 51e95c9260 [Docs] (P8-SE): the note's gate and retrace readings 2026-09-30 04:47:27 -04:00
swung0x48 982958ebb1 [Test] (P8-SV): rename WireIndirectDispatchScenario's dispatch store member m_parameters -> m_groupCounts - gen_pipe_dirty_surface.py reads writes to shutter member names across all of MobileGL, and the name made NEW_PATCH_STATE's shutter look written outside its roots (self-test negative control 16 stopped tripping) 2026-09-30 04:42:06 -04:00
swung0x48 3717424229 [Merge] (P8): merge dev f973008c - the Espryt partial-flush fix for a queued ring copy on Adreno 830 (wave 2 MD), and dev's empty-binding uniform block fix deduplicated against P11's 7784d6bb (one code path), UnboundUniformBlockScenario registered on the split arms 2026-09-30 04:34:15 -04:00
swung0x48 f973008c4d [Fix] (Espryt): a partial buffer flush no longer takes the range-invalidating map while an upload-ring copy into the same store may still be queued - Adreno 830 loses the queued copy, and create-instancing drew whole Embeddium terrain sections empty 2026-09-30 04:07:41 -04:00
swung0x48 edcb9328e2 [Docs] (P8-SE): cite the integration CMakeLists by path - the bare basename resolves to the top-level file 2026-09-30 03:55:20 -04:00
swung0x48 e61060fbdf [Docs] (P8-SE): the package note, and the P8 package notes' line citations re-anchored by content at HEAD (A 14, B 29, C 10, D 12, E 18 plus the copy-image-shadow-mirror line, F 4) 2026-09-30 03:55:01 -04:00
swung0x48 3ce8ce3cb1 [Test] (P8-SE): the ForcedDs knob lanes run the default-framebuffer depth/stencil case again on all three arms (it was subtracted while red on spawn / tcp) 2026-09-30 03:55:01 -04:00
swung0x48 394d255487 [Docs] (P8-SV): the package note - the four items with their before/after, the design, the gates, ten red-once rows, the monolith observation and what was found but not fixed 2026-09-30 03:45:40 -04:00
swung0x48 0222852b94 [Fix] (Espryt, P8-SE): a spawn / tcp server publishes the default framebuffer's depth/stencil format (it has no frontend placeholder; the case leaves pending-fix for the three arms); the server CPU mip arm declines by name (generate-mipmap-store-declined) instead of filtering a store the driver wrote over, and E's refused-readback knob reaches it (CpuMipRefused. entries); WireIndirectDrawScenario case 3 runs on the Espryt split arms; copy-image-shadow-mirror goes and unmigrated_emulation_sites.py holds PipeCatalogueTest's list to the call sites 2026-09-30 03:44:29 -04:00
swung0x48 2edb2aa82a [Test] (P8-SV): WireMipShapeScenario (3D chain on both backends x every arm, 1D depth decline on Magma split+spawn, the 3D chain under the validation layer inproc) and WireIndirectDispatchScenario (both backends x every arm, Magma gated .IndirectDispatch.), the MipmapDepthStencilAspect tally in F1's depth/stencil decline case, WireDeclinePeek, and the two parity rows 2026-09-30 03:44:03 -04:00
swung0x48 fc415aee76 [Fix] (Magma, P8-SV): the wire arm's 3D native mip blit uses one layer with the depth in its z offsets, the depth/stencil mip decline gets its WireDeclineTally row (MipmapDepthStencilAspect), the depth shader mip refuses 1D/1D-array/3D view types before the chain grows, and glDispatchComputeIndirect is issued as vkCmdDispatchIndirect from the wire store (P8-D's barrier rule, wdsp counter, two named declines) 2026-09-30 03:44:03 -04:00
swung0x48 307267c188 [Docs] (P8): the monolith defect count is 12 (13 reports, one duplicate between C and D), 9 of them left to P13, not 14 and 11 2026-09-30 03:24:47 -04:00
swung0x48 cbbaf77f6d [Docs] (P8): wave 1 integrated - rulings ID-P8-5..13 (the survey, packages A-F, the integration gate, the wave 2 scope and the monolith defects left to P13), the survey note, open questions 9/11/15 answered, the debts and the P8 contract amendments, the P8 README table, PLAN-P8 wave 2 and the status page 2026-09-30 03:09:09 -04:00
swung0x48 39cd8fb73b [Merge] (P8): integrate package E - a texture readback the Espryt driver refuses is answered from the split server's staged store, which now follows glCopyImageSubData (FollowCopy, client-box merge, sticky driver-written mark, named decline texture-readback-store-declined); CopyImageStoreReadbackScenario on both backends x every arm plus the extent-scoped StoreRead knob; the dead CopyImageSubData+RENDERBUFFER latch goes (census 79 -> 78) 2026-09-30 02:17:51 -04:00
swung0x48 92eab9f3b7 [Merge] (P8): integrate package D - the Magma wire arm issues glDraw*Indirect / glMultiDraw*Indirect[Count] natively from the wire store (vkCmdDraw*Indirect[Count]) instead of a CPU expansion behind a GPU-idle wait, with an INDIRECT_COMMAND_READ barrier once per shader write; windr[] PipeStats counters and WireIndirectDrawScenario on both backends x every arm 2026-09-30 01:28:20 -04:00
swung0x48 9441071567 [Merge] (P8): integrate package C - the Espryt split server refreshes its staged copy of a buffer a shader wrote before a CPU reader consumes it (storage/atomic/buffer-image marks, XFB captures into the copy; restart rewrite, multi-draw rebase, *IndirectCount, CPU-fed indirect fields, XFB scatter and fp64 narrowing read through SplitHostBytesForCpuRead); GpuWrittenDrawInputScenario (12 cases) on the Espryt split arms 2026-09-30 00:45:16 -04:00
swung0x48 2f530ad8a2 [Merge] (P8): integrate package B - Espryt glGenerateMipmap on the server: the record's generated window (BASE/MAX clip, B1) and RGB16F/RGB32F natively or on the server's CPU from the driver's level (B2), retiring both split generate-mipmap names; GenerateMipmapServerScenario on both backends x monolith/Split/Spawn/Tcp plus the forced-CPU arm 2026-09-30 00:01:29 -04:00
swung0x48 c00f5eabae [Docs] (P8-E): the note's gate and retrace readings and two residuals (the extent-scoped tcp knob, the ERROR line before a store answer) 2026-09-29 23:51:49 -04:00
swung0x48 24b8aef616 [Fix] (P8 A): exempt F's Magma-only F1WireScenario.GenerateMipmapWithoutAShaderArmShapeDeclinesAndKeepsTheSession on DirectGLES as single-backend - it landed with P8-F after A's exemption table was cut, skips on Espryt like its F1Wire decline siblings, and split_coverage.py named it missing on the merged head 2026-09-29 23:41:10 -04:00
swung0x48 8133cd52ae [Merge] (P8): integrate package A - register the monolith-only cases on split/spawn/tcp (P8Coverage.cmake, five server-knob tcp supervisors), gate them with split_coverage.py and its exemption table, pin the tcp supervisor's Vulkan env 2026-09-29 23:37:12 -04:00
swung0x48 06f28b0a1b [Docs] (P8-A): the note says what the census measured about the audit's 396 and DoublePrecision on tcp, and the knob-port comment names the split/verify one-port overlap 2026-09-29 23:18:08 -04:00
swung0x48 b9feb9d7be [Docs] (P8-B): the package note - the generated window and the server CPU arm, the coverage matrix, seven red-onces and monolith defects M1-M3 recorded for dev 2026-09-29 23:16:05 -04:00
swung0x48 2b4ae539da [Merge] (P8): integrate package F - reclassify what our client cannot send: the multi-draw latches and Magma client-vertex-array become ProtocolCorruption, Magma mip shader-shape becomes a pre-growth named decline, unpublished cap bits and the index-mirror sentinel marked reserved 2026-09-29 23:07:03 -04:00
swung0x48 e815ce8d33 [Docs] (P8-C): the package note - readers and writers of the server's staged copy, the twelve GPU-written draw-input cases, each red-once, the retrace subset and the five monolith defects recorded for dev 2026-09-29 22:53:18 -04:00
swung0x48 00d6c2480e [Docs] (P8-E): the package note - the split read never touched a shadow, the store route and FollowCopy, the red-once table, the monolith defect for dev 2026-09-29 22:43:02 -04:00
swung0x48 f823422bfa [Fix] (Espryt, P8-E): a texture readback the driver refuses is answered from the server's staged store, which now follows glCopyImageSubData (FollowCopy, client-box merge, sticky driver-written mark, named decline); CopyImageStoreReadbackScenario on both backends x four arms plus the extent-scoped store-read knob; the dead CopyImageSubData+RENDERBUFFER latch goes (census 79 -> 78) 2026-09-29 22:43:02 -04:00
swung0x48 e52a997f34 [Fix] (Espryt, P8-C): the split server refreshes its staged copy of a buffer a shader wrote before a CPU reader consumes it - storage/atomic/buffer-image writes mark the server twin, XFB captures land in the copy, the restart rewrite, multi-draw rebase, *IndirectCount, CPU-fed indirect fields, XFB scatter and fp64 narrowing read through SplitHostBytesForCpuRead; GpuWrittenDrawInputScenario (12 cases) on the split arms 2026-09-29 22:38:33 -04:00
swung0x48 c7a14ed790 [Test] (P8-F): give the mip shader-shape decline its own .ShaderMipDecline. tail - spawn_lane_parity.py holds each server-knob tail to exactly one case, and sharing .ShaderMip1. named two 2026-09-29 22:37:49 -04:00
swung0x48 d639e54616 [Perf] (P8-D, Magma): the wire arm issues glDraw*Indirect / glMultiDraw*Indirect[Count] as vkCmdDraw*Indirect[Count] from the wire store instead of expanding them on the CPU behind a GPU-idle wait (create-indirect split: 321 waits / ~8 s -> 0), with an INDIRECT_COMMAND_READ barrier once per shader write; windr[] PipeStats counters, WireIndirectDrawScenario on both backends x every arm, notes/p8/D.md 2026-09-29 22:32:49 -04:00
swung0x48 32f222dbc2 [Docs] (P8-A): the package note - the 304-case census per arm, the server-knob lanes and their supervisors, the exemption classes, the red-once record, lane wall times and the findings 2026-09-29 22:30:20 -04:00
swung0x48 a2c81a2f64 [Fix] (P8-A): the tcp lane's shared supervisor carries the Vulkan ICD and the three lavapipe iterationRP repairs, which Magma reads in the server - DirectVulkan.Tcp IterationRPProgram203 missed its golden output by one texel without them 2026-09-29 22:30:20 -04:00
swung0x48 8dac1fd8a4 [Fix] (Espryt, P8-B): glGenerateMipmap on the server - the record's plan is the generated window (B1) and RGB16F/RGB32F generate natively or on the server's CPU from its own level (B2), retiring both split generate-mipmap names; GenerateMipmapServerScenario on both backends x monolith/Split/Spawn/Tcp plus the forced-CPU arm 2026-09-29 22:22:55 -04:00
swung0x48 b71cb9cc2c [Docs] (P8-F): cite MG_Backend/Init.cpp by path - the bare basename is ambiguous 2026-09-29 22:15:45 -04:00
swung0x48 efd3533bd1 [Docs] (P8-F): the package note - the four items, their evidence, the red-once record and what was found but not fixed 2026-09-29 22:15:29 -04:00
swung0x48 67890f523a [Refactor] (P8-F): reclassify what our client cannot send - the multi-draw client-indices/instanced latches and Magma's client-vertex-array become ProtocolCorruption, Magma's mipmap-shader-format-or-shape becomes a named decline decided before the chain grows (with a knob-forced 3D case on split and spawn), and the three unpublished cap bits and the index-mirror sentinel are marked reserved 2026-09-29 22:15:29 -04:00
swung0x48 976fbe4cd4 [Test] (P8-A): register the monolith-only integration cases on the split, spawn and tcp arms (P8Coverage.cmake: 246 ambient cases, five server-knob lanes each on its own tcp supervisor), retire the ClientSideIndices and GuiBatch exclusions, and gate the rest with split_coverage.py and its exemption table 2026-09-29 22:14:31 -04:00
swung0x48 8d3e2317a6 [Docs] (P8): re-scope the phase - strike the original scope in the roadmap and the P8 README, add PLAN-P8 (six items closed, packages P8-0 and A-G, the rewritten gates) and rulings ID-P8-1..4, and mark P8 in progress on the status page 2026-09-29 21:31:15 -04:00
swung0x48 0b55b55d4d [Docs] (P11): the status page lists the decisions waiting on the user - the P8 start and the six debt rulings 2026-09-29 21:17:21 -04:00
swung0x48 642ac891af [Docs] (P11): ruling ID-P11-20 - the closing head's gate, lanes and retrace numbers 2026-09-29 21:13:12 -04:00
swung0x48 592558f5fc [Docs] (Debts): the debt review - drop the closed E1, DualBlock and GetCaps-blobref rows (open question 24 closed, the blobref rule now a CONTRACT-P5 row-18 invariant), move the unmeasured M2 note to MEASUREMENTS, rewrite HyperOS / P11 RSS / named refusals, split the P12 row, merge the two CI rows, add three B2 rows, and mark each destination as needing a ruling or ready 2026-09-29 21:12:28 -04:00
swung0x48 0a117243d5 [Docs] (P11): close the phase - rulings ID-P11-17..19 (B2 T0 zero-copy, the stale indexed binding and Magma empty-uniform fixes with the skip-hole, T0 on by default), B2-DEVICE, the P11 README, the roadmap row and header, the status page and the debts P11 hands on 2026-09-29 20:33:00 -04:00
swung0x48 344e2f8439 [Docs] (P11 B2): T0 is the default (the user, 2026-09-29) - CONTRACT-P11 B2's tier table, unset versus 0, the revision-5 fix-up; the knob rows in design/09 2026-09-29 20:15:29 -04:00
swung0x48 91900bdc5e [Test] (P11 B2): the unset knob on a stream and on a no-AHardwareBuffer shared-segment session - T2, no W/I fallback line, the fallback counted; the 0x80 ask accepted and 0x81 refused 2026-09-29 20:15:29 -04:00
swung0x48 b32140e614 [Feat] (MG_Remote, P11 B2): T0 is the default - an unset MOBILEGL_IPC_ADOPT_TIER asks T0 (ask 0x80) and falls back to T2 quietly (MGLOG_D, still counted: client T0Fallbacks, server T0Refusals), while an explicit 0 keeps every named line; revision 5's schema comment and pin fixed up 2026-09-29 20:15:29 -04:00
swung0x48 7030d307be [Test] (Magma): UnboundUniformBlockScenario - a dispatch and a draw whose program declares a uniform block at an empty binding point (never bound, or its buffer deleted while bound) run and produce their fallback, and a buffer bound there afterwards is read, on both backends 2026-09-29 19:58:41 -04:00
swung0x48 853c5f1264 [Fix] (Magma): a uniform block declared at an empty binding point reads a zeroed block instead of dereferencing a null buffer 2026-09-29 19:55:53 -04:00
swung0x48 e44d6ac7da [Docs] (P11 B2): CONTRACT-P11 - a buffer deleted while bound to an indexed point: the defect, the fix, Magma's behaviour before it, the tests with their red-once, and the G1 delta 2026-09-29 19:44:08 -04:00
swung0x48 8571e0c001 [Test] (P11 B2): DeletedBoundBufferScenario - a buffer deleted while bound to a storage, uniform, atomic-counter or transform-feedback point, its slot reused, then a dispatch and a draw, on monolith and Split/Spawn/Tcp for both backends; and the one-process arena pair as a regression 2026-09-29 19:32:19 -04:00
swung0x48 7784d6bbf7 [Fix] (Magma): a uniform block declared at an empty binding point reads a zeroed block on the monolith arm instead of dereferencing a null buffer, as the wire arm already does 2026-09-29 19:32:19 -04:00
swung0x48 4aec5621a8 [Fix] (MG_State, P11 B2): glDeleteBuffers moves the bind-point generation of every indexed target it unbinds, and a transform-feedback object swap moves it too - the server's window no longer names a deleted buffer's handle after its slot is reused 2026-09-29 19:32:19 -04:00
swung0x48 4c02279f22 [Docs] (P11 B2): CONTRACT-P11 B2 - the Espryt split/spawn itest entries now pin MOBILEGL_ITEST_REQUIRE_GPU themselves 2026-09-29 19:12:46 -04:00
swung0x48 e869d18ce8 [Test] (P11 B2): pin MOBILEGL_ITEST_REQUIRE_GPU=1 on every Espryt split/spawn itest environment, as the Magma and Espryt TCP lists do - a handshake-time Fatal fails there instead of skipping as a ctest pass 2026-09-29 18:57:04 -04:00
swung0x48 9d1e788597 [Docs] (P11 B2): CONTRACT-P11 B2 - M2 staying green on Espryt is consistent with the driver holding its own reference, not verified 2026-09-29 18:44:25 -04:00
swung0x48 ce5753c883 [Docs] (P11 B2): CONTRACT-P11 B2 - the tier table that replaces A1's shared-segment Fatal, revision 5, the Offer and its thread, POST, the staged-copy audit, lifetime, CPU reads, host and device gates with their red-once 2026-09-29 18:24:53 -04:00
swung0x48 5e65907d63 [Test] (P11 B2): RemoteClientAdoptT0.KnobTwoIsTodaysT2WithNoT0Line - the default asks 2 and neither side settles, imports or logs T0 even where the stand-in platform could run it 2026-09-29 18:20:25 -04:00
swung0x48 98bf1925aa [Test] (P11 B2): LargeArenaAdoptionScenario - a draw still queued when the arena is rewritten or respecified keeps the bytes it was issued against (no readback between the two draws, so an in-place write or an early free shows) 2026-09-29 18:06:48 -04:00
swung0x48 17f1c0b59b [Tool] (P11 B2): extmem probe --hold-ms - the T0S child keeps its AHardwareBuffer locked while the parent holds both imports, for the dmabuf memory calibration 2026-09-29 17:50:50 -04:00
swung0x48 2522f1803c [Test] (P11 B2): T0 protocol on the host - the Offer inbox (late, out of order, stale, missing) over both aux channels, the whole T0 path on a stand-in platform, A1's two handshake-Fatal cases rewritten as named fallbacks, revision 4 refused by name, AdoptTier0 lanes on the shared-segment arms and the allow switch through spawn 2026-09-29 17:50:50 -04:00
swung0x48 7cde55b1c1 [Feat] (Magma, P11 B2): import a T0 store through VK_ANDROID_external_memory_android_hardware_buffer as a wire buffer (external VkBufferObject), serial-gated release, done-no-bytes readback, and the sustained-lock POST on the renderer's device 2026-09-29 17:50:50 -04:00
swung0x48 2a69f61c88 [Feat] (Espryt, P11 B2): import a T0 store with glBufferStorageExternalEXT as the adopted twin - no staged copy, server writes queued GPU-ordered, CPU readers on the import's coherent map, fence-gated release, re-import after a context loss, and the sustained-lock POST in a throwaway context 2026-09-29 17:50:50 -04:00
swung0x48 590d74525d [Feat] (MG_Remote, P11 B2): T0 behind MOBILEGL_IPC_ADOPT_TIER - control revision 5 (LinkTerms.adoptTier ask), kCapAdoptT0 granted at the first native bind after the backend's POST, the AHardwareBuffer Offer on the aux socket matched to its map_persistent record by seq, named T2 fallbacks instead of A1's handshake Fatal, the server allow switch, done-no-bytes CPU reads 2026-09-29 17:40:56 -04:00
swung0x48 b57276b88a [Tool] (extmem_probe, P11 B2): route mode - the probe serves in the render server's place (-Pmobilegl.extmemProbeAsServer) and --route-client sends a held-lock AHB over B1's fd: pair, directly on aux and through a socketpair hop 2026-09-29 16:44:37 -04:00
swung0x48 ac25e3323a [Tool] (extmem_probe): build inside the API-26 trace APK - memfd_create through syscall() below API 30, the two Vulkan 1.1 calls through vkGetInstanceProcAddr below API 28 2026-09-29 16:31:52 -04:00
swung0x48 96881f639c [Build] (P11 A3): MOBILEGL_BUILD_EXTMEM_PROBE / -Pmobilegl.buildExtmemProbe packages tools/spikes/extmem_probe as libMobileGLExtmemProbe.so in the trace flavour 2026-09-29 16:31:52 -04:00
swung0x48 16ea23c927 [Tool] (extmem_probe): marker/args-file mode for the trace APK exec hook, --regions-at-end, and the T0S sustained-lock rows (Vulkan copy/fill + GL compute on a never-unlocked client AHB) 2026-09-29 16:31:52 -04:00
swung0x48 b9b14c4775 [Docs] (P11): status page - the two handed-on dev bugs are merged, the gate row points at the B1 and M2 rulings 2026-09-29 16:14:34 -04:00
swung0x48 37c7aede31 [Docs] (P11): ruling ID-P11-16 on B1 (fd: endpoint, token broker pairing at hand-off, app_process helper, per-peer apply affinity, the HyperOS battery requirement, the device pass), the B1 debts, the status page, the phase README and the roadmap row 2026-09-29 16:14:18 -04:00
swung0x48 64b5794bc9 [Docs] (P11 B1): CONTRACT-P11 B1 - under the EOF Hello wait a second client slower than 2 s sees a send failure rather than Refuse{Busy}, and the unix-reach argument is the device's 2026-09-29 16:10:45 -04:00
swung0x48 ce4dca2835 [Docs] (P11 B1): CONTRACT-P11 B1 - the rebased head's device pass: rd12 shm vs tcp on both routes, Magma openra re-measured clean, the 10 s slow start and the plugin flavour 2026-09-29 16:06:30 -04:00
swung0x48 df826f426f [Docs] (P11 B1): CONTRACT-P11 B1 - the broker pairs at hand-off, a paired unix session waits for its Hello until EOF (the decision and its file:line), the lone-prime guard, the supervisor's announced core as the one source, and the follow-up gates 2026-09-29 15:03:52 -04:00
swung0x48 7ccb920ad2 [Feat] (Plugin, P11 B1): the broker presents the PairBind pair at hand-off (PairBindFrames, the encoder's bytes) and answers v2 with that flag and the apply core its supervisor announced; the helper passes MOBILEGL_IPC_FD_PAIRED=1 on, and Java no longer re-derives the reserved core 2026-09-29 15:03:52 -04:00
swung0x48 582adb0dd6 [Test] (P11 B1): the broker's PairBind templates against the encoder, a pair presented at hand-off whose Hello comes 5 s later, a pair presented only at program start refused by name, and the reserved core on lone-prime, symmetric and unreadable topologies 2026-09-29 15:00:30 -04:00
swung0x48 578795c84d [Feat] (MG_Remote, P11 B1): pairing at hand-off - the fd: endpoint presents no PairBind when MOBILEGL_IPC_FD_PAIRED=1 says the broker did, names the server's refusal when its own PairBinds arrive too late, and a paired unix session waits for its Hello until EOF instead of 10 s 2026-09-29 15:00:30 -04:00
swung0x48 9af4c8aefd [Feat] (MG_Remote, P11 B1): the reserved apply core never takes a lone prime core - several prime cores give the lowest, one gives a big core beside it or none, and the log names which - and the supervisor announces the core it reserves on stdout at start-up, the one source the broker relays 2026-09-29 15:00:30 -04:00
swung0x48 5a97eb8d1b [Docs] (P11 B1): CONTRACT-P11 B1 - anchor the evidence paths to ~/w7/notes/p11/evidence/b1 and the report 2026-09-29 14:47:31 -04:00
swung0x48 fc427a14d6 [Docs] (P11 B1): CONTRACT-P11 - the external-client route (helper, token broker, fd: endpoint), the token rule and the named refusals, the apply-thread policy by peer and plane with its selection measurements, the keep-alive setting users must make, the ROM dependence and the gates 2026-09-29 14:47:31 -04:00
swung0x48 54e7c92cf4 [Feat] (Plugin, P11 B1): the broker hands the helper the core the server reserves for the session's apply thread and the helper execs the program through taskset on every other core (--keep-affinity opts out), so a client the scheduler would put there is not starved 2026-09-29 14:47:31 -04:00
swung0x48 1abddf0d00 [Test] (P11 B1): ApplyThreadPolicy - every row of the auto table by peer and plane, the Redmi topology, the shapes with no prime pair to split, explicit masks, spins and unrecognised text 2026-09-29 14:47:31 -04:00
swung0x48 0819f9d410 [Feat] (MG_Remote, P11 B1): the apply thread's auto affinity and spin chosen per session from the peer's dial mode and data plane - a dialled-in shared-segment client leaves the apply thread only the lowest prime core, stream, forked and in-process peers keep every big core 2026-09-29 14:47:31 -04:00
swung0x48 952a1ca4a7 [Tool] (P11 B1): trace flavour gate tooling - package the retrace CLI as libMobileGLRetrace.so (packageRetraceExe) and run an external client as a child of the app kept in front (mobilegl_exec_argv) 2026-09-29 14:47:31 -04:00
swung0x48 bd05e061fc [Feat] (Plugin, P11 B1): same-device external clients - the exported token-checked broker that connects twice to the server's own endpoint and answers every request, the app_process helper that execs the program on the pair, a private broker listener beside a TCP-only supervisor, and the copyable launch command on the server screen 2026-09-29 14:47:31 -04:00
swung0x48 23a9470a28 [Test] (P11 B1): the fd: endpoint end to end against a real server process after two bare connects, its PairBind pair read off the server side, and its named refusals that write nothing 2026-09-29 14:47:31 -04:00
swung0x48 53098bc57f [Feat] (MG_Remote, P11 B1): the fd:<control>,<aux> client endpoint - adopt two connected unix sockets a broker opened for this process, refuse by name anything that is not a distinct connected AF_UNIX stream pair, and present the client's own PairBind pair before the Hello, over shared segments 2026-09-29 14:47:31 -04:00
swung0x48 76f75f3a0c [Docs] (P11): ruling ID-P11-15 on the M2 dev merge - the scattered-upload fix completed on the split arms, the trim fence re-audited on the split server, the census and ratchet notes; DEBTS closes the two merged dev bugs and takes M2 leftovers 2026-09-29 13:46:37 -04:00
swung0x48 d587d394fe [Docs] (P11): zero-copy stays switchable (ruling ID-P11-14) - MOBILEGL_IPC_ADOPT_TIER 2 (default) keeps today shared-memory push, 0 takes the T0 import, and an unusable T0 falls back to T2 with a named line; the server gets its own allow switch 2026-09-29 13:45:57 -04:00
swung0x48 de996d539f [Test] (P11 M2): DescriptorPoolGrowthScenario waits for the inproc server to apply every record it emitted before each census read - under run-ahead a census right after the burst's swap read the pools before the burst, or a trim a frame late 2026-09-29 13:29:38 -04:00
swung0x48 b4b1825951 [Docs] (P11 M2): TextureEmit.h and the applier's accumulation name the one box-only record that is not a whole level - the oversize fallback's withheld cover, a partial box spanning gaps 2026-09-29 12:36:41 -04:00
swung0x48 ec5f6ead3f [Test] (P11 M2): TextureEmit.MoreThanKMaxDirtyRectsCollapsesToTheBoxWithRegionCountZero keeps its name and pins the one collapse left - a footprint larger than a record carries falls back to the storage's rect list 2026-09-29 12:26:10 -04:00
swung0x48 f636a4cdae [Test] (P11 M2): the link ratchet records TextureObjectWithOneMipmap::GetStorageDirtyFootprint - dev 9524c688's new virtual on the placeholder vtable UniformManager already reaches (171 -> 172) 2026-09-29 12:26:10 -04:00
swung0x48 bd2e43ede3 [Test] (P11 M2): dev's two Magma scenarios on the split arms - both on Espryt's three arms, OversizedRecordingSplitScenario on Magma's three (and pinned to a 64-draw budget on split and spawn, where the server reads it), DescriptorPoolGrowthScenario's census cases on Magma's split arm only; the split-recording arming reads both role logs and dev's pinned lane holds its log's RESOURCE_LOCK, and the census cases count the sets the wire arm holds between its descriptor rewinds 2026-09-29 12:20:15 -04:00
swung0x48 e2333d98a8 [Fix] (Magma, P11 M2): DirectVulkan::GetDescriptorPoolCensus reads the server renderer's pools on the apply thread under inproc (the test probe, between drain batches) instead of racing its growth, rewinds and trims, and answers unavailable on spawn and tcp 2026-09-29 12:20:15 -04:00
swung0x48 5c9ad5b499 [Test] (P11 M2): RemoteGuards pins GetStorageDirtyFootprint on the apply thread as Fatal{RoleViolation, "texture-legacy-arm"}, beside the rest of the dirty model's rows 2026-09-29 12:16:55 -04:00
swung0x48 9145433890 [Fix] (P11 M2, Espryt, Magma): the texture emitter carries a level's exact dirty footprint as its regions (dev 9524c688) - a single partial box as one region, only a whole-level write as none - so both servers upload exactly the texels written over GPU content; the applier keeps it exact up to 4096 regions, and Espryt's handle arm applies the storage's box-or-rects cut-offs itself while its texture holds no GPU writes 2026-09-29 12:16:55 -04:00
swung0x48 c371599811 [Merge] (P11): dev@c14283ee - scattered sub-image writes upload exactly their texels, Magma submits a frame's recording every 16384 draws with backpressure, and Magma grows descriptor pools geometrically and trims a quiet slot whole; the handle arm notes driver-side writes by handle and the wire arm splits recordings and keeps the trim's epoch peak across descriptor rewinds 2026-09-29 12:11:37 -04:00
swung0x48 b34eed580d [Docs] (P11): HSPIKE - an app_process helper with no Context gets connected sockets to the server app over Binder under the shell uid and another app uid, token-checked; ruling ID-P11-13 on the single broker mechanism and the ROM caveat 2026-09-29 11:42:59 -04:00
swung0x48 c14283ee2d [Docs] (Magma): the trim threshold's comment states the bound its argument proves - a growth adds at most twice what the slot held, so the slot stays over a third full 2026-09-29 10:58:33 -04:00
swung0x48 9dccabf03c [Test] (Magma): descriptor pool growth and trim - the pure sizing policy adds O(log N) pools for an N-set frame, and DescriptorPoolGrowthScenario's 4096-distinct-set frame lands whole on both backends, grows at most 7 pools, and is given back after quiet frames and re-grown while they are still in flight 2026-09-29 10:34:16 -04:00
swung0x48 53fa16c2c2 [Fix] (Magma): a frame slot whose descriptor pools stay under a quarter full for 120 frames is handed back whole - its set cache cleared, grown pools destroyed, base pool reset and every memo holding a set handle dropped - so rd12's loading burst no longer lives for the whole process (live sets 15,042 -> 39, pools 65 -> 6); MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES (0 = never) and a test-visible pool census 2026-09-29 10:34:16 -04:00
swung0x48 c2de6576ad [Fix] (Magma): a frame slot's next descriptor pool doubles its LARGEST pool of the same update-after-bind flavour, capped at 4096 sets - doubling the first (oldest, smallest) one made every grown pool the same size, so rd12's 15,016-set loading frame grew one slot to 60 pools (1x128 + 59x256); now 9 (64 ... 4096, 4096, 4096) 2026-09-29 10:34:15 -04:00
swung0x48 35e1c861a3 [Docs] (P11): ruling ID-P11-12 on pairing by PairBind nonce and revision 3 -> 4; B0 F2 corrected - the product TCP path already paired by identity, the defect was unix endpoints and the one-shot TCP server; the TCP fixture comment no longer names AcceptPair 2026-09-29 10:20:53 -04:00
swung0x48 9ebcf5a73d [Docs] (P11 PAIR): CONTRACT-P11 - the PairBind wire change and revision 3 -> 4, the acceptor's rules, cross-revision behaviour, the gates and the red-once 2026-09-29 10:19:59 -04:00
swung0x48 05dc94fa38 [Test] (P11 PAIR): PairAcceptorTest - a stray before a client, four interleaved connections, an orphaned aux past the budget and a Hello-first client, each on unix and tcp - and the device's probe-then-client case end to end in ServerSpawnTest 2026-09-29 10:19:59 -04:00
swung0x48 abe29eae2d [Fix] (MG_Remote, P11 PAIR): a socket client's control and aux connections pair by the nonce each presents first, not by arrival order - a readiness probe or a second client can no longer take half a session, and a client that sends its Hello first is refused by name 2026-09-29 10:19:59 -04:00
swung0x48 0a6af63655 [Feat] (P11 PAIR): the PairBind control message - the client's nonce and which of its two connections this is, the first frame on each - with the control revision 3 -> 4 and its pin 2026-09-29 10:19:59 -04:00
swung0x48 32b81dae42 [Docs] (P11): the two dev bugs P11 handed on are fixed on dev (Espryt scattered uploads 9524c688; Magma rd12 command-buffer growth d20c2d01) and arrive with the next dev merge 2026-09-29 09:52:37 -04:00
swung0x48 9c32d9fa68 [Test] (Magma): OversizedRecordingSplitScenario carries state across the split - a depth occluder, a stencil mark, an additive blend destination, a 4x multisample target resolved after it, occlusion and primitives-generated queries and a transform-feedback capture left open across it, each checked on both backends and in the budget-64 lane 2026-09-29 09:44:58 -04:00
swung0x48 4d3d677a7f [Test] (Magma): OversizedRecordingSplitScenario - a 1024-draw frame, one pixel and one uniform value per draw, lands whole on both backends; the DirectVulkan.SplitRecording lane pins the draw budget to 64 and asserts the split fired 2026-09-29 09:44:58 -04:00
swung0x48 d20c2d01ae [Fix] (Magma): a frame's recording is submitted every 16384 draws/dispatches and continues on a fresh command buffer, with at most frames-in-flight + 1 submissions outstanding - rd12's 981k-draw loading frame no longer grows one command buffer until Adreno's 16 KiB driver mappings exhaust vm.max_map_count (MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER, 0 = unbounded) 2026-09-29 09:44:58 -04:00
swung0x48 2fcde6eb9b [Docs] (P11): B0-CROSS-APP - SELinux refuses a foreign context the connect to the server app unix endpoint, past a brokered connect the shared segments work unchanged, default placement makes shm slower than tcp; ruling ID-P11-11 on what B1 must build, and the HyperOS uid freeze as a debt 2026-09-29 09:34:26 -04:00
swung0x48 21c7e8cd63 [Test] (MG_IntegrationTest, MG_Test): scattered writes over a GPU clear on both backends - two distant 2x2 boxes, a hundred boxes, boxes filling 3/4 of their union and three quadrants spanning the level, the 3/4 and level-spanning cases again at the image-bindable re-mint, and a MipmapStorage footprint fuzz against a reference bitmap 2026-09-29 09:06:07 -04:00
swung0x48 9524c688ca [Fix] (Espryt, Magma): scattered glTexSubImage writes upload exactly their texels - MipmapStorage keeps an exact dirty footprint beside its rect list (a write log once a merge or a fold would widen the list past the writes), Magma always uploads it, Espryt uploads it box by box (through the unpack ring too) once the driver texture may hold GPU writes and merges it at the image-bindable re-mint; the union box, the 3/4 cut-off, the 96-rect fold and a level-spanning union no longer put the stale shadow over GPU-written texels (two 2x2 writes over a GPU clear: 41 of 64 texels red -> 0) 2026-09-29 09:06:07 -04:00
swung0x48 6481691d1e [Docs] (P11): the status page, the roadmap row and the phase README for P11 in progress, and ruling ID-P11-10 on absorbing origin@86dc38a0 2026-09-29 08:06:38 -04:00
swung0x48 9176dcff3d [Merge] (P11): origin feat/disaggregated@86dc38a0 - its eglSwapInterval port is dev c60639db (already merged) plus ForgetRequestedSwapInterval, so an ended Magma server session no longer hands its interval to the next 2026-09-29 08:05:57 -04:00
swung0x48 26de2b60fc [Docs] (P11): rulings ID-P11-1..9 with the integration gate, A-DEVICE (T0 is GO from the app domain on both backends; adoption buys RSS not frame time on the Adreno; split T2 costs RSS and p99), the debts the dev merge closes and the ones P11 hands on, and W2-REMINT marking the monolith re-mint fixed 2026-09-29 08:03:09 -04:00
swung0x48 82467823ef [Docs] (P11 C): the contract's A1/A2 sections, Config.h's GPU-ordered landing, Init.cpp's DirectVulkan resource table, and the stream refusal CONTRACT-P6 / design 06 / 07 claimed now cites its code 2026-09-29 07:23:41 -04:00
swung0x48 2a902cbd79 [Test] (P11 A1/A2): LargeArenaAdoptionScenario asserts the tier its arena landed in and reads the tcp AdoptTier0 refusal; registered on Split/Spawn/Tcp for both backends (Espryt through the split-arms macro, Magma's Arena. block and Buffers lane), the monolith lanes declare adopted 2026-09-29 07:23:41 -04:00
swung0x48 3c72e5c637 [Fix] (P11 A1): settle MOBILEGL_IPC_ADOPT_TIER at the handshake from the data plane - a stream refuses T0/T1 by name and runs T2 on either side, a client over shared segments dies by name before any record, the at-use check trusts the settlement; unit gates on a real tcp session and a real inproc handshake 2026-09-29 07:23:41 -04:00
swung0x48 1797db0070 [Docs] (P11): B0 drives the cross-app client first as a retrace executable run from adb shell, then re-checks the connection and fd passing from a retrace client inside an app, the domain a Termux-style client really runs in 2026-09-29 07:23:41 -04:00
swung0x48 01a33dfc58 [Docs] (P11): B0 tests the cross-app client as a retrace client in one app against a render server in another - no Termux route; the fallback channel is a bound Service in the server app handing over a socketpair end 2026-09-29 07:23:41 -04:00
swung0x48 93a72678ca [Docs] (P11): B is scheduled - the consumer is a GL program started from another app on the same device (Termux and the like); B0 probes a cross-app fd channel, B1 moves such a client onto shared-memory segments, B2 adds the T0 import 2026-09-29 07:23:41 -04:00
swung0x48 3cb3e32b87 [Test] (P11 M): ImageBindableRemintScenario pins a barrier before its imageLoad copy - the kept-allocation case read the pre-clear upload once on push-monolith under load, the ID-P9-11 shape 2026-09-29 06:47:11 -04:00
swung0x48 89f1e6cfba [Test] (P11 M): P4aFinalFixScenario expects no re-mint count on the monolith arm either - dev's 568090f0 keeps an immutable, unwidened allocation there as the server already did 2026-09-29 06:47:10 -04:00
swung0x48 2c7d99d5e8 [Test] (P11 M): the server half of a split eglSwapInterval - Magma's backend object carries the request into its table's SetSwapInterval (null before dev@c60639db) 2026-09-29 06:33:36 -04:00
swung0x48 9cde08064c [Test] (P11 M): dev's ImageBindableRemint and PartialUploadOverGpuContent scenarios on the Split/Spawn/Tcp arms of both backends, three scattered-upload cases for the handle arm's carried layout, and TextureRemintPullScenario's monolith Espryt skip retired 2026-09-29 06:30:54 -04:00
swung0x48 48a306025d [Fix] (Espryt, P11 M): the push builds' monolith re-mint merges the applier's pending boxes over the GPU readback - the client clears its dirty flags at emission there, so a pending glTexSubImage was read back over and lost 2026-09-29 06:30:54 -04:00
swung0x48 e0102bad8c [Merge] (P11): dev@c60639db - the Espryt image-bindable re-mint reads GPU levels back on the monolith arm too, partial writes into converted or reshaped levels upload only their region on every arm, Magma orders an upload after the recording's writes and queued clears, and Magma's eglSwapInterval reaches the swapchain 2026-09-29 06:15:56 -04:00
swung0x48 ffeed0da3a [Docs] (P11): the re-scoped plan - each roadmap item against the tree at 74ae652e; A makes the adopt tier honest (client-side verdict, the Stream refusal, a tier assertion, three arms) and re-runs open question 3 and the baseline on the device, B (T0) is conditional, C closes by docs 2026-09-29 05:52:51 -04:00
swung0x48 86dc38a07e [Fix] (Magma): eglSwapInterval reaches the swapchain - 0 picks IMMEDIATE, else MAILBOX, else FIFO; >= 1 FIFO; < 0 FIFO_RELAXED, else FIFO; rebuilt after the next present when the mode changes, and an ended server session forgets its interval (MC 26.3 inproc vsync on: flat 120) 2026-09-29 05:52:22 -04:00
swung0x48 c60639db1d [Fix] (Magma): eglSwapInterval reaches the swapchain - 0 picks IMMEDIATE, else MAILBOX, else FIFO; >= 1 FIFO; < 0 FIFO_RELAXED, else FIFO; rebuilt after the next present when the mode changes (MC 26.3 vsync on: 190-290 fps -> 120) 2026-09-29 05:51:18 -04:00
swung0x48 74ae652ecd [Docs] (P10): close the phase - CONTRACT-P10 sections 2-3, rulings ID-P10-1..14 with the integration gate, C-MEASUREMENTS (device fence bench, loopback credit 1/2/3, the retrace round-trip census), the roadmap row, the debts P10 hands on, design/09, the phase README and the status page 2026-09-29 05:29:00 -04:00
swung0x48 414dd09bda [Test] (P10): the fence bench comment - the replay corpus does carry timed fence waits (improved-transparency-minecraft-26.3), the bench isolates the shape 2026-09-29 05:29:00 -04:00
swung0x48 47bbf5e6fd [Docs] (P10 A): CONTRACT-P10 section 1 - the fence-report protocol, the present-time report, the gates and red-once - with the re-scoped plan and A-FENCE-POLL, the paired host bench and the llvmpipe finding 2026-09-29 04:34:16 -04:00
swung0x48 73089cf2b3 [Test] (P10 B): the caps mirror's parallel-shader-compile entry is the client's switch in both directions - the extension-list read-back case pins compilation inline, and a new case holds the reconcile (red once with it removed) 2026-09-29 04:34:02 -04:00
swung0x48 80d205c401 [Refactor] (Remote, P10 B): the emit table's SetSwapInterval slot is the server forwarder - SetSwapInterval_Unmigrated and the BackendObject_Remote override deleted, class C empty (69 emitted / 0 unmigrated), and a unit control that the slot crosses as one control frame 2026-09-29 04:34:02 -04:00
swung0x48 4373964443 [Test] (P10 B): AsyncCompileScenario (AsyncOn/AsyncOff/OptimisticShaderStatus), XfbPrimitiveQueryScenario and PrimitivesGeneratedNoXfbScenario on the split arms of both backends - Espryt Split/Spawn/Tcp, Magma Split/Spawn/Tcp gated, PrimGenReroute split+spawn only 2026-09-29 04:34:02 -04:00
swung0x48 2271f6c1ec [Fix] (Remote, P10 B): GL_KHR_parallel_shader_compile follows the client's compile switch - the caps mirror re-derives it on adoption, so a tcp server started with another MOBILEGL_ASYNC_SHADER_COMPILE no longer decides the client's string 2026-09-29 04:34:02 -04:00
swung0x48 8d0a1b6ca5 [Test] (P10 A): PacedFrameFenceWaitsBench - the per-frame timed wait on an older frame's fence as an opt-in measurement (MGITEST_FENCE_PACE_BENCH, _LAG) on both backends 2026-09-29 04:29:49 -04:00
swung0x48 625f4db47d [Feat] (MG_Remote, P10 A): a present reports the fences finished so far before it returns the credit, so the client the credit releases finds the fence of a frame ago already known 2026-09-29 04:29:49 -04:00
swung0x48 b1d60523e4 [Fix] (Magma): an upload into an image the open recording already wrote, or that still has a queued clear, is submitted after that work - a glTexSubImage after a clear or a draw into the level no longer ends up under it 2026-09-29 04:28:55 -04:00
swung0x48 8966d5c294 [Fix] (Espryt, Magma): a partial write into a level whose upload is converted (widened RGB formats, Magma's RGB->RGBA expansion) or reshaped (Espryt 1D arrays) uploads only its dirty region - the rest of the stale shadow no longer lands over GPU-drawn texels 2026-09-29 04:28:22 -04:00
swung0x48 bcff9b00cc [Feat] (MG_Remote): fence polls are answered from the server's reports - the apply thread posts kEventFenceSignaled as the GPU finishes each client fence (and submits and re-checks while idle), glClientWaitSync(0) / glGetSynciv answer locally, and only every MOBILEGL_IPC_POLL_ESCALATE-th unanswered poll crosses 2026-09-29 03:59:44 -04:00
swung0x48 de33d40173 [Docs] (P9): close the phase - the contract's remint section and invariants, rulings ID-P9-11..12 with the integration gate, the roadmap row, the debts P9 hands on, the phase README and the status page 2026-09-29 03:36:45 -04:00
swung0x48 7aa00761f8 [Docs] (P9 W2): W2-REMINT - no pull protocol, the census, the coverage matrix and the red-once record; design/04 8.4, the P5c/P5e contract amendments, open question 2 and the P7 census attribution 2026-09-29 03:24:03 -04:00
swung0x48 aa0c242b51 [Test] (P9 W2): TextureRemintPullScenario - fourteen re-mint cases on both backends x monolith/Split/Spawn/Tcp, and the forced-readback-failure fallback entries 2026-09-29 03:23:54 -04:00
swung0x48 6c2f8a560c [Fix] (Espryt, P9 W2): a texture re-mint never needs the client - read back only levels the driver holds, replay the staged level when the readback is refused, count trp on the server path, and retire the texture-remint-pull markers and OnTexturePullRequest (callbacks 9 -> 8) 2026-09-29 03:23:54 -04:00
swung0x48 568090f09f [Fix] (Espryt): the image-bindable re-mint reads every level back from the driver texture before replaying it, so GPU-written levels survive the first glBindImageTexture - iris-derivative SSIM 0.996196552 -> 0.996376198 2026-09-29 03:06:45 -04:00
swung0x48 738b29f3e4 [Docs] (P9): the contract's first three sections (pack-buffer readback, graded log forwarding, the glGetError observation point), both fault injections, the W1 note with its red-once evidence, and rulings ID-P9-1..10 2026-09-29 03:03:03 -04:00
swung0x48 a7a82ffee5 [Test] (MG_Remote): P9 W3 - LogForwardTest leaves no role logs in /tmp on a green run 2026-09-29 02:42:20 -04:00
swung0x48 24560ffe81 [Fix] (MG_Remote): P9 W3 - a FATAL forward waits once per stall rather than once per line (MGLOG_F also marks survivable conditions), and F2 proves the flood got through on a live connection, inside TCP_USER_TIMEOUT 2026-09-29 02:42:20 -04:00
swung0x48 6bcfb1fba1 [Feat] (MG_Remote): P9 W3 - the server's log forward carries each line's level and never blocks a logging thread (<=WARN dropped and counted, >=ERROR lossless and paced, FATAL a bounded flush), fault injection F2, and the producerless OnLog callback deleted (9 -> 8) 2026-09-29 02:42:20 -04:00
swung0x48 362daafeed [Fix] (MG_Remote): the pack-buffer readbacks check their whole shape before the read and refuse it at one latch site per sink, each with a raw-peer control, and a verb counts as statically barriered only when every op that stamps it waits 2026-09-29 02:21:36 -04:00
swung0x48 6485ae1dc0 [Feat] (MG_Remote): a read into a pixel pack buffer crosses as read_pixels_to_buffer / get_texture_image_to_buffer and waits for nothing - the server lands the rows through the buffer-write gate on both backends, the client marks the buffer GPU-written, and a device lost with the write still owed ends in Fatal{ReadbackDeclined, "buffer-writeback"} 2026-09-29 02:02:29 -04:00
swung0x48 ad705a7392 [Docs] (P9): the implementation hand-off - the roadmap row checked against the tree (half of it is done, one design premise changed), five work packages and two fault injections 2026-09-29 01:23:58 -04:00
swung0x48 acfd367852 [Docs] (P12): the FCL fork's keep-running setting is committed and pushed 2026-09-29 01:10:19 -04:00
swung0x48 743f4e8e6e [Docs] (P12): close the phase - the contract, the disposition of every leftover item, a G1 line that only compares same-machine builds, and the doc citation the docs reshuffle broke 2026-09-29 01:05:45 -04:00
swung0x48 6e35ea2c02 [Docs] (P12): the portrait-window clipping was the 0x0 window request, not Magma - root cause, fix and evidence 2026-09-29 00:54:46 -04:00
swung0x48 c932af7f3c [Fix] (EGLImpl): a server-owned window surface with no EGL_WIDTH/HEIGHT asks for the native window's size, so a landscape game is no longer clipped in a portrait server window 2026-09-29 00:54:46 -04:00
swung0x48 8a71ea5954 [Docs] (P12): exit gate (a) passes - FCL's Minecraft renders into the render server window on both backends and killing the server latches device-lost in tens of milliseconds 2026-09-29 00:27:29 -04:00
swung0x48 984e2b69f4 [Feat] (tools): the FCL exit-gate driver - Minecraft renders into the render server window, then the server is killed 2026-09-29 00:27:28 -04:00
swung0x48 f2ad2fae9b [Fix] (MG_Remote): once the device-lost latch has spoken, declined verbs no longer log one ERROR line each - an FCL game wrote 113,080 of them in 48 s 2026-09-29 00:27:28 -04:00
swung0x48 27cbc53241 [Docs] (P12): exit gate (b) passes - a WSL client replays the in-world trace over Wi-Fi TCP at ssim 0.999883; the load frame is bandwidth-bound, the steady state is latency-bound 2026-09-28 23:22:06 -04:00
swung0x48 b60da6729d [Feat] (tools): the cross-host acceptance driver and its report reducer, and a note that netbench's nc sink understates the link 2026-09-28 23:22:05 -04:00
swung0x48 b4f0c53426 [Docs] (Disaggregated): the cross-host eglMakeCurrent hang was the host's xray TUN proxy stalling its relay, not MobileGL; direct Wi-Fi runs pass and are bandwidth-bound 2026-09-28 22:57:20 -04:00
swung0x48 429b4472bb [Fix] (tools): cross-host runs first prove the WSL -> phone TCP path is end to end, and netbench's pkill no longer kills its own shell 2026-09-28 22:57:20 -04:00
swung0x48 221a8144c2 [Docs] (Disaggregated): sync stage progress and roadmap with the five-arm perf comparison and the cross-host hang blocker 2026-09-28 22:33:53 -04:00
swung0x48 1987bdff5c [Feat] (Android): the render server screen and binary ship in the plugin APK too, not just trace 2026-09-28 22:10:56 -04:00
swung0x48 2fa58940e4 [Fix] (ci): the trace APK is a launchable app; assert exactly one launcher activity
The "Trace APK must not expose a launcher activity" check predates the render-server
screen: 1f72597dd gave the trace APK a launcher on purpose and 884f6aac9 moved the entry
icon back to PostActivity with a bottom navigation bar between the two screens, so the
apk.yml build job has been red ever since - taking the whole retrace matrix down with it
(skipped, not failed), which hid the distinction.

What is left of the old isolation contract is the count: exactly one
android.intent.action.MAIN in the merged manifest (a second one means a screen grew a
launcher entry by accident), and it belongs to PostActivity. The fclPlugin metadata ban is
unchanged.
2026-09-28 21:54:16 -04:00
swung0x48 35dddc0c89 [Fix] (MG_Remote): the socket-io stats are LinkMetrics free functions, not StreamLink members
6bccf98d7 put TakeSendStats/TakeReadStats on the concrete StreamLink class, so
ClientSession.cpp and ServerLoop.cpp had to include <MG_Remote/Transport/StreamLink.h> -
which the P6.5 seam gate (scripts/ci/link_seam_purity.py, "concrete link included above
Transport") rejects, and the MGPipe generators and hygiene gates job has been red since.

The counters stay in StreamLink.cpp beside the recv/sendmsg loops that feed them; only the
struct and the two Take functions move, to LinkMetrics.h as LinkIoStats +
LinkMetricsTakeReadStats/TakeSendStats. The readers drop the concrete include; the hot
loops are untouched.
2026-09-28 21:54:08 -04:00
swung0x48 3bdd565dc6 [Chore] (docs): Remove orphaned CS_Refactor notes accidentally committed in 4e48702f2 2026-09-28 21:16:35 -04:00
swung0x48 884f6aac96 [Feat] (Android): trace APK boots to POST; bottom nav bar switches to the render server
The trace flavor manifest removed PostActivity and made ServerControlActivity
the launcher, so the APK opened straight into the render server screen. Keep
both screens: PostActivity is the launcher again, and a bottom navigation bar
(NavBar, plugin flavor unaffected) switches between them via
FLAG_ACTIVITY_REORDER_TO_FRONT with transitions suppressed, so each screen
keeps its state. ServerControlActivity is singleTop now and handles extras in
onNewIntent so adb-driven starts still work. The POST screen also shows an
indeterminate progress bar while the self-test runs.
2026-09-28 21:06:44 -04:00
swung0x48 b9a89967b0 [Docs] (Disaggregated): five-arm transport perf comparison and bottleneck attribution
rd12/openra on-device matrix (monolith/inproc/spawn+shm/spawn+tcp),
tcp-localhost transport_wait decomposition (present credit=1 ~74% +
futex wakeup tax; SPIN_US=2000 alone 2.9x), cross-host control-plane
hang evidence chain blocking P12 acceptance B, Wi-Fi bandwidth ceiling,
and the prioritized knob/code optimization list.
2026-09-28 20:55:45 -04:00
swung0x48 141227e8ba [Feat] (tools): add the disaggregated-mode device perf matrix and profiling harness
run_matrix.sh drives interleaved monolith/inproc/spawn+shm/spawn+tcp
benchmark rounds on an adb device with per-run arm-marker verification;
profile_matrix.sh layers simpleperf, ftrace wakeup capture and a /proc
sampler on top; make_report.py aggregates runs into REPORT.md. The wifi
kit carries the cross-host control-plane hang repro and phone-side
strace/fd-reconcile helpers. .trace-work/ (run output) is now ignored.
2026-09-28 19:36:30 -04:00
swung0x48 930c29e9e0 [Feat] (Android): the render server screen takes intent extras for prefill and autostart
listen/token/env/onscreen prefill the form, --ez start true starts the
server, --ez stop true stops it - adb drives the screen without the IME,
which is how the on-screen unix path (@name + shm, ServerOwned window)
was verified end to end from the UI.
2026-09-27 08:36:57 -04:00
swung0x48 c16fb5a8ac [Feat] (MG_Remote): the in-process display server serves unix endpoints too
mobilegl_server_serve_inprocess refused anything but tcp:// because its
only supervisor was the TCP one's thread hand-off. A unix endpoint
(@abstract name or filesystem path) now runs a supervisor of the same
shape minus the TCP exposure machinery: AcceptPair pairs a client's
control and SCM_RIGHTS connections by arrival order, there is no
DataBind to route and no pre-auth gate, sessions run on threads one at
a time with Busy for a second client, the latch resets between sessions
and g_inProcessStop ends it. That gives the on-screen server the
on-device fast path: ServerOwned windows over a unix socket with shm
data, no TCP copies.

The control screen's tcp-only guard for the on-screen mode goes with
the refusal it mirrored.
2026-09-27 07:43:56 -04:00
swung0x48 e3e890ed5d [Feat] (Trace): the replay runner builds as an adb-shell executable for Android, and unix endpoints carry a headless window
MOBILEGL_BUILD_TRACE_REPLAY now also produces mobilegl_trace_replay on
ANDROID: the same CLI as desktop, linked against the APK runner's
glws/glproc glue. On the device it dials a render server with no
Activity at all - MOBILEGL_IPC_CONTROL=unix:@name with shm data to the
offscreen supervisor (a cross-uid abstract connect needs root or an
SELinux waiver; loopback TCP works from plain adb shell), or
tcp:// with MOBILEGL_IPC_SURFACE=server to the on-screen display server.

glws_android grows the desktop glws_egl's server-owned null-window
createWindowSurface, gated on MOBILEGL_TRACE_SURFACE=window so the APK's
pbuffer mode keeps its old answer, and the config carries EGL_WINDOW_BIT
for it. The control screen refuses unix endpoints in on-screen mode:
the in-process display server serves tcp:// only.

Verified on-device with an OpenRA retrace, ssim=1.0 both ways:
offscreen service on @mgl-cli with the CLI as root, and the display
Activity on tcp://127.0.0.1:40613 with the replay's frames on the
phone's screen.
2026-09-27 06:28:00 -04:00
swung0x48 bf6feebb22 [Perf] (MG_Remote): The producer's transport park is timed as its own per-frame number, and a reducer pairs the two role logs into a frame-time breakdown
transport_wait_ns measures wall time in Ring::Wait's flush/spin/sleep, so
the client's parked time is read directly instead of inferred by
subtracting CPU clocks that run concurrently with the server.
frame_stats.py matches client and server present serials into per-frame
avg/p50/p99 splits; the README documents the MOBILEGL_PIPE_STATS=1
workflow.
2026-09-27 04:45:07 -04:00
swung0x48 f10db410e8 [Feat] (Android): the render server screen also starts the on-screen display server
An On-screen window checkbox switches Start from the offscreen service
to MobileGLDisplayActivity (process :mglwin), which serves in its own
process and renders a client's window surface onto its SurfaceView.
Stop ends either shape: stopService for the supervisor, killProcess for
:mglwin, and the running check covers both. Verified end to end with an
OpenRA retrace: the client dialed tcp://127.0.0.1:40613 with
MOBILEGL_IPC_SURFACE=server from a freeform window while the display
Activity held the screen, frames landed on the SurfaceView and the run
passed with ssim=1.0.
2026-09-27 04:39:01 -04:00
swung0x48 aaa50c88e1 [Feat] (Android): the render server screen takes unix endpoints, and is titled for what it serves
The endpoint field accepts @abstract names and filesystem paths beside
tcp://, matching what mobilegl_server_main's Listen already binds - on
Android the abstract namespace is the on-device rendezvous that always
works, and a relative path lands in the app's files dir, the service's
working directory. Unix endpoints skip the token rule (same-device
reach by construction) and are probed with LocalSocket instead of a
TCP connect. The screen is now titled MobileGL Render Server.
2026-09-27 04:39:00 -04:00
swung0x48 1f72597dd8 [Feat] (Android): a launcher screen starts and stops the offscreen TCP server
ServerControlActivity (trace flavor) collects the listen endpoint, auth
token and extra env, then drives MobileGLServerService with the same
extras adb used. Non-loopback listens are held to the 16-byte token the
server enforces, and a connect probe confirms the supervisor bound its
socket. The screen is the APK's launcher icon, which the trace flavor
lacked since PostActivity is removed there.
2026-09-27 04:39:00 -04:00
2257 changed files with 211621 additions and 386035 deletions
@@ -0,0 +1,3 @@
# Everything here is executed by bash/sh on Linux, Android or WSL: keep LF even on a Windows checkout
# (core.autocrlf=true would otherwise hand sh a script full of $'\r').
* text=auto eol=lf
@@ -0,0 +1,183 @@
---
name: anland-mobilegl-plasma
description: Bring up, rebuild, deploy, switch backends on, test and debug the "MobileGL + anland + KDE Plasma" stack - a rooted Android tablet whose anland APK embeds the MobileGL server, plus a Droidspaces Arch Linux ARM container running KWin/plasmashell/Chrome through the MobileGL client library and GBM backend - driven from this Windows machine (Git Bash, WSL archlinux, adb). Use it after a phone reboot, when changing server (NDK) or client (container glibc) MobileGL code or anland's KWin backend/patch, when switching Espryt/Magma, when validating zero-copy dma-buf presentation, and when chasing crashes, black windows, noise or software-rendering fallbacks in the Plasma session.
---
# MobileGL on anland KDE Plasma
**Setting this up on a new machine or phone from source: `reproduce.md` next to this file.**
Human-readable runbook (Chinese, same content): `docs/archive/notes/anland/runbook-plasma.md`.
Background: `docs/archive/notes/anland/handoff-legacy-mobilegl-unified.md` (state, commits, known
issues) and `plan-ahb-dmabuf.md` (zero-copy design). All scripts are in `scripts/` next to this file;
parameters are documented at the top of each script and in `scripts/env.sh`.
**The phone may be shared with another session.** Before anything that restarts the app, the daemon,
the container session or replaces a library, run the read-only `scripts/device/status.sh` and make sure
nobody else is mid-test. Use your own `ct.sh` channel names (others use c, c2, c3, xd, xr, gbm).
## Architecture (10 lines)
1. Phone: Lenovo TB321FU (Y700, Adreno 750, Android 15, kernel 6.1), rooted with **KernelSU** (`/data/adb/ksu`, `ksud`; `su -c` works). USB serial `HA27Q3LQ`, or wireless adb `ip:port`.
2. anland APK `com.anland.consumer.mobilegl` (debuggable build of anland branch `legacy-mobilegl-unified`): UI process + private service process `:mobilegl` (`MobileGLWorker`), which loads the **server** `libMobileGL.so` (NDK, from MobileGL `feat/disaggregated`) and listens on abstract socket `@anland-mobilegl` from the first attached window on. One process serves every client (KWin, plasmashell, ksplash, Chrome...), one backend per process. `MobileGLWorker` is a **foreground service** (notification "Linux desktop is running" / action "Stop desktop"), so the server and the session outlive a hidden, locked or swiped-away window.
3. Backend = the app setting "Renderer backend" (Settings > Connection > MobileGL desktop; file `/data/data/<pkg>/files/mobilegl-backend`, default DirectGLES; also `am start ... --es mobilegl_backend X`). Espryt = DirectGLES, Magma = DirectVulkan. The server reads it when its process starts; the app publishes it as `/data/local/tmp/anland-mobilegl/backend` and the session copies that into the container's `/etc/mobilegl/backend` (what every client reports; mismatch = "Hello.backendType disagrees with pinned backend"). `debug.mobilegl.backend`, if someone sets it, still overrides the setting until the next reboot.
4. Experiment display daemon `/data/adb/modules/anland-daemon/display_daemon /data/local/tmp/anland-mobilegl/display.sock` (display/input broker), started by the app (below). The module only starts the original one on `/data/local/tmp/display_daemon.sock` at boot; leave that alone.
5. Container `arch-kde-mgl` (Droidspaces 6.4.5, Arch Linux ARM aarch64 glibc, host net, `rootfs.img` mounted at `/mnt/Droidspaces/arch-kde-mgl` on the Android side). Bind mounts: `/data/local/tmp/anland-mobilegl` -> `/run/anland-mobilegl`, `/data/local/tmp/display_daemon.sock` -> `/run/display.sock`.
6. `desktop-session.service` (User=swung0x48, **enabled**: the container boots straight into it, as the original image does) has drop-in `/etc/systemd/system/desktop-session.service.d/mobilegl.conf` (anland `producers/kde/Arch_v5/desktop-session-mobilegl.conf`, installed + enabled by `sync-build-kwin.sh`): `ExecStartPre=mobilegl-startup.sh wait` (until `@anland-mobilegl` listens in `/proc/net/unix` and the daemon socket exists), `ExecStartPre=+... sync-backend`, `Restart=always`, then `ExecStart=/opt/mobilegl/bin/mobilegl-startup.sh plasma`, which deletes the sticky software-renderer key, sets `MOBILEGL_IPC_PROBE_TIMEOUT_MS=0` session-wide (MobileGL never declines, so glvnd never falls through to another GL stack), writes a user drop-in for `plasma-kwin_wayland.service` (kwin_wayland_wrapper -> `/opt/mobilegl/kwin/bin/kwin_wayland` + `/opt/mobilegl/kwin/lib/libkwin.so.6.7.4`, `ANLAND_MOBILEGL=1`, `MOBILEGL_IPC_SURFACE=server`) and runs `startplasma-wayland`.
7. KWin 6.7.4 with anland's `anland` backend renders straight into the anland Android Surface through the server. While the window is not on screen (consumer disconnected) the backend powers its output down (DPMS Off + RenderLoop inhibited: no compositing, no frame callbacks) and back up when the window returns. Every other GL process is `MOBILEGL_IPC_SURFACE=offscreen` and presents through KWin.
8. Client = `/opt/mobilegl/lib/libMobileGL.so` (glibc aarch64), the container's **system-wide** GL vendor (section below): glvnd EGL vendor `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json` (sorts before the distribution's `50_*.json`, which serves whenever MobileGL declines), GLX vendor `/usr/lib/libGLX_mobilegl.so.0`, settings from `/etc/mobilegl/client.conf` (`MOBILEGL_TRANSPORT=spawn MOBILEGL_IPC_DATA=shm MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl`) + `/etc/mobilegl/backend`. No per-process environment is needed.
9. Zero-copy windows: client `wl_egl_window` frames are server-allocated AHardwareBuffer "shared images" exported as dma-buf fds over `zwp_linux_dmabuf_v1`; KWin imports them with EGL and the server recognises its own buffer by inode. KWin gets a DRM device via `GBM_BACKEND=mobilegl` -> `/usr/lib/gbm/mobilegl_gbm.so` (node is only an identity, never ioctl'd). Without linux-dmabuf the client falls back to wl_shm readback.
10. X11 (`docs/archive/notes/anland/plan-x11-gpu.md`): Xwayland draws with **glamor on MobileGL** (an offscreen session; `GBM_BACKEND=mobilegl` on KWin's render node; window pixmaps are shared images handed to KWin as linux-dmabuf buffers; its `glFlush` publishes them through the server, `/tmp/mgl-xwayland.client.log`); `ANLAND_XWAYLAND_GLAMOR=0` in KWin's drop-in brings back the software `-shm` Xwayland. GLX windows present their frames as shared images through **DRI3 + Present** (log `presented through DRI3+Present shared images`), falling back to readback + MIT-SHM / `xcb_put_image` when the X server has no DRI3 (`MOBILEGL_GLX_PRESENT=readback` forces it per app). Chrome runs ANGLE-GLES over MobileGL EGL with the GPU in its own process (`mobilegl-startup.sh chrome`; `MOBILEGL_CHROME_GPU=in-process` opts out), presenting GBM dma-bufs over linux-dmabuf; after a device loss Chrome restarts that process and repaints.
## First-time prerequisites (Windows host)
- Git Bash, `adb` (SDK platform-tools), CMake >= 3.22 and Ninja on PATH (seen: CMake 3.27.6, Ninja 1.13.2), Python 3 + Pillow (only `rec.sh`).
- Android NDK **27.2.12479018** at `%LOCALAPPDATA%/Android/Sdk/ndk/27.2.12479018` (override `ANDROID_NDK`).
- WSL distro **archlinux** with clang, lld, llvm, cmake, ninja. **lld's major version must equal clang/llvm-libs'** (lld 23 on llvm 22 broke `ld.lld`; working set: all 22.1.6). Then export the sysroot once: `bash scripts/xsysroot/refresh-sysroot.sh` (-> `~/sysroots/arch-kde-mgl` + `~/sysroots/aarch64-arch.cmake`; re-run after container package upgrades, then `xbuild.sh --reconfigure`).
- MobileGL worktree (never build in the shared main checkout): `GIT_LFS_SKIP_SMUDGE=1 git worktree add ...`, `git submodule update --init --recursive`, and copy `3rdparty/glslang/External/spirv-tools` from the main checkout (the submodule leaves it empty).
- anland worktree on `legacy-mobilegl-unified` (KWin patch/backend, startup script, `mobilegl-tools/`).
- Phone side that already exists and is NOT recreated by this skill: the APK (build: anland `consumers/anland_v5/android_consumer/MOBILEGL.md`, gradle `:app:assemblePlainDebug` with `MOBILEGL_DIST`, `-PmobileglApplicationId=com.anland.consumer.mobilegl -PanlandDefaultSocket=/data/local/tmp/anland-mobilegl/display.sock`, from Git Bash with `MSYS_NO_PATHCONV=1`; signed with this machine's `~/.android/debug.keystore`, so `adb install -r` works; reinstalling with another key changes the UID and needs the KernelSU grant again - `mobilegl-tools/ksu_grant.c`), the anland KernelSU modules (`anland-daemon`, `anland-awl`), the container with KWin's configured build tree `/root/mobilegl-build/kwin-6.7.4/build` and pristine `/root/gbm-dev/kwin-a/src`.
- Install the helpers once (container must be running): `bash scripts/push-tools.sh` (device/*.sh -> `/data/local/tmp/anl`, `mgrun` -> container `/usr/local/bin`, `kwprep.sh` -> container `/root`).
Run host scripts from Git Bash as `bash scripts/<x>.sh`. Raw adb with device paths needs `MSYS_NO_PATHCONV=1` and the whole root command in one quoted string: `adb shell 'su -c "..."'`. A device script can also be run without pushing: `MSYS_NO_PATHCONV=1 adb shell 'su -c sh' < scripts/device/status.sh`.
## Starting, idling, stopping - no manual steps
Nothing starts at phone boot (container `run_at_boot=0`). **Opening the app** (launcher icon = `am start -n com.anland.consumer.mobilegl/com.anland.consumer.MainActivity`, no extras; the APK's default socket is the experiment's, `-PanlandDefaultSocket`) calls `MobileGLDesktop.ensureStarted()` - the single trigger - which starts the foreground service and runs the APK's `assets/mobilegl-desktop.sh up` as root (`su`): display daemon if absent (the script leaves the app's cgroup first, so it survives the app) -> backend published -> container started if stopped -> `systemctl start --no-block desktop-session`. The window waits for the daemon socket; the server listens once the Surface is attached; the session's `wait` step then lets Plasma start. A reopen with everything up only reattaches the window. Log: `/data/local/tmp/anland-mobilegl/desktop.log`, logcat `AnlandMobileGL` ("MobileGL desktop up done in N ms").
Hidden window, screen off, lock screen or swiped-away task: server and session stay (foreground service); KWin turns the workspace's DPMS off (logs "viewer gone: output powered down"; windows get xdg_toplevel `suspended`, no frame callbacks) and back on ("viewer back"). Client swaps are paced by frame callbacks (`MG_Impl/EGLImpl/FrameThrottle.h`): interval >= 1 waits like any Wayland EGL; interval 0 (Chrome) drops to ~1 fps once a callback goes unanswered for 200 ms.
Measured 2026-10-03 (Espryt, Chrome on webglsamples aquarium; % of one core, phone = all of Android): open-to-desktop cold 7.5-12.5 s (daemon 0.5, server 1.0, container 1.5, kwin ~5, plasmashell ~6-7), after a phone reboot 8.5 s, reopen of a running desktop 0.2 s; visible phone 249% / kwin 13 / chrome 99 / server 57 / GPU 22%, hidden 54 / 0 / 6 / 2 / 0%, locked 38 / 0 / 8 / 3 / 0%. Stop desktop 1.7 s. Wireless adb does not come back by itself after a reboot (re-enable it on the phone). **Stop desktop** (notification action, or `am startservice -n <pkg>/com.anland.consumer.MobileGLWorker -a com.anland.consumer.mobilegl.STOP`) runs `mobilegl-desktop.sh down` (session + container stopped, daemon stays), closes the windows, ends `:mobilegl`.
```sh
adb connect <ip>:<port> # only for wireless debugging
export MSYS_NO_PATHCONV=1
adb shell 'su -c "sh /data/local/tmp/anl/bringup.sh"' # after a reboot: wake, dismiss keyguard, open the app, print the timeline
bash scripts/desktop-verify.sh push cold reopen idle # timed cold start, swipe-away + reopen, CPU/GPU visible/hidden/locked
adb shell 'su -c "sh /data/local/tmp/anl/status.sh"'
```
`droidspaces` has no `status` command; `droidspaces show` lists running containers. `device/run-plasma.sh` is a developer restart (session + logind user, then the app), needed after a rebuild because KWin does not reconnect to a new server process. `device/desktop-reset.sh` takes everything down to the post-reboot state. The old manual sequence (setprop, start container, `restart-daemon.sh`, `run-plasma.sh`) is gone; `restart-daemon.sh` remains for daemon debugging only.
## Build and deploy loops
**Server (Android, NDK) -> APK lib**
```sh
bash scripts/build-android.sh # -> <worktree>/build-android/libMobileGL.so (RelWithDebInfo, unstripped)
bash scripts/cyc.sh s1 # clear sticky SW-renderer key, push, swap into APK, restart session, screenshot $ANL_DIR/s1.png, print fatals
```
`device/swap.sh` overwrites `/data/app/.../lib/arm64/libMobileGL.so` keeping owner/mode/SELinux context (original saved as `/data/local/tmp/anland-mobilegl/libMobileGL.so.orig`); no re-signing. To swap without the session restart: push to `/data/local/tmp/anland-mobilegl/libMobileGL.new.so`, `su -c "sh /data/local/tmp/anl/swap.sh"`, then `run-plasma.sh` anyway (KWin does not reconnect to a new server process).
**Client + GBM backend (container glibc) via WSL cross-build**
```sh
bash scripts/xbuild.sh [--reconfigure] # WSL ~/mgl-xbuild-a64; ~150 s full, seconds incremental
bash scripts/xdeploy.sh # rename-into-place install of libMobileGL.so + mobilegl_gbm.so, aliases, vendor JSONs, client.conf, GLX/GBM env drop-ins
```
Running clients keep the old inode; restart the client, or `run-plasma.sh` for KWin/plasmashell. Build server and client from the **same commit** (both stamp `git rev-parse HEAD`), else the logs WARN "compatible wire with different build". Fallback in-container build (slow): `scripts/sync-src.sh` + anland `mobilegl-tools/anland-build-client.sh`.
**KWin (anland backend / kwin.patch) - built inside the container**
```sh
bash scripts/kwin-stage.sh build # stage CR-stripped patch+backend+startup script, kwprep.sh, background sync-build-kwin.sh
printf 'tail -5 /root/kwin-build.log\n' | bash scripts/ct.sh kw # wait for "== ALL DONE =="
adb shell 'su -c "sh /data/local/tmp/anl/run-plasma.sh"'
```
`sync-build-kwin.sh` (anland `producers/kde/Arch_v5/`) copies only the backend sources, then `ninja -j4`, installs `kwin_wayland`, `libkwin.so.6.7.4`, the patched screencast plugin and `/opt/mobilegl/bin/mobilegl-startup.sh` by atomic rename. `kwprep.sh` is what applies changed `kwin.patch` hunks to core KWin files. Repo-root `libdisplay_producer/` is not synced by either. `kwin-stage.sh` is assembled from those two scripts and was not run as one unit yet.
**Unit tests (no device)** - in WSL (the MSVC build of this branch is broken):
```sh
MSYS2_ARG_CONV_EXCL='*' wsl -d archlinux -- bash /mnt/c/<worktree>/.claude/skills/anland-mobilegl-plasma/scripts/wsl-test.sh MultiSessionTest TwinOwnershipTest
MSYS2_ARG_CONV_EXCL='*' wsl -d archlinux -- bash -lc 'cd ~/mgl-anl-bld && ctest -R "MultiSession|TwinOwnership" -E DirectVulkan --output-on-failure'
```
`wsl-all.sh` builds and runs every unit test. Never run DirectVulkan integration tests; never two ctest runs at once.
## Switching backends
In the app: Settings > Connection > MobileGL desktop > Renderer backend, then Stop desktop and open the app. Or `adb shell 'su -c "sh /data/local/tmp/anl/switch.sh DirectGLES"'`, which does the same (saves the setting through the launch extra, STOPs the service, reopens; clears a stale `debug.mobilegl.backend`). No setprop, no file to edit in the container: the session copies the published backend into `/etc/mobilegl/backend` on its next start. `cyc.sh` keeps whatever is set.
## System-wide vendor and fallback
Installed by `xdeploy.sh` (and anland's `anland-build-client.sh`), so any process in the container gets MobileGL through the stock libglvnd/libgbm:
| File | Does |
|---|---|
| `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json` | EGL vendor, asked before the system's `50_*.json` |
| `/opt/mobilegl/share/glvnd/egl_vendor.d/50_mobilegl.json` | same JSON, for per-process forcing with `__EGL_VENDOR_LIBRARY_FILENAMES` |
| `/etc/mobilegl/client.conf` | `KEY=value` defaults for every client (transport, data, endpoint); rewritten on deploy |
| `/etc/mobilegl/backend` | the backend (the session's `sync-backend` step copies the app's choice in); wins over a `MOBILEGL_BACKEND_TYPE` line in client.conf |
| `/etc/environment.d/10-mobilegl.conf` | `__GLX_VENDOR_LIBRARY_NAME=mobilegl`, `GBM_BACKEND=mobilegl` for the systemd user manager (all Plasma services and what they start, terminals included) |
| `/etc/profile.d/mobilegl.sh` | the same two for login shells (`bash -l`, `su -l`, ssh, `droidspaces run bash -l`) |
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. 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 \
MOBILEGL_CONFIG_FILE=/tmp/down.conf eglinfo -B -p wayland # vendor = the other one, ~0.1 s
```
## Test clients and how to verify
X11 on the GPU has its own driver, `scripts/x11-verify.sh` (preflight, measure before/after with glxgears at `vblank_mode=0` on both GLX paths (presentation throughput only: glxgears is fixed-function GL, which MobileGL does not implement, so its window is black by design), deploy-client/-kwin/-server, check, apps, stop); `scripts/device/x11-measure.sh` refuses to load the phone below 40% battery.
Inside the container as root (`printf '...' | bash scripts/ct.sh <chan>`), `mgrun offscreen <cmd>` runs a client as swung0x48 in the live session through a login shell, with nothing but the surface mode and the log path set (log `/tmp/mgl-run.client.log`, override base with `MGLOG=/tmp/x.log`); a plain `env -i ... bash -l -c '<cmd>'` as swung0x48 gets MobileGL the same way:
```sh
mgrun offscreen eglinfo -B ; mgrun offscreen glxinfo -B
timeout 25 mgrun offscreen glmark2-es2-wayland -s 1280x720 --run-forever > /tmp/gm.out 2>&1 &
sleep 8; grep -a "presented through" /tmp/mgl-run.client.log
```
Chrome: `bash scripts/chrome-launch.sh <tag> [flags]` (log `/tmp/mgl-chrome-<tag>.client.log`, screenshot `ch-<tag>.png`; `CHROME_GPU=in-process` puts the GPU back in the browser, which cannot recover from a device loss).
Video: Chrome decodes in hardware. The GPU process's render node has a VA-API driver in the container (`/usr/lib/dri/msm_drm_drv_video.so`, DroidSpaces' V4L2 driver): Chrome allocates its frames with `vaCreateSurfaces` and exports them (`vaExportSurfaceHandle`, one fd, R8 + GR88 layers) - the driver's own buffers, never GBM ones - and imports each as an NV12 (P010 for 10-bit) EGLImage bound to `GL_TEXTURE_EXTERNAL_OES`. MobileGL takes such a FOREIGN YUV dma-buf through its labelled CPU-copy fallback: the server maps it and copies it into a YUV AHardwareBuffer of its own (`AHardwareBuffer_lockPlanes`) at the reader's first use in each frame (any present, publishing flush or native fence), then converts that into the texture's RGBA level on the GPU (Espryt: `GL_EXT_YUV_target` with the import's colour hints, `MOBILEGL_YUV_DRIVER_CONVERSION=1` for the driver's external sampler; Magma: `VkSamplerYcbcrConversion` on the AHB's external format). A server-allocated NV12/P010 buffer (GBM `gbm_bo_create(NV12)`) needs no copy. `MOBILEGL_CHROME_VIDEO_DECODE=software` in `mobilegl-startup.sh chrome` brings back `--disable-accelerated-video-decode`. Check a video with `scripts/probes/cdpmedia.py <port> <s>` in the container (CDP `Media.enable`: `kVideoDecoderName = VaapiVideoDecoder`, `kIsPlatformVideoDecoder = true`; `CDP_NAVIGATE=<url>` / `CDP_EXPR=<js>`), which needs Chrome started with `--remote-debugging-port` and a non-default `--user-data-dir` (a fresh profile also needs `--password-store=basic`, else page loads wait forever on KWallet), and grep Chrome's log for `exit_code` (a GPU-process restart). `scripts/probes/yuvprobe.c` pixel-checks foreign NV12/P010 imports, their per-frame refresh and a GBM NV12 buffer (`mgrun offscreen mgl-yuvprobe`, exit 0 = pass). KWin's own NV12 path (it splits a YUV buffer into R8 + GR88 imports) is not served: MobileGL imports no single-channel dma-bufs, so KWin advertises no YUV format over linux-dmabuf. A vendor-agnostic replacement driver (MobileVA: VA-API on Android MediaCodec, decoding into server YUV images with no CPU copy, the current driver kept as an A/B fallback) is planned, not built: `docs/MobileVA/plan.md`.
Device loss: `setprop debug.mobilegl.inject_device_lost_pid <pid>` (as root) loses the server session of the client whose pid - as it sees itself in the container - is `<pid>`, once per value, on any backend and whether or not it presents; `debug.mobilegl.inject_device_lost_at N` instead loses whichever session makes the N-th device check (Espryt presents and read-backs only). `bash scripts/chrome-recover.sh <in-process|process> [rounds]` checks that Chrome repaints after such a loss without a restart (battery-guarded, stops below 40%). `bash scripts/kwin-recover.sh [rounds]` loses KWin's own session (default twice) and checks that KWin restarts compositing on a fresh session - same KWin, plasmashell and client processes, the screen still animating (glmark2 on Wayland, glxgears on X11, `CHROME=1` adds Chrome) - which needs the anland backend + kwin.patch reset handling deployed (`kwin-stage.sh build`).
Probes: `scripts/probes/texfmt.c` (texture formats keep alpha?), `dmahash.c` (hash a process's dma-bufs), `yuvprobe.c` (YUV dma-buf sampling, `gcc -O1 /root/yuvprobe.c -o /usr/local/bin/mgl-yuvprobe -lEGL -lGLESv2 -lgbm -lm`), `cdpmedia.py` (which decoder a Chrome page uses); build in the container, e.g. `gcc -O1 /root/texfmt.c -o /usr/local/bin/mgl-texfmt -lEGL`, run with `mgrun offscreen`.
GPU hangs: a shader that never ends stops the WHOLE GPU (every process, MobileGL or not) until the kernel resets it - ~2 s, or ~10 s ("Preemption Fault" in dmesg) when a higher-priority context waits; nothing in user space shortens one stall. What the server does is end the guilty session so it cannot hang the GPU again (Magma: event-bracketed submissions + `mgl-gpu-watch`, budget `MOBILEGL_GPU_HANG_BUDGET_MS` / `debug.mobilegl.gpu_hang_budget_ms`, default 2000; Espryt: the driver's GUILTY reset status, which it reports after the ~2 s reset only). Repro: `scripts/probes/hangclient.c` (Wayland + GL 3.3 core; `-H 60 -N 1 -E 60` hangs every 60 frames, `-H -1` is a frame-gap observer; build with wayland-scanner's xdg-shell files, `-lwayland-client -lwayland-egl -lEGL -lOpenGL -lm`), and `gpuprobe.c` (NDK, plain adb shell; `gpuprobe 30 high` is a high-priority GLES context outside MobileGL - it both measures the device-wide stall and forces the 10 s preemption path).
A result counts only if ALL hold:
- client log says `presented through linux-dmabuf shared images` (not `wl_shm`; `could not be presented through a linux-dmabuf shared image` = fell back);
- `bash scripts/shot.sh g 2 1` gives two different hashes while the client animates (a "fast" score can mean frames are not shown);
- `adb logcat -b crash -d` and `adb logcat -d | grep -E "Fatal signal|Abort message|E MobileGL"` are clean (a frozen window is usually a dead server);
- for plasmashell verdicts, the sticky software renderer is off (below) and `/tmp/mgl-plasmashell.client.log` is freshly written.
Reference (glmark2-es2-wayland 1280x720, zero-copy vs wl_shm): Espryt 187 vs 49, Magma 158 vs 34. Rerun any surprising pass.
## Logs
| What | Where |
|---|---|
| KWin client side | container `/tmp/mobilegl-compositor.client.log` |
| plasmashell / ksplash | `/tmp/mgl-plasmashell.client.log`, `/tmp/mgl-ksplash.client.log` (user drop-ins `~/.config/systemd/user/plasma-{plasmashell,ksplash}.service.d/mobilegl-log.conf`) |
| mgrun / Chrome | `/tmp/mgl-run.client.log`, `/tmp/mgl-chrome-<tag>.client.log` (+ `/tmp/chrome-<tag>-out.log`) |
| Server | `adb logcat -s MobileGL`; file `/data/data/com.anland.consumer.mobilegl/files/mobilegl-server.server.log` |
| Native crashes | `adb logcat -b crash -d`; container `coredumpctl list` |
| Session / KWin stderr | container `journalctl -u desktop-session --no-pager`, `journalctl --no-pager --since -2min \| grep kwin_wayland` |
| Display daemon | `/data/local/tmp/anland-mobilegl/display-daemon.log` |
| Desktop start/stop (app's su script) | `/data/local/tmp/anland-mobilegl/desktop.log`, `container-start.log`; logcat `-s AnlandMobileGL` |
| Session wait / backend sync | container `journalctl -u desktop-session --no-pager` ("waiting for the MobileGL server ...") |
`MOBILEGL_LOG_FILE_PATH=/tmp/x.log` produces `/tmp/x.client.log` (and `.server.log` for forwarded lines); files are truncated on open, so one base per process. Client builds log at INFO. Symbolize server frames with `$ANDROID_NDK/toolchains/llvm/prebuilt/windows-x86_64/bin/llvm-addr2line.exe -Cfe build-android/libMobileGL.so <offset>`.
## Pitfalls
- **pkill -f self-match**: a `ct.sh`/`droidspaces run bash -lc "<script>"` process carries the script in its argv; `pkill -f chrome` kills the runner. Use `pkill -x`.
- **Root-owned /tmp files**: a log created by a root-run client cannot be reopened by swung0x48 later (no log appears). `rm -f` it as root first.
- **Sticky Plasma software renderer**: after any server crash Plasma writes `[QtQuickRendererSettings] SceneGraphBackend=software` into `~swung0x48/.config/kdeglobals`; plasmashell then renders on the CPU and screenshots still look fine (tray icon "正在使用的软件渲染器"). `mobilegl-startup.sh plasma` deletes it at every session start; within a running session that hit a crash, restart the session (`run-plasma.sh`) before any verdict.
- **Lock screen**: autolock is off (`kscreenlockerrc [Daemon] Autolock=false`); a real lock can only be left by typing the password (logind lock/unlock is not supported for this session type - `device/unlock.sh` is unverified). Test the greeter with `/usr/lib/kscreenlocker_greet --testing` and kill it.
- **CRLF**: `core.autocrlf=true` here; anland's backend sources and `sync-build-kwin.sh` are CRLF in the Windows checkout. Strip CR before anything runs on Linux/Android (`tr -d '\r'`; `ct.sh`, `push-tools.sh`, `kwin-stage.sh` do). This skill has a `.gitattributes` forcing LF.
- **Display daemon**: the app starts it; the session's `wait` step keeps KWin from starting without it (a KWin that cannot reach it logs "failed to connect to display daemon at /run/anland-mobilegl/display.sock" and exits in ~3 s). Anything started from the app's `su` must leave the app's cgroup (`echo $$ > /sys/fs/cgroup/cgroup.procs`) or a force-stop kills it.
- **Device power/connection**: low battery or the screen turning off (30 s default) can kill the SurfaceView - `svc power stayon true`; wireless adb `ip:port` changes per pairing - never pin `-s HA27Q3LQ`, use `ANDROID_SERIAL` if several devices are attached.
- **Container vs phone paths**: container `/tmp` is a different place from the phone's `/data/local/tmp`; move files via `/mnt/Droidspaces/arch-kde-mgl/root` (container `/root`) or `/data/local/tmp/anland-mobilegl` (= container `/run/anland-mobilegl`).
- **Never overwrite a mapped .so in place** (container or APK while running): copy to a new inode and `mv -f`; the APK swap force-stops the app first.
- **Input**: plain `adb shell input` is blocked - use `su -c input`; anland grabs the touchscreen, so if `input tap` does nothing use `input mouse tap x y`. Never `debuggerd -b` a container process (signal 35 kills glibc processes).
- **linux-dmabuf**: the client must use `create` + roundtrip, never `create_immed` (KWin answers a failed immediate import with a protocol error that kills the client).
- **Cross-session bugs**: noise / another window's pixels after some client starts or exits = process-global state in the multi-session server (handles are per client, FBO/VAO per context). Repro with `kscreenlocker_greet --testing`.
- `/etc/environment` leaks the baseline session's driver variables through pam_env (and, via `/usr/lib/environment.d/99-environment.conf`, into the user manager); `mobilegl-startup.sh` unsets them - keep that when editing it.
- **Shared log path**: Xwayland inherits KWin's `MOBILEGL_LOG_FILE_PATH`; any line a process logs opens (truncates) that file. MobileGL stays silent in processes it does not serve, but give any extra client its own `MOBILEGL_LOG_FILE_PATH`.
- **Stale per-process overrides**: an `__EGL_VENDOR_LIBRARY_FILENAMES` / `MOBILEGL_*` left in a unit drop-in or the user manager's environment (`systemctl --user show-environment`) still wins over the system files; `run-plasma.sh` (terminate-user) clears the manager's.
- `handoff` mentions a `kwin_wayland` logging shim and `/tmp/mgl-kwin-<pid>.log`: stale - `/opt/mobilegl/kwin/bin/kwin_wayland` is now the real binary and logs to `/tmp/mobilegl-compositor.client.log`.
## House rules
- Work in a git worktree, never in the shared main checkout or its build dirs; worktrees need `GIT_LFS_SKIP_SMUDGE=1`.
- Commit as `CONTRIBUTING.md` describes (`[Type] (Scope): content`, no trailer), with `git commit --no-verify`.
- Never run the DirectVulkan integration tests (`ctest -E DirectVulkan`).
- No external project names ("Mesa", "Zink", ...) in source code or comments; describe the technique.
- Vendor-agnostic only on the zero-copy path: AHB, EGL_ANDROID_*, VK_ANDROID AHB import, sync_file, dma-buf, libgbm ABI - no kgsl/gralloc layouts/QCOM modifiers.
- Identity caches key on monotonic lifetime ids, never GL names or pointers.
@@ -0,0 +1,183 @@
# Reproducing the MobileGL + anland + KDE Plasma setup from source
This is the from-scratch path for a NEW development machine and/or a NEW phone. `SKILL.md` covers the
day-to-day loop once everything below exists. Paths and names that are specific to the original
machine are listed in step 9: set them in `scripts/env.sh` (or the environment) before running anything.
## 0. What you need
**Phone.**
- arm64 Android 11+ with **root**. KernelSU is what this was built on; the scripts only need `su -c`.
- A Vulkan 1.1 driver with `VK_ANDROID_external_memory_android_hardware_buffer` for Magma, and/or GLES 3.2 + EGL_ANDROID_native_fence_sync for Espryt. Nothing vendor-specific is used: no kgsl and no vendor ioctls, only AHardwareBuffer, sync_file and the vendor's public GLES/Vulkan.
- About 20 GB free for the container image.
**Development host.**
- The scripts are written for **Windows + Git Bash** with WSL (`archlinux` distro) for the unit tests and the cross build.
- A Linux host works too: drop the `cygpath`/`MSYS_NO_PATHCONV` handling in `env.sh`, and run the WSL parts natively.
- Tools:
- `adb`, CMake >= 3.22, Ninja, Python 3 (+ Pillow for screenshots/recording).
- Android NDK **27.2.12479018**.
- JDK 17 and Android SDK platform 36 (for the APK).
- In the Linux environment: clang/lld/llvm of ONE major version (a mismatched lld breaks `ld.lld`), cmake, ninja.
## 1. Sources
```sh
# MobileGL: branch feat/disaggregated (everything here is on it)
git clone https://github.com/MobileGL-Dev/MobileGL.git && cd MobileGL
git checkout feat/disaggregated
GIT_LFS_SKIP_SMUDGE=1 git submodule update --init --recursive
# 3rdparty/glslang is the MobileGL-Dev fork (main); its External/spirv-tools must be populated
# (glslang's own update_glslang_sources.py, or copy it from another checkout).
# Keep builds out of a shared checkout: work in a worktree (GIT_LFS_SKIP_SMUDGE=1 git worktree add ...).
# anland: branch legacy-mobilegl-unified (KWin backend + patch, startup script, APK, daemon module)
git clone https://github.com/MobileGL-Dev/anland.git && cd anland # the fork that carries this branch
git checkout legacy-mobilegl-unified
```
Build the server (APK) and the client (container) from the **same MobileGL commit**: both stamp
`git rev-parse HEAD`, and a mismatch logs "compatible wire with different build".
## 2. Phone base: root, Droidspaces, display daemon
1. **Root** with KernelSU (or another `su` provider).
2. **Droidspaces:** install the module and its app. Containers live under `/data/local/Droidspaces/Containers/<name>/`, and the CLI is `/data/local/Droidspaces/bin/droidspaces`.
3. **anland display daemon** (KernelSU/Magisk module `anland-daemon`):
- Build it from the anland repo with `build_daemon_android.sh`, then package it with `build_magisk_module.sh`, which uses `magisk_module/`. Flash it and reboot.
- The module's `service.sh` starts the original daemon on `/data/local/tmp/display_daemon.sock`.
- The MobileGL session uses a SECOND instance of the same binary on `/data/local/tmp/anland-mobilegl/display.sock`: see `scripts/device/restart-daemon.sh`, which also chmods the directory and socket.
## 3. The container
Use an **Arch Linux ARM (aarch64, glibc) KDE Plasma Wayland rootfs** that has a systemd `desktop-session.service` running the desktop as a normal user with `PAMName=login`. A ready-made one is the Arch KDE "Wayland" image from Droidspaces RootFS Desktop Builder (see anland `doc/UserManual/anland_guide.md`, "Use prebuilt packages or a RootFS"). Or install `plasma-desktop`, `plasma-workspace`, `libglvnd`, `xorg-xwayland`, `base-devel`, `cmake` and `ninja` on plain Arch Linux ARM and write the service yourself.
Import it into Droidspaces as `arch-kde-mgl`. The default name is in `env.sh`. Reference `container.config` keys:
```
enable_gpu_mode=1
net_mode=host
bind_mounts=/data/local/tmp/display_daemon.sock:/run/display.sock,/data/local/tmp/anland-mobilegl:/run/anland-mobilegl
run_at_boot=0
```
- **Host networking:** it is required. The MobileGL server listens on the ABSTRACT socket `@anland-mobilegl`, which crosses into the container only when the network namespace is shared.
- **The anland-mobilegl directory:** create `/data/local/tmp/anland-mobilegl` on the phone before the first start, mode 777.
- **Desktop user:** the default is `swung0x48` (`MGL_DESKTOP_USER`). Create your own and point `desktop-session.service` at it.
- **Starting it:** `droidspaces -C .../container.config start`. Android root sees the rootfs at `/mnt/Droidspaces/<name>`, which `CT_ROOT` in `env.sh` relies on.
- **Container tools:** install them with `bash scripts/push-tools.sh` once the container runs.
## 4. MobileGL server and the anland APK
```sh
bash scripts/build-android.sh # -> <worktree>/build-android/libMobileGL.so
```
The script runs, with the NDK toolchain:
```
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-30 -DCMAKE_BUILD_TYPE=RelWithDebInfo
-DMOBILEGL_PIPE_PUSH=ON -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON
-DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF
```
APK, from anland `consumers/anland_v5/android_consumer` (details in its `MOBILEGL.md`):
```sh
mkdir -p dist/arm64-v8a && cp <worktree>/build-android/libMobileGL.so dist/arm64-v8a/
MOBILEGL_DIST=$PWD/dist ./gradlew :app:assemblePlainDebug \
-PanlandNdkVersion=27.2.12479018 -PmobileglApplicationId=com.anland.consumer.mobilegl \
-PanlandDefaultSocket=/data/local/tmp/anland-mobilegl/display.sock
adb install -r app/build/outputs/apk/plain/debug/app-plain-debug.apk
```
- **Grant root:** the app needs root for its helper; approve it in KernelSU.
- **Reinstall with another signing key:** that changes the UID, so grant root again (anland `mobilegl-tools/ksu_grant.c` does it from a shell). Don't uninstall to fix a signature clash: that wipes the app data.
- **Later server-only changes:** swap just the library into the installed APK (`scripts/cyc.sh` / `device/swap.sh`) instead of rebuilding the APK.
## 5. MobileGL client in the container
Pick one of these two:
- **Cross build** (fast, from WSL or Linux):
1. `bash scripts/xsysroot/refresh-sysroot.sh` exports the container's headers and libraries to `~/sysroots/arch-kde-mgl` and installs `~/sysroots/aarch64-arch.cmake`. Re-run it after container package upgrades.
2. `bash scripts/xbuild.sh`, then `bash scripts/xdeploy.sh`.
- **Native build in the container** (slow, no WSL): `scripts/sync-src.sh` copies the worktree to `/root/mobilegl-unified-src`, then run anland `producers/kde/Arch_v5/mobilegl-tools/anland-build-client.sh` inside (g++, `-DMOBILEGL_BUILD_DISAGGREGATED=ON`, Release).
Either route installs the same files, renamed into place so running clients keep their old mapping:
| File | What it is |
|---|---|
| `/opt/mobilegl/lib/libMobileGL.so` (+ `libEGL`/`libGL` aliases) | the client library |
| `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json` | system-wide EGL vendor; sorts before the distribution's `50_*.json`, which serves whenever MobileGL declines (no server reachable) |
| `/opt/mobilegl/share/glvnd/egl_vendor.d/50_mobilegl.json` | per-process forcing |
| `/usr/lib/libGLX_mobilegl.so.0` | GLX vendor |
| `/usr/lib/gbm/mobilegl_gbm.so` | GBM backend (zero-copy window buffers) |
| `/etc/mobilegl/client.conf` | `MOBILEGL_TRANSPORT=spawn`, `MOBILEGL_IPC_DATA=shm`, `MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl` |
| `/etc/mobilegl/backend` | `DirectVulkan` (Magma) or `DirectGLES` (Espryt) |
| `/etc/environment.d/10-mobilegl.conf`, `/etc/profile.d/mobilegl.sh` | `__GLX_VENDOR_LIBRARY_NAME=mobilegl`, `GBM_BACKEND=mobilegl` |
## 6. KWin 6.7.4 with the anland backend, and Xwayland
The MobileGL session runs a rebuilt KWin from `/opt/mobilegl/kwin`, so the packaged one stays untouched.
1. **Source tree:** in the container, unpack KWin 6.7.4 to `/root/mobilegl-build/kwin-6.7.4`, apply anland `producers/kde/Arch_v5/kwin.patch`, and overlay `producers/kde/anland_backend_Arch_v5/src/backends/anland` into `src/backends/anland`. Also take the current `common/` and `libdisplay_producer/`, as `mobilegl.md` there says.
2. **Configure:**
```sh
cmake -S kwin-6.7.4 -B kwin-6.7.4/build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=lib -DBUILD_TESTING=OFF
```
3. **Build and install:** run `producers/kde/Arch_v5/sync-build-kwin.sh` (`ninja -j4`). It installs `kwin_wayland` + `libkwin.so.6.7.4` into `/opt/mobilegl/kwin/{bin,lib}`, the patched screencast plugin in place (the original is kept beside it), and `/opt/mobilegl/bin/mobilegl-startup.sh`.
- From the dev host, `bash scripts/kwin-stage.sh build` does steps 1–3 incrementally: `kwprep.sh` applies changed patch hunks.
4. **Xwayland:** the anland-patched Xwayland 24.1.13 comes from `producers/kde/Arch_v5/build.sh` (makepkg, as a normal user; it builds the anland KWin and Xwayland packages). A prebuilt anland rootfs already has it.
5. **Session hook:** add `/etc/systemd/system/desktop-session.service.d/mobilegl.conf`:
```ini
[Service]
ExecStart=
ExecStart=/opt/mobilegl/bin/mobilegl-startup.sh plasma
Environment=ANLAND_SOCKET=/run/anland-mobilegl/display.sock
Environment=QT_LOGGING_RULES=kwin_*.info=true
```
`mobilegl-startup.sh plasma` writes the user drop-in for `plasma-kwin_wayland.service` (the `/opt/mobilegl/kwin` binary, `ANLAND_MOBILEGL=1`, `MOBILEGL_IPC_SURFACE=server`, `GBM_BACKEND=mobilegl`) and runs `startplasma-wayland`.
## 7. Bring it up
Nothing has to be started by hand, and the order no longer matters: **open the app** (its launcher
icon, or `am start -n com.anland.consumer.mobilegl/com.anland.consumer.MainActivity` with no extras).
That is the single trigger. The app starts its foreground service and runs its bundled
`mobilegl-desktop.sh up` as root, which:
1. starts the display daemon if it is not running;
2. publishes the backend (the app setting "Renderer backend", default DirectGLES) to
`/data/local/tmp/anland-mobilegl/backend`;
3. starts the container if it is stopped;
4. starts `desktop-session`, which waits for the server and the daemon socket, copies the backend into
`/etc/mobilegl/backend` and runs Plasma.
Cold start to a desktop takes about 8-12 s. `scripts/device/bringup.sh` wakes the phone, dismisses the
keyguard, opens the app and prints the timeline. To change the backend, use the app setting and then
Stop desktop and reopen, or run `scripts/device/switch.sh DirectGLES|DirectVulkan`. Do not use
`setprop debug.mobilegl.backend`: it overrides the setting until the next reboot. **Stop desktop**
(the notification action) stops the session and the container.
**After a phone reboot:** wireless adb does not come back by itself. Re-enable it on the phone
(Developer options > Wireless debugging), then `adb connect <ip>:<port>`; the port changes. After
that, open the app as above. See SKILL.md "Starting, idling, stopping".
## 8. Verify
- **Plasma:** it appears in the app. The tray must NOT show the "software renderer" icon; if it does, delete `SceneGraphBackend=software` from the user's `kdeglobals` and restart.
- **Plain shell:** in a plain container shell as the desktop user, `eglinfo -B` names MobileGL, and `glxinfo -B` from a login shell does too.
- **Wayland client:** `timeout 25 mgrun offscreen glmark2-es2-wayland -s 1280x720`. The log must say `presented through linux-dmabuf shared images`.
- **Chrome:** `bash scripts/chrome-launch.sh t1` gives windows with correct icons.
- **Resize and lock:** BACK toggles the extra-keys bar (resize); lock/unlock resumes the desktop. `rtest.sh` in the scratch dir checks both.
- **Unit tests:** `bash scripts/wsl-all.sh` in WSL. NEVER run the DirectVulkan integration tests.
## 9. Machine-specific values to change
| Value | Original | Where |
|---|---|---|
| MobileGL worktree | this skill's worktree | `MGL_WORKTREE` (auto) |
| scratch dir | `%LOCALAPPDATA%/Temp/anl` | `ANL_DIR` |
| NDK | `<SDK>/ndk/27.2.12479018` | `ANDROID_NDK` |
| anland worktree | `%USERPROFILE%/AndroidStudioProjects/anland/.claude/worktrees/mgl-unified` | `ANLAND_WORKTREE` (`kwin-stage.sh`) |
| APK id | `com.anland.consumer.mobilegl` | `MGL_PKG` |
| container | `arch-kde-mgl` | `MGL_CONTAINER` |
| desktop user | `swung0x48` | `MGL_DESKTOP_USER` + `desktop-session.service` |
| WSL distro | `archlinux` | `WSL_DISTRO` (`xbuild.sh`, `wsl-*.sh`) |
| cross build dir | WSL `~/mgl-xbuild-a64` | `MGL_XBUILD_DIR` |
| phone | Lenovo TB321FU, Adreno 750 | nothing: no serial is pinned; with several devices set `ANDROID_SERIAL` |
**Thermals:** a phone vendor's thermal daemon may throttle charging while the screen is on during long sessions. That is not part of this setup.
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# build-android.sh - build the Android SERVER libMobileGL.so (NDK, arm64-v8a) that the anland APK embeds.
# Output: $MGL_WORKTREE/build-android/libMobileGL.so (RelWithDebInfo, unstripped, ~320 MB; keep it for
# llvm-addr2line, it is what cyc.sh pushes). Stamp = worktree HEAD, so the container client
# (xbuild.sh, same stamp) and the server agree on the build ("compatible wire with different build"
# WARN otherwise). Env: MGL_WORKTREE, ANDROID_NDK, BUILD_DIR (default build-android), JOBS (12).
# Log level is compiled to INFO. android-30 = the APK's minSdk: thread_local is then the platform's
# own TLS rather than the emulated one (which cost 7-12% of an apply thread on the API 26 build).
set -euo pipefail
. "$(dirname "$0")/env.sh"
W=$MGL_WORKTREE
B=$W/${BUILD_DIR:-build-android}
STAMP=$(git -C "$W" rev-parse HEAD)
if [ ! -f "$B/build.ninja" ]; then
cmake -S "$W" -B "$B" -G Ninja -DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK/build/cmake/android.toolchain.cmake" \
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-30 -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DMOBILEGL_PIPE_PUSH=ON -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON \
-DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF \
-DCMAKE_CXX_FLAGS=-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
-DMOBILEGL_BUILD_STAMP="$STAMP"
else
cmake "$B" -DMOBILEGL_BUILD_STAMP="$STAMP" >/dev/null
fi
cmake --build "$B" --target MobileGL -j"${JOBS:-12}"
ls -la "$B/libMobileGL.so"
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# chrome-launch.sh <tag> [extra chrome flags...] - (re)start Chrome inside the running Plasma session
# through mobilegl-startup.sh's `chrome` mode (ANGLE GLES over MobileGL EGL), client log
# /tmp/mgl-chrome-<tag>.client.log, then print its "presented through" line and save a screenshot
# to $ANL_DIR/ch-<tag>.png. Kills the running chrome first (pkill -x, never -f). MobileGL is the
# container's system-wide vendor (10_mobilegl.json + /etc/mobilegl/client.conf), so only the log is set here.
# Env: WAIT (s before checking, default 20), CHANNEL (ct.sh channel, default chrome),
# CHROME_GPU (process, the default: the GPU in its own process, which presents
# through GBM dma-bufs and is restarted by Chrome after a device loss; in-process opts out -
# needs the anland startup script that knows MOBILEGL_CHROME_GPU; in process mode the GPU process and the browser
# write the one client log, the GPU process last); CHROME_GPU_SANDBOX=on keeps its GPU sandbox.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
TAG=${1:?usage: chrome-launch.sh <tag> [chrome flags...]}; shift
bash "$here/ct.sh" "${CHANNEL:-chrome}" <<IN
pkill -x chrome; sleep 2
U=\$(id -u $MGL_DESKTOP_USER)
rm -f /tmp/mgl-chrome-$TAG.client.log /tmp/chrome-$TAG-out.log
sudo -u $MGL_DESKTOP_USER env XDG_RUNTIME_DIR=/run/user/\$U WAYLAND_DISPLAY=wayland-0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/\$U/bus MOBILEGL_LOG_FILE_PATH=/tmp/mgl-chrome-$TAG.log MOBILEGL_CHROME_GPU=${CHROME_GPU:-process} MOBILEGL_CHROME_GPU_SANDBOX=${CHROME_GPU_SANDBOX:-off} bash -c 'nohup /opt/mobilegl/bin/mobilegl-startup.sh chrome --restore-last-session $* > /tmp/chrome-$TAG-out.log 2>&1 &'
sleep ${WAIT:-20}
grep -ah 'presented through' /tmp/mgl-chrome-$TAG.client.log | head -3
IN
adb_ shell "su -c 'screencap -p $DEV_SHARE/ch-$TAG.png'"
adb_ pull "$DEV_SHARE/ch-$TAG.png" "$ANL_DIR/ch-$TAG.png" >/dev/null && echo "screenshot: $ANL_DIR/ch-$TAG.png"
@@ -0,0 +1,146 @@
#!/usr/bin/env bash
# chrome-recover.sh <in-process|process> [rounds] - does Chrome repaint after a GPU device loss,
# without a restart? Starts Chrome (chrome-launch.sh, CHROME_GPU=<mode>) on an animated WebGL page,
# then per round: screenshots (the page must animate), loses the session of Chrome's GPU client with
# the server's pid-targeted debug knob (debug.mobilegl.inject_device_lost_pid; the pid as Chrome sees
# itself in the container), waits, and screenshots again. A round PASSES when the server latched
# that pid's session, the browser process is the same one (no restart), a GPU client is drawing on a
# NEW server session, and the screen still animates. Stops early below MIN_BATTERY percent.
#
# in-process the GPU in the browser process (the startup script's default today): the target is
# the browser itself; recovery needs ANGLE to make a new native context.
# process the GPU in its own process (MOBILEGL_CHROME_GPU=process): the target is the GPU
# process; Chrome restarts it after the loss, so the next round's target is a new pid.
#
# Deploy first (same MobileGL commit on both sides; see SKILL.md):
# bash scripts/build-android.sh && bash scripts/cyc.sh pre # server -> APK, restarts the session
# bash scripts/xbuild.sh && bash scripts/xdeploy.sh # client -> container
# STARTUP=<anland worktree>/producers/kde/Arch_v5/mobilegl-startup.sh bash scripts/chrome-recover.sh deploy-startup
# Env: ROUNDS (default 2), SETTLE (s after the loss, default 8), MIN_BATTERY (default 40),
# CHANNEL (ct.sh channel, default crec), TAG (log tag, default crec-<mode>).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
MODE=${1:?usage: chrome-recover.sh <in-process|process|deploy-startup> [rounds]}
ROUNDS=${2:-${ROUNDS:-2}}
SETTLE=${SETTLE:-8}
MIN_BATTERY=${MIN_BATTERY:-40}
CH=${CHANNEL:-crec}
TAG=${TAG:-crec-$MODE}
PAGE=/tmp/mgl-spin.html
ct() { bash "$here/ct.sh" "$CH"; }
battery() { adb_ shell dumpsys battery | tr -d '\r' | awk '/ level:/ {print $2}'; }
check_battery() {
local level; level=$(battery)
echo "battery: $level%"
if [ -n "$level" ] && [ "$level" -lt "$MIN_BATTERY" ]; then
echo "STOP: battery below $MIN_BATTERY%"; cleanup; exit 3
fi
}
cleanup() { adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'" || true; }
if [ "$MODE" = deploy-startup ]; then
# The anland startup script that knows MOBILEGL_CHROME_GPU, installed by rename (a running
# session keeps its open copy).
src=${STARTUP:?STARTUP=<path to the anland mobilegl-startup.sh>}
tr -d '\r' < "$src" > "$ANL_DIR/mobilegl-startup.sh"
adb_ push "$ANL_DIR/mobilegl-startup.sh" "$DEV_SHARE/mobilegl-startup.sh.new" >/dev/null
ct <<'IN'
install -m 755 /run/anland-mobilegl/mobilegl-startup.sh.new /opt/mobilegl/bin/mobilegl-startup.sh.new
mv -f /opt/mobilegl/bin/mobilegl-startup.sh.new /opt/mobilegl/bin/mobilegl-startup.sh
grep -c MOBILEGL_CHROME_GPU /opt/mobilegl/bin/mobilegl-startup.sh
IN
exit 0
fi
case "$MODE" in in-process|process) ;; *) echo "mode must be in-process or process" >&2; exit 2;; esac
trap cleanup EXIT
check_battery
adb_ shell "su -c 'sh $DEV_TOOLS/status.sh'" || true
cleanup
# The page: WebGL clears that change colour every frame plus a moving block, so a frame that
# reaches the screen changes the screenshot.
ct <<IN
cat > $PAGE <<'HTML'
<!doctype html><html><body style="margin:0;background:#000">
<canvas id=c width=640 height=360 style="width:100vw;height:100vh"></canvas>
<script>
const gl = document.getElementById('c').getContext('webgl');
let t = 0, lost = 0;
document.getElementById('c').addEventListener('webglcontextlost', e => { e.preventDefault(); lost++; });
function frame() {
t++;
if (gl && !gl.isContextLost()) {
gl.clearColor((t % 60) / 60, ((t + 20) % 60) / 60, ((t + 40) % 60) / 60, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.enable(gl.SCISSOR_TEST); gl.scissor((t * 7) % 600, 100, 40, 160);
gl.clearColor(1, 1, 1, 1); gl.clear(gl.COLOR_BUFFER_BIT); gl.disable(gl.SCISSOR_TEST);
}
document.title = 'spin ' + t + ' lost ' + lost;
requestAnimationFrame(frame);
}
frame();
</script></body></html>
HTML
chmod 644 $PAGE
IN
WAIT=${WAIT:-20} CHROME_GPU=$MODE CHANNEL=$CH bash "$here/chrome-launch.sh" "$TAG" --new-window "file://$PAGE"
# The target pid inside the container: the browser (in-process) or the GPU process.
target_pid() {
ct <<'IN' | tr -d '\r' | tail -1
for p in $(pgrep -x chrome); do
args=$(tr '\0' ' ' < /proc/$p/cmdline)
case "$args" in
*--type=gpu-process*) echo "gpu $p" ;;
*--type=*) ;;
*) echo "browser $p" ;;
esac
done | sort > /tmp/mgl-crec-pids
cat /tmp/mgl-crec-pids >&2
echo "$(awk '/^browser/ {print $2; exit}' /tmp/mgl-crec-pids) $(awk '/^gpu/ {print $2; exit}' /tmp/mgl-crec-pids)"
IN
}
pass=0
for round in $(seq 1 "$ROUNDS"); do
echo "=== round $round/$ROUNDS ($MODE)"
check_battery
read -r browser gpu < <(target_pid)
if [ "$MODE" = process ]; then target=$gpu; else target=$browser; fi
echo "browser=$browser gpu=${gpu:-none} target=$target"
if [ -z "$target" ]; then echo "FAIL: no target process"; break; fi
bash "$here/shot.sh" "$TAG-r$round-pre" 2 1
adb_ logcat -c
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid $target'"
sleep "$SETTLE"
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'"
bash "$here/shot.sh" "$TAG-r$round-post" 3 1 | tee "$ANL_DIR/$TAG-r$round-post.md5"
latched=$(adb_ logcat -d | grep -c "injected:pid=$target" || true)
started=$(adb_ logcat -d | grep -c "session #[0-9]* started" || true)
adb_ logcat -d | grep -E "inject_device_lost_pid|SessionLatch|session #[0-9]+ (started|pid)" | cut -c1-220 | head -12 || true
read -r browser2 gpu2 < <(target_pid)
echo "after: browser=$browser2 gpu=${gpu2:-none}"
ct <<IN || true
echo "--- client log ($TAG):"; grep -aE "DEVICE LOST|RECOVERED|GL_UNKNOWN_CONTEXT_RESET|failed to make|session #" /tmp/mgl-chrome-$TAG.client.log | tail -8
echo "--- chrome stderr:"; grep -aiE "context lost|GPU process|make.*current|lost" /tmp/chrome-$TAG-out.log | tail -8
IN
adb_ logcat -b crash -d | tail -5
hashes=$(awk '{print $1}' "$ANL_DIR/$TAG-r$round-post.md5" | sort -u | wc -l)
verdict=PASS
[ "$latched" -ge 1 ] || { echo " - the server never latched pid $target"; verdict=FAIL; }
[ "$browser2" = "$browser" ] || { echo " - the browser process changed ($browser -> $browser2): Chrome restarted"; verdict=FAIL; }
[ "$started" -ge 1 ] || { echo " - no new server session started after the loss"; verdict=FAIL; }
if [ "$MODE" = process ]; then
[ -n "$gpu2" ] && [ "$gpu2" != "$gpu" ] || { echo " - no new GPU process"; verdict=FAIL; }
fi
[ "$hashes" -ge 2 ] || { echo " - the screen does not change after the loss (frozen)"; verdict=FAIL; }
echo "round $round: $verdict"
[ "$verdict" = PASS ] && pass=$((pass + 1))
[ "$verdict" = PASS ] || break
done
echo "=== $pass/$ROUNDS rounds passed ($MODE); screenshots in $ANL_DIR/$TAG-r*-*.png"
@@ -0,0 +1,29 @@
#!/bin/bash
# kwprep.sh - (container, as root) bring the live KWin tree /root/mobilegl-build/kwin-6.7.4 in line with
# anland's kwin.patch + anland backend sources, touching ONLY files whose content changed (keeps the
# ninja build incremental). It re-applies the whole patch to a pristine copy of KWin's src/
# (/root/gbm-dev/kwin-a/src) in /root/kp and copies every patched file that differs into the live tree,
# then copies changed backend files. Build + install afterwards with anland's sync-build-kwin.sh.
#
# Input: /root/kwsync.tgz, a tar made in the anland worktree's producers/kde directory:
# tar czf kwsync.tgz Arch_v5/kwin.patch Arch_v5/mobilegl-startup.sh anland_backend_Arch_v5/src/backends/anland/
# and pushed to the container's /root (e.g. via /mnt/Droidspaces/arch-kde-mgl/root/ on the Android side).
# The pristine tree /root/gbm-dev/kwin-a/src was set up by hand earlier (unverified that it is exactly
# upstream 6.7.4); patch hunks that fail are reported by `patch`.
set -e
rm -rf /root/kwsync && mkdir -p /root/kwsync && tar xzf /root/kwsync.tgz -C /root/kwsync
P=/root/kwsync/Arch_v5/kwin.patch
rm -rf /root/kp && mkdir -p /root/kp && cp -a /root/gbm-dev/kwin-a/src /root/kp/
cd /root/kp && patch -p1 -s < $P
LIVE=/root/mobilegl-build/kwin-6.7.4
FILES=$(grep -E '^\+\+\+ b/' $P | sed 's|^+++ b/||' | awk '{print $1}' | sort -u)
for f in $FILES; do
if [ -f "/root/kp/$f" ] && ! cmp -s "/root/kp/$f" "$LIVE/$f"; then cp "/root/kp/$f" "$LIVE/$f"; echo "updated $f"; fi
done
for f in /root/kwsync/anland_backend_Arch_v5/src/backends/anland/*; do
# libdisplay_producer is a symlink in git; a Windows checkout turns it into a small text file. The live
# tree has the real directory (repo-root libdisplay_producer/), which neither script syncs.
[ -f "$f" ] && [ "$(basename $f)" != libdisplay_producer ] || continue
b=$(basename $f); if ! cmp -s "$f" "$LIVE/src/backends/anland/$b"; then cp "$f" "$LIVE/src/backends/anland/$b"; echo "updated anland/$b"; fi
done
echo PREP_DONE
@@ -0,0 +1,21 @@
#!/bin/bash
# mgrun <surface: offscreen|server> <command...> - (container, as root) run one MobileGL client as the
# desktop user inside the running Plasma session. Installed as /usr/local/bin/mgrun (push-tools.sh).
# mgrun offscreen glmark2-es2-wayland -s 1280x720 # Wayland client, presents through KWin
# mgrun offscreen eglinfo -B ; mgrun offscreen glxinfo -B # glxinfo goes through Xwayland + GLX vendor
# "server" is only for a client that owns the Android Surface itself (KWin does); everything else is
# offscreen. MobileGL is the container's system-wide vendor, so nothing selects it here: EGL comes from
# /usr/share/glvnd/egl_vendor.d/10_mobilegl.json, transport/endpoint from /etc/mobilegl/client.conf,
# the backend from /etc/mobilegl/backend (must match the server), and the GLX vendor name / GBM backend
# from /etc/profile.d/mobilegl.sh (sourced by the login shell below). Client log:
# ${MGLOG:-/tmp/mgl-run.log} -> the library writes /tmp/mgl-run.client.log (truncated per run; give
# concurrent clients different MGLOG values). Env: MGLOG, DESKTOP_USER (swung0x48).
S=$1; shift
DU=${DESKTOP_USER:-swung0x48}
U=$(id -u "$DU")
cd /tmp
exec sudo -u "$DU" env XDG_RUNTIME_DIR=/run/user/$U WAYLAND_DISPLAY=wayland-0 DISPLAY=:0 \
XAUTHORITY=$(ls /run/user/$U/xauth_* 2>/dev/null | head -1) \
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$U/bus QT_QPA_PLATFORM=wayland \
MOBILEGL_IPC_SURFACE=$S MOBILEGL_LOG_FILE_PATH=${MGLOG:-/tmp/mgl-run.log} \
bash -lc 'exec "$@"' mgrun "$@"
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# ct.sh <channel> [script-file] - run a bash script as ROOT inside the container (login shell),
# reading the script from the file or from stdin. Generic form of the old c.sh/c2.sh/c3.sh.
#
# printf '%s\n' 'ls -la /tmp/*.log' | bash ct.sh mychan
# bash ct.sh mychan ./something.cmd
#
# How: the script is written to $ANL_DIR/<channel>.cmd, pushed to $DEV_TOOLS/<channel>.cmd together with
# a one-line <channel>.sh that does `droidspaces --name=<container> run bash -lc "$(cat <channel>.cmd)"`,
# and that runs under `su`. Quoting through adb + su + droidspaces is thereby avoided entirely.
#
# Pick a channel name of your own: other sessions on the same phone use c, c2, c3, xd, xr, gbm...;
# writing to their channel files races with them. CR characters are stripped (Windows checkouts).
# Inside, run user-side commands with `sudo -u <desktop user> env XDG_RUNTIME_DIR=/run/user/<uid> ...`.
# Never `pkill -f <word>` in a channel script: the `bash -lc "<script>"` process carries the whole
# script on its command line and kills itself. Use `pkill -x <name>`.
set -euo pipefail
. "$(dirname "$0")/env.sh"
ch=${1:?usage: ct.sh <channel> [script-file]}
src=${2:--}
if [ "$src" = - ]; then tr -d '\r' > "$ANL_DIR/$ch.cmd"; else tr -d '\r' < "$src" > "$ANL_DIR/$ch.cmd"; fi
printf '%s\n' "$DS_BIN --name=$MGL_CONTAINER run bash -lc \"\$(cat $DEV_TOOLS/$ch.cmd)\"" > "$ANL_DIR/$ch.sh"
adb_ shell "mkdir -p $DEV_TOOLS"
adb_ push "$ANL_DIR/$ch.cmd" "$DEV_TOOLS/$ch.cmd" >/dev/null
adb_ push "$ANL_DIR/$ch.sh" "$DEV_TOOLS/$ch.sh" >/dev/null
adb_ shell "su -c 'sh $DEV_TOOLS/$ch.sh'"
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# cyc.sh <shot-name> - one server iteration: clear Plasma's sticky software-renderer key, push the
# NDK-built server lib, swap it into the installed APK (device/swap.sh), restart app + Plasma session
# (device/run-plasma.sh, ~90 s), screenshot to $ANL_DIR/<shot-name>.png, print fatal logcat lines.
# Env: SERVER_LIB (default $MGL_WORKTREE/build-android/libMobileGL.so), WAIT (s after the session
# restart, default 15), N (logcat lines, default 20), CHANNEL (ct.sh channel, default cyc).
# Needs device/swap.sh + device/run-plasma.sh in $DEV_TOOLS (push-tools.sh). RESTARTS THE SESSION.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
shot=${1:?usage: cyc.sh <shot-name>}
LIB=${SERVER_LIB:-$MGL_WORKTREE/build-android/libMobileGL.so}
printf '%s\n' "sudo -u $MGL_DESKTOP_USER kwriteconfig6 --file kdeglobals --group QtQuickRendererSettings --key SceneGraphBackend --delete; rm -f /tmp/mgl-plasmashell.client.log" \
| bash "$here/ct.sh" "${CHANNEL:-cyc}"
adb_ push "$LIB" "$DEV_SHARE/libMobileGL.new.so" >/dev/null
adb_ logcat -c
adb_ shell "su -c 'sh $DEV_TOOLS/swap.sh >/dev/null && sh $DEV_TOOLS/run-plasma.sh >/dev/null 2>&1; sleep ${WAIT:-15}; screencap -p $DEV_SHARE/$shot.png'"
adb_ pull "$DEV_SHARE/$shot.png" "$ANL_DIR/$shot.png" >/dev/null && echo "screenshot: $ANL_DIR/$shot.png"
adb_ logcat -d | grep -E "Fatal signal|FATAL|Abort message|Magma wire buffer|E MobileGL" | cut -c1-260 | head -"${N:-20}" || true
@@ -0,0 +1,94 @@
#!/usr/bin/env bash
# desktop-verify.sh <phase>... - check that the MobileGL desktop starts, idles and stops by itself
# (anland app: foreground service + mobilegl-desktop.sh; container: desktop-session.service).
# Phases run in the order given; screenshots go to $ANL_DIR/dv-<phase>.png.
#
# push install device/*.sh into $DEV_TOOLS (push-tools.sh's device half)
# reset everything down: app force-stopped (its service with it), container stopped,
# experiment display daemon killed - the state after a phone reboot
# cold reset, then open the app like its icon does and time each piece (desktop-timeline.sh)
# open open the app without resetting (after a reboot: the reboot was the reset)
# chrome (re)start Chrome in the session on an animated WebGL page (kills a running Chrome)
# idle CPU/GPU for visible, hidden (HOME), locked (screen off), visible again (measure.sh)
# reopen swipe the app away (remove its task), check KWin survives, open it again, time it
# stop Stop desktop from the notification (the service's STOP action) and check it all ends
# rtest lock/unlock + window resize regression ($ANL_DIR/rtest.sh)
# Env: SECS (idle window, default 20), URL (chrome page).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
SECS=${SECS:-20}
URL=${URL:-https://webglsamples.org/aquarium/aquarium.html}
SOCK=$DEV_SHARE/display.sock
su_() { adb_ shell "su -c '$*'"; }
shot() {
su_ "screencap -p $DEV_SHARE/dv-$1.png"
adb_ pull "$DEV_SHARE/dv-$1.png" "$ANL_DIR/dv-$1.png" > /dev/null && echo "screenshot $ANL_DIR/dv-$1.png"
}
kwin_log() { # the anland backend's lines since <time>
printf '%s\n' "journalctl --no-pager --since '$1' 2>/dev/null | grep -a -E 'viewer (gone|back)|consumer (reconnected|disconnected)|failed to connect' | tail -${2:-8}" |
bash "$here/ct.sh" dv 2>/dev/null | grep -v pushed || true
}
task_id() {
su_ "am stack list" | tr -d '\r' | grep -m1 "$MGL_PKG/" | sed -n 's/.*taskId=\([0-9]*\).*/\1/p'
}
wake() {
su_ "input keyevent KEYCODE_WAKEUP"; sleep 1; su_ "wm dismiss-keyguard"; sleep 2
}
for phase in "$@"; do
echo "=== $phase"
case $phase in
push)
for f in "$here"/device/*.sh; do
tr -d '\r' < "$f" > "$ANL_DIR/push.tmp"
adb_ push "$ANL_DIR/push.tmp" "$DEV_TOOLS/$(basename "$f")" > /dev/null
done
rm -f "$ANL_DIR/push.tmp"; echo "device scripts in $DEV_TOOLS" ;;
reset)
su_ "sh $DEV_TOOLS/desktop-reset.sh" ;;
cold)
bash "$0" reset
wake
su_ "sh $DEV_TOOLS/desktop-timeline.sh 240"
sleep 3; shot cold ;;
open)
wake
su_ "sh $DEV_TOOLS/desktop-timeline.sh 300"
sleep 3; shot open ;;
chrome)
WAIT=15 bash "$here/chrome-launch.sh" dv "$URL" ;;
idle)
su_ "sh $DEV_TOOLS/measure.sh visible $SECS"
t=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
su_ "input keyevent KEYCODE_HOME"; sleep 4
su_ "sh $DEV_TOOLS/measure.sh hidden $SECS"
su_ "am start -n $MGL_PKG/com.anland.consumer.MainActivity >/dev/null"; sleep 5
su_ "input keyevent KEYCODE_SLEEP"; sleep 4
su_ "sh $DEV_TOOLS/measure.sh locked $SECS"
wake
su_ "am start -n $MGL_PKG/com.anland.consumer.MainActivity >/dev/null"; sleep 5
su_ "sh $DEV_TOOLS/measure.sh visible2 $SECS"
shot idle
kwin_log "$t" 12 ;;
reopen)
t=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
id=$(task_id); echo "removing task $id"
[ -n "$id" ] && su_ "am stack remove $id"
sleep 8
su_ "echo server: \$(pidof $MGL_PKG:mobilegl) ui: \$(pidof $MGL_PKG) kwin: \$(pidof kwin_wayland) plasmashell: \$(pidof plasmashell)"
su_ "sh $DEV_TOOLS/measure.sh closed 10"
su_ "sh $DEV_TOOLS/desktop-timeline.sh 120"
sleep 3; shot reopen
kwin_log "$t" ;;
stop)
su_ "am startservice -n $MGL_PKG/com.anland.consumer.MobileGLWorker -a com.anland.consumer.mobilegl.STOP" || true
sleep 25
su_ "echo server: \$(pidof $MGL_PKG:mobilegl) ui: \$(pidof $MGL_PKG); $DS_BIN show"
su_ "tail -3 $DEV_SHARE/desktop.log" ;;
rtest)
bash "$ANL_DIR/rtest.sh" dv ;;
*)
echo "unknown phase $phase" >&2; exit 2 ;;
esac
done
@@ -0,0 +1,7 @@
# bringup.sh - (Android root shell) bring the MobileGL desktop up after a phone reboot. There is
# nothing left to do by hand: opening the anland app (its launcher icon, or this am start) starts
# the display daemon, the MobileGL server, the container and its Plasma session. Prints the timeline.
# Run: su -c "sh /data/local/tmp/anl/bringup.sh" Env: as desktop-timeline.sh.
input keyevent KEYCODE_WAKEUP
wm dismiss-keyguard
sh "$(dirname "$0")/desktop-timeline.sh" 300
@@ -0,0 +1,17 @@
# desktop-reset.sh - (Android root shell) take the MobileGL desktop fully down, as after a phone
# reboot: the app force-stopped (its foreground service and server with it), the container
# stopped, the experiment display daemon killed. Leaves the original anland daemon alone.
# Run: su -c "sh /data/local/tmp/anl/desktop-reset.sh" Env: PKG, CONTAINER, SOCK, DS.
P=${PKG:-com.anland.consumer.mobilegl}
C=${CONTAINER:-arch-kde-mgl}
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
am force-stop $P
$D --name=$C stop > /dev/null 2>&1
for p in $(pidof display_daemon); do
tr '\0' ' ' < /proc/$p/cmdline | grep -qF " $SOCK " && kill $p
done
rm -f $SOCK
sleep 2
echo "app: $(pidof $P) server: $(pidof $P:mobilegl) daemon: $(ls $SOCK 2>/dev/null)"
$D show
@@ -0,0 +1,30 @@
# desktop-timeline.sh [timeout-s] - (Android root shell) open the anland app the way its launcher
# icon does (am start, no extras) and print when each piece of the MobileGL desktop comes up,
# in seconds after the open: display daemon socket, the app's MobileGL server listening, the
# container, kwin_wayland, plasmashell, and the splash (ksplashqml) gone = desktop on screen.
# Pieces that already run show 0.0. Read-only apart from the am start.
# Run: su -c "sh /data/local/tmp/anl/desktop-timeline.sh 180" Env: PKG, CONTAINER, SOCK, DS.
T=${1:-180}
P=${PKG:-com.anland.consumer.mobilegl}
C=${CONTAINER:-arch-kde-mgl}
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
now() { awk '{print $1}' /proc/uptime; }
since() { awk -v a="$1" -v b="$(now)" 'BEGIN{printf "%.1f", b - a}'; }
listening() { grep -q '00010000 0001 01 [0-9]* @anland-mobilegl$' /proc/net/unix; }
t0=$(now)
am start -n $P/com.anland.consumer.MainActivity > /dev/null
for k in daemon server container kwin shell splash_seen shown; do eval "t_$k="; done
while :; do
[ -z "$t_daemon" ] && [ -S "$SOCK" ] && t_daemon=$(since $t0) && echo "daemon socket +${t_daemon}s"
[ -z "$t_server" ] && listening && t_server=$(since $t0) && echo "server listening +${t_server}s"
[ -z "$t_container" ] && [ -n "$($D --name=$C pid 2>/dev/null | tr -dc 0-9)" ] && t_container=$(since $t0) && echo "container up +${t_container}s"
[ -z "$t_kwin" ] && pidof kwin_wayland > /dev/null && t_kwin=$(since $t0) && echo "kwin_wayland +${t_kwin}s"
[ -z "$t_shell" ] && pidof plasmashell > /dev/null && t_shell=$(since $t0) && echo "plasmashell +${t_shell}s"
[ -z "$t_splash_seen" ] && pidof ksplashqml > /dev/null && t_splash_seen=1
if [ -n "$t_shell" ] && [ -z "$t_shown" ] && ! pidof ksplashqml > /dev/null; then
t_shown=$(since $t0); echo "splash gone +${t_shown}s (desktop on screen)"; break
fi
[ "$(since $t0 | cut -d. -f1)" -ge "$T" ] && { echo "TIMEOUT after ${T}s"; break; }
sleep 0.3
done
@@ -0,0 +1,47 @@
# measure.sh <label> [seconds] - (Android root shell) CPU and GPU load of the MobileGL desktop over a
# window: % of ONE core (so 800 = all 8 cores) for the whole phone, KWin (+Xwayland), plasmashell,
# Chrome (all its processes), the app's :mobilegl server and its UI process, plus the mean of the
# kgsl gpu_busy_percentage samples (1/s). The container's processes are visible from Android's
# /proc (Droidspaces only gives them their own pid namespace).
# Run: su -c "sh /data/local/tmp/anl/measure.sh visible 20" Env: PKG.
L=${1:-sample}
N=${2:-20}
P=${PKG:-com.anland.consumer.mobilegl}
GPU=/sys/class/kgsl/kgsl-3d0/gpu_busy_percentage
pids_named() { # exact process names (comm or argv0 basename)
for n in "$@"; do pidof "$n"; done
}
pids_matching() { # argv contains the word
for d in /proc/[0-9]*; do
{ tr '\0' ' ' < $d/cmdline; } 2>/dev/null | grep -q "$1" && echo ${d#/proc/}
done
}
ticks() { # utime+stime of the pids, in clock ticks
t=0
for p in "$@"; do
[ -r /proc/$p/stat ] || continue
v=$(sed 's/^.*) //' /proc/$p/stat | awk '{print $12 + $13}')
t=$((t + v))
done
echo $t
}
total() { awk '/^cpu /{print $2+$3+$4+$6+$7+$8}' /proc/stat; }
KW="$(pids_named kwin_wayland Xwayland)"
SH="$(pids_named plasmashell)"
CH="$(pids_matching /opt/google/chrome/)"
SV="$(pidof $P:mobilegl)"
UI="$(pidof $P)"
a0=$(total); k0=$(ticks $KW); s0=$(ticks $SH); c0=$(ticks $CH); v0=$(ticks $SV); u0=$(ticks $UI)
g=0; i=0
while [ $i -lt $N ]; do
sleep 1
b=$(cat $GPU 2>/dev/null | tr -dc '0-9'); g=$((g + ${b:-0})); i=$((i + 1))
done
a1=$(total); k1=$(ticks $KW); s1=$(ticks $SH); c1=$(ticks $CH); v1=$(ticks $SV); u1=$(ticks $UI)
# CLK_TCK is 100: ticks per second == percent of one core.
pct() { echo $(( ($2 - $1) / N )); }
printf '%-10s %3ss phone %4s%% kwin %3s%% plasmashell %3s%% chrome %3s%% server %3s%% app-ui %3s%% gpu %3s%%\n' \
"$L" "$N" "$(pct $a0 $a1)" "$(pct $k0 $k1)" "$(pct $s0 $s1)" "$(pct $c0 $c1)" "$(pct $v0 $v1)" "$(pct $u0 $u1)" "$((g / N))"
@@ -0,0 +1,20 @@
# restart-daemon.sh - (Android root shell) (re)start the EXPERIMENT's anland display daemon on $SOCK.
# It is not started at boot (the anland-daemon module's service.sh only starts the original daemon on
# /data/local/tmp/display_daemon.sock, which must be left alone). Without it KWin logs
# "failed to connect to display daemon at /run/anland-mobilegl/display.sock" and the session dies in ~3 s.
# The container sees $SOCK as /run/anland-mobilegl/display.sock (bind mount in container.config);
# uid 1000 in the container must connect, hence 777 dir / 666 socket.
# Env: SOCK, DAEMON (/data/adb/modules/anland-daemon/display_daemon).
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
DAEMON=${DAEMON:-/data/adb/modules/anland-daemon/display_daemon}
DIR=$(dirname $SOCK)
for p in $(ps -A -o PID,ARGS | grep "display_daemon $SOCK" | grep -v grep | awk '{print $1}'); do kill $p; done
sleep 1
rm -f $SOCK
mkdir -p $DIR
nohup $DAEMON $SOCK > $DIR/display-daemon.log 2>&1 &
sleep 1
chmod 777 $DIR
chmod 666 $SOCK
echo "daemon restarted:"
ps -A -o PID,ARGS | grep "display_daemon $SOCK" | grep -v grep
@@ -0,0 +1,27 @@
# run-plasma.sh - (Android root shell) DEVELOPER restart of the whole MobileGL desktop, e.g. after a
# server, client or KWin rebuild: ends the Plasma session (and the user's logind session, which takes
# a while to wind down), then restarts the anland app; opening it brings up the server, starts
# desktop-session.service again and the session waits for the server. Normal use needs none of this:
# opening the app is enough. ~60 s. Prints the unit status, plasma user units and recent coredumps.
# Run: su -c "sh /data/local/tmp/anl/run-plasma.sh"
# Env: PKG, CONTAINER, DESKTOP_USER (swung0x48), DS (droidspaces binary).
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
P=${PKG:-com.anland.consumer.mobilegl}
C=${CONTAINER:-arch-kde-mgl}
U=${DESKTOP_USER:-swung0x48}
$D --name=$C run bash -lc "
systemctl stop desktop-session.service 2>/dev/null
loginctl terminate-user $U 2>/dev/null; sleep 30
systemctl reset-failed desktop-session.service
rm -f /tmp/mobilegl-compositor.log
date +%s > /tmp/plasma-start
"
am force-stop $P
am start -n $P/com.anland.consumer.MainActivity > /dev/null
sleep 30
$D --name=$C run bash -lc "
systemctl --no-pager status desktop-session.service | head -8
UID_=\$(id -u $U)
sudo -u $U XDG_RUNTIME_DIR=/run/user/\$UID_ systemctl --user --no-pager list-units --all 'plasma*' 2>&1 | head -30
coredumpctl --no-pager list --since '-2min' 2>&1 | tail -5
"
@@ -0,0 +1,13 @@
# status.sh - (Android root shell) READ-ONLY health check of the whole stack.
# Run: su -c "sh /data/local/tmp/anl/status.sh" Env: PKG, CONTAINER, DS, SOCK.
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
P=${PKG:-com.anland.consumer.mobilegl}
C=${CONTAINER:-arch-kde-mgl}
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
echo "battery: $(dumpsys battery | grep ' level:' | tr -d ' ') $(dumpsys power | grep -o 'mStayOn=[a-z]*')"
echo "backend: app setting=$(cat /data/data/$P/files/mobilegl-backend 2>/dev/null || echo 'DirectGLES (default)') published=$(cat ${SOCK%/*}/backend 2>/dev/null) container /etc/mobilegl/backend=$(cat /mnt/Droidspaces/$C/etc/mobilegl/backend 2>/dev/null) prop override=$(getprop debug.mobilegl.backend)"
echo "desktop service log: $(tail -1 ${SOCK%/*}/desktop.log 2>/dev/null)"
echo "app pid: $(pidof $P) server (:mobilegl) pid: $(pidof $P:mobilegl)"
ps -A -o PID,ARGS | grep "display_daemon $SOCK" | grep -v grep || echo "experiment display daemon NOT running"
$D show
$D --name=$C run bash -lc 'echo "desktop-session: $(systemctl is-active desktop-session.service)"; pgrep -a -x kwin_wayland; pgrep -a -x plasmashell; grep -A1 QtQuickRendererSettings /home/*/.config/kdeglobals 2>/dev/null || echo "kdeglobals: no sticky software renderer"'
@@ -0,0 +1,17 @@
# swap.sh - (Android root shell) replace the server libMobileGL.so inside the INSTALLED anland APK with
# $SHARE/libMobileGL.new.so, keeping owner, mode and SELinux context; force-stops the app. The first
# run backs the APK's original lib up to $SHARE/libMobileGL.so.orig. No re-signing/reinstall (that
# would change the app UID and lose its root grant). Run: su -c "sh /data/local/tmp/anl/swap.sh"
# Env: PKG (com.anland.consumer.mobilegl), SHARE (/data/local/tmp/anland-mobilegl).
set -e
P=${PKG:-com.anland.consumer.mobilegl}
SHARE=${SHARE:-/data/local/tmp/anland-mobilegl}
D=$(dirname $(pm path $P | sed 's/package://'))/lib/arm64
ls -laZ $D
[ -f $SHARE/libMobileGL.so.orig ] || cp $D/libMobileGL.so $SHARE/libMobileGL.so.orig
am force-stop $P
CTX=$(ls -Z $D/libMobileGL.so | awk '{print $1}')
OWN=$(stat -c %u:%g $D/libMobileGL.so)
cp $SHARE/libMobileGL.new.so $D/libMobileGL.so
chown $OWN $D/libMobileGL.so; chmod 755 $D/libMobileGL.so; chcon $CTX $D/libMobileGL.so
ls -laZ $D/libMobileGL.so
@@ -0,0 +1,17 @@
# switch.sh <DirectGLES|DirectVulkan> - (Android root shell) change the MobileGL backend the anland
# app's server runs (Espryt = DirectGLES, Magma = DirectVulkan). The choice is the app's setting
# (Settings > Connection > MobileGL desktop, saved in the app's files/mobilegl-backend); this does
# what a user does there: save it, Stop desktop, open the app again. The new server publishes the
# backend and the container's session copies it to /etc/mobilegl/backend before it starts, so
# server and clients agree. ~30-60 s. No setprop: debug.mobilegl.backend, if set, still overrides
# the setting until the next reboot - this clears it.
# Env: PKG, SOCK.
B=${1:?usage: switch.sh DirectGLES|DirectVulkan}
P=${PKG:-com.anland.consumer.mobilegl}
[ -n "$(getprop debug.mobilegl.backend)" ] && setprop debug.mobilegl.backend ""
if pidof $P:mobilegl > /dev/null; then
am startservice -n $P/com.anland.consumer.MobileGLWorker -a com.anland.consumer.mobilegl.STOP > /dev/null
i=0; while pidof $P:mobilegl > /dev/null && [ $i -lt 60 ]; do sleep 1; i=$((i + 1)); done
fi
am start -n $P/com.anland.consumer.MainActivity --es mobilegl_backend $B > /dev/null
echo "backend $B saved; the desktop is starting (watch: sh $(dirname $0)/status.sh)"
@@ -0,0 +1,8 @@
# unlock.sh - (Android root shell) ask logind to unlock the desktop user's session. UNVERIFIED that this
# helps here: the handoff notes this logind session type does not support lock-session (a real lock
# screen then needs the password). Prevention: [Daemon] Autolock=false in ~/.config/kscreenlockerrc.
# Env: DS, CONTAINER, DESKTOP_USER.
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
C=${CONTAINER:-arch-kde-mgl}
U=${DESKTOP_USER:-swung0x48}
$D --name=$C run bash -lc "S=\$(loginctl list-sessions --no-legend | awk '\$3==\"$U\"{print \$1}' | head -1); loginctl unlock-session \$S; echo unlocked \$S"
@@ -0,0 +1,47 @@
# x11-measure.sh <label> <app-process-name> [seconds] - (Android root shell) CPU of the X11 path while
# an X11 app runs: % of ONE core (800 = all 8 cores) for the whole phone, the app, Xwayland,
# kwin_wayland, the app's :mobilegl server; mean kgsl gpu_busy_percentage; and the number of dma-buf
# descriptors Xwayland and KWin hold (window pixmaps / client buffers as dma-bufs). Refuses to run
# below 40% battery: the phone drains faster under load than USB charges it.
# Run: su -c "sh /data/local/tmp/anl/x11-measure.sh glamor glxgears 10" Env: PKG, MIN_BATTERY.
L=${1:-sample}
APP=${2:-glxgears}
N=${3:-10}
P=${PKG:-com.anland.consumer.mobilegl}
GPU=/sys/class/kgsl/kgsl-3d0/gpu_busy_percentage
level=$(dumpsys battery | sed -n 's/^ *level: *//p')
if [ "${level:-0}" -lt "${MIN_BATTERY:-40}" ]; then
echo "battery ${level}% < ${MIN_BATTERY:-40}%: not loading the phone; charge first" >&2
exit 3
fi
ticks() {
t=0
for p in "$@"; do
[ -r /proc/$p/stat ] || continue
v=$(sed 's/^.*) //' /proc/$p/stat | awk '{print $12 + $13}')
t=$((t + v))
done
echo $t
}
total() { awk '/^cpu /{print $2+$3+$4+$6+$7+$8}' /proc/stat; }
dmabufs() { # dma-buf descriptors held by the pids (Android kernels link them to /dmabuf:...)
n=0
for p in "$@"; do n=$((n + $(ls -l /proc/$p/fd 2>/dev/null | grep -c dmabuf))); done
echo $n
}
AP="$(pidof $APP)"
XW="$(pidof Xwayland)"
KW="$(pidof kwin_wayland)"
SV="$(pidof $P:mobilegl)"
[ -n "$AP" ] || { echo "no running process named $APP" >&2; exit 2; }
a0=$(total); p0=$(ticks $AP); x0=$(ticks $XW); k0=$(ticks $KW); v0=$(ticks $SV)
g=0; i=0
while [ $i -lt $N ]; do
sleep 1
b=$(cat $GPU 2>/dev/null | tr -dc '0-9'); g=$((g + ${b:-0})); i=$((i + 1))
done
a1=$(total); p1=$(ticks $AP); x1=$(ticks $XW); k1=$(ticks $KW); v1=$(ticks $SV)
pct() { echo $(( ($2 - $1) / N )); }
printf '%-12s %3ss phone %4s%% %s %3s%% Xwayland %3s%% kwin %3s%% server %3s%% gpu %3s%% dmabufs Xwayland %s kwin %s battery %s%%\n' \
"$L" "$N" "$(pct $a0 $a1)" "$APP" "$(pct $p0 $p1)" "$(pct $x0 $x1)" "$(pct $k0 $k1)" "$(pct $v0 $v1)" "$((g / N))" \
"$(dmabufs $XW)" "$(dmabufs $KW)" "$level"
@@ -0,0 +1,41 @@
# env.sh - shared parameters for the host-side (Git Bash) scripts of the anland-mobilegl-plasma skill.
# Sourced, never run. Every value can be overridden from the environment before calling a script.
#
# MGL_WORKTREE MobileGL worktree (Windows form C:/...). Default: the worktree this skill lives in.
# ANL_DIR Local scratch dir: channel files, screenshots, xdist/. Default: %LOCALAPPDATA%/Temp/anl
# ANDROID_NDK NDK used for the Android server lib. Default: <SDK>/ndk/27.2.12479018
# MGL_PKG Anland APK that embeds the server. Default: com.anland.consumer.mobilegl
# MGL_CONTAINER Droidspaces container. Default: arch-kde-mgl
# MGL_DESKTOP_USER Desktop user inside the container. Default: swung0x48
# DS_BIN droidspaces binary on the phone. Default: /data/local/Droidspaces/bin/droidspaces
# DEV_TOOLS Phone dir for device/*.sh and channel files. Default: /data/local/tmp/anl
# DEV_SHARE Phone dir bind-mounted into the container as /run/anland-mobilegl (screenshots,
# server lib staging, display daemon socket). Default: /data/local/tmp/anland-mobilegl
# ANDROID_SERIAL Honoured by adb itself; leave unset when only one device is attached
# (USB serial or a wireless "ip:port").
_skill_scripts=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
if [ -z "${MGL_WORKTREE:-}" ]; then
MGL_WORKTREE=$(cd "$_skill_scripts/../../../.." && pwd -W 2>/dev/null || pwd)
fi
: "${ANL_DIR:=$(cygpath -m "${LOCALAPPDATA:-C:/Users/$USERNAME/AppData/Local}" 2>/dev/null || echo /tmp)/Temp/anl}"
: "${ANDROID_NDK:=$(cygpath -m "${LOCALAPPDATA:-C:/Users/$USERNAME/AppData/Local}" 2>/dev/null)/Android/Sdk/ndk/27.2.12479018}"
: "${MGL_PKG:=com.anland.consumer.mobilegl}"
: "${MGL_CONTAINER:=arch-kde-mgl}"
: "${MGL_DESKTOP_USER:=swung0x48}"
: "${DS_BIN:=/data/local/Droidspaces/bin/droidspaces}"
: "${DEV_TOOLS:=/data/local/tmp/anl}"
: "${DEV_SHARE:=/data/local/tmp/anland-mobilegl}"
# The container rootfs as Android root sees it (the container's /tmp is NOT visible here).
CT_ROOT=/mnt/Droidspaces/$MGL_CONTAINER
export MGL_WORKTREE ANL_DIR ANDROID_NDK MGL_PKG MGL_CONTAINER MGL_DESKTOP_USER DS_BIN DEV_TOOLS DEV_SHARE CT_ROOT
# Git Bash would rewrite /data/... arguments into C:/Program Files/Git/data/...; keep them literal.
# Consequence: pass LOCAL paths to Windows tools (adb, cmake, git) in C:/ form, never /c/.
export MSYS_NO_PATHCONV=1
adb_() { MSYS2_ARG_CONV_EXCL='*' adb "$@"; }
# C:/x/y -> /mnt/c/x/y (for WSL).
win2wsl() { local p=$1 d; d=$(printf '%s' "${p:0:1}" | tr 'A-Z' 'a-z'); printf '/mnt/%s%s' "$d" "${p:2}"; }
mkdir -p "$ANL_DIR"
@@ -0,0 +1,146 @@
#!/usr/bin/env bash
# kwin-recover.sh [rounds] - does the desktop keep working when KWin's OWN MobileGL session is lost?
# Starts animated clients (glmark2-es2-wayland: Wayland, zero-copy shared images; glxgears: X11 through
# Xwayland's GLX; with CHROME=1 also Chrome on an animated WebGL page), then per round loses the server
# session of KWin with the pid-targeted debug knob (debug.mobilegl.inject_device_lost_pid, KWin's pid as
# it sees itself in the container), waits, and checks. A round PASSES when:
# - the server latched KWin's session (the server log file: "injected:pid=<kwin>"; logcat is too small);
# - KWin, plasmashell and every test client are the SAME processes (nothing restarted);
# - KWin restarted compositing on a fresh session (its journal: "compositing restarted on a fresh
# MobileGL session"; its client log: one more "RECOVERED");
# - the screen still animates (shot.sh hashes differ) and the clients keep presenting;
# - no native crash (logcat -b crash).
# Between rounds the knob is reset to 0 while the clients run, which re-arms it for the same pid.
# Stops early below MIN_BATTERY percent (the phone drains faster under load than USB charges it).
#
# Deploy first (same MobileGL commit on both sides; see SKILL.md):
# bash scripts/build-android.sh && bash scripts/cyc.sh pre # server -> APK, restarts the session
# bash scripts/xbuild.sh && bash scripts/xdeploy.sh # client -> container
# ANLAND_WORKTREE=<anland worktree> bash scripts/kwin-stage.sh build # kwin.patch + anland backend
# (wait for "== ALL DONE ==" in /root/kwin-build.log), then: adb shell 'su -c "sh /data/local/tmp/anl/run-plasma.sh"'
# Env: ROUNDS (default 2), SETTLE (s after the loss, default 10), REARM (s with the knob at 0 between
# rounds, default 4), MIN_BATTERY (default 40), CHROME (1: add Chrome), CHANNEL (ct.sh channel,
# default krec), TAG (screenshot/log tag, default krec).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
ROUNDS=${1:-${ROUNDS:-2}}
SETTLE=${SETTLE:-10}
REARM=${REARM:-4}
MIN_BATTERY=${MIN_BATTERY:-40}
CH=${CHANNEL:-krec}
TAG=${TAG:-krec}
COMPOSITOR_LOG=/tmp/mobilegl-compositor.client.log
SERVER_LOG=/data/data/$MGL_PKG/files/mobilegl-server.server.log
ct() { bash "$here/ct.sh" "$CH"; }
battery() { adb_ shell dumpsys battery | tr -d '\r' | awk '/ level:/ {print $2}'; }
stop_clients() {
ct <<'IN' || true
pkill -x glmark2-es2-way 2>/dev/null; pkill -x glmark2-es2-wayland 2>/dev/null; pkill -x glxgears 2>/dev/null
true
IN
}
cleanup() {
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'" || true
stop_clients
}
check_battery() {
local level; level=$(battery)
echo "battery: $level%"
if [ -n "$level" ] && [ "$level" -lt "$MIN_BATTERY" ]; then
echo "STOP: battery below $MIN_BATTERY%"; exit 3
fi
}
trap cleanup EXIT
check_battery
adb_ shell "su -c 'sh $DEV_TOOLS/status.sh'" || true
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'"
# The clients, each with its own log (mgrun's MGLOG), in the background for the whole run.
ct <<'IN'
rm -f /tmp/mgl-krec-gm.client.log /tmp/mgl-krec-gx.client.log /tmp/krec-gm.out /tmp/krec-gx.out
MGLOG=/tmp/mgl-krec-gm.log nohup timeout 900 mgrun offscreen glmark2-es2-wayland -s 960x540 --run-forever \
> /tmp/krec-gm.out 2>&1 < /dev/null &
MGLOG=/tmp/mgl-krec-gx.log nohup timeout 900 mgrun offscreen glxgears -info > /tmp/krec-gx.out 2>&1 < /dev/null &
sleep 8
echo "glmark2: $(pgrep -x glmark2-es2-way || pgrep -x glmark2-es2-wayland) glxgears: $(pgrep -x glxgears)"
grep -aE "presented through|could not be presented" /tmp/mgl-krec-gm.client.log | head -2
grep -aE "presented through|readback" /tmp/mgl-krec-gx.client.log | head -2
IN
if [ "${CHROME:-0}" = 1 ]; then
ct <<'IN'
cat > /tmp/mgl-krec-spin.html <<'HTML'
<!doctype html><html><body style="margin:0;background:#000">
<canvas id=c width=640 height=360 style="width:100vw;height:100vh"></canvas>
<script>
const gl = document.getElementById('c').getContext('webgl'); let t = 0;
function frame() { t++; if (gl && !gl.isContextLost()) { gl.clearColor((t % 60) / 60, 0.2, 0.6, 1); gl.clear(gl.COLOR_BUFFER_BIT); }
requestAnimationFrame(frame); }
frame();
</script></body></html>
HTML
chmod 644 /tmp/mgl-krec-spin.html
IN
WAIT=${WAIT:-20} CHANNEL=$CH bash "$here/chrome-launch.sh" "$TAG" --new-window "file:///tmp/mgl-krec-spin.html"
fi
# KWin, plasmashell and the clients, as pids inside the container; how often KWin recovered so far.
snapshot() {
ct <<IN | tr -d '\r' | tail -1
k=\$(pgrep -x kwin_wayland | head -1); p=\$(pgrep -x plasmashell | head -1)
g=\$(pgrep -x glmark2-es2-way || pgrep -x glmark2-es2-wayland); x=\$(pgrep -x glxgears | head -1)
c=\$(pgrep -x chrome | head -1)
r=\$(grep -ac "RECOVERED" $COMPOSITOR_LOG 2>/dev/null || true)
f=\$(grep -ac "FPS" /tmp/krec-gm.out 2>/dev/null || true)
echo "\${k:-none} \${p:-none} \${g:-none} \${x:-none} \${c:-none} \${r:-0} \${f:-0}"
IN
}
pass=0
for round in $(seq 1 "$ROUNDS"); do
echo "=== round $round/$ROUNDS"
check_battery
read -r kwin plasma gm gx chrome recovered fps < <(snapshot)
echo "kwin=$kwin plasmashell=$plasma glmark2=$gm glxgears=$gx chrome=$chrome recoveries=$recovered fps-lines=$fps"
if [ "$kwin" = none ]; then echo "FAIL: KWin is not running"; break; fi
bash "$here/shot.sh" "$TAG-r$round-pre" 2 1
srvlines=$(adb_ shell "su -c 'wc -l < $SERVER_LOG'" | tr -dc '0-9')
adb_ logcat -b crash -c || true
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid $kwin'"
sleep "$SETTLE"
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'"
bash "$here/shot.sh" "$TAG-r$round-post" 3 1 | tee "$ANL_DIR/$TAG-r$round-post.md5"
read -r kwin2 plasma2 gm2 gx2 chrome2 recovered2 fps2 < <(snapshot)
echo "after: kwin=$kwin2 plasmashell=$plasma2 glmark2=$gm2 glxgears=$gx2 chrome=$chrome2 recoveries=$recovered2 fps-lines=$fps2"
adb_ shell "su -c 'tail -n +$((srvlines + 1)) $SERVER_LOG'" > "$ANL_DIR/$TAG-r$round-server.log"
latched=$(grep -c "injected:pid=$kwin" "$ANL_DIR/$TAG-r$round-server.log" || true)
grep -aE "inject_device_lost_pid|SessionLatch|session #[0-9]+ (started|pid)|owner=server|lease" \
"$ANL_DIR/$TAG-r$round-server.log" | cut -c1-220 | head -14 || true
ct <<IN || true
echo "--- KWin's MobileGL client log:"; grep -aE "DEVICE LOST|RECOVERED|GL_UNKNOWN_CONTEXT_RESET|re-created on the fresh|Refuse" $COMPOSITOR_LOG | tail -6
echo "--- KWin journal:"; journalctl --no-pager --since -40s 2>/dev/null | grep -aE "kwin_wayland" | grep -aiE "reset|restarted|MobileGL swap|context|fatal|abort" | tail -10
echo "--- clients:"; grep -aE "DEVICE LOST|lost|Fatal" /tmp/mgl-krec-gm.client.log /tmp/mgl-krec-gx.client.log | tail -4
IN
crashes=$(adb_ logcat -b crash -d | grep -c "Fatal signal\|Abort message" || true)
hashes=$(awk '{print $1}' "$ANL_DIR/$TAG-r$round-post.md5" | sort -u | wc -l)
verdict=PASS
[ "$latched" -ge 1 ] || { echo " - the server never latched KWin's session (pid $kwin)"; verdict=FAIL; }
[ "$kwin2" = "$kwin" ] || { echo " - KWin is a different process ($kwin -> $kwin2): it restarted"; verdict=FAIL; }
[ "$plasma2" = "$plasma" ] || { echo " - plasmashell restarted ($plasma -> $plasma2)"; verdict=FAIL; }
[ "$gm2" = "$gm" ] && [ "$gx2" = "$gx" ] || { echo " - a test client restarted or died"; verdict=FAIL; }
[ "${CHROME:-0}" != 1 ] || [ "$chrome2" = "$chrome" ] || { echo " - Chrome restarted"; verdict=FAIL; }
[ "$recovered2" -gt "$recovered" ] || { echo " - KWin's client log shows no new RECOVERED session"; verdict=FAIL; }
[ "$fps2" -gt "$fps" ] || { echo " - glmark2 stopped reporting frames"; verdict=FAIL; }
[ "$hashes" -ge 2 ] || { echo " - the screen does not change after the loss (frozen)"; verdict=FAIL; }
[ "$crashes" -eq 0 ] || { echo " - native crash(es):"; adb_ logcat -b crash -d | tail -8; verdict=FAIL; }
echo "round $round: $verdict"
[ "$verdict" = PASS ] && pass=$((pass + 1))
[ "$verdict" = PASS ] || break
# Re-arm the knob for the same pid: a session must observe 0 before the next value counts.
sleep "$REARM"
done
echo "=== $pass/$ROUNDS rounds passed; screenshots in $ANL_DIR/$TAG-r*-*.png"
[ "$pass" = "$ROUNDS" ]
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# kwin-stage.sh [build] - stage anland's KWin changes (kwin.patch, anland backend sources,
# mobilegl-startup.sh, sync-build-kwin.sh) for the in-container KWin rebuild; with "build", also run
# kwprep.sh (apply changed patch hunks / backend files to the live tree) and start sync-build-kwin.sh
# (ninja -j4 + atomic install into /opt/mobilegl/kwin, screencast plugin, startup script) in the
# background, log /root/kwin-build.log. KWin is NOT restarted: run device/run-plasma.sh afterwards.
# Poll: printf 'tail -5 /root/kwin-build.log\n' | bash ct.sh kw
#
# Two staging places, because the two container scripts read different ones:
# /root/kwsync.tgz <- kwprep.sh (installed by push-tools.sh)
# $DEV_SHARE/kwin-sync = /run/anland-mobilegl/kwin-sync <- sync-build-kwin.sh (as kwin-sync/sync-build.sh)
# All files are CR-stripped: the Windows checkout of anland has CRLF backend sources and a CRLF
# sync-build-kwin.sh (bash then fails on $'\r').
# Assembled from kwprep.sh + sync-build-kwin.sh; not exercised as one unit yet.
# Env: ANLAND_WORKTREE (anland checkout on branch legacy-mobilegl-unified, Windows form),
# CHANNEL (ct.sh channel, default kw).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
: "${ANLAND_WORKTREE:=$(cygpath -m "$USERPROFILE" 2>/dev/null)/AndroidStudioProjects/anland/.claude/worktrees/mgl-unified}"
K=$ANLAND_WORKTREE/producers/kde
st=$ANL_DIR/kwstage
rm -rf "$st"; mkdir -p "$st/kwsync/Arch_v5" "$st/kwsync/anland_backend_Arch_v5/src/backends/anland" "$st/kwin-sync/anland" "$st/kwin-sync/misc"
lf() { tr -d '\r' < "$1" > "$2"; }
for f in "$K"/anland_backend_Arch_v5/src/backends/anland/*; do
b=$(basename "$f"); [ "$b" = libdisplay_producer ] && continue
lf "$f" "$st/kwsync/anland_backend_Arch_v5/src/backends/anland/$b"
lf "$f" "$st/kwin-sync/anland/$b"
done
for f in kwin.patch mobilegl-startup.sh desktop-session-mobilegl.conf; do
lf "$K/Arch_v5/$f" "$st/kwsync/Arch_v5/$f"
lf "$K/Arch_v5/$f" "$st/kwin-sync/misc/$f"
done
lf "$K/Arch_v5/sync-build-kwin.sh" "$st/kwin-sync/sync-build.sh"
(cd "$st/kwsync" && tar --force-local -czf ../kwsync.tgz Arch_v5 anland_backend_Arch_v5)
tar --force-local -C "$st" -czf "$st/kwin-sync.tgz" kwin-sync
adb_ push "$st/kwsync.tgz" "$DEV_TOOLS/kwsync.tgz" >/dev/null
adb_ push "$st/kwin-sync.tgz" "$DEV_TOOLS/kwin-sync.tgz" >/dev/null
adb_ shell "su -c 'cp $DEV_TOOLS/kwsync.tgz $CT_ROOT/root/kwsync.tgz && tar xzf $DEV_TOOLS/kwin-sync.tgz -C $DEV_SHARE && chmod -R a+rX $DEV_SHARE/kwin-sync && rm -f $DEV_TOOLS/kwsync.tgz $DEV_TOOLS/kwin-sync.tgz && echo staged'"
if [ "${1:-}" = build ]; then
bash "$here/ct.sh" "${CHANNEL:-kw}" <<'IN'
bash /root/kwprep.sh || exit 1
nohup bash /run/anland-mobilegl/kwin-sync/sync-build.sh > /root/kwin-build.log 2>&1 < /dev/null &
echo "kwin build started (pid $!), log /root/kwin-build.log; it ends with '== ALL DONE =='"
IN
fi
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
# cdpmedia.py [port] [seconds] - ask a Chrome started with --remote-debugging-port=<port> (and a
# non-default --user-data-dir) which video decoder its first page's players use, and evaluate
# `window.__mglStats` / the element `#t` text of the page. Standard library only (no websocket
# module in the container): a minimal RFC 6455 client.
# python3 cdpmedia.py 9333 6
# Prints kVideoDecoderName / kIsPlatformVideoDecoder / kVideoTracks properties and media errors as
# they arrive, then the page's stats text.
import base64, json, os, socket, struct, sys, time, urllib.request
port = int(sys.argv[1]) if len(sys.argv) > 1 else 9333
seconds = float(sys.argv[2]) if len(sys.argv) > 2 else 6.0
targets = json.load(urllib.request.urlopen(f"http://127.0.0.1:{port}/json"))
page = next(t for t in targets if t.get("type") == "page")
url = page["webSocketDebuggerUrl"]
path = url.split(f"127.0.0.1:{port}", 1)[1]
sock = socket.create_connection(("127.0.0.1", port))
key = base64.b64encode(os.urandom(16)).decode()
sock.sendall((f"GET {path} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
f"Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n").encode())
response = b""
while b"\r\n\r\n" not in response:
response += sock.recv(4096)
buffer = response.split(b"\r\n\r\n", 1)[1]
def send(message):
data = json.dumps(message).encode()
header = bytes([0x81])
mask = os.urandom(4)
if len(data) < 126:
header += bytes([0x80 | len(data)])
elif len(data) < 65536:
header += bytes([0x80 | 126]) + struct.pack(">H", len(data))
else:
header += bytes([0x80 | 127]) + struct.pack(">Q", len(data))
sock.sendall(header + mask + bytes(b ^ mask[i % 4] for i, b in enumerate(data)))
def recv_exact(n):
global buffer
while len(buffer) < n:
chunk = sock.recv(65536)
if not chunk:
raise EOFError
buffer += chunk
out, buffer = buffer[:n], buffer[n:]
return out
def recv():
first, second = recv_exact(2)
length = second & 0x7F
if length == 126:
length = struct.unpack(">H", recv_exact(2))[0]
elif length == 127:
length = struct.unpack(">Q", recv_exact(8))[0]
payload = recv_exact(length)
return json.loads(payload) if first & 0x0F == 1 else None
send({"id": 1, "method": "Media.enable"})
# Players created before the domain was enabled report nothing: reload so the page makes new ones.
if os.environ.get("CDP_NAVIGATE"):
send({"id": 3, "method": "Page.navigate", "params": {"url": os.environ["CDP_NAVIGATE"]}})
elif os.environ.get("CDP_RELOAD", "1") == "1":
send({"id": 3, "method": "Page.reload"})
deadline = time.time() + seconds
sock.settimeout(0.5)
seen = set()
while time.time() < deadline:
try:
message = recv()
except socket.timeout:
continue
if not message or "method" not in message:
continue
if message["method"] == "Media.playerPropertiesChanged":
for prop in message["params"]["properties"]:
if prop["name"] in ("kVideoDecoderName", "kIsPlatformVideoDecoder", "kVideoTracks", "kResolution"):
line = f"{prop['name']} = {prop['value']}"
if line not in seen:
seen.add(line)
print(line)
elif message["method"] in ("Media.playerErrorsRaised",):
print("media error:", json.dumps(message["params"])[:300])
send({"id": 2, "method": "Runtime.evaluate",
"params": {"expression": os.environ.get("CDP_EXPR", "(document.getElementById('t')||{}).textContent"), "returnByValue": True}})
sock.settimeout(3)
while True:
message = recv()
if message and message.get("id") == 2:
print("page:", message.get("result", {}).get("result", {}).get("value"))
break
@@ -0,0 +1,34 @@
// dmahash <pid> : for each dma-buf fd of <pid> at least 1 MB, print inode, size and a sampled checksum.
#include <dirent.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
int main(int argc, char** argv) {
char dir[64];
snprintf(dir, sizeof dir, "/proc/%s/fd", argv[1]);
DIR* d = opendir(dir);
if (!d) { perror("opendir"); return 1; }
struct dirent* e;
while ((e = readdir(d))) {
char p[128], l[128] = {0};
snprintf(p, sizeof p, "%s/%s", dir, e->d_name);
if (readlink(p, l, sizeof l - 1) <= 0 || strncmp(l, "/dmabuf", 7) != 0) continue;
int fd = open(p, O_RDONLY);
if (fd < 0) continue;
struct stat st; fstat(fd, &st);
if (st.st_size < (1 << 20)) { close(fd); continue; }
const uint8_t* m = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (m == MAP_FAILED) { printf("fd %s ino %lu mmap failed\n", e->d_name, (unsigned long)st.st_ino); close(fd); continue; }
uint64_t h = 1469598103934665603ull; unsigned nz = 0;
for (off_t i = 0; i < st.st_size; i += 64) { h = (h ^ m[i]) * 1099511628211ull; nz += m[i] != 0; }
printf("fd %s ino %lu size %ld hash %016llx nz %u\n", e->d_name, (unsigned long)st.st_ino, (long)st.st_size,
(unsigned long long)h, nz);
munmap((void*)m, st.st_size); close(fd);
}
return 0;
}
@@ -0,0 +1,55 @@
// gpuprobe - a plain Android process (outside MobileGL) that measures whether the GPU keeps executing
// other contexts' work: a pbuffer GLES context clears + draws and glFinish()es every ~10 ms, printing any
// round trip over 50 ms with a CLOCK_REALTIME timestamp. Usage: gpuprobe <seconds> [priority: low|med|high]
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES3/gl3.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#ifndef EGL_CONTEXT_PRIORITY_LEVEL_IMG
#define EGL_CONTEXT_PRIORITY_LEVEL_IMG 0x3100
#define EGL_CONTEXT_PRIORITY_HIGH_IMG 0x3101
#define EGL_CONTEXT_PRIORITY_MEDIUM_IMG 0x3102
#define EGL_CONTEXT_PRIORITY_LOW_IMG 0x3103
#endif
static double now_ms(void) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); return ts.tv_sec * 1e3 + ts.tv_nsec / 1e6; }
int main(int argc, char** argv) {
int seconds = argc > 1 ? atoi(argv[1]) : 20;
EGLint prio = EGL_CONTEXT_PRIORITY_MEDIUM_IMG;
if (argc > 2 && !strcmp(argv[2], "high")) prio = EGL_CONTEXT_PRIORITY_HIGH_IMG;
if (argc > 2 && !strcmp(argv[2], "low")) prio = EGL_CONTEXT_PRIORITY_LOW_IMG;
setvbuf(stdout, NULL, _IOLBF, 0);
EGLDisplay d = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(d, NULL, NULL);
const EGLint ca[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_RED_SIZE, 8, EGL_NONE};
EGLConfig c; EGLint n;
eglChooseConfig(d, ca, &c, 1, &n);
const EGLint pa[] = {EGL_WIDTH, 256, EGL_HEIGHT, 256, EGL_NONE};
EGLSurface s = eglCreatePbufferSurface(d, c, pa);
const EGLint xa[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_CONTEXT_PRIORITY_LEVEL_IMG, prio, EGL_NONE};
EGLContext x = eglCreateContext(d, c, EGL_NO_CONTEXT, xa);
if (x == EGL_NO_CONTEXT) { printf("no ctx 0x%x\n", eglGetError()); return 1; }
eglMakeCurrent(d, s, s, x);
EGLint got = 0; eglQueryContext(d, x, EGL_CONTEXT_PRIORITY_LEVEL_IMG, &got);
const double t0 = now_ms(); double maxRt = 0; long iters = 0, slow = 0;
printf("[probe] start %.0f prio=0x%x\n", t0, got);
while (now_ms() - t0 < seconds * 1000.0) {
const double a = now_ms();
glClearColor((iters & 1) ? 1.f : 0.f, 0.f, 0.f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);
glFinish();
const double rt = now_ms() - a;
if (rt > maxRt) maxRt = rt;
if (rt > 50) { ++slow; printf("[probe] glFinish %.0f ms ending at %.0f\n", rt, now_ms()); }
++iters;
usleep(10000);
}
printf("[probe] END iters=%ld max=%.0f ms slow=%ld\n", iters, maxRt, slow);
return 0;
}
@@ -0,0 +1,150 @@
// hangclient - a Wayland xdg-toplevel, EGL + desktop GL 3.3 core client that animates and, on request,
// submits fragment work that never ends (the driver has to reset it). Prints per-frame gaps so the same
// binary measures "other app" frame pacing (frame callbacks come from the compositor) when run with -H -1.
//
// hangclient -T tag [-H hang_at_frame] [-N hang_repeat] [-I iters] [-S seconds] [-W w] [-X h] [-F]
// -H n frame at which the hanging draw is submitted (-1: never; default -1)
// -N k how many consecutive hanging frames (default 1)
// -E e re-arm a hang every e frames after the first (0: once)
// -I i loop count of the hanging shader (default 2^31-1)
// -S s run time in seconds (default 30)
// -F glFinish after every frame (puts the wait inside the client's own session)
#define GL_GLEXT_PROTOTYPES 1
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GL/glcorearb.h>
#include <wayland-client.h>
#include <wayland-egl.h>
#include "xdg-shell-client-protocol.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
static struct wl_compositor* g_comp;
static struct xdg_wm_base* g_wm;
static int g_configured, g_closed;
static int g_w = 480, g_h = 360;
static void reg_global(void* d, struct wl_registry* r, uint32_t name, const char* iface, uint32_t ver) {
(void)d;
if (!strcmp(iface, "wl_compositor")) g_comp = wl_registry_bind(r, name, &wl_compositor_interface, 4);
else if (!strcmp(iface, "xdg_wm_base")) g_wm = wl_registry_bind(r, name, &xdg_wm_base_interface, 1);
}
static void reg_remove(void* d, struct wl_registry* r, uint32_t n) { (void)d; (void)r; (void)n; }
static const struct wl_registry_listener reg_l = {reg_global, reg_remove};
static void wm_ping(void* d, struct xdg_wm_base* wm, uint32_t s) { (void)d; xdg_wm_base_pong(wm, s); }
static const struct xdg_wm_base_listener wm_l = {wm_ping};
static void xs_conf(void* d, struct xdg_surface* s, uint32_t serial) { (void)d; xdg_surface_ack_configure(s, serial); g_configured = 1; }
static const struct xdg_surface_listener xs_l = {xs_conf};
static void tl_conf(void* d, struct xdg_toplevel* t, int32_t w, int32_t h, struct wl_array* st) { (void)d; (void)t; (void)st; (void)w; (void)h; }
static void tl_close(void* d, struct xdg_toplevel* t) { (void)d; (void)t; g_closed = 1; }
static const struct xdg_toplevel_listener tl_l = {tl_conf, tl_close};
static double now_ms(void) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); return ts.tv_sec * 1e3 + ts.tv_nsec / 1e6; }
static GLuint sh(GLenum type, const char* src) {
GLuint s = glCreateShader(type);
glShaderSource(s, 1, &src, NULL);
glCompileShader(s);
GLint ok = 0; glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
if (!ok) { char log[2048]; glGetShaderInfoLog(s, sizeof log, NULL, log); fprintf(stderr, "shader: %s\n", log); exit(2); }
return s;
}
int main(int argc, char** argv) {
const char* tag = "hc"; int hangAt = -1, hangN = 1, every = 0, seconds = 30, finish = 0; long iters = 2147483647L;
for (int i = 1; i < argc; ++i) {
if (!strcmp(argv[i], "-T") && i + 1 < argc) tag = argv[++i];
else if (!strcmp(argv[i], "-H") && i + 1 < argc) hangAt = atoi(argv[++i]);
else if (!strcmp(argv[i], "-N") && i + 1 < argc) hangN = atoi(argv[++i]);
else if (!strcmp(argv[i], "-E") && i + 1 < argc) every = atoi(argv[++i]);
else if (!strcmp(argv[i], "-I") && i + 1 < argc) iters = atol(argv[++i]);
else if (!strcmp(argv[i], "-S") && i + 1 < argc) seconds = atoi(argv[++i]);
else if (!strcmp(argv[i], "-W") && i + 1 < argc) g_w = atoi(argv[++i]);
else if (!strcmp(argv[i], "-X") && i + 1 < argc) g_h = atoi(argv[++i]);
else if (!strcmp(argv[i], "-F")) finish = 1;
}
setvbuf(stdout, NULL, _IOLBF, 0);
struct wl_display* dpy = wl_display_connect(NULL);
if (!dpy) { fprintf(stderr, "no wayland\n"); return 1; }
struct wl_registry* reg = wl_display_get_registry(dpy);
wl_registry_add_listener(reg, &reg_l, NULL);
wl_display_roundtrip(dpy);
if (!g_comp || !g_wm) { fprintf(stderr, "no compositor/xdg_wm_base\n"); return 1; }
xdg_wm_base_add_listener(g_wm, &wm_l, NULL);
struct wl_surface* surf = wl_compositor_create_surface(g_comp);
struct xdg_surface* xs = xdg_wm_base_get_xdg_surface(g_wm, surf);
xdg_surface_add_listener(xs, &xs_l, NULL);
struct xdg_toplevel* tl = xdg_surface_get_toplevel(xs);
xdg_toplevel_add_listener(tl, &tl_l, NULL);
xdg_toplevel_set_title(tl, tag);
wl_surface_commit(surf);
while (!g_configured) wl_display_dispatch(dpy);
EGLDisplay ed = eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_KHR, dpy, NULL);
if (!eglInitialize(ed, NULL, NULL)) { fprintf(stderr, "eglInitialize\n"); return 1; }
eglBindAPI(EGL_OPENGL_API);
const EGLint ca[] = {EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_NONE};
EGLConfig cfg; EGLint n = 0;
if (!eglChooseConfig(ed, ca, &cfg, 1, &n) || n < 1) { fprintf(stderr, "no config\n"); return 1; }
const EGLint xa[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, EGL_NONE};
EGLContext ctx = eglCreateContext(ed, cfg, EGL_NO_CONTEXT, xa);
if (ctx == EGL_NO_CONTEXT) { fprintf(stderr, "no context 0x%x\n", eglGetError()); return 1; }
struct wl_egl_window* ew = wl_egl_window_create(surf, g_w, g_h);
EGLSurface es = eglCreatePlatformWindowSurface(ed, cfg, ew, NULL);
eglMakeCurrent(ed, es, es, ctx);
eglSwapInterval(ed, 1);
const char* vs = "#version 330 core\nlayout(location=0) in vec2 p; uniform float t; out vec2 uv;\n"
"void main(){ uv=p*0.5+0.5; gl_Position=vec4(p*0.4+vec2(0.5*sin(t),0.0),0.0,1.0);}";
const char* fs = "#version 330 core\nin vec2 uv; uniform int iters; uniform float t; out vec4 c;\n"
"void main(){ float a=uv.x; for(int i=0;i<iters;++i){ a=fract(a*1.0001+0.37+uv.y); }\n"
" c=vec4(uv, 0.5+0.5*sin(t)+a*1e-6, 1.0);}";
GLuint prog = glCreateProgram();
glAttachShader(prog, sh(GL_VERTEX_SHADER, vs));
glAttachShader(prog, sh(GL_FRAGMENT_SHADER, fs));
glLinkProgram(prog);
GLint ok = 0; glGetProgramiv(prog, GL_LINK_STATUS, &ok);
if (!ok) { fprintf(stderr, "link failed\n"); return 2; }
GLint uIt = glGetUniformLocation(prog, "iters"), uT = glGetUniformLocation(prog, "t");
GLuint vao, vbo; glGenVertexArrays(1, &vao); glBindVertexArray(vao);
const float q[] = {-1, -1, 1, -1, -1, 1, 1, 1};
glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, sizeof q, q, GL_STATIC_DRAW);
glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0);
const double t0 = now_ms(); double last = t0, maxGap = 0; long frames = 0, gaps100 = 0;
printf("[%s] start %.0f renderer=%s hangAt=%d N=%d every=%d iters=%ld\n", tag, t0, glGetString(GL_RENDERER), hangAt, hangN, every, iters);
while (!g_closed && now_ms() - t0 < seconds * 1000.0) {
wl_display_dispatch_pending(dpy);
const double t = (now_ms() - t0) / 1000.0;
int hang = 0;
if (hangAt >= 0 && frames >= hangAt) {
long rel = frames - hangAt;
if (every > 0) rel %= every;
hang = rel < hangN;
}
glViewport(0, 0, g_w, g_h);
glClearColor(0.5f + 0.5f * (float)sin(t * 3.0), 0.2f, 0.4f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(prog);
glUniform1i(uIt, hang ? (int)iters : 0);
glUniform1f(uT, (float)t);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
if (hang) printf("[%s] HANG submitted frame %ld at %.0f\n", tag, frames, now_ms());
if (finish) glFinish();
if (!eglSwapBuffers(ed, es)) printf("[%s] swap failed 0x%x at %.0f\n", tag, eglGetError(), now_ms());
const GLenum rs = glGetError();
if (rs != GL_NO_ERROR) printf("[%s] glGetError 0x%x at %.0f\n", tag, rs, now_ms());
const double nowv = now_ms(), gap = nowv - last;
last = nowv; ++frames;
if (gap > maxGap) maxGap = gap;
if (gap > 100.0) { ++gaps100; printf("[%s] GAP %.0f ms ending at %.0f (frame %ld)\n", tag, gap, nowv, frames); }
if (frames % 120 == 0) printf("[%s] frame %ld t=%.1fs fps=%.1f\n", tag, frames, (nowv - t0) / 1e3, frames * 1e3 / (nowv - t0));
}
const double total = now_ms() - t0;
printf("[%s] END frames=%ld fps=%.1f maxGap=%.0f ms gaps>100ms=%ld\n", tag, frames, frames * 1e3 / total, maxGap, gaps100);
return 0;
}
@@ -0,0 +1,95 @@
/* texfmt: does a texture keep its alpha? One opaque red texel and one transparent texel per
* format, sampled across a 64x64 ES3 pbuffer cleared white with SRC_ALPHA blending. Expect the
* left half red and the right half WHITE; a black right half means the alpha was lost. */
#include <EGL/egl.h>
#include <GLES3/gl3.h>
#include <GLES2/gl2ext.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define P(T, n) T n = (T)eglGetProcAddress(#n)
static const char* vs = "#version 300 es\nlayout(location=0) in vec2 p; out vec2 uv; void main(){uv=p*0.5+0.5; gl_Position=vec4(p,0,1);}";
static const char* fs = "#version 300 es\nprecision mediump float; uniform sampler2D t; uniform int mode; in vec2 uv; out vec4 o;"
"void main(){vec4 c=texture(t,uv); o = mode==1 ? vec4(1.0,0.0,0.0,c.a) : (mode==2 ? vec4(c.r,0.0,0.0,c.r) : c);}";
int main(void) {
EGLDisplay d = eglGetDisplay(EGL_DEFAULT_DISPLAY); eglInitialize(d, 0, 0); eglBindAPI(EGL_OPENGL_ES_API);
EGLint ca[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_NONE};
EGLConfig c; EGLint n; eglChooseConfig(d, ca, &c, 1, &n);
EGLint xa[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
EGLContext x = eglCreateContext(d, c, EGL_NO_CONTEXT, xa);
EGLint pa[] = {EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE};
EGLSurface s = eglCreatePbufferSurface(d, c, pa); eglMakeCurrent(d, s, s, x);
P(PFNGLCREATESHADERPROC, glCreateShader); P(PFNGLSHADERSOURCEPROC, glShaderSource); P(PFNGLCOMPILESHADERPROC, glCompileShader);
P(PFNGLCREATEPROGRAMPROC, glCreateProgram); P(PFNGLATTACHSHADERPROC, glAttachShader); P(PFNGLLINKPROGRAMPROC, glLinkProgram);
P(PFNGLUSEPROGRAMPROC, glUseProgram); P(PFNGLGETUNIFORMLOCATIONPROC, glGetUniformLocation); P(PFNGLUNIFORM1IPROC, glUniform1i);
P(PFNGLGENBUFFERSPROC, glGenBuffers); P(PFNGLBINDBUFFERPROC, glBindBuffer); P(PFNGLBUFFERDATAPROC, glBufferData);
P(PFNGLVERTEXATTRIBPOINTERPROC, glVertexAttribPointer); P(PFNGLENABLEVERTEXATTRIBARRAYPROC, glEnableVertexAttribArray);
P(PFNGLDRAWARRAYSPROC, glDrawArrays); P(PFNGLCLEARCOLORPROC, glClearColor); P(PFNGLCLEARPROC, glClear);
P(PFNGLREADPIXELSPROC, glReadPixels); P(PFNGLVIEWPORTPROC, glViewport); P(PFNGLGENTEXTURESPROC, glGenTextures);
P(PFNGLBINDTEXTUREPROC, glBindTexture); P(PFNGLTEXIMAGE2DPROC, glTexImage2D); P(PFNGLTEXPARAMETERIPROC, glTexParameteri);
P(PFNGLENABLEPROC, glEnable); P(PFNGLBLENDFUNCPROC, glBlendFunc); P(PFNGLGETERRORPROC, glGetError);
P(PFNGLTEXSTORAGE2DPROC, glTexStorage2D); P(PFNGLTEXSUBIMAGE2DPROC, glTexSubImage2D); P(PFNGLGETSTRINGPROC, glGetString);
printf("GL_VERSION %s\n", glGetString(GL_VERSION));
printf("BGRA ext: %s\n", strstr((const char*)glGetString(GL_EXTENSIONS), "GL_EXT_texture_format_BGRA8888") ? "yes" : "no");
GLuint v = glCreateShader(GL_VERTEX_SHADER), f = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(v, 1, &vs, 0); glCompileShader(v); glShaderSource(f, 1, &fs, 0); glCompileShader(f);
GLuint pr = glCreateProgram(); glAttachShader(pr, v); glAttachShader(pr, f); glLinkProgram(pr); glUseProgram(pr);
float q[] = {-1,-1, 1,-1, -1,1, 1,1};
GLuint vbo; glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof q, q, GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, 0, 0, 0); glEnableVertexAttribArray(0);
GLint lm = glGetUniformLocation(pr, "mode");
glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
struct { const char* name; GLint ifmt; GLenum fmt; int bpp; int mode; int storage; GLenum sized; unsigned char px[8]; } cases[] = {
{"RGBA/TexImage", GL_RGBA, GL_RGBA, 4, 0, 0, 0, {255,0,0,255, 0,0,0,0}},
{"BGRA_EXT/TexImage", GL_BGRA_EXT, GL_BGRA_EXT, 4, 0, 0, 0, {0,0,255,255, 0,0,0,0}},
{"BGRA8_EXT/TexStorage+Sub", 0, GL_BGRA_EXT, 4, 0, 1, GL_BGRA8_EXT, {0,0,255,255, 0,0,0,0}},
{"RGBA8/TexStorage+Sub", 0, GL_RGBA, 4, 0, 1, GL_RGBA8, {255,0,0,255, 0,0,0,0}},
{"ALPHA", GL_ALPHA, GL_ALPHA, 1, 1, 0, 0, {255, 0}},
{"LUMINANCE_ALPHA", GL_LUMINANCE_ALPHA, GL_LUMINANCE_ALPHA, 2, 1, 0, 0, {255,255, 0,0}},
{"R8/TexStorage+Sub", 0, GL_RED, 1, 2, 1, GL_R8, {255, 0}},
};
for (unsigned i = 0; i < sizeof cases / sizeof cases[0]; ++i) {
GLuint t; glGenTextures(1, &t); glBindTexture(GL_TEXTURE_2D, t);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
while (glGetError()) {}
if (cases[i].storage) {
glTexStorage2D(GL_TEXTURE_2D, 1, cases[i].sized, 2, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, cases[i].fmt, GL_UNSIGNED_BYTE, cases[i].px);
} else {
glTexImage2D(GL_TEXTURE_2D, 0, cases[i].ifmt, 2, 1, 0, cases[i].fmt, GL_UNSIGNED_BYTE, cases[i].px);
}
GLenum err = glGetError();
glUniform1i(lm, cases[i].mode);
glViewport(0, 0, 64, 64); glClearColor(1, 1, 1, 1); glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
unsigned char l[4], r[4];
glReadPixels(16, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, l); glReadPixels(48, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, r);
const int ok = l[0] > 200 && l[1] < 60 && l[2] < 60 && r[0] > 200 && r[1] > 200 && r[2] > 200;
printf("%-26s err=0x%04x left=%3d,%3d,%3d right=%3d,%3d,%3d %s\n", cases[i].name, err, l[0], l[1], l[2], r[0], r[1], r[2], ok ? "ok" : "BAD");
}
/* ANGLE's emulation of ALPHA8 / LUMINANCE_ALPHA8 on a desktop-GL driver: R8 / RG8 + swizzle. */
P(PFNGLTEXPARAMETERIPROC, glTexParameteri2);
struct { const char* name; GLenum sized, fmt; int bpp; GLint sw[4]; unsigned char px[4]; unsigned char wantL[3], wantR[3]; } sz[] = {
{"R8 swizzled as ALPHA8", GL_R8, GL_RED, 1, {GL_ZERO, GL_ZERO, GL_ZERO, GL_RED}, {255, 0}, {0,0,0}, {255,255,255}},
{"RG8 swizzled as LUM_ALPHA", GL_RG8, GL_RG, 2, {GL_RED, GL_RED, GL_RED, GL_GREEN}, {255,255, 0,0}, {255,255,255}, {255,255,255}},
{"RG8 as LUM_ALPHA (lum 0)", GL_RG8, GL_RG, 2, {GL_RED, GL_RED, GL_RED, GL_GREEN}, {0,255, 0,0}, {0,0,0}, {255,255,255}},
};
glUniform1i(lm, 0);
for (unsigned i = 0; i < sizeof sz / sizeof sz[0]; ++i) {
GLuint t; glGenTextures(1, &t); glBindTexture(GL_TEXTURE_2D, t);
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexStorage2D(GL_TEXTURE_2D, 1, sz[i].sized, 2, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, sz[i].fmt, GL_UNSIGNED_BYTE, sz[i].px);
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, sz[i].sw[0]); glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, sz[i].sw[1]);
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, sz[i].sw[2]); glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, sz[i].sw[3]);
GLenum err = glGetError();
glClearColor(1, 1, 1, 1); glClear(GL_COLOR_BUFFER_BIT); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
unsigned char l[4], r[4];
glReadPixels(16, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, l); glReadPixels(48, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, r);
int ok = 1;
for (int k = 0; k < 3; ++k) ok = ok && abs(l[k] - sz[i].wantL[k]) < 40 && abs(r[k] - sz[i].wantR[k]) < 40;
printf("%-26s err=0x%04x left=%3d,%3d,%3d right=%3d,%3d,%3d %s\n", sz[i].name, err, l[0], l[1], l[2], r[0], r[1], r[2], ok ? "ok" : "BAD");
}
return 0;
}
@@ -0,0 +1,410 @@
// yuvprobe.c - pixel check of YUV dma-buf sampling through MobileGL (GL_OES_EGL_image_external).
//
// gcc -O1 yuvprobe.c -o /usr/local/bin/mgl-yuvprobe -lEGL -lGLESv2 -lgbm
// mgrun offscreen mgl-yuvprobe # exit 0 = every check passed
//
// What it does, in an ES 3.0 context on a pbuffer:
// 1. FOREIGN NV12 (and P010): a dma-buf from /dev/dma_heap/system - a buffer the MobileGL server
// did not allocate, as a video decoder's would be - with padded pitches and a gap before the
// chroma plane, filled with four vertical bars (red, green, blue, white) in BT.709 narrow
// range. Imported with eglCreateImage(EGL_LINUX_DMA_BUF_EXT) and its colour hints, bound to
// GL_TEXTURE_EXTERNAL_OES, drawn through a samplerExternalOES shader into an RGBA8 FBO and read
// back: each bar must come out its colour. The buffer is then rewritten with the bars in
// reverse order and, after a native fence (what ends a frame for an offscreen producer), drawn
// again: the new order must show (the server refreshes a foreign buffer at each frame).
// 2. GBM NV12: gbm_bo_create(NV12) through the mobilegl GBM backend (a server-allocated YUV
// buffer, zero-copy), its planes printed, imported and sampled once (content undefined; the
// check is that every call succeeds).
// Also prints the dma-buf formats/modifiers EGL lists for NV12/P010.
#define _GNU_SOURCE
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES3/gl3.h>
#include <GLES2/gl2ext.h>
#include <fcntl.h>
#include <gbm.h>
#include <linux/dma-buf.h>
#include <linux/dma-heap.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#ifndef DRM_FORMAT_NV12
#define DRM_FORMAT_NV12 0x3231564E
#endif
#ifndef DRM_FORMAT_P010
#define DRM_FORMAT_P010 0x30313050
#endif
static int g_failures = 0;
#define CHECK(cond, ...) \
do { \
if (!(cond)) { \
printf("FAIL: " __VA_ARGS__); \
printf("\n"); \
++g_failures; \
} \
} while (0)
static PFNEGLCREATEIMAGEKHRPROC pCreateImage;
static PFNEGLDESTROYIMAGEKHRPROC pDestroyImage;
static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC pTargetTexture;
static PFNEGLQUERYDMABUFFORMATSEXTPROC pQueryFormats;
static PFNEGLQUERYDMABUFMODIFIERSEXTPROC pQueryModifiers;
static PFNEGLCREATESYNCKHRPROC pCreateSync;
static PFNEGLDESTROYSYNCKHRPROC pDestroySync;
static EGLDisplay g_display;
enum { W = 256, H = 128, BARS = 4 };
static const unsigned char kBars[BARS][3] = {{255, 0, 0}, {0, 255, 0}, {0, 0, 255}, {255, 255, 255}};
// RGB -> BT.709 narrow-range codes at `bits` (8 or 10).
static void Encode709(const unsigned char rgb[3], int bits, int out[3]) {
const double kr = 0.2126, kb = 0.0722, kg = 1.0 - kr - kb;
const double r = rgb[0] / 255.0, g = rgb[1] / 255.0, b = rgb[2] / 255.0;
const double y = kr * r + kg * g + kb * b;
const double cb = (b - y) / (2.0 * (1.0 - kb)), cr = (r - y) / (2.0 * (1.0 - kr));
const double scale = (double)(1 << (bits - 8));
out[0] = (int)lround((16.0 + 219.0 * y) * scale);
out[1] = (int)lround((128.0 + 224.0 * cb) * scale);
out[2] = (int)lround((128.0 + 224.0 * cr) * scale);
}
// Writes the bars (`reverse`d or not) into a semi-planar buffer: 1 byte samples, or P010's 16-bit
// samples with the 10 significant bits high.
static void FillBars(uint8_t* base, uint32_t pitchY, uint32_t offsetC, uint32_t pitchC, int bytes, int reverse) {
const int bits = bytes == 1 ? 8 : 10;
for (int y = 0; y < H; ++y) {
for (int x = 0; x < W; ++x) {
int bar = x * BARS / W;
if (reverse) bar = BARS - 1 - bar;
int code[3];
Encode709(kBars[bar], bits, code);
uint8_t* py = base + (size_t)y * pitchY + (size_t)x * bytes;
if (bytes == 1) py[0] = (uint8_t)code[0];
else *(uint16_t*)py = (uint16_t)(code[0] << 6);
if ((x & 1) == 0 && (y & 1) == 0) {
uint8_t* pc = base + offsetC + (size_t)(y / 2) * pitchC + (size_t)(x / 2) * 2 * bytes;
if (bytes == 1) {
pc[0] = (uint8_t)code[1];
pc[1] = (uint8_t)code[2];
} else {
((uint16_t*)pc)[0] = (uint16_t)(code[1] << 6);
((uint16_t*)pc)[1] = (uint16_t)(code[2] << 6);
}
}
}
}
}
static GLuint Compile(GLenum type, const char* source) {
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &source, NULL);
glCompileShader(shader);
GLint ok = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
if (!ok) {
char log[2048];
glGetShaderInfoLog(shader, sizeof(log), NULL, log);
printf("shader: %s\n", log);
}
return shader;
}
static GLuint g_program, g_fbo, g_rgba;
static void SetUpDraw(void) {
const char* vs = "#version 300 es\n"
"out vec2 uv;\n"
"void main() {\n"
" vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n"
" gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n"
" uv = p;\n"
"}\n";
const char* fs = "#version 300 es\n"
"#extension GL_OES_EGL_image_external_essl3 : require\n"
"precision mediump float;\n"
"uniform samplerExternalOES tex;\n"
"in vec2 uv;\n"
"out vec4 color;\n"
"void main() { color = texture(tex, uv); }\n";
g_program = glCreateProgram();
glAttachShader(g_program, Compile(GL_VERTEX_SHADER, vs));
glAttachShader(g_program, Compile(GL_FRAGMENT_SHADER, fs));
glLinkProgram(g_program);
GLint linked = 0;
glGetProgramiv(g_program, GL_LINK_STATUS, &linked);
CHECK(linked, "the samplerExternalOES program did not link");
GLint type = 0, size = 0;
char name[64];
glGetActiveUniform(g_program, 0, sizeof(name), NULL, &size, (GLenum*)&type, name);
printf("uniform 0: %s type 0x%04x\n", name, type);
CHECK(type == GL_SAMPLER_EXTERNAL_OES, "the sampler reports type 0x%04x, not GL_SAMPLER_EXTERNAL_OES", type);
glGenTextures(1, &g_rgba);
glBindTexture(GL_TEXTURE_2D, g_rgba);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, W, H);
glGenFramebuffers(1, &g_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, g_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, g_rgba, 0);
CHECK(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, "the readback FBO is incomplete");
GLuint vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
}
// Draws the external texture `texture` and checks each bar's center (row `y` of the image).
static void DrawAndCheck(GLuint texture, const char* what, int reverse, int tolerance) {
glBindFramebuffer(GL_FRAMEBUFFER, g_fbo);
glViewport(0, 0, W, H);
glUseProgram(g_program);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture);
glUniform1i(glGetUniformLocation(g_program, "tex"), 0);
glDrawArrays(GL_TRIANGLES, 0, 3);
static unsigned char pixels[W * H * 4];
glReadPixels(0, 0, W, H, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
GLenum error = glGetError();
CHECK(error == GL_NO_ERROR, "%s: GL error 0x%04x", what, error);
if (tolerance < 0) {
printf("%s: center pixel %d %d %d %d (content undefined)\n", what, pixels[(H / 2 * W + W / 2) * 4],
pixels[(H / 2 * W + W / 2) * 4 + 1], pixels[(H / 2 * W + W / 2) * 4 + 2],
pixels[(H / 2 * W + W / 2) * 4 + 3]);
return;
}
// Rows: GL row 0 is the image's first row (the uv mapping above), and the bars are vertical,
// so any row does; take two to catch a broken chroma row pitch.
const int rows[2] = {H / 4, (3 * H) / 4};
for (int r = 0; r < 2; ++r) {
for (int bar = 0; bar < BARS; ++bar) {
const int x = bar * (W / BARS) + W / (2 * BARS);
const unsigned char* p = &pixels[(rows[r] * W + x) * 4];
const unsigned char* want = kBars[reverse ? BARS - 1 - bar : bar];
int worst = 0;
for (int c = 0; c < 3; ++c) {
int d = abs((int)p[c] - (int)want[c]);
if (d > worst) worst = d;
}
printf("%s: row %d bar %d -> %3d %3d %3d %3d (want %3d %3d %3d)%s\n", what, rows[r], bar, p[0], p[1], p[2],
p[3], want[0], want[1], want[2], worst > tolerance ? " <-- off" : "");
CHECK(worst <= tolerance, "%s: bar %d is off by %d", what, bar, worst);
}
}
}
static int AllocateDmaHeap(size_t size) {
int heap = open("/dev/dma_heap/system", O_RDWR | O_CLOEXEC);
if (heap < 0) {
perror("open /dev/dma_heap/system");
return -1;
}
struct dma_heap_allocation_data data = {.len = size, .fd_flags = O_RDWR | O_CLOEXEC};
if (ioctl(heap, DMA_HEAP_IOCTL_ALLOC, &data) != 0) {
perror("DMA_HEAP_IOCTL_ALLOC");
close(heap);
return -1;
}
close(heap);
return (int)data.fd;
}
static void Sync(int fd, int start) {
struct dma_buf_sync sync = {.flags = DMA_BUF_SYNC_WRITE | (start ? DMA_BUF_SYNC_START : DMA_BUF_SYNC_END)};
ioctl(fd, DMA_BUF_IOCTL_SYNC, &sync);
}
static EGLImageKHR Import(int fd, uint32_t fourcc, uint32_t pitchY, uint32_t offsetC, uint32_t pitchC, int modifier) {
EGLint attribs[64];
int n = 0;
#define A(k, v) (attribs[n++] = (k), attribs[n++] = (EGLint)(v))
A(EGL_WIDTH, W);
A(EGL_HEIGHT, H);
A(EGL_LINUX_DRM_FOURCC_EXT, fourcc);
A(EGL_DMA_BUF_PLANE0_FD_EXT, fd);
A(EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0);
A(EGL_DMA_BUF_PLANE0_PITCH_EXT, pitchY);
A(EGL_DMA_BUF_PLANE1_FD_EXT, fd);
A(EGL_DMA_BUF_PLANE1_OFFSET_EXT, offsetC);
A(EGL_DMA_BUF_PLANE1_PITCH_EXT, pitchC);
if (modifier) {
A(EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, 0);
A(EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, 0);
A(EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT, 0);
A(EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT, 0);
}
A(EGL_YUV_COLOR_SPACE_HINT_EXT, EGL_ITU_REC709_EXT);
A(EGL_SAMPLE_RANGE_HINT_EXT, EGL_YUV_NARROW_RANGE_EXT);
attribs[n] = EGL_NONE;
#undef A
EGLImageKHR image = pCreateImage(g_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, NULL, attribs);
if (image == EGL_NO_IMAGE_KHR) printf("eglCreateImage failed: 0x%04x\n", eglGetError());
return image;
}
static GLuint ExternalTexture(EGLImageKHR image) {
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture);
pTargetTexture(GL_TEXTURE_EXTERNAL_OES, image);
GLenum error = glGetError();
CHECK(error == GL_NO_ERROR, "glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES) -> 0x%04x", error);
GLint bound = 0;
glGetIntegerv(GL_TEXTURE_BINDING_EXTERNAL_OES, &bound);
CHECK((GLuint)bound == texture, "GL_TEXTURE_BINDING_EXTERNAL_OES reads %d", bound);
return texture;
}
// A frame boundary as an offscreen producer makes one: a native fence.
static void EndFrame(void) {
EGLint attribs[] = {EGL_NONE};
EGLSyncKHR sync = pCreateSync(g_display, EGL_SYNC_NATIVE_FENCE_ANDROID, attribs);
CHECK(sync != EGL_NO_SYNC_KHR, "eglCreateSync(native fence) failed");
if (sync != EGL_NO_SYNC_KHR) pDestroySync(g_display, sync);
}
static void ForeignTest(uint32_t fourcc, int bytes, int tolerance) {
const char* what = fourcc == DRM_FORMAT_NV12 ? "foreign NV12" : "foreign P010";
// Padded pitches and a gap before the chroma plane, as decoders lay buffers out.
const uint32_t pitchY = W * bytes + 64, pitchC = W * bytes + 128;
const uint32_t offsetC = pitchY * (H + 16);
const size_t size = offsetC + (size_t)pitchC * (H / 2) + 4096;
int fd = AllocateDmaHeap(size);
if (fd < 0) {
CHECK(0, "%s: no dma-buf to test with", what);
return;
}
uint8_t* map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
CHECK(map != MAP_FAILED, "%s: mmap failed", what);
if (map == MAP_FAILED) return;
Sync(fd, 1);
FillBars(map, pitchY, offsetC, pitchC, bytes, 0);
Sync(fd, 0);
EGLImageKHR image = Import(fd, fourcc, pitchY, offsetC, pitchC, fourcc == DRM_FORMAT_NV12);
CHECK(image != EGL_NO_IMAGE_KHR, "%s: import refused", what);
if (image == EGL_NO_IMAGE_KHR) return;
GLuint texture = ExternalTexture(image);
DrawAndCheck(texture, what, 0, tolerance);
EndFrame();
// New content, same buffer: shows after the frame boundary.
Sync(fd, 1);
FillBars(map, pitchY, offsetC, pitchC, bytes, 1);
Sync(fd, 0);
EndFrame();
DrawAndCheck(texture, what, 1, tolerance);
glDeleteTextures(1, &texture);
pDestroyImage(g_display, image);
munmap(map, size);
close(fd);
}
static void GbmTest(void) {
int node = open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC);
if (node < 0) node = open("/dev/null", O_RDWR | O_CLOEXEC);
struct gbm_device* gbm = gbm_create_device(node);
if (gbm == NULL) {
CHECK(0, "gbm_create_device failed");
return;
}
printf("gbm backend: %s\n", gbm_device_get_backend_name(gbm));
const int supported = gbm_device_is_format_supported(gbm, DRM_FORMAT_NV12, 0);
printf("gbm NV12 supported: %d, P010: %d\n", supported,
gbm_device_is_format_supported(gbm, DRM_FORMAT_P010, 0));
CHECK(supported == 1, "gbm does not support NV12");
struct gbm_bo* bo = gbm_bo_create(gbm, W, H, DRM_FORMAT_NV12, 0);
CHECK(bo != NULL, "gbm_bo_create(NV12) failed");
if (bo != NULL) {
const int planes = gbm_bo_get_plane_count(bo);
printf("gbm NV12 bo: planes %d, stride %u/%u, offset %u/%u, modifier 0x%llx\n", planes,
gbm_bo_get_stride_for_plane(bo, 0), gbm_bo_get_stride_for_plane(bo, 1), gbm_bo_get_offset(bo, 0),
gbm_bo_get_offset(bo, 1), (unsigned long long)gbm_bo_get_modifier(bo));
CHECK(planes == 2, "the NV12 bo has %d planes", planes);
int fd = gbm_bo_get_fd(bo);
EGLImageKHR image =
Import(fd, DRM_FORMAT_NV12, gbm_bo_get_stride_for_plane(bo, 0), gbm_bo_get_offset(bo, 1),
gbm_bo_get_stride_for_plane(bo, 1), 0);
CHECK(image != EGL_NO_IMAGE_KHR, "the server's own NV12 buffer was not imported");
if (image != EGL_NO_IMAGE_KHR) {
GLuint texture = ExternalTexture(image);
DrawAndCheck(texture, "gbm NV12", 0, -1);
glDeleteTextures(1, &texture);
pDestroyImage(g_display, image);
}
close(fd);
gbm_bo_destroy(bo);
}
gbm_device_destroy(gbm);
close(node);
}
int main(void) {
setvbuf(stdout, NULL, _IONBF, 0);
g_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
EGLint major, minor;
if (!eglInitialize(g_display, &major, &minor)) {
printf("FAIL: eglInitialize\n");
return 2;
}
const char* eglExtensions = eglQueryString(g_display, EGL_EXTENSIONS);
printf("EGL %d.%d dma_buf_import: %s\n", major, minor,
strstr(eglExtensions, "EGL_EXT_image_dma_buf_import") ? "yes" : "no");
eglBindAPI(EGL_OPENGL_ES_API);
const EGLint configAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_NONE};
EGLConfig config;
EGLint count = 0;
eglChooseConfig(g_display, configAttribs, &config, 1, &count);
if (count < 1) {
printf("FAIL: no config\n");
return 2;
}
const EGLint pbufferAttribs[] = {EGL_WIDTH, 16, EGL_HEIGHT, 16, EGL_NONE};
EGLSurface surface = eglCreatePbufferSurface(g_display, config, pbufferAttribs);
const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE};
EGLContext context = eglCreateContext(g_display, config, EGL_NO_CONTEXT, contextAttribs);
if (context == EGL_NO_CONTEXT || !eglMakeCurrent(g_display, surface, surface, context)) {
printf("FAIL: no ES 3 context\n");
return 2;
}
pCreateImage = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
pDestroyImage = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
pTargetTexture = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
pQueryFormats = (PFNEGLQUERYDMABUFFORMATSEXTPROC)eglGetProcAddress("eglQueryDmaBufFormatsEXT");
pQueryModifiers = (PFNEGLQUERYDMABUFMODIFIERSEXTPROC)eglGetProcAddress("eglQueryDmaBufModifiersEXT");
pCreateSync = (PFNEGLCREATESYNCKHRPROC)eglGetProcAddress("eglCreateSyncKHR");
pDestroySync = (PFNEGLDESTROYSYNCKHRPROC)eglGetProcAddress("eglDestroySyncKHR");
printf("GL_RENDERER: %s\n", glGetString(GL_RENDERER));
const char* glExtensions = (const char*)glGetString(GL_EXTENSIONS);
const int external = strstr(glExtensions, "GL_OES_EGL_image_external_essl3") != NULL;
printf("GL_OES_EGL_image_external_essl3: %s\n", external ? "yes" : "no");
CHECK(external, "GL_OES_EGL_image_external_essl3 is not advertised");
EGLint formats[32], formatCount = 0;
pQueryFormats(g_display, 32, formats, &formatCount);
int nv12 = 0;
for (int i = 0; i < formatCount; ++i) {
printf("dma-buf format %.4s", (const char*)&formats[i]);
EGLuint64KHR modifiers[4];
EGLBoolean externalOnly[4];
EGLint modifierCount = 0;
pQueryModifiers(g_display, formats[i], 4, modifiers, externalOnly, &modifierCount);
for (int m = 0; m < modifierCount; ++m)
printf(" [modifier 0x%llx%s]", (unsigned long long)modifiers[m], externalOnly[m] ? " external-only" : "");
printf("\n");
nv12 |= formats[i] == (EGLint)DRM_FORMAT_NV12;
}
CHECK(nv12, "NV12 is not a listed dma-buf format");
SetUpDraw();
ForeignTest(DRM_FORMAT_NV12, 1, 12);
ForeignTest(DRM_FORMAT_P010, 2, 16);
GbmTest();
printf("%s (%d failure(s))\n", g_failures ? "FAILED" : "PASSED", g_failures);
eglMakeCurrent(g_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
eglTerminate(g_display);
return g_failures ? 1 : 0;
}
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# push-tools.sh - install this skill's device- and container-side helpers on the phone.
# device/*.sh -> $DEV_TOOLS (run as root: adb shell 'su -c "sh $DEV_TOOLS/<x>.sh"')
# container/mgrun -> <container>/usr/local/bin/mgrun
# container/kwprep.sh -> <container>/root/kwprep.sh
# Needs the container running (its rootfs is mounted at $CT_ROOT only then).
# Overwrites same-named files other sessions may be using in $DEV_TOOLS; check first on a shared phone.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
adb_ shell "mkdir -p $DEV_TOOLS"
for f in "$here"/device/*.sh; do
tr -d '\r' < "$f" > "$ANL_DIR/push.tmp"
adb_ push "$ANL_DIR/push.tmp" "$DEV_TOOLS/$(basename "$f")" >/dev/null
echo "pushed $DEV_TOOLS/$(basename "$f")"
done
for f in mgrun kwprep.sh; do
tr -d '\r' < "$here/container/$f" > "$ANL_DIR/push.tmp"
adb_ push "$ANL_DIR/push.tmp" "$DEV_TOOLS/$f" >/dev/null
done
rm -f "$ANL_DIR/push.tmp"
adb_ shell "su -c 'cp $DEV_TOOLS/mgrun $CT_ROOT/usr/local/bin/mgrun && chmod 755 $CT_ROOT/usr/local/bin/mgrun && cp $DEV_TOOLS/kwprep.sh $CT_ROOT/root/kwprep.sh && echo installed mgrun kwprep.sh'"
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# rec.sh <name> - record 3 s of the screen while opening Kickoff, extract frames inside the container
# with ffmpeg, pull them to $ANL_DIR/af_<name>/ and build a 3x4 contact sheet $ANL_DIR/grid_<name>.png
# (needs Python + Pillow on Windows). Use it to catch flicker/black frames a single screenshot misses.
# Env: TAP="x y" Kickoff button position (default "69 1349" = bottom-left on the 2560x1600 tablet;
# device specific). Note: anland grabs the touchscreen (EVIOCGRAB); if `input tap` does nothing, use
# `input mouse tap x y`. Env CHANNEL (default rec).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
N=${1:?usage: rec.sh <name>}; TAP=${TAP:-69 1349}
adb_ shell "su -c 'input keyevent 111; sleep 1.5; (screenrecord --time-limit 3 --bit-rate 20000000 $DEV_SHARE/$N.mp4 &); sleep 0.7; input tap $TAP; sleep 3; cp $DEV_SHARE/$N.mp4 $CT_ROOT/root/$N.mp4'"
bash "$here/ct.sh" "${CHANNEL:-rec}" <<IN
rm -rf /root/af_$N; mkdir -p /root/af_$N; ffmpeg -loglevel quiet -i /root/$N.mp4 -vf scale=1000:-1 -fps_mode passthrough /root/af_$N/f%03d.png; ls /root/af_$N | wc -l
IN
adb_ shell "su -c 'rm -rf $DEV_SHARE/af_$N; cp -r $CT_ROOT/root/af_$N $DEV_SHARE/ && chmod -R 777 $DEV_SHARE/af_$N'"
rm -rf "$ANL_DIR/af_$N"; adb_ pull "$DEV_SHARE/af_$N" "$ANL_DIR/" >/dev/null
python - "$ANL_DIR/af_$N" "$ANL_DIR/grid_$N.png" <<'P'
import sys, os
from PIL import Image
d, out = sys.argv[1], sys.argv[2]
fs = sorted(os.listdir(d)); n = len(fs)
pick = [fs[min(n-1, int(i * n / 12))] for i in range(12)]
ims = [Image.open(os.path.join(d, f)) for f in pick]
w, h = ims[0].size; tw, th = w // 2, h // 2
g = Image.new('RGB', (tw * 3, th * 4))
for i, im in enumerate(ims): g.paste(im.resize((tw, th)), ((i % 3) * tw, (i // 3) * th))
g.save(out); print(n, pick)
P
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# shot.sh <name> [count] [interval-s] - take <count> (default 2) phone screenshots <interval> s apart
# (default 1), pull them to $ANL_DIR/<name>-<i>.png and print their md5. Identical hashes while an
# animated client (glmark2, a video) runs mean the frames are NOT reaching the screen.
set -euo pipefail
. "$(dirname "$0")/env.sh"
name=${1:?usage: shot.sh <name> [count] [interval]}; n=${2:-2}; dt=${3:-1}
cmd=""; i=1
while [ "$i" -le "$n" ]; do cmd="$cmd screencap -p $DEV_SHARE/$name-$i.png; sleep $dt;"; i=$((i+1)); done
adb_ shell "su -c '$cmd'"
i=1
while [ "$i" -le "$n" ]; do
adb_ pull "$DEV_SHARE/$name-$i.png" "$ANL_DIR/$name-$i.png" >/dev/null
md5sum "$ANL_DIR/$name-$i.png"; i=$((i+1))
done
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# sync-src.sh - FALLBACK path only (the WSL cross build, xbuild.sh, replaced it). Pushes the files the
# worktree changed since the container's source stamp into the container's MobileGL source tree
# (/root/mobilegl-unified-src), keeping mtimes so the in-container ninja build stays incremental;
# then build/install inside the container with anland's anland-build-client.sh
# (producers/kde/Arch_v5/mobilegl-tools/, gcc, -j4, slow on the phone).
# A full re-sync of the tree is anland's anland-sync-source.sh (MOBILEGL_REPO=<worktree>).
set -euo pipefail
. "$(dirname "$0")/env.sh"
SRC=$CT_ROOT/root/mobilegl-unified-src
STAMP=$(adb_ shell "su -c 'cat $SRC/.anland-build-stamp'" | tr -d '\r\n')
cd "$MGL_WORKTREE"
FILES=$( (git diff --name-only --diff-filter=d "$STAMP"; git ls-files --others --exclude-standard) \
| grep -v -E '^(3rdparty/|build-android|\.anland-build-stamp|\.claude/)' | sort -u)
echo "$(echo "$FILES" | wc -l) files since $STAMP"
tar --force-local -czf "$ANL_DIR/src-delta.tgz" $FILES
adb_ push "$ANL_DIR/src-delta.tgz" /data/local/tmp/src-delta.tgz >/dev/null
HEAD=$(git rev-parse HEAD)
adb_ shell "su -c 'tar xzf /data/local/tmp/src-delta.tgz -C $SRC && echo $HEAD > $SRC/.anland-build-stamp && echo synced'"
@@ -0,0 +1,9 @@
#!/bin/bash
# wsl-all.sh - run INSIDE WSL archlinux after wsl-test.sh configured $B: build every *Test target except
# DirectVulkan*/Benchmark* and run the unit label. NEVER run the DirectVulkan integration tests
# (they need a live device + manual intervention); never run two ctest suites at once.
B=${B:-$HOME/mgl-anl-bld}
cd $B
T=$(ninja -t targets all 2>/dev/null | grep -oE "^[A-Za-z]+Test:" | tr -d : | sort -u | grep -v -E "^(DirectVulkan|Benchmark)" | tr "\n" " ")
ninja -j14 MobileGLServer $T 2>&1 | grep -E "error:|FAILED" | head -10
ctest -L unit -j10 --timeout 300 -E "DirectVulkan" 2>&1 | tail -25
@@ -0,0 +1,13 @@
#!/bin/bash
# wsl-test.sh <targets...> - run INSIDE WSL archlinux: configure (once) and build MobileGL unit-test
# targets for the host (clang, disaggregated + in-process server ON). From Git Bash:
# MSYS2_ARG_CONV_EXCL='*' wsl -d archlinux -- bash /mnt/c/<worktree>/.claude/skills/anland-mobilegl-plasma/scripts/wsl-test.sh MultiSessionTest
# then ctest in $B (see wsl-all.sh). Env: W (worktree, WSL path; default: derived from this file),
# B (build dir, default ~/mgl-anl-bld). The Windows MSVC build of feat/disaggregated is broken; use this.
set -e
W=${W:-$(cd "$(dirname "$0")/../../../.." && pwd)}
B=${B:-$HOME/mgl-anl-bld}
if [ ! -f $B/build.ninja ]; then
cmake -S $W -B $B -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DMOBILEGL_BUILD_TEST=ON -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON -DMOBILEGL_BUILD_STAMP=wsl
fi
cmake --build $B --target "$@" -j16
@@ -0,0 +1,135 @@
#!/usr/bin/env bash
# x11-verify.sh <step> [...] - device verification of X11 on the GPU (docs/archive/notes/anland/
# plan-x11-gpu.md): Xwayland drawing with glamor on MobileGL, and GLX windows presented through
# DRI3 + Present from shared images. Steps, in the order a full run takes them:
#
# preflight read-only: battery, stack status, Xwayland package, render node, xcb libs,
# test apps, Xwayland's extensions and its GLX vendor
# measure <label> [s] glxgears 1280x720 (GEARS_ARGS; vblank_mode=0) for s seconds (10): its fps, then CPU/GPU/dma-buf
# counts (device/x11-measure.sh); also with MOBILEGL_GLX_PRESENT=readback
# deploy-client xbuild.sh + xdeploy.sh of this worktree's client (MGL_XBUILD_DIR)
# deploy-kwin kwin-stage.sh build from ANLAND_WORKTREE; waits for "== ALL DONE =="
# deploy-server build-android.sh + cyc.sh x11-server (swaps the APK lib, RESTARTS the session)
# check the evidence: Xwayland took glamor (journal), its MobileGL log, GLX's path
# line, dma-buf counts in Xwayland/KWin, the server's import lines
# apps X11 clients on screen: konsole and kate on Qt's xcb platform (glamor-drawn
# core X), glxgears (GLX), xterm/xeyes when installed; two screenshots
# stop kill the test apps
#
# Typical run: preflight; measure before; deploy-client; deploy-kwin; deploy-server; check; measure
# after; apps; stop. Battery: every load step refuses below MIN_BATTERY (40) %.
# Env: CHANNEL (ct.sh channel, default x11), ANLAND_WORKTREE (anland x11-gpu worktree, Windows
# form), MGL_XBUILD_DIR (default /home/swung/mgl-xbuild-x11), MIN_BATTERY.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
CH=${CHANNEL:-x11}
: "${MGL_XBUILD_DIR:=/home/swung/mgl-xbuild-x11}"
: "${ANLAND_WORKTREE:=$(cygpath -m "$USERPROFILE" 2>/dev/null)/AndroidStudioProjects/anland/.claude/worktrees/x11-gpu}"
export MGL_XBUILD_DIR ANLAND_WORKTREE
battery_ok() {
local level
level=$(adb_ shell "dumpsys battery" | sed -n 's/^ *level: *//p' | tr -d '\r')
echo "battery: ${level}%"
if [ "${level:-0}" -lt "${MIN_BATTERY:-40}" ]; then
echo "below ${MIN_BATTERY:-40}%: stopping GPU-heavy work; charge the phone first" >&2
return 1
fi
}
push_measure() {
tr -d '\r' < "$here/device/x11-measure.sh" > "$ANL_DIR/x11-measure.sh"
adb_ push "$ANL_DIR/x11-measure.sh" "$DEV_TOOLS/x11-measure.sh" >/dev/null
}
step=${1:?usage: x11-verify.sh <preflight|measure|deploy-client|deploy-kwin|deploy-server|check|apps|stop>}
shift
case "$step" in
preflight)
battery_ok || true
adb_ shell "su -c 'sh $DEV_TOOLS/status.sh'" || true
bash "$here/ct.sh" "$CH" <<'IN'
echo "== packages"; pacman -Q xorg-xwayland libxcb mesa-utils xorg-xdpyinfo qt6-base 2>&1 | sed 's/^/ /'
echo "== render nodes"; ls -l /dev/dri/ 2>&1 | sed 's/^/ /'
U=swung0x48; for n in /dev/dri/renderD*; do sudo -u $U test -r "$n" -a -w "$n" && echo " $U can open $n"; done
echo "== KWin's thin node"; tr '\0' '\n' < /proc/$(pgrep -x kwin_wayland)/environ 2>/dev/null | grep -E "MOBILEGL_GBM_NODE|ANLAND_XWAYLAND_GLAMOR|GBM_BACKEND" | sed 's/^/ /'
echo "== xcb libraries"; for l in libxcb-dri3.so.0 libxcb-present.so.0 libxcb-xfixes.so.0 libxcb-shm.so.0; do ls /usr/lib/$l >/dev/null 2>&1 && echo " $l ok" || echo " $l MISSING"; done
echo "== Xwayland"; pgrep -a -x Xwayland | sed 's/^/ /'
mgrun offscreen xdpyinfo 2>/dev/null | grep -E "^ +(DRI3|Present|MIT-SHM|XFIXES|GLX)$" | sed 's/^/ ext/'
mgrun offscreen glxinfo -B 2>&1 | grep -E "vendor|renderer|direct rendering" | sed 's/^/ /'
IN
;;
measure)
label=${1:?usage: x11-verify.sh measure <label> [seconds]}
secs=${2:-10}
battery_ok
push_measure
for mode in auto readback; do
printf '%s\n' \
"pkill -x glxgears; rm -f /tmp/mgl-x11-gears.client.log /tmp/x11-gears.out" \
"MGLOG=/tmp/mgl-x11-gears.log timeout $((secs + 8)) mgrun offscreen env vblank_mode=${VBLANK_MODE:-0} MOBILEGL_GLX_PRESENT=$mode glxgears ${GEARS_ARGS:--geometry 1280x720} > /tmp/x11-gears.out 2>&1 &" \
"sleep 3" | bash "$here/ct.sh" "$CH" >/dev/null
adb_ shell "su -c 'sh $DEV_TOOLS/x11-measure.sh $label-$mode glxgears $secs'" || true
sleep 6
printf '%s\n' "grep -a 'frames in' /tmp/x11-gears.out | tail -3; grep -a 'presented through' /tmp/mgl-x11-gears.client.log | head -1; pkill -x glxgears; true" \
| bash "$here/ct.sh" "$CH"
done
;;
deploy-client)
bash "$here/xbuild.sh"
CHANNEL=$CH bash "$here/xdeploy.sh"
;;
deploy-kwin)
CHANNEL=$CH bash "$here/kwin-stage.sh" build
for _ in $(seq 1 120); do
out=$(printf 'tail -3 /root/kwin-build.log\n' | bash "$here/ct.sh" "$CH" 2>/dev/null || true)
echo "$out" | tail -1
echo "$out" | grep -q "== ALL DONE ==" && exit 0
echo "$out" | grep -qiE "error|failed" && { echo "KWin build failed" >&2; exit 1; }
sleep 30
done
echo "KWin build did not finish in an hour" >&2; exit 1
;;
deploy-server)
battery_ok
bash "$here/build-android.sh"
CHANNEL=$CH bash "$here/cyc.sh" x11-server
;;
check)
bash "$here/ct.sh" "$CH" <<'IN'
echo "== Xwayland's command line (no -shm with glamor)"; pgrep -a -x Xwayland | sed 's/^/ /'
echo "== Xwayland's glamor (journal)"
journalctl --no-pager --since -30min 2>/dev/null | grep -aiE "glamor|Xwayland.*(EGL|gbm|dri3|falling back)" | tail -10 | sed 's/^/ /'
echo "== Xwayland's MobileGL client log"; grep -aE "presented|shared image|declin|Fatal|ERROR" /tmp/mgl-xwayland.client.log /tmp/mgl-xwayland.client 2>/dev/null | tail -8 | sed 's/^/ /' || echo " (none)"
X=$(pgrep -x Xwayland); K=$(pgrep -x kwin_wayland)
echo "== dma-bufs held: Xwayland $(ls -l /proc/$X/fd 2>/dev/null | grep -c dmabuf), kwin $(ls -l /proc/$K/fd 2>/dev/null | grep -c dmabuf)"
echo "== Xwayland's GLX vendor and extensions"
mgrun offscreen xdpyinfo 2>/dev/null | grep -E "^ +(DRI3|Present|MIT-SHM)$" | sed 's/^/ ext/'
echo "== a GLX window's path"
rm -f /tmp/mgl-x11-check.client.log
MGLOG=/tmp/mgl-x11-check.log timeout 6 mgrun offscreen glxgears >/dev/null 2>&1
grep -a "presented through\|leaves DRI3" /tmp/mgl-x11-check.client.log | sed 's/^/ /'
IN
adb_ logcat -d | grep -aE "MobileGL.*(shared image|dma-buf import|refused)" | tail -8 || true
adb_ logcat -b crash -d | tail -5 || true
;;
apps)
battery_ok
printf '%s\n' \
"pkill -x xterm; pkill -x xeyes; pkill -x glxgears; pkill -x konsole; pkill -x kate" \
"MGLOG=/tmp/mgl-x11-konsole.log mgrun offscreen env QT_QPA_PLATFORM=xcb konsole >/dev/null 2>&1 &" \
"MGLOG=/tmp/mgl-x11-kate.log mgrun offscreen env QT_QPA_PLATFORM=xcb kate >/dev/null 2>&1 &" \
"command -v xterm >/dev/null && MGLOG=/tmp/mgl-x11-xterm.log mgrun offscreen xterm >/dev/null 2>&1 &" \
"command -v xeyes >/dev/null && MGLOG=/tmp/mgl-x11-xeyes.log mgrun offscreen xeyes >/dev/null 2>&1 &" \
"MGLOG=/tmp/mgl-x11-gears.log mgrun offscreen glxgears >/dev/null 2>&1 &" \
"sleep 6; pgrep -a -x konsole; pgrep -a -x kate; pgrep -a -x xterm; pgrep -a -x xeyes; pgrep -a -x glxgears; true" | bash "$here/ct.sh" "$CH"
bash "$here/shot.sh" x11-apps 2 1
;;
stop)
printf '%s\n' "pkill -x xterm; pkill -x xeyes; pkill -x glxgears; pkill -x konsole; pkill -x kate; true" | bash "$here/ct.sh" "$CH"
;;
*)
echo "unknown step $step" >&2; exit 2
;;
esac
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# xbuild.sh [--reconfigure] [extra ninja targets...] - cross-build the container CLIENT: libMobileGL.so
# (glibc aarch64, glvnd EGL/GLX vendor) + mobilegl_gbm.so (GBM backend), in WSL archlinux with
# clang + lld against a sysroot exported from the container (xsysroot/refresh-sysroot.sh).
# Full build ~150 s, incremental seconds. Deploy with xdeploy.sh.
# Env: MGL_WORKTREE (source, Windows form), MGL_XBUILD_DIR (WSL build dir, default ~/mgl-xbuild-a64),
# MGL_JOBS (16), WSL_DISTRO (archlinux).
# Prereq in WSL: clang, lld, llvm (lld major MUST equal clang/llvm-libs major - lld 23 on llvm 22
# broke ld.lld), cmake, ninja; ~/sysroots/aarch64-arch.cmake + ~/sysroots/arch-kde-mgl.
set -euo pipefail
. "$(dirname "$0")/env.sh"
STAMP=$(git -C "$MGL_WORKTREE" rev-parse HEAD)
RECONF=0
if [ "${1:-}" = "--reconfigure" ]; then RECONF=1; shift; fi
EXTRA="$*"
SRC_WSL=$(win2wsl "$MGL_WORKTREE")
wsl -d "${WSL_DISTRO:-archlinux}" -- env SRC="$SRC_WSL" STAMP="$STAMP" RECONF="$RECONF" EXTRA="$EXTRA" \
OUT="${MGL_XBUILD_DIR:-}" JOBS="${MGL_JOBS:-16}" bash -s <<'IN' 2>&1 | tr -d '\0' | grep -av '^wsl:'
set -euo pipefail
OUT=${OUT:-$HOME/mgl-xbuild-a64}
TC=$HOME/sysroots/aarch64-arch.cmake
[ -f "$TC" ] || { echo "missing toolchain $TC (run xsysroot/refresh-sysroot.sh)"; exit 1; }
ld.lld --version >/dev/null || { echo "ld.lld is not runnable in WSL (lld/llvm version mismatch?)"; exit 1; }
t0=$(date +%s)
if [ "$RECONF" = 1 ] || [ ! -f "$OUT/build.ninja" ]; then
rm -rf "$OUT/CMakeCache.txt" "$OUT/CMakeFiles"
cmake -S "$SRC" -B "$OUT" -G Ninja --toolchain "$TC" -DMOBILEGL_BUILD_STAMP="$STAMP" \
-DCMAKE_BUILD_TYPE=Release -DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF \
-DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_ENABLE_LTO=OFF
elif ! grep -qx "MOBILEGL_BUILD_STAMP:STRING=$STAMP" "$OUT/CMakeCache.txt"; then
cmake -S "$SRC" -B "$OUT" -DMOBILEGL_BUILD_STAMP="$STAMP" >/dev/null
fi
t1=$(date +%s)
cmake --build "$OUT" --target MobileGL mobilegl_gbm $EXTRA -j"$JOBS"
t2=$(date +%s)
echo "stamp: $STAMP"
echo "configure: $((t1-t0))s build: $((t2-t1))s"
for f in "$OUT/libMobileGL.so" "$OUT/MobileGL/MG_Gbm/mobilegl_gbm.so"; do
ls -la "$f"; echo " windows: $(wslpath -w "$f")"
done
IN
exit "${PIPESTATUS[0]}"
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# xdeploy.sh - install the WSL cross-built libMobileGL.so + mobilegl_gbm.so into the container, exactly
# like anland's anland-build-client.sh: copy to a NEW inode and rename into place (overwriting a mapped
# .so in place crashes every running client, KWin included), refresh the libEGL/libGL aliases, the
# glvnd GLX vendor link, the EGL vendor JSON (system-wide 10_mobilegl.json + the /opt copy), the client
# config /etc/mobilegl/client.conf and the GLX/GBM selection drop-ins. Does not restart anything: already-running clients
# keep the old inode; restart the client (or the session) to pick up the new library.
# Env: MGL_XBUILD_DIR (WSL build dir), CHANNEL (ct.sh channel, default xd), WSL_DISTRO.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
mkdir -p "$ANL_DIR/xdist"
# 1. Copy the build outputs out of WSL.
wsl -d "${WSL_DISTRO:-archlinux}" -- env OUT="${MGL_XBUILD_DIR:-}" D="$(win2wsl "$ANL_DIR")/xdist" bash -s <<'IN' 2>&1 | tr -d '\0' | grep -av '^wsl:'
set -e; OUT=${OUT:-$HOME/mgl-xbuild-a64}
cp -f "$OUT/libMobileGL.so" "$D/libMobileGL.so"; cp -f "$OUT/MobileGL/MG_Gbm/mobilegl_gbm.so" "$D/mobilegl_gbm.so"
sha256sum "$D/libMobileGL.so" "$D/mobilegl_gbm.so"
IN
# 2. Push to the phone and stage them in the container's /root/xdeploy (the container's /tmp and the
# phone's /data/local/tmp are different places; the rootfs is reachable at $CT_ROOT).
adb_ shell "mkdir -p $DEV_TOOLS/xdist"
adb_ push "$ANL_DIR/xdist/libMobileGL.so" "$DEV_TOOLS/xdist/libMobileGL.so"
adb_ push "$ANL_DIR/xdist/mobilegl_gbm.so" "$DEV_TOOLS/xdist/mobilegl_gbm.so"
adb_ shell "su -c 'mkdir -p $CT_ROOT/root/xdeploy && cp $DEV_TOOLS/xdist/libMobileGL.so $DEV_TOOLS/xdist/mobilegl_gbm.so $CT_ROOT/root/xdeploy/ && rm -f $DEV_TOOLS/xdist/*.so'"
# 3. Install inside the container.
bash "$here/ct.sh" "${CHANNEL:-xd}" <<'IN'
set -euo pipefail
in=/root/xdeploy
mkdir -p /opt/mobilegl/lib /opt/mobilegl/share/glvnd/egl_vendor.d
cp "$in/libMobileGL.so" /opt/mobilegl/lib/libMobileGL.so.new
mv -f /opt/mobilegl/lib/libMobileGL.so.new /opt/mobilegl/lib/libMobileGL.so
for lib in libEGL.so libEGL.so.1 libGL.so libGL.so.1; do ln -sf libMobileGL.so "/opt/mobilegl/lib/$lib"; done
ln -sf /opt/mobilegl/lib/libMobileGL.so /usr/lib/libGLX_mobilegl.so.0
vendor='{"file_format_version":"1.0.0","ICD":{"library_path":"/opt/mobilegl/lib/libMobileGL.so"}}'
# Per-process forcing (__EGL_VENDOR_LIBRARY_FILENAMES=<this file>) keeps working off the /opt copy.
printf '%s\n' "$vendor" >/opt/mobilegl/share/glvnd/egl_vendor.d/50_mobilegl.json
# System-wide vendor: 10_ sorts ahead of the system's 50_mesa.json, so stock libglvnd asks MobileGL
# first and moves on to the other vendor whenever MobileGL declines (no server reachable).
mkdir -p /usr/share/glvnd/egl_vendor.d
printf '%s\n' "$vendor" >/usr/share/glvnd/egl_vendor.d/10_mobilegl.json.new
mv -f /usr/share/glvnd/egl_vendor.d/10_mobilegl.json.new /usr/share/glvnd/egl_vendor.d/10_mobilegl.json
# Client settings every process reads when its environment does not say otherwise (the backend
# comes from /etc/mobilegl/backend, which switch.sh rewrites).
mkdir -p /etc/mobilegl
cat >/etc/mobilegl/client.conf <<'CONF'
# MobileGL client configuration, read by every process that loads libMobileGL.so.
# KEY=value lines; a MOBILEGL_* variable in a process's environment overrides the line here.
# The backend is /etc/mobilegl/backend. Written by xdeploy.sh / anland-build-client.sh.
MOBILEGL_TRANSPORT=spawn
MOBILEGL_IPC_DATA=shm
MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl
CONF
# What glvnd's libGLX and libgbm cannot infer: environment.d reaches the systemd user manager (every
# Plasma service and what it launches, terminals included), profile.d reaches login shells.
mkdir -p /etc/environment.d /etc/profile.d
printf '%s\n' '__GLX_VENDOR_LIBRARY_NAME=mobilegl' 'GBM_BACKEND=mobilegl' >/etc/environment.d/10-mobilegl.conf
printf '%s\n' 'export __GLX_VENDOR_LIBRARY_NAME=mobilegl' 'export GBM_BACKEND=mobilegl' >/etc/profile.d/mobilegl.sh
mkdir -p /usr/lib/gbm
cp "$in/mobilegl_gbm.so" /usr/lib/gbm/mobilegl_gbm.so.new
mv -f /usr/lib/gbm/mobilegl_gbm.so.new /usr/lib/gbm/mobilegl_gbm.so
rm -f "$in/libMobileGL.so" "$in/mobilegl_gbm.so"
sha256sum /opt/mobilegl/lib/libMobileGL.so /usr/lib/gbm/mobilegl_gbm.so
ls /usr/share/glvnd/egl_vendor.d/ /etc/mobilegl/ /etc/environment.d/
echo DEPLOYED
IN
@@ -0,0 +1,47 @@
# Cross toolchain: WSL clang -> aarch64 Arch Linux ARM (glibc) container "arch-kde-mgl".
# Sysroot exported from the container by refresh-sysroot.sh (default $HOME/sysroots/arch-kde-mgl).
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(_triple aarch64-unknown-linux-gnu)
if (DEFINED ENV{MGL_SYSROOT})
set(_sysroot "$ENV{MGL_SYSROOT}")
else()
set(_sysroot "$ENV{HOME}/sysroots/arch-kde-mgl")
endif()
# The container's GCC install dir (crtbegin/crtend, libgcc, libstdc++ via /usr/include/c++/<ver>).
file(GLOB _gcc_dirs LIST_DIRECTORIES true "${_sysroot}/usr/lib/gcc/${_triple}/*")
list(SORT _gcc_dirs COMPARE NATURAL ORDER DESCENDING)
list(GET _gcc_dirs 0 _gcc_install_dir)
set(CMAKE_SYSROOT "${_sysroot}")
set(CMAKE_C_COMPILER clang)
set(CMAKE_CXX_COMPILER clang++)
set(CMAKE_ASM_COMPILER clang)
set(CMAKE_C_COMPILER_TARGET ${_triple})
set(CMAKE_CXX_COMPILER_TARGET ${_triple})
set(CMAKE_ASM_COMPILER_TARGET ${_triple})
set(CMAKE_AR llvm-ar)
set(CMAKE_RANLIB llvm-ranlib)
set(CMAKE_NM llvm-nm)
set(CMAKE_OBJCOPY llvm-objcopy)
set(CMAKE_STRIP llvm-strip)
set(_xflags "--gcc-install-dir=${_gcc_install_dir}")
set(CMAKE_C_FLAGS_INIT "${_xflags}")
set(CMAKE_CXX_FLAGS_INIT "${_xflags}")
set(CMAKE_ASM_FLAGS_INIT "${_xflags}")
set(CMAKE_EXE_LINKER_FLAGS_INIT "-fuse-ld=lld")
set(CMAKE_SHARED_LINKER_FLAGS_INIT "-fuse-ld=lld")
set(CMAKE_MODULE_LINKER_FLAGS_INIT "-fuse-ld=lld")
set(CMAKE_LINKER_TYPE LLD)
set(CMAKE_FIND_ROOT_PATH "${_sysroot}")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(ENV{PKG_CONFIG_SYSROOT_DIR} "${_sysroot}")
set(ENV{PKG_CONFIG_LIBDIR} "${_sysroot}/usr/lib/pkgconfig:${_sysroot}/usr/share/pkgconfig")
set(ENV{PKG_CONFIG_PATH} "")
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# xsysroot/refresh-sysroot.sh - (re)export the cross sysroot from the container into WSL
# ~/sysroots/arch-kde-mgl and install the toolchain file as ~/sysroots/aarch64-arch.cmake.
# Read-only on the container side (it only tars headers/libs into /root and moves the tarball out).
# Run once on a new machine, and again after the container upgrades glibc/gcc/vulkan/libdrm/gbm;
# then `bash xbuild.sh --reconfigure`. Env: CHANNEL (default xr), WSL_DISTRO.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/../env.sh"
cp -f "$here/aarch64-arch.cmake" "$ANL_DIR/aarch64-arch.cmake"
bash "$here/../ct.sh" "${CHANNEL:-xr}" <<'IN'
set -e; cd /
L=$(ls -d usr/lib/*crt*.o usr/lib/lib{c,m,mvec,pthread,dl,rt,util,resolv,anl,stdc++,stdc++fs,stdc++exp,supc++,gcc_s,atomic,vulkan,drm,drm_*,gbm,z}.* usr/lib/lib{c,m}_nonshared* usr/lib/ld-linux* usr/lib/pkgconfig usr/share/pkgconfig usr/share/cmake/VulkanHeaders usr/lib/cmake/Vulkan* 2>/dev/null)
tar czf /root/xsysroot.tar.gz --exclude='usr/lib/gcc/*/*/cc1' --exclude='usr/lib/gcc/*/*/cc1plus' --exclude='usr/lib/gcc/*/*/lto1' --exclude='usr/lib/gcc/*/*/lto-wrapper' --exclude='usr/lib/gcc/*/*/collect2' --exclude='usr/lib/gcc/*/*/plugin' --exclude='usr/lib/gcc/*/*/install-tools' --exclude='usr/lib/gcc/*/*/liblto_plugin.so' lib usr/include usr/lib/gcc $L
echo SYSROOT_TAR_DONE
IN
adb_ shell "su -c 'mv $CT_ROOT/root/xsysroot.tar.gz $DEV_TOOLS/xsysroot.tar.gz && chmod 644 $DEV_TOOLS/xsysroot.tar.gz'"
adb_ pull "$DEV_TOOLS/xsysroot.tar.gz" "$ANL_DIR/xsysroot.tar.gz"
adb_ shell "su -c 'rm -f $DEV_TOOLS/xsysroot.tar.gz'"
wsl -d "${WSL_DISTRO:-archlinux}" -- env A="$(win2wsl "$ANL_DIR")" bash -s <<'IN' 2>&1 | tr -d '\0' | grep -av '^wsl:'
set -e; S=$HOME/sysroots/arch-kde-mgl; rm -rf "$S.new"; mkdir -p "$S.new"
tar xzf "$A/xsysroot.tar.gz" -C "$S.new"
rm -rf "$S"; mv "$S.new" "$S"; cp -f "$A/aarch64-arch.cmake" "$HOME/sysroots/aarch64-arch.cmake"
du -sh "$S"; ls "$S/usr/lib/gcc/aarch64-unknown-linux-gnu/"
IN
rm -f "$ANL_DIR/xsysroot.tar.gz"
+5
View File
@@ -0,0 +1,5 @@
# Layout-only commits for `git blame` to look past:
# git config blame.ignoreRevsFile .git-blame-ignore-revs
# clang-format every C and C++ source
f4994ee36e3aa2d53c22370e471c976a3fae719f
@@ -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
+29 -4
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'
@@ -61,15 +61,40 @@ if ! grep -Eq '^lib/[^/]+/libMobileGL\.so$' <<<"$plugin_contents"; then
exit 1
fi
# The render server is not trace-only: both flavors carry the control screen, the offscreen
# service, the on-screen display activity, and (split native builds) the supervisor executable.
# The plugin APK ships to users, so its build keeps the three components private (manifest
# placeholder serverComponentsExported=false); only the debuggable trace APK exports them for adb.
for component in ServerControlActivity MobileGLServerService MobileGLDisplayActivity; do
require "$component" "$plugin_manifest" "plugin render server component ($component)"
require "$component" "$trace_manifest" "trace render server component ($component)"
done
if [[ "${MOBILEGL_CI_DISAGGREGATED:-OFF}" == "ON" ]]; then
trace_contents=$(unzip -Z1 "$trace_apk")
for entry in "plugin:${plugin_contents}" "trace:${trace_contents}"; do
if ! grep -Eq '^lib/[^/]+/libMobileGLServer\.so$' <<<"${entry#*:}"; then
echo "::error::${entry%%:*} APK does not contain libMobileGLServer.so (both flavors ship the render server)" >&2
exit 1
fi
done
fi
require 'top.mobilegl.plugin.trace' "$trace_manifest" 'trace package name'
require 'top.mobilegl.plugin.TRACE_REPLAY' "$trace_manifest" 'trace replay action'
if grep -Fq 'fclPlugin' <<<"$trace_manifest"; then
echo '::error::Trace APK must not advertise renderer-plugin metadata' >&2
exit 1
fi
if grep -Fq 'android.intent.action.MAIN' <<<"$trace_manifest"; then
echo '::error::Trace APK must not expose a launcher activity' >&2
# The trace APK IS a launchable app now: PostActivity (the POST self-test screen) is its
# entry icon and the bottom navigation bar switches to the render server screen. What is
# left of the old "no launcher" contract is the count - exactly ONE MAIN activity, so a
# screen that accidentally grows a launcher entry (ServerControlActivity was one for a day)
# reds here instead of shipping two icons.
main_count=$(grep -Fc 'android.intent.action.MAIN' <<<"$trace_manifest")
if [[ "$main_count" -ne 1 ]]; then
echo "::error::Trace APK must expose exactly one launcher activity (PostActivity); found ${main_count} android.intent.action.MAIN" >&2
exit 1
fi
require 'PostActivity' "$trace_manifest" 'trace launcher activity (PostActivity)'
echo 'Validated unified MobileGL plugin APK and isolated trace APK.'
+61 -16
View File
@@ -11,6 +11,7 @@ on:
# someone remembering to dispatch the workflow by hand.
- feat/disaggregated
- codex/fix-github-ci
- cleanup
# A docs-only push builds nothing this workflow tests (231 emulator legs per run). The Test
# workflow keeps running for docs: it hosts the doc citation lint.
paths-ignore:
@@ -18,6 +19,10 @@ on:
- '**/*.md'
workflow_dispatch:
env:
# Software-rendered CI submissions may exceed the interactive GPU hang budget.
MOBILEGL_GPU_HANG_BUDGET_MS: "0"
# ONE LIVE RUN PER BRANCH, as in test.yml: a superseded push is cancelled rather than left queued
# behind its successor; a manual dispatch gets a group of its own; dev queues rather than cancels.
concurrency:
@@ -37,7 +42,7 @@ jobs:
CCACHE_MAXSIZE: 4G
CCACHE_NOHASHDIR: "true"
MOBILEGL_CMAKE_COMPILER_LAUNCHER: ccache
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci' || github.ref_name == 'cleanup') && 'ON' || 'OFF' }}
steps:
- name: Checkout repo
@@ -72,12 +77,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
@@ -88,8 +106,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
@@ -102,7 +125,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 }}
@@ -151,7 +173,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 }}
@@ -235,7 +256,7 @@ jobs:
runs-on: ubuntu-latest
needs: build
env:
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci' || github.ref_name == 'cleanup') && 'ON' || 'OFF' }}
outputs:
android: ${{ steps.trace-cases.outputs.android }}
names: ${{ steps.trace-cases.outputs.names }}
@@ -332,6 +353,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
@@ -411,6 +444,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
@@ -468,16 +513,14 @@ 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}"
extra_retrace_args=()
if [ "${MOBILEGL_CI_TRANSPORT}" = "inproc" ]; then
extra_retrace_args+=(
--env "MOBILEGL_TRANSPORT=inproc;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1"
--env "MOBILEGL_TRANSPORT=inproc;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}"
--require-inproc
)
elif [ "${MOBILEGL_CI_TRANSPORT}" = "spawn" ]; then
@@ -488,17 +531,18 @@ 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"
--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
--require-spawn
)
else
extra_retrace_args+=(--env "MOBILEGL_TRANSPORT=monolith")
extra_retrace_args+=(--env "MOBILEGL_TRANSPORT=monolith;MOBILEGL_GPU_HANG_BUDGET_MS=${MOBILEGL_GPU_HANG_BUDGET_MS}")
fi
# Bliss needs the newer signed ANGLE variant plus sampler mipmap
# min-filter downgrading on ANGLE llvmpipe.
@@ -633,7 +677,7 @@ jobs:
needs: retrace
if: always()
env:
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci' || github.ref_name == 'cleanup') && 'ON' || 'OFF' }}
steps:
- name: Checkout repo
@@ -731,7 +775,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" \
+40
View File
@@ -0,0 +1,40 @@
name: Text hygiene
# CONTRIBUTING.md's checks: the pushed commits' messages, the source-text rules and ratchets,
# clang-format on each commit's changed lines, and the layering and size ratchets. This is its own
# workflow without a concurrency group, so a later push never cancels it and every push's commits
# are checked.
on:
push:
branches: ['**']
workflow_dispatch:
permissions:
contents: read
jobs:
text-hygiene:
name: Commit messages and source text
runs-on: ubuntu-latest
timeout-minutes: 15
env:
TEXT_HYGIENE_BASE_BRANCH: feat/disaggregated
steps:
- name: Checkout repo
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install clang-format
run: |
python3 -m venv "${RUNNER_TEMP}/clang-format"
"${RUNNER_TEMP}/clang-format/bin/pip" install --quiet 'clang-format==23.1.3'
echo "CLANG_FORMAT=${RUNNER_TEMP}/clang-format/bin/clang-format" >> "${GITHUB_ENV}"
- name: Lint self-test
run: |
python3 scripts/check_text_hygiene.py self-test
python3 scripts/check_structure.py self-test
- name: Check commits, source text and formatting
run: python3 scripts/check_text_hygiene.py ci
- name: Check layering and file and function size
if: ${{ !cancelled() }}
run: python3 scripts/check_structure.py ci
+966 -991
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -28,3 +28,4 @@ tools/trace_replay/work/
__pycache__/
*.py[cod]
/.gradle
/.trace-work/
+11
View File
@@ -0,0 +1,11 @@
# glvnd vendor ABI headers
'libeglabi.h' and 'GLdispatchABI.h' from libglvnd (MIT), copied verbatim with their own licence
headers intact.
They are here because MobileGL implements the *vendor* half of glvnd's EGL ABI - the side a library
exposes so the system's 'libEGL.so.1', which IS glvnd, can dispatch to it. The distribution ships
the runtime but not these headers in the base image, and a vendored copy of two MIT headers is a
smaller dependency than a build-time download of a source tree.
Upstream ABI version implemented: 'EGL_VENDOR_ABI_MAJOR_VERSION' 0 (the header pins it).
+141
View File
@@ -0,0 +1,141 @@
/*
* Copyright (c) 2013, NVIDIA CORPORATION.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and/or associated documentation files (the
* "Materials"), to deal in the Materials without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Materials, and to
* permit persons to whom the Materials are furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* unaltered in all copies or substantial portions of the Materials.
* Any additions, deletions, or changes to the original source files
* must be clearly indicated in accompanying documentation.
*
* THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
*/
#include <GL/gl.h>
#if !defined(__GL_DISPATCH_ABI_H)
#define __GL_DISPATCH_ABI_H
#if defined(__cplusplus)
extern "C" {
#endif
/*!
* \defgroup gldispatchabi GL dispatching ABI
*
* This is not a complete ABI, but rather a fragment common to the libEGL and
* libGLX ABIs. Changes to this file should be accompanied by a version bump to
* these client ABIs.
*/
/*!
* Thread-local implementation used by libglvnd. This is passed into the patch
* function callback via the type parameter.
*
* For most architectures, the vendor library can ignore this parameter, since
* it will always be the same value. It's used for systems like ARM, where the
* stubs might be use the ARM or Thumb instruction sets.
*
* The stub type does not make any distinction between TLS and TSD stubs. The
* entire purpose of entrypoint rewriting is to skip the dispatch table in
* libGLdispatch.so, so it doesn't matter how that dispatch table is stored.
*/
enum {
/*!
* Indicates that the stubs aren't defined in assembly. For example, if the
* dispatch stubs are written in C. Vendor libraries generally won't see
* this value.
*/
__GLDISPATCH_STUB_UNKNOWN,
/*!
* Used for stubs on x86 systems.
*/
__GLDISPATCH_STUB_X86,
/*!
* Used for stubs on x86-64 systems.
*/
__GLDISPATCH_STUB_X86_64,
/*!
* Used for stubs on ARMv7, using the Thumb instruction set.
*/
__GLDISPATCH_STUB_ARMV7_THUMB,
/*!
* Used for stubs on ARMv7, using the normal ARM instruction set.
*/
__GLDISPATCH_STUB_ARMV7_ARM,
/*!
* Used for stubs on ARMv8/aarch64.
*/
__GLDISPATCH_STUB_AARCH64,
/*!
* Used for stubs on x32 builds (x86-64 with 32-bit pointers).
*/
__GLDISPATCH_STUB_X32,
/*!
* Used for stubs on PPC64 systems.
*/
__GLDISPATCH_STUB_PPC64,
/*!
* Used for stubs on PPC64LE systems. Same as PPC64, for compatibility.
*/
__GLDISPATCH_STUB_PPC64LE = __GLDISPATCH_STUB_PPC64,
/*!
* Used for stubs on LOONGARCH/loongarch64.
*/
__GLDISPATCH_STUB_LOONGARCH64,
};
/*!
* A callback function called by the vendor library to fetch the address of an
* entrypoint.
*
* The function returns two pointers, one writable and one executable. The two
* pointers may or may not be the same virtual address, but they will both be
* mappings of the same physical memory.
*
* The vendor library should write its entrypoint to the address returned by
* \p writePtr, but should use the address from \p execPtr for things like
* calculating PC-relative offsets.
*
* Note that if this function fails, then the vendor library can still try to
* patch other entrypoints.
*
* Note that on ARM, the low-order bit of both \c execPtr and \p writePtr will
* be zero, even if the stub uses the thumb instruction set. The vendor library
* should use the \c type parameter of \c initiatePatch to determine which
* instruction set to use.
*
* \param funcName The function name.
* \param[out] writePtr The pointer that the vendor library can write to.
* \param[out] execPtr The pointer to the executable code.
* \return GL_TRUE if the entrypoint exists, or GL_FALSE if it doesn't.
*/
typedef GLboolean (*DispatchPatchLookupStubOffset)(const char *funcName,
void **writePtr, const void **execPtr);
#if defined(__cplusplus)
}
#endif
#endif // __GL_DISPATCH_ABI_H
+464
View File
@@ -0,0 +1,464 @@
/*
* Copyright (c) 2013, NVIDIA CORPORATION.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and/or associated documentation files (the
* "Materials"), to deal in the Materials without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Materials, and to
* permit persons to whom the Materials are furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* unaltered in all copies or substantial portions of the Materials.
* Any additions, deletions, or changes to the original source files
* must be clearly indicated in accompanying documentation.
*
* THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
*/
#if !defined(__LIB_EGL_ABI_H)
#define __LIB_EGL_ABI_H
#include <stdint.h>
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include "glvnd/GLdispatchABI.h"
#if defined(__cplusplus)
extern "C" {
#endif
/*!
* \defgroup eglvendorabi EGL Vendor ABI
*
* Definition of ABI exported by libEGL.so to libEGL_VENDOR.so libraries.
*
* Each vendor is associated with three distinct dispatch table types:
*
* - static EGL dispatch table: this is the fixed list of EGL 1.5 entrypoints
* provided by the vendor at load time during the initial handshake.
* - dynamic EGL dispatch table: this is a structure allocated by the API
* library at runtime used to manage EGL extension functions which are not
* present in the static table.
* - core GL dispatch table: this is a structure maintained by the API library
* which contains both GL core (static) and GL extension (dynamic) functions.
*
* @{
*/
/*
* Rendering API handling:
*
* libEGL only supports OpenGL and OpenGL ES, not OpenVG. If OpenVG or any
* other API is added, then the major version number will be incremented.
*
* When the application calls eglBindAPI, libEGL will forward the call to every
* vendor library. In addition, a vendor library can query the current API from
* libEGL using the getCurrentApi callback.
*
* Vendor libraries are not required to support both GL and GLES, but they must
* be able to deal with either one as the current rendering API. If a vendor
* doesn't support the current API, then it should return an error from
* eglCreateContext.
*/
/*!
* Current version of the ABI.
*
* This version number contains a major number in the high-order 16 bits, and
* a minor version number in the low-order 16 bits.
*
* The major version number is incremented when an interface change will break
* backwards compatibility with existing vendor libraries. The minor version
* number is incremented when there's a change but existing vendor libraries
* will still work.
*/
#define EGL_VENDOR_ABI_MAJOR_VERSION ((uint32_t) 0)
#define EGL_VENDOR_ABI_MINOR_VERSION ((uint32_t) 2)
#define EGL_VENDOR_ABI_VERSION ((EGL_VENDOR_ABI_MAJOR_VERSION << 16) | EGL_VENDOR_ABI_MINOR_VERSION)
static inline uint32_t EGL_VENDOR_ABI_GET_MAJOR_VERSION(uint32_t version)
{
return version >> 16;
}
static inline uint32_t EGL_VENDOR_ABI_GET_MINOR_VERSION(uint32_t version)
{
return version & 0xFFFF;
}
/*!
* This opaque structure stores function pointers for EGL extension functions.
* It is allocated at runtime by the API library. Vendor-provided dispatch
* functions retrieve and operate on this structure using the API below.
*/
typedef struct __EGLvendorInfoRec __EGLvendorInfo;
/****************************************************************************
* API library exports *
****************************************************************************/
typedef struct __EGLapiExportsRec {
/************************************************************************
* The following routines are used by vendor-implemented EGL dispatch
* functions to lookup and call into the right vendor.
************************************************************************/
/*!
* This function must be called at the start of every EGL dispatch stub. It
* performs any necessary per-call bookkeeping.
*/
void (* threadInit) (void);
EGLenum (* getCurrentApi)(void);
/*!
* This function retrieves the appropriate current dynamic dispatch table,
* if a GL context is current. Otherwise, this returns NULL.
*/
__EGLvendorInfo *(*getCurrentVendor)(void);
/*!
* This retrieves the current context for this thread.
*/
EGLContext (*getCurrentContext)(void);
/*!
* Returns the current display for this thread.
*/
EGLDisplay (*getCurrentDisplay)(void);
/*!
* Returns the current drawable for this thread.
*/
EGLSurface (*getCurrentSurface)(EGLint readDraw);
/*!
* This function retrieves an entry point from the dynamic dispatch table
* given an index into the table.
*/
__eglMustCastToProperFunctionPointerType (*fetchDispatchEntry) (__EGLvendorInfo *dynDispatch, int index);
/*!
* Sets the last error for the current thread. The error will be returned
* the next time the app calls eglGetError().
*
* This function will override a call to \c setLastVendor, and vice-versa.
*/
void (* setEGLError) (EGLint errorCode);
/*!
* Notifies libEGL about the vendor library that an EGL function is
* dispatched to. This is used to look up the last EGL error code from a
* vendor.
*
* A vendor library may generate an EGL error asynchronously, if it
* offloads some EGL functions onto a worker thread. In that case, calling
* \c setEGLError to set the last error would not work.
*
* When an EGL dispatch stubs finds the vendor library, it must call this
* function before it dispatches the function call.
*
* This function will override a call to \c setEGLError, and vice-versa.
*
* \param vendor The vendor library of the current EGL function call.
* \return EGL_TRUE on success, or EGL_FALSE on failure.
*/
EGLBoolean (* setLastVendor) (__EGLvendorInfo *vendor);
/************************************************************************
* These routines are used by vendor dispatch functions to look up
* and add mappings between various objects and vendors.
************************************************************************/
// As far as I can tell, all EGL functions that take a context or drawable
// also take an EGLDisplay. So, we don't need any extra functions for
// mapping a context/surface/whatever to a EGLDisplay or vendor.
/*!
* Returns the EGL vendor for an EGLDisplay handle.
*/
__EGLvendorInfo *(*getVendorFromDisplay)(EGLDisplay dpy);
/*!
* Returns the EGL vendor for an EGLDeviceEXT handle.
*/
__EGLvendorInfo *(*getVendorFromDevice)(EGLDeviceEXT dev);
/**
* Sets the EGL vendor for an EGLDeviceEXT handle. The dispatch stub for
* any function that returns an EGLDeviceEXT handle should call this.
*
* Supported since ABI version 1.2.
*/
EGLBoolean (* setVendorForDevice)(EGLDeviceEXT dev, __EGLvendorInfo *vendor);
} __EGLapiExports;
/*****************************************************************************
* API library imports *
*****************************************************************************/
/*!
* The enum values accepted by \c __EGLapiImports::getVendorString.
*/
enum
{
/*!
* The set of platform extensions that the vendor supports.
*
* Platform extensions typically just add enums to the existing
* eglGetPlatformDisplay function, so we don't need any additional logic
* or special handling from libEGL itself.
*
* As a result, vendor libraries can add these extensions without having
* to modify libEGL to match.
*/
__EGL_VENDOR_STRING_PLATFORM_EXTENSIONS,
};
/*!
* This structure stores required and optional vendor library callbacks.
*/
typedef struct __EGLapiImportsRec {
/**
* Creates an EGLDisplay. This function is used to handle both
* \c eglGetDisplay and \c eglGetPlatformDisplay.
*
* For some platform types, libEGL may be able to identify a specific
* vendor to use.
*
* If libEGL can't figure out which vendor to use on its own, then it will
* go through the list of available vendor libraries and call this function
* until one succeeds.
*
* If the application calls eglGetPlatformDisplay, then the parameters are
* passed through from the application.
*
* If the application calls eglGetDisplay, and the native display handle is
* not \c EGL_DEFAULT_DISPLAY, then libEGL will try to guess a platform
* type, and will pass that platform to the vendor library.
*
* Lastly, if eglGetDisplay is called with \c EGL_DEFAULT_DISPLAY, then
* libEGL will call into each vendor library with \p platform set to
* \c EGL_NONE. The vendor library can then select a default display to
* return.
*
* In all cases, if the vendor library can't find a matching EGLDisplay,
* then it should return \c EGL_NO_DISPLAY. Any errors should be reported
* through the vendor's \c eglGetError function.
*/
EGLDisplay (* getPlatformDisplay) (EGLenum platform, void *nativeDisplay,
const EGLAttrib *attrib_list);
/*!
* Checks if the vendor library supports a given client API (that is, the
* API value passed to \c eglBindAPI).
*/
EGLBoolean (* getSupportsAPI) (EGLenum api);
/*!
* (OPTIONAL) Returns a string from the vendor library. This is used for
* anything that isn't available from eglQueryString.
*/
const char * (* getVendorString) (int name);
/*
* NOTE: libEGL will load all of the vendor libraries up front, so it
* handles extension functions slightly differently than EGL.
*/
/*!
* This retrieves the pointer to the real EGL or core GL function.
*
* \param procName The name of the function.
* \return A pointer to a function, or \c NULL if the vendor does not
* support the function.
*/
void * (* getProcAddress) (const char *procName);
/*!
* This retrieves vendor-neutral functions which use the
* __EGLdispatchTableDynamic API above to dispatch to the correct vendor.
*
* A vendor library must provide a dispatch function for all EGL display
* extension functions that it supports.
*
* Client extension functions cannot be dispatched based on an EGDisplay,
* so they must be handled in libEGL itself.
*
* \param procName The name of the function.
* \return A pointer to a function, or \c NULL if the vendor does not
* support the function or \p procName is not a EGL display function.
*/
void * (*getDispatchAddress) (const char *procName);
/*!
* This notifies the vendor library which dispatch table index is
* assigned to a particular EGL extension function.
*/
void (*setDispatchIndex) (const char *procName, int index);
/*
* The vendor library may use the isPatchSupported, initiatePatch,
* releasePatch, and patchThreadAttach callbacks to re-write libglvnd's
* entrypoints at make current time, provided no other contexts are current
* and the TLS model supports this functionality. This is a performance
* optimization that may not be available at runtime; the vendor library
* must not depend on this functionality for correctness.
*
* To use this optimization, the vendor library must provide at least the
* isPatchSupported and initiatePatch entrypoints.
*
* Note that these functions are identical to the ones used in GLX.
*/
/*!
* (OPTIONAL) Checks to see if the vendor library supports patching the
* given stub type and size.
*
* \param type The type of entrypoints. This will be a one of the
* __GLDISPATCH_STUB_* values.
* \param stubSize The maximum size of the stub that the vendor library can
* write, in bytes.
* \param lookupStubOffset A callback into libglvnd to look up the address
* of each entrypoint.
*/
GLboolean (* isPatchSupported)(int type, int stubSize);
/*!
* (OPTIONAL) Called by libglvnd to request that a vendor library patch its
* top-level entrypoints.
*
* The vendor library should use the \p lookupStubOffset callback to find
* the addresses of each entrypoint.
*
* This function may be called more than once to patch multiple sets of
* entrypoints. For example, depending on how they're built, libOpenGL.so
* or libGL.so may have their own entrypoints that are separate functions
* from the ones in libGLdispatch.
*
* Note that during this call is the only time that the entrypoints can be
* modified. After the call to \c initiatePatch returns, the vendor library
* should treat the entrypoints as read-only.
*
* \param type The type of entrypoints. This will be a one of the
* __GLDISPATCH_STUB_* values.
* \param stubSize The maximum size of the stub that the vendor library can
* write, in bytes.
* \param lookupStubOffset A callback into libglvnd to look up the address
* of each entrypoint.
*
* \return GL_TRUE if the vendor library supports patching with this type
* and size.
*/
GLboolean (*initiatePatch)(int type,
int stubSize,
DispatchPatchLookupStubOffset lookupStubOffset);
/*!
* (OPTIONAL) Called by libglvnd to notify the current vendor that it no
* longer owns the top-level entrypoints.
*
* Libglvnd will take care of the restoring the entrypoints back to their
* original state. The vendor library must not try to modify them.
*/
void (*releasePatch)(void);
/*!
* (OPTIONAL) Called at the start of window-system functions (GLX and EGL).
* This callback allows vendor libraries to perform any per-thread
* initialization.
*
* This is basically a workaround for broken applications. A lot of apps
* will make one or more invalid GLX/EGL calls on a thread (often including
* a MakeCurrent with invalid parameters), and then will try to call an
* OpenGL function.
*
* A non-libglvnd-based driver would be able to initialize any thread state
* even on a bogus GLX call, but with libglvnd, those calls wouldn't get
* past libGLX.
*
* This function is optional. If it's \c NULL, then libGLdispatch will
* simply ignore it.
*
* \note This function may be called concurrently from multiple threads.
*/
void (*patchThreadAttach)(void);
/*!
* (OPTIONAL) Tries to determine the platform type for a native display.
*
* If the vendor library provides this function, then libglvnd will call it
* to determine which platform to use for a native display handle in
* eglGetDisplay.
*
* If no vendor library identifies the platform, then libglvnd will fall
* back to its own platform detection logic.
*
* Libglvnd can call this function for any native display handle except
* \c EGL_DEFAULT_DISPLAY.
*
* No matter what the value of \p native_display, the vendor library must
* not crash, and must not return a false match. If the vendor library
* can't identify the display, then it must return \c EGL_NONE.
*
* In particular, that means that a vendor library must not return any sort
* of default or fallback platform.
*
* \param native_display The native display handle passed to eglGetDisplay.
* \return Either a platform type enum or EGL_NONE.
*/
EGLenum (* findNativeDisplayPlatform) (void *native_display);
} __EGLapiImports;
/*****************************************************************************/
#define __EGL_MAIN_PROTO_NAME "__egl_Main"
typedef EGLBoolean (* __PFNEGLMAINPROC) (uint32_t version, const __EGLapiExports *exports,
__EGLvendorInfo *vendor, __EGLapiImports *imports);
/*!
* Vendor libraries must export a function called __egl_Main() with the
* following prototype.
*
* This function also performs a handshake based on the ABI version number.
* Vendor libraries can optionally use the version number to support older
* versions of the ABI.
*
* \param[in] version The ABI version. The upper 16 bits contains the major version
* number, and the lower 16 bits contains the minor version number.
*
* \param[in] exports The table of functions provided by libEGL. This pointer will
* remain valid for as long as the vendor is loaded.
*
* \param[in] vendor The opaque pointer used to identify this vendor library. This
* may be used in future versions to provide additional per-vendor information.
*
* \param[out] imports The function table that the vendor library should fill
* in. The vendor library must assign every non-optional function in the
* struct.
*
* \return True on success. If the vendor library does not support the
* requested ABI version or if some other error occurs, then it should return
* False.
*/
EGLBoolean __egl_Main(uint32_t version, const __EGLapiExports *exports,
__EGLvendorInfo *vendor, __EGLapiImports *imports);
/*!
* @}
*/
#if defined(__cplusplus)
}
#endif
#endif /* __LIB_EGL_ABI_H */
+17
View File
@@ -0,0 +1,17 @@
# Notes for coding agents
Read `CONTRIBUTING.md` before changing anything. Every rule in it applies to agents.
@CONTRIBUTING.md
- Before each commit:
1. Stage exactly what you mean to commit (no `git commit -a`).
2. Print the scope with `python3 scripts/check_text_hygiene.py scope`.
3. Write the message as UTF-8 without a byte-order mark, to a file outside the repository.
4. Check it with `python3 scripts/check_text_hygiene.py message <file>`.
5. Commit with `git commit -F <file>` once the check passes.
- On Windows, run `python` instead of `python3`.
- Do not copy the format of older commits. Much of the history before October 2026 breaks these rules.
- When you brief a sub-agent, give it the exact commit format and these rules, not a paraphrase.
- Keep working papers (briefs, reviews, scratch measurements, logs) outside the repository.
- Add no `Co-Authored-By` or other trailers.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+416 -136
View File
@@ -21,7 +21,7 @@ option(MOBILEGL_IOS "Build MobileGL for iOS instead of macOS when
# That emptiness is one of the two byte-level equalities the plan's validation
# gates keep (section 10.3).
option(MOBILEGL_BUILD_DISAGGREGATED "Build the MG_Remote transport layer (two-process shape)" OFF)
# The CI / debugging shape (ARCHITECTURE.md:581): both roles in ONE process, talking over the
# The CI / debugging shape (ARCHITECTURE.md §16): both roles in ONE process, talking over the
# same SEG_CMD ring and the same G3 codec a spawned server would use. It IMPLIES
# MOBILEGL_BUILD_DISAGGREGATED (below) and additionally admits the role-isolation shims that
# only make sense when the two roles share an address space. It is a SUPERSET, never a
@@ -29,18 +29,27 @@ option(MOBILEGL_BUILD_DISAGGREGATED "Build the MG_Remote transport layer (two-pr
# only decides whether the shims are compiled in.
option(MOBILEGL_BUILD_DISAGGREGATED_INPROC "Compile the in-process (one-process, two-role) split shims; implies MOBILEGL_BUILD_DISAGGREGATED" OFF)
option(MOBILEGL_BUILD_SERVER_SPIKE "Build the P0 spike-A MobileGLServer delivery-chain executable (Android only)" OFF)
# The PipeInputs strangler (ARCHITECTURE.md 9.2). OFF is the pull build and must stay
# byte-identical to a tree without either option: MGB_CTX is the live GLContext, no
# MGPipe/PipeInputs source is compiled, every MGP_FILL is ((void)0).
option(MOBILEGL_PIPE_PUSH "Backends read frontend state through the MGPipe PipeInputs block instead of MG_State::pGLContext (ARCHITECTURE.md 9.2 phase A)" OFF)
option(MOBILEGL_PIPE_VERIFY "Compile SnapshotFromGLContext() and the G4 per-verb shadow comparator; implies MOBILEGL_PIPE_PUSH; never shipped" OFF)
# Track H's old-versus-new arm (ARCHITECTURE.md 9.6). With a MOBILEGL_PIPE_PUSH bit clear
# the backend would still run the RE-KEYED memo code, so the bitmask alone stops being a
# valid A/B the moment a handle wave lands: this option compiles the pre-handle arm - the
# registries, OwnerEquals, the TwinLookupMemos, g_fbSlotCache, ComputePipelineStateHash,
# the address-keyed VaoDrawMemo - beside it, behind the same PipeInputs interface. ON for
# the whole migration window; it retires with the pull path itself at P13.
option(MOBILEGL_PIPE_LEGACY_MEMOS "Compile the pre-handle memo arm beside the {slot, gen} arm so Track H has a real A/B (ARCHITECTURE.md 9.6)" ON)
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) 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")
@@ -109,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}")
@@ -124,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)
@@ -155,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()
@@ -302,6 +353,7 @@ set(SOURCE_FILES
MobileGL/ConfigLoader.cpp
MobileGL/MG_Util/Debug/Log.cpp
MobileGL/MG_Util/X11/DisplayGuard.cpp
MobileGL/MG_Util/Async/JobNode.cpp
MobileGL/MG_Util/Async/ShaderCompilePool.cpp
@@ -357,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
@@ -364,11 +417,15 @@ 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
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeduplicateImageTypesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DSampledImagesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
@@ -395,13 +452,20 @@ 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
MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp
MobileGL/MG_Impl/GLXImpl/X11Present.cpp
MobileGL/MG_Impl/GLXImpl/X11PresentXcb.cpp
MobileGL/MG_Impl/GLXImpl/LookUp/LookUp.cpp
MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp
MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp
MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp
MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp
# @INSERTION_POINT:SOURCE_FILE_GLIMPL@ #
MobileGL/MG_Impl/GLImpl/Sampler/Validators.cpp
@@ -411,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
@@ -431,17 +496,51 @@ set(SOURCE_FILES
MobileGL/MG_Backend/Init.cpp
MobileGL/MG_Backend/BackendObject.cpp
MobileGL/MG_Backend/ClientReadback.cpp
MobileGL/MG_Backend/GpuHangWatch.cpp
MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp
MobileGL/MG_Backend/DirectGLES/DirectGLESBind.cpp
MobileGL/MG_Backend/DirectGLES/DirectGLESBlit.cpp
MobileGL/MG_Backend/DirectGLES/DirectGLESBuffers.cpp
MobileGL/MG_Backend/DirectGLES/DirectGLESContext.cpp
MobileGL/MG_Backend/DirectGLES/DirectGLESCopy.cpp
MobileGL/MG_Backend/DirectGLES/DirectGLESDraw.cpp
MobileGL/MG_Backend/DirectGLES/DirectGLESReadback.cpp
MobileGL/MG_Backend/DirectGLES/DirectGLESSharedImage.cpp
MobileGL/MG_Backend/DirectGLES/DirectGLESState.cpp
MobileGL/MG_Backend/DirectGLES/DirectGLESSync.cpp
MobileGL/MG_Backend/DirectGLES/DirectGLESTextures.cpp
MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp
MobileGL/MG_Backend/DirectGLES/Utils.cpp
MobileGL/MG_Backend/DirectGLES/Managers.cpp
MobileGL/MG_Backend/DirectGLES/ManagersBuffers.cpp
MobileGL/MG_Backend/DirectGLES/ManagersFramebuffers.cpp
MobileGL/MG_Backend/DirectGLES/ManagersPrograms.cpp
MobileGL/MG_Backend/DirectGLES/ManagersSamplers.cpp
MobileGL/MG_Backend/DirectGLES/ManagersTextureSync.cpp
MobileGL/MG_Backend/DirectGLES/ManagersTextures.cpp
MobileGL/MG_Backend/DirectGLES/ManagersVertexArrays.cpp
MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp
MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp
MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp
MobileGL/MG_Backend/DirectVulkan/MagmaSession.cpp
MobileGL/MG_Backend/DirectVulkan/VmaImpl.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRendererCopy.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRendererDevice.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRendererDraw.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRendererPipeline.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRendererPresent.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRendererSubmit.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlit.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthMipmap.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolve.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/WireYuvImage.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp
@@ -457,8 +556,10 @@ set(SOURCE_FILES
MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/GpuProgressMarkers.cpp
MobileGL/MG_State/GLState/Core.cpp
MobileGL/MG_State/GLState/ShareGroupState.cpp
MobileGL/MG_State/EGLState/Core.cpp
MobileGL/MG_State/GLState/ErrorState/Error.cpp
MobileGL/MG_State/GLState/BufferState/BufferState.cpp
@@ -531,63 +632,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 (docs/archive/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")
@@ -607,9 +679,17 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
MobileGL/MG_Remote/Transport/InProcessTransport.cpp
MobileGL/MG_Remote/Transport/SocketTransport.cpp
MobileGL/MG_Remote/Transport/ControlInbox.cpp
# P9 W3: the server's log forward - a queue and a sender thread, graded by severity
# (<= WARN lossy, >= ERROR lossless and paced), so no logging thread writes the socket.
MobileGL/MG_Remote/Transport/LogForward.cpp
# Keeps MG_Util/Debug/Log.h - and through it the GL frontend's
# umbrella header - out of the header-only wire code (WireLog.h).
MobileGL/MG_Remote/Transport/WireLog.cpp
# P11 A1: the adopt tier settled at the handshake from the knob and the data plane, by
# both roles (so SERVER-partition code in the link ratchet, not the client's tracker).
MobileGL/MG_Remote/Transport/AdoptTier.cpp
# P11 B2: T0 - the client's AHardwareBuffer store and its hop descriptor (both roles).
MobileGL/MG_Remote/Transport/AdoptT0.cpp
# ---- P5: the three new directories ------------------------------
# Wire/ the G3 codec: MGPWireRec_* in and out of SEG_CMD, blobs and
# var-tails in and out of SEG_STAGE. [w1]
@@ -630,6 +710,9 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
MobileGL/MG_Remote/FatalFunnel.cpp
MobileGL/MG_Remote/Wire/PipeWireCodec.cpp
MobileGL/MG_Remote/Client/ClientSession.cpp
# Whether the configured server is there at all, asked before a display is offered so the
# system's EGL/GLX/GBM loaders can move on to another implementation when it is not.
MobileGL/MG_Remote/Client/ServerProbe.cpp
MobileGL/MG_Remote/Client/EmitTables.cpp
MobileGL/MG_Remote/Client/CapsMirror.cpp
# P5 c1: the client role's BackendObject. pActiveBackendObject holds one of these
@@ -645,11 +728,24 @@ 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
# P14 S2: the per-session runtime and the thread routing that lets one process serve
# several sessions (the real session a singleton accessor answers with on that
# session's own threads).
MobileGL/MG_Remote/Server/SessionRuntime.cpp
MobileGL/MG_Remote/Server/ServerMain.cpp
# P11 PAIR: a socket client's control and aux connections paired by the nonce each
# presents first (PairBind), not by arrival order - both unix supervisors take pairs here.
MobileGL/MG_Remote/Server/PairAcceptor.cpp
# P11 B2: T0's Offers on the aux socket, matched to their map_persistent records by seq.
MobileGL/MG_Remote/Server/AdoptInbox.cpp
# Shared images: server-allocated AHardwareBuffers exported to clients as dma-buf descriptors
# (docs/archive/notes/anland/plan-ahb-dmabuf.md).
MobileGL/MG_Remote/Server/SharedImageRegistry.cpp
MobileGL/MG_Remote/Server/ServerSpawn.cpp
# P5f fc: the value frame that replaced the control mailbox's function-pointer payload,
# and its codec against protocol.fbs's SurfaceOp/SurfaceReply (the spawn arm's seam).
@@ -659,14 +755,30 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
# (the display Activity's SurfaceView on Android; nobody installs one on the host, so a
# ServerOwned request is refused by name there).
MobileGL/MG_Remote/Server/ServerDisplay.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
# Package-neutral C ABI for an APK that owns the server and its ANativeWindow.
MobileGL/MG_Remote/Server/EmbeddedServer.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
@@ -682,7 +794,7 @@ if (ANDROID)
MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp
)
if (MOBILEGL_BUILD_DISAGGREGATED)
# P12 (on-screen server window): the trace APK's MobileGLDisplayActivity - the in-process
# P12 (on-screen server window): the APK's MobileGLDisplayActivity - the in-process
# display server's thread, its SurfaceView's ANativeWindow in and out of ServerDisplay, and
# the geometry up-call. Split build only: nothing else has an in-process server to drive.
list(APPEND SOURCE_FILES
@@ -729,15 +841,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}")
@@ -764,9 +870,86 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
# is committed and regenerated by scripts/gen_protocol.py.
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/flatbuffers/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)
@@ -791,25 +974,11 @@ 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
libEGL.so libEGL.so.1)
libEGL.so libEGL.so.1 libGL.so libGL.so.1 libGLX_mobilegl.so.0)
add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E create_symlink
"$<TARGET_FILE_NAME:${CMAKE_PROJECT_NAME}>"
@@ -832,8 +1001,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")
@@ -850,29 +1019,81 @@ 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.
#
# MobileGL can serve as a glvnd EGL vendor - the library the system's libEGL.so.1 dispatches to -
# and a vendor must hand glvnd ITS OWN entry points: glvnd calls whatever address it is given, so
# an address that resolves to another libEGL in the process is a call back into the dispatcher.
# With default visibility every reference inside this library resolves against the whole process,
# and a process that loaded the system libEGL first wins. Marking the entry points protected says
# the same thing in the source, except the platform's own GL/EGL headers declare them default and
# the two visibilities do not mix; this flag is that promise made by the linker instead.
if (UNIX AND NOT APPLE)
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE "LINKER:-Bsymbolic-functions")
endif()
# ---- Shared images outside GL: the C ABI and the GBM backend (Linux client, split builds) ----
# docs/archive/notes/anland/plan-ahb-dmabuf.md. libMobileGL.so exports the shared-image C ABI
# (MG_Impl/SharedImageApi.h); mobilegl_gbm.so (MG_Gbm/) is a separate GBM loader module that reaches
# it through dlsym. Self-contained: nothing outside this block refers to either.
if (MOBILEGL_BUILD_DISAGGREGATED AND UNIX AND NOT APPLE AND NOT ANDROID)
target_sources(${CMAKE_PROJECT_NAME} PRIVATE MobileGL/MG_Impl/SharedImageApi.cpp)
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
@@ -913,7 +1134,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"
@@ -924,7 +1145,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"
@@ -939,13 +1159,22 @@ 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 ()
# Discover gtest cases when ctest starts, one binary after another, instead of in a post-build
# step. The post-build discovery of the CMake releases that never pass the target name down
# writes every binary's `--gtest_output=json` result to ONE per-directory file
# (cmake_test_discovery_<hash of an empty name>.json), so binaries linked in parallel in the same
# directory overwrite and delete each other's list: the registered count then differs between
# runs of the same tree and a binary ends up with none or only some of its cases. The pre-test
# mode reads the list from stdout, serially, and also works when the binary is cross-built.
if (NOT DEFINED CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE)
set(CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE PRE_TEST)
endif()
if (NOT ANDROID)
# Enable testing in the top-level scope so a CTestTestfile.cmake is emitted
# at the build-tree root. This lets `ctest` be invoked from the top-level
@@ -982,6 +1211,12 @@ if (ANDROID AND MOBILEGL_BUILD_INTEGRATION_TEST)
add_subdirectory(MobileGL/MG_IntegrationTest)
endif()
# The replay runner also builds for Android: an adb-shell executable that can
# dial a render server on the device (unix or TCP) without any Activity.
if (ANDROID AND MOBILEGL_BUILD_TRACE_REPLAY)
add_subdirectory(tools/trace_replay)
endif()
# ---------------------------------------------------------------------------
# P0 spike A: the Android delivery chain for a second native executable.
#
@@ -1001,7 +1236,7 @@ endif()
# P0 spike, which links no MobileGL library and exists only to prove the Android
# delivery chain - this one links the library and runs mobilegl_server_main.
#
# ARCHITECTURE.md:496 called for a ~30-line stub that dlopen's libMobileGL.so and
# ARCHITECTURE.md §15.2 called for a ~30-line stub that dlopen's libMobileGL.so and
# dlsym's the entry point, so one shared library serves both roles. On a desktop
# configure we link it directly instead: there is no APK packaging rule to satisfy
# and a direct link is what makes an undefined symbol a BUILD error rather than a
@@ -1090,3 +1325,48 @@ if (ANDROID AND MOBILEGL_BUILD_SERVER_SPIKE)
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
endif()
endif()
if (ANDROID AND MOBILEGL_BUILD_EXTMEM_PROBE)
# P11 A3 (open question 3): the spike-B external-memory probe, exec'd from the trace
# app's own process by the spike hook (`--es mobilegl_spike_spawn
# libMobileGLExtmemProbe.so`), so its rows answer for the untrusted_app domain. The
# same PIE-executable-named-like-a-library shape as MobileGLServerSpike above; it
# links nothing from MobileGL.
add_executable(MobileGLExtmemProbe
${CMAKE_CURRENT_SOURCE_DIR}/tools/spikes/extmem_probe/extmem_probe.cpp)
set_target_properties(MobileGLExtmemProbe PROPERTIES
PREFIX "lib"
SUFFIX ".so"
OUTPUT_NAME "MobileGLExtmemProbe"
POSITION_INDEPENDENT_CODE ON)
target_compile_options(MobileGLExtmemProbe PRIVATE -fPIE)
target_link_options(MobileGLExtmemProbe PRIVATE -pie)
target_link_libraries(MobileGLExtmemProbe PRIVATE vulkan EGL GLESv3 android log)
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
set_target_properties(MobileGLExtmemProbe PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
endif()
if (MOBILEGL_EXTMEM_PROBE_AS_SERVER)
# P11 B2 step 1, spike APKs only: MobileGLServerService execs `libMobileGLServer.so
# <endpoint> --serve` and B1's broker connects to whatever that listens on, so the probe
# takes the server's file name and answers "does a client's AHardwareBuffer cross this
# route into the server app" on the real route (tools/spikes/extmem_probe, route mode).
if (TARGET MobileGLServer)
set_target_properties(MobileGLServer PROPERTIES OUTPUT_NAME "MobileGLServerReal")
endif()
add_executable(MobileGLExtmemProbeAsServer
${CMAKE_CURRENT_SOURCE_DIR}/tools/spikes/extmem_probe/extmem_probe.cpp)
set_target_properties(MobileGLExtmemProbeAsServer PROPERTIES
PREFIX "lib"
SUFFIX ".so"
OUTPUT_NAME "MobileGLServer"
POSITION_INDEPENDENT_CODE ON)
target_compile_options(MobileGLExtmemProbeAsServer PRIVATE -fPIE)
target_link_options(MobileGLExtmemProbeAsServer PRIVATE -pie)
target_link_libraries(MobileGLExtmemProbeAsServer PRIVATE vulkan EGL GLESv3 android log)
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
set_target_properties(MobileGLExtmemProbeAsServer PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
endif()
endif()
endif()
+116
View File
@@ -0,0 +1,116 @@
# Contributing to MobileGL
These rules keep the code and its history readable. `scripts/check_text_hygiene.py` checks the ones listed under [Checks](#checks); the rest rely on review.
Commands below use `python3`. On Windows, run `python` instead.
## Commit messages
```
[Type] (Scope): content
- Detail 1
- Detail 2
```
- **Subject only by default.** Add `- ` detail lines only when a detail really needs to be in the commit message. Leave one blank line after the subject. A detail that wraps continues on at most two lines indented by two spaces. No paragraphs and no trailers.
- **Content** after `): ` is at most 72 characters of plain English in the imperative mood ("flush once per frame", not "flushes" or "flushed").
- **Type** is one or more of these, comma-separated (`[Fix, Test]`):
- `Feat`: new behaviour.
- `Fix`: corrects behaviour.
- `Perf`: faster or lighter, same behaviour.
- `Refactor`: restructures code, same behaviour.
- `Test`: tests only.
- `Docs`: documentation and comments only.
- `Build`: CMake, Gradle, toolchains.
- `CI`: workflows and CI scripts.
- `Chore`: housekeeping such as headers, file moves and dead files.
- `Tools`: developer tools under `tools/` and `scripts/`.
- **Scope** is each changed path without a leading `MobileGL/`, cut to its first two parts: `MG_Backend/DirectGLES`, `MG_Test/Wire`, `scripts/ci`, `docs/Disaggregated`.
- A shorter path is used whole: `MG_Backend` for a file directly in a subsystem, `scripts` for a file directly in `scripts/`, and the file name for a file at the repository root (`CMakeLists.txt`).
- A file directly in `MobileGL/` uses `MobileGL`, and a submodule uses its path (`3rdparty/glslang`).
- List every scope the commit touches, comma-separated: `(MG_Backend/DirectGLES, MG_Test/Wire)`.
- `python3 scripts/check_text_hygiene.py scope` prints the scope for the staged changes.
- **No** planning ids (phase or decision codes such as `P13` or `ID-54`), local paths, external project names or non-English text anywhere in the message.
- **Merges** use `[Merge] (area): what the merge takes`. The scope is free text there. Write the subject yourself, since git's default merge message fails the check. The commits a merge brings in are not checked again.
- **Reverts** use `[Fix] (Scope): revert <the reverted subject>`.
Example:
```
[Perf] (MG_Backend/DirectGLES): flush once per frame before the window pass
```
## Comments, strings and documentation
- Code, comments, log strings and commit messages are in English. Documentation may be in Chinese or English.
- A comment states what the code does now and why, in a few lines. Rationale longer than about 15 lines belongs in a design document under `docs/`, with a one-line pointer in the code.
- In code and comments, cite a symbol, file or section, never a line number: `ARCHITECTURE.md §1.3`, `VulkanRenderer::BeginFrame()`. Documents may keep `file:line` citations; the documentation citation check resolves them.
- State the rule, not its history. No "previously" or "after review", and no planning ids in comments, strings, identifiers or file names.
- Plain wording, without ALL-CAPS emphasis.
- In code and comments (C, C++, GLSL, CMake, shell, Python, YAML), do not name external projects such as Mesa, Zink or gallium; describe the technique instead. Naming the driver or hardware where a behaviour was observed (llvmpipe, ANGLE, Adreno, Mali) is fine, and so are file, package and extension names that contain these words (`50_mesa.json`, `GL_MESA_*`).
## Code
- C and C++ sources are formatted with `.clang-format`, using clang-format 23. Install it with `pip install 'clang-format==23.1.3'`. `git clang-format <base>` from the same package formats the lines changed since `<base>`, and `scripts/format_code.sh` formats every file the check covers. The tree is already formatted, so neither touches lines you did not change.
- The commit that reformatted the whole tree is listed in `.git-blame-ignore-revs`. Run `git config blame.ignoreRevsFile .git-blame-ignore-revs` once so that `git blame` looks past it.
- `.inc` files are fragments included in the middle of a `.cpp` file, inside a namespace, or are generated tables. clang-format cannot format them on their own, so the check skips them. Match the indentation of the file that includes them. The check also skips generated files and the GL export table, `MG_Impl/GLImpl/Exporting/Definitions.cpp`.
- Text files are UTF-8.
- These source files under `MobileGL/`, `scripts/` and `tools/` start with the license header:
- C, C++, `.inc`, GLSL, Java and JavaScript modules, with `//` comments;
- Python, shell, CMake modules and `CMakeLists.txt`, with `#` comments.
- Generated files and files under another license are exempt. `python3 scripts/update-source-file-header.py` adds the header.
- Keep files to about 3,000 lines and functions to about 300 lines.
- Modules are layered. A file includes files of its own layer and of the layers below it, never above:
1. the root headers (`Includes.h`, `Config.h`), `MG_Util` and `MG_State`;
2. `MG_Pipe`;
3. `MG_Backend`, `MG_Remote` and `MG_Impl`;
4. the root `.cpp` files and `MG_Gbm`;
5. `MG_Test`, `MG_IntegrationTest` and `MG_Benchmark`.
- Each backend is isolated. Files under `MG_Backend/DirectGLES/` or `MG_Backend/DirectVulkan/` are included only by the same backend, by the registry files directly in `MG_Backend/`, by the root `.cpp` files and by tests.
- Platform-specific APIs stay behind an abstraction, never raw in logic code.
- A fix that restores intended behaviour ships unconditionally. Add a switch only for a real trade-off.
- A cache keyed on object identity uses a monotonic lifetime id, never a GL name or a pointer, because both are recycled.
## What goes in the repository
- Source, tests, build files and maintained documentation.
- Generated code, only together with a CI step that regenerates it and fails on any difference.
- Binary test fixtures.
- Not raw logs, and not working papers such as agent briefs, review rounds and drafts. Keep those outside the repository and commit the conclusion: a design-document change, a `DEBTS.md` entry, a measurement under `docs/Performance`.
- `docs/archive/` keeps the planning notes and agent contracts of the disaggregation work as they were. It is frozen: no file there is added, edited or removed.
## Checks
`python3 scripts/check_text_hygiene.py ci` runs what CI runs. It checks the commits on the branch's own line of history since the check was added. The base branch it compares with is `feat/disaggregated`, which you can change with `TEXT_HYGIENE_BASE_BRANCH`.
These fail on any violation:
- commit messages, as described above;
- a new file under `docs/Disaggregated/notes/` or a new `.log` file (moving a file is not adding it);
- any change under `docs/archive/`;
- a missing or wrong license header;
- a text file that is not UTF-8;
- an external project name in a comment;
- a `file:line` citation in code that points past the end of the file;
- changed lines that clang-format would change (a line moved unchanged, the same commit removing its exact text elsewhere, is not a changed line).
These are ratchets: each total may only go down.
- planning ids in comments;
- comment lines beyond 15 in a block;
- ALL-CAPS comment lines;
- non-English comment lines;
- line-number citations in code.
`python3 scripts/check_text_hygiene.py tree --update-baseline` lowers `scripts/data/text_hygiene_baseline.json` after a cleanup. It never raises it, and CI rejects a raised baseline.
`python3 scripts/check_structure.py ci` checks three more ratchets over the C and C++ sources under `MobileGL/`, against `scripts/data/structure_baseline.json`:
- includes that break the layering or the backend isolation above;
- lines beyond 3,000 in a file;
- lines beyond 300 in a function body.
`python3 scripts/check_structure.py report` lists what each total counts, and `tree --update-baseline` lowers the baseline the same way.
Not checked by CI: the platform, switch and lifetime-id rules, planning ids outside comments, English outside comments, and the regeneration step for generated code.
+422
View File
@@ -0,0 +1,422 @@
// MobileGL - MobileGL/BackendCaps.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include <MG_State/GLState/TextureState/TextureEnum.h>
// The vocabulary every layer uses to talk about a backend's capabilities: which backend it is,
// what each texture format supports, and the probed limits and features. It holds values only;
// the backend itself is MG_Backend/BackendObject.h.
namespace MobileGL {
enum class BackendType {
DirectGLES,
DirectVulkan,
BackendTypeCount,
Unknown = -1
};
namespace MG_Backend {
enum class FormatCapability : Uint64 {
Creatable = 1ull << 0,
Sampled = 1ull << 1,
LinearFilter = 1ull << 2,
GenerateMipmap = 1ull << 3,
TextureGather = 1ull << 4,
TextureShadow = 1ull << 5,
FramebufferRenderable = 1ull << 6,
FramebufferLayered = 1ull << 7,
MultisampleTexture = 1ull << 8,
MultisampleRenderbuffer = 1ull << 9,
ColorAttachment = 1ull << 10,
DepthAttachment = 1ull << 11,
StencilAttachment = 1ull << 12,
TextureBuffer = 1ull << 13
};
using FormatCapabilityFlags = Flags<FormatCapability>;
inline constexpr Array<FormatCapability, 14> kReportedFormatCapabilities = {
FormatCapability::Creatable,
FormatCapability::Sampled,
FormatCapability::LinearFilter,
FormatCapability::GenerateMipmap,
FormatCapability::TextureGather,
FormatCapability::TextureShadow,
FormatCapability::FramebufferRenderable,
FormatCapability::FramebufferLayered,
FormatCapability::MultisampleTexture,
FormatCapability::MultisampleRenderbuffer,
FormatCapability::ColorAttachment,
FormatCapability::DepthAttachment,
FormatCapability::StencilAttachment,
FormatCapability::TextureBuffer,
};
inline constexpr SizeT kFormatCapabilityTextureTargetCount =
static_cast<SizeT>(TextureTarget::TextureTargetCount);
inline constexpr SizeT kFormatCapabilityRenderbufferTargetIndex = kFormatCapabilityTextureTargetCount;
inline constexpr SizeT kFormatCapabilityTargetCount = kFormatCapabilityTextureTargetCount + 1;
inline constexpr SizeT kFormatCapabilityFormatCount =
static_cast<SizeT>(TextureInternalFormat::TextureInternalFormatCount);
using FormatCapabilityTable =
Array<Array<FormatCapabilityFlags, kFormatCapabilityFormatCount>, kFormatCapabilityTargetCount>;
using FormatSampleCountTable =
Array<Array<Vector<Int>, kFormatCapabilityFormatCount>, kFormatCapabilityTargetCount>;
struct FormatCapabilityCache {
FormatCapabilityTable FullCaps{};
FormatCapabilityTable CaveatCaps{};
FormatSampleCountTable SampleCounts{};
void Clear();
};
Bool HasFormatCapability(FormatCapabilityFlags caps, FormatCapability capability);
SizeT GetFormatCapabilityTargetIndex(TextureTarget target);
SizeT GetRenderbufferFormatCapabilityTargetIndex();
const char* GetFormatCapabilityName(FormatCapability capability);
String GetFormatCapabilityTargetName(SizeT targetIndex);
void PrintFormatCapabilities(const FormatCapabilityCache& cache);
// 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.
enum class GpuVendorKind : Uint8 {
Unknown = 0,
Qualcomm,
Arm,
Nvidia,
Amd,
Intel,
ImgTec,
// Software rasterizers (llvmpipe/lavapipe, SwiftShader).
Software,
};
struct DynamicBackendParameters {
Uint64 UniformBufferOffsetAlignment = 256;
// GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, which is a SEPARATE limit from the
// uniform one and is routinely larger: Adreno 830 reports 32 for uniform buffers and
// 64 for storage buffers. Answering the storage query with the uniform value let an
// application bind a storage range at an offset the driver cannot address, which it
// accepted without error and then wrote somewhere else entirely.
Uint64 ShaderStorageBufferOffsetAlignment = 256;
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT. 1.0 means the backend cannot filter anisotropically,
// which is also why the extension is not advertised in that case.
Float MaxTextureMaxAnisotropy = 1.0f;
Float AliasedLineWidthRangeMin = 1.0f;
Float AliasedLineWidthRangeMax = 1.0f;
Float SmoothLineWidthRangeMin = 1.0f;
Float SmoothLineWidthRangeMax = 1.0f;
Float SmoothLineWidthGranularity = 1.0f;
Float PointSizeRangeMin = 1.0f;
Float PointSizeRangeMax = 1.0f;
Float PointSizeGranularity = 1.0f;
Int Max3DTextureSize = 16384;
Int MaxArrayTextureLayers = 2048;
Int MaxCubeMapTextureSize = 16384;
Int MaxFramebufferWidth = 16384;
Int MaxFramebufferHeight = 16384;
Int MaxFramebufferLayers = 2048;
Int MaxRenderbufferSize = 16384;
Int MaxTextureSize = 16384;
Int MaxColorTextureSamples = 1;
Int MaxDepthTextureSamples = 1;
Int MaxFramebufferSamples = 1;
Int MaxIntegerSamples = 1;
Int MaxSamples = 1;
Int MaxSampleMaskWords = 1;
// Tessellation limits; defaults are the GL 4.0 core minimums.
Int MaxPatchVertices = 32;
Int MaxTessGenLevel = 64;
// GL_MIN/MAX_PROGRAM_TEXTURE_GATHER_OFFSET. Defaults are the GL 4.0 core
// minimums, which every ES 3.1 driver also guarantees.
Int MinProgramTextureGatherOffset = -8;
Int MaxProgramTextureGatherOffset = 7;
Int MaxTextureImageUnits = 32;
Int MaxVertexTextureImageUnits = 32;
Int MaxComputeTextureImageUnits = 32;
Int MaxCombinedTextureImageUnits = 192;
Int MaxVertexAttribs = 16;
Int MaxComputeShaderStorageBlocks = 8;
Int MaxCombinedShaderStorageBlocks = 32;
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. Zero is a legal answer for the four
// non-compute, non-fragment stages and these defaults are the spec minimums, not
// placeholders: GL 4.6 table 23.64 and ES 3.2 table 21.44 both set the minimum for
// vertex, tessellation control, tessellation evaluation and geometry at 0, and only
// fragment (8 in GL, 4 in ES) and compute are guaranteed to have any. Every real ARM
// GLES driver takes that allowance - a Mali-G925 reports 0 for all four - so a
// backend that cannot honour a graphics-stage storage block MUST report 0 here
// rather than a hopeful number. Advertising a non-zero count the driver will refuse
// does not make the block work; it only moves the failure from an honest
// "unsupported" at query time to a backend link error the frontend never surfaces,
// after which every draw with that program silently renders nothing.
Int MaxVertexShaderStorageBlocks = 0;
Int MaxTessControlShaderStorageBlocks = 0;
Int MaxTessEvaluationShaderStorageBlocks = 0;
Int MaxGeometryShaderStorageBlocks = 0;
Int MaxFragmentShaderStorageBlocks = 8;
Int MaxComputeUniformBlocks = 12;
Int MaxComputeWorkGroupInvocations = 128;
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / GL_MAX_COMPUTE_WORK_GROUP_SIZE, one value per
// axis. These six, with the invocations limit above, are the only indexed limits a
// backend genuinely OWNS - the device answers them (glGetIntegeri_v on DirectGLES,
// VkPhysicalDeviceLimits::maxComputeWorkGroupCount/Size on DirectVulkan) - and so
// the only ones that survive the retirement of the GetIntegeri_v table entry: they
// cross the MGPipe boundary inside MGPCaps, by inclusion of this struct (plan B
// section 4.4.1). Every other indexed pname names frontend state. RAW driver
// answers, like the invocations limit: GL_Getter and the compile environment floor
// them at the shared MIN_COMPUTE_WORK_GROUP_* minimums themselves. The defaults are
// the GL 4.3 core minimums (table 23.60) and describe the no-backend case, as
// MaxClipDistances' does.
Int MaxComputeWorkGroupCount[3] = {65535, 65535, 65535};
Int MaxComputeWorkGroupSize[3] = {1024, 1024, 64};
Int MaxShaderStorageBufferBindings = 8;
Int MaxTextureBufferSize = 65536;
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
Int TextureBufferOffsetAlignment = 1;
Int MaxUniformBufferBindings = 24;
Int MaxUniformBlockSize = 16384;
Int MaxImageUnits = 8;
Int MaxCombinedImageUniforms = 8;
Int MaxVertexImageUniforms = 0;
Int MaxTessControlImageUniforms = 0;
Int MaxTessEvaluationImageUniforms = 0;
Int MaxGeometryImageUniforms = 0;
Int MaxFragmentImageUniforms = 8;
Int MaxComputeImageUniforms = 8;
Int MaxDrawBuffers = 8;
Int MaxColorAttachments = 8;
// GL_MAX_CLIP_DISTANCES. Zero is a legal answer here, not a placeholder, and a
// backend that cannot host a clip distance MUST report it: advertising eight the
// backend will refuse does not make gl_ClipDistance work, it only moves the failure
// from an honest "unsupported" at query time to a backend shader-compile error the
// frontend never surfaces, after which every draw with that program silently renders
// nothing. DirectGLES fills it from GL_EXT_clip_cull_distance, DirectVulkan from the
// shaderClipDistance device feature. The DEFAULT stays at the GL 4.3 core minimum
// because it describes the no-backend case (standalone shader compiles, unit tests),
// where there is no device to be honest about and BuildTBuiltInResource still has to
// hand glslang a workable gl_MaxClipDistances.
Int MaxClipDistances = 8;
// GL_MAX_CULL_DISTANCES and GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, under exactly
// the contract stated for MaxClipDistances above: ZERO IS A LEGAL ANSWER and a
// backend that cannot host a cull distance MUST report it. The failure this prevents
// is worse than the clip one, because cull distance discards the whole primitive:
// glslang bounds gl_CullDistance[i] against maxCullDistances and expands
// gl_MaxCullDistances from it, SPIRV-Cross then emits
// `#extension GL_EXT_clip_cull_distance : require` into the ESSL, and a host driver
// without that extension rejects the program in an info log nobody surfaces. These
// used to be bare 8s inside BuildTBuiltInResource with no backend consulted at all.
// The DEFAULTS are the GL 4.5 core minimums for the same reason MaxClipDistances'
// is: they describe the no-backend case (standalone compiles, unit tests).
Int MaxCullDistances = 8;
Int MaxCombinedClipAndCullDistances = 8;
Int MaxViewports = 16;
// GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX: which vertex of a
// primitive supplies gl_Layer and gl_ViewportIndex. GL 4.6 table 23.65 makes
// GL_UNDEFINED_VERTEX a legal answer for both, and it is the honest default - naming
// a convention is a statement about behaviour, so a backend that does not pin one
// must not claim it does. DirectGLES fills the layer one from the ES 3.2 query and
// the viewport one from GL_OES_viewport_array, and leaves UNDEFINED where the
// capability is absent: without the viewport array extension only viewport 0 is ever
// rasterized, so no convention selects anything. DirectVulkan keeps UNDEFINED for
// both - which vertex provokes is decided per pipeline by
// VulkanRenderer::SelectProvokingVertexMode out of VK_EXT_provoking_vertex,
// provokingVertexModePerPipeline and the topology, so no single convention is true
// of the backend.
Uint32 LayerProvokingVertex = GL_UNDEFINED_VERTEX;
Uint32 ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
Int MaxViewportWidth = 16384;
Int MaxViewportHeight = 16384;
Float ViewportBoundsRangeMin = 0.0f;
Float ViewportBoundsRangeMax = 0.0f;
Int ViewportSubpixelBits = 0;
// GL 4.x fragment-interpolation offset limits. These defaults are the
// core minimums and are replaced by live GLES/Vulkan device limits.
Float MinFragmentInterpolationOffset = -0.5f;
// For four fractional bits the greatest required legal offset is
// 0.5 - 2^-4 = 0.4375 (GL 4.6 table 23.70).
Float MaxFragmentInterpolationOffset = 0.4375f;
Int FragmentInterpolationOffsetBits = 4;
Bool SupportsWideLines = false;
// Whether a framebuffer whose depth and stencil attachments are distinct
// images can be rendered to. GL only requires support when both refer to the
// same image and lets an implementation answer GL_FRAMEBUFFER_UNSUPPORTED
// otherwise, which is what DirectVulkan (one combined attachment) and the
// real ES drivers behind DirectGLES both do. Defaults to true so a backend
// that never sets it keeps the permissive behaviour.
Bool SupportsDistinctDepthStencilAttachments = true;
// Whether attaching a single layer of a 3D or array texture to a framebuffer actually
// renders to that layer. DirectGLES hands the layer straight to
// glFramebufferTextureLayer, so it does; DirectVulkan maps a GL layer onto a Vulkan
// array layer with no notion of a 3D depth slice, so it does not yet. Defaults to false
// so a backend that never sets it gets the conservative answer.
// Which layered texture targets this backend can attach ONE layer of to a framebuffer
// and then really clear, render and read back that layer. Bit (1u << TextureTarget) is
// set for each supported target. Deliberately per target rather than one flag: the three
// ways a GL layer maps onto Vulkan are independent capabilities. A 2D or 2D multisample
// array layer IS a VkImage array layer and needs nothing extra; a 3D texture's layer is
// a z slice, which needs a 2D-array-compatible image and a per-slice clear that
// vkCmdClearColorImage cannot express; a cube map array needs an image shape and the
// imageCubeArray feature before it can be attached at any layer at all. Defaults to 0 so
// a backend that never sets it gets the conservative answer.
Uint32 PerLayerFramebufferAttachmentTargets = 0;
static constexpr Uint32 PerLayerFramebufferAttachmentBit(TextureTarget target) {
return (static_cast<Int>(target) >= 0 &&
static_cast<Int>(target) < static_cast<Int>(TextureTarget::TextureTargetCount))
? (1u << static_cast<Uint32>(target))
: 0u;
}
Bool SupportsPerLayerFramebufferAttachment(TextureTarget target) const {
const Uint32 bit = PerLayerFramebufferAttachmentBit(target);
return bit != 0 && (PerLayerFramebufferAttachmentTargets & bit) != 0;
}
// Whether this backend can CONSUME a shader module that still declares 64-bit floats,
// i.e. whether `double` survives the transpile instead of being narrowed to `float`
// (ShaderTranspiler::DemoteFloat64Pass). Detected, never assumed:
// * DirectVulkan sets it from VkPhysicalDeviceFeatures::shaderFloat64, the feature
// VUID-VkShaderModuleCreateInfo-pCode-08740 requires before a module declaring
// OpCapability Float64 may be created at all. lavapipe has it; Adreno and Mali
// both report VK_FALSE, so no real mobile device does.
// * DirectGLES can NEVER have it. GLSL ES has no 64-bit float type in any version
// or extension, so SPIRV-Cross cannot emit one ("FP64 not supported in ES
// profile") and the demotion there is mathematically mandatory, always.
// Defaults to false so a backend that never sets it - and the no-backend case, which
// is what standalone shader compiles and the unit tests run under - keeps the
// demotion, which is the behaviour that works everywhere.
Bool SupportsShaderFloat64 = false;
// Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e.
// whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected,
// never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (the
// attribute travels as its 32-bit word pair, so no VK_FORMAT_R64* is required, but the
// bitcast result is Float64); DirectGLES can never have it, ESSL having no fp64 type at
// all. Defaults to false so a backend that never sets it gets the conservative answer.
//
// INDEPENDENT of SupportsShaderFloat64, and it has to be: this flag decides a VkFormat
// from the VAO ATTRIBUTE alone, which does not know what type the shader declared, and
// glVertexAttribFormat(GL_DOUBLE) feeding a plain `in vec4` is both legal and common
// (KHR-GL43.vertex_attrib_binding.basic-input-case4/5, advanced-bindingUpdate). A
// backend with native fp64 that still cannot FETCH 64 bits keeps this false and relies
// on the per-MODULE rule in ShaderCompiler::SanitizeAndOptimizeBinary instead: a vertex
// module that declares a 64-bit float INPUT is demoted whole, so the two shader-side
// halves (PackDoubleVertexInputsPass and VertexInputStateFactory::ToVkVertexFormat)
// still see one consistent world.
Bool SupportsFloat64VertexAttributes = false;
// Whether a TESSELLATION stage of this backend may access gl_PointSize - i.e.
// whether a module declaring OpCapability TessellationPointSize can reach the
// driver at all. DirectVulkan sets both this and the geometry twin from the one
// shaderTessellationAndGeometryPointSize feature; DirectGLES sets them
// independently from the EXT/OES_tessellation_point_size /
// geometry_point_size extension pairs (PointSizeTier), which really do come
// separately. When absent, ProgramSpirvTask demotes the built-in to an ordinary
// varying program-wide (ShaderCompiler::
// DemoteTessellationGeometryPointSizeForProgram); MOBILEGL_POINT_SIZE_DEMOTION
// overrides the detection in either direction at backend init.
//
// Defaults TRUE, deliberately against the house "assume absent" rule: false
// ARMS a rewrite, so the conservative no-backend answer (standalone compiles,
// unit tests) is the one that leaves modules untouched. A backend that never
// sets it gets standard modules and, at worst, the old honest declines.
Bool SupportsTessellationPointSize = true;
// The geometry-stage twin (OpCapability GeometryPointSize).
Bool SupportsGeometryPointSize = true;
Uint64 MaxShaderStorageBlockSize = 128 * 1024 * 1024;
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;
}
};
} // namespace MG_Backend
} // namespace MobileGL
+270 -158
View File
@@ -8,13 +8,13 @@
#pragma once
#include <Includes.h>
#include <MG_Backend/BackendObjects.h>
#include <BackendCaps.h>
namespace MobileGL::MG_Config {
inline const String ProjectName = "MobileGL";
inline const String CoreName = "MobileGL Core";
inline const String CoreVendor = "MobileGL-Dev (BZLZHH, Swung0x48, Tungsten)";
inline const Version CoreVersion = {26, 9, 0, "-dev", VersionType::Development};
inline const Version CoreVersion = {26, 10, 0, "-dev", VersionType::Development};
inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true};
inline const Uint64 CacheVersion = 0;
@@ -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
@@ -130,8 +146,39 @@ namespace MobileGL::MG_Config {
Bool AdvertiseFp64 = false;
// MOBILEGL_MAGMA_R11G11B10F_FALLBACK: use fallback format for R11G11B10F on Vulkan.
Bool MagmaR11G11B10FFallback = false;
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
Uint32 MagmaFramesInFlight = 3;
// MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV: hand a shader module that fails SPIR-V validation to
// the driver anyway. Off by default: Magma declines such a program (no shader module, its
// 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_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
// freed; Adreno maps it into the process in 16 KiB chunks, so one ~1M-draw loading frame
// exhausts vm.max_map_count. See VulkanRenderer::SplitOversizedRecording.
Uint32 MagmaMaxDrawsPerCommandBuffer = 16384;
// MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES: how many consecutive frames (strictly, BeginFrame
// epochs) a frame slot's descriptor pools must stay under a quarter full before Magma
// hands the slot's whole set cache back and lets it re-grow. 0 = never trim. A loading
// frame otherwise keeps its burst of sets (rd12: ~15k, where steady frames use a few dozen)
// for the life of the process. See UniformManager::TrimFrameDescriptorPools.
Uint32 MagmaDescriptorTrimFrames = 120;
// MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
// resolves certain rendering bugs on ANGLE + llvmpipe.
Bool EsprytAvoidSamplerMipmapMinFilter = false;
@@ -152,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
@@ -202,13 +274,16 @@ namespace MobileGL::MG_Config {
// persistently+coherently mapped storage at definition time (negative control /
// escape hatch). Frontend-scoped: it engages only where the active backend
// provides AcquirePersistentMap. With adoption on, an app SubData into a busy
// 128MB arena is a plain memcpy into GPU-visible memory; every driver-mediated
// route for the same write stalls the thread or ghost-copies the whole arena on
// this class of Mali driver, and the arena stops costing its size again in RAM.
// 128MB arena lands GPU-ordered through the backend's resident-subdata op
// (BufferObject::LandBytesIntoResidentStore, since 0ee3384b: an in-place write
// tore the frames still reading the old bytes) - still no driver-mediated write
// into the busy mutable store, which on this class of Mali driver stalls the
// thread or ghost-copies the whole arena, and the arena stops costing its size
// again in RAM.
Bool DisableLargeBufferAdoption = false;
// MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
// depth/stencil reads and always go through the shader-sampling emulation. Core GL
// ES has no depth or stencil readback, but some drivers accept it anyway (Mesa does,
// ES has no depth or stencil readback, but some drivers accept it anyway (llvmpipe does,
// Adreno does not), which means the emulation is dead code on exactly the stack the
// headless suite runs on. This forces it live so the scenarios and the CTS can
// exercise the path, and gives the device an A/B lever over the same choice.
@@ -303,7 +378,7 @@ namespace MobileGL::MG_Config {
// MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE: DirectVulkan's GL_PRIMITIVES_GENERATED
// reroute for draws made while transform feedback is INACTIVE. The stream query
// (VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT primitivesNeeded) is defined to count
// them, but a Mali driver - and Mesa lavapipe - answers 0 unless a capture span is
// them, but a Mali driver - and lavapipe - answers 0 unless a capture span is
// open, which is exactly the shape the CTS uses to measure the tessellator, so ~29
// tessellation tests per tree size a capture buffer from the 0 and die on the
// zero-length map. Auto defers to a device probe at renderer bring-up
@@ -316,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.
@@ -350,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
@@ -357,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
@@ -376,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
@@ -408,8 +486,9 @@ namespace MobileGL::MG_Config {
Uint32 PipeIndexMirrorMb = 64;
// MOBILEGL_PIPE_STATS_PERIOD: frames per boundary-counter summary line. 120 is the
// steady-state cadence; the device retrace harness never reaches the teardown dump
// and a trimmed fixture (create-indirect) is shorter than 120 frames, so a run that
// needs its numbers at all sets this low enough to land at least one window.
// and a trimmed fixture (minecraft-1.21.1-neoforge-create-indirect-in-world-align1024)
// is shorter than 120 frames, so a run that needs its numbers at all sets this low
// enough to land at least one window.
Uint32 PipeStatsPeriod = 120;
// MOBILEGL_PIPE_STATS_FILE: where the boundary counters' teardown JSON dump goes.
// Empty (the default) means no dump; the per-120-frame summary line still goes to
@@ -424,7 +503,7 @@ namespace MobileGL::MG_Config {
//
// MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>.
//
// WHY `Transport` IS NOT A FeaturesTable MEMBER. ARCHITECTURE.md:580 requires that with
// WHY `Transport` IS NOT A FeaturesTable MEMBER. ARCHITECTURE.md §16 requires that with
// MOBILEGL_BUILD_DISAGGREGATED=OFF it be a `constexpr Monolith`, so that the single hook
// in MG_Backend/Init.cpp compiles away entirely rather than becoming a branch nobody can
// take. A FeaturesTable member is a runtime field in every build, which is the opposite
@@ -441,13 +520,49 @@ namespace MobileGL::MG_Config {
// MG_Remote` must be non-empty in build-split (CONTRACT-P5.md table 3, and the CI job
// P5 adds beside build-linux-verify).
enum class TransportMode : Uint8 {
Monolith = 0, // today's in-library backend; no MG_Remote object is constructed
InProcess = 1, // P5: a real apply thread in this process, over the same G3 codec
Spawn = 2, // P6: fork/exec MobileGLServer, socketpair
UnixSocket = 3,// P6: connect to an existing AF_UNIX endpoint (Endpoint = <path>)
NamedPipe = 4, // P6: Windows named pipe (Endpoint = <name>)
Monolith = 0, // today's in-library backend; no MG_Remote object is constructed
InProcess = 1, // P5: a real apply thread in this process, over the same G3 codec
Spawn = 2, // P6: fork/exec MobileGLServer, socketpair
UnixSocket = 3, // P6: connect to an existing AF_UNIX endpoint (Endpoint = <path>)
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
@@ -475,7 +590,7 @@ namespace MobileGL::MG_Config {
// transport: a build that cannot reach the MG_Remote code cannot honour one of them.
//
// P5 lands exactly the knobs P5's own packages read. A later phase's knob is added HERE,
// through the integrator, and not invented at its call site - ARCHITECTURE.md:615 holds
// through the integrator, and not invented at its call site - ARCHITECTURE.md appendix A holds
// the full planned inventory (PRESENT_CREDIT, POLL_ESCALATE, SHADOW_SHM,
// INLINE_PAYLOADS, TRACE, ATTACH, RESPAWN, IDLE_EXIT_S), and every one of those belongs
// to P6 or later.
@@ -485,6 +600,9 @@ namespace MobileGL::MG_Config {
Offscreen = 0, // a window surface is the client's own window, as before (the default)
Server = 1, // a window surface is the SERVER's window: WindowKind::ServerOwned
};
// IpcTable::AdoptTier when MOBILEGL_IPC_ADOPT_TIER is unset (or unparseable): T0 by default,
// with QUIET fallbacks. Not a tier a user can type - the knob admits 0..2.
inline constexpr Uint32 kAdoptTierUnset = 3;
struct IpcTable {
// MOBILEGL_IPC_SERVER_PATH: where to find libMobileGLServer. P6 consumes it; P5
// lands the parse because t1's ctest ENVIRONMENT blocks and add_trace_replay_test's
@@ -503,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
@@ -534,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
@@ -549,64 +645,59 @@ namespace MobileGL::MG_Config {
// restores the per-record barrier of R-1.
Uint32 BatchWaits = 1;
// MOBILEGL_IPC_CREATE_WINDOW: how many RESOURCE_CREATE answers may be outstanding before the
// client waits for one. 1 = every create waits (the shape R-5 shipped with); N > 1 = up to N
// creates go out fire-and-forget, their caller reads a PROVISIONAL accept, and the oldest
// answer is taken when the window fills.
//
// DEFAULT 2, AND IT TOOK THREE THINGS TO GET HERE. The deferral was inert for two
// measured reasons and one structural one, in the order they were found:
// 1. the reply buffer was a ring of the last 8 answers, and between two creates this
// client emits on the order of a thousand records - so the answer was always gone
// (`want=310` on every miss while the ring spanned [1175..1504]);
// 2. a residency budget that tried to take answers back early could not help, because
// what displaces an answer is what the SERVER sends, not what the client emits;
// 3. and so the fix was RETENTION - a link keeps a declared answer until its reader
// takes it (Transport/ILink.h's RetainsReplies, StreamLink's declared-answer store).
//
// MEASURED ON THE DEVICE, load frame, both ends rebuilt (create 4,536, records 103,097,
// frame-sent identical per op throughout):
// window 1 4,536 blocking takes 28.66 s (the shape R-5 shipped)
// window 2 2,500 17.27 s
// window 4 1,644 11.02 s
// and the round trip does NOT grow as the window deepens (5,285 / 4,082 us against 5,672
// at 1), which is the measurement that says the ~5 ms is a WAKEUP the server batches away
// rather than its apply throughput - so amortising four creates per wakeup is a real 2.6x.
//
// 6 IS THE DEFAULT, AND IT IS WHERE THE CURVE FLATTENS - the sweep that says so is the
// one the paragraph above asked for, and it moved the ceiling too (kCreateWindowMax is 8
// now; the old 4 was derived from a reply pool the window does not run on). Three
// interleaved runs per arm on one binary, load frame, create 4,536 sent in every arm:
//
// window 4 1,503 blocking takes 13.23 s median (spread 11.37-16.29)
// window 5 1,277 11.31 s (10.61-12.12)
// window 6 1,155 10.79 s ( 9.69-10.81)
// window 8 859 10.96 s ( 8.92-11.03)
//
// 4->5 buys 1.92 s, 5->6 buys 0.53 s, 6->8 buys NOTHING (-0.18 s, inside a session noise
// of ~1.5-2 s) - so 6 is the smallest window on the flat part and the extra answers a
// deeper one holds buy no time. AND PAST THIS POINT THE FRAME IS NOT WAIT-BOUND ANY MORE:
// window 8 pays 296 fewer blocking takes than window 6 and the wall clock does not move.
// The store is not the constraint either - its peak is logged as it crosses (StreamLink's
// P12 line) and reads 6..11 against a cap of 24 at every arm from 5 up. What is left is
// the constant ~350-400 creates that block for a reason depth does not reach, and the
// 101 MB / 103k records the SERVER has to apply. NOTE THE PREDICATE the whole mechanism rests
// on: an answer is retained because the wire DECLARED it will be read - NOT because its
// caller was fire-and-forget. The window's own creates are fire-and-forget AND read, so a
// flag keyed on wantReply would disable the very mechanism it exists to enable
// (docs/Disaggregated/notes/p12/CREATE-WINDOW-MEASURED.md sections 7-11).
// client waits for one. 1 turns the window off; N > 1 lets up to N creates go out without
// waiting: each caller reads a provisional accept, the answers are collected in order (the
// oldest once the window is full), and a create that comes back declined is un-published.
// The window runs only on a link that keeps a declared answer until its reader takes it (the
// stream link); on the shared-memory link it is always 1.
// Default 10: on a device world-load frame the wall clock stops improving at about 6, and a
// deeper window costs only entries in the link's declared-answer store, so 10 sits on the
// flat part of the curve.
// Range 1..12; a value outside it is ignored with a warning. 12 is kCreateWindowMax
// (WireTables.cpp), half of the stream link's 24-entry answer store.
// Measurements: docs/Performance/ipc-create-window.md.
Uint32 CreateWindow = 10;
// MOBILEGL_IPC_ADOPT_TIER: 2 = emulate (client keeps the shadow and pushes), which
// is the only tier P5 implements and the reason persistent-map-push can be non-zero
// at all (R-6). 0 and 1 parse and are Fatal at use with "P11"; they exist now so the
// negative control has a spelling the day P11 writes it.
Uint32 AdoptTier = 2;
// MOBILEGL_IPC_PBO_READBACK_SYNC (P9 W1, docs/archive/contracts/CONTRACT-P9.md §1): 1 = a glReadPixels
// or glGetTexImage into a bound GL_PIXEL_PACK_BUFFER takes the reply-slot form again -
// the client waits for the pixels and uploads them into the buffer itself, as before P9.
// 0 (default) sends read_pixels_to_buffer / get_texture_image_to_buffer, waits for
// nothing, and marks the buffer GPU-written. The 1 arm is the A/B control and the
// red-once of the wait-count gate (PackBufferReadbackScenario): it must show a reply
// wait per read.
Uint32 PboReadbackSync = 0;
// MOBILEGL_IPC_POLL_ESCALATE (P10, docs/archive/contracts/CONTRACT-P10.md §1): a glClientWaitSync with a
// zero timeout or a glGetSynciv(GL_SYNC_STATUS) is answered by the client from what the
// server reported (kEventFenceSignaled) - "signaled" when it has been, "not yet" otherwise
// - and only the Nth "not yet" in a row on one fence crosses as a real round trip, so a
// poll loop cannot outlive a late report. 0 = every poll crosses, as before P10: the A/B
// arm and the red-once of the no-reply gate (FencePollScenario).
Uint32 PollEscalate = 64;
// MOBILEGL_IPC_RECOVER (client): 1 (default) = after a device loss ends this client's
// session, the application's next eglCreateContext / eglCreate*Surface / eglInitialize
// dials the server for a FRESH session, and contexts created from then on work. Contexts
// (and their objects) from before the loss stay lost, as EGL's power-management event
// has them; surfaces survive and are re-created on the new session when next used.
// 0 = the loss is final for the process (every verb DECLINED for the rest of its life).
Uint32 RecoverAfterLoss = 1;
// MOBILEGL_IPC_ADOPT_TIER, THE ZERO-COPY SWITCH (ruling ID-P11-14, CONTRACT-P11 B2):
// UNSET (kAdoptTierUnset, the default since 2026-09-29, the user's decision) = T0 where the
// session can run it, T2 QUIETLY where it cannot (MGLOG_D only, still counted);
// 0 = T0 asked by name - a T0 this session cannot use (a stream plane, the server's allow
// switch, no AHardwareBuffer, a failed server POST) is T2 plus one named line;
// 1 = T1, closed - refused by name, then T2; 2 = emulate (T2: the client keeps the shadow
// and pushes). Never a Fatal (MG_Remote/Transport/AdoptTier.h).
Uint32 AdoptTier = kAdoptTierUnset;
// MOBILEGL_IPC_ALLOW_ADOPT_T0 (server): 1 (default) = grant a client's T0 ask when this
// platform and the backend's POST self-test allow it; 0 = never grant it (the session
// runs T2 and says so). Read by the server role only; kept through ServerSpawn.
Uint32 AllowAdoptT0 = 1;
// MOBILEGL_IPC_VERB_BARRIER: 1 = the client blocks at every verb boundary until
// appliedSeq reaches its emitSeq (R-1). 0 is the negative control: it is EXPECTED to
// be red, because 31 of the 63 PipeInputs fields are still pulled from a live
// GLContext by the client's residual fill and a free-running queue lets the server
// read a FUTURE value of them.
Uint32 VerbBarrier = 1;
// MOBILEGL_IPC_RUN_AHEAD (P5e, MG_Remote/CONTRACT-P5E.md §1): 1 = after publishing an
// MOBILEGL_IPC_RUN_AHEAD (P5e, docs/archive/contracts/CONTRACT-P5E.md §1): 1 = after publishing an
// UNBARRIERED record the client returns immediately instead of waiting for its apply.
// It is one half of a conjunction and never a switch on its own - the client arms
// run-ahead only when the server also publishes kCapRunAheadApply, so on Magma, and on
@@ -624,14 +715,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
@@ -660,8 +756,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.
@@ -689,10 +786,25 @@ 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
+292 -76
View File
@@ -6,21 +6,20 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#if defined(__ANDROID__)
#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>
#include <cstdlib>
#if MOBILEGL_BUILD_DISAGGREGATED
// std::strcmp, for InitIpc's server-role check (P12). Split-only, so the pull build's TU is
// unchanged (G1).
// std::strcmp: InitIpc's server-role check (P12) and the system configuration's.
#include <cstring>
#endif
#ifndef _WIN32
extern char** environ;
@@ -40,6 +39,7 @@ namespace MobileGL::MG_Config {
Bool SplitTransportRequestedByConfig = false;
IpcTable Ipc;
#endif
RecordArmTable RecordArm;
} // namespace MobileGL::MG_Config
namespace MobileGL::MG_ConfigLoader {
@@ -49,6 +49,138 @@ namespace MobileGL::MG_ConfigLoader {
return (key.compare(0, 6, "LIBGL_") == 0 || key.compare(0, 9, "MOBILEGL_") == 0);
}
// ---- the system configuration -----------------------------------------------------------
//
// A library that is every process's GL implementation (installed as the system's EGL/GLX
// vendor) cannot rely on each process having been started with MOBILEGL_* in its environment:
// most are started by something that has never heard of it. So the settings a whole machine
// shares live in files, and the environment only overrides them:
//
// environment > /etc/mobilegl/backend (MOBILEGL_BACKEND_TYPE only) > client.conf > built-in
//
// client.conf is `KEY=value` lines; blank lines and lines starting with '#' are skipped, the
// key and the value are trimmed, and one pair of matching quotes around the value is removed.
// Only MOBILEGL_* / LIBGL_* keys are taken, exactly as from the environment. The backend file
// holds just the backend's name (the file the backend switch rewrites), and names it over a
// MOBILEGL_BACKEND_TYPE line in client.conf so that switch cannot be silently shadowed.
//
// MOBILEGL_CONFIG_FILE / MOBILEGL_BACKEND_FILE name other files; set to empty they turn the
// file off. The defaults exist only on desktop Linux: an Android app configures its process
// itself, and a server process (MOBILEGL_IPC_ROLE=server) never reads the clients' file.
namespace {
#if defined(__linux__) && !defined(__ANDROID__)
constexpr const char* kDefaultConfigFile = "/etc/mobilegl/client.conf";
constexpr const char* kDefaultBackendFile = "/etc/mobilegl/backend";
#else
constexpr const char* kDefaultConfigFile = "";
constexpr const char* kDefaultBackendFile = "";
#endif
String FilePathSetting(const char* variable, const char* fallback) {
const char* value = std::getenv(variable);
return value != nullptr ? String(value) : String(fallback);
}
String Trimmed(const String& text) {
const auto first = text.find_first_not_of(" \t\r\n");
if (first == String::npos) return String();
const auto last = text.find_last_not_of(" \t\r\n");
return text.substr(first, last - first + 1);
}
Bool IsServerProcess() {
const char* role = std::getenv("MOBILEGL_IPC_ROLE");
return role != nullptr && std::strcmp(role, "server") == 0;
}
// Reads `path` into `out` without replacing what `out` already holds. A missing file is
// not an error: the defaults are the absence of one.
void ReadConfigFile(const String& path, UnorderedMap<String, String>& out, Bool log) {
if (path.empty()) return;
std::FILE* file = std::fopen(path.c_str(), "r");
if (file == nullptr) return;
char buffer[4096];
Uint32 lineNumber = 0;
while (std::fgets(buffer, sizeof(buffer), file) != nullptr) {
++lineNumber;
const String line = Trimmed(buffer);
if (line.empty() || line[0] == '#') continue;
const auto equals = line.find('=');
const String key = equals == String::npos ? String() : Trimmed(line.substr(0, equals));
if (key.empty() || !IsAcceptedPrefix(key)) {
if (log) {
MGLOG_W("Config: %s:%u is not a MOBILEGL_*/LIBGL_* KEY=value line; ignored", path.c_str(),
lineNumber);
}
continue;
}
String value = Trimmed(line.substr(equals + 1));
if (value.size() >= 2 && (value.front() == '"' || value.front() == '\'') &&
value.back() == value.front()) {
value = value.substr(1, value.size() - 2);
}
out.emplace(key, value);
}
std::fclose(file);
}
void ReadBackendFile(const String& path, UnorderedMap<String, String>& out) {
if (path.empty()) return;
std::FILE* file = std::fopen(path.c_str(), "r");
if (file == nullptr) return;
char buffer[256];
const Bool read = std::fgets(buffer, sizeof(buffer), file) != nullptr;
std::fclose(file);
if (!read) return;
const String name = Trimmed(buffer);
if (!name.empty()) out.emplace("MOBILEGL_BACKEND_TYPE", name);
}
// Everything the system files say, the backend file ahead of client.conf (first emplace
// wins). Re-read on every call: callers are few (Init, and a handful of lookups before it),
// and a file that changed between two processes' starts must not be served stale.
void ReadSystemConfig(UnorderedMap<String, String>& out, Bool log) {
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
Bool LookupSetting(const char* key, String& outValue) {
if (const char* value = std::getenv(key)) {
outValue = value;
return true;
}
UnorderedMap<String, String> system;
ReadSystemConfig(system, false);
const auto it = system.find(key);
if (it == system.end()) return false;
outValue = it->second;
return true;
}
inline void InitializeAcceptedEnvVariables() {
if (!acceptedEnvVariablesMap) {
acceptedEnvVariablesMap = MakeUnique<UnorderedMap<String, String>>();
@@ -79,6 +211,15 @@ namespace MobileGL::MG_ConfigLoader {
}
}
}
// Below the environment: a key the environment set is never replaced.
UnorderedMap<String, String> system;
ReadSystemConfig(system, true);
for (const auto& [key, value] : system) {
if (acceptedEnvVariablesMap->emplace(key, value).second) {
MGLOG_D("Config: Accepted system setting: %s=%s", key.c_str(), value.c_str());
}
}
}
inline void QueryEnvVariable(const String& key, String& outValue, const String& defaultValue) {
@@ -109,13 +250,20 @@ 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;
}
return IsTruthyValue(it->second) ? MG_Config::QuirkOverride::ForceOn
: MG_Config::QuirkOverride::ForceOff;
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;
}
// Multi-draw mode is a named-value preference: unset keeps Auto (best supported tier),
@@ -221,19 +369,37 @@ 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.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
features.EsprytAvoidSamplerMipmapMinFilter =
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV");
// 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);
features.EsprytAvoidSamplerMipmapMinFilter = 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");
@@ -242,8 +408,7 @@ namespace MobileGL::MG_ConfigLoader {
features.EsprytDisableUploadRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING");
features.EsprytDisableInvalidateFlush = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH");
features.DisableLargeBufferAdoption = QueryEnvFlag("MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION");
features.EsprytForceDepthStencilReadbackEmulation =
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
features.EsprytForceDepthStencilReadbackEmulation = QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
features.MagmaDisableBlendedDepthWriteQuirk =
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
@@ -252,45 +417,43 @@ namespace MobileGL::MG_ConfigLoader {
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
features.AsyncOptimisticShaderStatus =
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
features.AsyncOptimisticShaderStatus = QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE");
features.EsprytViewportArrayEmulation =
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION");
features.EsprytWidenPacked16Storage =
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE");
features.EsprytViewportArrayEmulation = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION");
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, "");
@@ -299,6 +462,17 @@ namespace MobileGL::MG_ConfigLoader {
inline void InitBackendType() {
String backendTypeStr;
QueryEnvVariable("MOBILEGL_BACKEND_TYPE", backendTypeStr, "DirectGLES");
#if defined(__ANDROID__)
// An Android app that hosts the library sets the environment itself, before the library
// loads; the debug property is how a developer picks the backend for such a process
// without rebuilding it (`adb shell setprop debug.mobilegl.backend DirectVulkan`).
char property[PROP_VALUE_MAX] = {};
if (__system_property_get("debug.mobilegl.backend", property) > 0) {
MGLOG_I("Config: debug.mobilegl.backend=%s overrides MOBILEGL_BACKEND_TYPE=%s", property,
backendTypeStr.c_str());
backendTypeStr = property;
}
#endif
#define ENTRY(backendType) \
if (backendTypeStr == #backendType) { \
MG_Config::ActiveBackendType = BackendType::backendType; \
@@ -314,7 +488,7 @@ namespace MobileGL::MG_ConfigLoader {
#if MOBILEGL_BUILD_DISAGGREGATED
// MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>
// (ARCHITECTURE.md:583). Shaped after InitBackendType above: an exact-name table, then
// (ARCHITECTURE.md §16). Shaped after InitBackendType above: an exact-name table, then
// one fallback that names what it did instead. The two prefixed forms are the only
// reason this is not literally that function's ENTRY macro.
//
@@ -325,6 +499,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)); });
@@ -344,12 +527,12 @@ 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
// independent, so this is `Dial = Connect` with a launcher attached
// rather than the fork-coupled shape ARCHITECTURE.md:488 described.
// rather than the fork-coupled shape ARCHITECTURE.md §15.1 described.
if (lowered == "spawn") {
MG_Config::Transport = MG_Config::TransportMode::Spawn;
MG_Config::SplitTransportRequestedByConfig = true; // P7 F1, as above.
@@ -368,9 +551,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());
@@ -397,40 +581,35 @@ 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
// was inert before, the device numbers, and the sweep that put the default at 6 rather
// than 4 or 8 (the curve flattens at 6: 4->5 buys 1.92 s, 5->6 0.53 s, 6->8 nothing).
// Capped at 8: the retained set is bounded by the window's own depth, the store holds 24
// declared answers, and the device sweep showed the wall clock still falling at the old
// cap of 4 (see WireTables.cpp's kCreateWindowMax). WireTables.cpp's static_assert and
// StreamLink's crossing log are the checks on this number.
// The create window: default 10, range 1..12. 12 is WireTables.cpp's kCreateWindowMax, half
// of the stream link's declared-answer store, and its static_assert checks that bound.
// Config.h has the semantics and the reason for the default.
ipc.CreateWindow = QueryEnvUint32("MOBILEGL_IPC_CREATE_WINDOW", 10, 1, 12);
// P9 W1: the pack-buffer readback's A/B and red-once control (Config.h says both).
ipc.PboReadbackSync = QueryEnvUint32("MOBILEGL_IPC_PBO_READBACK_SYNC", 0, 0, 1);
// P10: fence polls answered locally; 0 is the A/B arm (Config.h).
ipc.PollEscalate = QueryEnvUint32("MOBILEGL_IPC_POLL_ESCALATE", 64, 0, 1000000);
// A lost session is replaced by a fresh one on the application's next create (Config.h).
ipc.RecoverAfterLoss = QueryEnvUint32("MOBILEGL_IPC_RECOVER", 1, 0, 1);
// The verify harness compares the pushed block against the applier per verb; a
// batched queue lets the comparer read a supplied field mid-apply, which is a
// torn read rather than a divergence. The batch is therefore off whenever the
// shadow comparer is armed.
if (MG_Config::Features.PipeVerify) ipc.BatchWaits = 0;
// 2 is the only tier P5 implements (R-6). 0 and 1 parse here and are refused at the
// point of use, which is where the "P11" in the message belongs.
ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", 2, 0, 2);
// UNSET is T0 with quiet fallbacks (the default since 2026-09-29); 0 asks for T0 by name, 1 is
// refused by name, 2 is T2 - settled at the handshake from the data plane (P11 A1/B2,
// MG_Remote/Transport/AdoptTier.h). An unparseable value is warned about and reads as unset.
ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", MG_Config::kAdoptTierUnset, 0, 2);
// P11 B2: the server's own T0 allow switch (Config.h).
ipc.AllowAdoptT0 = QueryEnvUint32("MOBILEGL_IPC_ALLOW_ADOPT_T0", 1, 0, 1);
ipc.VerbBarrier = QueryEnvUint32("MOBILEGL_IPC_VERB_BARRIER", 1, 0, 1);
// P5e (MG_Remote/CONTRACT-P5E.md §1). The wait rule's A/B, parsed here like every
// P5e (docs/archive/contracts/CONTRACT-P5E.md §1). The wait rule's A/B, parsed here like every
// other IPC knob and armed only where the server publishes kCapRunAheadApply.
ipc.RunAhead = QueryEnvUint32("MOBILEGL_IPC_RUN_AHEAD", 1, 0, 1);
// ... and forced OFF by the verify harness for BatchWaits' reason, one step further:
@@ -440,19 +619,32 @@ 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); "
@@ -522,13 +714,17 @@ namespace MobileGL::MG_ConfigLoader {
// changes the wall clock by seconds (the P12 sweep: 28.3 s at 1 down to 10.8 s at 6) a
// log that does not name it makes every run ambiguous about its own configuration.
MGLOG_I("Config: IPC ring=%uMiB stage=%uMiB wire-deferred=%uMiB spin=%uus event-wait=%ums "
"persistent-block=%uKiB create-window=%u "
"adopt-tier=%u verb-barrier=%u run-ahead=%u present-credit=%u control-timeout=%ums "
"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.AdoptTier,
ipc.VerbBarrier, ipc.RunAhead, ipc.PresentCredit, ipc.ControlTimeoutMs, ipc.ColdStartMs,
static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
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, static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
static_cast<int>(ipc.RoleSplitState), ipc.ServerAffinity.c_str(),
MG_Config::ServerOwnedWindowSurfaces() ? "server" : "offscreen");
if (ipc.VerbBarrier == 0) {
@@ -536,6 +732,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 "
@@ -546,7 +762,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...");
@@ -554,6 +769,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.
+14 -9
View File
@@ -34,6 +34,11 @@
#ifdef _WIN32
#define MOBILEGL_EXPORT extern "C" __declspec(dllexport)
#else
// Default visibility, which is what the platform's own GL and EGL headers declare their entry
// points with. The one place where that is not enough - a vendor library handing glvnd its own
// entry points, where a reference resolving against the process instead of this library would be a
// call back into the dispatcher - is answered at link time with -Bsymbolic-functions rather than
// here, because a second visibility on the same declaration does not compile.
#define MOBILEGL_EXPORT extern "C" __attribute__((visibility("default")))
#endif
@@ -102,14 +107,14 @@
// 2026-08-13 renumbering unchanged; the contract is and stays
// "INFO builds: asserts OFF; DEBUG builds: asserts ON".
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
#define MOBILEGL_ASSERT(condition, ...) \
do { \
if (!(condition)) { \
MGLOG_F("Assertion failed" __VA_OPT__(": ") __VA_ARGS__); \
MGLOG_F(" at %s:%d (%s)", __FILE__, __LINE__, __func__); \
TRAP; \
} \
} while (0)
#define MOBILEGL_ASSERT(condition, ...) \
do { \
if (!(condition)) { \
MGLOG_F("Assertion failed" __VA_OPT__(": ") __VA_ARGS__); \
MGLOG_F(" at %s:%d (%s)", __FILE__, __LINE__, __func__); \
TRAP; \
} \
} while (0)
#else
#define MOBILEGL_ASSERT(condition, ...)
#define MOBILEGL_ASSERT(condition, ...)
#endif
+1
View File
@@ -7,6 +7,7 @@
// End of Source File Header
#include "Config.h"
#include <MG_Backend/BackendObject.h>
namespace MobileGL {
namespace MG_Config {
+37 -3
View File
@@ -24,6 +24,10 @@
#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
#include <atomic>
#include <mutex>
@@ -47,7 +51,7 @@ namespace MobileGL {
MGLOG_I("MobileGL closing...");
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5: the split roles come down FIRST - ARCHITECTURE.md:537's order puts the whole
// P5: the split roles come down FIRST - ARCHITECTURE.md §14's order puts the whole
// of it before MobileGL::Destroy(), and this function IS MobileGL::Destroy. A no-op
// in a monolith RUN; absent from a monolith BUILD, because G1 admits no new pull
// symbol and no resized one (the first version called it unconditionally and moved
@@ -102,6 +106,10 @@ namespace MobileGL {
MG_State::GLState::ClearProgramTranslationCache();
MG_Backend::gBackendFunctionsTable = {};
g_isInitialized = false;
#if MOBILEGL_BUILD_DISAGGREGATED
// A later re-initialization asks whether the server is there again.
MG_Remote::Client::ResetServerProbeCache();
#endif
#if MOBILEGL_PIPE_VERIFY
// BEFORE Close, and from here rather than from a static destructor (V1 fix round 2):
// Close() nulls the role's sink and Log.cpp's next write reopens it with "w", so the
@@ -116,7 +124,7 @@ namespace MobileGL {
// TODO: add and use Destroy functions for other subsystems
}
}
} // namespace
void Initialize() {
if (g_isInitialized) {
@@ -155,7 +163,7 @@ namespace MobileGL {
// lavapipe and on an Adreno 830, because the two Init calls are two statements in one
// thread. And "the legacy pull path runs for it" is false under split - the client has
// no pull path to fall back to - so the records are simply lost, and only a later
// respecify's self-healing create (TextureEmit.h:872) ever brings one back.
// respecify's self-healing create (EmitResourceRespecify in TextureEmit.h) ever brings one back.
//
// A BOUNDED WAIT INSIDE ServerConsumes CANNOT FIX THIS AND WOULD DEADLOCK: during
// MG_State::Init() no handshake is in flight, and the one that will land is started
@@ -175,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();
@@ -220,6 +235,25 @@ namespace MobileGL {
DestroyImpl(true);
}
Bool ImplementationAvailable() {
if (g_isInitialized.load(std::memory_order_acquire)) {
#if MOBILEGL_BUILD_DISAGGREGATED
// Initialized: under a split transport the session either came up (a backend object
// exists) or it did not, and a display on a session that never came up cannot draw.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
return MG_Backend::pActiveBackendObject != nullptr;
}
#endif
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// Not yet: the bring-up would dial the configured server and wait for it. Ask first.
return MG_Remote::Client::ConfiguredServerAvailable();
#else
return true;
#endif
}
// MobileGL's lifecycle is owned entirely by the host-API layers
// (EGL/WGL/CGL): initialization happens lazily on the first entry point
// via EnsureInitialized(), and full teardown happens deterministically
+17
View File
@@ -26,13 +26,30 @@ namespace MobileGL {
namespace MG_ConfigLoader {
void Init();
// One setting resolved the way Init() resolves it - the environment, then the system
// configuration files (ConfigLoader.cpp) - for the few readers that must decide something
// before MobileGL initializes. False when neither names it.
Bool LookupSetting(const char* key, String& outValue);
} // 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), 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. 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();
namespace MG_Backend {
void Init();
} // namespace MG_Backend
namespace MG_Impl {
void Init();
// GL framebuffer 0, built into the calling thread's GLContext (the process default at
// Init) and installed as the one every later context starts with. Init's step, and the
// rebuild after a recovered device loss, whose fresh session holds none of the old one's.
void BuildDefaultFramebuffer();
} // namespace MG_Impl
} // namespace MobileGL
+65 -20
View File
@@ -24,10 +24,8 @@ namespace MobileGL::MG_Backend {
return std::this_thread::get_id();
}
const char* GetFormatCapabilitySupportString(const FormatCapabilityCache& cache,
SizeT targetIndex,
SizeT formatIndex,
FormatCapability capability) {
const char* GetFormatCapabilitySupportString(const FormatCapabilityCache& cache, SizeT targetIndex,
SizeT formatIndex, FormatCapability capability) {
if (HasFormatCapability(cache.FullCaps[targetIndex][formatIndex], capability)) return "Full";
if (HasFormatCapability(cache.CaveatCaps[targetIndex][formatIndex], capability)) return "Caveat";
return "None";
@@ -58,9 +56,7 @@ namespace MobileGL::MG_Backend {
return line.str();
}
String BuildFormatCapabilityRow(const FormatCapabilityCache& cache,
SizeT targetIndex,
SizeT formatIndex,
String BuildFormatCapabilityRow(const FormatCapabilityCache& cache, SizeT targetIndex, SizeT formatIndex,
SizeT formatNameWidth) {
const auto format = static_cast<TextureInternalFormat>(formatIndex);
std::ostringstream line;
@@ -97,6 +93,8 @@ namespace MobileGL::MG_Backend {
static_cast<SizeT>(target) >= kFormatCapabilityTextureTargetCount) {
return kFormatCapabilityTargetCount;
}
// An external texture is a 2D one wherever a format can go.
if (target == TextureTarget::External) return static_cast<SizeT>(TextureTarget::Texture2D);
return static_cast<SizeT>(target);
}
@@ -172,11 +170,15 @@ namespace MobileGL::MG_Backend {
return false;
}
if (m_eglDisplayInitialized && m_eglDisplay != dpy) {
MGLOG_E("InitializeEGLDisplay failed: backend already bound to a different EGLDisplay");
return false;
}
// A DISPLAY IS A CONNECTION, NOT A RESOURCE THIS BACKEND OWNS, and refusing the second one
// refused a caller for a reason that is not there: the surface and context tables are keyed
// by the display each was created on, every operation carries its own display handle, and
// this field has no reader anywhere - it was the whole of the rule.
//
// What the rule cost was a compositor. One asks for the display it will draw into AFTER
// probing another, so the second eglInitialize is the one that matters, and a refusal there
// is reported one layer up as "no usable DRM render device" - the probe having succeeded
// and the real display having failed, with nothing in between that says which was refused.
m_eglDisplay = dpy;
m_eglDisplayInitialized = true;
if (major) {
@@ -196,12 +198,15 @@ namespace MobileGL::MG_Backend {
Bool BackendObject::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
auto surfaceIt = m_eglSurfaces.find(surface);
if (surfaceIt == m_eglSurfaces.end() || surfaceIt->second.Kind != SurfaceKind::Window) {
if (surfaceIt == m_eglSurfaces.end() || surfaceIt->second.Kind == SurfaceKind::None) {
MGLOG_E("ResizeEGLWindowSurface failed: no window surface is initialized");
return false;
}
// A pbuffer-backed window (a Wayland window presented through wl_shm) resizes too.
surfaceIt->second.Window.Width = width;
surfaceIt->second.Window.Height = height;
surfaceIt->second.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
surfaceIt->second.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
if (m_eglSurface == surface) {
m_windowHandle.Width = width;
m_windowHandle.Height = height;
@@ -279,6 +284,7 @@ namespace MobileGL::MG_Backend {
return true;
}
m_activatingSurface = surface;
if (surfaceState->Kind == SurfaceKind::Window) {
SetWindowHandle(surfaceState->Window);
if (!InitWindowSurface()) {
@@ -298,8 +304,14 @@ namespace MobileGL::MG_Backend {
m_eglSurface = surface;
m_eglSurfaceInitialized = true;
m_eglSurfaceKind = surfaceState->Kind;
m_eglCurrentThreads.clear();
m_backendCapabilitiesInitialized = false;
// NOT m_eglCurrentThreads.clear(): another thread's current context and surface are its
// own (a multi-window client renders each window on its own thread), and wiping them made
// that thread's next eglSwapBuffers fail with "no current context attached".
auto activatedIt = m_eglSurfaces.find(surface);
if (activatedIt != m_eglSurfaces.end() && !activatedIt->second.Activated) {
activatedIt->second.Activated = true;
m_backendCapabilitiesInitialized = false;
}
return true;
}
@@ -366,6 +378,28 @@ namespace MobileGL::MG_Backend {
m_eglCurrentThreads.clear();
}
void BackendObject::ForgetActiveEGLSurface() {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
m_eglSurfaceInitialized = false;
m_eglSurfaceKind = SurfaceKind::None;
m_eglSurface = EGL_NO_SURFACE;
m_windowHandle = {};
}
namespace {
MG_Util::Damage::Region& PresentDamageSlot() {
thread_local MG_Util::Damage::Region damage = MG_Util::Damage::Region::Full();
return damage;
}
} // namespace
const MG_Util::Damage::Region& CurrentPresentDamage() {
return PresentDamageSlot();
}
void SetCurrentPresentDamage(const MG_Util::Damage::Region& damage) {
PresentDamageSlot() = damage;
}
Bool BackendObject::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_eglDisplayInitialized || m_eglDisplay != dpy) {
@@ -382,10 +416,16 @@ namespace MobileGL::MG_Backend {
MGLOG_E("SwapEGLBuffers failed: draw surface is not current on this thread");
return false;
}
if (!m_eglSurfaceInitialized || draw == EGL_NO_SURFACE || draw != m_eglSurface) {
if (draw == EGL_NO_SURFACE || !GetRegisteredEGLSurface(draw)) {
MGLOG_E("SwapEGLBuffers failed: invalid draw surface");
return false;
}
// The calling thread's surface, which another thread's make-current may have moved the
// backend off: bring it back before presenting it.
if ((!m_eglSurfaceInitialized || draw != m_eglSurface) && !ActivateEGLSurface(draw)) {
MGLOG_E("SwapEGLBuffers failed: the draw surface could not be re-activated");
return false;
}
const auto& backendFunctions = GetBackendFunctions();
if (!backendFunctions.Present) {
@@ -418,16 +458,16 @@ namespace MobileGL::MG_Backend {
void BackendObject::DestroyPendingEGLSurfaceIfUnused(EGLSurface surface) {
auto surfaceIt = m_eglSurfaces.find(surface);
if (surfaceIt == m_eglSurfaces.end() || !surfaceIt->second.DestroyPending ||
IsEGLSurfaceCurrent(surface)) {
if (surfaceIt == m_eglSurfaces.end() || !surfaceIt->second.DestroyPending || IsEGLSurfaceCurrent(surface)) {
return;
}
m_eglSurfaces.erase(surfaceIt);
if (m_eglSurface == surface) {
OnEGLSurfaceReleased(surface);
ResetEGLRuntimeState();
ForgetActiveEGLSurface();
}
OnEGLSurfaceForgotten(surface);
}
void BackendObject::ReleaseEGLCurrentThread(const std::thread::id& threadKey) {
@@ -458,8 +498,9 @@ namespace MobileGL::MG_Backend {
m_eglSurfaces.erase(surfaceIt);
if (m_eglSurface == surface) {
OnEGLSurfaceReleased(surface);
ResetEGLRuntimeState();
ForgetActiveEGLSurface();
}
OnEGLSurfaceForgotten(surface);
}
void BackendObject::ReleaseEGLResources() {
@@ -492,4 +533,8 @@ namespace MobileGL::MG_Backend {
void BackendObject::OnEGLSurfaceReleased(EGLSurface surface) {
(void)surface;
}
void BackendObject::OnEGLSurfaceForgotten(EGLSurface surface) {
(void)surface;
}
} // namespace MobileGL::MG_Backend
+266 -343
View File
@@ -8,24 +8,17 @@
#pragma once
#include <Includes.h>
#include <BackendCaps.h>
#include "MG_State/GLState/TextureState/TextureEnum.h"
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Util/Damage/Damage.h>
#include <MG_Pipe/MGPipeHandles.h>
#endif
namespace MobileGL {
namespace MG_State::GLState {
class FramebufferObject;
class ITextureObject;
class RenderbufferObject;
}
enum class BackendType {
DirectGLES,
DirectVulkan,
BackendTypeCount,
Unknown = -1
};
} // namespace MG_State::GLState
namespace MG_Backend {
// One endpoint of a glCopyImageSubData. GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER
@@ -36,93 +29,20 @@ 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;
}
};
enum class FormatCapability : Uint64 {
Creatable = 1ull << 0,
Sampled = 1ull << 1,
LinearFilter = 1ull << 2,
GenerateMipmap = 1ull << 3,
TextureGather = 1ull << 4,
TextureShadow = 1ull << 5,
FramebufferRenderable = 1ull << 6,
FramebufferLayered = 1ull << 7,
MultisampleTexture = 1ull << 8,
MultisampleRenderbuffer = 1ull << 9,
ColorAttachment = 1ull << 10,
DepthAttachment = 1ull << 11,
StencilAttachment = 1ull << 12,
TextureBuffer = 1ull << 13
};
using FormatCapabilityFlags = Flags<FormatCapability>;
inline constexpr Array<FormatCapability, 14> kReportedFormatCapabilities = {
FormatCapability::Creatable,
FormatCapability::Sampled,
FormatCapability::LinearFilter,
FormatCapability::GenerateMipmap,
FormatCapability::TextureGather,
FormatCapability::TextureShadow,
FormatCapability::FramebufferRenderable,
FormatCapability::FramebufferLayered,
FormatCapability::MultisampleTexture,
FormatCapability::MultisampleRenderbuffer,
FormatCapability::ColorAttachment,
FormatCapability::DepthAttachment,
FormatCapability::StencilAttachment,
FormatCapability::TextureBuffer,
};
inline constexpr SizeT kFormatCapabilityTextureTargetCount =
static_cast<SizeT>(TextureTarget::TextureTargetCount);
inline constexpr SizeT kFormatCapabilityRenderbufferTargetIndex = kFormatCapabilityTextureTargetCount;
inline constexpr SizeT kFormatCapabilityTargetCount = kFormatCapabilityTextureTargetCount + 1;
inline constexpr SizeT kFormatCapabilityFormatCount =
static_cast<SizeT>(TextureInternalFormat::TextureInternalFormatCount);
using FormatCapabilityTable =
Array<Array<FormatCapabilityFlags, kFormatCapabilityFormatCount>, kFormatCapabilityTargetCount>;
using FormatSampleCountTable =
Array<Array<Vector<Int>, kFormatCapabilityFormatCount>, kFormatCapabilityTargetCount>;
struct FormatCapabilityCache {
FormatCapabilityTable FullCaps{};
FormatCapabilityTable CaveatCaps{};
FormatSampleCountTable SampleCounts{};
void Clear();
};
Bool HasFormatCapability(FormatCapabilityFlags caps, FormatCapability capability);
SizeT GetFormatCapabilityTargetIndex(TextureTarget target);
SizeT GetRenderbufferFormatCapabilityTargetIndex();
const char* GetFormatCapabilityName(FormatCapability capability);
String GetFormatCapabilityTargetName(SizeT targetIndex);
void PrintFormatCapabilities(const FormatCapabilityCache& cache);
// Opaque backend fence-sync handle, created by GLFunctionsTable::FenceSync
// and released by GLFunctionsTable::DeleteSync.
using BackendSyncHandle = void*;
@@ -146,8 +66,8 @@ namespace MobileGL {
void (*MultiDrawElementsIndirect)(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount,
GLsizei stride);
void (*MultiDrawArraysIndirect)(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
void (*MultiDrawElementsIndirectCount)(GLenum mode, GLenum type, const void* indirect,
GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride);
void (*MultiDrawElementsIndirectCount)(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride);
void (*MultiDrawArraysIndirectCount)(GLenum mode, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride);
void (*DrawRangeElementsBaseVertex)(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
@@ -185,19 +105,24 @@ namespace MobileGL {
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);
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, GLsizei height);
void (*CopyImageSubData)(const CopyImageEndpoint& src,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
// The by-name form: the destination is `texture`, whatever the units hold. `target` is
// the one the record carries (the texture's own 2D-class target).
void (*CopyTextureSubImage2D)(const SharedPtr<MG_State::GLState::ITextureObject>& texture, GLenum target,
GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height);
void (*CopyImageSubData)(const CopyImageEndpoint& src, GLenum srcTarget, GLint srcLevel, GLint srcX,
GLint srcY, GLint srcZ, const CopyImageEndpoint& dst, GLenum dstTarget,
GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth,
GLsizei srcHeight, GLsizei srcDepth);
void (*GenerateMipmap)(GLenum target);
// The by-name form, also used once the bound-target form has resolved its texture.
void (*GenerateTextureMipmap)(const SharedPtr<MG_State::GLState::ITextureObject>& texture, GLenum target);
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);
@@ -316,256 +241,6 @@ namespace MobileGL {
void (*SetSwapInterval)(Int interval);
};
// 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.
enum class GpuVendorKind : Uint8 {
Unknown = 0,
Qualcomm,
Arm,
Nvidia,
Amd,
Intel,
ImgTec,
// Software rasterizers (llvmpipe/lavapipe, SwiftShader).
Software,
};
struct DynamicBackendParameters {
Uint64 UniformBufferOffsetAlignment = 256;
// GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, which is a SEPARATE limit from the
// uniform one and is routinely larger: Adreno 830 reports 32 for uniform buffers and
// 64 for storage buffers. Answering the storage query with the uniform value let an
// application bind a storage range at an offset the driver cannot address, which it
// accepted without error and then wrote somewhere else entirely.
Uint64 ShaderStorageBufferOffsetAlignment = 256;
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT. 1.0 means the backend cannot filter anisotropically,
// which is also why the extension is not advertised in that case.
Float MaxTextureMaxAnisotropy = 1.0f;
Float AliasedLineWidthRangeMin = 1.0f;
Float AliasedLineWidthRangeMax = 1.0f;
Float SmoothLineWidthRangeMin = 1.0f;
Float SmoothLineWidthRangeMax = 1.0f;
Float SmoothLineWidthGranularity = 1.0f;
Float PointSizeRangeMin = 1.0f;
Float PointSizeRangeMax = 1.0f;
Float PointSizeGranularity = 1.0f;
Int Max3DTextureSize = 16384;
Int MaxArrayTextureLayers = 2048;
Int MaxCubeMapTextureSize = 16384;
Int MaxFramebufferWidth = 16384;
Int MaxFramebufferHeight = 16384;
Int MaxFramebufferLayers = 2048;
Int MaxRenderbufferSize = 16384;
Int MaxTextureSize = 16384;
Int MaxColorTextureSamples = 1;
Int MaxDepthTextureSamples = 1;
Int MaxFramebufferSamples = 1;
Int MaxIntegerSamples = 1;
Int MaxSamples = 1;
Int MaxSampleMaskWords = 1;
// Tessellation limits; defaults are the GL 4.0 core minimums.
Int MaxPatchVertices = 32;
Int MaxTessGenLevel = 64;
// GL_MIN/MAX_PROGRAM_TEXTURE_GATHER_OFFSET. Defaults are the GL 4.0 core
// minimums, which every ES 3.1 driver also guarantees.
Int MinProgramTextureGatherOffset = -8;
Int MaxProgramTextureGatherOffset = 7;
Int MaxTextureImageUnits = 32;
Int MaxVertexTextureImageUnits = 32;
Int MaxComputeTextureImageUnits = 32;
Int MaxCombinedTextureImageUnits = 192;
Int MaxVertexAttribs = 16;
Int MaxComputeShaderStorageBlocks = 8;
Int MaxCombinedShaderStorageBlocks = 32;
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. Zero is a legal answer for the four
// non-compute, non-fragment stages and these defaults are the spec minimums, not
// placeholders: GL 4.6 table 23.64 and ES 3.2 table 21.44 both set the minimum for
// vertex, tessellation control, tessellation evaluation and geometry at 0, and only
// fragment (8 in GL, 4 in ES) and compute are guaranteed to have any. Every real ARM
// GLES driver takes that allowance - a Mali-G925 reports 0 for all four - so a
// backend that cannot honour a graphics-stage storage block MUST report 0 here
// rather than a hopeful number. Advertising a non-zero count the driver will refuse
// does not make the block work; it only moves the failure from an honest
// "unsupported" at query time to a backend link error the frontend never surfaces,
// after which every draw with that program silently renders nothing.
Int MaxVertexShaderStorageBlocks = 0;
Int MaxTessControlShaderStorageBlocks = 0;
Int MaxTessEvaluationShaderStorageBlocks = 0;
Int MaxGeometryShaderStorageBlocks = 0;
Int MaxFragmentShaderStorageBlocks = 8;
Int MaxComputeUniformBlocks = 12;
Int MaxComputeWorkGroupInvocations = 128;
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / GL_MAX_COMPUTE_WORK_GROUP_SIZE, one value per
// axis. These six, with the invocations limit above, are the only indexed limits a
// backend genuinely OWNS - the device answers them (glGetIntegeri_v on DirectGLES,
// VkPhysicalDeviceLimits::maxComputeWorkGroupCount/Size on DirectVulkan) - and so
// the only ones that survive the retirement of the GetIntegeri_v table entry: they
// cross the MGPipe boundary inside MGPCaps, by inclusion of this struct (plan B
// section 4.4.1). Every other indexed pname names frontend state. RAW driver
// answers, like the invocations limit: GL_Getter and the compile environment floor
// them at the shared MIN_COMPUTE_WORK_GROUP_* minimums themselves. The defaults are
// the GL 4.3 core minimums (table 23.60) and describe the no-backend case, as
// MaxClipDistances' does.
Int MaxComputeWorkGroupCount[3] = {65535, 65535, 65535};
Int MaxComputeWorkGroupSize[3] = {1024, 1024, 64};
Int MaxShaderStorageBufferBindings = 8;
Int MaxTextureBufferSize = 65536;
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
Int TextureBufferOffsetAlignment = 1;
Int MaxUniformBufferBindings = 24;
Int MaxUniformBlockSize = 16384;
Int MaxImageUnits = 8;
Int MaxCombinedImageUniforms = 8;
Int MaxVertexImageUniforms = 0;
Int MaxGeometryImageUniforms = 0;
Int MaxFragmentImageUniforms = 8;
Int MaxComputeImageUniforms = 8;
Int MaxDrawBuffers = 8;
Int MaxColorAttachments = 8;
// GL_MAX_CLIP_DISTANCES. Zero is a legal answer here, not a placeholder, and a
// backend that cannot host a clip distance MUST report it: advertising eight the
// backend will refuse does not make gl_ClipDistance work, it only moves the failure
// from an honest "unsupported" at query time to a backend shader-compile error the
// frontend never surfaces, after which every draw with that program silently renders
// nothing. DirectGLES fills it from GL_EXT_clip_cull_distance, DirectVulkan from the
// shaderClipDistance device feature. The DEFAULT stays at the GL 4.3 core minimum
// because it describes the no-backend case (standalone shader compiles, unit tests),
// where there is no device to be honest about and BuildTBuiltInResource still has to
// hand glslang a workable gl_MaxClipDistances.
Int MaxClipDistances = 8;
// GL_MAX_CULL_DISTANCES and GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, under exactly
// the contract stated for MaxClipDistances above: ZERO IS A LEGAL ANSWER and a
// backend that cannot host a cull distance MUST report it. The failure this prevents
// is worse than the clip one, because cull distance discards the whole primitive:
// glslang bounds gl_CullDistance[i] against maxCullDistances and expands
// gl_MaxCullDistances from it, SPIRV-Cross then emits
// `#extension GL_EXT_clip_cull_distance : require` into the ESSL, and a host driver
// without that extension rejects the program in an info log nobody surfaces. These
// used to be bare 8s inside BuildTBuiltInResource with no backend consulted at all.
// The DEFAULTS are the GL 4.5 core minimums for the same reason MaxClipDistances'
// is: they describe the no-backend case (standalone compiles, unit tests).
Int MaxCullDistances = 8;
Int MaxCombinedClipAndCullDistances = 8;
Int MaxViewports = 16;
// GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX: which vertex of a
// primitive supplies gl_Layer and gl_ViewportIndex. GL 4.6 table 23.65 makes
// GL_UNDEFINED_VERTEX a legal answer for both, and it is the honest default - naming
// a convention is a statement about behaviour, so a backend that does not pin one
// must not claim it does. DirectGLES fills the layer one from the ES 3.2 query and
// the viewport one from GL_OES_viewport_array, and leaves UNDEFINED where the
// capability is absent: without the viewport array extension only viewport 0 is ever
// rasterized, so no convention selects anything. DirectVulkan keeps UNDEFINED for
// both - which vertex provokes is decided per pipeline by
// VulkanRenderer::SelectProvokingVertexMode out of VK_EXT_provoking_vertex,
// provokingVertexModePerPipeline and the topology, so no single convention is true
// of the backend.
Uint32 LayerProvokingVertex = GL_UNDEFINED_VERTEX;
Uint32 ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
Int MaxViewportWidth = 16384;
Int MaxViewportHeight = 16384;
Float ViewportBoundsRangeMin = 0.0f;
Float ViewportBoundsRangeMax = 0.0f;
Int ViewportSubpixelBits = 0;
// GL 4.x fragment-interpolation offset limits. These defaults are the
// core minimums and are replaced by live GLES/Vulkan device limits.
Float MinFragmentInterpolationOffset = -0.5f;
// For four fractional bits the greatest required legal offset is
// 0.5 - 2^-4 = 0.4375 (GL 4.6 table 23.70).
Float MaxFragmentInterpolationOffset = 0.4375f;
Int FragmentInterpolationOffsetBits = 4;
Bool SupportsWideLines = false;
// Whether a framebuffer whose depth and stencil attachments are distinct
// images can be rendered to. GL only requires support when both refer to the
// same image and lets an implementation answer GL_FRAMEBUFFER_UNSUPPORTED
// otherwise, which is what DirectVulkan (one combined attachment) and the
// real ES drivers behind DirectGLES both do. Defaults to true so a backend
// that never sets it keeps the permissive behaviour.
Bool SupportsDistinctDepthStencilAttachments = true;
// Whether attaching a single layer of a 3D or array texture to a framebuffer actually
// renders to that layer. DirectGLES hands the layer straight to
// glFramebufferTextureLayer, so it does; DirectVulkan maps a GL layer onto a Vulkan
// array layer with no notion of a 3D depth slice, so it does not yet. Defaults to false
// so a backend that never sets it gets the conservative answer.
// Which layered texture targets this backend can attach ONE layer of to a framebuffer
// and then really clear, render and read back that layer. Bit (1u << TextureTarget) is
// set for each supported target. Deliberately per target rather than one flag: the three
// ways a GL layer maps onto Vulkan are independent capabilities. A 2D or 2D multisample
// array layer IS a VkImage array layer and needs nothing extra; a 3D texture's layer is
// a z slice, which needs a 2D-array-compatible image and a per-slice clear that
// vkCmdClearColorImage cannot express; a cube map array needs an image shape and the
// imageCubeArray feature before it can be attached at any layer at all. Defaults to 0 so
// a backend that never sets it gets the conservative answer.
Uint32 PerLayerFramebufferAttachmentTargets = 0;
static constexpr Uint32 PerLayerFramebufferAttachmentBit(TextureTarget target) {
return (static_cast<Int>(target) >= 0 &&
static_cast<Int>(target) < static_cast<Int>(TextureTarget::TextureTargetCount))
? (1u << static_cast<Uint32>(target))
: 0u;
}
Bool SupportsPerLayerFramebufferAttachment(TextureTarget target) const {
const Uint32 bit = PerLayerFramebufferAttachmentBit(target);
return bit != 0 && (PerLayerFramebufferAttachmentTargets & bit) != 0;
}
// Whether this backend can CONSUME a shader module that still declares 64-bit floats,
// i.e. whether `double` survives the transpile instead of being narrowed to `float`
// (ShaderTranspiler::DemoteFloat64Pass). Detected, never assumed:
// * DirectVulkan sets it from VkPhysicalDeviceFeatures::shaderFloat64, the feature
// VUID-VkShaderModuleCreateInfo-pCode-08740 requires before a module declaring
// OpCapability Float64 may be created at all. lavapipe has it; Adreno and Mali
// both report VK_FALSE, so no real mobile device does.
// * DirectGLES can NEVER have it. GLSL ES has no 64-bit float type in any version
// or extension, so SPIRV-Cross cannot emit one ("FP64 not supported in ES
// profile") and the demotion there is mathematically mandatory, always.
// Defaults to false so a backend that never sets it - and the no-backend case, which
// is what standalone shader compiles and the unit tests run under - keeps the
// demotion, which is the behaviour that works everywhere.
Bool SupportsShaderFloat64 = false;
// Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e.
// whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected,
// never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (the
// attribute travels as its 32-bit word pair, so no VK_FORMAT_R64* is required, but the
// bitcast result is Float64); DirectGLES can never have it, ESSL having no fp64 type at
// all. Defaults to false so a backend that never sets it gets the conservative answer.
//
// INDEPENDENT of SupportsShaderFloat64, and it has to be: this flag decides a VkFormat
// from the VAO ATTRIBUTE alone, which does not know what type the shader declared, and
// glVertexAttribFormat(GL_DOUBLE) feeding a plain `in vec4` is both legal and common
// (KHR-GL43.vertex_attrib_binding.basic-input-case4/5, advanced-bindingUpdate). A
// backend with native fp64 that still cannot FETCH 64 bits keeps this false and relies
// on the per-MODULE rule in ShaderCompiler::SanitizeAndOptimizeBinary instead: a vertex
// module that declares a 64-bit float INPUT is demoted whole, so the two shader-side
// halves (PackDoubleVertexInputsPass and VertexInputStateFactory::ToVkVertexFormat)
// still see one consistent world.
Bool SupportsFloat64VertexAttributes = false;
// Whether a TESSELLATION stage of this backend may access gl_PointSize - i.e.
// whether a module declaring OpCapability TessellationPointSize can reach the
// driver at all. DirectVulkan sets both this and the geometry twin from the one
// shaderTessellationAndGeometryPointSize feature; DirectGLES sets them
// independently from the EXT/OES_tessellation_point_size /
// geometry_point_size extension pairs (PointSizeTier), which really do come
// separately. When absent, ProgramSpirvTask demotes the built-in to an ordinary
// varying program-wide (ShaderCompiler::
// DemoteTessellationGeometryPointSizeForProgram); MOBILEGL_POINT_SIZE_DEMOTION
// overrides the detection in either direction at backend init.
//
// Defaults TRUE, deliberately against the house "assume absent" rule: false
// ARMS a rewrite, so the conservative no-backend answer (standalone compiles,
// unit tests) is the one that leaves modules untouched. A backend that never
// sets it gets standard modules and, at worst, the old honest declines.
Bool SupportsTessellationPointSize = true;
// The geometry-stage twin (OpCapability GeometryPointSize).
Bool SupportsGeometryPointSize = true;
Uint64 MaxShaderStorageBlockSize = 128 * 1024 * 1024;
Uint32 SubgroupSize = 0;
Uint32 SubgroupSupportedStages = 0;
Uint32 SubgroupSupportedFeatures = 0;
Bool SubgroupQuadOperationsInAllStages = false;
GpuVendorKind GpuVendor = GpuVendorKind::Unknown;
};
enum class WindowBackend {
Android,
X11,
@@ -583,26 +258,257 @@ namespace MobileGL {
Uint32 Height = 0;
};
// SHARED IMAGES (docs/archive/notes/anland/plan-ahb-dmabuf.md): server-allocated colour
// buffers exported to clients as dma-buf descriptors.
//
// The server-side view of one image, as a backend sees it: `NativeBuffer` is the image's
// AHardwareBuffer (null where there is none), `Fourcc` a DRM fourcc - ABGR8888 (R, G, B, A in
// memory, GL RGBA8) or XBGR8888 (alpha ignored); ARGB8888 / XRGB8888 images are stored the
// same way (SharedImageRegistry.h says why), so a backend binds every one as RGBA8.
struct SharedImageView {
Uint64 Id = 0;
Uint32 Width = 0;
Uint32 Height = 0;
Uint32 Fourcc = 0;
void* NativeBuffer = nullptr;
};
// The client-side answer of an allocation: the image's id and its exported descriptor
// (`Fd`, owned by the caller), with the plane layout a linux-dmabuf wl_buffer is created with.
struct SharedImageExport {
Uint64 Id = 0;
int Fd = -1;
Uint32 Width = 0;
Uint32 Height = 0;
Uint32 Fourcc = 0;
Uint32 Stride = 0;
Uint32 Offset = 0;
Uint64 Modifier = 0;
// A YUV image's Cb Cr plane: in the same descriptor, at this offset and pitch.
Uint32 Plane1Stride = 0;
Uint32 Plane1Offset = 0;
};
// An import of a two-plane (YUV) dma-buf: where its planes are in the one descriptor, and
// EGL_EXT_image_dma_buf_import's colour hints (0 = not given).
struct SharedImageImportLayout {
Uint32 PlaneCount = 1;
Uint32 Offset[2] = {0, 0};
Uint32 Pitch[2] = {0, 0};
Uint32 ColorSpace = 0;
Uint32 Range = 0;
Uint32 SitingX = 0;
Uint32 SitingY = 0;
// The DRM format modifier the import named; HasModifier false = none (the implicit one).
Bool HasModifier = false;
Uint64 Modifier = 0;
};
// THE DAMAGE OF THE PRESENT BEING APPLIED on this thread: the server's apply thread sets it
// around the backend's Present (from the present record), and a backend that can hand damage
// to its native present reads it there. Full whenever nothing set it.
const MG_Util::Damage::Region& CurrentPresentDamage();
void SetCurrentPresentDamage(const MG_Util::Damage::Region& damage);
class BackendObject {
public:
virtual ~BackendObject() = default;
// ---- shared images, SERVER side (a backend implements these) ---------------------
//
// Copies the default framebuffer of the surface current on this (the apply) thread into
// `image`, rows TOP FIRST - GL's top row (y = height-1) lands in the image's first memory
// row, which is what a wl_buffer's first row is - scaled to the image's extent if the two
// differ. The image is ORDERED THROUGH ITS SYNC STATE (SharedImageRegistry.h): the copy
// waits for the image's pending read fences before it writes, and its own completion
// is published with SharedImages::PublishWrite as a sync_file - the call returns once
// the copy is submitted, never waiting for it on the CPU (a backend with no fence export
// waits and publishes -1). A reader binding the image waits for that fence on its GPU
// when the write generation moved, and at its frame boundary publishes its frame's fence
// with PublishRead / ReadTracker. The application's GL state is untouched. False = not
// supported / failed (logged).
//
// `region` (GL window coordinates, origin bottom-left) is the part of the frame to copy;
// Full copies all of it. The rest of the image is left as it is - the client sends a
// region only for an image that already holds the frame outside it - and a backend that
// cannot be sure the image's contents survived (a target it has never written) copies
// all of it regardless.
virtual Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image,
const MG_Util::Damage::Region& region) {
(void)image, (void)region;
return false;
}
// EGL_BUFFER_AGE_EXT of the draw surface of the context current on this (the apply)
// thread: how many presents ago the buffer the next frame draws into held the frame then
// presented; 0 = unknown (the application repaints everything). Asking says the client
// relies on the answer, so from then on the backend keeps what it presents.
//
// `damageRegionFollows`: the client declares the region it draws (SetCurrentDamageRegion)
// before drawing. A driver whose only buffer age is EGL_KHR_partial_update's keeps a
// buffer's content only outside that region, so without one coming it is not asked.
virtual Int32 QueryCurrentBufferAge(Bool damageRegionFollows) {
(void)damageRegionFollows;
return 0;
}
// eglSetDamageRegionKHR for that draw surface: the frame draws `region` (GL window
// coordinates; Full = all of it) and nothing outside it. A backend without the notion
// ignores it.
virtual Bool SetCurrentDamageRegion(const MG_Util::Damage::Region& region) {
(void)region;
return true;
}
// IMPLICIT SYNC BY FLUSH (docs/archive/notes/anland/plan-x11-gpu.md): the session
// on this (the apply) thread ends a frame at a glFlush. Every shared image it used since
// its last boundary is published as written and read by one fence of everything it
// submitted (SharedImages::ReadTracker::PublishFrameAsWrite), and from now on its first
// use of an image in a frame waits for that image's pending reads (it may be about to
// overwrite what another session still samples). A backend without shared images has
// nothing to publish.
virtual Bool PublishSharedImageAccesses() { return true; }
// EGL_ANDROID_native_fence_sync's fence command, on the server: in `*fence`, a sync_file
// (the caller's to close) that signals once everything the session submitted so far has
// completed - and, for a session that writes images, what PublishSharedImageAccesses
// does. -1 when there is none to give: the backend cannot export one, or the work has
// already completed - in both cases it HAS completed when this returns.
virtual Bool ExportNativeFence(int* fence) {
*fence = -1;
return PublishSharedImageAccesses();
}
// ---- shared images, CLIENT side (BackendObject_Remote implements these) ----------
//
// A new image, allocated by the server; `out->Fd` is the caller's to close.
virtual Bool AllocateSharedImage(Uint32 width, Uint32 height, Uint32 fourcc, SharedImageExport* out) {
(void)width, (void)height, (void)fourcc, (void)out;
return false;
}
// Names the image `fd` was exported from (the caller keeps `fd`). False: not one of this
// server's images, or the size/format disagree.
virtual Bool ImportSharedImage(int fd, Uint32 width, Uint32 height, Uint32 fourcc, Uint64* outId) {
(void)fd, (void)width, (void)height, (void)fourcc, (void)outId;
return false;
}
// The same for a YUV fourcc, whose planes and hints `layout` gives. A YUV buffer the
// server did not export is taken too (its labelled CPU-copy fallback).
virtual Bool ImportSharedImagePlanes(int fd, Uint32 width, Uint32 height, Uint32 fourcc,
const SharedImageImportLayout& layout, Uint64* outId) {
(void)fd, (void)width, (void)height, (void)fourcc, (void)layout, (void)outId;
return false;
}
// Drops this client's reference (an allocation's or an import's).
virtual Bool ReleaseSharedImage(Uint64 id) {
(void)id;
return false;
}
// In the GL stream: the current context's default framebuffer, copied top row first into
// the image (BlitDefaultFramebufferToSharedImage on the server) - `region` of it, Full for
// all. Returns when it completed.
virtual Bool PresentToSharedImage(Uint64 id, const MG_Util::Damage::Region& region) {
(void)id, (void)region;
return false;
}
// In the GL stream: EGL_BUFFER_AGE_EXT of the current draw surface (QueryCurrentBufferAge
// on the server). False: the backend has no answer (the caller reports 0).
virtual Bool QueryBufferAge(Bool damageRegionFollows, EGLint* age) {
(void)damageRegionFollows, (void)age;
return false;
}
// In the GL stream: eglSetDamageRegionKHR for the current draw surface
// (SetCurrentDamageRegion on the server).
virtual Bool SetDamageRegion(const MG_Util::Damage::Region& region) {
(void)region;
return false;
}
// In the GL stream: the texture whose lifetime id is `textureLifetimeId` takes the image as
// its level-0 storage (glEGLImageTargetTexture2DOES), until its next storage definition.
// The caller has already defined level 0 as Width x Height RGBA8 with no data.
virtual Bool AttachSharedImageToTexture(Uint64 textureLifetimeId, Uint64 id) {
(void)textureLifetimeId, (void)id;
return false;
}
// In the GL stream, from glFlush/glFinish: PublishSharedImageAccesses on the server,
// returning once the fence is published. Skipped (true) when nothing reached the stream
// since the last one.
virtual Bool FlushSharedImageAccesses() { return false; }
// In the GL stream, from eglCreateSync(EGL_SYNC_NATIVE_FENCE_ANDROID) with no descriptor:
// ExportNativeFence on the server. `*fence` is the sync_file it handed back (the
// caller's to close), or -1 when it had none - the fenced work has then completed.
virtual Bool CreateNativeFence(int* fence) {
*fence = -1;
return false;
}
virtual void Initialize() = 0;
virtual Bool InitCapabilities() = 0;
virtual Bool InitWindowSurface() = 0;
// A served session's apply thread - the one thread that runs this backend from here on -
// calls this first, in a process that may serve several sessions at once. A backend whose
// native state is process-global by construction gives this object its own copy and binds
// it to the calling thread. The default has nothing per process to separate.
virtual void BindSessionStateToThisThread() {}
// A served session's apply thread asks this each time it wakes, BEFORE it applies another
// record: true when this session's own GPU work faulted or hung the GPU and the backend has
// latched the session for it (MGPipeSessionLatch, family DeviceLost), so the next frame -
// which would very likely stop the GPU again - is never applied. Cheap by contract: no
// wait, at most a status read. The default knows of no fault.
virtual Bool LatchIfGpuFaulted() { return false; }
// A served session's ring moved to another of its client's contexts (bind_context). A
// backend whose surface follows the native context it binds needs nothing; one that keeps
// a single native surface binding re-binds the surface that context was last made current
// with. `token` is the client's context token (the EGLContext MakeEGLCurrent was given).
virtual void OnClientContextBound(Uint64 token) { (void)token; }
virtual Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor);
virtual Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle);
virtual Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height);
virtual Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height);
virtual Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
virtual Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw);
// eglSwapBuffersWithDamage: `damage` (GL window coordinates) is a hint for the native
// present. A backend with nothing to hand it to swaps as SwapEGLBuffers does.
virtual Bool SwapEGLBuffersWithDamage(EGLDisplay dpy, EGLSurface draw,
const MG_Util::Damage::Region& damage) {
(void)damage;
return SwapEGLBuffers(dpy, draw);
}
// Forwards the app-requested eglSwapInterval to the backend's native
// presentation path (no-op for backends without a SetSwapInterval hook).
virtual void SetEGLSwapInterval(Int interval);
virtual void ReleaseEGLSurface(EGLSurface surface);
virtual void ReleaseEGLResources();
// ---- the server's own display window going away and coming back -------------------
//
// `window` (a window surface's native window) is about to be destroyed - the display's
// screen went off. Release ONLY what is bound to it: every window surface on it keeps its
// client handle and draws into a placeholder from here on, and every context, object and
// device stays. Called on the thread that runs this backend; nothing may reference the
// window once it returns true. False: not supported (the caller ends the session instead).
virtual Bool SuspendServerWindow(void* window) {
(void)window;
return false;
}
// The surfaces SuspendServerWindow parked are rebuilt on `window` (a new native window,
// `width`x`height`), and their new extent is published as a creation publishes it. False:
// they could not be (the caller ends the session).
virtual Bool ResumeServerWindow(void* window, Uint32 width, Uint32 height) {
(void)window, (void)width, (void)height;
return false;
}
// `window` changed size to `width`x`height` (the display's report - the one size to publish).
// Its surfaces publish that extent to the client now, without waiting for a present; a
// swapchain that has to follow is rebuilt when no frame is half-recorded, else at the
// present. False: nothing was published (no surface on it, or not supported).
virtual Bool ServerWindowResized(void* window, Uint32 width, Uint32 height) {
(void)window, (void)width, (void)height;
return false;
}
// CLIENT side, from eglQuerySurface(EGL_WIDTH/EGL_HEIGHT): a backend whose surface extents
// change under the application (a remote server's window) applies what it has received.
virtual void RefreshSurfaceExtent(EGLSurface surface) { (void)surface; }
void SetWindowHandle(const WindowHandle& handle);
virtual const RendererInfo& GetRendererInfo() const = 0;
@@ -629,18 +535,28 @@ namespace MobileGL {
struct EGLSurfaceState {
SurfaceKind Kind = SurfaceKind::None;
Bool DestroyPending = false;
// Activated at least once: a re-activation (a multi-window client switching back)
// is a re-bind and does not re-run the backend's capability probe.
Bool Activated = false;
WindowHandle Window;
EGLint Width = 1;
EGLint Height = 1;
};
void ResetEGLRuntimeState();
// ResetEGLRuntimeState for a released ACTIVE surface: the backend has no active surface
// any more, but every OTHER thread's current context and surface stay as they were (a
// multi-window client closing one popup must not unbind its other windows' threads).
void ForgetActiveEGLSurface();
Bool RegisterEGLWindowSurface(EGLSurface surface, const WindowHandle& handle);
Bool RegisterEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height);
const EGLSurfaceState* GetRegisteredEGLSurface(EGLSurface surface) const;
Bool ActivateEGLSurface(EGLSurface surface);
virtual Bool InitPbufferSurface(EGLint width, EGLint height);
virtual void OnEGLSurfaceReleased(EGLSurface surface);
// Every registered surface the base class drops, current or not, after it is gone from
// m_eglSurfaces. A backend that keeps a native surface per client surface frees it here.
virtual void OnEGLSurfaceForgotten(EGLSurface surface);
FormatCapabilityCache& MutableFormatCapabilities();
mutable std::recursive_mutex m_eglStateMutex;
@@ -648,6 +564,10 @@ namespace MobileGL {
WindowHandle m_windowHandle;
EGLDisplay m_eglDisplay = EGL_NO_DISPLAY;
EGLSurface m_eglSurface = EGL_NO_SURFACE;
// The client surface ActivateEGLSurface is bringing up while it calls InitWindowSurface /
// InitPbufferSurface, which take no surface argument: a backend that maps client surfaces
// to native ones reads it to re-bind an existing native surface instead of making another.
EGLSurface m_activatingSurface = EGL_NO_SURFACE;
Bool m_eglDisplayInitialized = false;
Bool m_eglSurfaceInitialized = false;
Bool m_backendCapabilitiesInitialized = false;
@@ -660,5 +580,8 @@ namespace MobileGL {
void DestroyPendingEGLSurfaceIfUnused(EGLSurface surface);
void ReleaseEGLCurrentThread(const std::thread::id& threadKey);
};
extern UniquePtr<BackendObject>& pActiveBackendObject;
extern GlobalBackendFunctionsTable gBackendFunctionsTable;
} // namespace MG_Backend
} // namespace MobileGL
+6 -12
View File
@@ -13,19 +13,16 @@
#include "DirectVulkan/BackendObject_DirectVulkan.h"
namespace MobileGL::MG_Backend {
extern UniquePtr<BackendObject>& pActiveBackendObject;
extern GlobalBackendFunctionsTable gBackendFunctionsTable;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5 v1. The counterpart of Init()'s single split hook, and a NO-OP in every run that is
// not split. It exists ONLY in a split build: G1 admits no new symbol in the pull build,
// so MobileGL/Init.cpp's call sits under the same #if rather than calling a no-op.
//
// IT MUST RUN FIRST, BEFORE ANYTHING ELSE IN DestroyImpl. ARCHITECTURE.md:537's order is
// IT MUST RUN FIRST, BEFORE ANYTHING ELSE IN DestroyImpl. ARCHITECTURE.md §14's order is
// publish -> the server drains and acks -> stop the apply thread (Kill, then join) -> close
// the transport -> the client drains the compile pool -> MobileGL::Destroy() -> release the
// sync/query handles; DestroyImpl IS MobileGL::Destroy, so everything before that arrow has
// to happen at its top. Two consequences are load-bearing rather than tidy:
// the transport -> the client drains the compile pool -> MobileGL::Destroy(); DestroyImpl IS
// MobileGL::Destroy, so everything before that last arrow has to happen at its top. Two
// consequences are load-bearing rather than tidy:
//
// * the apply thread is JOINED before PipeStats::Shutdown() dumps, so nothing is writing
// a counter while the final line is produced;
@@ -33,11 +30,8 @@ namespace MobileGL::MG_Backend {
// BackendObject - which is NOT that global (table 3) - is destroyed on the thread that
// owns the context, by ServerLoop::Stop, rather than on the app thread.
//
// The sync/query registries stay where they are, drained at MobileGL/Init.cpp:62 and :67
// BEFORE pActiveBackendObject.reset(). ARCHITECTURE.md:537 puts them after
// MobileGL::Destroy(); the two are only reconcilable if a split sync handle is
// client-minted and needs no backend call, which is P10's. CONTRACT-P5 4 flags this as a
// KNOWN OPEN ITEM and asks v1 to record which way it went: P5 keeps today's order.
// The sync/query registries stay where they are: DestroyImpl drains them with
// DestroyAllSyncObjects and DestroyAllQueryObjects before pActiveBackendObject.reset().
void ShutdownSplitRoles();
// F1 (P7 wave 2). Init()'s SPLIT ARM, run BEFORE MG_State::Init() rather than after it.
+491
View File
@@ -0,0 +1,491 @@
// MobileGL - MobileGL/MG_Backend/ClientReadback.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "ClientReadback.h"
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Util/Math/HalfFloat.h>
#include <MG_Util/Math/SmallFloat.h>
#include <algorithm>
#include <cmath>
namespace MobileGL::MG_Backend {
// ---- Client-format readback conversion helpers -------------------------------------------------
// ReadPixels/GetTexImage read a guaranteed wide RGBA(_INTEGER) layout from the ES driver and repack
// it on the CPU into the client's (format, type) layout. Everything here is pure byte shuffling so
// unit tests can assert the exact packed words; field positions follow GL 3.3 table 3.6 and mirror
// the GL CTS packed_pixels oracle (glcPackedPixelsTests.cpp pack_UNSIGNED_* helpers).
namespace ClientReadback {
using MG_Util::DecodeHalfBitsToFloat;
using MG_Util::EncodeFloatToHalfBits;
Bool GetReadbackChannelMapping(GLenum format, ReadbackChannelMapping& outMapping) {
switch (format) {
case GL_RED:
outMapping = {{0, 0, 0, 0}, 1, false};
return true;
case GL_RED_INTEGER:
outMapping = {{0, 0, 0, 0}, 1, true};
return true;
// Desktop-GL single-channel client formats (GL CTS packed_pixels rgba8_format_green/blue):
// the destination holds one component sourced from the named channel of the wide RGBA read.
// GL_ALPHA is mapped here from the raw enum because the state layer folds it into Red for the
// legacy alpha-texture upload hack.
case GL_GREEN:
outMapping = {{1, 0, 0, 0}, 1, false};
return true;
case GL_GREEN_INTEGER:
outMapping = {{1, 0, 0, 0}, 1, true};
return true;
case GL_BLUE:
outMapping = {{2, 0, 0, 0}, 1, false};
return true;
case GL_BLUE_INTEGER:
outMapping = {{2, 0, 0, 0}, 1, true};
return true;
case GL_ALPHA:
outMapping = {{3, 0, 0, 0}, 1, false};
return true;
case GL_ALPHA_INTEGER:
outMapping = {{3, 0, 0, 0}, 1, true};
return true;
case GL_RG:
outMapping = {{0, 1, 0, 0}, 2, false};
return true;
case GL_RG_INTEGER:
outMapping = {{0, 1, 0, 0}, 2, true};
return true;
case GL_RGB:
outMapping = {{0, 1, 2, 0}, 3, false};
return true;
case GL_RGB_INTEGER:
outMapping = {{0, 1, 2, 0}, 3, true};
return true;
case GL_BGR:
outMapping = {{2, 1, 0, 0}, 3, false};
return true;
case GL_BGR_INTEGER:
outMapping = {{2, 1, 0, 0}, 3, true};
return true;
case GL_RGBA:
outMapping = {{0, 1, 2, 3}, 4, false};
return true;
case GL_RGBA_INTEGER:
outMapping = {{0, 1, 2, 3}, 4, true};
return true;
case GL_BGRA:
outMapping = {{2, 1, 0, 3}, 4, false};
return true;
case GL_BGRA_INTEGER:
outMapping = {{2, 1, 0, 3}, 4, true};
return true;
default:
return false;
}
}
Bool GetPackedReadbackLayout(GLenum type, PackedReadbackLayout& out) {
switch (type) {
// Non-REV types pack the first format component starting at the most significant bit,
// *_REV types starting at the least significant bit (GL CTS pack_UNSIGNED_SHORT_5_6_5:
// R bits 15-11; pack_UNSIGNED_SHORT_1_5_5_5_REV: R bits 4-0, A bit 15).
case GL_UNSIGNED_BYTE_3_3_2:
out = {3, {3, 3, 2, 0}, {5, 2, 0, 0}, 1, false};
return true;
case GL_UNSIGNED_BYTE_2_3_3_REV:
out = {3, {3, 3, 2, 0}, {0, 3, 6, 0}, 1, false};
return true;
case GL_UNSIGNED_SHORT_5_6_5:
out = {3, {5, 6, 5, 0}, {11, 5, 0, 0}, 2, false};
return true;
case GL_UNSIGNED_SHORT_5_6_5_REV:
out = {3, {5, 6, 5, 0}, {0, 5, 11, 0}, 2, false};
return true;
case GL_UNSIGNED_SHORT_4_4_4_4:
out = {4, {4, 4, 4, 4}, {12, 8, 4, 0}, 2, false};
return true;
case GL_UNSIGNED_SHORT_4_4_4_4_REV:
out = {4, {4, 4, 4, 4}, {0, 4, 8, 12}, 2, false};
return true;
case GL_UNSIGNED_SHORT_5_5_5_1:
out = {4, {5, 5, 5, 1}, {11, 6, 1, 0}, 2, false};
return true;
case GL_UNSIGNED_SHORT_1_5_5_5_REV:
out = {4, {5, 5, 5, 1}, {0, 5, 10, 15}, 2, false};
return true;
case GL_UNSIGNED_INT_8_8_8_8:
out = {4, {8, 8, 8, 8}, {24, 16, 8, 0}, 4, false};
return true;
case GL_UNSIGNED_INT_8_8_8_8_REV:
out = {4, {8, 8, 8, 8}, {0, 8, 16, 24}, 4, false};
return true;
case GL_UNSIGNED_INT_10_10_10_2:
out = {4, {10, 10, 10, 2}, {22, 12, 2, 0}, 4, false};
return true;
case GL_UNSIGNED_INT_2_10_10_10_REV:
out = {4, {10, 10, 10, 2}, {0, 10, 20, 30}, 4, false};
return true;
// Packed-float RGB types: fields hold unsigned small floats; 5_9_9_9_REV's shared 5-bit
// exponent (bits 31-27) is emitted by EncodeSharedExponentRGB9E5, not a component field.
case GL_UNSIGNED_INT_10F_11F_11F_REV:
out = {3, {11, 11, 10, 0}, {0, 11, 22, 0}, 4, true};
return true;
case GL_UNSIGNED_INT_5_9_9_9_REV:
out = {3, {9, 9, 9, 0}, {0, 9, 18, 0}, 4, true};
return true;
default:
return false;
}
}
SizeT GetReadbackComponentSize(GLenum type) {
PackedReadbackLayout packedLayout{};
if (GetPackedReadbackLayout(type, packedLayout)) {
return packedLayout.byteSize;
}
switch (type) {
case GL_UNSIGNED_BYTE:
case GL_BYTE:
return 1;
case GL_UNSIGNED_SHORT:
case GL_SHORT:
case GL_HALF_FLOAT:
return 2;
case GL_UNSIGNED_INT:
case GL_INT:
case GL_FLOAT:
return 4;
default:
return 0;
}
}
SizeT GetReadbackDstPixelSize(const ReadbackChannelMapping& mapping, GLenum type) {
PackedReadbackLayout packedLayout{};
if (GetPackedReadbackLayout(type, packedLayout)) {
if (packedLayout.fieldCount != mapping.channelCount) {
return 0; // 3-field packed types pair with 3-component formats only, 4 with 4
}
if (mapping.isInteger && packedLayout.isFloatPacked) {
return 0; // packed-float RGB types never pair with integer formats
}
return packedLayout.byteSize;
}
if (mapping.isInteger && (type == GL_FLOAT || type == GL_HALF_FLOAT)) {
return 0;
}
const SizeT componentSize = GetReadbackComponentSize(type);
return componentSize == 0 ? 0 : static_cast<SizeT>(mapping.channelCount) * componentSize;
}
namespace {
void WritePackedReadbackWord(Uint8* dst, Uint32 word, SizeT byteSize) {
switch (byteSize) {
case 1: {
const auto out = static_cast<Uint8>(word);
Memcpy(dst, &out, sizeof(out));
break;
}
case 2: {
const auto out = static_cast<Uint16>(word);
Memcpy(dst, &out, sizeof(out));
break;
}
default:
Memcpy(dst, &word, sizeof(word));
break;
}
}
} // namespace
// Shared encoders live in MG_Util/Math/SmallFloat.h so the upload conversion
// (PixelStoreProcessor) uses byte-identical packing; kept exported here for unit tests.
Uint32 EncodeFloatToUnsignedF11(Float value) {
return MG_Util::EncodeFloatToUnsignedF11(value);
}
Uint32 EncodeFloatToUnsignedF10(Float value) {
return MG_Util::EncodeFloatToUnsignedF10(value);
}
Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]) {
return MG_Util::EncodeSharedExponentRGB9E5(rgb);
}
void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType,
const ReadbackChannelMapping& mapping, GLenum type) {
PackedReadbackLayout packedLayout{};
const Bool isPacked = GetPackedReadbackLayout(type, packedLayout);
const SizeT dstComponentSize = GetReadbackComponentSize(type);
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
const SizeT srcPixelBytes = 4 * GetReadbackComponentSize(wideType);
for (SizeT col = 0; col < width; ++col) {
const Uint8* srcPixel = src + col * srcPixelBytes;
Uint8* dstPixel = dst + col * dstPixelBytes;
if (mapping.isInteger) {
Int64 srcValues[4];
for (Int c = 0; c < 4; ++c) {
srcValues[c] = wideType == GL_INT
? static_cast<Int64>(reinterpret_cast<const Int32*>(srcPixel)[c])
: static_cast<Int64>(reinterpret_cast<const Uint32*>(srcPixel)[c]);
}
if (isPacked) {
// Integer sources clamp each component to the unsigned range of its field
// (GL 3.3 section 4.3.1 final conversion).
Uint32 word = 0;
for (Int ch = 0; ch < packedLayout.fieldCount; ++ch) {
const Int64 fieldMax = (Int64{1} << packedLayout.width[ch]) - 1;
const auto v = static_cast<Uint32>(
std::clamp<Int64>(srcValues[mapping.sourceChannel[ch]], 0, fieldMax));
word |= v << packedLayout.shift[ch];
}
WritePackedReadbackWord(dstPixel, word, packedLayout.byteSize);
} else {
for (Int ch = 0; ch < mapping.channelCount; ++ch) {
const Int64 v = srcValues[mapping.sourceChannel[ch]];
Uint8* dstComponent = dstPixel + static_cast<SizeT>(ch) * dstComponentSize;
switch (type) {
case GL_UNSIGNED_BYTE:
*dstComponent = static_cast<Uint8>(std::clamp<Int64>(v, 0, 255));
break;
case GL_BYTE: {
const auto out = static_cast<Int8>(std::clamp<Int64>(v, -128, 127));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_UNSIGNED_SHORT: {
const auto out = static_cast<Uint16>(std::clamp<Int64>(v, 0, 65535));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_SHORT: {
const auto out = static_cast<Int16>(std::clamp<Int64>(v, -32768, 32767));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_UNSIGNED_INT: {
const auto out = static_cast<Uint32>(std::clamp<Int64>(v, 0, 4294967295LL));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_INT: {
const auto out = static_cast<Int32>(std::clamp<Int64>(v, -2147483648LL, 2147483647LL));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
default:
break;
}
}
}
} else {
Float srcValues[4];
switch (wideType) {
case GL_UNSIGNED_BYTE:
for (Int c = 0; c < 4; ++c) {
srcValues[c] = static_cast<Float>(srcPixel[c]) / 255.0f;
}
break;
case GL_BYTE:
for (Int c = 0; c < 4; ++c) {
srcValues[c] = std::max(
static_cast<Float>(reinterpret_cast<const Int8*>(srcPixel)[c]) / 127.0f, -1.0f);
}
break;
case GL_UNSIGNED_SHORT:
for (Int c = 0; c < 4; ++c) {
srcValues[c] = static_cast<Float>(reinterpret_cast<const Uint16*>(srcPixel)[c]) / 65535.0f;
}
break;
case GL_SHORT:
for (Int c = 0; c < 4; ++c) {
srcValues[c] = std::max(
static_cast<Float>(reinterpret_cast<const Int16*>(srcPixel)[c]) / 32767.0f, -1.0f);
}
break;
case GL_HALF_FLOAT:
for (Int c = 0; c < 4; ++c) {
srcValues[c] = DecodeHalfBitsToFloat(reinterpret_cast<const Uint16*>(srcPixel)[c]);
}
break;
default: // GL_FLOAT
for (Int c = 0; c < 4; ++c) {
srcValues[c] = reinterpret_cast<const Float*>(srcPixel)[c];
}
break;
}
if (isPacked) {
Uint32 word = 0;
if (packedLayout.isFloatPacked) {
const Float fields[3] = {srcValues[mapping.sourceChannel[0]],
srcValues[mapping.sourceChannel[1]],
srcValues[mapping.sourceChannel[2]]};
word = type == GL_UNSIGNED_INT_5_9_9_9_REV
? EncodeSharedExponentRGB9E5(fields)
: (EncodeFloatToUnsignedF11(fields[0]) << packedLayout.shift[0]) |
(EncodeFloatToUnsignedF11(fields[1]) << packedLayout.shift[1]) |
(EncodeFloatToUnsignedF10(fields[2]) << packedLayout.shift[2]);
} else {
// Normalized encode: round(clamp(v, 0, 1) * (2^bits - 1)) into each field.
for (Int ch = 0; ch < packedLayout.fieldCount; ++ch) {
const auto fieldMax = static_cast<Float>((1u << packedLayout.width[ch]) - 1u);
const auto v = static_cast<Uint32>(std::llround(
std::clamp(srcValues[mapping.sourceChannel[ch]], 0.0f, 1.0f) * fieldMax));
word |= v << packedLayout.shift[ch];
}
}
WritePackedReadbackWord(dstPixel, word, packedLayout.byteSize);
} else {
for (Int ch = 0; ch < mapping.channelCount; ++ch) {
const Float v = srcValues[mapping.sourceChannel[ch]];
Uint8* dstComponent = dstPixel + static_cast<SizeT>(ch) * dstComponentSize;
switch (type) {
case GL_UNSIGNED_BYTE:
*dstComponent = static_cast<Uint8>(std::llround(std::clamp(v, 0.0f, 1.0f) * 255.0));
break;
case GL_BYTE: {
const auto out = static_cast<Int8>(std::llround(std::clamp(v, -1.0f, 1.0f) * 127.0));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_UNSIGNED_SHORT: {
const auto out = static_cast<Uint16>(std::llround(std::clamp(v, 0.0f, 1.0f) * 65535.0));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_SHORT: {
const auto out = static_cast<Int16>(std::llround(std::clamp(v, -1.0f, 1.0f) * 32767.0));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_UNSIGNED_INT: {
const auto out = static_cast<Uint32>(
std::llround(static_cast<Double>(std::clamp(v, 0.0f, 1.0f)) * 4294967295.0));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_INT: {
const auto out = static_cast<Int32>(
std::llround(static_cast<Double>(std::clamp(v, -1.0f, 1.0f)) * 2147483647.0));
Memcpy(dstComponent, &out, sizeof(out));
break;
}
case GL_FLOAT:
Memcpy(dstComponent, &v, sizeof(v));
break;
case GL_HALF_FLOAT: {
const Uint16 out = EncodeFloatToHalfBits(v);
Memcpy(dstComponent, &out, sizeof(out));
break;
}
default:
break;
}
}
}
}
}
}
static SizeT AlignReadbackRow(SizeT rowBytes, Int alignment) {
const SizeT align = alignment > 0 ? static_cast<SizeT>(alignment) : 1;
return (rowBytes + align - 1) / align * align;
}
// Walks the client-side destination the PACK parameters describe and hands each row to
// `fillRow(slice, row, dstRow)`, which writes width * dstPixelBytes bytes of finished client
// texels. Shared by the converting and the raw-word stores so both address the destination
// identically. The destination is always client memory: a readback into a pack buffer takes
// the verb path and never reaches these stores.
// applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage
// of 3D/array images; ReadPixels and 2D GetTexImage ignore them (GL 3.3 sections 4.3.1, 6.1.4).
// Per the GL addressing rules, slice k row j lands at
// SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes
// + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride.
template <typename FillRow>
static Bool StoreClientRows(SizeT dstPixelBytes, SizeT swapGroupSize, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, void* pixels, Bool applyPackImageParams, FillRow&& fillRow) {
// 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 = 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 = applyPackImageParams && packParams.ImageHeight > 0
? static_cast<SizeT>(packParams.ImageHeight)
: static_cast<SizeT>(sliceHeight);
const SizeT dstImageStride = imageRows * dstRowStride;
const SizeT skipImages =
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
const SizeT dstSkipOffset = skipImages * dstImageStride +
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
Vector<Uint8> convertedRow(dstRowBytes);
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
for (GLsizei row = 0; row < sliceHeight; ++row) {
fillRow(slice, row, convertedRow.data());
if (packParams.SwapBytes && swapGroupSize > 1) {
for (SizeT offset = 0; offset + swapGroupSize <= dstRowBytes; offset += swapGroupSize) {
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + swapGroupSize);
}
}
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
static_cast<SizeT>(row) * dstRowStride;
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
}
}
return true;
}
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
// client-side PACK parameters. `wide` holds
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
// 4 components x GetReadbackComponentSize(wideType) bytes each.
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type, void* pixels,
Bool applyPackImageParams) {
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
if (dstPixelBytes == 0) {
return false;
}
PackedReadbackLayout packedLayout{};
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
const SizeT swapGroupSize = isPackedType ? packedLayout.byteSize : GetReadbackComponentSize(type);
const SizeT srcPixelBytes = 4 * GetReadbackComponentSize(wideType);
return StoreClientRows(
dstPixelBytes, swapGroupSize, width, sliceHeight, sliceCount, pixels, applyPackImageParams,
[&](GLsizei slice, GLsizei row, Uint8* dstRow) {
const SizeT flatRow =
static_cast<SizeT>(slice) * static_cast<SizeT>(sliceHeight) + static_cast<SizeT>(row);
const Uint8* srcRow = wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
ConvertWideReadbackRow(srcRow, dstRow, static_cast<SizeT>(width), wideType, mapping, type);
});
}
Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount,
GLenum type, void* pixels, Bool applyPackImageParams) {
PackedReadbackLayout packedLayout{};
if (!GetPackedReadbackLayout(type, packedLayout) || packedLayout.byteSize != 4) {
return false;
}
const SizeT srcRowBytes = static_cast<SizeT>(width) * 4;
return StoreClientRows(4, packedLayout.byteSize, width, sliceHeight, sliceCount, pixels,
applyPackImageParams, [&](GLsizei slice, GLsizei row, Uint8* dstRow) {
const SizeT flatRow =
static_cast<SizeT>(slice) * static_cast<SizeT>(sliceHeight) +
static_cast<SizeT>(row);
Memcpy(dstRow, srcWords + flatRow * srcRowBytes, srcRowBytes);
});
}
} // namespace ClientReadback
} // namespace MobileGL::MG_Backend
+75
View File
@@ -0,0 +1,75 @@
// MobileGL - MobileGL/MG_Backend/ClientReadback.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
namespace MobileGL::MG_Backend {
// Pure CPU helpers of the client-format readback conversion (ReadPixels/GetTexImage repack a wide
// RGBA(_INTEGER) read into the caller's (format, type) layout). Kept context-free so unit tests can
// exercise the exact packing the GL CTS packed_pixels oracle compares against.
namespace ClientReadback {
struct ReadbackChannelMapping {
Int sourceChannel[4]; // RGBA source channel feeding each destination component
Int channelCount; // destination component count
Bool isInteger;
};
Bool GetReadbackChannelMapping(GLenum format, ReadbackChannelMapping& outMapping);
// Byte size of one destination component of `type`; packed types report the packed word size.
// 0 = type not supported by the conversion path.
SizeT GetReadbackComponentSize(GLenum type);
// Bit-field layout of a GL packed pixel type. width/shift are indexed in the client format's
// component order (matching ReadbackChannelMapping); shift is the LSB position of the field in
// the packed word: non-REV types pack the first component from the MSB, *_REV types from the
// LSB (GL 3.3 table 3.6; field positions mirror the GL CTS glcPackedPixelsTests pack_* oracle).
struct PackedReadbackLayout {
Int fieldCount; // format components stored in the packed word
Int width[4]; // bit width of each component's field
Int shift[4]; // LSB bit position of each component's field
SizeT byteSize; // packed word size in bytes (1, 2 or 4)
Bool isFloatPacked; // 10F_11F_11F_REV / 5_9_9_9_REV: fields hold unsigned small floats
};
Bool GetPackedReadbackLayout(GLenum type, PackedReadbackLayout& out);
// Unsigned small-float encoders (EXT_packed_float / EXT_texture_shared_exponent semantics).
Uint32 EncodeFloatToUnsignedF11(Float value);
Uint32 EncodeFloatToUnsignedF10(Float value);
Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]);
// Destination bytes per pixel for a (format mapping, type) readback pair; 0 when the pair is
// not convertible (unknown type, packed field count != format component count, floating-point
// or packed-float type with an integer format).
SizeT GetReadbackDstPixelSize(const ReadbackChannelMapping& mapping, GLenum type);
// Repacks one row of wide RGBA(_INTEGER) texels (4 components of wideType each) into the
// client's (format, type) layout. src holds width * 4 * GetReadbackComponentSize(wideType)
// bytes, dst receives width * GetReadbackDstPixelSize(mapping, type) bytes.
void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType,
const ReadbackChannelMapping& mapping, GLenum type);
// Stores wide RGBA(_INTEGER) rows into client memory, honoring the client-side PACK
// pixel-store parameters (row length, alignment, skips, swap-bytes, and - when
// applyPackImageParams - image height/skip images). Shared by the DirectGLES and DirectVulkan
// readback conversion paths.
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type, void* pixels,
Bool applyPackImageParams);
// Stores packed 32-bit source words verbatim, with the same destination addressing and PACK
// parameters as StoreWideRowsToClient. For the sources whose storage word already IS the
// client word (MG_Util::IsRawPackedPixelTransfer): routing those through the wide float
// intermediate re-encodes them, and the RGB9_E5 encoder canonicalizes
// the shared exponent, so glGetTexImage would answer with different bits than were stored.
// `srcWords` holds sliceHeight * sliceCount tightly stacked rows of `width` 32-bit words.
// False when `type` is not a 4-byte packed type.
Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount,
GLenum type, void* pixels, Bool applyPackImageParams);
} // namespace ClientReadback
} // namespace MobileGL::MG_Backend
@@ -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)
@@ -50,14 +50,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the recorded tuple; only that exact tuple may skip. Process-wide like the native state it
// mirrors; under split exactly one DirectGLES object exists (the server's), and only the
// apply thread reaches these.
enum class NativeBindState : Uint8 { NotBound, FreshFromSurfaceCreation, BoundForTuple };
enum class NativeBindState : Uint8 {
NotBound,
FreshFromSurfaceCreation,
BoundForTuple
};
NativeBindState g_nativeBindState = NativeBindState::NotBound;
EGLDisplay g_nativeBoundDpy = EGL_NO_DISPLAY;
EGLSurface g_nativeBoundDraw = EGL_NO_SURFACE;
EGLSurface g_nativeBoundRead = EGL_NO_SURFACE;
EGLContext g_nativeBoundCtx = EGL_NO_CONTEXT;
void NoteNativeContextGone() { g_nativeBindState = NativeBindState::NotBound; }
void NoteNativeContextGone() {
g_nativeBindState = NativeBindState::NotBound;
}
void NoteNativeContextFreshFromSurfaceCreation() {
g_nativeBindState = NativeBindState::FreshFromSurfaceCreation;
}
@@ -70,15 +76,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
Bool NativeBindCanBeSkippedFor(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
switch (g_nativeBindState) {
case NativeBindState::NotBound: return false;
case NativeBindState::FreshFromSurfaceCreation: return true;
case NativeBindState::NotBound:
return false;
case NativeBindState::FreshFromSurfaceCreation:
return true;
case NativeBindState::BoundForTuple:
return g_nativeBoundDpy == dpy && g_nativeBoundDraw == draw && g_nativeBoundRead == read &&
g_nativeBoundCtx == ctx;
}
return false;
}
#endif
void ClearGLErrors(const MG_External::GLESFunctionsTable& gl) {
if (!gl.glGetError) return;
@@ -365,8 +372,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// for EVERY colour format while GL_MAX_SAMPLES - ES 3.0 core, so genuinely answered -
// reports 4. Once the frontend validates against this list, that would reject every
// multisample renderbuffer on such a context.
Int GetGLESRenderbufferFormatMaxSamples(const MG_External::GLESCapabilities& capabilities,
GLenum imageFormat) {
Int GetGLESRenderbufferFormatMaxSamples(const MG_External::GLESCapabilities& capabilities, GLenum imageFormat) {
const Bool isInteger = imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER ||
imageFormat == GL_RGB_INTEGER || imageFormat == GL_RGBA_INTEGER;
return isInteger ? capabilities.MaxIntegerSamples : capabilities.MaxSamples;
@@ -455,6 +461,58 @@ 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;
@@ -676,12 +734,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Flags<PixelFormatNormalizeOptionBit> forcedProbeOptions =
(outerHasForcedFallback || widenUnconditionally) ? forcedOptions | renderTargetOptions
: forcedOptions;
hasForcedFallback =
BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedProbeOptions, true,
fallbackInfo);
hasForcedFallback = BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedProbeOptions,
true, fallbackInfo);
if (!hasForcedFallback) {
BuildFallbackProbeFormatInfo(requestedInternalFormat,
driverOptions | renderTargetOptions, false, fallbackInfo);
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions | renderTargetOptions,
false, fallbackInfo);
}
// HONEST STATUS OF THE FORCED PATH. A forced fallback is only ever built
// for ANGLE (GetForcedPixelFormatNormalizeOptions returns nothing for any
@@ -843,7 +900,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 +934,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 +945,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);
}
@@ -906,16 +968,69 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif
}
Bool BackendObject_DirectGLES::RebindNativeSurfaceFor(EGLSurface surface) {
const auto it = m_nativeSurfaces.find(surface);
if (it == m_nativeSurfaces.end()) return false;
if (DirectGLES::BindSessionSurface(it->second)) return true;
// The session no longer owns it (destroyed with a window loss): forget and recreate.
m_nativeSurfaces.erase(it);
return false;
}
void BackendObject_DirectGLES::RestoreSurfaceAfterCreation(EGLSurface previousNativeDraw) {
// The ACTIVE CONTEXT's own surface, exactly: the creation's bind attached the new surface to
// whichever context the session had bound (often another window's thread), and putting back
// "the surface activated last" instead re-attached THAT context to a third window's surface -
// a desktop drawing into and reading back from the panel's pbuffer.
if (previousNativeDraw == EGL_NO_SURFACE) return;
if (!DirectGLES::BindSessionSurface(previousNativeDraw)) return;
for (const auto& [client, native] : m_nativeSurfaces) {
if (native != previousNativeDraw) continue;
const auto state = m_eglSurfaces.find(client);
if (state == m_eglSurfaces.end()) break;
m_eglSurface = client;
m_eglSurfaceInitialized = true;
m_eglSurfaceKind = state->second.Kind;
if (state->second.Kind == SurfaceKind::Window) m_windowHandle = state->second.Window;
break;
}
}
Bool BackendObject_DirectGLES::InitWindowSurface() {
if (RebindNativeSurfaceFor(m_activatingSurface)) return true;
// A suspended surface whose placeholder went: another placeholder - never the window, which
// is gone.
if (std::find(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), m_activatingSurface) !=
m_suspendedWindowSurfaces.end()) {
const EGLSurface placeholder = DirectGLES::CreateSessionPbuffer(1, 1);
if (placeholder == EGL_NO_SURFACE || !DirectGLES::BindSessionSurface(placeholder)) {
MGLOG_E("DirectGLES: no placeholder for the suspended window surface %p",
reinterpret_cast<void*>(m_activatingSurface));
return false;
}
m_nativeSurfaces[m_activatingSurface] = placeholder;
return true;
}
// Only use EGL for now
auto nativeWindow = reinterpret_cast<NativeWindowType>(m_windowHandle.Handle);
if (!DirectGLES::InitWindowSurface(nativeWindow)) {
MGLOG_E("Failed to initialize window surface for DirectGLES backend");
return false;
}
if (m_activatingSurface != EGL_NO_SURFACE) {
m_nativeSurfaces[m_activatingSurface] = DirectGLES::CurrentSessionDrawSurface();
}
return true;
}
Bool BackendObject_DirectGLES::LatchIfGpuFaulted() {
#if MOBILEGL_BUILD_DISAGGREGATED
return LatchIfGuiltyBeforeApply();
#else
return false;
#endif
}
void BackendObject_DirectGLES::Initialize() {
MG_Util::BackendLoader::AcquireEGLFunctions(m_EGLFunctions);
MG_Util::BackendLoader::AcquireGLESFunctions(m_GLESFunctions, m_EGLFunctions.eglGetProcAddress);
@@ -942,6 +1057,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;
@@ -976,8 +1093,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
// ---- P14 S4: A SECOND SURFACE NO LONGER TEARS THE CONTEXT DOWN. ----
//
// This used to be `DestroyEGLContext(); ResetEGLRuntimeState();` whenever a surface was
// created while another was active, i.e. every pbuffer -> window -> pbuffer switch destroyed
// the native context - and with it every buffer, texture and program the application had
// built. The native context now belongs to the (session, context token) tuple the backend
// resolves, and creating a surface for it only adds a surface: the tuple keeps its context
// and its objects. ResetEGLRuntimeState still runs because the base class's per-surface
// bookkeeping (which surface is active, which threads made it current) does describe the
// surface that is being replaced.
const EGLSurface previous = m_eglSurfaceInitialized ? m_eglSurface : EGL_NO_SURFACE;
const EGLSurface previousNativeDraw = DirectGLES::CurrentContextDrawSurface();
if (m_eglSurfaceInitialized) {
DestroyEGLContext();
ResetEGLRuntimeState();
}
@@ -986,7 +1114,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the first virtual tuple adopts that bind. On failure nothing is known to be bound. The
// pull arm below is the original statement, byte for byte (G1).
const Bool created = BackendObject::CreateEGLWindowSurface(surface, handle);
if (created) {
if (created && previous != EGL_NO_SURFACE && previous != surface) {
// Another surface was current: the creation must not leave the context that held it
// drawing to the new one (P14 multi-window clients).
RestoreSurfaceAfterCreation(previousNativeDraw);
NoteNativeContextGone();
} else if (created) {
NoteNativeContextFreshFromSurfaceCreation();
} else {
NoteNativeContextGone();
@@ -1008,28 +1141,57 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
// P14 S4: as for the window surface above - the native context is the (session, context
// token) tuple's and survives a surface switch. This used to be DestroyEGLContext().
const EGLSurface previous = m_eglSurfaceInitialized ? m_eglSurface : EGL_NO_SURFACE;
const EGLSurface previousNativeDraw = DirectGLES::CurrentContextDrawSurface();
if (m_eglSurfaceInitialized) {
DestroyEGLContext();
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);
if (created) {
if (created && previous != EGL_NO_SURFACE && previous != surface) {
RestoreSurfaceAfterCreation(previousNativeDraw);
NoteNativeContextGone();
} else if (created) {
NoteNativeContextFreshFromSurfaceCreation();
} else {
NoteNativeContextGone();
}
return created;
#else
return BackendObject::CreateEGLPbufferSurface(surface, width, height);
#endif
}
Bool BackendObject_DirectGLES::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
const auto state = m_eglSurfaces.find(surface);
const auto native = m_nativeSurfaces.find(surface);
if (state == m_eglSurfaces.end() || state->second.Kind != SurfaceKind::Pbuffer ||
native == m_nativeSurfaces.end()) {
return BackendObject::ResizeEGLWindowSurface(surface, width, height);
}
// A pbuffer has no resize: the client surface gets a new native pbuffer at the new size,
// and every context that drew to the old one draws to the new one.
const EGLint w = static_cast<EGLint>(std::max<Uint32>(width, 1));
const EGLint h = static_cast<EGLint>(std::max<Uint32>(height, 1));
const EGLSurface replacement = DirectGLES::CreateSessionPbuffer(w, h);
if (replacement == EGL_NO_SURFACE) {
MGLOG_E("ResizeEGLWindowSurface: no %dx%d pbuffer could replace the old drawable", w, h);
return false;
}
DirectGLES::ReplaceSessionSurface(native->second, replacement);
native->second = replacement;
return BackendObject::ResizeEGLWindowSurface(surface, width, height);
}
Bool BackendObject_DirectGLES::InitPbufferSurface(EGLint width, EGLint height) {
return DirectGLES::InitPbufferSurface(width, height);
if (RebindNativeSurfaceFor(m_activatingSurface)) return true;
if (!DirectGLES::InitPbufferSurface(width, height)) return false;
if (m_activatingSurface != EGL_NO_SURFACE) {
m_nativeSurfaces[m_activatingSurface] = DirectGLES::CurrentSessionDrawSurface();
}
return true;
}
Bool BackendObject_DirectGLES::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
@@ -1038,9 +1200,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!DirectGLES::ReleaseCurrent()) {
return false;
}
#if MOBILEGL_BUILD_DISAGGREGATED
NoteNativeContextGone();
#endif
return BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
}
@@ -1089,7 +1249,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_eglSurfaceInitialized && m_eglSurface == draw &&
DirectGLES::IsBackendContextCurrentOnThisThread() &&
NativeBindCanBeSkippedFor(dpy, draw, read, ctx);
if (!nativelyCurrentAlready && !DirectGLES::MakeCurrent()) {
// The draw surface's own native surface (P14 multi-window): a known one is re-bound, so the
// bind goes to it and not to whichever surface the session drew to last.
// The real bind still runs whenever the skip above says so (ID-67: a different tuple onto
// the same surface needs MakeCurrent's invalidations even though the driver triple is equal).
const auto nativeDraw = m_nativeSurfaces.find(draw);
if (!nativelyCurrentAlready && nativeDraw != m_nativeSurfaces.end()) {
(void)DirectGLES::SelectSessionSurface(nativeDraw->second);
}
const Bool bound = nativelyCurrentAlready || DirectGLES::MakeCurrent();
if (!bound) {
NoteNativeContextGone();
return false;
}
@@ -1102,14 +1271,47 @@ 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;
}
Bool BackendObject_DirectGLES::BlitDefaultFramebufferToSharedImage(const SharedImageView& image,
const MG_Util::Damage::Region& region) {
#if MOBILEGL_BUILD_DISAGGREGATED
return DirectGLES::SharedImageImpl::BlitDefaultFramebufferTo(image, region);
#else
(void)image, (void)region;
return false;
#endif
}
Int32 BackendObject_DirectGLES::QueryCurrentBufferAge(Bool damageRegionFollows) {
return DirectGLES::CurrentDrawBufferAge(damageRegionFollows);
}
Bool BackendObject_DirectGLES::SetCurrentDamageRegion(const MG_Util::Damage::Region& region) {
return DirectGLES::SetCurrentDrawDamageRegion(region);
}
Bool BackendObject_DirectGLES::PublishSharedImageAccesses() {
#if MOBILEGL_BUILD_DISAGGREGATED
return DirectGLES::SharedImageImpl::PublishPendingAccesses();
#else
return true;
#endif
}
Bool BackendObject_DirectGLES::ExportNativeFence(int* fence) {
*fence = -1;
#if MOBILEGL_BUILD_DISAGGREGATED
return DirectGLES::SharedImageImpl::ExportNativeFence(fence);
#else
return true;
#endif
}
Bool BackendObject_DirectGLES::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
return BackendObject::SwapEGLBuffers(dpy, draw);
}
@@ -1122,18 +1324,111 @@ 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();
}
void BackendObject_DirectGLES::OnEGLSurfaceReleased(EGLSurface surface) {
(void)surface;
DestroyEGLContext();
#if MOBILEGL_BUILD_DISAGGREGATED
Bool BackendObject_DirectGLES::SuspendServerWindow(void* window) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (window == nullptr) return false;
Vector<EGLSurface> onWindow;
for (const auto& [client, state] : m_eglSurfaces) {
if (state.Kind != SurfaceKind::Window || state.Window.Handle != window) continue;
if (std::find(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), client) !=
m_suspendedWindowSurfaces.end())
continue;
onWindow.push_back(client);
}
for (const EGLSurface client : onWindow) {
// A surface never activated has no native surface: only its record names the window.
const auto native = m_nativeSurfaces.find(client);
if (native != m_nativeSurfaces.end()) {
const EGLSurface placeholder = DirectGLES::SuspendSessionWindowSurface(native->second);
if (placeholder == EGL_NO_SURFACE) return false;
native->second = placeholder;
}
m_suspendedWindowSurfaces.push_back(client);
}
// 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);
return true;
}
Bool BackendObject_DirectGLES::ResumeServerWindow(void* window, Uint32 width, Uint32 height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (window == nullptr) return false;
Vector<EGLSurface> suspended;
suspended.swap(m_suspendedWindowSurfaces);
for (const EGLSurface client : suspended) {
const auto state = m_eglSurfaces.find(client);
if (state == m_eglSurfaces.end()) continue; // released while suspended
state->second.Window.Handle = window;
state->second.Window.Width = width;
state->second.Window.Height = height;
state->second.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
state->second.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
if (m_eglSurface == client) m_windowHandle = state->second.Window;
// Never activated: its first activation creates it on the new window.
const auto native = m_nativeSurfaces.find(client);
if (native == m_nativeSurfaces.end()) continue;
const EGLSurface surface =
DirectGLES::ResumeSessionWindowSurface(native->second, reinterpret_cast<NativeWindowType>(window));
if (surface == EGL_NO_SURFACE) return false;
native->second = surface;
}
NoteNativeContextGone();
MGLOG_I("DirectGLES: %zu suspended window surface(s) rebuilt on server window %p (%ux%u)", suspended.size(),
window, width, height);
return true;
}
Bool BackendObject_DirectGLES::ServerWindowResized(void* window, Uint32 width, Uint32 height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (window == nullptr) return false;
Bool published = false;
for (auto& [client, state] : m_eglSurfaces) {
if (state.Kind != SurfaceKind::Window || state.Window.Handle != window) continue;
if (std::find(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), client) !=
m_suspendedWindowSurfaces.end())
continue;
state.Window.Width = width;
state.Window.Height = height;
state.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
state.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
if (m_eglSurface == client) m_windowHandle = state.Window;
// The window surface's buffers follow the window by themselves (from the next one the
// driver dequeues); only the client has to be told.
const auto native = m_nativeSurfaces.find(client);
if (native == m_nativeSurfaces.end()) continue;
published =
DirectGLES::RepublishWindowSurfaceShape(native->second, reinterpret_cast<NativeWindowType>(window)) ||
published;
}
return published;
}
void BackendObject_DirectGLES::OnEGLSurfaceReleased(EGLSurface surface) {
// P14: releasing a surface releases THAT SURFACE (OnEGLSurfaceForgotten), never the
// contexts. This used to be DestroyEGLContext() - every native context of the session, with
// every object the client had built - which a multi-window client hit on its first closed
// popup.
(void)surface;
NoteNativeContextGone();
}
void BackendObject_DirectGLES::OnEGLSurfaceForgotten(EGLSurface surface) {
m_suspendedWindowSurfaces.erase(
std::remove(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), surface),
m_suspendedWindowSurfaces.end());
const auto it = m_nativeSurfaces.find(surface);
if (it == m_nativeSurfaces.end()) return;
const EGLSurface native = it->second;
m_nativeSurfaces.erase(it);
DirectGLES::DestroySessionSurface(native);
}
const RendererInfo& BackendObject_DirectGLES::GetRendererInfo() const {
@@ -1154,24 +1449,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
Bool drawIndirectSupported,
Bool nonZeroIndirectBaseInstanceSupported,
Bool textureViewSupported, Bool cubeMapArraySupported) {
Bool drawIndirectSupported, Bool nonZeroIndirectBaseInstanceSupported,
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
// application feature-detecting off these tokens the opposite of what
// GL_MAJOR_VERSION / GL_MINOR_VERSION told it.
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, V_OpenGL41, V_OpenGL42, V_OpenGL43,
V_OpenGL44, V_OpenGL45, V_OpenGL46,
E_GL_ARB_draw_buffers_blend,
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_texture_storage,
E_GL_ARB_texture_storage_multisample, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters, E_GL_ARB_shader_draw_parameters,
E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack,
E_GL_ARB_vertex_attrib_binding,
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, V_OpenGL41, V_OpenGL42, V_OpenGL43, V_OpenGL44,
V_OpenGL45, V_OpenGL46, E_GL_ARB_draw_buffers_blend,
// The GLSL 1.00/1.10-era ARB trio plus NPOT textures: all core since GL 2.0 and
// trivially true for every context this backend claims (4.6). KWin 6's
// EglContext::checkSupported() gates desktop-GL compositing on exactly these four
// strings, so withholding them fails its renderer bring-up on an otherwise capable
// implementation.
E_GL_ARB_shader_objects, E_GL_ARB_vertex_shader, E_GL_ARB_fragment_shader,
E_GL_ARB_texture_non_power_of_two, E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object,
E_GL_ARB_shader_image_load_store, E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query,
E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample, E_GL_ARB_clear_texture,
E_GL_ARB_direct_state_access, E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding,
// Both are core from GL 3.2/3.3 on and implemented here for
// every advertised version, but an app targeting 3.0/3.1
// only reaches them through the extension string - the CTS
@@ -1260,10 +1560,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.
@@ -1380,7 +1712,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;
@@ -1408,18 +1739,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;
@@ -1486,8 +1811,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BackendObject_DirectGLES::UpdateDynamicBackendParameters() {
m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment;
m_dynamicParameters.ShaderStorageBufferOffsetAlignment =
m_GLESCapabilities.ShaderStorageBufferOffsetAlignment;
m_dynamicParameters.ShaderStorageBufferOffsetAlignment = m_GLESCapabilities.ShaderStorageBufferOffsetAlignment;
m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy;
m_dynamicParameters.AliasedLineWidthRangeMin = m_GLESCapabilities.AliasedLineWidthRangeMin;
m_dynamicParameters.AliasedLineWidthRangeMax = m_GLESCapabilities.AliasedLineWidthRangeMax;
@@ -1593,6 +1917,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 =
@@ -1601,6 +1929,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.
@@ -1628,6 +1957,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;
@@ -1640,11 +1979,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// exercised on a healthy driver (the pinned integration lane); =0 restores the
// detected answer's declines.
m_dynamicParameters.SupportsTessellationPointSize =
m_GLESCapabilities.TessellationPointSizeSupport !=
MG_External::GLESCapabilities::PointSizeTier::None;
m_GLESCapabilities.TessellationPointSizeSupport != MG_External::GLESCapabilities::PointSizeTier::None;
m_dynamicParameters.SupportsGeometryPointSize =
m_GLESCapabilities.GeometryPointSizeSupport !=
MG_External::GLESCapabilities::PointSizeTier::None;
m_GLESCapabilities.GeometryPointSizeSupport != MG_External::GLESCapabilities::PointSizeTier::None;
switch (MG_Config::Features.PointSizeDemotion) {
case MG_Config::QuirkOverride::ForceOn:
MGLOG_I("DirectGLES: MOBILEGL_POINT_SIZE_DEMOTION=1 - treating tessellation/geometry "
@@ -1677,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;
@@ -15,8 +15,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Populates the same format-capability cache used by backend startup. The caller
// must keep the supplied GLES context current for the duration of this call.
void PopulateFormatCapabilities(const MG_External::GLESFunctionsTable& gl,
const MG_External::GLESCapabilities& capabilities,
FormatCapabilityCache& cache);
const MG_External::GLESCapabilities& capabilities, FormatCapabilityCache& cache);
// Clamps a requested sample count down to what the ES driver can really deliver for this
// format on this format-capability target: the probed per-format list when there is one, the
@@ -32,16 +31,38 @@ namespace MobileGL::MG_Backend::DirectGLES {
public:
~BackendObject_DirectGLES() override;
// The driver's verdict on this session's context (DirectGLES::LatchIfDeviceLost).
Bool LatchIfGpuFaulted() override;
void Initialize() override;
Bool InitCapabilities() override;
Bool InitWindowSurface() override;
Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) override;
Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) override;
Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) override;
Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) override;
Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override;
Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override;
// Shared images: a GL blit of default framebuffer 0 into a renderbuffer whose storage is
// the image's EGLImage, ordered after the image's pending reads and published as a native
// fence without a CPU wait (DirectGLES.h, SharedImageImpl).
Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image,
const MG_Util::Damage::Region& region) override;
// A window surface's age is the driver's (EGL_EXT_buffer_age); a pbuffer keeps its content,
// so it is 1 once it has been presented (DirectGLES::CurrentDrawBufferAge).
Int32 QueryCurrentBufferAge(Bool damageRegionFollows) override;
// eglSetDamageRegionKHR on a window surface whose driver has EGL_KHR_partial_update.
Bool SetCurrentDamageRegion(const MG_Util::Damage::Region& region) override;
// A glFlush that ends a shared-image producer's frame (SharedImageImpl::PublishPendingAccesses).
Bool PublishSharedImageAccesses() override;
// An EGL native fence of the session's work (SharedImageImpl::ExportNativeFence).
Bool ExportNativeFence(int* fence) override;
void ReleaseEGLSurface(EGLSurface surface) override;
void ReleaseEGLResources() override;
// The server window going and coming back: each window surface on it swaps its native EGL
// window surface for a placeholder pbuffer and back (DirectGLES::Suspend/ResumeSessionWindowSurface).
Bool SuspendServerWindow(void* window) override;
Bool ResumeServerWindow(void* window, Uint32 width, Uint32 height) override;
Bool ServerWindowResized(void* window, Uint32 width, Uint32 height) override;
const RendererInfo& GetRendererInfo() const override;
String GetBackendAPIVersionString() const override;
@@ -57,8 +78,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
void UpdateDynamicBackendParameters();
Bool InitPbufferSurface(EGLint width, EGLint height) override;
void OnEGLSurfaceReleased(EGLSurface surface) override;
void OnEGLSurfaceForgotten(EGLSurface surface) override;
// An existing native surface for the client surface being activated, re-bound; false when
// there is none yet (the caller creates it).
Bool RebindNativeSurfaceFor(EGLSurface surface);
// EGL: creating a surface does not change what is current. The creation bound the new
// surface natively; put the previous one back for the context that held it.
void RestoreSurfaceAfterCreation(EGLSurface previousNativeDraw);
Bool m_initialized = false;
// P14: ONE NATIVE SURFACE PER CLIENT SURFACE, created on its first activation and re-bound
// on every later one. A multi-window client switches surfaces every frame; making a new
// native surface each time leaked them and lost what the window had drawn.
UnorderedMap<EGLSurface, EGLSurface> m_nativeSurfaces;
// Window surfaces whose window is gone (SuspendServerWindow): their native surface is a
// placeholder pbuffer until ResumeServerWindow.
Vector<EGLSurface> m_suspendedWindowSurfaces;
MG_External::EGLFunctionsTable m_EGLFunctions;
MG_External::GLESFunctionsTable m_GLESFunctions;
MG_External::GLESCapabilities m_GLESCapabilities;
@@ -78,12 +113,13 @@ 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 drawIndirectSupported, Bool nonZeroIndirectBaseInstanceSupported,
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
@@ -1,3 +1,11 @@
// MobileGL - MobileGL/MG_Backend/DirectGLES/ContextEpoch.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
// MobileGL - DirectGLES context-owned static state identities (P5f fs).
#pragma once
@@ -19,5 +27,5 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Bool server = MG_Config::Transport != MG_Config::TransportMode::Monolith;
return {g_backendContextGeneration, server ? MG_Pipe::MGPipeApplierContextSerial() : 0, server};
}
}
} // namespace MobileGL::MG_Backend::DirectGLES
#endif
File diff suppressed because it is too large Load Diff
+353 -27
View File
@@ -23,16 +23,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Content uploads use scratch bindings, so draws and dispatches call this after
// texture synchronization.
void BindCurrentTextures();
#if MOBILEGL_BUILD_DISAGGREGATED
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& bytes);
// Native/server-only color level readback. Returns tightly packed, owned
// Driver-side color/depth level readback. Returns tightly packed, owned
// bytes in the requested pair; never consults a frontend texture or PACK/PBO.
// The caller resolves identity/extent from its resource record and supplies
// whether the SOURCE allocation already uses an image carrier.
// The caller supplies identity and extent, and whether the SOURCE allocation
// already uses an image carrier.
Bool ReadTextureLevelTight(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
TextureInternalFormat logicalFormat, GLint level, const IntVec3& logicalExtent,
Bool sourceUsesImageCarrier, GLenum format, GLenum type, Vector<Uint8>& bytes);
#endif
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
// the texture's last sync - so a re-mint does not read what is not there. A query the driver
// refuses answers true, keeping the caller on its readback path.
Bool NativeTextureLevelHasExtent(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
GLint level, const IntVec3& logicalExtent);
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);
@@ -42,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);
@@ -67,34 +69,18 @@ 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,
GLsizei height);
void CopyImageSubData(const CopyImageEndpoint& src,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const CopyImageEndpoint& dst,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void CopyImageSubData(const CopyImageEndpoint& src, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY,
GLint srcZ, const CopyImageEndpoint& dst, GLenum dstTarget, GLint dstLevel, GLint dstX,
GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
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);
@@ -105,11 +91,72 @@ namespace MobileGL::MG_Backend::DirectGLES {
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
Bool InitWindowSurface(NativeWindowType window);
Bool InitPbufferSurface(EGLint width, EGLint height);
// The calling thread's session's current native draw surface (the one the last Init*Surface
// created, or the last BindSessionSurface named).
EGLSurface CurrentSessionDrawSurface();
// The surface the calling thread's ACTIVE context last drew to (EGL_NO_SURFACE for none).
EGLSurface CurrentContextDrawSurface();
// EGL_BUFFER_AGE_EXT of the session's current draw surface for the next frame: the driver's
// answer for a window surface, 1 for a pbuffer that has been presented (a pbuffer's content stays
// across swaps), 0 otherwise. A driver whose buffer age is only EGL_KHR_partial_update's keeps
// nothing inside the frame's damage region - the whole surface unless one is set - so it is asked
// only when `damageRegionFollows` (SetCurrentDrawDamageRegion before the first draw).
Int32 CurrentDrawBufferAge(Bool damageRegionFollows);
// eglSetDamageRegionKHR(region) on the session's current window surface (a pbuffer, or a driver
// without EGL_KHR_partial_update, ignores it). The driver wants the age asked first in the frame;
// when the client was answered without asking it, it is asked here.
Bool SetCurrentDrawDamageRegion(const MG_Util::Damage::Region& region);
// Makes an EXISTING native surface of the calling thread's session the one it draws to, and
// binds it to the active context. False for a surface the session does not own.
Bool BindSessionSurface(EGLSurface surface);
// The same selection without the bind (the caller binds - MakeCurrent - unconditionally).
Bool SelectSessionSurface(EGLSurface surface);
// Destroys one native surface of the calling thread's session - and only it; the session's
// contexts and other surfaces are untouched.
void DestroySessionSurface(EGLSurface surface);
// A new pbuffer of the calling thread's session, created with the active context's config and
// bound to nothing (EGL: creating a surface changes no binding).
EGLSurface CreateSessionPbuffer(EGLint width, EGLint height);
// Every context of the session that drew to `previous` draws to `replacement` instead, and
// `previous` is destroyed (a pbuffer-backed window's resize).
void ReplaceSessionSurface(EGLSurface previous, EGLSurface replacement);
// The server window going away (screen off): every context of the session that drew to the
// window surface `windowSurface` draws to a new 1x1 placeholder pbuffer instead, and the window
// surface is destroyed - no longer current anywhere once this returns. The contexts and their
// objects stay. Returns the placeholder, or EGL_NO_SURFACE (nothing changed) when none could be
// made.
EGLSurface SuspendSessionWindowSurface(EGLSurface windowSurface);
// The way back: a window surface on `window` replaces `placeholder` (EGL_NO_SURFACE: none to
// replace) for every context that drew to it, is bound, and its extent is published as a
// creation publishes it. Returns the new surface, or EGL_NO_SURFACE.
EGLSurface ResumeSessionWindowSurface(EGLSurface placeholder, NativeWindowType window);
// The window under `surface` changed size: its extent (the display's) is published again, at once.
Bool RepublishWindowSurfaceShape(EGLSurface surface, NativeWindowType window);
Bool MakeCurrent();
Bool ReleaseCurrent();
// True when the backend ES context is current on the calling thread, i.e.
// immediate buffer ops may issue GL calls right now.
Bool IsBackendContextCurrentOnThisThread();
#if MOBILEGL_BUILD_DISAGGREGATED
// DEVICE LOSS, Espryt's half (Magma's is VulkanRenderer::LatchWireDeviceLoss). Asks the
// driver whether the calling thread's context was reset (glGetGraphicsResetStatus); a reset -
// or the debug knob MGPipeDebugDeviceLossDue - latches THIS session through
// MGPipeSessionLatch and answers true, and the caller returns from its verb without issuing
// more GL. Asked at the frame boundaries (Present, a shared-image present), before a readback,
// and by the apply loop each time it wakes (LatchIfGpuFaulted), never per draw. The contexts ask
// for reset notification (NativeContextsNotifyResets) so the driver can name the GUILTY one;
// only GUILTY (or a reset of unknown cause) ends the session - an INNOCENT report, a context
// whose work merely waited behind another's hang (the compositor's among them), is only noted.
// `debugKnob` false: the knob is not consulted here (the on-screen Present, so an injected loss
// lands on an offscreen client's shared-image present or readback, never on the compositor's).
Bool LatchIfDeviceLost(const char* site, Bool debugKnob = true);
// The apply loop's form of it (BackendObject::LatchIfGpuFaulted), asked each time the apply thread
// wakes: the driver's report only, no debug knob, and no EGL ground-truth verification spent.
Bool LatchIfGuiltyBeforeApply();
#endif
// True once the process's native contexts were created with EGL_LOSE_CONTEXT_ON_RESET (decided
// by the first one; the driver must offer EGL_EXT_create_context_robustness).
Bool NativeContextsNotifyResets();
// GL fence sync objects, backed by native ES fences. FenceSync returns null
// (the frontend then falls back to an always-signaled sync) when the calling
// thread does not own the ES context. Waits/queries degrade to "signaled" in
@@ -198,6 +245,216 @@ namespace MobileGL::MG_Backend::DirectGLES {
void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs);
void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs);
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities);
// ---------------------------------------------------------------------------------
// P14 S4 (docs/Disaggregated/design/11-state-ownership.md): THE NATIVE TUPLE IS PER CONTEXT
// ---------------------------------------------------------------------------------
//
// The display is process state and stays one object (eglInitialize once per process, terminated
// when the last native context leaves). The native EGLContext, the config it was created with
// and the native surfaces are keyed by (session, client context token) - the identity S1 put on
// the wire and S2 gave a per-thread owner. Creating a second context therefore no longer tears
// the first one's native context down, which is what lets two contexts (and two share groups)
// be live at once in one process.
// Answers, for the CALLING THREAD, which session it belongs to and which client context token
// that session has bound. Installed once by the server layer; with no resolver installed -
// monolith, a frontend-only process, a unit case - the answer is the single key {0, 0}, which is
// exactly the process singleton this replaces. `false` also means {0,0}.
using NativeContextKeyResolver = Bool (*)(Uint64* outSessionKey, Uint64* outContextToken);
void SetNativeContextKeyResolver(NativeContextKeyResolver resolver);
// ---------------------------------------------------------------------------------
// 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
// context's object namespace; the identity a twin table must be keyed on is therefore
// {session, share group} - the SAME two words the applier's OBJECT RECORDS are filed under
// (PipeApply.h's MGPipeApplierKey, derived by the same probe and the same `0 means its own
// group` rule, so the two agree by construction rather than by two conventions).
//
// Answers, for the CALLING THREAD, which session it belongs to and which share group that
// session's current context is in. Installed once by the server layer beside the native and
// applier probes; with no probe installed - or on a thread that is no session's own, which
// is the client process, the in-process client+server shape and every unit case - the
// answer is the single key {0, 0}, exactly the process-wide table this replaces.
struct TwinKey {
Uint64 SessionKey = 0;
Uint64 ShareGroupKey = 0;
Bool operator==(const TwinKey& other) const {
return SessionKey == other.SessionKey && ShareGroupKey == other.ShareGroupKey;
}
Bool operator!=(const TwinKey& other) const { return !(*this == other); }
};
struct TwinKeyHash {
SizeT operator()(const TwinKey& key) const {
// The 64-bit mix SlotAllocator and the applier's registry already use for their own
// pair keys, so every keyed table in this tree hashes a session the same way.
Uint64 mixed = key.SessionKey ^
(key.ShareGroupKey + 0x9E3779B97F4A7C15ull + (key.SessionKey << 6) + (key.SessionKey >> 2));
mixed ^= mixed >> 33;
mixed *= 0xFF51AFD7ED558CCDull;
mixed ^= mixed >> 33;
return static_cast<SizeT>(mixed);
}
};
using TwinKeyResolver = Bool (*)(TwinKey* outKey);
void SetTwinKeyResolver(TwinKeyResolver resolver);
// 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);
// 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
// context to eglCreateContext as the share argument - which is what makes two contexts see one
// set of buffer/texture objects. Idempotent: a token that already has one answers true.
Bool CreateNativeContextFor(Uint64 contextToken, Uint64 shareGroupToken);
// Destroys ONE context's native tuple (the DestroyContext control frame). The session's other
// contexts are untouched; the display is terminated only when the last context in the process
// has gone.
void DestroyNativeContextFor(Uint64 contextToken);
// ---- objects that belong to ONE native context ----
//
// Framebuffers, vertex arrays, transform feedbacks and queries are container objects: GL never
// shares them, not even between contexts of one share group, and every context numbers its own
// from 1. A process-wide scratch FBO id minted in one context and bound in another therefore
// names THAT context's own framebuffer - often an application's - and the scratch path's
// attach/scrub then rewires it onto staging textures. With one process serving several
// sessions (each with its own native contexts) that silently corrupted other clients' frames.
//
// 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.
//
// 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:
PerNativeContextBase();
PerNativeContextBase(const PerNativeContextBase&) = delete;
PerNativeContextBase& operator=(const PerNativeContextBase&) = delete;
// The context died, and its names with it: forget them (no GL call - there is no context
// left to make one in).
virtual void ForgetContext(Uint64 serial) = 0;
protected:
~PerNativeContextBase();
// This instance's entry in NativeContextThreadState::Cache.
const Uint32 m_cacheSlot;
};
// Called where a native context is destroyed.
void ForgetNativeContextObjects(Uint64 serial);
// Moves whenever any per-context entry is dropped; a thread's cached entry is valid while
// it has not moved.
Uint64 PerNativeContextEpoch();
void BumpPerNativeContextEpoch();
// One T per native context, created on first use from that context.
template <typename T>
class PerNativeContext final : public PerNativeContextBase {
public:
T& Current() {
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 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 *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};
return entry;
}
template <typename F>
void ForEach(F&& visit) {
const std::lock_guard<std::mutex> lock(m_mutex);
for (auto& entry : m_bySerial)
visit(entry.second);
}
void Clear() {
const std::lock_guard<std::mutex> lock(m_mutex);
BumpPerNativeContextEpoch();
m_bySerial.clear();
}
void ForgetContext(Uint64 serial) override {
const std::lock_guard<std::mutex> lock(m_mutex);
BumpPerNativeContextEpoch();
m_bySerial.erase(serial);
}
private:
std::mutex m_mutex;
std::unordered_map<Uint64, T> m_bySerial;
};
// The GL name of one per-context object, spelled like the plain `GLuint` it replaces: it reads
// as the CURRENT context's name, `= 0` resets that one, and `&name` is where glGen* writes it.
class NativeContextName {
public:
operator GLuint() const { return m_names.Current(); }
NativeContextName& operator=(GLuint name) {
m_names.Current() = name;
return *this;
}
GLuint* operator&() { return &m_names.Current(); }
private:
mutable PerNativeContext<GLuint> m_names;
};
// P14 S4. THE NATIVE HALF OF A CONTEXT SWITCH. `bind_context` moves which context the session's
// records belong to; this makes the calling thread's native tuple follow it, binding the
// session's current surface to the newly named context's native EGLContext. A no-op when the
// named context has no native context yet (the session never sent CreateContext) or when the
// 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();
// 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();
// 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();
Uint64 NativeContextCountForSession(Uint64 sessionKey);
// The calling thread's session, as the resolver above reports it (0 with no resolver).
Uint64 CurrentNativeSessionKey();
// ---- Test observables (P14 S4). EGL exposes no "which EGLContext does this client context
// hold" query and no way to ask a context for its share group, so a case that must tell "the
// same native context still serves" from "a fresh one was built", or a sharing pair from two
// unrelated contexts, reads it here. These are pure reads plus one key override; neither is on
// a production path and neither changes behaviour.
EGLContext NativeContextHandleFor(Uint64 sessionKey, Uint64 contextToken);
// Overrides the key the calling thread resolves to, for the life of the call. The backend's
// ordinary answer comes from the server layer's probe; this is how a case drives exactly the
// tuple it means when no session runtime is installed.
void ForceNativeContextKeyForTesting(Uint64 sessionKey, Uint64 contextToken);
void DestroyEGLContext();
// Transform feedback capture spans, performed by the real ES driver. The
@@ -241,8 +498,77 @@ namespace MobileGL::MG_Backend::DirectGLES {
// answers from the frontend state, which this function only reads.
void SyncRenderState(Bool forColorClear = false);
void InvalidateSyncedRenderState();
// GL_SCISSOR_TEST as the driver has it in the native context current on this thread -
// what a guard that turns the test off around its own blit, and puts it back, needs to
// know. The shadow answers only while it describes this context (synced since the last
// invalidation, in this context epoch); otherwise the driver is asked. The shadow is one
// per process: once another session's context synced into it, or a context switch
// invalidated it, its bits are some other context's.
Bool DriverScissorTestEnabled();
// TESTING: the shadow as SyncRenderState leaves it in the current context epoch, with
// GL_SCISSOR_TEST `enabled`.
void SetSyncedScissorTestForTesting(Bool enabled);
// TESTING: `inside` runs within the guard the shared-image present and the readbacks
// copy under (ScopedScissorDisable).
void RunScissorDisabledForTesting(void (*inside)(void*), void* data);
} // namespace RenderStateImpl
#if MOBILEGL_BUILD_DISAGGREGATED
// ---- SHARED IMAGES (docs/archive/notes/anland/plan-ahb-dmabuf.md) ----
//
// A shared image reaches the driver only through the Android import path: its AHardwareBuffer
// becomes an EGLImage (EGL_ANDROID_get_native_client_buffer + EGL_ANDROID_image_native_buffer)
// and that EGLImage the storage of a texture or renderbuffer (GL_OES_EGL_image). On a host
// build there is no such path and every entry below declines.
namespace SharedImageImpl {
// One EGLImage over one shared image, holding the image's registry reference for as long
// as the EGLImage lives (SharedImageRegistry.h's lifetime rule). The last reference
// destroys the EGLImage, unless the display that made it has been terminated since (which
// destroyed it already). Opaque outside DirectGLES.cpp.
struct EglImage;
using EglImageRef = SharedPtr<EglImage>;
// An EGLImage for the live shared image `id`, or null with `why`. Needs a current context
// (the first call resolves the extension entry points).
EglImageRef CreateEglImage(Uint64 id, String& why);
// glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image) on what the active unit has bound to
// GL_TEXTURE_2D. False when the driver refused it (its GL error is consumed).
Bool TargetBoundTexture2D(const EglImageRef& image);
// BackendObject::BlitDefaultFramebufferToSharedImage, for the calling thread's context.
Bool BlitDefaultFramebufferTo(const SharedImageView& image, const MG_Util::Damage::Region& region);
// THE READER'S HALF of the image's sync state (SharedImageRegistry.h). Called on every use
// of a name whose level 0 is `image`, before the command that samples it: when a write
// landed since this context last waited, the context's later commands wait for its fence
// on the GPU; and the image is noted as read by the calling session's current frame.
void AcquireForSampling(const EglImageRef& image);
// YUV IMAGES (NV12/P010, MG_Remote/Server/SharedImageYuv.h) are never a name's level 0: the
// name keeps its own RGBA8 storage `target` (width x height), into which the image is
// converted at its first use in each of the calling session's frames - the image bound as
// an external texture, one triangle drawn - after the registry's CPU copy for a foreign
// buffer. Every touched binding is put back. Called after AcquireForSampling.
Bool IsYuv(const EglImageRef& image);
void ConvertYuvForSampling(const EglImageRef& image, GLuint target, Uint32 width, Uint32 height);
// The calling session's frame boundary for what it has read: hands a fence for everything
// the current context has submitted to every image noted since the last boundary. Called
// at Present and wherever the current native context stops being current (its reads can
// only be fenced from it). Nothing noted = nothing done.
void PublishPendingReads();
// IMPLICIT SYNC BY FLUSH (BackendObject::PublishSharedImageAccesses): everything the
// calling session used since its last boundary is published as written and read by one
// fence, and the session's first use of an image in each frame from now on also waits for
// the image's pending reads.
Bool PublishPendingAccesses();
// EGL_ANDROID_native_fence_sync's fence command (BackendObject::ExportNativeFence): a
// sync_file of everything the current context submitted in `*fence` (-1: none, and the work
// is waited out here), and - for a session that writes images - the boundary
// PublishPendingAccesses makes.
Bool ExportNativeFence(int* fence);
} // namespace SharedImageImpl
#endif
extern MG_External::EGLFunctionsTable g_EGLFuncs;
extern MG_External::GLESFunctionsTable g_GLESFuncs;
extern MG_External::GLESCapabilities g_GLESCapabilities;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,707 @@
// MobileGL - MobileGL/MG_Backend/DirectGLES/DirectGLESBuffers.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "DirectGLESInternal.h"
namespace MobileGL::MG_Backend::DirectGLES {
namespace BufferImpl {
// See Managers.h for why these two are named functions over the applier state and
// nothing else.
Uint ResolveDrawVertexBuffersFromRecord(const MG_Pipe::MGPipeApplierState& st, DrawVertexBufferRequest* out,
Uint capacity) {
Uint count = 0;
const Uint32 begin = st.VertexBufferStart;
const Uint32 end = begin + st.VertexBufferCount;
for (Uint32 i = begin; i < end && i < MG_Pipe::kMGPipeMaxVertexAttribs; ++i) {
const MG_Pipe::MGPVertexBuffer& binding = st.VertexBuffers[i];
if (MG_Pipe::MGPipeHandleIsNull(binding.Res)) continue;
// A {slot, gen} compare, not a raw address compare: an address a successor
// object can reproduce is the identity hazard this phase exists to remove.
Bool alreadySeen = false;
for (Uint j = 0; j < count; ++j) {
if (out[j].Res == binding.Res) {
alreadySeen = true;
break;
}
}
if (alreadySeen) continue;
if (count >= capacity) {
MGLOG_E_ONCE("MGPipe: the vertex-buffer window named more distinct buffers than "
"a draw can hold (%u); the tail is dropped",
capacity);
break;
}
out[count].Res = binding.Res;
out[count].BindingIndex = binding.BindingIndex;
++count;
}
return count;
}
DrawIndexBufferRequest ResolveDrawIndexBufferFromRecord(const MG_Pipe::MGPipeApplierState& st) {
DrawIndexBufferRequest request;
request.Res = st.IndexBuffer.Res;
request.Serial = st.IndexBufferSerial;
return request;
}
// True from a draw or dispatch that ran with a storage buffer bound until the next
// barrier that orders storage-buffer writes. Every bound storage point counts as written,
// the client's own conservative set (see serverGpuWritten below).
//
// WHAT IT IS FOR: two dispatches with no barrier between them that write DIFFERENT
// components of one vector in the same storage buffer. GL leaves the two unordered, which
// allows them to run concurrently, but every write still lands - they are separate memory
// locations. A driver that stores a vector component as a read-modify-write of the whole
// vector loses one of them instead: KHR-GL43.shader_storage_buffer_object.
// advanced-switchBuffers-cs writes digest.x .. digest.w of one uvec4 from four
// back-to-back dispatches and read back a zero component in 8 runs of 20 on Adreno (0 of
// 20 with a storage barrier between the dispatches, 0 of 20 with uint[4]). The fix is the
// barrier the application did not owe: PrepareForCompute issues it when the previous
// shader work may have written a storage buffer and nothing ordered those writes since.
//
// Compute only. A dispatch that follows a dispatch is the shape the test has and the one
// a driver may overlap; draws keep their own ordering and are not taxed, they only SET
// the flag here - one store in the walk that already visits every bound point.
Bool g_shaderStorageWritesUnordered = false;
// The record arm of SyncBufferBindingPoints. Same shape, same order, same branches; the
// four frontend reads become the applier's window and a handle per entry.
//
// WHAT THE WINDOW IS: ShaderBufferCount[class] starting at ShaderBufferStart[class],
// which is the client's touched high-water mark clamped to the 84 the wire carries
// (ruling 10). It is NOT clamped on the client to the device's ceiling, so the ES clamp
// below stays exactly where it was - a client reading GL_MAX_UNIFORM_BUFFER_BINDINGS
// would be answering a driver question from the wrong side of the wire.
//
// WHOLE-VS-RANGE IS `Offset == 0 && Size == kMGPipeWholeBuffer` and obj->GetSize()
// disappears from this function: a base binding does not freeze an extent, the client
// deliberately does not resolve one, and the re-resolution happens HERE against the
// server's own storage - which is what makes a glBufferData issued between the emission
// and this apply bind the NEW extent, as GL does.
void SyncBufferBindingPointsByRecord(Uint32 shaderBufferClass, GLenum glTarget) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier();
const Uint32 start = st.ShaderBufferStart[shaderBufferClass];
SizeT end = static_cast<SizeT>(start) + st.ShaderBufferCount[shaderBufferClass];
// ...and never past what the ES driver itself can hold, exactly as the frontend
// walk does and for the same reason recorded there.
if (glTarget == GL_UNIFORM_BUFFER && g_GLESCapabilities.MaxUniformBufferBindings > 0) {
end = std::min(end, static_cast<SizeT>(g_GLESCapabilities.MaxUniformBufferBindings));
}
for (SizeT i = start; i < end; ++i) {
const MG_Pipe::MGPBufferRange& entry = st.BoundShaderBuffers[shaderBufferClass][i];
if (MG_Pipe::MGPipeHandleIsNull(entry.Res)) {
BindBufferBaseCached(glTarget, static_cast<GLuint>(i), 0);
continue;
}
auto* backendResource = EnsureBufferResourceForHandle(nullptr, entry.Res);
if (!backendResource || backendResource->id == 0) {
MGLOG_E_ONCE("No backend buffer found for %s binding point %zu (handle {%u, %u}).",
MG_Util::ConvertGLEnumToString(glTarget).c_str(), i, entry.Res.Slot, entry.Res.Gen);
continue;
}
// P8-C: the SERVER's half of the mark MarkShaderStorageBuffersGpuWritten no longer
// makes under a transport - on this side's twin, for this side's CPU readers
// (SplitHostBytesForCpuRead). Every bound storage point, which is the client's
// own conservative set (ShaderBufferEmit.h marks every one writable); a uniform
// point is read-only by definition.
if (shaderBufferClass != MG_Pipe::kMGPipeShaderBufferClassUniform) {
backendResource->serverGpuWritten = true;
g_shaderStorageWritesUnordered = true;
}
const auto backendBufferId = backendResource->id;
if (entry.Offset == 0 && entry.Size == MG_Pipe::kMGPipeWholeBuffer) {
BindBufferBaseCached(glTarget, static_cast<GLuint>(i), backendBufferId);
} else {
// CLAMPED AGAINST THE SERVER's OWN STORAGE, which is what the frontend walk
// clamped against obj->GetSize() for: a range that outruns the storage is a
// GL_INVALID_VALUE the driver would raise on a bind the application already
// made legally against a buffer that has since shrunk.
const SizeT storage = backendResource->storageSize;
const SizeT rangeStart = std::min<SizeT>(static_cast<SizeT>(entry.Offset), storage);
const SizeT rangeEnd = std::min<SizeT>(static_cast<SizeT>(entry.Offset + entry.Size), storage);
BindBufferRangeCached(glTarget, static_cast<GLuint>(i), backendBufferId,
static_cast<GLintptr>(rangeStart),
static_cast<GLsizeiptr>(rangeEnd - rangeStart));
}
}
}
void SyncAtomicCounterBuffers(const Vector<Int>& glBindings, Int esslBindingTop) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// P5e (sb, §5.6): THE RECORD ARM. `glBindings` stays the backend program's own list
// - it is server-owned since the link and names which GL counter bindings THIS
// program declares - and only the points themselves move sides. The bound is the
// applier's window instead of GetBufferBindingPointCount's sticky forward, which is
// the row this site was the Espryt reader of.
const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier();
const Uint32 windowStart = st.ShaderBufferStart[MG_Pipe::kMGPipeShaderBufferClassAtomicCounter];
const SizeT windowEnd =
static_cast<SizeT>(windowStart) + st.ShaderBufferCount[MG_Pipe::kMGPipeShaderBufferClassAtomicCounter];
for (const Int glBinding : glBindings) {
if (glBinding < 0) continue;
const Int esslBinding = esslBindingTop - glBinding;
// Already diagnosed once when the block was transpiled; nothing was bound to
// it there either, so there is nothing to unbind here.
if (esslBinding < 0) continue;
// OUTSIDE THE WINDOW IS "NOTHING BOUND", which is what the frontend array's
// default says too - so it takes the unbind arm rather than being skipped.
// Skipping would leave whatever the driver last had at that ESSL point,
// which is the hole the frontend walk's `continue` never had because its
// bound was the whole 84-point array.
const MG_Pipe::MGPBufferRange* entry = nullptr;
if (static_cast<SizeT>(glBinding) >= windowStart && static_cast<SizeT>(glBinding) < windowEnd) {
entry = &st.BoundShaderBuffers[MG_Pipe::kMGPipeShaderBufferClassAtomicCounter]
[static_cast<SizeT>(glBinding)];
}
if (entry == nullptr || MG_Pipe::MGPipeHandleIsNull(entry->Res)) {
BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding), 0);
continue;
}
auto* backendResource = EnsureBufferResourceForHandle(nullptr, entry->Res);
if (!backendResource || backendResource->id == 0) {
MGLOG_E_ONCE("No backend buffer found for atomic counter binding point %d "
"(handle {%u, %u}).",
glBinding, entry->Res.Slot, entry->Res.Gen);
continue;
}
if (entry->Offset == 0 && entry->Size == MG_Pipe::kMGPipeWholeBuffer) {
BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding), backendResource->id);
} else {
const SizeT storage = backendResource->storageSize;
const SizeT rangeStart = std::min<SizeT>(static_cast<SizeT>(entry->Offset), storage);
const SizeT rangeEnd = std::min<SizeT>(static_cast<SizeT>(entry->Offset + entry->Size), storage);
BindBufferRangeCached(GL_SHADER_STORAGE_BUFFER, static_cast<Uint>(esslBinding), backendResource->id,
static_cast<GLintptr>(rangeStart),
static_cast<GLsizeiptr>(rangeEnd - rangeStart));
}
// AND NO MarkBufferGpuWritten HERE. It was the second of the three backend
// GPU-write sites and it reached into a client object; the client's
// MarkAtomicCounterBindings (GpuWritePending.cpp) already marks the same set
// from MarkGpuWritesForDraw, WIDER on purpose - it marks every touched
// counter point rather than only the ones this program declares, which is
// the safe direction for an over-approximate set.
//
// P8-C: the server's own twin is marked instead, for the server's own CPU
// readers (SplitHostBytesForCpuRead) - a counter buffer later bound as a
// parameter or element buffer is read on this side.
backendResource->serverGpuWritten = true;
}
return;
}
} // namespace BufferImpl
namespace XfbImpl {
// One scratch ES buffer serves every scattered capture: only one span can be
// recording at a time (the driver would reject a second Begin), so its contents
// are consumed by the End that follows.
GLuint g_scatterBufferId = 0;
SizeT g_scatterBufferSize = 0;
UnorderedMap<GLuint, XfbObjectState> g_xfbObjects;
GLuint g_currentXfbName = 0;
// Cached address of g_xfbObjects[g_currentXfbName]: PrepareForDraw consults
// CurrentXfb on EVERY draw (StartPendingTransformFeedback) and the map
// lookup was pure per-draw overhead for the overwhelmingly common no-capture
// case. Open addressing keeps values in the bucket array, so ANY insert can
// rehash and move them - and erase moves them too, by shifting the rest of the
// probe cluster into the hole, which reaches entries other than the erased one.
// Every site that mutates the map or rebinds the current name resets this to
// null instead of reasoning about stability, and CurrentXfb re-resolves lazily.
XfbObjectState* g_currentXfbState = nullptr;
// P13 W6: one role - the record arm's (the frontend arm's GL-name keyed state went).
Array<RoleXfbState, 1> g_roleXfbState;
namespace {
RoleXfbState& ActiveXfbState() {
auto& state = g_roleXfbState[0];
if (state.NativeGeneration != g_backendContextGeneration) {
state = {};
state.NativeGeneration = g_backendContextGeneration;
}
return state;
}
} // namespace
XfbObjectState& CurrentXfb() {
auto& state = ActiveXfbState();
const Uint64 lifetime = MG_Pipe::MGPipeApplier().BoundStreamOutputLifetimeId;
if (state.Current != lifetime || state.Cached == nullptr || state.NeedsBind)
BindTransformFeedback(0); // identity is the applier's lifetime, not this argument
auto& g_currentXfbState = state.Cached;
auto& g_xfbObjects = state.Objects;
const auto g_currentXfbName = state.Current;
if (g_currentXfbState == nullptr) {
g_currentXfbState = &g_xfbObjects[g_currentXfbName];
}
return *g_currentXfbState;
}
namespace {
Bool AreTransformFeedbackObjectsSupported() {
return g_GLESFuncs.glGenTransformFeedbacks != nullptr &&
g_GLESFuncs.glBindTransformFeedback != nullptr &&
g_GLESFuncs.glDeleteTransformFeedbacks != nullptr &&
g_GLESFuncs.glPauseTransformFeedback != nullptr &&
g_GLESFuncs.glResumeTransformFeedback != nullptr;
}
// How wide one OnBufferWriteback may be for a capture range of `rangeBytes`, which is
// the whole range when the transport imposes no limit (monolith, where the callback is
// a direct call with no ring under it) or when the range already fits one.
//
// BOTH XFB WRITEBACK PRODUCERS ASK THE SAME FUNCTION. They post from different sources
// - one from a live glMapBufferRange mapping, one from a staged Vector<Uint8> - but
// they post the same KIND of record into the same ring, and a second spelling of the
// width is how one of them ends up being the producer that still kills the server.
SizeT XfbWritebackSliceStep(SizeT rangeBytes) {
const Uint64 slice = MG_Pipe::MGPipeBufferWritebackSliceBytes();
if (slice == 0 || slice >= static_cast<Uint64>(rangeBytes)) return rangeBytes;
return static_cast<SizeT>(slice);
}
// Mirrors one capture span's results into the frontend CPU shadows. The GPU wrote
// the capture buffers behind the frontend's back, so the shadows that back
// MapBuffer/GetBufferSubData still hold the pre-draw bytes. Buffers whose storage
// the backend already owns (coherent persistent map) need nothing: reads resolve
// against that storage directly.
void ReadbackCapturedRanges(Vector<XfbCaptureTarget>& targets) {
if (g_GLESFuncs.glMapBufferRange == nullptr || g_GLESFuncs.glUnmapBuffer == nullptr) {
MGLOG_E_ONCE("EndTransformFeedback: the ES driver exposes no glMapBufferRange/glUnmapBuffer, so "
"captured data can never reach the application's buffers");
}
if (targets.empty()) {
// The span closed with nothing to mirror back. Either the deferred Begin
// never ran (a span with no draw - legal) or it ran and found no bound
// capture buffer, which is not.
MGLOG_D("EndTransformFeedback: capture span closed with no recorded targets");
}
if (g_GLESFuncs.glMapBufferRange != nullptr && g_GLESFuncs.glUnmapBuffer != nullptr) {
for (const auto& target : targets) {
// P5c: under an active transport the frontend object is client
// memory (rule E), so the persistence question is the server
// resource's and the captured bytes go back as a writeback EVENT -
// WritebackFromBackend from the apply thread is the R1/R2 shape.
const MG_Pipe::MGPipeHandle res = target.handle;
auto* resource = BufferImpl::FindBufferResourceForHandle(res);
if (resource == nullptr) continue;
if (resource->persistentMapped) {
// P8-C: nothing to copy - the capture reaches a coherent map -
// but this side's CPU readers must finish before reading it.
resource->serverGpuWritten = true;
continue;
}
const SizeT size = target.end - target.start;
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, target.backendId);
void* mapped = g_GLESFuncs.glMapBufferRange(BufferImpl::TempBufferTarget,
static_cast<GLintptr>(target.start),
static_cast<GLsizeiptr>(size), GL_MAP_READ_BIT);
if (mapped == nullptr) {
MGLOG_E_ONCE("EndTransformFeedback: failed to map backend buffer %u [%zu, %zu) for "
"capture readback (ES error %s); the captured data will NOT be visible to "
"the application",
target.backendId, target.start, target.end,
MG_Util::ConvertGLEnumToString(TakeXfbDriverError()).c_str());
continue;
}
if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback != nullptr) {
// ONE EVENT PER SLICE, NOT ONE PER CAPTURE. The bytes travel
// INLINE in a SEG_EVENT record and the ring refuses any record
// above half its capacity outright, so a whole-range post of a
// capture wider than ~128 KiB was Fatal{EventRingOverflow} - the
// SERVER dying on a large buffer. The slicing shape is
// BufferObject.cpp's one ring over: in-order delivery makes the
// last slice's landing imply every earlier one, so the client
// needs nothing new to reassemble them.
//
// INSIDE THE MAPPING, deliberately: the map is the expensive part
// and the slices are reads of one live pointer, so this unmaps
// once below however many events it posts.
const SizeT step = XfbWritebackSliceStep(size);
for (SizeT off = 0; off < size; off += step) {
const SizeT bytes = std::min(step, size - off);
MG_Pipe::gMGPipeCallbacks.OnBufferWriteback(
res, target.start + off,
MG_Pipe::MGPBlobRef{reinterpret_cast<Uint64>(static_cast<Uint8*>(mapped) + off),
static_cast<Uint64>(bytes), MG_Pipe::kMGHostSpanSegNone, 0});
}
} else {
MGLOG_E_ONCE("EndTransformFeedback: no reverse channel is installed, so the "
"captured bytes of buffer {%u,%u} cannot reach the client shadow",
res.Slot, res.Gen);
}
// P8-C: the server's own staged copy takes the same bytes, from the
// same mapping, so this side's CPU readers see the capture too.
BufferImpl::StageServerWrittenRange(*resource, res, mapped, target.start, size);
g_GLESFuncs.glUnmapBuffer(BufferImpl::TempBufferTarget);
// Ops_H_Readback's order: writeback, unmap, THEN the serial
// stamp, so the next draw does not re-upload the server's stale
// staged bytes over the capture that just landed.
resource->syncedChangeSerial = BufferImpl::ResourceSerialForHandle(res);
BufferImpl::BumpBufferMutationEpoch();
continue;
}
}
targets.clear();
}
// Distributes the gap-free records the driver captured into the application's
// buffers at the offsets the GL layout asks for. Only the bytes a varying actually
// occupies are written, so the holes gl_SkipComponents asks for keep whatever the
// application had put there - which is the whole point of the feature.
void ScatterCapturedRecords(XfbObjectState& xfb) {
auto& g_scatterBufferId = xfb.serverScatterBuffer;
const XfbProgramSource program{xfb.scatterProgram, xfb.scatterArchive};
if (!program || xfb.targets.empty()) return;
if (g_GLESFuncs.glMapBufferRange == nullptr || g_GLESFuncs.glUnmapBuffer == nullptr) return;
const SizeT packedStride = program->GetTransformFeedbackPackedStride();
const SizeT modelledVertices =
static_cast<SizeT>(MG_Pipe::gPipeInputs->GetTransformFeedbackCapturedVertices());
const SizeT vertices = std::min<SizeT>(modelledVertices, xfb.scatterCapacityVertices);
if (packedStride == 0 || vertices == 0) {
// The scatter path redirected the DRIVER's capture into the scratch buffer,
// so bailing here leaves the application's buffers holding their pre-draw
// bytes - a total data loss, not a no-op. The vertex count is the CPU model
// (AccountTransformFeedbackPrimitives), which is 0 for any draw mode
// CountPrimitivesForDraw does not know and for the instanced/indirect entry
// points that never call it.
MGLOG_E_ONCE("EndTransformFeedback: scattered capture discarded - packedStride=%zu, "
"CPU-modelled captured vertices=%zu, scratch capacity=%zu. The capture buffers keep "
"their pre-draw contents.",
packedStride, modelledVertices, xfb.scatterCapacityVertices);
return;
}
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_scatterBufferId);
const void* packed = g_GLESFuncs.glMapBufferRange(
BufferImpl::TempBufferTarget, 0, static_cast<GLsizeiptr>(packedStride * vertices), GL_MAP_READ_BIT);
if (packed == nullptr) {
MGLOG_E_ONCE("EndTransformFeedback: failed to map the scatter capture buffer");
return;
}
// One staged copy per destination buffer: start from what the application had
// (the shadow is authoritative - uploads go shadow -> ES, and previous captures
// were mirrored back into it), patch the captured varyings in, then push the
// whole range down once.
for (SizeT targetIndex = 0; targetIndex < xfb.targets.size(); ++targetIndex) {
const auto& target = xfb.targets[targetIndex];
// By BUFFER index, not by position in the compacted list - see XfbCaptureTarget.
const SizeT stride = program->GetTransformFeedbackStride(static_cast<Uint32>(target.bufferIndex));
if (stride == 0) continue;
const SizeT rangeBytes = target.end - target.start;
Vector<Uint8> staged(rangeBytes);
// P5c: under an active transport BufferObject::MappedData on the apply
// thread is Fatal{RoleViolation, "buffer-legacy-arm"} (CONTRACT-P5C §3.8) -
// the pre-capture bytes are the SERVER's staged shadow, and the reconciled
// range goes back as a writeback EVENT instead of a WritebackFromBackend
// poke into client memory.
MG_Pipe::MGPipeHandle splitRes = MG_Pipe::kMGPipeNullHandle;
BufferImpl::GLESBufferResource* splitResource = nullptr;
splitRes = target.handle;
splitResource = BufferImpl::FindBufferResourceForHandle(splitRes);
if (splitResource == nullptr) {
// THE ONLY DISCARD LEFT, and it is the impossible one: this target
// was recorded by BindBufferRange/Base against a handle the applier
// had already minted a twin for, so no twin here means the slot
// table and the capture list disagree about one resource. There is
// no backendId to upload into either, so there is nothing to do but
// name it.
MGLOG_E_ONCE("EndTransformFeedback: a scattered capture target (handle {%u,%u}) has "
"no server twin at all; its capture is discarded",
splitRes.Slot, splitRes.Gen);
continue;
}
// P8-C: made current first - a store a shader wrote here is refreshed.
const Uint8* hostBytes =
BufferImpl::SplitHostBytesForCpuRead(*splitResource, splitRes, "xfb_scatter_pre_capture");
// A NULL SHADOW IS NOT A LOST CAPTURE. It is the ORPHANED store
// (glBufferData(size, NULL) with no resource_subdata since), which
// M-3 declares legal: every byte the application has not staged is
// UNDEFINED by its own declaration, and `staged` is already
// value-initialised to zero - which is byte for byte what the monolith
// arm scatters over when it reads a fresh MappedData(). Skipping the
// Memcpy is therefore the WHOLE of the difference; skipping the TARGET
// threw the varyings away and left the application's buffer holding its
// pre-draw bytes under GL_NO_ERROR, the silent-loss class this file's
// preamble exists to close. The streaming idiom that hits it - orphan,
// partial glBufferSubData, capture into the rest - is ordinary.
if (hostBytes != nullptr) {
// Managers.cpp's pool-reuse ladder guards its whole-store
// MGL_SERVER_STAGED_REQUIRE with exactly this predicate and for
// exactly this reason: for a store whose content the application
// SUPPLIED a coverage gap is a missing record and zero-filling past
// it is silent data loss (Fatal by name); for an orphaned one the
// gap is its own undefined content and the read is legal.
if (BufferImpl::ResourceContentIsDeclared(splitRes)) {
BufferImpl::RequireStagedCoverage(*splitResource, hostBytes, target.start, target.end,
"xfb_scatter_pre_capture");
}
Memcpy(staged.data(), hostBytes + target.start, rangeBytes);
}
for (const auto& varying : program->GetTransformFeedbackVaryings()) {
if (varying.bufferIndex != target.bufferIndex) continue;
for (SizeT v = 0; v < vertices; ++v) {
const SizeT dstOffset = v * stride + varying.offsetBytes;
if (dstOffset + varying.byteSize > rangeBytes) break;
Memcpy(staged.data() + dstOffset,
static_cast<const Uint8*>(packed) + v * packedStride + varying.packedOffsetBytes,
varying.byteSize);
}
}
if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback != nullptr) {
// Sliced for the reason the readback path above is sliced: the bytes
// are INLINE in a SEG_EVENT record and the ring refuses a record wider
// than half its capacity outright, so one whole-range post of a
// capture over ~128 KiB was Fatal{EventRingOverflow} and the server
// died. Only the EVENT is cut; the glBufferSubData below stays one
// call over the whole `staged` vector, because the ES store has no
// ring and cutting it would change how many backend calls one
// application call makes.
const SizeT step = XfbWritebackSliceStep(rangeBytes);
for (SizeT off = 0; off < rangeBytes; off += step) {
const SizeT bytes = std::min(step, rangeBytes - off);
MG_Pipe::gMGPipeCallbacks.OnBufferWriteback(
splitRes, target.start + off,
MG_Pipe::MGPBlobRef{reinterpret_cast<Uint64>(staged.data() + off),
static_cast<Uint64>(bytes), MG_Pipe::kMGHostSpanSegNone, 0});
}
} else {
MGLOG_E_ONCE("EndTransformFeedback: no reverse channel is installed, so the "
"scattered capture of buffer {%u,%u} cannot reach the client shadow",
splitRes.Slot, splitRes.Gen);
}
// P8-C: the staged copy takes the composed range too, so this side's CPU
// readers see the capture (before P8 it kept the pre-capture bytes).
BufferImpl::StageServerWrittenRange(*splitResource, splitRes, staged.data(), target.start,
rangeBytes);
// Ops_H_Readback's stamp, for its reason: the GL store now holds the
// capture, and the next draw must not re-upload an older range over it.
splitResource->syncedChangeSerial = BufferImpl::ResourceSerialForHandle(splitRes);
// Serial bumped with no backend op (see ReadbackCapturedRanges).
BufferImpl::BumpBufferMutationEpoch();
if (g_GLESFuncs.glBufferSubData != nullptr) {
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, target.backendId);
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, static_cast<GLintptr>(target.start),
static_cast<GLsizeiptr>(rangeBytes), staged.data());
}
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_scatterBufferId);
}
g_GLESFuncs.glUnmapBuffer(BufferImpl::TempBufferTarget);
xfb.targets.clear();
}
} // namespace
Bool AreTransformFeedbacksSupported() {
return g_GLESFuncs.glBeginTransformFeedback != nullptr && g_GLESFuncs.glEndTransformFeedback != nullptr &&
g_GLESFuncs.glTransformFeedbackVaryings != nullptr;
}
Bool IsCaptureSpanOpen() {
const auto& xfb = CurrentXfb();
return (xfb.pending || xfb.started) && !xfb.paused;
}
void BeginTransformFeedback(GLenum primitiveMode) {
if (!AreTransformFeedbacksSupported()) return;
auto& xfb = CurrentXfb();
xfb.primitiveMode = primitiveMode;
xfb.pending = true;
xfb.started = false;
xfb.paused = false;
xfb.targets.clear();
}
void EndTransformFeedback() {
auto& xfb = CurrentXfb();
const Bool wasPending = xfb.pending;
xfb.pending = false;
xfb.paused = false;
if (!xfb.started) {
// A span that never drew is legal and captures nothing by definition; one that
// is STILL pending here drew nothing the backend saw, which for a span the
// application expected data from is the whole bug in one line.
MGLOG_D("EndTransformFeedback: closing a span the driver never opened (pending=%d)",
wasPending ? 1 : 0);
return;
}
xfb.started = false;
g_GLESFuncs.glEndTransformFeedback();
if (const GLenum endError = TakeXfbDriverError(); endError != GL_NO_ERROR) {
MGLOG_E_ONCE("EndTransformFeedback: the ES driver rejected glEndTransformFeedback with %s - the "
"driver's capture state and MobileGL's have diverged",
MG_Util::ConvertGLEnumToString(endError).c_str());
}
if (xfb.scattered) {
ScatterCapturedRecords(xfb);
xfb.scattered = false;
xfb.scatterProgram.reset();
xfb.scatterArchive.reset();
} else {
ReadbackCapturedRanges(xfb.targets);
}
}
void PauseTransformFeedback() {
auto& xfb = CurrentXfb();
xfb.paused = true;
// A span the driver never opened (paused before the first draw) has nothing to
// pause; the flag above is what holds the deferred Begin back until the resume.
if (!xfb.started || g_GLESFuncs.glPauseTransformFeedback == nullptr) return;
g_GLESFuncs.glPauseTransformFeedback();
}
void ResumeTransformFeedback() {
auto& xfb = CurrentXfb();
xfb.paused = false;
if (!xfb.started || g_GLESFuncs.glResumeTransformFeedback == nullptr) return;
g_GLESFuncs.glResumeTransformFeedback();
}
void BindTransformFeedback(GLuint name) {
auto& roleState = ActiveXfbState();
auto& g_currentXfbState = roleState.Cached;
auto& g_xfbObjects = roleState.Objects;
auto& g_currentXfbName = roleState.Current;
const Uint64 key = MG_Pipe::MGPipeApplier().BoundStreamOutputLifetimeId;
(void)name;
roleState.NeedsBind = false;
g_currentXfbState = nullptr; // name changes; operator[] below may also rehash
// The capture buffer bindings are the OBJECT's, not the context's: the bind below
// swaps all of them for whatever the target object holds, which the redundant-bind
// shadow has never seen.
BufferImpl::InvalidateTransformFeedbackBindingShadows();
if (!AreTransformFeedbackObjectsSupported()) {
// Without driver objects there is only the default span; keep the frontend
// name so the bookkeeping below stays consistent.
g_currentXfbName = key;
return;
}
auto& xfb = g_xfbObjects[key];
// Even each virtual context's name-0 object gets its own native object.
if (xfb.esId == 0) {
g_GLESFuncs.glGenTransformFeedbacks(1, &xfb.esId);
}
g_GLESFuncs.glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, xfb.esId);
g_currentXfbName = key;
}
void DeleteTransformFeedback(GLuint name) {
auto& roleState = ActiveXfbState();
auto& g_currentXfbState = roleState.Cached;
auto& g_xfbObjects = roleState.Objects;
auto& g_currentXfbName = roleState.Current;
const Uint64 key = MG_Pipe::MGPipeApplier().VerbDeleteStreamOutputLifetimeId;
(void)name;
// A generated but never-bound name has no native object or lifetime.
if (key == 0) return;
const auto it = g_xfbObjects.find(key);
if (it == g_xfbObjects.end()) return;
if (it->second.esId != 0 && g_GLESFuncs.glDeleteTransformFeedbacks != nullptr) {
g_GLESFuncs.glDeleteTransformFeedbacks(1, &it->second.esId);
}
g_currentXfbState = nullptr; // erase shifts the probe cluster, moving other entries
g_xfbObjects.erase(it);
// The frontend reverts to the default object when the bound one is deleted.
if (g_currentXfbName == key) {
// The following BindStreamOutput names this context's actual
// default lifetime. Never create a native object for key 0.
g_currentXfbName = 0;
roleState.NeedsBind = true;
BufferImpl::InvalidateTransformFeedbackBindingShadows();
return;
}
}
// The ES context went away (or is being torn down): the spans, their buffer ids, the
// driver objects and the frontend objects they pinned all belonged to it.
void OnBackendContextDestroyed() {
for (auto& state : g_roleXfbState)
state = {};
g_currentXfbState = nullptr;
g_xfbObjects.clear();
g_currentXfbName = 0;
g_scatterBufferId = 0;
g_scatterBufferSize = 0;
BufferImpl::InvalidateTransformFeedbackBindingShadows();
}
} // namespace XfbImpl
namespace VertexArrayImpl {
// THE CLIENT-MEMORY VERTEX ARRAY UPLOAD, and the one read of this family that P5e does
// NOT retire - it MOVES the decision instead, which is what ID-82 / ruling 2 settles.
//
// The bytes of a client-memory array exist only in the application's own memory: the
// emitter publishes Res == kMGPipeNullHandle for such an attribute by design
// (VertexInputEmit.h) and this upload is where the server used to dereference
// attrib.Offset as a raw client pointer (Managers.cpp's
// SyncClientSideAttributesForDrawArrays, kimi audit row 14). Under run-ahead those bytes
// are movable and the read is torn by construction, so the CLIENT refuses the draw by
// name before it is ever emitted (EmitTables.cpp, Fatal{UnmigratedVerb,
// "DrawArrays+CLIENT_ARRAYS"}); staging them as a record tail is P8's.
//
// What is left here is therefore reachable in exactly two situations, and both are safe:
// * monolith / the push-monolith build - one process, one thread, no wire;
// * a LOCKSTEP split session - the client is parked in WaitForApplied for
// this very record, so its memory is stable
// (rule F is scoped to unbarriered records,
// ruling 4). Escalation (ii) of §2.1 pins the
// same thing from the other side: a draw
// carrying kDrawClientArrays is BARRIERED.
// Making it monolith-only instead would silently drop the attribute upload of a
// lockstep split session, which is a wrong picture today - before ra has landed
// anything - so the refusal is the client's to raise, not this arm's to assume.
//
// The frontend VAO is taken ONLY when the applier's own vertex-buffer window says a
// client-sourced attribute exists, which is what retires the per-draw
// GetBoundVertexArray row of the ordinary (VBO-backed) DrawArrays - the strict lane's
// `GetBoundVertexArray@DrawArrays` marker - without touching this path's behaviour.
// `static` on purpose (G1): the pull build must not gain an exported name for a
// function that only re-homes two identical inline blocks the two DrawArrays entry
// points used to carry. The same spelling governs the three below, and for a second
// reason on top of the symbol count: a Debug build drops CXX_VISIBILITY_PRESET to
// `default` (CMakeLists.txt:707-719), so an external-linkage helper here would be a
// DYNAMIC export in exactly the configuration a symbol gate cannot see it in.
//
// ALL FLAVORS, NOT JUST THE SPLIT ARM. These three were inside
// `#if MOBILEGL_BUILD_DISAGGREGATED` for one round and that was a misreading of their
// own gate: the body's first statement returns early when `MG_Config::Transport !=
// Monolith`, i.e. they exist FOR the monolith arm - and the monolith arm is what the
// pull/verify/push builds run. Their callers sit in entry points that compile in every
// flavor (the indexed family, both indirect executors), so a guard here breaks the
// pull build outright rather than merely omitting a split-arm path. In those flavors
// the guard's own test folds to a compile-time false (Config.h:566) and the body is
// the transport-free upload it always was.
//
// ---- THE MONOLITH ARM'S CLIENT-MEMORY SNAPSHOT, PER DRAW -----------------------------
//
// A client-memory attribute is uploaded HERE because its bytes have no store on this side
// to bind. SyncClientSideVertexArraysForDrawArrays below does that for the non-indexed
// family, whose fetched elements are exactly the contiguous (first, count) run. Every
// OTHER draw shape fetches by index and by instance, so its elements have to come from
// MGPipeClientFetchPlan - the same answer the wire arm's owned snapshot is built from
// (MG_Impl/Pipe/OwnedDrawInputs.h, which is why the two arms agree by construction).
//
// WHAT WAS MISSING BEFORE THIS: an indexed draw over a client array uploaded nothing at
// all. The ES context kept whatever the last VBO-backed attribute had left for that
// index - normally the array DISABLED, i.e. every vertex reading the generic current
// value - so the draw painted nothing, with no GL error anywhere to say why.
struct ClientSnapshotSources {
const SharedPtr<MG_State::GLState::BufferObject>* ElementBuffer = nullptr;
};
} // namespace VertexArrayImpl
} // namespace MobileGL::MG_Backend::DirectGLES
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,893 @@
// MobileGL - MobileGL/MG_Backend/DirectGLES/DirectGLESSharedImage.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "DirectGLESInternal.h"
namespace MobileGL::MG_Backend::DirectGLES {
#if MOBILEGL_BUILD_DISAGGREGATED
// ---- SHARED IMAGES (DirectGLES.h; docs/archive/notes/anland/plan-ahb-dmabuf.md) ------
namespace SharedImageImpl {
#if defined(__ANDROID__)
namespace {
namespace SI = MG_Remote::Server::SharedImages;
// EGL_ANDROID_image_native_buffer's target and EGL_KHR_image_base's attribute, spelled
// here so no extension header is needed.
constexpr EGLenum kEglNativeBufferAndroid = 0x3140;
constexpr EGLint kEglImagePreservedKhr = 0x30D2;
using GetNativeClientBufferFn = EGLClientBuffer (*)(const AHardwareBuffer*);
using CreateImageKhrFn = void* (*)(EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLint*);
using DestroyImageKhrFn = EGLBoolean (*)(EGLDisplay, void*);
using ImageTargetFn = void (*)(GLenum, void*);
// EGL_KHR_fence_sync / EGL_KHR_wait_sync / EGL_ANDROID_native_fence_sync, likewise.
constexpr EGLenum kEglSyncNativeFenceAndroid = 0x3144;
constexpr EGLint kEglSyncNativeFenceFdAndroid = 0x3145;
constexpr EGLint kEglConditionSatisfiedKhr = 0x30F6;
using CreateSyncKhrFn = void* (*)(EGLDisplay, EGLenum, const EGLint*);
using DestroySyncKhrFn = EGLBoolean (*)(EGLDisplay, void*);
using WaitSyncKhrFn = EGLint (*)(EGLDisplay, void*, EGLint);
using ClientWaitSyncKhrFn = EGLint (*)(EGLDisplay, void*, EGLint, Uint64);
using DupNativeFenceFdFn = EGLint (*)(EGLDisplay, void*);
struct EntryPoints {
Bool Resolved = false;
Bool Usable = false;
String Why;
GetNativeClientBufferFn GetNativeClientBuffer = nullptr;
CreateImageKhrFn CreateImage = nullptr;
DestroyImageKhrFn DestroyImage = nullptr;
ImageTargetFn TargetTexture2D = nullptr;
ImageTargetFn TargetRenderbuffer = nullptr;
// Fences as sync_files: what lets one session's GPU wait for another's without a
// CPU in between. Without them every wait below is a CPU wait.
Bool FencesUsable = false;
CreateSyncKhrFn CreateSync = nullptr;
DestroySyncKhrFn DestroySync = nullptr;
WaitSyncKhrFn WaitSync = nullptr;
ClientWaitSyncKhrFn ClientWaitSync = nullptr;
DupNativeFenceFdFn DupNativeFenceFd = nullptr;
// YUV images: sampled as external textures (GL_OES_EGL_image_external_essl3), and
// through GL_EXT_YUV_target's raw Y'CbCr sampler where the driver has it, so the
// conversion is the import's own (its colour hints) rather than the driver's guess.
Bool ExternalEssl3 = false;
Bool YuvTarget = false;
};
// None of these is in the loader's tables, and eglGetProcAddress hands back live-looking
// stubs for entry points a driver lacks, so the extension strings decide first. Resolved
// once, from the first call that has a context current to read the strings from; the
// answer is never modified afterwards, so it is read without the lock.
const EntryPoints& ResolvedEntryPoints() {
static auto* mutex = new std::mutex();
static auto* entry = new EntryPoints();
const std::lock_guard<std::mutex> lock(*mutex);
if (entry->Resolved) return *entry;
if (!g_EGLFuncs.eglGetProcAddress || !g_EGLFuncs.eglQueryString || !g_GLESFuncs.glGetString) {
entry->Why = "the loader has no eglGetProcAddress/eglQueryString/glGetString";
return *entry;
}
const char* egl =
g_Display != EGL_NO_DISPLAY ? g_EGLFuncs.eglQueryString(g_Display, EGL_EXTENSIONS) : nullptr;
const char* gl = reinterpret_cast<const char*>(g_GLESFuncs.glGetString(GL_EXTENSIONS));
if (egl == nullptr || gl == nullptr) {
entry->Why = "no current display and context to read the extension strings from";
return *entry; // asked again on the next call
}
entry->Resolved = true;
if (HasExtensionWord(egl, "EGL_KHR_fence_sync") && HasExtensionWord(egl, "EGL_KHR_wait_sync") &&
HasExtensionWord(egl, "EGL_ANDROID_native_fence_sync")) {
entry->CreateSync =
reinterpret_cast<CreateSyncKhrFn>(g_EGLFuncs.eglGetProcAddress("eglCreateSyncKHR"));
entry->DestroySync =
reinterpret_cast<DestroySyncKhrFn>(g_EGLFuncs.eglGetProcAddress("eglDestroySyncKHR"));
entry->WaitSync = reinterpret_cast<WaitSyncKhrFn>(g_EGLFuncs.eglGetProcAddress("eglWaitSyncKHR"));
entry->ClientWaitSync =
reinterpret_cast<ClientWaitSyncKhrFn>(g_EGLFuncs.eglGetProcAddress("eglClientWaitSyncKHR"));
entry->DupNativeFenceFd = reinterpret_cast<DupNativeFenceFdFn>(
g_EGLFuncs.eglGetProcAddress("eglDupNativeFenceFDANDROID"));
entry->FencesUsable = entry->CreateSync && entry->DestroySync && entry->WaitSync &&
entry->ClientWaitSync && entry->DupNativeFenceFd;
}
if (!entry->FencesUsable) {
MGLOG_I("Shared images: no EGL native fence sync / wait sync; sessions order their access "
"to an image with CPU waits");
}
if (!HasExtensionWord(egl, "EGL_KHR_image_base") ||
!HasExtensionWord(egl, "EGL_ANDROID_image_native_buffer") ||
!HasExtensionWord(egl, "EGL_ANDROID_get_native_client_buffer")) {
entry->Why = "EGL_KHR_image_base / EGL_ANDROID_image_native_buffer / "
"EGL_ANDROID_get_native_client_buffer is not advertised";
return *entry;
}
if (!HasExtensionWord(gl, "GL_OES_EGL_image")) {
entry->Why = "GL_OES_EGL_image is not advertised";
return *entry;
}
entry->ExternalEssl3 = HasExtensionWord(gl, "GL_OES_EGL_image_external_essl3");
entry->YuvTarget = HasExtensionWord(gl, "GL_EXT_YUV_target");
entry->GetNativeClientBuffer = reinterpret_cast<GetNativeClientBufferFn>(
g_EGLFuncs.eglGetProcAddress("eglGetNativeClientBufferANDROID"));
entry->CreateImage =
reinterpret_cast<CreateImageKhrFn>(g_EGLFuncs.eglGetProcAddress("eglCreateImageKHR"));
entry->DestroyImage =
reinterpret_cast<DestroyImageKhrFn>(g_EGLFuncs.eglGetProcAddress("eglDestroyImageKHR"));
entry->TargetTexture2D =
reinterpret_cast<ImageTargetFn>(g_EGLFuncs.eglGetProcAddress("glEGLImageTargetTexture2DOES"));
entry->TargetRenderbuffer = reinterpret_cast<ImageTargetFn>(
g_EGLFuncs.eglGetProcAddress("glEGLImageTargetRenderbufferStorageOES"));
if (!entry->GetNativeClientBuffer || !entry->CreateImage || !entry->DestroyImage ||
!entry->TargetTexture2D || !entry->TargetRenderbuffer) {
entry->Why = "eglGetProcAddress gave no address for an advertised EGLImage entry point";
return *entry;
}
entry->Usable = true;
return *entry;
}
void DrainErrors() {
for (Uint guard = 0; guard < 16 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++guard) {}
}
} // namespace
#endif
struct EglImage {
#if defined(__ANDROID__)
SI::ImageRef Image;
void* Handle = nullptr;
EGLDisplay Display = EGL_NO_DISPLAY;
Uint64 DisplayTerminations = 0;
DestroyImageKhrFn Destroy = nullptr;
// AcquireForSampling's memory, touched only by the session whose twin holds this: the
// write generation last waited for and the native context the wait was queued in (a
// GPU wait orders that context alone), and the read frame the image was last noted in.
Uint64 WaitedGeneration = 0;
Uint64 WaitedContext = 0;
Uint64 NotedFrame = 0;
// A YUV image (ConvertYuvForSampling): an external texture over the EGLImage, the source
// of the conversion into the texture's own RGBA storage, and the reader's frame the
// conversion last ran in.
GLuint ExternalTexture = 0;
Uint64 ConvertedFrame = 0;
~EglImage() {
// eglDestroyImageKHR needs the display, not a context, so a session's twin teardown
// (no context current) still releases the image; only exit() and a terminated
// display - which destroyed the image itself - skip it.
if (Handle == nullptr || Destroy == nullptr || InProcessExit()) return;
if (g_displayTerminations.load(std::memory_order_acquire) != DisplayTerminations) return;
// Texture names are the share group's: deleting one needs any context of it, and
// without one current the name goes with the display.
if (ExternalTexture != 0 && t_native.boundContext != EGL_NO_CONTEXT)
g_GLESFuncs.glDeleteTextures(1, &ExternalTexture);
Destroy(Display, Handle);
}
#endif
};
#if defined(__ANDROID__)
EglImageRef CreateEglImage(Uint64 id, String& why) {
const EntryPoints& entry = ResolvedEntryPoints();
if (!entry.Usable) {
why = entry.Why;
return nullptr;
}
SI::ImageRef image = SI::Find(id);
if (image == nullptr) {
why = "no live shared image has this id";
return nullptr;
}
if (image->Native == nullptr) {
why = "the image has no AHardwareBuffer";
return nullptr;
}
const EGLClientBuffer buffer =
entry.GetNativeClientBuffer(static_cast<const AHardwareBuffer*>(image->Native));
if (buffer == nullptr) {
why = "eglGetNativeClientBufferANDROID returned null";
return nullptr;
}
const EGLint attribs[] = {kEglImagePreservedKhr, EGL_TRUE, EGL_NONE};
void* handle = entry.CreateImage(g_Display, EGL_NO_CONTEXT, kEglNativeBufferAndroid, buffer, attribs);
if (handle == nullptr) {
char text[96];
std::snprintf(text, sizeof(text), "eglCreateImageKHR failed (EGL error 0x%04x)",
g_EGLFuncs.eglGetError ? static_cast<unsigned>(g_EGLFuncs.eglGetError()) : 0u);
why = text;
return nullptr;
}
auto result = MakeShared<EglImage>();
result->Image = std::move(image);
result->Handle = handle;
result->Display = g_Display;
result->DisplayTerminations = g_displayTerminations.load(std::memory_order_acquire);
result->Destroy = entry.DestroyImage;
return result;
}
Bool TargetBoundTexture2D(const EglImageRef& image) {
const EntryPoints& entry = ResolvedEntryPoints();
if (!entry.Usable || image == nullptr || image->Handle == nullptr) return false;
DrainErrors();
entry.TargetTexture2D(GL_TEXTURE_2D, image->Handle);
return g_GLESFuncs.glGetError() == GL_NO_ERROR;
}
namespace {
// ONE PRESENT TARGET PER IMAGE, PROCESS-WIDE: the EGLImage and a renderbuffer whose
// storage it is. Every native context is in one share group, so the renderbuffer is
// one object for every session's context; only the framebuffer it is attached to is
// per context. Kept until no session holds the image any more (pruned on every present
// - the cache's own reference is then the only one), with a small cap so a client that
// cycles through many images cannot pile them up between presents.
struct PresentTarget {
EglImageRef Image;
GLuint Renderbuffer = 0;
Uint64 LastUse = 0;
// A present has written the image through this target (its content is a frame).
Bool Written = false;
};
struct PresentTargets {
std::mutex Mutex;
std::unordered_map<Uint64, PresentTarget> ById;
Uint64 UseClock = 0;
};
constexpr SizeT kMaxPresentTargets = 8;
PresentTargets& Targets() {
static auto* targets = new PresentTargets();
return *targets;
}
// The framebuffer a present draws through, one per native context (container objects
// are never shared). It carries an attachment only for the duration of a present, so
// no context's framebuffer keeps a dropped target's renderbuffer alive.
PerNativeContext<GLuint> g_presentFramebuffers;
Bool TargetIsCurrent(const PresentTarget& target) {
return target.Image != nullptr &&
target.Image->DisplayTerminations == g_displayTerminations.load(std::memory_order_acquire);
}
void DeleteTarget(PresentTarget& target) {
if (target.Renderbuffer != 0 && TargetIsCurrent(target)) {
g_GLESFuncs.glDeleteRenderbuffers(1, &target.Renderbuffer);
}
target.Renderbuffer = 0;
target.Image = nullptr;
}
// Caller holds the mutex and has a context of the share group current.
void PruneLocked(PresentTargets& targets) {
for (auto it = targets.ById.begin(); it != targets.ById.end();) {
if (!TargetIsCurrent(it->second) || it->second.Image->Image.use_count() <= 1) {
DeleteTarget(it->second);
it = targets.ById.erase(it);
} else {
++it;
}
}
}
GLuint MakeRenderbuffer(const EglImage& image, String& why) {
GLuint renderbuffer = 0;
g_GLESFuncs.glGenRenderbuffers(1, &renderbuffer);
if (renderbuffer == 0) {
why = "glGenRenderbuffers gave no name";
return 0;
}
// Espryt keeps no renderbuffer-binding shadow; put the driver's binding back as found.
GLint previous = 0;
g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &previous);
DrainErrors();
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
ResolvedEntryPoints().TargetRenderbuffer(GL_RENDERBUFFER, image.Handle);
const GLenum error = g_GLESFuncs.glGetError();
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(previous));
if (error != GL_NO_ERROR) {
char text[96];
std::snprintf(text, sizeof(text), "glEGLImageTargetRenderbufferStorageOES -> 0x%04x",
static_cast<unsigned>(error));
why = text;
g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer);
return 0;
}
return renderbuffer;
}
// Everything the current context has submitted, waited out on the CPU: the fallback for
// a driver that cannot hand a fence to another session.
Bool WaitForCompletion() {
if (g_GLESFuncs.glFenceSync && g_GLESFuncs.glClientWaitSync && g_GLESFuncs.glDeleteSync) {
if (GLsync fence = g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0)) {
constexpr GLuint64 kSliceNs = 1000ull * 1000 * 1000;
GLenum status = GL_TIMEOUT_EXPIRED;
GLbitfield flags = GL_SYNC_FLUSH_COMMANDS_BIT;
for (Uint slice = 0; slice < 10 && status == GL_TIMEOUT_EXPIRED; ++slice) {
status = g_GLESFuncs.glClientWaitSync(fence, flags, kSliceNs);
flags = 0;
}
g_GLESFuncs.glDeleteSync(fence);
if (status == GL_ALREADY_SIGNALED || status == GL_CONDITION_SATISFIED) return true;
if (status == GL_TIMEOUT_EXPIRED) {
MGLOG_E("Shared images: the submitted work did not complete within 10 s");
return false;
}
// GL_WAIT_FAILED: glFinish is the same promise without the object.
}
}
g_GLESFuncs.glFinish();
return true;
}
// How long a CPU wait on another session's fence may take before the order is given up
// on (logged): a session whose GPU work never completes must not wedge its peers.
constexpr Uint32 kFenceCpuWaitMs = 2000;
void CloseFd(int fd) {
if (fd >= 0) ::close(fd);
}
// The current context's LATER commands wait for `fence` (a sync_file) on the GPU; a CPU
// wait when the driver cannot. Takes ownership of `fence`: a native fence sync owns it
// from a successful create on, every other path closes it here.
void WaitForFenceBeforeLaterCommands(int fence) {
if (fence < 0) return;
const EntryPoints& entry = ResolvedEntryPoints();
if (entry.FencesUsable) {
const EGLint attribs[] = {kEglSyncNativeFenceFdAndroid, fence, EGL_NONE};
if (void* sync = entry.CreateSync(g_Display, kEglSyncNativeFenceAndroid, attribs)) {
if (entry.WaitSync(g_Display, sync, 0) != EGL_TRUE) {
constexpr Uint64 kWaitNs = Uint64{kFenceCpuWaitMs} * 1000 * 1000;
if (entry.ClientWaitSync(g_Display, sync, 0, kWaitNs) != kEglConditionSatisfiedKhr) {
MGLOG_E_ONCE("Shared images: a peer session's fence could not be waited for");
}
}
// A pending server wait keeps the sync alive past this (EGL_KHR_wait_sync).
entry.DestroySync(g_Display, sync);
return;
}
}
if (!SI::SyncFile::Wait(fence, kFenceCpuWaitMs)) {
MGLOG_E_ONCE("Shared images: a peer session's fence did not signal within %u ms",
static_cast<unsigned>(kFenceCpuWaitMs));
}
CloseFd(fence);
}
// A sync_file for everything the current context has submitted so far (flushed, so it
// will signal), in `*fence`. False when the driver cannot export one; the caller then
// waits on the CPU and publishes -1.
Bool ExportFence(int* fence) {
*fence = -1;
const EntryPoints& entry = ResolvedEntryPoints();
if (!entry.FencesUsable) return false;
const EGLint attribs[] = {EGL_NONE};
void* sync = entry.CreateSync(g_Display, kEglSyncNativeFenceAndroid, attribs);
if (sync == nullptr) return false;
// The fence has no descriptor until its command reaches the driver's queue.
g_GLESFuncs.glFlush();
const EGLint fd = entry.DupNativeFenceFd(g_Display, sync);
entry.DestroySync(g_Display, sync);
if (fd < 0) return false;
*fence = fd;
return true;
}
// ONE READ FRAME PER SESSION: what the session has sampled since its last frame
// boundary. A session is served by exactly one apply thread, so the thread owns it -
// nothing about it is process-wide, and a session's end (its thread's) drops it. `Id`
// is drawn from one counter across all sessions and renewed at every boundary, so an
// EglImage's NotedFrame is a once-per-frame gate no other session's frame can match.
struct ReadFrame {
Uint64 Id = 0;
SI::ReadTracker Tracker;
// The session ends frames at publishing flushes (PublishPendingAccesses): it writes
// images, so its first use of one in a frame waits for that image's pending reads.
Bool ImplicitSync = false;
// When a YUV image is converted again (ConvertYuvForSampling): renewed at EVERY
// boundary - present, publishing flush, and each native fence whatever the session
// is - since an offscreen producer (a browser's GPU process) may have no other.
Uint64 YuvEpoch = 0;
};
std::atomic<Uint64> g_readFrameIds{0};
ReadFrame& CurrentReadFrame() {
thread_local ReadFrame frame;
if (frame.Id == 0) frame.Id = g_readFrameIds.fetch_add(1, std::memory_order_relaxed) + 1;
if (frame.YuvEpoch == 0) frame.YuvEpoch = g_readFrameIds.fetch_add(1, std::memory_order_relaxed) + 1;
return frame;
}
void RenewYuvEpoch(ReadFrame& frame) {
frame.YuvEpoch = g_readFrameIds.fetch_add(1, std::memory_order_relaxed) + 1;
}
} // namespace
void AcquireForSampling(const EglImageRef& image) {
if (image == nullptr || image->Image == nullptr) return;
EglImage& state = *image;
// Steady state: one lock and a compare (the write fence, once signaled, is dropped by
// the registry and nothing is duplicated any more).
Uint64 generation = 0;
int fence = SI::DupWriteFence(*state.Image, &generation);
const Uint64 context = CurrentNativeContextSerial();
if (generation != state.WaitedGeneration || context != state.WaitedContext) {
state.WaitedGeneration = generation;
state.WaitedContext = context;
WaitForFenceBeforeLaterCommands(fence);
fence = -1;
}
CloseFd(fence);
ReadFrame& frame = CurrentReadFrame();
if (state.NotedFrame != frame.Id) {
// A session that renders into images (it ends frames with a publishing flush) may be
// about to overwrite this one: what other sessions still sample goes first.
if (frame.ImplicitSync) {
for (const int read : SI::DupPendingReadFences(*state.Image))
WaitForFenceBeforeLaterCommands(read);
}
frame.Tracker.NoteRead(state.Image);
state.NotedFrame = frame.Id;
}
}
Bool IsYuv(const EglImageRef& image) {
return image != nullptr && image->Image != nullptr && SI::FourccIsYuv(image->Image->Fourcc);
}
namespace {
// THE CONVERSION PASS: one triangle over the texture, sampling the image through an
// external texture. Programs are the share group's and rebuilt with a new context
// generation; the framebuffer and vertex array are per context (never shared).
struct YuvConversion {
Uint ContextGeneration = ~0u;
GLuint ExternalProgram = 0; // the driver's conversion (samplerExternalOES)
GLuint TargetProgram = 0; // the import's own (GL_EXT_YUV_target)
GLint TargetMatrix = -1;
GLint TargetOffset = -1;
Bool Failed = false;
NativeContextName Framebuffer;
NativeContextName VertexArray;
};
YuvConversion& Conversion() {
static auto* conversion = new YuvConversion();
return *conversion;
}
constexpr const char* kYuvVertexSource =
"#version 300 es\n"
"out vec2 vUv;\n"
"void main() {\n"
" vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n"
" gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n"
" vUv = p;\n"
"}\n";
constexpr const char* kYuvExternalFragmentSource =
"#version 300 es\n"
"#extension GL_OES_EGL_image_external_essl3 : require\n"
"precision highp float;\n"
"uniform samplerExternalOES uSource;\n"
"in vec2 vUv;\n"
"out vec4 oColor;\n"
"void main() { oColor = vec4(texture(uSource, vUv).rgb, 1.0); }\n";
constexpr const char* kYuvTargetFragmentSource =
"#version 300 es\n"
"#extension GL_EXT_YUV_target : require\n"
"precision highp float;\n"
"uniform __samplerExternal2DY2YEXT uSource;\n"
"uniform mat3 uMatrix;\n"
"uniform vec3 uOffset;\n"
"in vec2 vUv;\n"
"out vec4 oColor;\n"
"void main() {\n"
" vec3 ycbcr = texture(uSource, vUv).xyz;\n"
" oColor = vec4(clamp(uMatrix * (ycbcr - uOffset), 0.0, 1.0), 1.0);\n"
"}\n";
GLuint BuildYuvProgram(const char* fragmentSource) {
const GLuint vertexShader = g_GLESFuncs.glCreateShader(GL_VERTEX_SHADER);
const GLuint fragmentShader = g_GLESFuncs.glCreateShader(GL_FRAGMENT_SHADER);
GLint linked = GL_FALSE;
GLuint program = 0;
if (vertexShader != 0 && fragmentShader != 0) {
g_GLESFuncs.glShaderSource(vertexShader, 1, &kYuvVertexSource, nullptr);
g_GLESFuncs.glCompileShader(vertexShader);
g_GLESFuncs.glShaderSource(fragmentShader, 1, &fragmentSource, nullptr);
g_GLESFuncs.glCompileShader(fragmentShader);
program = g_GLESFuncs.glCreateProgram();
if (program != 0) {
g_GLESFuncs.glAttachShader(program, vertexShader);
g_GLESFuncs.glAttachShader(program, fragmentShader);
g_GLESFuncs.glLinkProgram(program);
g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &linked);
}
}
if (vertexShader != 0) g_GLESFuncs.glDeleteShader(vertexShader);
if (fragmentShader != 0) g_GLESFuncs.glDeleteShader(fragmentShader);
if (linked != GL_TRUE) {
if (program != 0) g_GLESFuncs.glDeleteProgram(program);
return 0;
}
return program;
}
// MOBILEGL_YUV_DRIVER_CONVERSION=1: always the driver's conversion (samplerExternalOES),
// even where GL_EXT_YUV_target would let the import's hints decide.
Bool DriverConversionForced() {
static const Bool forced = [] {
const char* value = std::getenv("MOBILEGL_YUV_DRIVER_CONVERSION");
return value != nullptr && value[0] == '1';
}();
return forced;
}
} // namespace
void ConvertYuvForSampling(const EglImageRef& image, GLuint target, Uint32 width, Uint32 height) {
if (!IsYuv(image) || target == 0 || width == 0 || height == 0) return;
EglImage& state = *image;
ReadFrame& frame = CurrentReadFrame();
if (state.ConvertedFrame == frame.YuvEpoch) return;
// Whatever happens below, this frame does not try again.
state.ConvertedFrame = frame.YuvEpoch;
const EntryPoints& entry = ResolvedEntryPoints();
if (!entry.Usable || !entry.ExternalEssl3) {
MGLOG_E_ONCE("Shared images: a YUV image cannot be sampled here (%s)",
entry.Usable ? "no GL_OES_EGL_image_external_essl3" : entry.Why.c_str());
return;
}
// THE CPU-COPY FALLBACK, for a buffer this server did not allocate (SharedImageRegistry.h).
String why;
if (state.Image->IsForeign() && !SI::RefreshForeign(*state.Image, why)) {
MGLOG_E_ONCE("Shared images: foreign YUV image %llu could not be refreshed: %s",
static_cast<unsigned long long>(state.Image->Id), why.c_str());
}
YuvConversion& conversion = Conversion();
if (conversion.ContextGeneration != g_backendContextGeneration) {
conversion.ExternalProgram = 0;
conversion.TargetProgram = 0;
conversion.TargetMatrix = conversion.TargetOffset = -1;
conversion.Failed = false;
conversion.ContextGeneration = g_backendContextGeneration;
}
if (conversion.Failed) return;
const Bool ownConversion = entry.YuvTarget && !DriverConversionForced();
GLuint& program = ownConversion ? conversion.TargetProgram : conversion.ExternalProgram;
if (program == 0) {
program = BuildYuvProgram(ownConversion ? kYuvTargetFragmentSource : kYuvExternalFragmentSource);
if (program == 0) {
conversion.Failed = true;
MGLOG_E_ONCE("Shared images: the YUV conversion program could not be built");
return;
}
if (ownConversion) {
conversion.TargetMatrix = g_GLESFuncs.glGetUniformLocation(program, "uMatrix");
conversion.TargetOffset = g_GLESFuncs.glGetUniformLocation(program, "uOffset");
}
MGLOG_I("Shared images: YUV images convert through %s",
ownConversion ? "GL_EXT_YUV_target with the import's colour hints"
: "the driver's external-sampler conversion");
}
if (conversion.Framebuffer == 0) g_GLESFuncs.glGenFramebuffers(1, &conversion.Framebuffer);
if (conversion.VertexArray == 0) g_GLESFuncs.glGenVertexArrays(1, &conversion.VertexArray);
if (conversion.Framebuffer == 0 || conversion.VertexArray == 0) return;
// Every binding goes through Espryt's shadows or a guard that puts it back.
ScopedEmulationDrawState drawState; // also activates TextureImpl::TempTextureUnit
ScopedFramebufferBinding binding(/*saveRead=*/false, /*saveDraw=*/true);
DrainErrors();
if (state.ExternalTexture == 0) {
g_GLESFuncs.glGenTextures(1, &state.ExternalTexture);
g_GLESFuncs.glBindTexture(GL_TEXTURE_EXTERNAL_OES, state.ExternalTexture);
entry.TargetTexture2D(GL_TEXTURE_EXTERNAL_OES, state.Handle);
g_GLESFuncs.glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
g_GLESFuncs.glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
g_GLESFuncs.glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
g_GLESFuncs.glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
const GLenum error = g_GLESFuncs.glGetError();
if (error != GL_NO_ERROR) {
MGLOG_E_ONCE("Shared images: YUV image %llu cannot be bound as an external texture (0x%04x)",
static_cast<unsigned long long>(state.Image->Id), static_cast<unsigned>(error));
}
} else {
g_GLESFuncs.glBindTexture(GL_TEXTURE_EXTERNAL_OES, state.ExternalTexture);
}
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, conversion.Framebuffer);
g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target, 0);
const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER);
if (status == GL_FRAMEBUFFER_COMPLETE) {
VertexArrayImpl::BindBackendVAOId(conversion.VertexArray);
g_GLESFuncs.glUseProgram(program);
g_GLESFuncs.glUniform1i(g_GLESFuncs.glGetUniformLocation(program, "uSource"),
static_cast<GLint>(TextureImpl::TempTextureUnit));
if (ownConversion) {
// A Y2Y sample is the stored code normalized as the format stores it: 8 bits, or
// P010's 10 in the high end of 16.
const double sampleMax = state.Image->Fourcc == SI::kFourccP010 ? 65535.0 / 64.0 : 255.0;
const auto c = SI::Yuv::ConversionFor(SI::GetYuvHints(*state.Image), sampleMax);
g_GLESFuncs.glUniformMatrix3fv(conversion.TargetMatrix, 1, GL_FALSE, c.Matrix);
g_GLESFuncs.glUniform3fv(conversion.TargetOffset, 1, c.Offset);
}
g_GLESFuncs.glViewport(0, 0, static_cast<GLsizei>(width), static_cast<GLsizei>(height));
g_GLESFuncs.glDrawArrays(GL_TRIANGLES, 0, 3);
const GLenum error = g_GLESFuncs.glGetError();
if (error != GL_NO_ERROR) {
MGLOG_E_ONCE("Shared images: the YUV conversion draw failed (0x%04x)",
static_cast<unsigned>(error));
}
} else {
MGLOG_E_ONCE("Shared images: a YUV conversion target is not renderable (0x%04x)",
static_cast<unsigned>(status));
}
g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
g_GLESFuncs.glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0);
}
Bool PublishPendingAccesses() {
ReadFrame& frame = CurrentReadFrame();
frame.ImplicitSync = true;
RenewYuvEpoch(frame);
// Sub-data waiting for a sync that nothing of this session would otherwise trigger.
if (t_native.boundContext != EGL_NO_CONTEXT) TextureImpl::WritePendingSharedImageUploads();
if (frame.Tracker.Pending() == 0) return true;
int fence = -1;
if (t_native.boundContext != EGL_NO_CONTEXT && !ExportFence(&fence)) {
// No fence to publish: the work is waited out here, and published as completed.
(void)WaitForCompletion();
}
frame.Tracker.PublishFrameAsWrite(fence);
frame.Id = g_readFrameIds.fetch_add(1, std::memory_order_relaxed) + 1;
return true;
}
Bool ExportNativeFence(int* fence) {
*fence = -1;
ReadFrame& frame = CurrentReadFrame();
RenewYuvEpoch(frame);
// No context current: nothing of this session is in flight to fence.
if (t_native.boundContext == EGL_NO_CONTEXT) return true;
TextureImpl::WritePendingSharedImageUploads();
int exported = -1;
if (!ExportFence(&exported)) {
// No descriptor to give: the work is waited out, so the caller may report it done.
exported = -1;
(void)WaitForCompletion();
}
// A session that writes images (one that ends frames at flushes) ends one here too; a
// compositor's reads stay for its swap, which comes after the fence's work anyway.
if (frame.ImplicitSync && frame.Tracker.Pending() != 0) {
frame.Tracker.PublishFrameAsWrite(exported >= 0 ? ::dup(exported) : -1);
frame.Id = g_readFrameIds.fetch_add(1, std::memory_order_relaxed) + 1;
}
*fence = exported;
return true;
}
void PublishPendingReads() {
ReadFrame& frame = CurrentReadFrame();
RenewYuvEpoch(frame);
if (frame.Tracker.Pending() == 0) return;
int fence = -1;
// No context current: the reads' context is gone (and its work with it), and there is
// nothing to fence or wait from - the frame is only forgotten.
if (t_native.boundContext != EGL_NO_CONTEXT && !ExportFence(&fence)) {
// FALLBACK: there is no fence to hand a writer, so the frame's reads are waited out
// here instead - once this session moves on (and, say, releases the buffer to its
// producer), nothing it submitted still samples the image.
(void)WaitForCompletion();
}
// A session that writes images publishes every boundary as a write: an image it rendered
// into must not leave the frame without its generation moving.
if (frame.ImplicitSync)
frame.Tracker.PublishFrameAsWrite(fence);
else
frame.Tracker.PublishFrame(fence);
frame.Id = g_readFrameIds.fetch_add(1, std::memory_order_relaxed) + 1;
}
Bool BlitDefaultFramebufferTo(const SharedImageView& view, const MG_Util::Damage::Region& region) {
if (view.Id == 0 || view.Width == 0 || view.Height == 0 || view.NativeBuffer == nullptr) return false;
if (!IsBackendContextCurrentOnThisThread()) {
MGLOG_E_ONCE("Shared image present: no backend context is current on the calling thread");
return false;
}
if (LatchIfDeviceLost("shared-image-present")) return false;
const EGLSurface surface = CurrentContextDrawSurface();
EGLint sourceWidth = 0;
EGLint sourceHeight = 0;
if (surface == EGL_NO_SURFACE || !g_EGLFuncs.eglQuerySurface ||
!g_EGLFuncs.eglQuerySurface(g_Display, surface, EGL_WIDTH, &sourceWidth) ||
!g_EGLFuncs.eglQuerySurface(g_Display, surface, EGL_HEIGHT, &sourceHeight) || sourceWidth <= 0 ||
sourceHeight <= 0) {
MGLOG_E_ONCE("Shared image present: the current context has no default framebuffer to copy");
return false;
}
const SI::ImageRef image = SI::Find(view.Id);
if (image == nullptr) {
MGLOG_E_ONCE("Shared image present: image %llu is no longer live",
static_cast<unsigned long long>(view.Id));
return false;
}
// WRITE AFTER READ: the frames that still sample the image finish before the copy
// overwrites it - queued on this context's GPU timeline, or waited out on the CPU when
// the driver has no fence to queue. Outside the targets' lock, which every session's
// present takes.
if (ResolvedEntryPoints().FencesUsable) {
for (const int fence : SI::DupPendingReadFences(*image))
WaitForFenceBeforeLaterCommands(fence);
} else if (!SI::WaitForReads(*image, kFenceCpuWaitMs)) {
MGLOG_E_ONCE("Shared image present: image %llu's readers did not finish within %u ms",
static_cast<unsigned long long>(view.Id), static_cast<unsigned>(kFenceCpuWaitMs));
}
Bool copied = false;
{
auto& targets = Targets();
const std::lock_guard<std::mutex> lock(targets.Mutex);
PruneLocked(targets);
auto it = targets.ById.find(view.Id);
if (it != targets.ById.end() && it->second.Image->Image->Native != view.NativeBuffer) {
DeleteTarget(it->second);
targets.ById.erase(it);
it = targets.ById.end();
}
if (it == targets.ById.end()) {
String why;
PresentTarget target;
target.Image = CreateEglImage(view.Id, why);
if (target.Image != nullptr) target.Renderbuffer = MakeRenderbuffer(*target.Image, why);
if (target.Renderbuffer == 0) {
MGLOG_E_ONCE("Shared image present: image %llu cannot be rendered to: %s",
static_cast<unsigned long long>(view.Id), why.c_str());
return false;
}
while (targets.ById.size() >= kMaxPresentTargets) {
auto oldest = targets.ById.begin();
for (auto candidate = targets.ById.begin(); candidate != targets.ById.end(); ++candidate) {
if (candidate->second.LastUse < oldest->second.LastUse) oldest = candidate;
}
DeleteTarget(oldest->second);
targets.ById.erase(oldest);
}
it = targets.ById.emplace(view.Id, std::move(target)).first;
}
it->second.LastUse = ++targets.UseClock;
// A target written before keeps what it was given; one new to this process is
// copied whole whatever the client asked (it cannot know the image's history here).
const Bool wholeTarget = !it->second.Written;
it->second.Written = true;
GLuint& framebuffer = g_presentFramebuffers.Current();
if (framebuffer == 0) g_GLESFuncs.glGenFramebuffers(1, &framebuffer);
if (framebuffer == 0) {
MGLOG_E_ONCE("Shared image present: glGenFramebuffers gave no name");
return false;
}
// Every binding goes through Espryt's shadows (BindFramebufferId, the scissor test's
// render-state shadow) and is put back by the guards, so the application's state and
// the caches that describe it are what they were. A blit is scissored like a draw.
ScopedScissorDisable scissorOff;
ScopedFramebufferBinding binding(/*saveRead=*/true, /*saveDraw=*/true);
FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, 0);
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, framebuffer);
g_GLESFuncs.glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
it->second.Renderbuffer);
const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER);
if (status == GL_FRAMEBUFFER_COMPLETE) {
// The default framebuffer's read buffer is context state the application may
// have set to GL_NONE; the copy reads the back buffer either way.
GLint readBuffer = GL_BACK;
g_GLESFuncs.glGetIntegerv(GL_READ_BUFFER, &readBuffer);
if (readBuffer != GL_BACK) g_GLESFuncs.glReadBuffer(GL_BACK);
DrainErrors();
const Bool sameExtent = static_cast<Uint32>(sourceWidth) == view.Width &&
static_cast<Uint32>(sourceHeight) == view.Height;
// TOP ROW FIRST: GL's top row (y = height - 1) lands in the image's first memory
// row, which an AHardwareBuffer-backed renderbuffer addresses as y = 0.
//
// Only where the image differs from the frame: the client keeps, per image, the
// damage it missed (BufferDamageTracker), and a partial blit keeps the rest of
// the renderbuffer as GL promises (checked texel for texel against the frame on
// Adreno). What once left old texels around the rectangles was the scissor
// guard above trusting another session's shadow: the copy was clipped, and
// the missed-damage bookkeeping then never brought those texels over again.
if (sameExtent && !wholeTarget && !region.IsFull()) {
// Each rectangle flipped the same way (an empty region copies nothing: the
// image already is the frame).
for (const MG_Util::Damage::FlippedCopy& copy :
MG_Util::Damage::FlippedCopies(region, sourceWidth, sourceHeight)) {
const MG_Util::Damage::Rect& from = copy.Source;
const MG_Util::Damage::Rect& to = copy.Destination;
g_GLESFuncs.glBlitFramebuffer(from.X, from.Y, from.X + from.Width, from.Y + from.Height,
to.X, to.Y + to.Height, to.X + to.Width, to.Y,
GL_COLOR_BUFFER_BIT, GL_NEAREST);
}
} else {
g_GLESFuncs.glBlitFramebuffer(0, 0, sourceWidth, sourceHeight, 0,
static_cast<GLint>(view.Height), static_cast<GLint>(view.Width),
0, GL_COLOR_BUFFER_BIT, sameExtent ? GL_NEAREST : GL_LINEAR);
}
const GLenum error = g_GLESFuncs.glGetError();
copied = error == GL_NO_ERROR;
if (readBuffer != GL_BACK) g_GLESFuncs.glReadBuffer(static_cast<GLenum>(readBuffer));
if (!copied) {
MGLOG_E_ONCE("Shared image present: glBlitFramebuffer %dx%d -> %ux%u failed (0x%04x)",
sourceWidth, sourceHeight, view.Width, view.Height, static_cast<unsigned>(error));
}
} else {
MGLOG_E_ONCE("Shared image present: image %llu's framebuffer is incomplete (0x%04x)",
static_cast<unsigned long long>(view.Id), static_cast<unsigned>(status));
}
g_GLESFuncs.glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, 0);
}
if (!copied) return false;
// The copy's completion goes to the readers as a fence and the present returns at once;
// a reader that binds the image waits for it on its own GPU. Without fence export the
// copy is waited out here and published as already complete.
int fence = -1;
if (!ExportFence(&fence) && !WaitForCompletion()) return false;
SI::PublishWrite(*image, fence);
// A session that samples images and presents into one (a nested compositor) closes its
// read frame here too.
PublishPendingReads();
return true;
}
void ForgetPresentTargetsAfterTerminate() {
auto& targets = Targets();
const std::lock_guard<std::mutex> lock(targets.Mutex);
// TargetIsCurrent is already false for every entry, so nothing reaches the driver.
for (auto& entry : targets.ById)
DeleteTarget(entry.second);
targets.ById.clear();
}
#else
EglImageRef CreateEglImage(Uint64 id, String& why) {
(void)id;
why = "shared images are imported through AHardwareBuffer, which only Android has";
return nullptr;
}
Bool TargetBoundTexture2D(const EglImageRef& image) {
(void)image;
return false;
}
Bool BlitDefaultFramebufferTo(const SharedImageView& view, const MG_Util::Damage::Region& region) {
(void)view, (void)region;
return false;
}
void AcquireForSampling(const EglImageRef& image) {
(void)image;
}
Bool IsYuv(const EglImageRef& image) {
(void)image;
return false;
}
void ConvertYuvForSampling(const EglImageRef& image, GLuint target, Uint32 width, Uint32 height) {
(void)image, (void)target, (void)width, (void)height;
}
void PublishPendingReads() {}
Bool PublishPendingAccesses() {
return true;
}
Bool ExportNativeFence(int* fence) {
*fence = -1;
return true;
}
void ForgetPresentTargetsAfterTerminate() {}
#endif
} // namespace SharedImageImpl
#endif
} // namespace MobileGL::MG_Backend::DirectGLES
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,682 @@
// MobileGL - MobileGL/MG_Backend/DirectGLES/DirectGLESSync.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "DirectGLESInternal.h"
namespace MobileGL::MG_Backend::DirectGLES {
namespace {
// Backend fence handle: a native ES sync plus the ES context
// generation it was created under.
struct GLESSyncObject {
GLsync esSync = nullptr;
Uint contextGeneration = 0;
};
// Backend timer-query handle: a native GL query object name plus the
// ES context generation it was created under. Stale-generation
// handles read as available with a zero result, and deleting them
// only frees the wrapper (the dead ES context already reclaimed the
// query object).
struct GLESQueryObject {
GLuint queryId = 0;
Uint contextGeneration = 0;
// Non-zero for the core (non-timer) query targets - occlusion and transform
// feedback primitives - and then holds the glBeginQuery target, which glEndQuery
// needs back. Their results are counts reachable only through the core
// glGetQueryObjectuiv getter: GL_EXT_disjoint_timer_query's 64-bit
// glGetQueryObjectui64vEXT is timer-specific and may be entirely absent on
// drivers that otherwise fully support these core ES queries.
GLenum coreTarget = 0;
};
} // namespace
// EGL ground-truth verification stamp. glvnd's eglGetCurrentContext performs
// fork detection with a real getpid() syscall on every call, and this
// predicate sits 2-3 deep in every draw - measured at 16% of the render
// thread on a live workload. NOT thread_local although the stamp is
// per-owner-thread state: a shared-library thread_local costs a
// __tls_get_addr call per access, which itself showed at 1.5% of the draw
// loop. Plain globals are equivalent because only the thread that passes the
// owner-identity check below can ever stamp or trust them, only one thread
// can be the owner at a time, and the ONLY writers of the owner id -
// MakeCurrent/ReleaseCurrent - reset the stamps before publishing a new
// owner, so a stamp can never leak across an ownership change. Atomics
// (relaxed) because the resets may come from a non-owner thread; the
// owner-id release/acquire pairing orders them.
std::atomic<Uint64> g_eglVerifiedFrameSerial{~0ull};
std::atomic<Uint> g_eglVerifiedContextGeneration{0};
Bool IsBackendContextCurrentOnThisThread() {
const NativeContextTuple& tuple = ActiveNativeContext();
if (tuple.Context == EGL_NO_CONTEXT) {
return false;
}
if (g_backendContextOwnerThread.load(std::memory_order_acquire) != std::this_thread::get_id()) {
return false;
}
// Belt and braces: EGL itself is the ground truth. A migration that bypassed
// MakeCurrent()/ReleaseCurrent() must not leave a stale ownership claim
// standing, or GL calls would silently no-op while shadow bookkeeping (bind
// cache, synced serials) still advances. Re-verify once per (frame, context
// generation) rather than per call: an external migration is caught at the
// next frame boundary instead of the next call, which recovers the
// bookkeeping just the same, without paying a syscall on every draw.
const Uint64 frameSerial = g_currentFrameSerial.load(std::memory_order_relaxed);
if (g_eglVerifiedFrameSerial.load(std::memory_order_relaxed) == frameSerial &&
g_eglVerifiedContextGeneration.load(std::memory_order_relaxed) == g_syncContextGeneration) {
return true;
}
if (g_EGLFuncs.eglGetCurrentContext && g_EGLFuncs.eglGetCurrentContext() != tuple.Context) {
return false;
}
g_eglVerifiedFrameSerial.store(frameSerial, std::memory_order_relaxed);
g_eglVerifiedContextGeneration.store(g_syncContextGeneration, std::memory_order_relaxed);
return true;
}
BackendSyncHandle FenceSync() {
// ES fences can only be created on the thread that owns the ES context
// (Flywheel and friends fence on the render thread, which does).
// Returning null makes the frontend fall back to an always-signaled
// sync object.
if (!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glFenceSync) {
return nullptr;
}
GLsync esSync = g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
if (esSync == nullptr) {
return nullptr;
}
return new GLESSyncObject{esSync, g_syncContextGeneration};
}
GLenum ClientWaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) {
const auto* sync = static_cast<GLESSyncObject*>(handle);
if (sync == nullptr) {
return GL_ALREADY_SIGNALED;
}
// The creating ES context is gone: its GPU work either completed or
// died with the context; waiting is meaningless either way.
if (sync->contextGeneration != g_syncContextGeneration) {
return GL_ALREADY_SIGNALED;
}
// Degraded path: a thread that does not own the ES context cannot
// issue GL calls, so report signaled instead of blocking on state we
// cannot observe. Fence waits normally arrive on the render thread,
// which owns the context.
if (!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glClientWaitSync) {
return GL_ALREADY_SIGNALED;
}
return g_GLESFuncs.glClientWaitSync(sync->esSync, flags & GL_SYNC_FLUSH_COMMANDS_BIT, timeout);
}
void WaitSync(BackendSyncHandle handle, GLbitfield flags, GLuint64 timeout) {
(void)flags;
(void)timeout;
const auto* sync = static_cast<GLESSyncObject*>(handle);
if (sync == nullptr || sync->contextGeneration != g_syncContextGeneration ||
!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glWaitSync) {
return;
}
// ES 3.0 requires flags == 0 and timeout == GL_TIMEOUT_IGNORED.
g_GLESFuncs.glWaitSync(sync->esSync, 0, GL_TIMEOUT_IGNORED);
}
void DeleteSync(BackendSyncHandle handle) {
auto* sync = static_cast<GLESSyncObject*>(handle);
if (sync == nullptr) {
return;
}
if (sync->contextGeneration == g_syncContextGeneration && IsBackendContextCurrentOnThisThread() &&
g_GLESFuncs.glDeleteSync) {
g_GLESFuncs.glDeleteSync(sync->esSync);
}
// Otherwise the ES sync is abandoned; the ES context reclaims all of
// its sync objects when it is destroyed.
delete sync;
}
Bool GetSyncStatus(BackendSyncHandle handle) {
const auto* sync = static_cast<GLESSyncObject*>(handle);
if (sync == nullptr || sync->contextGeneration != g_syncContextGeneration ||
!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glGetSynciv) {
return true;
}
GLint status = GL_SIGNALED;
GLsizei length = 0;
g_GLESFuncs.glGetSynciv(sync->esSync, GL_SYNC_STATUS, 1, &length, &status);
return status == GL_SIGNALED;
}
// GL timer queries, backed by GL_EXT_disjoint_timer_query. The desktop
// tokens from glext.h are used throughout: GL_TIME_ELAPSED (0x88BF),
// GL_TIMESTAMP (0x8E28), GL_QUERY_RESULT (0x8866) and
// GL_QUERY_RESULT_AVAILABLE (0x8867) are numerically identical to their
// _EXT counterparts.
Bool AreTimerQueriesSupported() {
return g_GLESCapabilities.SupportsDisjointTimerQuery && g_GLESFuncs.glGenQueries &&
g_GLESFuncs.glDeleteQueries && g_GLESFuncs.glBeginQuery && g_GLESFuncs.glEndQuery &&
g_GLESFuncs.glGetQueryObjectuiv && g_GLESFuncs.glQueryCounterEXT && g_GLESFuncs.glGetQueryObjectui64vEXT;
}
namespace {
// The entry point the resolved tier's support ships, or null when there is none.
MG_External::GLES::glTexBuffer_PTR ResolveTexBufferEntryPoint() {
using Tier = MG_External::GLESCapabilities::TextureBufferTier;
switch (g_GLESCapabilities.TextureBufferSupport) {
case Tier::ExtensionEXT:
return g_GLESFuncs.glTexBufferEXT ? g_GLESFuncs.glTexBufferEXT : g_GLESFuncs.glTexBuffer;
case Tier::ExtensionOES:
return g_GLESFuncs.glTexBufferOES ? g_GLESFuncs.glTexBufferOES : g_GLESFuncs.glTexBuffer;
case Tier::CoreEs32:
return g_GLESFuncs.glTexBuffer;
case Tier::None:
default:
return nullptr;
}
}
MG_External::GLES::glTexBufferRange_PTR ResolveTexBufferRangeEntryPoint() {
using Tier = MG_External::GLESCapabilities::TextureBufferTier;
switch (g_GLESCapabilities.TextureBufferSupport) {
case Tier::ExtensionEXT:
return g_GLESFuncs.glTexBufferRangeEXT ? g_GLESFuncs.glTexBufferRangeEXT : g_GLESFuncs.glTexBufferRange;
case Tier::ExtensionOES:
return g_GLESFuncs.glTexBufferRangeOES ? g_GLESFuncs.glTexBufferRangeOES : g_GLESFuncs.glTexBufferRange;
case Tier::CoreEs32:
return g_GLESFuncs.glTexBufferRange;
case Tier::None:
default:
return nullptr;
}
}
} // namespace
Bool AreBufferTexturesSupported() {
// Both halves matter. The tier is what the driver ADVERTISES, and it is only meaningful
// once the capabilities have been filled in; the resolved pointer is what MobileGL can
// actually call, through the spelling that tier's support ships. Gating on the
// unsuffixed name alone would call an entry point an EXT/OES driver never exported.
return g_GLESCapabilities.TextureBufferSupport != MG_External::GLESCapabilities::TextureBufferTier::None &&
ResolveTexBufferEntryPoint() != nullptr;
}
void CallTexBuffer(GLenum target, GLenum internalFormat, GLuint buffer) {
MG_External::GLES::glTexBuffer_PTR entryPoint = ResolveTexBufferEntryPoint();
if (entryPoint == nullptr) {
return;
}
entryPoint(target, internalFormat, buffer);
}
Bool CallTexBufferRange(GLenum target, GLenum internalFormat, GLuint buffer, GLintptr offset, GLsizeiptr size) {
MG_External::GLES::glTexBufferRange_PTR entryPoint = ResolveTexBufferRangeEntryPoint();
if (entryPoint == nullptr) {
return false;
}
entryPoint(target, internalFormat, buffer, offset, size);
return true;
}
const char* GetBufferTextureTierName() {
using Tier = MG_External::GLESCapabilities::TextureBufferTier;
switch (g_GLESCapabilities.TextureBufferSupport) {
case Tier::CoreEs32:
return "core (ES 3.2)";
case Tier::ExtensionEXT:
return "GL_EXT_texture_buffer";
case Tier::ExtensionOES:
return "GL_OES_texture_buffer";
case Tier::None:
default:
return "unsupported";
}
}
BackendQueryHandle BeginTimeElapsedQuery() {
// Query objects can only be created on the thread that owns the ES
// context (MC's F3 profiler queries on the render thread, which
// does). Returning null makes the frontend fall back to an
// immediately available zero result.
if (!IsBackendContextCurrentOnThisThread() || !AreTimerQueriesSupported()) {
return nullptr;
}
GLuint queryId = 0;
g_GLESFuncs.glGenQueries(1, &queryId);
if (queryId == 0) {
return nullptr;
}
g_GLESFuncs.glBeginQuery(GL_TIME_ELAPSED, queryId);
return new GLESQueryObject{queryId, g_syncContextGeneration};
}
void EndTimeElapsedQuery(BackendQueryHandle handle) {
const auto* query = static_cast<GLESQueryObject*>(handle);
if (query == nullptr || query->contextGeneration != g_syncContextGeneration ||
!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glEndQuery) {
return;
}
// ES tracks the active query per target, not per object, so the
// handle only guards the degraded paths above.
g_GLESFuncs.glEndQuery(GL_TIME_ELAPSED);
}
BackendQueryHandle QueryCounterTimestamp() {
if (!IsBackendContextCurrentOnThisThread() || !AreTimerQueriesSupported()) {
return nullptr;
}
GLuint queryId = 0;
g_GLESFuncs.glGenQueries(1, &queryId);
if (queryId == 0) {
return nullptr;
}
g_GLESFuncs.glQueryCounterEXT(queryId, GL_TIMESTAMP);
return new GLESQueryObject{queryId, g_syncContextGeneration};
}
// The core (non-timer) query targets - occlusion and transform feedback primitives.
// Unlike the timer queries above these are core ES (no GL_EXT_disjoint_timer_query
// needed), so they share one begin/end pair keyed on the glBeginQuery target.
static Bool AreCoreQueriesSupported() {
return g_GLESFuncs.glGenQueries && g_GLESFuncs.glDeleteQueries && g_GLESFuncs.glBeginQuery &&
g_GLESFuncs.glEndQuery && g_GLESFuncs.glGetQueryObjectuiv;
}
static BackendQueryHandle BeginCoreQuery(GLenum target) {
if (!IsBackendContextCurrentOnThisThread() || !AreCoreQueriesSupported()) {
return nullptr;
}
GLuint queryId = 0;
g_GLESFuncs.glGenQueries(1, &queryId);
if (queryId == 0) {
return nullptr;
}
g_GLESFuncs.glBeginQuery(target, queryId);
return new GLESQueryObject{queryId, g_syncContextGeneration, target};
}
static void EndCoreQuery(BackendQueryHandle handle) {
const auto* query = static_cast<GLESQueryObject*>(handle);
if (query == nullptr || query->coreTarget == 0 || query->contextGeneration != g_syncContextGeneration ||
!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glEndQuery) {
return;
}
g_GLESFuncs.glEndQuery(query->coreTarget);
}
BackendQueryHandle BeginOcclusionQuery() {
// ES only implements the boolean ANY_SAMPLES_PASSED variant, not an exact
// GL_SAMPLES_PASSED count; the frontend already coerces ANY_SAMPLES_PASSED*
// targets to boolean, and desktop GL_SAMPLES_PASSED reads a 0/1 approximation.
return BeginCoreQuery(GL_ANY_SAMPLES_PASSED);
}
void EndOcclusionQuery(BackendQueryHandle handle) {
EndCoreQuery(handle);
}
// GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN / GL_PRIMITIVES_GENERATED. The frontend
// otherwise counts primitives on the CPU from the draw calls, which cannot see a
// geometry shader's amplification; the real driver's counters are exact. Returning
// null keeps that CPU accounting as the fallback.
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated) {
// GL_PRIMITIVES_GENERATED is only a legal query target from ES 3.2 on (it comes
// with geometry shaders); issuing it earlier just leaves a stray GL_INVALID_ENUM
// that some later unrelated glGetError would report as its own failure.
if (generated && g_GLESCapabilities.GLESVersion.Major * 10 + g_GLESCapabilities.GLESVersion.Minor < 32) {
return nullptr;
}
return BeginCoreQuery(generated ? GL_PRIMITIVES_GENERATED : GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
}
void EndXfbPrimitivesQuery(BackendQueryHandle handle) {
EndCoreQuery(handle);
}
Bool IsQueryResultAvailable(BackendQueryHandle handle) {
const auto* query = static_cast<GLESQueryObject*>(handle);
// Null/stale handles report available so the frontend proceeds to
// GetQueryResult64, which finalizes them as zero. A thread that does
// not own the ES context also reports available: GetQueryResult64
// then returns false and the frontend keeps the handle for a later
// read from the owning thread.
if (query == nullptr || query->contextGeneration != g_syncContextGeneration ||
!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glGetQueryObjectuiv) {
return true;
}
GLuint available = GL_FALSE;
g_GLESFuncs.glGetQueryObjectuiv(query->queryId, GL_QUERY_RESULT_AVAILABLE, &available);
return available != GL_FALSE;
}
Bool GetQueryResult64(BackendQueryHandle handle, Bool wait, Uint64* outNanoseconds) {
*outNanoseconds = 0;
const auto* query = static_cast<GLESQueryObject*>(handle);
// Null handles never had a GL query object, handles from a
// since-destroyed ES context lost theirs, and missing entry points
// can never produce a reading (belt and braces: the creators already
// require them): zero is the FINAL result in all three cases, so
// report it as produced and let the frontend cache it and release
// the handle.
if (query == nullptr || query->contextGeneration != g_syncContextGeneration ||
!g_GLESFuncs.glGetQueryObjectuiv || (query->coreTarget == 0 && !g_GLESFuncs.glGetQueryObjectui64vEXT)) {
return true;
}
// A thread that does not own the ES context cannot issue GL calls,
// but the result still lands on the owning context eventually: report
// "not obtainable yet" so the frontend keeps the handle and a later
// availability poll / result read from the owning thread can still
// produce the real value.
if (!IsBackendContextCurrentOnThisThread()) {
return false;
}
if (wait) {
// Reading GL_QUERY_RESULT blocks in the driver until the result
// lands, but only after the commands were flushed; flush once,
// then poll availability for a bounded ~100ms before dropping to
// a glFinish as the last resort (ClientWaitSync has no polling
// loop to mirror - it delegates its timeout to the driver, which
// a query-object read cannot do).
if (g_GLESFuncs.glFlush) {
g_GLESFuncs.glFlush();
}
constexpr Int kMaxAvailabilityPolls = 1000; // ~100ms at 100us per poll
GLuint available = GL_FALSE;
for (Int i = 0; i < kMaxAvailabilityPolls && available == GL_FALSE; ++i) {
g_GLESFuncs.glGetQueryObjectuiv(query->queryId, GL_QUERY_RESULT_AVAILABLE, &available);
if (available == GL_FALSE) {
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
}
if (available == GL_FALSE && g_GLESFuncs.glFinish) {
g_GLESFuncs.glFinish();
}
}
// GL_EXT_disjoint_timer_query's GPU_DISJOINT_EXT signal is
// deliberately ignored: after a disjoint event (power state change,
// context switch) the result may be garbage, which is tolerable for
// an F3 GPU% readout, and consuming the latched flag here could hide
// the event from another observer.
if (query->coreTarget != 0) {
GLuint result32 = 0;
g_GLESFuncs.glGetQueryObjectuiv(query->queryId, GL_QUERY_RESULT, &result32);
*outNanoseconds = static_cast<Uint64>(result32);
return true;
}
GLuint64 result = 0;
g_GLESFuncs.glGetQueryObjectui64vEXT(query->queryId, GL_QUERY_RESULT, &result);
*outNanoseconds = static_cast<Uint64>(result);
return true;
}
void DeleteBackendQuery(BackendQueryHandle handle) {
auto* query = static_cast<GLESQueryObject*>(handle);
if (query == nullptr) {
return;
}
if (query->contextGeneration == g_syncContextGeneration && IsBackendContextCurrentOnThisThread() &&
g_GLESFuncs.glDeleteQueries) {
g_GLESFuncs.glDeleteQueries(1, &query->queryId);
}
// Otherwise the GL query object is abandoned; the ES context reclaims
// all of its query objects when it is destroyed.
delete query;
}
Int64 GetGpuTimestampNs() {
// Synchronous GPU clock sample; 0 tells the frontend GL_TIMESTAMP
// getter to fall back.
if (!IsBackendContextCurrentOnThisThread() || !AreTimerQueriesSupported() || !g_GLESFuncs.glGetInteger64v) {
return 0;
}
GLint64 timestamp = 0;
g_GLESFuncs.glGetInteger64v(GL_TIMESTAMP, &timestamp);
return static_cast<Int64>(timestamp);
}
Uint64 CurrentFrameSerial() {
return g_currentFrameSerial.load(std::memory_order_relaxed);
}
Uint64 CompletedFrameSerial() {
return g_completedFrameSerial.load(std::memory_order_relaxed);
}
namespace {
// THE WATERMARK IS CONTIGUOUS. Fences signal in submission order only within ONE context,
// and the serials are handed out to every native context of every session in turn, so the
// highest signaled serial says nothing about an older fence of another context still
// pending. A serial is complete when no live fence at or below it is pending.
void PollFrameFences() {
if (!g_GLESFuncs.glGetSynciv) return;
Uint64 lowestPending = ~Uint64{0};
for (FrameFence& slot : g_frameFenceRing) {
if (!slot.sync) continue;
if (slot.contextGeneration != g_syncContextGeneration) {
slot.sync = nullptr; // its context reclaimed it, and its work is over
continue;
}
GLint status = GL_SIGNALED;
GLsizei length = 0;
g_GLESFuncs.glGetSynciv(slot.sync, GL_SYNC_STATUS, 1, &length, &status);
if (status == GL_SIGNALED) {
if (g_GLESFuncs.glDeleteSync) g_GLESFuncs.glDeleteSync(slot.sync);
slot.sync = nullptr;
} else if (slot.serial < lowestPending) {
lowestPending = slot.serial;
}
}
const Uint64 completed =
lowestPending == ~Uint64{0} ? g_currentFrameSerial.load(std::memory_order_relaxed) : lowestPending - 1;
if (completed > g_completedFrameSerial.load(std::memory_order_relaxed)) {
g_completedFrameSerial.store(completed, std::memory_order_relaxed);
}
}
FrameFence* OldestPendingFrameFence() {
FrameFence* oldest = nullptr;
for (FrameFence& slot : g_frameFenceRing) {
if (!slot.sync || slot.contextGeneration != g_syncContextGeneration) continue;
if (!oldest || slot.serial < oldest->serial) oldest = &slot;
}
return oldest;
}
} // namespace
Bool WaitForFrameSerialCompleted(Uint64 serial, Uint64 timeoutNs) {
if (CompletedFrameSerial() >= serial) return true;
if (!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glClientWaitSync) return false;
// Oldest pending fence first: the watermark can only pass a serial once every older fence
// (of any context) has signaled. Each fence was flushed when it was made (Present's swap),
// so waiting from this context needs no flush of the other.
for (int guard = 0; guard < kFrameFenceRingDepth + 1; ++guard) {
PollFrameFences();
if (CompletedFrameSerial() >= serial) return true;
FrameFence* oldest = OldestPendingFrameFence();
if (!oldest) return false;
const GLenum status = g_GLESFuncs.glClientWaitSync(oldest->sync, GL_SYNC_FLUSH_COMMANDS_BIT, timeoutNs);
if (status != GL_ALREADY_SIGNALED && status != GL_CONDITION_SATISFIED) return false;
}
PollFrameFences();
return CompletedFrameSerial() >= serial;
}
#if MOBILEGL_BUILD_DISAGGREGATED
namespace {
// Once per thread: a latched session declines everything after this verb anyway, and the
// apply thread is the session's own.
thread_local Bool t_deviceLossLatched = false;
Bool LatchOnResetStatus(const char* site, GLenum status) {
if (status == GL_NO_ERROR) return false;
// ONLY THE GUILTY CONTEXT ENDS ITS SESSION. The contexts ask for reset notification
// (EnsureNativeContext) so the driver can name the context whose work hung or faulted the
// GPU; a reset it reports to an INNOCENT context - one whose work only waited behind the
// stall, the compositor's above all - is noted and the session goes on as it did before
// contexts were told. Adreno 750 measured: the guilty context reads GUILTY once, after
// which the driver drops everything it submits; innocent ones read NO_ERROR.
static thread_local Bool innocentNoted = false;
if (status == GL_INNOCENT_CONTEXT_RESET) {
if (!innocentNoted) {
innocentNoted = true;
MGLOG_W("Espryt:%s - the driver reports a GPU reset this session's context did not cause "
"(GL_INNOCENT_CONTEXT_RESET); the session goes on",
site != nullptr ? site : "?");
}
return false;
}
t_deviceLossLatched = true;
(void)MG_Pipe::MGPipeSessionLatch(MG_Pipe::MGPipeFatalFamily::DeviceLost,
"MGPipe: Fatal{BackendDeviceLost, \"Espryt:%s\"} - this session's EGL "
"context was reset (glGetGraphicsResetStatus 0x%x); the session ends "
"and its client reads a lost context, every other session keeps running",
site != nullptr ? site : "?", static_cast<unsigned>(status));
return true;
}
} // namespace
Bool LatchIfDeviceLost(const char* site, Bool debugKnob) {
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return false;
if (t_deviceLossLatched) return true;
GLenum status = GL_NO_ERROR;
if (g_GLESFuncs.glGetGraphicsResetStatus != nullptr && IsBackendContextCurrentOnThisThread()) {
status = g_GLESFuncs.glGetGraphicsResetStatus();
}
if (status == GL_NO_ERROR && debugKnob && MG_Pipe::MGPipeDebugDeviceLossDue())
status = GL_UNKNOWN_CONTEXT_RESET;
return LatchOnResetStatus(site, status);
}
Bool LatchIfGuiltyBeforeApply() {
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return false;
if (t_deviceLossLatched) return true;
// Asked every time the apply thread wakes, so it must leave nothing behind: not
// IsBackendContextCurrentOnThisThread, whose once-per-frame EGL verification stamp it would
// spend at the top of the loop (where the session may be between two of its contexts) - only
// whether this thread holds a native context and the backend's ownership.
if (g_GLESFuncs.glGetGraphicsResetStatus == nullptr || t_native.boundContext == EGL_NO_CONTEXT ||
g_backendContextOwnerThread.load(std::memory_order_acquire) != std::this_thread::get_id())
return false;
return LatchOnResetStatus("apply", g_GLESFuncs.glGetGraphicsResetStatus());
}
#endif
void Present() {
#if MOBILEGL_BUILD_DISAGGREGATED
if (LatchIfDeviceLost("present", /*debugKnob=*/false)) return;
#endif
// Insert one fence per frame BEFORE the swap (eglSwapBuffers' implicit flush
// makes it reachable), then non-blocking-poll prior frames' fences AFTER to
// advance the completed-frame watermark that gates buffer-pool recycling.
const Bool canFence = IsBackendContextCurrentOnThisThread() && g_GLESFuncs.glFenceSync;
Uint64 presentedSerial = 0;
if (canFence) {
const Uint64 serial = g_currentFrameSerial.fetch_add(1, std::memory_order_relaxed) + 1;
presentedSerial = serial;
FrameFence& slot = g_frameFenceRing[serial % kFrameFenceRingDepth];
if (slot.sync && slot.contextGeneration == g_syncContextGeneration) {
// The slot's fence is a ring-depth of serials old and may be another context's:
// dropping it unsignaled would let the watermark pass work nobody waited for.
if (g_GLESFuncs.glClientWaitSync) {
constexpr Uint64 kOldFenceWaitNs = 100ull * 1000 * 1000;
(void)g_GLESFuncs.glClientWaitSync(slot.sync, GL_SYNC_FLUSH_COMMANDS_BIT, kOldFenceWaitNs);
}
if (g_GLESFuncs.glDeleteSync) g_GLESFuncs.glDeleteSync(slot.sync);
}
slot = {g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0), g_syncContextGeneration, serial};
}
#if MOBILEGL_BUILD_DISAGGREGATED
// The session's frame boundary: the shared images its frame sampled get the frame's fence,
// so their producers' next copies wait for it.
SharedImageImpl::PublishPendingReads();
#endif
// P14 S4: the session's surface, not the process's.
//
// EXT -> KHR -> plain. The loader fills eglSwapBuffersWithDamageEXT from whichever
// spelling the driver provides (Adreno exposes only eglSwapBuffersWithDamageKHR), so
// the null check below is the whole chain: a driver with neither presents with plain
// eglSwapBuffers. Passing n_rects == 0 is not a partial present - the extension
// defines it as "the entire surface is implicitly damaged and the behaviour is
// equivalent to calling eglSwapBuffers" - and the wire protocol carries no damage
// rectangles for the server to forward, so the full surface is what is meant anyway.
//
// The damage is the client's, from the present record (CurrentPresentDamage): GL window
// coordinates, which is what the native call takes. Whole surface = no rectangles.
//
// ONLY A WINDOW SURFACE TAKES RECTANGLES: the platform's swap-with-damage hands them to the
// surface's native window, and a pbuffer has none (Android's dereferences it regardless).
NativeSessionState& session = ActiveNativeSession(); // before the lock: it takes it too
const EGLSurface drawSurface = session.Draw;
Bool windowSurface = false;
if (drawSurface != EGL_NO_SURFACE) {
const std::lock_guard<std::mutex> lock(g_nativeRegistryMutex);
if (const auto* entry = FindSessionSurfaceLocked(session, drawSurface))
windowSurface = entry->Window != static_cast<NativeWindowType>(0);
}
EGLBoolean mglSwapped = EGL_FALSE;
if (g_EGLFuncs.eglSwapBuffersWithDamageEXT != nullptr) {
const MG_Util::Damage::Region& damage = MG_Backend::CurrentPresentDamage();
EGLint rects[MG_Util::Damage::kMaxRects * 4] = {};
const EGLint count = static_cast<EGLint>(MG_Util::Damage::NativeSwapRects(windowSurface, damage, rects));
mglSwapped =
g_EGLFuncs.eglSwapBuffersWithDamageEXT(g_Display, drawSurface, count > 0 ? rects : nullptr, count);
// A DRIVER THAT NAMES THE ENTRY POINT AND THEN REFUSES THE SWAP. Some vendor EGLs
// export eglSwapBuffersWithDamageEXT and answer EGL_FALSE on a window surface without
// setting an error (measured on a 4.14 vendor kernel with an Adreno driver; the plain
// entry point presents the same surface happily). Keeping the frame on that answer
// queues nothing at all: the compositor composites into a window whose buffer queue
// never sees a buffer. Fall back to the plain call instead of dropping the frame.
if (mglSwapped == EGL_FALSE && g_EGLFuncs.eglSwapBuffers != nullptr) {
MGLOG_W_ONCE("DirectGLES: this driver's eglSwapBuffersWithDamageEXT refused a window "
"surface; presenting through eglSwapBuffers for the rest of the process");
mglSwapped = g_EGLFuncs.eglSwapBuffers(g_Display, drawSurface);
}
} else if (g_EGLFuncs.eglSwapBuffers != nullptr) {
mglSwapped = g_EGLFuncs.eglSwapBuffers(g_Display, drawSurface);
}
NoteDrawSurfaceMoved(); // a window surface takes a new size at its swap
if (drawSurface != EGL_NO_SURFACE) {
const std::lock_guard<std::mutex> lock(g_nativeRegistryMutex);
if (auto* entry = FindSessionSurfaceLocked(session, drawSurface)) ++entry->Presents;
}
// A frame boundary: the next frame's age is asked anew, and its window buffer use flushed.
t_ageAskedSurface = EGL_NO_SURFACE;
t_windowBufferFlushIssued = false;
// P15: MOBILEGL_FRAMES_IN_FLIGHT, Espryt's half. Magma bounds the frames the GPU has
// queued with its frame contexts (frame S+1 reuses the context of frame S+1-N); this is
// the same bound on the ES driver: after presenting S, wait for S+1-N to complete. The
// platform's own queueBuffer throttle already waits for S-1, so only N = 1 is stricter
// than what Android does by itself; larger N is the same contract stated, not a change.
const Uint32 framesInFlight = MG_Config::Features.FramesInFlight;
if (presentedSerial != 0 && framesInFlight != 0 && framesInFlight < static_cast<Uint32>(kFrameFenceRingDepth) &&
g_GLESFuncs.glClientWaitSync && presentedSerial + 1 > framesInFlight) {
const Uint64 target = presentedSerial + 1 - framesInFlight;
const FrameFence& oldest = g_frameFenceRing[target % kFrameFenceRingDepth];
if (oldest.sync && oldest.serial == target && oldest.contextGeneration == g_syncContextGeneration) {
constexpr Uint64 kFramesInFlightWaitNs = 1000ull * 1000 * 1000;
(void)g_GLESFuncs.glClientWaitSync(oldest.sync, GL_SYNC_FLUSH_COMMANDS_BIT, kFramesInFlightWaitNs);
}
}
if (canFence) PollFrameFences();
// After the watermark advanced: retire grown-away ring stores and record the
// frame's ring high-water marks for slot reclamation.
BufferImpl::UboRingOnPresent();
BufferImpl::UnpackRingOnPresent();
BufferImpl::UploadRingOnPresent();
BufferImpl::TrimBufferPool();
// THE frame boundary for the MGPipe counters: publish this frame's plots, fold the
// frame into the run totals and, every 120th frame, emit the summary line.
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::OnPresent();
}
}
} // namespace MobileGL::MG_Backend::DirectGLES
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,675 @@
// MobileGL - MobileGL/MG_Backend/DirectGLES/ManagersInternal.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include "Managers.h"
#include "SubgroupPolicy.h"
#include <MG_Backend/MGPipe/PipeInputs.h>
// P3a: the handle-shaped resource op table, the applier's records and the reverse channel.
// Both headers are compiled into the library only under push, so they are included here
// under the same condition - a pull build must gain no declaration it cannot link.
#include <MG_Pipe/MGPipeCallbacks.h>
#include <MG_Pipe/MGPipeHostSpan.h>
#include <MG_Pipe/PipeApply.h>
// P5e (pg): the DEFINITION of the archive MGPipeShaderCsoRecord holds by SharedPtr. PipeApply.h
// needs only the name - the deleter is captured where the archive is constructed - but the
// program build reads through it, so the one translation unit that does needs the whole type.
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
// R-11's server-owned staging copy. Header-only and package v1's; see its own header block for
// why GLESBufferResource does not simply gain a member.
#include <MG_Backend/Record/StagedShadow.h>
#include <MG_Backend/Record/StagedTextureStore.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Remote/Server/ServerLoop.h>
// P5c (ct): object_death's producer (CONTRACT-P5C.md §5.2) - the death notice's split arm
// emits the record through the client's emit helper instead of hopping a stack struct to the
// apply thread.
#include <MG_Remote/Client/WireTables.h>
// P11 B2 (T0): the client's AHardwareBuffer store - its reference counting and, on Android, the
// allocation the POST self-test makes.
#include <MG_Remote/Transport/AdoptT0.h>
#if defined(__ANDROID__)
#include <android/hardware_buffer.h>
#endif
#endif
#include "Utils.h"
#include "DirectGLES.h"
#include "BackendObject_DirectGLES.h"
#include <Config.h>
#include <MG_State/GLState/StateObjectDeathNotice.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/DataTypeConverter.h>
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToGL/ProgramEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToStr/FramebufferEnumConverter.h>
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
#include <MG_Util/Converters/GLToMG/FramebufferEnumConverter.h>
#include <MG_Util/Converters/MGToGL/FramebufferEnumConverter.h>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdlib>
#include <limits>
#include <map>
#include <mutex>
#include <cstring>
#include <regex>
#include "ContextEpoch.h"
namespace MobileGL::MG_Backend::DirectGLES {
constexpr const char* BASE_INSTANCE_UNIFORM_NAME = "mg_BaseInstance";
constexpr const char* DRAW_ID_UNIFORM_NAME = "mg_DrawID";
constexpr const char* BASE_VERTEX_UNIFORM_NAME = "mg_BaseVertex";
constexpr const char* BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME = "mg_BaseInstanceWordIndex";
constexpr const char* INDIRECT_PARAMS_BLOCK_NAME = "mg_IndirectParams";
Bool ViewportArrayEmulationEnabled();
GLenum ResolveBackendMinFilter(const SamplerParameters& samplerParams, Bool avoidMipmapMinFilter);
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType);
constexpr const char* VIEWPORT_PASS_MASK_UNIFORM_NAME = "mg_ViewportPassMask";
Bool PromoteViewportIndexGlobalToVarying(String& source);
Bool InjectViewportIndexPassGate(String& source);
namespace BufferImpl {
using MG_State::GLState::BackendBufferResource;
using MG_State::GLState::BufferObject;
extern Uint g_bufferContextGeneration;
// --- Persistent-mapped bump rings (see Managers.h) -----------------------
// Shared machinery behind BOTH the global-UBO ring and the texture
// unpack-PBO ring: one EXT_buffer_storage persistent|coherent map per ring,
// monotonic head/tail cursors, and reclamation riding the Present()
// frame-fence watermark. The two rings differ only in size cap, offset
// alignment and log label - the reclamation, context-loss and
// emergency-drain rules are the part that was hard to get right, so they
// are shared rather than copied.
constexpr SizeT kUboRingInitialBytes = 4u * 1024u * 1024u;
constexpr SizeT kUboRingMaxBytes = 64u * 1024u * 1024u;
constexpr SizeT kUnpackRingInitialBytes = 4u * 1024u * 1024u;
constexpr SizeT kUnpackRingMaxBytes = 64u * 1024u * 1024u;
// A PBO-sourced glTexSubImage constrains the offset only to the pixel
// TYPE's size (GL_INVALID_OPERATION otherwise), and no ES client type is
// wider than 4 bytes - unlike a UBO bind, which owes the driver
// GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT. 64 covers every type with room to
// spare and keeps consecutive staged blocks off each other's cache lines.
constexpr SizeT kUnpackRingAlignment = 64;
// glCopyBufferSubData carries no offset-alignment requirement at all; 64
// keeps staged blocks cache-line separated, same as the unpack ring.
constexpr SizeT kUploadRingInitialBytes = 4u * 1024u * 1024u;
constexpr SizeT kUploadRingMaxBytes = 64u * 1024u * 1024u;
constexpr SizeT kUploadRingAlignment = 64;
struct PersistentRingStore {
Uint id = 0;
Uint8* mappedPtr = nullptr;
SizeT size = 0;
// Monotonic linear cursors: `head` counts every byte ever allocated
// (incl. wrap padding); everything below `tail` is GPU-complete. Ring
// offset of a linear position is pos % size, so in-flight bytes are
// head - tail and must stay <= size.
Uint64 head = 0;
Uint64 tail = 0;
Uint32 generation = 0; // bumped on every (re)create/grow; 0 = never valid
Uint contextGeneration = 0;
SizeT alignment = 256;
// A hard storage-creation failure under this context; stop retrying
// per use (cleared when the context generation moves on).
Bool creationFailed = false;
};
// Grown-away ring stores: deletable only once the GPU finished the last
// frame that could reference them (same watermark as the buffer pool).
struct RetiredRingStore {
Uint id = 0;
Uint contextGeneration = 0;
Uint64 retireSerial = 0;
};
// Present()-time high-water marks: every byte below headAtPresent was
// written during frames <= frameSerial, so once frameSerial completes,
// tail may advance to headAtPresent. FIFO by construction.
struct RingFrameMark {
Uint64 frameSerial = 0;
Uint64 headAtPresent = 0;
};
// One ring: its live store, its two reclamation lists, and the immutable
// knobs that tell it apart from the other one.
struct PersistentRing {
PersistentRingStore store;
Vector<RetiredRingStore> retired;
Vector<RingFrameMark> frameMarks;
SizeT initialBytes = 0;
SizeT maxBytes = 0;
// 0: take the offset alignment from GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT
// at store-creation time (the UBO ring's binds require it).
SizeT fixedAlignment = 0;
const char* label = "";
};
// THE RINGS ARE PER NATIVE CONTEXT. Their slots are reclaimed by frame marks recorded at
// Present, which say "everything this context wrote before its fence N is done once N is".
// One process-wide set used to be shared by every session's contexts: a fence from one
// context retired bytes another context's not-yet-executed copies still had to read
// (a glTexSubImage from the unpack ring is a deferred GPU read), and the next session's
// staging overwrote them - its pixels landed, at the wrong offsets, in the first one's
// textures. With one set per context, a context's bytes wait for that context's own
// fence. (The buffers themselves are shared objects; only who stages into them is split.)
// Staging ring for app buffer updates whose destination store may still be
// referenced by in-flight GPU work. Mali's glBufferSubData resolves that WAR
// hazard by BLOCKING in the call (osup_sync_object_wait) until every
// referencing job retires - under Minecraft 26.3's per-frame UBO and
// chunk-mesh SubData streams that serialized whole frames (~1 fps while
// chunks stream in). Staging the bytes here and issuing a
// glCopyBufferSubData instead keeps the hazard on the GPU timeline where it
// is just job ordering, and the CPU never waits.
struct RingSet {
PersistentRing ubo{{}, {}, {}, kUboRingInitialBytes, kUboRingMaxBytes, 0, "Global-UBO ring"};
PersistentRing unpack{
{}, {}, {}, kUnpackRingInitialBytes, kUnpackRingMaxBytes, kUnpackRingAlignment, "Texture unpack ring"};
PersistentRing upload{
{}, {}, {}, kUploadRingInitialBytes, kUploadRingMaxBytes, kUploadRingAlignment, "Buffer upload ring"};
RingSet();
~RingSet();
RingSet(const RingSet&) = delete;
RingSet& operator=(const RingSet&) = delete;
};
extern DirectGLES::PerNativeContext<RingSet> g_ringSets;
namespace {
Bool CanTouchGLNow() {
return DirectGLES::IsBackendContextCurrentOnThisThread();
}
} // namespace
MG_Record::StagedShadowStore& ServerStaged();
namespace {
// THE COVERAGE RULE FOR THE THREE-TIER FLUSH DRAIN, ASSERTED BEFORE THE CALL AND NOT
// INSIDE IT. FlushPendingRangesFrom is a G5-pinned body (p3a_untouched_regions.sh's
// PINNED_FUNCTIONS, ID-41): the split arm CALLS it, it does not re-spell it, and it
// does not add a line to it either. Its tier-1 arm - the range-invalidating map -
// copies with its own Memcpy and never reaches UploadRangeFrom, so the one tier that
// DECLARES THE OLD BYTES DEAD is the one tier no later check can see; the only place
// left to say "every queued range is inside the staged coverage" is the instant before
// the drain takes the queue. The clamp is the drain's own (limit = the smaller of the
// frontend size and the backend store; bytes past either end have nowhere to land and
// are not this rule's subject), so the two agree about which bytes are meant.
//
// Fires only for a base that IS this resource's server shadow: RequireCoverage
// answers nothing for the legacy arm's MappedData(), which is valid for the whole
// store. `pendingMutex` is taken here and released before the drain takes it again.
void RequireStagedCoverageForPendingRanges(GLESBufferResource& resource, const Uint8* hostBase,
SizeT frontendSize, const char* site) {
if (hostBase == nullptr) return;
const SizeT limit = std::min(frontendSize, resource.storageSize);
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
for (const auto& range : resource.pendingRanges) {
const SizeT end = std::min(range.end, limit);
const SizeT start = std::min(range.start, end);
if (start == end) continue;
ServerStaged().RequireCoverage(&resource, hostBase, start, end, site);
}
}
// The four hostBytes sites read the same in both builds. The non-split expansion is the
// ORIGINAL EXPRESSION, character for character - `raw - offset` - so a push or verify build
// compiles exactly what it compiled before R-11 and the split arm is the only new behaviour.
#define MGL_SERVER_STAGED_ADOPT(res, width, bytes, offset, size) \
ServerStaged().Adopt(&(res), (width), (bytes), (offset), (size))
#define MGL_SERVER_STAGED_REQUIRE_PENDING(res, base, frontendSize, site) \
RequireStagedCoverageForPendingRanges((res), (base), (frontendSize), (site))
} // namespace
// Ring machinery shared with the UBO/unpack rings; defined further down in
// this same unnamed namespace.
Bool RingAllocate(PersistentRing& ring, SizeT size, SizeT& outOffset);
// Push every queued range of `resource` from the shadow into the backend
// store, without ever letting a driver resolve the WAR hazard against
// in-flight frames at the WHOLE BUFFER's expense. Three tiers:
//
// 1. glMapBufferRange(WRITE | INVALIDATE_RANGE) + memcpy. The entire
// mapped range is rewritten from the authoritative shadow, so
// declaring its old bytes dead is exact - and it lets the driver
// swap fresh pages in for JUST that range. This is the only tier
// whose cost scales with the RANGE on this Mali driver: both the
// immediate glBufferSubData (pre-queueing) and a staged
// glCopyBufferSubData into a busy MUTABLE store ghost the whole
// destination with a worker-thread memcpy - Minecraft 26.3 streams
// ~1MB section meshes into 128MB arenas about nine times a frame
// during a camera pan, and 9 x 128MB of ghosting per frame is
// ~380ms, the measured 2-4 fps. (Backing the arenas with immutable
// stores also kills the ghost, but eagerly commits every arena's
// full extent - +hundreds of MB - which LMK'd the whole device.)
// 2. The staging ring + glCopyBufferSubData: the copy is ordered on
// the GPU timeline, no CPU wait (MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH
// forces this tier as the map path's negative control).
// 3. Direct glBufferSubData (potentially stalling) when neither the
// map entry points nor the ring exist.
//
// The ranges are flushed AS QUEUED (VecRange1D::Add already merges
// near-adjacent ones): bytes, not flush calls, are the cost axis here,
// and collapsing a scattered flush into its union re-copied nearly whole
// chunk-mesh arenas every frame.
// The caller owns syncedChangeSerial; this only drains the queue.
//
// ONE body for both arms, and deliberately so: the three-tier decision (whole-buffer
// orphan map / >= 128 KiB range-invalidating map / staged ring copy) is what the MC
// 26.3 p99 depends on, and a second copy of it for the handle arm is exactly how a
// tier silently changes. The arms differ only in where `hostBase` and `frontendSize`
// come from.
//
// R-11'S PARAMETER, AND IT IS A PARAMETER RATHER THAN AN ASSUMPTION (P5 b1).
// `hostBase` is re-read at every use today precisely because a shadow resize or an
// adoption moves what an earlier base pointed at. Under split the server may hold
// no pointer into the client's shadow at all, so the base becomes a SNAPSHOT taken
// into SEG_STAGE at emission - and a snapshot covers a RANGE, not the store. The
// two extra arguments say which range `hostBase` is good for; the default is the
// whole store, which is exactly what a live shadow is, so every caller today is
// byte-identical. The moment w1 passes a real snapshot extent, tier 1's widening
// refusal below stops being unreachable and a too-narrow snapshot is named instead
// of silently clobbering GPU-written bytes with stale ones.
constexpr SizeT kHostBaseCoversWholeStore = ~static_cast<SizeT>(0);
void FlushPendingRangesFrom(GLESBufferResource& resource, const Uint8* hostBase, SizeT frontendSize,
SizeT hostBaseFrom = 0, SizeT hostBaseTo = kHostBaseCoversWholeStore);
void DrainResidentWritesNow(GLESBufferResource& resource);
namespace {
// The two-argument spelling the legacy arm's call sites use, kept so those sites
// are the SAME text in both builds (G1). The object was never read.
void DrainResidentWritesNow(GLESBufferResource& resource, BufferObject& bufferObject) {
(void)bufferObject;
DrainResidentWritesNow(resource);
}
// ---- P3a: the same seven ops plus the create/unmap pair, BY HANDLE ------------
//
// Every body below is its Ops_* counterpart above with exactly the substitutions
// D-A2's table names and nothing else: the frontend object's GetSize() / GetUsage()
// / HasDefinedContent() / MappedData() / GetChangeSerial() become the applier
// record's Desc.Width / Desc.Usage / Desc.HasDefinedContent, the shadow base the
// call carried, and the record's server-owned Serial. Every branch survives -
// pendingRespecify early-out, the off-thread queue, the adopted zero-copy stamp,
// the upload-ring kill switch, the Mali WAR-stall queue-only default.
//
// NOTHING here reads a frontend type. The one thing a handle op cannot do is ask
// an object for its shadow, which is why GLESBufferResource::hostBytes exists: the
// three content-carrying calls hand the base over and the later drains read it.
// The applier's record for this resource, or null when the client never created it
// (or created it into a slot that has since been recycled).
const MG_Pipe::MGPipeResourceRecord* ResourceRecordOf(MG_Pipe::MGPipeHandle res) {
if (MG_Pipe::MGPipeHandleIsNull(res)) return nullptr;
const auto& records = MG_Pipe::MGPipeApplier().Resources;
if (res.Slot >= records.size()) return nullptr;
const auto& record = records[res.Slot];
if (!record.Live || record.Gen != res.Gen) return nullptr;
return &record;
}
// The server-owned MGGen this backend mirrors in syncedChangeSerial. Zero for a
// resource with no record, which is the same "never synced" answer a frontend
// change serial of zero gave.
Uint64 ResourceSerialOf(MG_Pipe::MGPipeHandle res) {
const auto* record = ResourceRecordOf(res);
return record != nullptr ? record->Serial : 0;
}
SizeT ResourceWidthOf(MG_Pipe::MGPipeHandle res) {
const auto* record = ResourceRecordOf(res);
return record != nullptr ? static_cast<SizeT>(record->Desc.Width) : 0;
}
} // namespace
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
using MG_Util::BackendLoader::HasExtensionWord;
#endif // MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__
const Vector<TextureUploadTarget>& StagedUploadTargetsForPipeTarget(Uint8 pipeResourceTarget);
TextureTarget StagedTextureTargetForPipeTarget(Uint8 pipeResourceTarget);
void BumpBufferMutationEpoch();
GLESBufferResource* FindBufferResourceForHandle(MG_Pipe::MGPipeHandle res);
SizeT ResourceWidthForHandle(MG_Pipe::MGPipeHandle res);
Uint64 ResourceSerialForHandle(MG_Pipe::MGPipeHandle res);
GLESBufferResource* EnsureBufferResourceForHandle(
const SharedPtr<MG_State::GLState::BufferObject>& bufferObject, MG_Pipe::MGPipeHandle res);
void BindBufferId(GLenum target, Uint id);
void BindPixelUnpackBufferId(Uint id);
void NoteBufferIdDeleted(Uint id);
Bool CreateRingStorage(PersistentRing& ring, SizeT minBytes);
Bool RingAllocateSlow(PersistentRing& ring, SizeT size, SizeT& outOffset);
Bool RingAllocate(PersistentRing& ring, SizeT size, SizeT& outOffset);
Bool RingAllocateSlow(PersistentRing& ring, SizeT size, SizeT& outOffset);
Bool UnpackRingAvailable();
Bool UnpackRingAllocate(SizeT size, SizeT& outOffset);
void* UnpackRingMappedPtr();
Uint UnpackRingBufferId();
SizeT UnpackRingMaxBytes();
} // namespace BufferImpl
// The release-build VOICE for StateBackendObjectRegistry::GetOrCreateByHandle's two
// silent refusals, shared by the kinds P4a re-keys so the wording cannot drift between
// them. It is the shape GetOrCreateBufferResourceForHandle gives P3a's buffer family,
// lifted into one template because a copy per kind is a chance per kind to write one of
// them differently.
//
// ITS CALLERS IN THIS PACKAGE ARE THE FRAMEBUFFER ATTACHMENT WALK'S TWO
// (FramebufferImpl::SyncAttachmentSurface: the texture registry and the renderbuffer
// registry), because that is where a HANDLE arrives in a payload - MGPSurface::Res -
// and an ADOPTION is what the arm owes rather than a lookup. The other three re-keyed
// kinds resolve at sites in DirectGLES.cpp (package E's SyncCurrentFBO, the lazy
// sampler mint and SyncCurrentProgram), so their first adoption is E's to write; this
// template is what they call so all five say the same thing.
//
// WHY A VOICE AT ALL: the table asserts, and MOBILEGL_ASSERT compiles out at INFO -
// which all three gate builds and every shipped build are - so an object that silently
// stops being twinned is exactly the failure mode the refusal exists to replace.
//
// Returns null on the legacy arm too, so a caller that reaches this without checking
// its family's arm gets nothing rather than a twin the legacy arm would never see.
//
// SPLIT IN TWO (review N-3), and the split is what makes the two refusals REACHABLE.
// Both of this package's call sites stand behind a cross-check that the record's Res
// equals registry.HandleOf(frontendObject) - the LIVE handle - so by the time the
// adoption ran, the slot was in bounds and the generation was current BY CONSTRUCTION
// and neither refusal below could ever fire: the letter of M-4 was closed and its
// substance was not. The record's handle is the UNTRUSTED input and the frontend is the
// corroboration, so the validity question is asked FIRST, on its own, at both sites;
// the adoption then performs the resolve. Asking it first is also side-effect free,
// which the ordering rule N-1 states for the framebuffer refusal wants here too:
// GetOrCreateByHandle can reset an incumbent twin, and a handle this predicate rejects
// must not reach it.
template <typename Registry>
Bool PipeTwinHandleIsAdoptable(Registry& registry, MG_Pipe::MGPipeHandle handle, const char* kindName) {
if (MG_Pipe::MGPipeHandleIsNull(handle)) return false;
if (handle.Slot >= Registry::SlotTable::kMaxHandleSlot) {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12
MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.HandleSlot\"} - "
"%s handle slot %u is past this table's %u bound",
kindName, handle.Slot, Registry::SlotTable::kMaxHandleSlot);
#else
MGLOG_E_ONCE("MGPipe: %s handle slot %u is past the backend table's %u bound - "
"refusing to twin it",
kindName, handle.Slot, Registry::SlotTable::kMaxHandleSlot);
return false;
#endif
}
const Uint32 liveGen = registry.LiveGenAt(handle.Slot);
if (liveGen != 0 && liveGen > handle.Gen) {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12
MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.Generation\"} - "
"%s handle {%u, %u} names generation behind live generation %u",
kindName, handle.Slot, handle.Gen, liveGen);
#else
// SlotTables.h:301-321: forward is a recycle and resets the twin, BACKWARD is
// refused, because adopting it would release the incumbent LIVE twin's driver
// id and then stamp the slot back to the dead object's generation, after which
// the incumbent's own FindByHandle refuses it and it is silently handed a
// fresh, empty twin - a leak AND an object that loses its storage with no
// diagnostic. That is the shape commit d7655247 fixed on the buffer side.
MGLOG_E_ONCE("MGPipe: %s handle {%u, %u} names a generation BEHIND the live "
"twin's %u - refusing rather than dropping the incumbent's driver "
"object",
kindName, handle.Slot, handle.Gen, liveGen);
return false;
#endif
}
return true;
}
namespace {
template <typename Registry>
typename Registry::BackendPtr* AdoptTwinByHandle(Registry& registry, MG_Pipe::MGPipeHandle handle,
const char* kindName) {
if (!PipeTwinHandleIsAdoptable(registry, handle, kindName)) return nullptr;
return registry.GetOrCreateByHandle(handle);
}
} // namespace
// ---- P4a: the record readers (Managers.h documents the contract) ----
namespace {
// One shape for all five, so a kind cannot grow a different liveness rule than its
// neighbours: index the applier's own dense table, refuse a slot it has not grown to,
// refuse a record that is not Live, and refuse a generation that has moved on under
// the caller. The last of the three is the one that matters: a stale handle resolving
// to its successor's record is how a twin ends up describing another object's storage.
template <typename Record>
const Record* PipeRecordAt(const Vector<Record>& table, MG_Pipe::MGPipeHandle handle) {
if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr;
if (handle.Slot >= table.size()) return nullptr;
const Record& record = table[handle.Slot];
if (!record.Live || record.Gen != handle.Gen) return nullptr;
return &record;
}
} // namespace
const MG_Pipe::MGPipeResourceRecord* PipeTextureRecordForHandle(MG_Pipe::MGPipeHandle res);
const MG_Pipe::MGPipeSamplerCsoRecord* PipeSamplerCsoRecordForHandle(MG_Pipe::MGPipeHandle cso);
void NotePipeSubDataTargetLowByte(const MG_Pipe::MGPipeResourceRecord& record, Uint16 packedTarget, Uint16 level);
const MG_Pipe::MGPipeResourceRecord::PendingUpload* FindPipeTextureUpload(
const MG_Pipe::MGPipeResourceRecord& record, Uint16 uploadTarget, Uint16 level);
void ConsumePipeTextureUpload(MG_Pipe::MGPipeHandle res, Uint16 uploadTarget, Uint16 level);
Bool RearmPipeTextureLevelUpload(MG_Pipe::MGPipeHandle res, Uint16 packedTarget, Uint16 level,
const MG_Pipe::MGPBox& wholeLevel);
namespace TextureImpl {
// Sets the backend GL unpack state to MobileGL's upload default for the scope,
// then restores it. The previous state is read from a shadow instead of via
// glGetIntegerv - that query forces a driver pipeline sync and, because texture
// uploads run it per dirty texture per frame, it dominated the DirectGLES draw
// path. The backend unpack state is set ONLY by MobileGL's own save/restore
// helpers (this class and, historically, the R32F copy path), all of
// which restore to the resting default, so the shadow stays accurate; a one-time
// forced sync pins the backend to that known default up front. Apply() is
// compare-and-set, so the (now redundant) glPixelStorei calls also usually no-op.
class ScopedDefaultUnpackState {
public:
ScopedDefaultUnpackState() {
EnsureShadowSynced();
m_prevAlignment = s_alignment;
m_prevRowLength = s_rowLength;
m_prevSkipRows = s_skipRows;
m_prevSkipPixels = s_skipPixels;
m_prevImageHeight = s_imageHeight;
m_prevSkipImages = s_skipImages;
// Shadow mip data is tightly packed (ProcessTexturePixelsDataUnpack emits
// width * bpp rows with no padding), so uploads must use UNPACK_ALIGNMENT = 1.
// Alignment 4 made the driver read e.g. 7-byte R8 rows at an 8-byte stride,
// shifting every row of a non-multiple-of-4 upload by one pixel.
Apply(1, 0, 0, 0, 0, 0);
}
~ScopedDefaultUnpackState() {
Apply(m_prevAlignment, m_prevRowLength, m_prevSkipRows, m_prevSkipPixels, m_prevImageHeight,
m_prevSkipImages);
}
private:
static void EnsureShadowSynced() {
#if MOBILEGL_BUILD_DISAGGREGATED
const auto epoch = CurrentContextEpoch();
if (s_epoch != epoch) {
s_synced = false;
s_epoch = epoch;
}
#endif
if (s_synced) {
return;
}
s_synced = true;
g_GLESFuncs.glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0);
g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_IMAGES, 0);
s_alignment = 4;
s_rowLength = 0;
s_skipRows = 0;
s_skipPixels = 0;
s_imageHeight = 0;
s_skipImages = 0;
}
static void Apply(GLint alignment, GLint rowLength, GLint skipRows, GLint skipPixels, GLint imageHeight,
GLint skipImages) {
if (alignment != s_alignment) {
g_GLESFuncs.glPixelStorei(GL_UNPACK_ALIGNMENT, alignment);
s_alignment = alignment;
}
if (rowLength != s_rowLength) {
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, rowLength);
s_rowLength = rowLength;
}
if (skipRows != s_skipRows) {
g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_ROWS, skipRows);
s_skipRows = skipRows;
}
if (skipPixels != s_skipPixels) {
g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_PIXELS, skipPixels);
s_skipPixels = skipPixels;
}
if (imageHeight != s_imageHeight) {
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, imageHeight);
s_imageHeight = imageHeight;
}
if (skipImages != s_skipImages) {
g_GLESFuncs.glPixelStorei(GL_UNPACK_SKIP_IMAGES, skipImages);
s_skipImages = skipImages;
}
}
GLint m_prevAlignment = 4;
GLint m_prevRowLength = 0;
GLint m_prevSkipRows = 0;
GLint m_prevSkipPixels = 0;
GLint m_prevImageHeight = 0;
GLint m_prevSkipImages = 0;
// Shadow of the backend GL unpack state (GL defaults). See class comment.
static inline Bool s_synced = false;
#if MOBILEGL_BUILD_DISAGGREGATED
static inline ContextEpoch s_epoch{};
#endif
static inline GLint s_alignment = 4;
static inline GLint s_rowLength = 0;
static inline GLint s_skipRows = 0;
static inline GLint s_skipPixels = 0;
static inline GLint s_imageHeight = 0;
static inline GLint s_skipImages = 0;
};
void NoteInPlaceTextureUpload();
Uint GetWidenableClientComponentCount(TextureInternalFormat format);
Bool IsIntegerWidenableFormat(TextureInternalFormat format);
// P5e (tx2): the GL name a log line wants, from whichever side of the arm can answer it.
// Desc.GlNameForDiag is the declared diagnostics carrier (MGPipeTypes.h) and it is the
// ONLY thing the by-handle arm may say about a texture's frontend name - a GL name is
// never an identity, never a memo key and never part of a hash (section 4.2.1). The
// twenty-odd GetExternalIndex() log reads this family carried (scout G-S2-4) all become
// this call.
static Uint32 TextureDiagName(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject,
const MG_Pipe::MGPipeResourceRecord* record) {
if (stateTextureObject) return static_cast<Uint32>(stateTextureObject->GetExternalIndex());
return record != nullptr ? record->Desc.GlNameForDiag : 0u;
}
// P5e (fix1, ID-81 / CONTRACT-P5E §5.8): THE NULL FRONTEND OBJECT IS A CONTRACT AND THIS
// IS WHERE IT IS CHECKED, ONCE, FOR ALL THREE SYNC BODIES.
//
// tx2's three sync bodies accept a null `stateTextureObject` because the RECORD answers
// every read they make - but only where the record arm is actually SELECTED. Two reads
// inside SyncMipmapsToBackend are gated on `Transport != Monolith` (the texture-VIEW test
// and RequireImageBindableStorage's re-dirty transition, §5.2), and every read in all
// three is gated on TextureResourceSubsystemEnabled() through `pushedStorage`. So a null
// object is servable exactly when BOTH hold, and the by-handle entries beside them
// (SyncTextureToBackendByHandle / SyncMipmapsToBackendByHandle) pass null on the strength
// of a NOTED HANDLE, which says nothing about either.
//
// The device found the gap: with MOBILEGL_TRANSPORT=monolith the push build still drives
// its framebuffer attachments from the record (P4a, D-C2), fb's SyncAttachmentSurface
// called the by-handle storage sync from that arm, and `stateTextureObject->IsTextureView()`
// dereferenced the null SharedPtr in Lightmap.<init> -> clearColorTexture -> glClear.
// The arm selection is repaired at that call site; this refusal is what makes the shape
// UNREPEATABLE rather than merely repaired - a null frontend object reaching a body that
// can still read one aborts BY NAME instead of taking a SIGSEGV three frames deep.
//
// Returns true when the caller must decline (no handle noted: the caller's bug it always
// was, named by the caller's own log line), false when the record arm can serve the call,
// and never returns at all when a handle was noted on an arm that cannot serve it.
static Bool RefuseNullFrontendTextureOffTheHandleArm(const char* entry, MG_Pipe::MGPipeHandle notedHandle) {
if (MG_Pipe::MGPipeHandleIsNull(notedHandle)) return true;
return false;
}
// The guard the three sync bodies spell. In the PULL build it folds to the constant the
// pre-fix1 text folded to, so that build's preprocessed text gains no call (D-P).
#define MGB_TEXTURE_NULL_FRONTEND_REFUSED(twin, entry) \
(RefuseNullFrontendTextureOffTheHandleArm((entry), (twin).PushedSyncHandle()))
extern Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
g_boundTexturesCache;
void NoteDriverSideTextureWriteByHandle(MG_Pipe::MGPipeHandle res, Bool storeFollowsTheWrite);
} // namespace TextureImpl
namespace ScratchFBOImpl {
void NoteTextureIdDeleted(Uint textureId);
} // namespace ScratchFBOImpl
} // namespace MobileGL::MG_Backend::DirectGLES
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,267 @@
// MobileGL - MobileGL/MG_Backend/DirectGLES/ManagersSamplers.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "ManagersInternal.h"
namespace MobileGL::MG_Backend::DirectGLES {
static Bool ShouldAvoidSamplerMipmapMinFilterOnAngleLlvmpipe() {
// IsAngleLlvmpipeRenderer combined with the
// MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER feature toggle,
// both resolved in FillInGLESCapabilities.
return g_GLESCapabilities.AvoidSamplerMipmapMinFilter;
}
namespace SamplerImpl {
BackendSamplerObject::BackendSamplerObject() {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
g_GLESFuncs.glGenSamplers(1, &m_backendSamplerId);
m_contextGeneration = g_backendContextGeneration;
if (m_backendSamplerId == 0) {
MGLOG_E_ONCE("Failed to generate sampler object.");
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
} else {
MGLOG_D("Generated sampler object with ID: %u.", m_backendSamplerId);
}
}
BackendSamplerObject::~BackendSamplerObject() {
if (InProcessTeardown()) {
return; // see InProcessTeardown(): the driver may be unloaded already
}
if (m_backendSamplerId == 0) {
return;
}
// Scrub the unit shadow whether or not the id can still be deleted - the next
// twin can land on this heap address and would otherwise false-skip its Bind.
for (auto& boundSampler : g_boundSamplersCache) {
if (boundSampler == this) {
boundSampler = nullptr; // glDeleteSamplers unbinds from every unit
}
}
if (m_contextGeneration == g_backendContextGeneration && g_GLESFuncs.glDeleteSamplers) {
g_GLESFuncs.glDeleteSamplers(1, &m_backendSamplerId);
}
m_backendSamplerId = 0;
}
void BackendSamplerObject::SyncToBackend(const SharedPtr<MG_State::GLState::SamplerObject>& stateSamplerObject,
MG_Pipe::MGPipeHandle pushedCso) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (!stateSamplerObject) {
// A CSO TWIN HAS NO FRONTEND OBJECT (P4a fable seam F-4, ResolveSamplerCsoTwin):
// the content-addressed handle is its identity and the applier's record its
// only authority, so a null object beside a live handle is the record arm and
// not the pre-P4a error. Everything below that names the object is guarded on
// it; the pull build's text is the three lines the #if brackets.
if (MG_Pipe::MGPipeHandleIsNull(pushedCso)) {
MGLOG_E_ONCE("State sampler object is null, cannot sync to backend.");
return;
}
}
// P4a (D-F1): a SamplerObject is a pure 100-byte value with no driver-side per-object
// binding state, so its CSO is CONTENT-ADDRESSED on the client at capacity 256 and
// two identical samplers share one record. What that means here is that the record's
// server-owned Serial, not the frontend object's version, is what says the values
// moved - and that the values themselves cross byte for byte INCLUDING
// borderColorForm, which is why all four border comparands below are still compared.
//
// The whole arm choice is a preprocessor #if/#else so the PULL build's text is the
// pre-P4a text token for token (D-P).
const SamplerParameters* pushedParams = nullptr;
// The GL name for the two log lines below, or 0 for a CSO twin, which has no object
// to name (F-4): the handle in the same line is its name.
const Uint samplerName = stateSamplerObject ? stateSamplerObject->GetExternalIndex() : 0u;
// THE HANDLE COMES FROM THE CALLER, NOT FROM THIS TWIN'S REGISTRY, and the
// difference is the seam D's verification round found on the integrated tree.
//
// v2 asked g_backendSamplerObjects.HandleOf(object) - the twin's own identity
// handle, minted off the frontend lifetime id - and looked the record up by it.
// That can only ever miss: a SamplerCso is CONTENT-ADDRESSED on the client
// (D-F1, ID-14/ID-17), its handles come out of MGPipeSlots().Allocate keyed on a
// parameter hash, and nothing ever emits a create_sampler_state at an identity
// handle. On the integrated tree every glBindSampler-driven parameter set was
// therefore refused and the driver sampler kept its defaults - two integration
// scenarios red, and the twin's own comment below (content-addressed, the
// record's Serial is the authority) already said why it could not work.
//
// The carried fact is MGPipeApplier().BoundSamplerStates[unit], written by the
// client at bind_sampler_states; the caller that knows the unit passes it as
// `pushedCso`. See the declaration for why the parameter is push-only.
if (!MG_Pipe::MGPipeHandleIsNull(pushedCso)) {
const auto* record = PipeSamplerCsoRecordForHandle(pushedCso);
if (record == nullptr) {
MGLOG_E_ONCE("MGPipe: sampler %u has no applier record on the handle arm, so its "
"parameters cannot be pushed (handle {%u, %u})",
samplerName, pushedCso.Slot, pushedCso.Gen);
return;
}
if (m_isInitialized && m_syncedSamplerSerial != 0 && m_syncedSamplerSerial == record->Serial) {
MGLOG_D("Sampler parameters have not changed for sampler ID: %u, skipping sync.", samplerName);
return;
}
m_syncedSamplerSerial = record->Serial;
pushedParams = &record->Params;
} else {
// P13: every sampler twin is a CSO twin, synced with its handle
// (ResolveSamplerCsoTwin); a sampler with no record has nothing to push.
MGLOG_E_ONCE("MGPipe: sampler %u was synced without a SamplerCso handle", samplerName);
return;
}
MGLOG_D("Syncing sampler with backend ID %u to backend for state ID %u", m_backendSamplerId, samplerName);
const SamplerParameters& samplerParams = *pushedParams;
#define SYNC_SAMPLER_PARAM_IF_CHANGED(internalName, glName, type) \
if (m_cacheSamplerParameters.internalName != samplerParams.internalName) { \
g_GLESFuncs.glSamplerParameteri(m_backendSamplerId, glName, \
(GLint)MG_Util::ConvertSampler##type##ToGLEnum(samplerParams.internalName)); \
m_cacheSamplerParameters.internalName = samplerParams.internalName; \
}
if (m_cacheSamplerParameters.minFilter != samplerParams.minFilter ||
m_cacheSamplerParameters.mipmapMode != samplerParams.mipmapMode) {
g_GLESFuncs.glSamplerParameteri(
m_backendSamplerId, GL_TEXTURE_MIN_FILTER,
(GLint)ResolveBackendMinFilter(samplerParams, ShouldAvoidSamplerMipmapMinFilterOnAngleLlvmpipe()));
m_cacheSamplerParameters.minFilter = samplerParams.minFilter;
m_cacheSamplerParameters.mipmapMode = samplerParams.mipmapMode;
}
if (m_cacheSamplerParameters.magFilter != samplerParams.magFilter) {
g_GLESFuncs.glSamplerParameteri(
m_backendSamplerId, GL_TEXTURE_MAG_FILTER,
(GLint)MG_Util::ConvertSamplerFilterModeToGLEnum(samplerParams.magFilter, SamplerMipmapMode::None));
m_cacheSamplerParameters.magFilter = samplerParams.magFilter;
}
SYNC_SAMPLER_PARAM_IF_CHANGED(wrapS, GL_TEXTURE_WRAP_S, WrapMode)
SYNC_SAMPLER_PARAM_IF_CHANGED(wrapT, GL_TEXTURE_WRAP_T, WrapMode)
SYNC_SAMPLER_PARAM_IF_CHANGED(wrapR, GL_TEXTURE_WRAP_R, WrapMode)
SYNC_SAMPLER_PARAM_IF_CHANGED(compareFunc, GL_TEXTURE_COMPARE_FUNC, CompareFunc)
SYNC_SAMPLER_PARAM_IF_CHANGED(compareMode, GL_TEXTURE_COMPARE_MODE, CompareMode)
if (m_cacheSamplerParameters.minLod != samplerParams.minLod) {
g_GLESFuncs.glSamplerParameterf(m_backendSamplerId, GL_TEXTURE_MIN_LOD, samplerParams.minLod);
m_cacheSamplerParameters.minLod = samplerParams.minLod;
}
if (m_cacheSamplerParameters.maxLod != samplerParams.maxLod) {
g_GLESFuncs.glSamplerParameterf(m_backendSamplerId, GL_TEXTURE_MAX_LOD, samplerParams.maxLod);
m_cacheSamplerParameters.maxLod = samplerParams.maxLod;
}
if (m_cacheSamplerParameters.maxAnisotropy != samplerParams.maxAnisotropy) {
if (g_GLESCapabilities.SupportsTextureFilterAnisotropy) {
g_GLESFuncs.glSamplerParameterf(m_backendSamplerId, GL_TEXTURE_MAX_ANISOTROPY_EXT,
samplerParams.maxAnisotropy);
}
m_cacheSamplerParameters.maxAnisotropy = samplerParams.maxAnisotropy;
}
if (m_cacheSamplerParameters.borderColor != samplerParams.borderColor ||
m_cacheSamplerParameters.borderColorI != samplerParams.borderColorI ||
m_cacheSamplerParameters.borderColorUI != samplerParams.borderColorUI ||
m_cacheSamplerParameters.borderColorForm != samplerParams.borderColorForm) {
// Same gate as the texture-side border colour above, and the same reason for
// branching on the form: an integer border colour must reach the driver through
// glSamplerParameterIiv/Iuiv or an integer sampler reads the float's bit pattern
// back instead of the value.
if (g_GLESCapabilities.SupportsTextureBorderClamp) {
if (samplerParams.borderColorForm == BorderColorForm::Int && g_GLESFuncs.glSamplerParameterIiv) {
const GLint borderColorArray[4] = {
samplerParams.borderColorI.x(), samplerParams.borderColorI.y(),
samplerParams.borderColorI.z(), samplerParams.borderColorI.w()};
g_GLESFuncs.glSamplerParameterIiv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR,
borderColorArray);
} else if (samplerParams.borderColorForm == BorderColorForm::Uint &&
g_GLESFuncs.glSamplerParameterIuiv) {
const GLuint borderColorArray[4] = {
samplerParams.borderColorUI.x(), samplerParams.borderColorUI.y(),
samplerParams.borderColorUI.z(), samplerParams.borderColorUI.w()};
g_GLESFuncs.glSamplerParameterIuiv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR,
borderColorArray);
} else if (g_GLESFuncs.glSamplerParameterfv) {
const GLfloat borderColorArray[4] = {
samplerParams.borderColor.x(), samplerParams.borderColor.y(), samplerParams.borderColor.z(),
samplerParams.borderColor.w()};
g_GLESFuncs.glSamplerParameterfv(m_backendSamplerId, GL_TEXTURE_BORDER_COLOR, borderColorArray);
}
}
m_cacheSamplerParameters.borderColor = samplerParams.borderColor;
m_cacheSamplerParameters.borderColorI = samplerParams.borderColorI;
m_cacheSamplerParameters.borderColorUI = samplerParams.borderColorUI;
m_cacheSamplerParameters.borderColorForm = samplerParams.borderColorForm;
}
#undef SYNC_SAMPLER_PARAM_IF_CHANGED
m_isInitialized = true;
}
void BackendSamplerObject::Bind(Uint unit) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (g_boundSamplersCache[unit] == this) return;
g_GLESFuncs.glBindSampler(static_cast<GLenum>(unit), m_backendSamplerId);
g_boundSamplersCache[unit] = this;
}
Uint BackendSamplerObject::GetBackendSamplerId() const {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
return m_backendSamplerId;
}
void UnbindSampler(Uint unit) {
if (g_boundSamplersCache[unit] == nullptr) return;
g_GLESFuncs.glBindSampler(static_cast<GLenum>(unit), 0);
g_boundSamplersCache[unit] = nullptr;
}
Array<BackendSamplerObject*, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS> g_boundSamplersCache;
TwinRegistry<MG_State::GLState::SamplerObject, BackendSamplerObject, MG_Pipe::MGPipeKind::SamplerCso>
g_backendSamplerObjects;
// P4a fable seam F-4. THE TWIN FOR A CONTENT-ADDRESSED SamplerCso HANDLE, keyed by that
// handle and synced from its record - see the declaration for why the identity-keyed
// lookup it replaces could never hit.
BackendSamplerObject* ResolveSamplerCsoTwin(MG_Pipe::MGPipeHandle cso) {
if (MG_Pipe::MGPipeHandleIsNull(cso)) return nullptr;
// THE RECORD FIRST, before the table is touched: a handle with no record is a seam
// defect (the client minted and named a CSO it never described, or evicted one a
// standing set still names), and adopting a slot for it would leave a twin that
// syncs nothing. The census stem is the sampler family's.
const auto* record = PipeSamplerCsoRecordForHandle(cso);
if (record == nullptr) {
MGLOG_E_ONCE("MGPipe: sampler CSO {%u, %u} has no applier record on the handle arm, so no "
"driver sampler can be built for it and the unit keeps what it holds",
cso.Slot, cso.Gen);
return nullptr;
}
// The same slot table the identity twins live in: one allocator serves both handle
// families of this kind, so a content-addressed slot and an identity-minted slot can
// never coincide, and the generation discipline (forward = recycle, backward =
// refused) is what retires a twin whose CSO the client's LRU evicted and re-minted.
auto* slot = g_backendSamplerObjects.GetOrCreateByHandle(cso);
if (slot == nullptr) {
MGLOG_E_ONCE("MGPipe: sampler CSO {%u, %u} cannot be adopted on the handle arm (the slot's "
"live generation is %u), so no driver sampler is built for it",
cso.Slot, cso.Gen, g_backendSamplerObjects.LiveGenAt(cso.Slot));
return nullptr;
}
if (!*slot) *slot = MakeShared<BackendSamplerObject>();
// Serial-gated inside: a CSO whose record did not move since this twin last synced
// costs the record lookup above and one compare.
(*slot)->SyncToBackend(nullptr, cso);
return slot->get();
}
} // namespace SamplerImpl
} // namespace MobileGL::MG_Backend::DirectGLES
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+114 -142
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>
@@ -25,10 +25,14 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
SizeT IndexTypeSize(GLenum type) {
switch (type) {
case GL_UNSIGNED_BYTE: return 1;
case GL_UNSIGNED_SHORT: return 2;
case GL_UNSIGNED_INT: return 4;
default: return 0;
case GL_UNSIGNED_BYTE:
return 1;
case GL_UNSIGNED_SHORT:
return 2;
case GL_UNSIGNED_INT:
return 4;
default:
return 0;
}
}
@@ -44,14 +48,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
@@ -60,12 +64,18 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// first) and for GL_PATCHES, whose primitive size is dynamic tessellation state.
Uint32 ConcatenablePrimitiveSize(GLenum mode) {
switch (mode) {
case GL_POINTS: return 1;
case GL_LINES: return 2;
case GL_TRIANGLES: return 3;
case GL_LINES_ADJACENCY: return 4;
case GL_TRIANGLES_ADJACENCY: return 6;
default: return 0;
case GL_POINTS:
return 1;
case GL_LINES:
return 2;
case GL_TRIANGLES:
return 3;
case GL_LINES_ADJACENCY:
return 4;
case GL_TRIANGLES_ADJACENCY:
return 6;
default:
return 0;
}
}
@@ -95,7 +105,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.
@@ -112,10 +122,10 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// value is exactly the work its original call site did, in its original order, so the
// monolith arm makes the same calls in the same sequence it made before this package.
enum class IndexBufferQuestion {
Presence, // is an element array buffer bound at all
DriverName, // + the GL name currently bound to GL_ELEMENT_ARRAY_BUFFER
DriverNameAndSize,// + the store's size in bytes (the compute tier's source)
HostBytes, // a CPU-readable copy of the whole store, and its size
Presence, // is an element array buffer bound at all
DriverName, // + the GL name currently bound to GL_ELEMENT_ARRAY_BUFFER
DriverNameAndSize, // + the store's size in bytes (the compute tier's source)
HostBytes, // a CPU-readable copy of the whole store, and its size
};
struct BoundIndexBufferView {
@@ -125,7 +135,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
@@ -151,8 +160,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// the other buffer target: a missing record on the handle arm aborts by name and never
// falls back to the frontend binding slot, which answers for whatever the CLIENT has
// bound NOW - a later verb than the one being applied.
[[noreturn]] void RefuseMissingIndirectBufferRecord(const char* entry,
MG_Pipe::MGPipeHandle res) {
[[noreturn]] void RefuseMissingIndirectBufferRecord(const char* entry, MG_Pipe::MGPipeHandle res) {
MGLOG_F("MGPipe: Fatal{RoleViolation, \"multidraw-indirect-buffer-arm\"} - %s found no "
"backend resource for the indirect buffer {%u, %u} this verb's record named. "
"The indirect buffer of this family is MGPipeApplier().VerbIndirectBuffer "
@@ -168,87 +176,51 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// server shadow is a missing record and Fatal by name; for one the application ORPHANED
// the gap is its own undefined content, and reading the shadow's zero-fill is exactly
// what the monolith arm's MappedData() hands back.
Bool IndexBufferRecordHasDefinedContent(const MG_Pipe::MGPipeApplierState& st,
MG_Pipe::MGPipeHandle res) {
Bool IndexBufferRecordHasDefinedContent(const MG_Pipe::MGPipeApplierState& st, MG_Pipe::MGPipeHandle res) {
if (res.Slot >= st.Resources.size()) return false;
const auto& candidate = st.Resources[res.Slot];
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) {
// No Sync* pair here and none is missing: on this side the applier has
// already consumed the persistent-map blocks and the shader writebacks for
// this resource before the draw verb replayed, so the server's staged shadow
// IS the synced 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 = resource->hostBytes;
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;
}
@@ -268,7 +240,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
@@ -292,23 +264,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;
}
// ---------------------------------------------------------------------------
@@ -369,7 +331,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id);
// Grow in powers of two so a batch that creeps up in size stops respecifying.
SizeT capacity = buffer.capacity == 0 ? bytes : buffer.capacity;
while (capacity < bytes) capacity *= 2;
while (capacity < bytes)
capacity *= 2;
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(capacity), nullptr,
GL_STREAM_DRAW);
buffer.capacity = capacity;
@@ -403,7 +366,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
const SizeT aligned = (bytes + kRingAlignment - 1) & ~(kRingAlignment - 1);
if (buffer.capacity < aligned) {
SizeT capacity = buffer.capacity == 0 ? kMinRingBytes : buffer.capacity;
while (capacity < aligned) capacity *= 2;
while (capacity < aligned)
capacity *= 2;
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(capacity), nullptr,
GL_STREAM_DRAW);
buffer.capacity = capacity;
@@ -436,7 +400,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// driver entries it saves. mc_sodium_multidraw (132 batches x 32 sub-draws),
// ns/op, median of three:
//
// NVIDIA ES 3.2 Mesa llvmpipe ES 3.2
// NVIDIA ES 3.2 llvmpipe ES 3.2
// ext n/a 19300
// basevertex 2500 25200
// multiindirect 5700 27600
@@ -468,9 +432,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
void ResolveTierOnce() {
if (g_tierResolved) return;
g_tierResolved = true;
g_resolvedTier =
ResolveTier(g_GLESCapabilities, g_GLESFuncs, MG_Config::Features.EsprytMultiDrawMode,
&g_tierResolution);
g_resolvedTier = ResolveTier(g_GLESCapabilities, g_GLESFuncs, MG_Config::Features.EsprytMultiDrawMode,
&g_tierResolution);
MGLOG_D("DirectGLES multi-draw: %s", g_tierResolution.c_str());
}
@@ -493,8 +456,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// the batch's own shape - not the driver - rules it out; the compute tier keeps
// its remaining feasibility checks inside its implementation, where the data it
// has to walk is already in hand.
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool perSubDrawBaseVertex,
Bool hasIndexBuffer, Bool arbitraryRestart) {
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool perSubDrawBaseVertex, Bool hasIndexBuffer,
Bool arbitraryRestart) {
ResolveTierOnce();
GLESMultiDrawMode tier = g_resolvedTier;
@@ -526,8 +489,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// The indirect tiers describe each sub-draw as an element offset into the
// bound element array buffer. A client-memory index array has no such buffer,
// and indirect draws are not defined without one.
if (!hasIndexBuffer &&
(tier == GLESMultiDrawMode::MultiIndirect || tier == GLESMultiDrawMode::Indirect)) {
if (!hasIndexBuffer && (tier == GLESMultiDrawMode::MultiIndirect || tier == GLESMultiDrawMode::Indirect)) {
tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex
: GLESMultiDrawMode::DrawElements;
}
@@ -549,14 +511,18 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
for (SizeT i = 0; i < sourceIndexCount; ++i) {
Uint32 value = 0;
switch (indexSize) {
case 1: value = source[i]; break;
case 1:
value = source[i];
break;
case 2: {
Uint16 narrow = 0;
std::memcpy(&narrow, source + i * 2, sizeof(narrow));
value = narrow;
break;
}
default: std::memcpy(&value, source + i * 4, sizeof(value)); break;
default:
std::memcpy(&value, source + i * 4, sizeof(value));
break;
}
// Unsigned wraparound is the defined behaviour for a negative base vertex.
out[i] = (restartActive && value == restartSentinel) ? 0xFFFFFFFFu : value + baseVertexBits;
@@ -571,9 +537,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// P5e (mv): `hasIndexBuffer` is a Bool and not the frontend object it used to be,
// because presence is the only thing this function ever asked of it - and on the record
// arm there is no such object on this side to hand it.
const Uint8* ResolveSubDrawIndices(Bool hasIndexBuffer, const Uint8* indexBufferBytes,
SizeT indexBufferSize, const void* indices, SizeT indexCount,
SizeT indexSize) {
const Uint8* ResolveSubDrawIndices(Bool hasIndexBuffer, const Uint8* indexBufferBytes, SizeT indexBufferSize,
const void* indices, SizeT indexCount, SizeT indexSize) {
if (!hasIndexBuffer) {
return static_cast<const Uint8*>(indices);
}
@@ -656,7 +621,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
for (GLsizei i = 0; i < drawcount; ++i) {
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
const SizeT commandOffset = commandBase + static_cast<SizeT>(i) * sizeof(DrawElementsIndirectCommand);
const SizeT commandOffset =
commandBase + static_cast<SizeT>(i) * sizeof(DrawElementsIndirectCommand);
ForEachViewportRoutingPass([&] {
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
});
@@ -696,8 +662,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// ---------------------------------------------------------------------------
Bool RunRebasedDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID,
Bool feedBaseVertex) {
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID, Bool feedBaseVertex) {
const SizeT indexSize = IndexTypeSize(type);
if (indexSize == 0) return false;
@@ -734,8 +699,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
indices[i], subDrawCount, indexSize);
if (!source) {
MGLOG_E_ONCE("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
"buffer; skipping the batch",
i);
"buffer; skipping the batch",
i);
return false;
}
RebaseIndices(source, subDrawCount, indexSize, basevertex ? basevertex[i] : 0, restartActive,
@@ -853,7 +818,8 @@ void main() {
if (status != GL_TRUE) {
char log[1024] = {};
g_GLESFuncs.glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log);
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s",
log);
g_GLESFuncs.glDeleteShader(shader);
return false;
}
@@ -1062,13 +1028,20 @@ void main() {
const char* TierName(GLESMultiDrawMode tier) {
switch (tier) {
case GLESMultiDrawMode::Auto: return "auto";
case GLESMultiDrawMode::Ext: return "ext";
case GLESMultiDrawMode::MultiIndirect: return "multiindirect";
case GLESMultiDrawMode::Indirect: return "indirect";
case GLESMultiDrawMode::BaseVertex: return "basevertex";
case GLESMultiDrawMode::DrawElements: return "drawelements";
case GLESMultiDrawMode::Compute: return "compute";
case GLESMultiDrawMode::Auto:
return "auto";
case GLESMultiDrawMode::Ext:
return "ext";
case GLESMultiDrawMode::MultiIndirect:
return "multiindirect";
case GLESMultiDrawMode::Indirect:
return "indirect";
case GLESMultiDrawMode::BaseVertex:
return "basevertex";
case GLESMultiDrawMode::DrawElements:
return "drawelements";
case GLESMultiDrawMode::Compute:
return "compute";
}
return "unknown";
}
@@ -1107,8 +1080,7 @@ void main() {
const Bool arbitraryRestart = restartKind == RestartSubstitutionKind::RewriteIndices;
const ScopedSuppressedPrimitiveRestart restartCapOverride(restartKind);
const Bool hasIndexBuffer =
ResolveBoundIndexBuffer(IndexBufferQuestion::Presence, "DrawElementsBatch").Present;
const Bool hasIndexBuffer = ResolveBoundIndexBuffer(IndexBufferQuestion::Presence, "DrawElementsBatch").Present;
// The compute tier dispatches BEFORE the draw state is established: doing it
// afterwards would mean unpicking the program, SSBO and index bindings
@@ -1162,8 +1134,8 @@ void main() {
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
break;
case GLESMultiDrawMode::DrawElements:
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID,
feedBaseVertex);
drawn =
RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
break;
case GLESMultiDrawMode::Compute:
// Its pre-pass ran above; reaching here means it declined this batch's shape.
@@ -1182,13 +1154,13 @@ void main() {
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
}
if (!drawn) {
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID,
feedBaseVertex);
drawn =
RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
}
if (!drawn) {
MGLOG_E_ONCE("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
"the batch was dropped",
drawcount, mode, type);
"the batch was dropped",
drawcount, mode, type);
}
}
} // namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl
+331 -351
View File
@@ -11,9 +11,12 @@
#include <MG_Pipe/MGPipeHandles.h>
#if MOBILEGL_PIPE_PUSH
#include <MG_Impl/Pipe/SlotAllocator.h>
#endif
// P14 S6: the key this table is filed under includes the calling thread's {session, share
// group}, and the probe that answers it lives on the backend's public surface beside S4's
// native-tuple key (DirectGLES.h). Included here so this header is self-contained for the two
// translation units that include it without copying Managers.h's include order.
#include "DirectGLES.h"
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Pipe/PipeSessionFail.h>
#endif
@@ -37,69 +40,36 @@
// MG_Remote/Server/PipeObjectTables) be an array rather than an object graph.
//
// Death is ANNOUNCED, and that is what lets this table have no garbage collector - the
// deliverable ROADMAP.md:18 spells "GC" in and the one D13 makes a precondition of the switch-
// one the slot-table plan lists for deletion as "GC" 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
// arming site moves to the slot table's first insertion").
@@ -116,9 +86,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
@@ -126,7 +93,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// sequence in a FORKED CHILD (MG_IntegrationTest/Harness/HeadlessGL.cpp): a child that dies
// on a signal is reported as "no usable GPU/display/ICD" and every scenario in the lane is
// SKIPPED - i.e. the lane goes green having run nothing, on the very pair of env vars the
// D14/D18 A/B is driven with, which is what ROADMAP.md:7 forbids. So bring-up only
// D14/D18 A/B is driven with, which is what ARCHITECTURE.md §13.2 forbids. So bring-up only
// DIAGNOSES; the stop is raised by ResolveEsprytSlotTablesArm() at the first twin lookup,
// which happens in the test body where the harness reports it as a failure.
//
@@ -134,6 +101,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
@@ -145,7 +115,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
@@ -155,11 +125,239 @@ namespace MobileGL::MG_Backend::DirectGLES {
return enabled;
}
// =====================================================================================
// P14 S6 (docs/Disaggregated/design/11-state-ownership.md): THE TWIN TABLE IS PER
// {session, share group}, NOT PER PROCESS.
// =====================================================================================
//
// WHAT WAS WRONG, in one sentence: a twin is a DRIVER object - a GL buffer id, a program, a
// texture, the storage behind them - so it belongs to one native context's object namespace,
// and a table keyed by {slot, gen} ALONE resolves the same handle value to the same twin
// whichever context asked. The frontend gives two contexts that do NOT share a group two
// independent name spaces, so the same {slot, gen} in both is legal and means two different
// objects; one process-wide table made the second context adopt the first's twin (including
// its driver ids, its persistent map and its pooled store). At the same time two contexts of
// ONE group - which must see one set of objects - were served by whichever of them twinned
// first, i.e. by luck rather than by the group the two agree on.
//
// THE KEY IS the same pair the applier files its OBJECT RECORDS under: {session, share
// group}. Same probe, same `0 means this context started its own group` rule (see
// DirectGLES.h's TwinKey and SessionRuntime.cpp's ResolveThreadTwinKey), so a record and the
// twin it describes are always in the same bucket. Session is in the key because a twin
// names a driver id and no driver id crosses a connection - two connections in one process
// 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: 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
// every bucket of every table.
//
// THE SAME TABLE OBJECT IS REUSED, NOT REPLACED, and that is what keeps ~200 call sites
// alive: `g_backendTextureObjects.Find(...)` still answers from the bucket the calling thread
// is in, because the object the global names multiplexes on the key at each lookup. A caller
// that must address a bucket OTHER than the calling thread's - the death walks, the
// session-teardown drop - takes Shared() or calls the static entries, which walk buckets;
// and a caller that wants the whole process's view (the teardown, the tests) takes Snapshot().
// FORWARD-DECLARED, because the registry below is DEFINED before the table it holds; the
// members that name the table are defined out of line at the bottom of this header.
template <typename StateObject, typename BackendObject, MG_Pipe::MGPipeKind kKind>
class BackendSlotTable;
template <typename StateObject, typename BackendObject, MG_Pipe::MGPipeKind kKind>
class SlotTableRegistry {
public:
using Table = BackendSlotTable<StateObject, BackendObject, kKind>;
using Key = TwinKey;
using StatePtr = SharedPtr<StateObject>;
using BackendPtr = SharedPtr<BackendObject>;
// The bound the two tables that ARE a bucket registry have always spelled by name; the
// six registries reach it through their own `SlotTable` alias.
static constexpr Uint32 kMaxHandleSlot = 1u << 20;
// One bucket. SHARED, so a caller may keep it across other lookups: the map owns the
// pointee and never moves it.
SharedPtr<Table> Shared(const Key& key) const {
// The common case on the hot path: the same key as last time. One compare, no probe.
if (m_memoTable != nullptr && m_memoKey == key) return m_memoTable;
const auto found = m_buckets.find(key);
if (found != m_buckets.end()) {
m_memoKey = key;
m_memoTable = found->second;
return found->second;
}
SharedPtr<Table> fresh = MakeShared<Table>();
fresh->SetOwnerSession(key.SessionKey);
m_buckets.emplace(key, fresh);
m_memoKey = key;
m_memoTable = fresh;
return fresh;
}
// The calling thread's bucket - the ONE entry every in-place call site below resolves
// through, and the process-wide answer on a thread that belongs to no session.
//
// CONST, AND IT ANSWERS A MUTABLE BUCKET: the index is `mutable` because a LOOKUP may
// have to create the bucket it is about to miss in (a handle with no twin yet is the
// ordinary case), and the bucket's own entries are the table's business rather than the
// registry's. The alternative - a const overload returning "no table" - would make every
// read-only caller handle a case that cannot arise.
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);
}
// Every bucket, in no particular order.
Vector<SharedPtr<Table>> Snapshot() const {
Vector<SharedPtr<Table>> all;
all.reserve(m_buckets.size());
for (const auto& bucket : m_buckets)
all.push_back(bucket.second);
return all;
}
SizeT BucketCount() const { return m_buckets.size(); }
// THE PER-BUCKET ONE-SHOT, because a bucket held by SharedPtr is never dropped by an
// assignment: a caller that means "this context/group is gone" has to be able to say so,
// and the key is how it says which one. The bucket LEAVES the index before its twins are
// released, so a twin destructor that re-entered Shared() cannot find a half-erased one.
Bool Drop(const Key& key) {
const auto found = m_buckets.find(key);
if (found == m_buckets.end()) return false;
SharedPtr<Table> dropped = found->second;
m_buckets.erase(found);
if (m_memoTable == dropped) {
m_memoKey = Key{};
m_memoTable = nullptr;
}
dropped->Reset();
return true;
}
// EVERY bucket of one session, for the teardown of a session that owned several groups.
// The session is the key's high half, so this is one pass and no per-group bookkeeping -
// and it is the difference between "this session's twins go" and "the process's do".
Uint32 DropSession(Uint64 sessionKey) {
Uint32 dropped = 0;
for (auto bucket = m_buckets.begin(); bucket != m_buckets.end();) {
if (bucket->first.SessionKey != sessionKey) {
++bucket;
continue;
}
SharedPtr<Table> table = bucket->second;
if (m_memoTable == table) {
m_memoKey = Key{};
m_memoTable = nullptr;
}
bucket = m_buckets.erase(bucket);
table->Reset();
++dropped;
}
return dropped;
}
// Replace this registry with an empty one, releasing every bucket's twins. This is what
// `registry = {}` meant when the registry WAS one table; a caller that must keep the
// tables alive past the call takes a Snapshot() first.
void Reset() {
const Vector<SharedPtr<Table>> all = Snapshot();
m_buckets.clear();
m_memoKey = Key{};
m_memoTable = nullptr;
for (const SharedPtr<Table>& table : all)
table->Reset();
}
// ---- the calling thread's bucket, member for member ---------------------------------
//
// THIS IS WHY THE SHAPE COST THE CALL SITES NOTHING. Every handle-keyed entry the table
// has is repeated here and forwarded to the bucket the calling thread resolves to, so
// `g_backendTextureObjects.FindByHandle(h)` and `g_backendBufferResources.GetOrCreate(h)`
// - unchanged source - answer from the right context's object namespace. A caller that
// must name a bucket OTHER than its own (a death walk, a case) takes Shared()/Snapshot()
// and calls the table directly.
BackendPtr& GetOrCreate(MG_Pipe::MGPipeHandle handle) { return ForCallingThread().GetOrCreate(handle); }
BackendPtr* FindByHandle(MG_Pipe::MGPipeHandle handle) { return ForCallingThread().FindByHandle(handle); }
const BackendPtr* FindByHandle(MG_Pipe::MGPipeHandle handle) const {
return ForCallingThread().FindByHandle(handle);
}
// Resolve-or-create in a bucket the caller NAMES rather than the one it is in. The
// monolith-glue frontend-keyed entry (StateBackendObjectRegistry::GetOrCreate below)
// needs it: a state object handed over on the client side of a sync is described by a
// record in the CALLING thread's group, and a twin built for it must land in the same
// bucket as that record.
BackendPtr& GetOrCreateIn(const Key& key, MG_Pipe::MGPipeHandle handle) {
return Shared(key)->GetOrCreate(handle);
}
Uint32 LiveGenAt(Uint32 slot) const { return ForCallingThread().LiveGenAt(slot); }
template <typename Fn>
void ForEachLive(Fn&& fn) const {
// THE CALLING THREAD'S BUCKET, not every bucket, and that is the semantic answer
// rather than a shortcut: a caller walking twins is asking "which of the objects I
// can see need this", and what it can see is its own share group. Every bucket in
// one process would walk a neighbour session's objects.
ForCallingThread().ForEachLive(fn);
}
// Every bucket, in one pass. The caller receives the key with the handle so it can tell
// which context a twin belongs to - the only walks that need the whole process's view
// are a diagnostic and a teardown.
template <typename Fn>
void ForEachBucket(Fn&& fn) const {
for (const auto& bucket : m_buckets) {
bucket.second->ForEachLive(
[&](MG_Pipe::MGPipeHandle handle, const BackendPtr& twin) { fn(bucket.first, handle, twin); });
}
}
// The process-wide answer for the two statics: a notice is about an OBJECT, so it must
// reach the twin in EVERY bucket of EVERY holder. They forward to the table's own
// statics, which walk the holder list - each SHARED holder of it is one bucket.
static Bool ReleaseTwinByHandle(MG_Pipe::MGPipeHandle handle) { return Table::ReleaseTwinByHandle(handle); }
// 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
// what happens to the driver id the twin owns. The bucket is the CALLING THREAD'S, which
// is the bucket that twin was created in.
BackendPtr ReleaseByHandle(MG_Pipe::MGPipeHandle handle) { return ForCallingThread().ReleaseByHandle(handle); }
static Uint32 HolderCount() { return Table::HolderCount(); }
Uint32 LiveCount() const { return ForCallingThread().LiveCount(); }
Uint32 CompositeLiveCount() const { return ForCallingThread().CompositeLiveCount(); }
SizeT OrdinaryCapacityForTest() const { return ForCallingThread().OrdinaryCapacityForTest(); }
SizeT CompositeCapacityForTest() const { return ForCallingThread().CompositeCapacityForTest(); }
// ---- the bucket index's own controls, for the two tables that ARE a registry --------
//
// The six TwinRegistry registries carry the same three under the names in Managers.h
// (DropBucket / DropBucketsOfSession / ResetBuckets); they are repeated here because
// SlotTableRegistry IS the public type of the buffer and sampler-view tables, so a caller
// of those has no registry object in between.
Bool DropBucket(const Key& key) { return Drop(key); }
// Drop every bucket of one session; answers how many groups that took with it.
Uint32 DropBucketsOfSession(Uint64 sessionKey) { return DropSession(sessionKey); }
// Empty every bucket (the process-shaped world's teardown, key {0, 0}).
void ResetBuckets() { Reset(); }
private:
// ONE BUCKET PER LIVE {session, share group}, held by SHARED pointer so a caller may
// snapshot one across other lookups and so `Drop` can take a bucket's twins away
// explicitly - an embedded table could do neither. `std::unordered_map` rather than the
// tree's own flat Vector: this map is probed once per twin lookup on the draw path.
mutable std::unordered_map<Key, SharedPtr<Table>, TwinKeyHash> m_buckets;
// Hot-path memo. A registry is per KIND and each kind is touched several times per draw;
// a thread only ever moves between buckets when its session binds another context.
mutable SharedPtr<Table> m_memoTable;
mutable Key m_memoKey;
};
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
@@ -189,14 +387,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;
@@ -206,25 +396,18 @@ 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_slots(other.m_slots),
m_band(other.m_band),
m_nullTwin(other.m_nullTwin),
m_memoLifetimeId(other.m_memoLifetimeId),
m_memoHandle(other.m_memoHandle) {
BackendSlotTable(const BackendSlotTable& other)
: m_ownerSession(other.m_ownerSession), m_slots(other.m_slots), m_band(other.m_band),
m_nullTwin(other.m_nullTwin) {
LinkHolder();
}
BackendSlotTable(BackendSlotTable&& other) noexcept:
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) {
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)) {
other.m_slots.clear();
other.m_band.clear();
other.ForgetHandle();
LinkHolder();
}
BackendSlotTable& operator=(const BackendSlotTable& other) {
@@ -232,8 +415,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;
}
@@ -242,101 +423,30 @@ 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;
}
~BackendSlotTable() { UnlinkHolder(); }
// Resolve-or-create. The handle comes from the client allocator keyed on the frontend
// object's lifetime id, so two calls for the same live object always land on the same
// slot, and a successor object at the same heap address never does.
BackendPtr& GetOrCreate(const StatePtr& stateObj) {
// No assert on null here, unlike the map arm: null is TOLERATED, so a DEBUG build
// must not trap where the release build quietly does the documented thing.
if (stateObj == nullptr) {
// The registry this replaces inserted a null KEY and handed back that entry's
// twin (DirectGLES.cpp's SyncTextureObjectToBackend documents relying on
// exactly that tolerance), so a release build never dereferenced null here.
// Keep the shape exactly, INCLUDING across calls: the map kept its null-keyed
// entry, so a second null call was handed the same twin the first one got.
// Resetting here instead would have destroyed it - an arm difference in the one
// path that documents relying on this. One per-table parking slot, never live,
// never handed a handle, because a null object has no identity and
// therefore cannot have a {slot, gen}.
return m_nullTwin;
}
// The served session whose bucket this table is (SlotTableRegistry::Shared), 0 for a
// table that belongs to none (the process-wide registry on a sessionless thread, a
// fixture's copy of one).
void SetOwnerSession(Uint64 session) { m_ownerSession = session; }
Uint64 OwnerSession() const { return m_ownerSession; }
// 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");
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle), "GetOrCreate(handle) named the reserved null handle");
if (MG_Pipe::MGPipeHandleIsNull(handle)) return m_nullTwin;
// A slot index that ARRIVED in a payload indexes a vector this call would RESIZE,
@@ -357,29 +467,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
"GetOrCreate(handle) named slot %u, past this table's %u bound",
handle.Slot, kMaxHandleSlot);
#else
MOBILEGL_ASSERT(false, "GetOrCreate(handle) named slot %u, past this table's %u bound",
handle.Slot, kMaxHandleSlot);
MOBILEGL_ASSERT(false, "GetOrCreate(handle) named slot %u, past this table's %u bound", handle.Slot,
kMaxHandleSlot);
return m_nullTwin;
#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,
// a persistent map, a pooled store, released by a defaulted destructor that issues
// no glDeleteBuffers and no pool enrolment - and then stamp the slot back to the
// dead resource's generation, after which the incumbent's own FindByHandle refuses
// it and it is silently handed a fresh, empty twin. That is a leak AND a resource
// that loses its storage with no diagnostic, i.e. the shape commit d7655247 fixed
// and the thing MGPipeHandle::Gen exists to prevent. So: forward is a recycle and
// resets the twin, BACKWARD is refused - which is the same answer FindByHandle
// below already gives the same input.
// 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 it and it is silently handed a fresh, empty twin.
// That is a leak AND a resource that loses its storage with no diagnostic, i.e. the shape commit d7655247
// fixed and the thing MGPipeHandle::Gen exists to prevent. So: forward is a recycle and resets the twin,
// BACKWARD is refused - which is the same answer FindByHandle below already gives the same input.
Entry& entry = EntryAt(handle.Slot);
if (entry.Live && entry.Gen > handle.Gen) {
#if MOBILEGL_BUILD_DISAGGREGATED
@@ -413,31 +517,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
@@ -447,35 +526,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
// GetOrCreate from a twin destructor cannot resurrect it.
//
// The slot itself is NOT freed here: it belongs to the kind, and for a handle-keyed
// kind the CLIENT frees it after the destroy call returns (SlotAllocator.h:60 - the
// kind the CLIENT frees it after the destroy call returns (MGPipeSlotAllocator::Free - the
// Gen bump rides the next handout, so a double free cannot skip a generation). An
// entry whose Gen no longer matches is a twin of the slot's previous owner and is
// left alone: the successor's own GetOrCreate resets it.
BackendPtr ReleaseByHandle(MG_Pipe::MGPipeHandle handle) {
if (MG_Pipe::MGPipeHandleIsNull(handle)) return BackendPtr{};
if (m_memoHandle.Slot == handle.Slot) ForgetHandle();
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);
@@ -483,65 +547,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
@@ -553,12 +558,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
// double Free would be refused by generation anyway. Idempotent against the kind's
// own delete opcode, exactly as the notice arm is: a twin the delete already released
// fails ReleaseTwinAt's generation check and the walk moves on.
//
// ONE SESSION'S HOLDERS, WHEN THE DEATH IS ONE SESSION'S. A handle is numbered by its own
// client's allocator, so with several clients served by one process the same {slot, gen}
// names a live, unrelated object in every other session: walking every holder released
// THOSE twins too - a client deleting a texture (or exiting) deleted the compositor's
// driver texture with the same handle, and its windows sampled freed memory. On a thread
// that belongs to a session only that session's buckets answer; a sessionless thread
// (the in-library backend, a unit fixture) keeps the process-wide walk.
static Bool ReleaseTwinByHandle(MG_Pipe::MGPipeHandle handle) {
if (MG_Pipe::MGPipeHandleIsNull(handle)) return false;
TwinKey key;
const Bool sessionScoped = CurrentTwinKey(&key) && key.SessionKey != 0;
Bool released = false;
for (BackendSlotTable* holder = s_firstHolder; holder != nullptr;) {
BackendSlotTable* const next = holder->m_nextHolder;
released = holder->ReleaseTwinAt(handle) || released;
if (!sessionScoped || holder->m_ownerSession == key.SessionKey) {
released = holder->ReleaseTwinAt(handle) || released;
}
holder = next;
}
return released;
@@ -568,8 +585,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// list; nothing on a shipping path asks.
static Uint32 HolderCount() {
Uint32 count = 0;
for (const BackendSlotTable* holder = s_firstHolder; holder != nullptr;
holder = holder->m_nextHolder) {
for (const BackendSlotTable* holder = s_firstHolder; holder != nullptr; holder = holder->m_nextHolder) {
++count;
}
return count;
@@ -581,14 +597,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.
@@ -609,13 +617,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Entry& entry = m_band[index];
if (!entry.Live || !entry.backend) continue;
const BackendPtr twin = entry.backend;
fn(MG_Pipe::MGPipeHandle{
static_cast<Uint32>(index) + MG_Pipe::kMGPipeShaderCsoCompositeSlotBase,
entry.Gen},
fn(MG_Pipe::MGPipeHandle{static_cast<Uint32>(index) + MG_Pipe::kMGPipeShaderCsoCompositeSlotBase,
entry.Gen},
twin);
}
}
// Empty this table, releasing every twin it holds. THE SPACES ARE MOVED OUT FIRST and
// that is the whole of the care taken here: a twin's destructor is a driver call that
// could re-enter GetOrCreate on this table (SlotTables.h's ReleaseTwinAt says why), and
// destroying the entries in place would leave that re-entry resizing the vector the
// destructors are being walked out of. Here the table is already empty when the entries
// die, so a re-entrant insert grows a fresh vector.
//
// This is what `table = {}` used to mean, spelled so a caller cannot miss the ordering.
void Reset() {
Vector<Entry> slots = std::move(m_slots);
Vector<Entry> band = std::move(m_band);
BackendPtr nullTwin = std::move(m_nullTwin);
slots.clear();
band.clear();
nullTwin.reset();
}
Uint32 LiveCount() const {
Uint32 count = 0;
for (const Entry& entry : m_slots) {
@@ -646,13 +670,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,
@@ -663,7 +682,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();
@@ -680,9 +698,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
return kHasCompositeBand && MG_Pipe::MGPipeIsCompositeShaderSlot(slot);
}
static constexpr SizeT IndexOfSlot(Uint32 slot) {
return SlotIsBanded(slot)
? static_cast<SizeT>(slot - MG_Pipe::kMGPipeShaderCsoCompositeSlotBase)
: static_cast<SizeT>(slot);
return SlotIsBanded(slot) ? static_cast<SizeT>(slot - MG_Pipe::kMGPipeShaderCsoCompositeSlotBase)
: static_cast<SizeT>(slot);
}
// The entry a slot names, or null when its space has never grown that far. NEVER grows:
@@ -694,12 +711,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (index >= table.size()) return nullptr;
return &table[index];
}
const Entry* EntryOrNull(Uint32 slot) const {
return const_cast<BackendSlotTable*>(this)->EntryOrNull(slot);
}
const Entry* EntryOrNull(Uint32 slot) const { 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.
@@ -710,15 +724,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
@@ -745,6 +750,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
static inline BackendSlotTable* s_firstHolder = nullptr;
BackendSlotTable* m_prevHolder = nullptr;
BackendSlotTable* m_nextHolder = nullptr;
Uint64 m_ownerSession = 0;
// Indexed by MGPipeHandle::Slot; [0] is the reserved slot and is never live.
Vector<Entry> m_slots;
@@ -754,32 +760,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
Vector<Entry> m_band;
// 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

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