Skip to content

Upload only the shader constants a draw reads - #221

Merged
YuutaTsubasa merged 3 commits into
mainfrom
claude/perf-constants
Oct 20, 2026
Merged

YuutaTsubasa merged 3 commits into
mainfrom
claude/perf-constants

Conversation

@YuutaTsubasa

Copy link
Copy Markdown
Owner

Follow-up to #215: the per-draw shader constant cost on the main thread.

What changed

  • Only the constants a shader reads. Every draw used to byte-swap both stages' whole 4 KiB constant files into the upload ring (8 KiB a draw). XenosRecomp reads a float4 constant only through the name the shader's constant table gives it, so each native shader now records those registers (src/shader_constant_use.cpp, from the guest container's table) and a draw swaps only them. The rest of each 4 KiB block keeps what an earlier draw left; the shader never reads it.
  • Relative addressing gets everything. A shader whose ALU instructions use a0/aL constant addressing may read past its array, so it still uploads all 256 registers: 33 of the game's 287 vertex shaders, no pixel shaders. The microcode walk vertex_palette.cpp already had moved to src/xenos_microcode.h so both use it.
  • Skinning palette copied once. It is swapped from guest memory straight into the ring instead of copy, swap, copy.
  • Switches. SFR_CONSTANT_RUNS=0 (also on the Test options page, and kept in the Android diagnostic ZIP) restores the full upload and the old palette copy. SFR_TEST_CONSTANT_POISON=1 fills the registers a draw does not write with NaNs. SFR_CONSTANT_TIMING=1 prints the constant writes' ns and bytes per draw, plus what each shader uploads.

Dirty tracking at write time was not needed after this: the writes left are ~620 bytes a draw.

A/B on the AYN Thor

One APK (versionCode 61), SFR_CONSTANT_RUNS=0 for the baseline. Moving Dolphin Resort race, HOST_THREADS over frames 13000-21000, frame metrics off, runs interleaved. drawn_ms stays near 16 ms because of the 60 fps pacing.

Constants draw_ms record_ms cpu_ms undrawn_ms frames skipped of 8040
all (baseline) 3.75 / 3.73 / 3.73 1.04 / 1.01 / 1.01 14.36 / 14.27 / 14.25 9.82 / 9.81 / 9.57 214 / 127 / 124
only the ones read 3.45 / 3.39 / 3.38 0.75 / 0.73 / 0.72 13.77 / 13.84 / 13.63 9.21 / 9.62 / 8.42 35 / 34 / 15

Mean change: draw_ms -0.33, record_ms -0.29, cpu_ms -0.54 ms a frame. Race speed was 0.9994 in every run.

Per-draw timing of the constant writes (SFR_CONSTANT_TIMING=1, one run each, two clock reads of ~0.13 us included):

ns / draw bytes / draw
Thor, before 561 8192
Thor, after 237 623
Windows i9 Vulkan, before / after 776 / 199 8192 / 914
Windows i9 D3D12, before / after 778 / 140 8192 / 868

A frame has about 820 draws. The Windows machine had other load on it, so its frame-level numbers are not reported.

Pixel check

Screenshots from two runs can't be compared pixel for pixel in this harness. The menus animate in real time and the fixed-step race diverges: two runs of the same build differ in about 520,000 pixels at present 5000. Behaviour was checked three other ways instead:

  • Statically, all 476 shaders of the 0.10.13 pack.
    • In SPIR-V, every OpAccessChain into the stage's constant block uses a constant row inside the uploaded registers.
    • In DXIL, every cbufferLoadLegacy on the stage's cbuffer does the same.
    • Dynamic rows appear in exactly the 33 relative shaders, which get the full upload.
    • The game's own split (SHADER_CONSTANT_RUNS) matches.
  • NaN poison. SFR_TEST_CONSTANT_POISON=1 races on Windows Vulkan and D3D12 render normally (characters, skinning, effects, HUD). Any read of an unwritten register would show up as NaN garbage.
  • Visual comparison. Vulkan and D3D12 races with the change on and off look the same.

Tests

  • Windows: ctest 166/166.
  • Linux (WSL, clang 15): ctest 159/159.
  • Python: 353 tests OK, 11 skipped without the game files.
  • Android arm64 build.
  • Windows Vulkan race with the Khronos validation layer: 0 errors, 455 shipped pipelines loaded.
  • Windows D3D12 race.
  • Thor races at speed 0.9994.

New unit test: tests/shader_constant_use_test.cpp.

Risks

  • A future translator change. The ranges rely on how XenosRecomp reads constants. If a new translator version read a register outside the constant table, that read would see stale data. The poison switch and the pack checks above would catch it. SFR_CONSTANT_RUNS=0 turns the change off.
  • Shaders outside the pack (mods, runtime translation) are classified the same way from their own containers. Anything this code doesn't understand falls back to the full upload.
  • SFR_CONSTANT_UPLOAD_REUSE and SFR_CONSTANT_REUSE_TRACE still compare and upload whole arrays, as before.

🤖 Generated with Claude Code

YuutaTsubasa and others added 2 commits October 20, 2026 21:14
Every draw byte-swapped both stages' whole 4 KiB constant files into the
upload ring (8 KiB a draw, ~3% of an AYN Thor race's main thread). The
translated shaders read a float4 constant only through the name their
constant table gives it, so each native shader now records those registers
(shader_constant_use.cpp, from the container's table) and a draw swaps only
them; the rest of each 4 KiB block keeps what an earlier draw left, which
the shader never reads. A shader that addresses constants relatively (a0/aL,
33 of the game's vertex shaders) still gets all 256. The skinning palette is
swapped from guest memory straight into the ring instead of a copy, a swap
and a second copy.

SFR_CONSTANT_RUNS=0 (also a Test options choice) restores the full upload;
SFR_TEST_CONSTANT_POISON=1 fills the unwritten registers with NaNs to show a
missed read; SFR_CONSTANT_TIMING=1 prints the constant writes' time and bytes
per draw and what each shader uploads. The microcode walk vertex_palette.cpp
used is shared in xenos_microcode.h.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 20, 2026 15:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@YuutaTsubasa
YuutaTsubasa merged commit 83e1e96 into main Oct 20, 2026
4 checks passed
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.

2 participants