Repository navigation
Raw global memory at Metal's own GPU addresses, opt-in per game (shader_raw_direct, Ocarina of Time) - #1833
Raw global memory at Metal's own GPU addresses, opt-in per game (shader_raw_direct, Ocarina of Time)#1833MartinBouteruche wants to merge 9 commits into
Conversation
…would cover, and the Metal GPU addresses of the pool buffers (research for raw memory access without the lookup)
…into an unmapped shadow, move them back at every NVN entry, the raw pool table and driver constants in the game's view; count CPU accesses through moved addresses (repaired in the fault handler) and CPU addresses handed in Ocarina of Time, 6 minutes with a scene change: 9.77 M addresses handed out, no CPU access through one, no CPU address handed in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… GPU addresses (a large pool's from a buffer over all of it, resident only once raw global memory may reach it) and maps each one handed back to the CPU; the raw pool table holds GPU ranges with delta 0 Ocarina of Time, 5 minutes with a scene change: correct picture, 10.4 M addresses handed out, no CPU address handed in, frame times unchanged. The GPU page fault at its first scene load happens with the switch off as well. The shift diagnostic's guest-range functions get weak stand-ins, so the graphics unit tests link again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ss itself (chain cfb1ac0c52d4) The translator (GlobalToStorage.TryGenerateRawDirectOp) accesses an address loaded from memory as it is: no pool table, no search. Loads have no check (an Apple GPU reads 0 where nothing is mapped, and any branch, even one that only returns 0, cost the whole gain); stores and atomics outside the GPU's address range are dropped and set their 2 MiB chunk's bit in the write log. dmsh_abi.h DMSH_RAW_DIRECT makes the host hand out Metal's addresses, read the log by chunk (gva_chunk_ranges) and make the reachable pools resident at each commit. Ocarina of Time's 62 raw-memory shaders: 384 -> 562 threads a threadgroup on average (M1 Pro), as the hand-edited prototypes measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…self (chain 535496428142) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…CT), not a switch; test_dmsh_raw_direct.py replaces the pool table lookup's test (chain db98b8fa2a9f) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rs reference), the lookup's entries marked superseded; symptom index Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er_raw_direct, on for Ocarina of Time only (chain 01c2651261b7) dmsh_build.py gives the translator raw_direct for such a profile (IGpuAccessor.QueryHostRawDirect, read by GlobalToStorage), and the host takes DM_GAME_SHADER_RAW_DIRECT (mtl_gva_on) in place of the chain-wide DMSH_RAW_DIRECT: every other game's jobs, shaders and host behaviour stay as on main, with the pool table's lookup. test_dmsh_raw_last.py is back for the lookup; test_dmsh_raw_direct.py asks for raw_direct. A large pool's address buffer is made with pool_buf, like its other buffers, so one made over a write-protected page takes the GPU's writes; the pools raw global memory reaches are made resident before the commit even when no slot needs patching. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (main's expectations) and, under the profile's shader_raw_direct (mtl_gva_on), Metal's t_nvn: a pool's and a buffer's GPU address are Metal's own and map back to the storage. t_geometry: the c0 storage buffer rows carry the address the game holds. t_gmem_words: the table's entries are the pools' GPU ranges. t_pool_chunks: a large pool reached by raw global memory is its address buffer's whole GPU range, made resident, while bindings keep their chunks; the lookup hint and the write log's bits are 2 MiB chunks of GPU addresses; and a large pool's address buffer made over a page protected before the pool was added takes the GPU's writes (fails with the buffer made by newBufferWithBytesNoCopy instead of pool_buf, issue seedds#1591). With mtl_gva_on 0 every check is main's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Data point from an M5 Pro (24 GB), Ocarina of Time v131072. We ported this PR onto our tree and read Separately, in a compute and an object/mesh pipeline, reads at null, 0x100, an unmapped hole, a stray CPU address and an address above 0x70_0000_0000 returned 0 with no error, and stores there completed. One note on the store window: a fresh process's first Metal buffer got a GPU address of about 1.1 TB, outside |
|
Data point from a MacBook Air M2 (10-core GPU, 16 GB), macOS 27.0, Ocarina of Time v131072. Build: this PR ( Speed (paired A/B, interleaved runs, windows with the CPU clock below 2.9 GHz excluded; 5 runs on A, 7 on B). Caveat: A isn't exactly B minus this PR. B also carries:
Of those, only #1902 / #1897 change gameplay shaders, and they are rounding fixes. Most of the difference should be this PR, but it isn't isolated.
Reach: in a profiled gameplay frame, 0 of the 272 shaders (fragment, compute, task, mesh) still call the pool lookup. We matched on the lookup helper's signature. On the build without the PR, some of the same shaders did call it. What unchecked reads do on the M2. One-thread probe through a 64-bit address; every command buffer completed with no error and no page fault:
So a stale pointer into a freed pool would read another pool's data rather than 0. That differs from the lookup only when Metal reuses a GPU range that the guest hasn't. Stale-pointer check: two gameplay runs with We now use it as our daily build for OoT on the Air. |
|
Follow-up to my comment above, on uncraftablexd's point about the fixed store window. On the M2 Air (macOS 27.0), every OoT pool buffer sat inside the first window: the It agrees with the M4 Pro report, though: the window depends on the Mac, so taking its bounds from the host's address table instead of a literal would make the result portable. |
…s (PR seedds#1833) with the M4 fix, on the synced main The branch is PR seedds#1833 (shader_raw_direct, on for Ocarina of Time) plus one commit that lets raw stores and atomics reach pool buffers above 2^40, which an M4's addresses are. Conflicts resolved by hand: - GlobalToStorage.cs: the PR's structure (TryGenerateRawDirectOp, EmitRawAccess) with main's two changes to the lookup path kept: the 'a miss reads 0 in every word of a vector load' block in TryGenerateRawStorageOp's tail (the direct op has no miss path for loads: the GPU reads 0 at an unmapped address) and the GlobalVectorGroup guard of the follower scan. - nvn_mtl.m (mtl_pool_del): main collects the retired chunks in 'gone' and releases them behind the fence; the PR's whole-pool address buffer (va) joins that batch instead of a separate defer_obj_release. - The translator is rebuilt from the merged sources (dotnet publish; dmsh_xlate.dll, Ryujinx.Graphics.Shader.dll, Ryujinx.Common.dll, Spv.Generator.dll and the pdb replaced, the universal launcher and the other files kept), dmsh_chain.lock updated with tool_lock.py --update-lock (only the four translator fingerprints change), dmsh_chain.h regenerated. tool_lock.py --check-tools passes. - Docs: game-patches.md, symptoms.md and env_switches.tsv regenerated. Checked: test_dmsh_raw_direct, test_xlate_anycpu, test_nsp_chain, the translator tests for border-fc, float-atomic-add, seedds#2081, seedds#2030 and layer_out, docs tests; the host builds for Ocarina of Time. Not run: a game. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…on scope The test builds a namespace for dmsh_build.py's job_for by hand; PR seedds#1833's job_for reads the module constant RAW_DIRECT (shader_raw_direct), so both JobFor tests failed with NameError once the two met. The checks are about the early-fragment flag, so the constant is False. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d the allocator pool tried and reverted Game note, "For an iPhone or iPad build": a new current bullet for the iPhone 17 at the owner's Kokiri spot. Its table gives the GPU span, the GPU work and the gaps before and inside submissions, before and after A-2. Gameplay GPU p50 is 21-24 ms, under the cap's 33.3 ms, so shader pull requests seedds#1752 and seedds#1833 were not tried. The frame is CPU-bound at 16-20 fps under thermal state 1-2. The bullet records the uncapped A/B of A-2, and a command allocator pool of 128 that removed its waits but no frame time, at a cost of about 300 MB (not kept). Open: translation cost, samples-passed reports, footprint growth. The CPU timeline the table was measured with is a local diagnostic, not in this pull request. rendering.md: the A-2 entry gets its measured result. A Found-on line in the note, and the outdated iPhone line under "Not run on an iPhone yet" now points to the section.
…d the allocator pool tried and reverted Game note, "For an iPhone or iPad build": a new current bullet for the iPhone 17 at the owner's Kokiri spot. Its table gives the GPU span, the GPU work and the gaps before and inside submissions, before and after A-2. Gameplay GPU p50 is 21-24 ms, under the cap's 33.3 ms, so shader pull requests seedds#1752 and seedds#1833 were not tried. The frame is CPU-bound at 16-20 fps under thermal state 1-2. The bullet records the uncapped A/B of A-2, and a command allocator pool of 128 that removed its waits but no frame time, at a cost of about 300 MB (not kept). Open: translation cost, samples-passed reports, footprint growth. The CPU timeline the table was measured with is a local diagnostic, not in this pull request. rendering.md: the A-2 entry gets its measured result. A Found-on line in the note, and the outdated iPhone line under "Not run on an iPhone yet" now points to the section.
…d the allocator pool tried and reverted Game note, "For an iPhone or iPad build": a new current bullet for the iPhone 17 at the owner's Kokiri spot. Its table gives the GPU span, the GPU work and the gaps before and inside submissions, before and after A-2. Gameplay GPU p50 is 21-24 ms, under the cap's 33.3 ms, so shader pull requests seedds#1752 and seedds#1833 were not tried. The frame is CPU-bound at 16-20 fps under thermal state 1-2. The bullet records the uncapped A/B of A-2, and a command allocator pool of 128 that removed its waits but no frame time, at a cost of about 300 MB (not kept). Open: translation cost, samples-passed reports, footprint growth. The CPU timeline the table was measured with is a local diagnostic, not in this pull request. rendering.md: the A-2 entry gets its measured result. A Found-on line in the note, and the outdated iPhone line under "Not run on an iPhone yet" now points to the section.
…d the allocator pool tried and reverted Game note, "For an iPhone or iPad build": a new current bullet for the iPhone 17 at the owner's Kokiri spot. Its table gives the GPU span, the GPU work and the gaps before and inside submissions, before and after A-2. Gameplay GPU p50 is 21-24 ms, under the cap's 33.3 ms, so shader pull requests seedds#1752 and seedds#1833 were not tried. The frame is CPU-bound at 16-20 fps under thermal state 1-2. The bullet records the uncapped A/B of A-2, and a command allocator pool of 128 that removed its waits but no frame time, at a cost of about 300 MB (not kept). Open: translation cost, samples-passed reports, footprint growth. The CPU timeline the table was measured with is a local diagnostic, not in this pull request. rendering.md: the A-2 entry gets its measured result. A Found-on line in the note, and the outdated iPhone line under "Not run on an iPhone yet" now points to the section.
…d the allocator pool tried and reverted Game note, "For an iPhone or iPad build": a new current bullet for the iPhone 17 at the owner's Kokiri spot. Its table gives the GPU span, the GPU work and the gaps before and inside submissions, before and after A-2. Gameplay GPU p50 is 21-24 ms, under the cap's 33.3 ms, so shader pull requests seedds#1752 and seedds#1833 were not tried. The frame is CPU-bound at 16-20 fps under thermal state 1-2. The bullet records the uncapped A/B of A-2, and a command allocator pool of 128 that removed its waits but no frame time, at a cost of about 300 MB (not kept). Open: translation cost, samples-passed reports, footprint growth. The CPU timeline the table was measured with is a local diagnostic, not in this pull request. rendering.md: the A-2 entry gets its measured result. A Found-on line in the note, and the outdated iPhone line under "Not run on an iPhone yet" now points to the section.
…d the allocator pool tried and reverted Game note, "For an iPhone or iPad build": a new current bullet for the iPhone 17 at the owner's Kokiri spot. Its table gives the GPU span, the GPU work and the gaps before and inside submissions, before and after A-2. Gameplay GPU p50 is 21-24 ms, under the cap's 33.3 ms, so shader pull requests seedds#1752 and seedds#1833 were not tried. The frame is CPU-bound at 16-20 fps under thermal state 1-2. The bullet records the uncapped A/B of A-2, and a command allocator pool of 128 that removed its waits but no frame time, at a cost of about 300 MB (not kept). Open: translation cost, samples-passed reports, footprint growth. The CPU timeline the table was measured with is a local diagnostic, not in this pull request. rendering.md: the A-2 entry gets its measured result. A Found-on line in the note, and the outdated iPhone line under "Not run on an iPhone yet" now points to the section.
…d the allocator pool tried and reverted Game note, "For an iPhone or iPad build": a new current bullet for the iPhone 17 at the owner's Kokiri spot. Its table gives the GPU span, the GPU work and the gaps before and inside submissions, before and after A-2. Gameplay GPU p50 is 21-24 ms, under the cap's 33.3 ms, so shader pull requests seedds#1752 and seedds#1833 were not tried. The frame is CPU-bound at 16-20 fps under thermal state 1-2. The bullet records the uncapped A/B of A-2, and a command allocator pool of 128 that removed its waits but no frame time, at a cost of about 300 MB (not kept). Open: translation cost, samples-passed reports, footprint growth. The CPU timeline the table was measured with is a local diagnostic, not in this pull request. rendering.md: the A-2 entry gets its measured result. A Found-on line in the note, and the outdated iPhone line under "Not run on an iPhone yet" now points to the section.
…d the allocator pool tried and reverted Game note, "For an iPhone or iPad build": a new current bullet for the iPhone 17 at the owner's Kokiri spot. Its table gives the GPU span, the GPU work and the gaps before and inside submissions, before and after A-2. Gameplay GPU p50 is 21-24 ms, under the cap's 33.3 ms, so shader pull requests seedds#1752 and seedds#1833 were not tried. The frame is CPU-bound at 16-20 fps under thermal state 1-2. The bullet records the uncapped A/B of A-2, and a command allocator pool of 128 that removed its waits but no frame time, at a cost of about 300 MB (not kept). Open: translation cost, samples-passed reports, footprint growth. The CPU timeline the table was measured with is a local diagnostic, not in this pull request. rendering.md: the A-2 entry gets its measured result. A Found-on line in the note, and the outdated iPhone line under "Not run on an iPhone yet" now points to the section.
Draft, for discussion. Raw global memory (shaders reading through an address they loaded from memory) read at Metal's own GPU address, with no pool table lookup. Opt-in per game with a new profile key,
shader_raw_direct, set only for Ocarina of Time. Every other game's jobs, shaders and host behaviour stay as on main.Why
On an M1 Pro, Ocarina of Time is GPU-bound and latency-bound: every limiter is low, and its raw-memory compute shaders run at 384 threads per threadgroup. The lookup (2 MiB hint plus a 13-step binary search) holds registers across every raw access. Measured offline on the game's 62 starved compute shaders (
maxTotalThreadsPerThreadgroup):What it does
hle_nvn.c,nvn_mtl.m, whenmtl_gva_on()):gpuAddressplus the offset.GlobalToStorage.TryGenerateRawDirectOp, throughIGpuAccessor.QueryHostRawDirectand the job'sraw_direct):DM_NVN_GPUVA_DIAG: what the pool table holds.DM_NVN_GPUVA_SHIFT: hands out every GPU address moved into an unmapped range and counts CPU accesses through one.Evidence
All measured on an M1 Pro (32 GB) during development on a branch with this change:
DM_NVN_GPUVA_SHIFT, OoT made no CPU access through a GPU address and handed back no CPU address as a GPU one, over 9.8 M addresses handed out.[RED]line, picture correct.Not done yet (why it is a draft)
bench.pyrun of this exact commit against main.regress.py check): the key is off for every other game, andtest_dmsh_raw_last.py(main's lookup test) passes unchanged with this translator, so their translation should be byte-identical.regress.py chainon a couple of games would confirm it.[RED]line says so), and its raw accesses would read 0.Tests
test_dmsh_raw_direct.py(withDMSH_TEST_XLATE): helper shape, and GPU reads of three Metal buffers through their own addresses, scalar and 128-bit.test_dmsh_raw_last.py,test_dmsh_ampere_mem.py,test_dmsh_meshstage.py,test_nsp_chain.pyandtest_docs.pypass.test_env_switches.pyfails as on main:DM_NVN_CUT_FIRST,DM_NVN_RT_RECLAIM,DM_NVN_VC_WAYS,DM_NVN_BC_ALIAS.make -C dm/gfx/tests run): same results as main.t_pool_chunks,t_nvn,t_gmem_wordsandt_geometrynow check both modes: with the key off (GAME=010015100B514000) they run main's checks unchanged; with it on (GAME=0400486024DEC000) they run the direct-address ones. Writing them turned up two host fixes, included here:pool_buf, so one made over a write-protected page still takes the GPU's writes (issue GPU copies into a pool chunk get lost if its pages were write-protected (LEGO Star Wars text garbled) #1591). This is checked by a new case that fails without the fix.gmem_commit_patchreads its lag statistics before the table rebuild, so lagging words are no longer counted as older.Questions for reviewers
shader_raw_direct 1?Docs: a new reference entry in
docs/reference/shaders.md("Raw global memory read through Metal's own GPU addresses"), and the key's row indm/games/README.md.🤖 Generated with Claude Code