perf(host): the game's zstd stream decode on the host's libzstd (HOS_NATIVE_ZSTD, opt-in) #2738
Closed
github-sync
wants to merge 8 commits from
github/pr/2738 into main
pull from: github/pr/2738
merge into: MagicBeanDev:main
MagicBeanDev:main
MagicBeanDev:github/pr/2893
MagicBeanDev:github/pr/2890
MagicBeanDev:github/pr/2887
MagicBeanDev:github/pr/2878
MagicBeanDev:github/pr/2875
MagicBeanDev:github/pr/2874
MagicBeanDev:github/pr/2873
MagicBeanDev:github/pr/2871
MagicBeanDev:github/pr/2756
MagicBeanDev:github/pr/2897
MagicBeanDev:github/pr/2896
MagicBeanDev:github/pr/2718
MagicBeanDev:github/pr/2842
MagicBeanDev:github/pr/2691
MagicBeanDev:github/pr/2895
MagicBeanDev:github/pr/2743
MagicBeanDev:github/pr/2765
MagicBeanDev:github/pr/2708
MagicBeanDev:github/pr/2732
MagicBeanDev:github/pr/2755
MagicBeanDev:github/pr/2817
MagicBeanDev:github/pr/2876
MagicBeanDev:release-please--branches--main
MagicBeanDev:release-please--branches--main--release-notes
MagicBeanDev:github/pr/2778
MagicBeanDev:github/pr/2888
MagicBeanDev:github/pr/2866
MagicBeanDev:github/pr/2877
MagicBeanDev:github/pr/2800
MagicBeanDev:github/pr/2829
MagicBeanDev:github/pr/2850
MagicBeanDev:github/pr/2889
MagicBeanDev:github/pr/2882
MagicBeanDev:github/pr/2860
MagicBeanDev:github/pr/2849
MagicBeanDev:github/pr/2886
MagicBeanDev:github/pr/2794
MagicBeanDev:github/pr/2840
MagicBeanDev:github/pr/2830
MagicBeanDev:github/pr/2834
MagicBeanDev:github/pr/2719
MagicBeanDev:github/pr/2742
MagicBeanDev:github/pr/2741
MagicBeanDev:github/pr/2820
MagicBeanDev:github/pr/2771
MagicBeanDev:github/pr/2859
MagicBeanDev:github/pr/2858
MagicBeanDev:github/pr/2823
MagicBeanDev:github/pr/2757
MagicBeanDev:github/pr/2769
MagicBeanDev:github/pr/2844
MagicBeanDev:github/pr/2843
MagicBeanDev:github/pr/2673
MagicBeanDev:github/pr/2855
MagicBeanDev:github/pr/2739
MagicBeanDev:github/pr/2807
MagicBeanDev:github/pr/2851
MagicBeanDev:github/pr/2789
MagicBeanDev:github/pr/2814
MagicBeanDev:github/pr/2818
MagicBeanDev:github/pr/2774
MagicBeanDev:github/pr/2772
MagicBeanDev:github/pr/2750
MagicBeanDev:github/pr/2786
MagicBeanDev:github/pr/2785
MagicBeanDev:github/pr/2782
MagicBeanDev:github/pr/2779
MagicBeanDev:github/pr/2775
MagicBeanDev:github/pr/2706
MagicBeanDev:github/pr/2766
MagicBeanDev:github/pr/2747
MagicBeanDev:github/pr/2759
MagicBeanDev:github/pr/2745
MagicBeanDev:github/pr/2740
MagicBeanDev:github/pr/2733
MagicBeanDev:github/pr/2725
MagicBeanDev:github/pr/2727
MagicBeanDev:github/pr/2728
MagicBeanDev:github/pr/2729
MagicBeanDev:github/pr/2721
MagicBeanDev:github/pr/2722
MagicBeanDev:github/pr/2715
MagicBeanDev:github/pr/2714
MagicBeanDev:github/pr/2707
MagicBeanDev:github/pr/2703
MagicBeanDev:github/pr/2705
MagicBeanDev:github/pr/2697
MagicBeanDev:github/pr/2679
MagicBeanDev:github/pr/2676
MagicBeanDev:github/pr/2669
MagicBeanDev:docs/what-hookshot-connects-to
MagicBeanDev:fix/hdr-encode-test
MagicBeanDev:fix/main-tests
MagicBeanDev:fix/play-cache-files
MagicBeanDev:perf/game-barrier-order
MagicBeanDev:feat/heap-lazy
MagicBeanDev:feat/texture-detail
MagicBeanDev:perf/astc-opaque-bc1
MagicBeanDev:fix/fs-dir-lock
MagicBeanDev:fix/framegen-native-kept
MagicBeanDev:fix/update-no-downgrade
MagicBeanDev:port/k10-gcn-f16-mirrors
MagicBeanDev:fix/framegen-mult-cap
MagicBeanDev:chore/release-as-2.1.1
MagicBeanDev:perf/pcache-load-switch
MagicBeanDev:fix/prepare-progress-log
MagicBeanDev:fix/update-closes-stale-game
MagicBeanDev:fix/hdr-scaled-sampled
MagicBeanDev:perf/first-start-preset
MagicBeanDev:perf/zero-upload
MagicBeanDev:fix/vcore-escape-2s
MagicBeanDev:test/fake-gh-atomic
MagicBeanDev:perf/batch-idle-flush
MagicBeanDev:test/finish-stub-a
MagicBeanDev:chore/record-2.1.0
MagicBeanDev:fix/main-red-tests
MagicBeanDev:fix/test-display-startup-driver-cache
MagicBeanDev:feat/mesh-shader-mode
MagicBeanDev:fix/menu-probes-at-startup
MagicBeanDev:fix/fps-counter-classic
MagicBeanDev:fix/test-log-batch-flake
MagicBeanDev:fix/narrow-screen-hud
MagicBeanDev:ci/prereleases
MagicBeanDev:release/v2.0.15
MagicBeanDev:hotfix/v2.0.15
MagicBeanDev:fix/present-ahead-replay
MagicBeanDev:release/v2.0.13
MagicBeanDev:fix/bracket-paths-2013
MagicBeanDev:fix/hdr-overlays-2013
MagicBeanDev:fix/fps-counter-scale-2013
MagicBeanDev:fix/stick-seen-at-rest
MagicBeanDev:hotfix/menu-fits-screen
MagicBeanDev:fix/f1-always-opens-menu
MagicBeanDev:fix/ini-keep-lines
MagicBeanDev:claude/cranky-pike-7cf247
MagicBeanDev:perf/texture-answer-flat
MagicBeanDev:fix/stamp-test-modules
MagicBeanDev:fix/appimage-fonts
MagicBeanDev:fix/display-awake
MagicBeanDev:release/v2.0.12
MagicBeanDev:fix/discord-windows
MagicBeanDev:rc/v2.0.12-tests
MagicBeanDev:fix/dlss-keep-feature
MagicBeanDev:rc/v2.0.12
MagicBeanDev:button-icons
No reviewers
Labels
Clear labels
accessibility
Barrier affecting people with disabilities
autorelease: pending
autorelease: tagged
bug
Something isn't working
dependencies
Pull requests that update a dependency file
documentation
Improvements or additions to documentation
duplicate
This issue or pull request already exists
enhancement
New feature or request
github_actions
Pull requests that update GitHub Actions code
good first issue
Good for newcomers
help wanted
Extra attention is needed
invalid
This doesn't seem right
question
Further information is requested
testing wanted
wontfix
This will not be worked on
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
Milestone
Clear milestone
No items
No milestone
Assignees
Clear assignees
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!2738
Loading…
Reference in a new issue
No description provided.
Delete branch "github/pr/2738"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The game decodes every streamed file with its own lifted copy of zstd (ParallelDecompThread, about 3.5 ms of CPU a frame in the standard test). This adds an opt-in switch,
HOS_NATIVE_ZSTD, that hands that decode to the host's libzstd 1.5.7, already vendored and linked forhost/prompts.c. In a stub, libzstd takes about a third of the lifted decoder's time per MiB. In the game the difference is too small for 4 pairs of the standard test to resolve, so it stays default off.This is a performance change, not a bug fix.
What changed
host/hos_zstd.c(new):hos_zstd_stream, a replacement for the game's per-chunk wrapperf_80115c80around zstd's buffer-less API (frame header from the first chunk,begin_usingDDict, then thenextSrcSizeToDecompress/decompressContinueloop with the game's staging buffer for a piece split between chunks). Every field of the game's stream object (state, dst, room, bytes still to come, staged fill) and every result (bytes to come, 0, -1, -2 fordstSize_tooSmall, -3 for input past the frame's end) is kept as the game's code leaves it. libzstd keeps its ownZSTD_DCtxper stream; the game's context is never written. The game's dictionary is used by reference: aZSTD_DDictmade from the game's content and checked against a copy of the game's entropy tables, so a dictionary remade at the same address is never served stale. Output pages are marked withHOS_WRITTENafter the writes. States 3 and 4 (failed, done) and anything unexpected (a misaligned workspace, an allocation failure) go back to the lifted code. Fixed tables of 64 streams and 64 dictionaries, under a spin lock; the peak in use is logged.HOS_NATIVE_ZSTD=1: decodes on the host and gives the virtual core up while it decodes (hos_vcore_away/hos_vcore_back), as a sleep does.HOS_NATIVE_ZSTD=2: decodes on the host and keeps the core.0: off, the lifted code runs as before.nx2/oot_patches.py:patch_zstd_streamputsif (hos_zstd_stream(c)) return;first inf_80115c80, for the supported build only, counted byreport()like the other hooks (warns unless found exactly once).nx2/build.py:ZSTD_HOST, the host files that get the zstd include path and headers in their dependency key (prompts.c,hos_zstd.c).tests/test_native_zstd.py(new, game-free;zstandardfromRUN_ALL_MODULESmakes the frames): compileshos_zstd.cas it is, with the vendored zstd decoder, over a small fake guest window. It feeds frames with and without checksums, with a dictionary (entropy tables and raw content), empty and one byte long, RLE and raw blocks, whole and in chunks from 7 bytes to 128 KB + 3 and at random, so headers and blocks are split through the staging buffer. Each call's result is checked against the stream object's remaining-byte field and state byte, and the final state against what the game's wrapper leaves. Failures must answer as the game's do: wrong checksum or dictionary, bad magic, unknown content size -1; too little room -2; bytes after the frame -3. Also covered: a dictionary remade in place with the same id; the fallbacks (switch off, misaligned workspace, all 64 entries used); page marks after the last write; the core given up once a call with=1and kept with=2. A broken copy (the staging fill never stored) runs inside the test and must fail.docs/tested-configurations.md: the three rows below.How it was tested
f_80115c80and its callees, run in Unicorn from a local image, againsthos_zstd.con the same inputs. 110 cases (the test's kinds plus larger and multi-frame sets), about 88,000 calls. Results, every stream field, the staging buffer, the dictionary region and the decoded output were identical at every call. Differences were only in the up-to-32 bytes just past the decoded output, which both decoders' fast copies over-write: the game's writes 3-11 bytes past it, libzstd up to 27. For a corrupt block, the partial output's last bytes differ; both report -1. A broken variant fails.python -I -B tests/run_all.pyon the merged tree (Windows, llvm-mingw): 457 passed, 36 skipped, 1 failed. The failure isnx2/test_nup_paths.py, which cannot create its file-link fixture on this PC (no symlink right); this PR does not touch that test or what it covers.hos_zstd.cat the build's flags (-O2 -march=x86-64-v3,ZSTD_DISABLE_ASM) takes 0.99 ms per MiB, against 3.09 ms for the lifted decoder at -O2 (#909) and 3.70 ms at -O1. That is 0.27-0.36 of the lifted -O2 time per frame.HOS_FPS_MAX=240, 20 s window): execacdc864b4239f07(1bde1868, before the merge with main),hookshot_vk.dllbfdac38fbf7d7c23. RX 9070 XT, Vulkan driver 0x8001a0, Ryzen 7 5700X3D, 64 GB, Windows 11, 5120x1440 (32:9). 2 warm-ups, then 4 alternating pairs per arm, same binaries:=1(core given up)=2(core kept)The CPU-time differences (-1.0 and -2.3 ms a frame) are inside the run-to-run range (111.5-117.9 ms). The stub predicts about 2.4 ms a frame (3.54 ms x 0.68), which 4 pairs cannot resolve. Save load: -0.3 and -0.5 s, also inside the range. All of
=1's frames over 100 ms came in its first run, which also had 5 virtual-core waits over 200 ms, against 1-2 in every other run. The control's first run (220.6 ms) and a warm-up (267.2 ms) spiked too, so I don't attribute them. The escapes were measured at main's 2 s escape default (#2127). None had ParallelDecompThread holding a core, so giving the core up buys nothing measurable here.=2is the mode to try.These runs were on a main from before #2602, which fixed a regression on main (this machine: about 41 → 53 fps and 125 → 78 ms of CPU a frame on the standard test), so every arm ran on the slower main. I can rerun the A/B on a main with #2602 if wanted.
Not covered
f_8010b380(whole-buffer decode): not hooked. A follow-up if this one proves out.ZSTD_DISABLE_ASMstays, as for the rest of the build): a follow-up.Related
🤖 Generated with Claude Code
Linux test
I built and ran this on Linux (the part the PR says is not covered).
python3 -I -B tests/test_native_zstd.py-> both cases PASS (zstandard 0.25.0).fce53453with the host file and thebuild.pychange. My shared lift predates the PR, so I appliedpatch_zstd_streamto the generated zstd stream wrapper manually; a fresh lift will do it. (Using the PR's own older base in that lift hit an unrelateda64_stubs/nn_audioduplicate, so the runs below are current main + the PR's host side.)HOS_NATIVE_ZSTDThe hook is active (
zstd: the game's stream decode on the host's libzstd 1.5.7 ...) and no arm crashed or regressed.Caveat: the intro is vsync-capped at 60, so this is not a CPU-time measurement. I did not run the Hyrule Field standard test or measure CPU ms/frame; the 2.4 ms/frame prediction remains untested on Linux. If a Linux perf row is wanted, I can run an uncapped Hyrule Field A/B with process CPU time, but it needs the save/camera automation.
No game files or saves used.
🤖 Generated with opencode using DeepSeek V4.1 Flash.
I merged this into current main and ran tests/test_native_zstd.py (zstandard 0.25.0) and test_patch_hooks_defined.py: both pass, and so does the broken-copy control. I read host/hos_zstd.c. Off,
hos_zstd_streamreturns after a cachedenv_on(line 295), so the default path costs one call per chunk. The stream table only frees an entry on failure, on done, or when a new frame starts. An object the game abandons mid-stream keeps its entry, which is documented, and after 64 of those new streams fall back to the lifted decoder. The DDict is checked against a copy of the game's entropy tables, so one remade in place isn't served stale. CIB's Linux run covers the "not covered: Linux" line.I see no correctness problem. What's left is whether to carry 336 lines of host code and a hook in the lifted f_80115c80 for a gain 4 pairs couldn't resolve. Those runs were also on main before #2602. With #2602 CPU time a frame is about 78 ms instead of 115, so the predicted ~2.4 ms is a bigger share and might show. I'd rerun
=2against off on current main before deciding.Test request T-M4T5: pass
Hardware: NVIDIA GeForce RTX 3080 (10 GB), driver 621.14, AMD Ryzen 9 3900X (12 cores, 24 threads), Windows 11. Built
f0f7dfda(the PR head;v2.1.0-rc.3-260-gf0f7dfda), with the Builder's llvm-mingw; the build log names the hook:oot_patches: the zstd stream decode's host hook in code_000.c.What I checked: the same build, switch off against
HOS_NATIVE_ZSTD=2and=1, alternating (off, =2, =1, off, =2, =1). Each run starts headless from a save through the control bridge withHOS_FPS_MAX=240, teleports to Hyrule Field (Lost Woods bridge), settles 10 s, then holds the right stick fully right (the camera turning at full speed) and samples the status fps once a second for 20 s.Result: no difference beyond run-to-run spread, and nothing broken.
=2(core kept)=1(core given up)No
device lost, crash or exception line in any run.The host decoder was in use in every
=1and=2run, and in none of the off runs:=1reached 3 streams at once; nothing came near the 64-entry tables.This scene is CPU-bound here (about 26 fps against the 44 in your RX 9070 XT / 5700X3D table), so a ~2 ms/frame saving would show as about 1.3 fps, inside the 2 fps spread between the two off runs. Two runs per arm can't resolve it; it confirms no regression on NVIDIA and a Zen 2 CPU, not a gain.
Test request T-M4T5: pass
Hardware: AMD Radeon RX 580 8 GB (RADV Polaris10), Mesa 26.2.6, CachyOS Linux, 8 threads, 15 GB RAM. Built
f0f7dfda(the PR head). One build;HOS_NATIVE_ZSTDunset,=1and=2alternated (3 runs each, the order rotated each round, after one warm-up run).What I checked: start to play from the test save, then Debug teleports Kokiri Forest → Hyrule Field → Market, with a 34 s window in each scene, the camera turning (
HOS_FPS_MAX=240 RX_VK_FRAMEGEN=0 RX_VK_PRECOMPILE=0, headless). CPU time of the process and of the fourParallelDecompThreadthreads, sampled from/proconce a second. Loads are timed from the log: game start to first present and to the first frame of play; each teleport from its request to the first frame of 300+ draws after the loading screen. Frame times fromRX_VK_FRAME_TRACE.zstd: the game's stream decode on the host's libzstd 1.5.7. The binary has no runtime libzstd dependency (lddshows none; the only dynamic zstd symbols are libzstd's two weak trace hooks). The system libzstd here is also 1.5.7, but it isn't used, so a missing or different system library can't affect it.tests/test_native_zstd.pypasses on Linux (gcc, the PR's own fixture, including the fallback cases and the broken-copy control): "the host's zstd stream decode answers, writes and marks as the game's wrapper does"; "the broken copy fails".Results, per run → median:
=1=2=1's Market median came out a little lower in all three runs (37.8 / 34.3 / 36.9 vs 40.2 / 39.2 / 38.9 off), and=2matched off. The decode threads are idle in those windows, so I don't think it's the decode; with 3 runs I can't tell.Correctness:
1,2,3 stream(s) decoding on the host at once. No stream fell back to the lifted code (a stream decodes lifted: 0 in all runs).=1vs off, no camera turn, matched by time after each scene drew: mean absolute difference 3-8 of 255 and 0.4-3% of pixels more than 32 off. That's the same as two runs of one build (wind and animation), with the same mean brightness per scene. No missing or corrupted textures or models in Kokiri, the Field or the Market.=1/=2/ off plus the warm-up and shot runs).HOS_AUDIO=0), so audio streams weren't checked.I merged main (
bd9b07e6) into the branch:6d0a9a5f. The only conflict was the rows in docs/tested-configurations.md, all kept. Main's newer oot_patches hooks don't touch this one's function, and test_native_zstd, test_patch_hooks_defined and the oot_patches/build tests pass.Merging this as opt-in on the four test results: our RX 580 run (decode-thread CPU 2-3x lower during loads, no regression in play), jordanella's RTX 3080, CIB's RTX 4090 on Linux, and Joey's read, which found no correctness problem. Turning it on by default would need an audio check (none of the runs had audio on) and a rerun on current main, as Joey suggested.
Thanks for this, and for the careful fallbacks!