Skip to content

feat(vk): the game's anti-aliasing networks on AMD RDNA3/4 matrix units (16x16x16 cooperative matrices) - #1975

Open
unixman69 wants to merge 8 commits into
NotRexed:mainfrom
unixman69:feat/staged-hmma
Open

unixman69 wants to merge 8 commits into
NotRexed:mainfrom
unixman69:feat/staged-hmma

Conversation

@unixman69

Copy link
Copy Markdown
Contributor

On AMD cards the game's own anti-aliasing never runs as on the console: at 100% its network runs on a slow software fallback, and at 75% and below (or Auto) the network is left out and an FXAA stand-in takes its place, which is visibly softer. This runs the game's networks on the matrix units of AMD RDNA3/4 cards, so every Render resolution gets the game's own anti-aliasing and upscaling. Closes #1284 (tracking: #1006).

Why it did not run: the networks multiply with HMMA on NVIDIA's 16x8x16 fragments. RDNA3/4 on RADV offers only 16x16x16 cooperative matrices (compute stage, subgroup scope), so vk_coopmat.cpp fell back to the software function, and the lowering left out every network but the 100% one.

What changed

  • gpu/vk/sirit: OpCooperativeMatrixStoreKHR, and memory operands on the cooperative matrix load and store (NonPrivatePointer, which the Vulkan memory model needs for workgroup memory).
  • backend/spirv/mma_staged.h, mma_route.h (new), and the SPIR-V back end: a third route for HMMA. Each HMMA's A and B are staged through shared memory into 16x16 tiles, multiplied as f16 x f16 -> f32 with a zero accumulator, and the console's two roundings are kept: D = f16(f16(C + S1) + S2), with NoContraction so the driver cannot fold them. PlanMatrixRoute picks native, staged or the software function for each program from its shared memory and workgroup size.
  • vk_coopmat.cpp, vk_mma_staged_test.h (new): before the route is used, a self-test at start runs the route's own function on 1024 random problems (131,072 results) against the software function's arithmetic and HMMA's rounding bound, behind vk_draw.cpp's driver fault guard. If it fails, the software function is used as before. On this card: 31 differ from the software function by a rounding (max 2 f16 ulps of D, 0.95 of the bound of HMMA's roundings), 0 beyond it.
  • vk_shader.cpp, vk_draw.cpp: on the staged route every network translates, the 67/58/50% networks count as gameplay's, and a network that still fails falls back as before (the RGB9E5 decode keeps fix(vk): decode gameplay RGB9E5 upscale fallback #1552's rule: every network but the full-size pass). The route is in the program cache's stamp.
  • Default: on with Mesa RADV only, where it was tested. AMD's own driver lists 16x16x16 on RDNA2 too, without the matrix hardware, and has crashed building such pipelines there (Lichii Optional NPU-Accelerated Post-Processing (ONNX Runtime / DirectML) #633), so other drivers need RX_VK_MMA_STAGED=1. RX_VK_MMA_STAGED=0 turns it off. NVIDIA keeps its native route and its code path is unchanged; Intel's matrices are a different shape, so nothing changes there.

Testing: RX 9070 XT 16 GB, Mesa RADV 26.2.6 (Vulkan 1.4), Ryzen 9 9950X3D, 30 GB, Linux (KDE Plasma, Wayland). Output resolution 2560x1440, Render resolution 75%, RX_VK_BATCH=0. Kokiri Forest from a save, standing, then 28 s of camera turning (frames from hookshot_log.txt).

