WIP: perf(host): the game's whole-buffer zstd decode on the host's libzstd (HOS_NATIVE_ZSTD_FRAMES, opt-in) #155
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
good first issue
gpu/amd
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!155
Loading…
Reference in a new issue
No description provided.
Delete branch "klimovich008/Longshot:perf/native-zstd-frames"
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?
Why
Area loads and streaming spend CPU in the game's own copy of zstd. NotRexed/Hookshot#2738 moved its stream decode (ParallelDecompThread) to the host's libzstd. The game's other zstd path, the resource loaders' whole-buffer decode
f_8010b380on the ARes_Loading threads, is about 22% of a core in Kokiri Forest and 6% in the standard test (CPU report). This PR moves that decode to libzstd too, behind a switch that stays off by default: in game the difference is too small for 5 pairs (Kokiri Forest) or 6 (the standard test) to resolve.This is a performance change, not a bug fix. It also fixes a first-call data race (C memory model; no wrong output seen) in #2738's
HOS_NATIVE_ZSTDswitch, found in review.What changed
hos_zstd_framesinhost/hos_zstd.c, run first inf_8010b380(zstd'sZSTD_decompressMultiFrame, inlined: x0 the game's context, x1/x2 the output and its room, x3/x4 the frames, x5/x6 a raw dictionary, x7 the game's DDict or 0). It returns what the game's function returns: the decoded size, or a zstd error code the three callers map throughZSTD_getErrorCode, as before.ZSTD_decompress_usingDDict(the game's dictionary by reference, through the stream decode's DDict cache, checked against a copy of the game's tables) orZSTD_decompress_usingDict(NULL, 0); libzstd contexts come from the stream decode's spare list.HOS_WRITTENafter the decode (libzstd may keep literals in the room's far end while it decodes).zs_mode(HOS_NATIVE_ZSTD) and the newzf_modecached their switch in a plain static that several decode threads read and wrote, and the once-logged fallback table was unsynchronized.zs_publishnow publishes each switch once with a compare-and-swap, and only the thread that published it logs it.zs_noteclaims its table entry under the existingzs_lockand logs after releasing it. Only first calls and logging change; the decode path does not.patch_zstd_framesinnx2/oot_patches.pyputsif (hos_zstd_frames(c)) return;first inf_8010b380, counted byreport()like the stream hook (it warns unless found exactly once).tests/test_native_zstd.py, game-free:env_onholds each caller that reads a switch unset until two have, so both go on to publish it; and a stand-in log holds the first thread in its first log line while the second runs start to end;docs/development.md's switch reference (#2738 had not addedHOS_NATIVE_ZSTD); five rows indocs/tested-configurations.md.Flags and settings
HOS_NATIVE_ZSTD_FRAMES(set)HOS_NATIVE_ZSTD(from #2738) is unchanged apart from the race fix.Measurements
main: not measured on Longshot yet. This PR stays WIP until it is, on a Longshot base.
this PR: perf/native-zstd-frames @
a0ff2d94, not yet measured itself; the numbers below are its Hookshot form d0b8141f, before the rename and before the race fix (which changes only first-call setup and logging; the decode path is the same)Hardware: RX 9070 XT, AMD 26.10.2 (Vulkan driver 0x8001a0), Ryzen 7 5700X3D (no efficiency cores), 64 GB, Windows 11
Scene: Kokiri Forest (the default save, standing) and the standard test (Hyrule Field, the camera turning at full speed); 5120x1440 (32:9, the standard test's aspect until 2026-11-13), no upscaler, no frame generation, frame rate unlocked (
HOS_FPS_MAX=240)Method: same binary, the switch off and on, alternating runs, 20 s windows after the save loads; fps and frame times from the present trace, CPU from the process. A run counts when every game-clock reading whose interval lies wholly inside the window is within 1.000 ± 0.01 and the window has no frame over 250 ms; a pair with a run that fails is left out whole. Medians, with the mean of paired CPU differences.
How to reproduce: build this branch, then for each run set
HOS_FPS_MAX=240andHOS_FPS_LOG=1, load the save and stand (Kokiri) or hold the right stick fully right (Hyrule); alternateHOS_NATIVE_ZSTD_FRAMESunset and=1.Baseline: this PR @ d0b8141f (Hookshot),
HOS_NATIVE_ZSTD_FRAMESunset: the build's own switch-off runs, which run main's code path. Main was not the comparison because the switch is off by default, so off and on one binary isolates the change; a Longshot main @ / this PR @ comparison comes with the measurement on a Longshot base. The A/B is by switch on one binary, so the two sides differ only in this decode.Build: d0b8141f, Hookshot v2.1.0-rc.3-250-g46780503 with #2738 and this change before the race fix (exe
a46bf3a229310166,hookshot_vk.dll418aa37c49bc96ce).Kokiri Forest, 3 warm-ups, then 6 runs an arm: 5 of 6 pairs count.
HOS_NATIVE_ZSTD_FRAMESunsetHOS_NATIVE_ZSTD_FRAMES=1HOS_NATIVE_ZSTD_FRAMES=1,HOS_NATIVE_ZSTD=2=1run had frames of 276 and 253 ms in the window and a clock reading of 0.933 over that stretch. The same pair's control run had a 131.6 ms frame, and the warm-ups, which run with the switch off, had frames of 295 and 345 ms. With pair 1 included, the paired CPU differences are -1.66 and -2.09 ms.The standard test, 2 warm-ups, then 6 pairs: all 6 count.
HOS_NATIVE_ZSTD_FRAMESunsetHOS_NATIVE_ZSTD_FRAMES=1HOS_NATIVE_ZSTD=1while moving (context for #2738)A tester saw
HOS_NATIVE_ZSTD=1slower than unset in Kokiri Forest while moving (RTX 5090, i7-13700K: 72.2 against 66.7 fps). I checked it on this PC with #2738 as merged (before the race fix) and this change switched off (exee9be69d36122b25e): Kokiri Forest from the default save, running with the camera turning, 2 warm-ups, then 6 triples. Two triples are left out by the rules above: an unset run with frames of 576, 284 and 135 ms, and an=2run with a window clock reading of 0.990. On the other 4:HOS_NATIVE_ZSTDunsetHOS_NATIVE_ZSTD=1HOS_NATIVE_ZSTD=2=1was faster in all 4 of those pairs; in one more pair (valid for unset against=1only) it was slower, 51.0 against 53.2 fps.=1makes no more virtual-core hand-offs than unset. ParallelDecompThread went from about 2% of a core to about 0.=1changes.Testing
Tested:
f_8010b380called through one of its callers in Unicorn from a local image, againsthos_zstd_frameson the same inputs: 46 cases, 49 calls (the test's kinds plus larger sets up to 2 MB, errors, a DDict made again). Results, including error codes, and the decoded output were identical. In 2 of the cases with 1000 bytes more room than the content, the bytes in the room past the content differ (libzstd keeps literals there while it decodes). A broken copy fails 8 cases.python -I -B tests/run_all.pyon this branch at ca40bb33, on main39647e1a(Windows, llvm-mingw,LONGSHOT_MINGWset; since then rebased onto6b7fa356, whose #148 touches only the README, the release script and its test): 496 passed, 38 skipped, 4 failed. The failures arenx2/test_nup_paths.py,tests/test_check_third_party.py,tests/test_setup_runtime.pyandtests/test_release.py, which fail onmainon this PC too; this PR does not touch what they cover.tests/test_native_zstd.pypasses with llvm-mingw and with MinGW gcc 16.2, and every broken copy in it fails.host/hos_zstd.ccompiles as a whole (clang -fsyntax-onlyagainst a build's generated headers).Not tested:
46780503builds. To be measured on a Longshot base before this leaves WIP.Needs other testers:
HOS_NATIVE_ZSTD_FRAMESunset against=1, expecting fps and frame times unchanged or better (request to open)HOS_NATIVE_ZSTD=2against unset, expecting no slowdown (request to open)Screenshots
Nothing on screen.
Risk and rollback
A wrong decode would show as missing or broken models, textures or effects after an area load, or a load that fails. It is off unless
HOS_NATIVE_ZSTD_FRAMESis set; unset, the hook returns at its first check and the game's own decode runs. The race fix changes only first-call setup and logging; reverting its commit restores the old first-call code.Reviewer notes
Start with
hos_zstd_framesand the race fix at the top ofhost/hos_zstd.c, then the whole-buffer and concurrent cases intests/test_native_zstd.py. Prior art: zortos293/Lichii#869 puts this game's zstd decode on the host's libzstd in an emulator fork. I did not read its code; it's credited as related work. Also related: NotRexed/Hookshot#909 (the lifted decoder at -O2).Issues
See NotRexed/Hookshot#2738 (its first-call race is fixed here).
Checklist
For everyone
type(scope): description).main.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above).third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
Assisted-by:/Co-Authored-By:trailers.docs/tested-configurations.mdfor each measured run.mainand passing here.tests/that reads changed source updated in this PR, not weakened.🤖 Generated with Claude Code
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.