Build Anti-aliasing at 75% fps 1% low GPU ms a frame
main 8a8ac27 FXAA stand-in 58.2 43.2 9.6
main 8a8ac27 + this PR the game's network, 1920x1080 -> 2560x1440 57.3 43.3 10.4
main 8a8ac27 + this PR, HDR Native the game's network 58.0 43.2 10.2
  • Shader preparation: 46,666 programs prepared and 1 failed (main: 46,597 and 70 failed). The log names a staged route for each network, the self-test passes, and there was no device lost.
  • At 100%, on an earlier base (main 5609af4 with local changes), the network gave the same picture as the software fallback at the same GPU time (10.4 ms software, 10.3 ms staged).
  • spirv-val (Vulkan 1.3, with the device's layout features) passes on all 70 network modules dumped from the game on that earlier base; nx2/test_hmma_staged_spirv.py checks synthetic programs on each route and the self-test's module in CI.
  • python -I -B tests/run_all.py: 336 passed, 0 failed, 11 skipped (Linux), on this branch rebased onto fc375e9. The game runs above were on 8a8ac27; the branch rebased onto fc375e9 without conflicts.

Screenshots: the same frame at 75%, 2x crops: main's FXAA stand-in on the left, the game's network with this PR on the right.

main vs this PR at 75%

Not tested: RDNA3 on RADV (only RDNA4 here), the Steam Deck, AMD's own driver (off by default there; RX_VK_MMA_STAGED=1 to try), Windows, NVIDIA (native route untouched), cutscene upscalers in a movie (they now translate; if one takes the GPU over 40 ms a frame, the existing movie fallback applies), and Render resolution Auto.

New checks: nx2/test_hmma_staged_spirv.py (routes, memory model, spirv-val), tests/test_mma_staged_driver.py (the RADV default ahead of the self-test; fails without the change). tests/test_upscale_fallback.py and tests/test_taau_env_lookup.py follow the new route.

Suggested row for docs/tested-configurations.md:

| 2026-11-10 | main 8a8ac27 + this PR | Gentoo Linux, KDE Plasma 6 Wayland | RX 9070 XT 16 GB, Mesa RADV 26.2.6 | Ryzen 9 9950X3D, 30 GB | 2560x1440 output, Render resolution 75%, RX_VK_BATCH=0 | Kokiri Forest, camera turning | 57.3 fps (main: 58.2 with the FXAA stand-in); the game's network at every Render resolution |

🤖 Generated with Claude Code

unixman69 and others added 3 commits November 10, 2026 19:07
… cooperative matrix load and store

The staged matrix route that follows writes cooperative matrices back to shared memory, and on
the Vulkan memory model its loads and stores of workgroup memory must carry the NonPrivatePointer
operand. Sirit had the load only, without memory operands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts (16x16x16 cooperative matrices)

On AMD cards the game's anti-aliasing and upscaling networks never run as on the console: the
100% network runs its matrix multiplies in a slow software function, and the 75/67/58/50/33%
networks and the cutscene upscalers are left out for stand-ins (issue 1284, tracking 1006). The
networks use HMMA on 16x8x16 fragments; RDNA3/4 on RADV offers only 16x16x16 cooperative matrices.

The multiplies now go through those matrices staged through shared memory (mma_staged.h): each
HMMA's A and B into 16x16 tiles, f16 x f16 -> f32 products with a zero accumulator, and the
console's two roundings, D = f16(f16(C + S1) + S2), kept with NoContraction so the driver cannot
fold them. mma_route.h plans the route for each program from its shared memory and workgroup
size (native, staged, or the software function). At start the renderer runs a self-test of the
route's own function on 1024 random problems (vk_mma_staged_test.h) against the software
function's arithmetic and HMMA's rounding bound, behind the driver fault guard; the route is used
only when it passes. With it every network translates, the 67/58/50% networks are gameplay's,
and a network that still fails falls back as before.

On by default with Mesa RADV only, where it was tested; RX_VK_MMA_STAGED=1 asks for it on any
driver (AMD's own lists 16x16x16 on RDNA2 too, without matrix hardware), RX_VK_MMA_STAGED=0
never. NVIDIA keeps its native route. The route is in the program cache's stamp.

Tests: nx2/test_hmma_staged_spirv.py builds programs on each route and the self-test's module
and runs spirv-val on them, with the staged route's Vulkan memory model rules;
tests/test_mma_staged_driver.py holds the driver gate ahead of the self-test;
tests/test_upscale_fallback.py and tests/test_taau_env_lookup.py follow the new route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Players on AMD cards need to know the game's own anti-aliasing now runs there, how to tell from
the log, and how to turn it off if the self-test stops the start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Gonzxlx

Gonzxlx commented Nov 10, 2026

Copy link
Copy Markdown

this one is huge, thanks for the amazing work 👍

@hckrman101 hckrman101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice work. The design holds up: it picks a route for each program from one place (PlanMatrixRoute) and the translator, vk_shader.cpp and vk_draw.cpp all decide by it, it checks itself at start before turning on, it is limited to RADV by default, and the cache stamp covers it. I went through the routing logic and found nothing wrong:

  • left_out is unchanged from main on every non-staged path (NVIDIA native, mma_software, RX_VK_NO_F16).
  • taau_key gives the same answers as before off the staged route and checks the key before it asks for the route.
  • upscale_stub's new !full_size_pass is equivalent to the old !gameplay_network(key) || gameplay_taau.
  • profile() and profile_pinned() agree on SupportsSubgroupStage, so the renderer and the translator can't pick different routes.
  • Info is serialized whole, so cached programs get the same route.
  • The scratch sizing (threads/32 * kStagedBytes, X a multiple of 32, pin_subgroup asking for full 32-lane subgroups, and staged_multiplies requiring sg32_stages + computeFullSubgroups) is consistent.

One thing I'd fix before merge:

Not every barrier gets MakeAvailable/MakeVisible on the staged route. Only EmitBarrier was changed. EmitSubgroupBarrier, which the Ampere lowering emits as MEMBAR bit 20 for LDSM (the warp's shared-memory stores followed by the fragment loads), and MemoryBarrier (MEMBAR.CTA/GL) still send AcquireRelease | WorkgroupMemory with no availability or visibility. Under the Vulkan memory model, which these programs now declare, the non-private STS -> LDSM exchange inside a subgroup is therefore not formally ordered. The networks are the HMMA programs that do exactly this. RADV/NIR doesn't really act on those bits, which would explain why the self-test and the game run fine there. A driver that follows the model strictly (RX_VK_MMA_STAGED=1 elsewhere, which the readme invites) could read stale fragments. The fix is to OR in the same visibility mask in EmitSubgroupBarrier and MemoryBarrier when ctx.workgroup_non_private, and to add a ir.SubgroupBarrier() to tests/hmma_staged_ir.cpp, so that nx2/test_hmma_staged_spirv.py's "every barrier makes memory available and visible" check covers that barrier too. At the moment the fixture only has ir.Barrier().

Smaller points:

  • CI hasn't run on this head (no checks reported, presumably the fork's workflows need approval), so the new nx2/test_hmma_staged_spirv.py and the updated test_taau_env_lookup.py / test_upscale_fallback.py haven't run in CI yet. Your local run_all result (336 passed) is good to have, but someone should approve the run.
  • On the staged route every movie network now translates as well, not only the gameplay ones, and RX_VK_NETWORK_KEEP no longer applies to them. The description already lists movies as untested. Given the history of issue 26 (AMD compile times on these networks), it would be worth one cutscene run on RADV, checking compile stalls and the 40 ms movie fallback, before this goes in.
  • Nit: the self-test module declares SignedZeroInfNanPreserve for 16 and 32 bits. If the game's own staged modules don't, the test isn't checking quite the same float mode as the programs it vouches for. That's harmless on RADV, but you may want them to match.

The RDNA3 and Windows/AMDVLK results are still open, as the description says. The merge decision for a change this large is NotRexed's.

unixman69 and others added 3 commits November 11, 2026 14:06
…ble and visible

On the staged matrix route the program's own shared memory is on the Vulkan memory model, where a barrier orders
non-private accesses only when its semantics make memory available and visible. EmitBarrier did; EmitSubgroupBarrier
(the warp's stores before Ampere's LDSM, MEMBAR bit 20) and the MEMBAR.CTA/GL memory barriers did not, so the
STS -> LDSM exchange inside a subgroup was not formally ordered. RADV does not act on those bits, which is why the
self-test and the game ran right; a driver that follows the model strictly could read stale fragments.

All three now take the same mask when ctx.workgroup_non_private; the GLSL450 and native routes are unchanged.
tests/hmma_staged_ir.cpp's program gains a subgroup barrier and a MEMBAR.CTA, and the module inspection counts
OpMemoryBarrier too, so nx2/test_hmma_staged_spirv.py's "every barrier makes memory available and visible" covers
them (3 of 5 without this change). The fixture's profile keeps the float modes vk_shader.cpp's make_profile keeps
(SignedZeroInfNanPreserve 16 and 32), and the test checks that the staged modules and the self-test's declare the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/staged-hmma

# Conflicts:
#	gpu/vk/vk_draw.cpp
#	gpu/vk/vk_shader.cpp
#	tests/test_taau_env_lookup.py
…/staged-hmma

# Conflicts:
#	gpu/vk/vk_draw.cpp
@unixman69

Copy link
Copy Markdown
Contributor Author

Thanks for the careful review. You were right about the barrier gap. It's fixed in 4dd396d ("every barrier of a staged program makes shared memory available and visible"), on top of the commits you reviewed. Main is merged in (5aa3971), with #2042's switch changes: the staged route keeps its own taau_key(const Program&) and left_out logic and now reads RX_VK_NO_F16 and RX_VK_COMPUTE_WAIT through env_on. #2195's fault counting now also covers the self-test's pipelines.

Barriers. EmitSubgroupBarrier (the warp's stores before Ampere's LDSM) and MemoryBarrier (MEMBAR.CTA/GL) now take the same MakeAvailable | MakeVisible as EmitBarrier when ctx.workgroup_non_private, through one helper. The GLSL450 and native routes are unchanged. tests/hmma_staged_ir.cpp's program now has an ir.SubgroupBarrier() and an ir.WorkgroupMemoryBarrier() besides its ir.Barrier(), and the module inspection counts OpMemoryBarrier as well as OpControlBarrier. nx2/test_hmma_staged_spirv.py therefore covers all three: staged modules show 5 of 5 barriers making memory available and visible, against 3 of 5 without the fix (the test fails there). spirv-val (vulkan1.3) passes on all 11 modules, including the memory barriers with the new semantics on the Vulkan memory model.

SignedZeroInfNanPreserve. These already match. vk_shader.cpp's make_profile sets support_fp16/fp32_signed_zero_nan_preserve for every program, and the staged route only takes programs with f16 (DefineMatrixRoute requires F16). So every staged program declares 16 and 32, as the self-test's module does. To keep it that way, the fixture's profile now mirrors make_profile there, and the test asserts that the staged modules and the self-test declare the same widths. It also checks that make_profile still sets both.

Movies. RADV, RX 9070 XT, Mesa 26.2.6, this branch before the merge (on main 07b0359): the intro movie plays at every start, so every lab run covers it. With shaders prepared at build time, as the builder does, the movie's networks run on the staged route throughout: 14 dispatches a frame, 6.4-7.6 ms of GPU time a frame at 2560x1440 output, 60 fps from the second 5-second block on. No waits for shaders or pipelines, no draws left out, and the 40 ms movie fallback never triggered (no "movie upscaling uses the linear compute path"). I haven't run it with an empty program cache, where compile time would show.

After the merge. I ran a game build of the merged branch: the self-test passes, the networks run on the staged route, and the game network's picture matches the earlier capture of the same setting (1-px outlines 6400 vs 6201 per megapixel, standing flicker 5.00 vs 5.20). python -I -B tests/run_all.py: 384 passed, 0 failed, 11 skipped on Linux, including nx2/test_hmma_staged_spirv.py, test_taau_env_lookup.py and #2042's test_env_on.py.

CI. It still needs a maintainer to approve the workflows for this fork.

@Joey-Magnus Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4dd396d fixes the barrier gap: EmitSubgroupBarrier and MemoryBarrier now make memory available and visible like EmitBarrier, and the IR fixture covers all three. The movie run on the 9070 XT answers the other open point.

It conflicts with main again, at one place. In gpu/vk/vk_shader.cpp around line 1238, #2343 (GCN f16 lowering) added || !dev.vk12.shaderFloat16 to the network left-out condition, and your branch replaces that line with the PlanMatrixRoute version. When you resolve it, keep #2343's term in your left_out, i.e. env_on("RX_VK_NO_F16") || !dev.vk12.shaderFloat16. Otherwise GCN cards without f16 stop leaving those networks out. I resolved it that way locally: run_all gives 377 passed, 0 failed, including test_mma_staged_driver, test_taau_env_lookup, test_upscale_fallback and test_amd_subgroup_profiles.

The Windows "Python source checks part 1/2" failure is on a base from before #2397 (the Windows test_native_astc_texture_count build fix), so I don't think it's this PR. A re-run after the rebase should show. Results on RDNA3 and on Windows/AMDVLK are still missing, as the description says. The final call on a change this size is NotRexed's.

@hckrman101

Copy link
Copy Markdown
Collaborator

This now conflicts with main in gpu/vk/vk_coopmat.cpp, gpu/vk/vk_shader.cpp. Could you merge main into the branch (or rebase) when you get a chance? I'll take another look once it's up to date.

@unixman69

Copy link
Copy Markdown
Contributor Author

Main is merged in (01faa82, then a7ec84b for the four commits after it), so the PR is mergeable again. Three conflicts:

Checked on the merge (RX 9070 XT, Mesa RADV 26.2.6, Linux):

Thanks for the reviews. RDNA3 and Windows/AMDVLK results are still open, as the description says.

@Joey-Magnus Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point from my last review is fixed. The network left-out condition at gpu/vk/vk_shader.cpp:1288-1290 keeps #2343's env_on("RX_VK_NO_F16") || !dev.vk12.shaderFloat16. Moving the staged route's cache flags to 256/512 (vk_shader.cpp:1189) is also the right call, because #2457 now uses bits 8 to 128.

I merged the branch into current main (51da046) and ran tests/run_all.py with the Vulkan and SPIR-V headers: 470 passed. The only failures were test_lift_cache, which fails on main on this machine too, and a load timeout in test_recorder_sync, which passes when run alone. The red Python checks on GitHub are test_env_on from main at your base, and a re-run should clear them. I also merged it together with #2306, which touches spirv_emit_context.cpp and emit_spirv_atomic.cpp: no conflicts. #2742 changes the cooperative matrix probe in vk_coopmat.cpp, so whichever of the two lands second should re-check the self-test path.

Still open, as the description says: the route is on by default for every RADV card with 16x16x16 matrices, so RDNA3 (Tier 1) too, and neither RDNA3 nor Windows/AMDVLK has been run. Whether to take a change this size is NotRexed's call.

@lucasmazzoni45-del

Copy link
Copy Markdown
Contributor

Tested on RDNA3 with AMD's own driver on Windows, head a7ec84b (built here with make_game.py, llvm-mingw, shaders prepared at the first start). This is the first result on RDNA3 and on Windows/AMD's driver.

Testing: RX 7800 XT 16 GB (RDNA3), AMD Adrenalin 26.10.1 (Windows driver 32.0.32015.7029; the log says "AMD proprietary driver 26.10.1 (LLPC)", Vulkan 1.4), Ryzen 7 5800X3D, 32 GB, Windows 11 Home 10.0.26300. Windowed, output 2560x1440, Render resolution 75%, Upscaler "the game's", frame generation Auto (it was on in my options file), empty hookshot_env.txt. RX_VK_MMA_STAGED=1 against =0 on the same build, each in a fresh game folder with a cold program cache. Scripted input (HOS_INPUT_SCRIPT) from the title screen into a save slot (which scene it loads was not checked), 170 s a run, RX_VK_PRECOMPILE=0 for the timed runs.

Results

  • AMD's driver takes the route when it is forced: the self-test passes. staged matrix self-test: 1024 problems, 131072 elements: 39 differ from the software function by a rounding (max 2 f16 ulps of D, 0.95 of the bound of HMMA's roundings), 0 beyond it, 27 from exact sums rounded as HMMA rounds; ... 0 exact-problem errors, 0 unwritten, 0 of the program's own shared words changed, then matrix multiplies on 16x16x16 cooperative matrices, staged through shared memory (2816 bytes a subgroup; the self-test passed ...).
  • Routes per network (first start, shaders prepared): 62 networks staged through shared memory, 2 left on the software function because they do not fit (workgroup 32x4x1, 21600 bytes of shared memory, the device's limit 32768).
  • Shader preparation: prepared 38809 shader programs (46663 new, 4 failed, 9919 compute pipelines; ...) in 764.0 s with RX_VK_MMA_STAGED=1, against (43031 new, 70 failed, 0 compute pipelines; ...) in 75.0 s with =0. So 9919 compute pipelines, 16x16x16 matrices included, were built by the driver with no crash, no TDR and no device lost (the run was stopped by closing the window while the pipeline pass started; no Display 4101 or application-error events in the Windows logs for the session).
  • Networks left out on this card at 75%: 10 programs with =0 (the FXAA stand-in), 1 with =1 (the 21600-byte one). With =0 the movie line movie upscaling uses the linear compute path appears; it did not appear with =1.
  • Gameplay stat lines of the two 170 s runs (frames with 800 draws or more, so the sample is small and the two runs did not see the same scenes; the first minute of each had 200 or so draws left out while shaders and pipelines were still built in play):
=0 =1
samples (5 s lines, 800+ draws) 29 23
fps, mean (min) 62.5 (47.0) 53.1 (43.4)
GPU ms a frame, mean (min-max) 7.1 (5.6-9.5) 8.4 (6.3-10.4)

I do not take the fps difference as a measurement of the route: the scenes differ, and the =1 run built 860 pipelines in play (322480 ms of pipeline build time in the stat line) against 757 for =0, with a heavier first minute. A matched capture from a save, standing then turning, would be needed for that.

  • device lost, driver faults, did not complete in time: none in either run.

Not tested: RADV on RDNA3, the default (RX_VK_MMA_STAGED unset: off on this driver, as the PR says), a long session (170 s a run), the 100% Render resolution and Auto, cutscenes beyond the intro movie, an empty program cache over a whole play session, NVIDIA, Linux, the picture compared against RADV's (no screenshots taken), the pipeline-compile time on first run beyond the numbers above.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Run OoT’s TAAU network on AMD RDNA3/RDNA4 matrix hardware (16x16x16 cooperative matrices)

5 participants