perf(host): three per-call costs on hot paths removed (own-thread lookups, a Signal with no waiter, switches read once) #2708
Open
github-sync
wants to merge 8 commits from
github/pr/2708 into main
pull from: github/pr/2708
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/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!2708
Loading…
Reference in a new issue
No description provided.
Delete branch "github/pr/2708"
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 calls a few nn::os and host paths thousands of times a frame, and each call does work whose answer is already known. Each change below removes that work and keeps its result. They are small, and together they are hard to measure apart (see Measured).
The change
host/hos_thread.cthread_arg). GetThreadPriority, GetThreadId, ChangeThreadPriority and the others scanned every thread underthreads_lock. The game asks about its own thread most of the time (GetThreadPriority(GetCurrentThread()), about 2,400 a frame in Hyrule Field).host/nn_os_sync.c). It returns at once when the condition variable's waiter count is 0, without the lock pair and wake call.HOS_CV_SIGNAL_ALWAYS=1: as before.cv_enter, a seq-cst add under the condition variable's lock) before it gives the guest mutex back (mutex_release_all). A signaller that holds the mutex therefore sees the waiter, and the waiter cannot be skipped.menu_vsync(RX_VK_FIFO/RX_VK_IMMEDIATE, twice a present) andHOS_FS_OVERLAY(every OpenFile). They hold for a whole run. Each is now read once and published atomically (HOS_FS_OVERLAYas one atomic pointer).RX_VK_DUMP_STAGESstays a per-readgetenv:vk_shader.cppis part of the shader-cache stamp, so changing it would make every player's prepared shaders stale.RX_VK_TAAU_KEYSstays a per-dispatch read:tests/test_taau_env_lookup.pypins that.Measured
Windows 11, Ryzen 7 5700X3D (16 logical processors), RX 9070 XT, AMD driver 26.10.2. Standard Hyrule Field test: right stick held right from the load,
HOS_FPS_MAX=240, 2560x720, 20 s window, warm caches, 6 alternating pairs a cell, base main46780503(with #2602).These changes were measured only together with #2709 (both over the base; 8 and 6 threads: the game held to
0xFF/0x3F):46780503(standard test, n=1) counted ~2,800 SignalConditionVariable calls a frame in the play window, none of them with a waiter: the case this early return skips. (During the load, 170-304 per 5 s found a waiter.) This counts the calls; their time is not measured separately.57ed3aa8(before #2602's fix of a main regression, 41 → 53 fps on this test) are replaced by these.Tested
tests/test_thread_arg_self.py(new):thread_argfrom the source, over own and other ThreadTypes. Broken variants must fail: every call scanning, the caller's record returned for any ThreadType.tests/test_cv_signal_early.py(new): Signal over waiter counts, with and withoutHOS_CV_SIGNAL_ALWAYS. Broken variants must fail: the early return taken even withHOS_CV_SIGNAL_ALWAYS=1, the early return reading the wrong count, a waiter that does not count itself in. A structural check:cv_entercomes beforemutex_release_allin both waits.tests/test_vsync_row.py: one process per switch setting now (the value is read once), with a negative control.tests/run_all.py; syntax compile of every changedhost/*.c; a gpt-6.1-sol review (three rounds, last clean).Not tested
🤖 Generated with Claude Code
I merged this into current main (
51da0464) and ran tests/run_all.py: everything passes except test_lift_cache.py, which fails on main here too. The new test_thread_arg_self.py and test_cv_signal_early.py pass, and so does the reworked test_vsync_row.py.I checked the Signal early return closely, since a missed wake would hang a thread. cv_enter counts the waiter in under cv->lk before mutex_release_all, and the gen snapshot is taken there too. So a signaller that skips at host/nn_os_sync.c:605 is ordered before the waiter's entry, and the old code would have missed that waiter in the same way. thread_arg's shortcut returns the same record hos_thread_of would. The once-read switches are run-wide.
Two notes. The Measured section still says provisional, on
57ed3aa8and only together with #2709; please put in the re-measure on current main when you have it. #2765 adds BroadcastConditionVariable right below Signal. It merges cleanly with this, but it could take the same no-waiter return later.Hardware: RTX 4090 24 GB, NVIDIA 615.78.08, i9-13900K, Arch Linux 7.2.15-1-omarchy.
Built
8ee32e5c6139e6fd0d1dac68f3c16196cd507a60, on branch basec544828ff2d545829505cd894bb7968c7c2c5fcb.The head is now
f0943875: the same six commits (identical patches) plus two merges of main, the first of which brought #2709's neighbouring idle-thread and short-sleep changes. I did not run that combination.Mixed Linux result: no lasting gameplay hang in these checks; CPU savings remain unmeasured. I compared
HOS_CV_SIGNAL_ALWAYS=1with unset on one binary. Windowless, muted, requested 2048x1152, game's own upscaler at 100%, frame generation off, 60 cap. These were ordinary shared runs, not quiet speed measurements.Both modes loaded copied saves, used the game pause menu, transferred from the forest to the field entrance, and wrote a new progress save. I captured the old-mode Insert menu and the new-mode saving indicator. Both dungeon runs used the blue warp, played the story movie through its decoder stop, and continued the later dialogue.
All reached their timers. The gaps resumed; they are stall observations under unequal load/cache conditions, not a timing win. Both story runs logged the same background mutex wait of 10 seconds. The movie decoded 7040 pictures in each mode, but skipped 174 with old Signal and 1740 with new Signal to catch up: the movie finished, but I cannot call playback equally smooth or attribute that difference from this shared pair.
Removed work: a local logging-only diff counted calls and actual early returns. I can share it: 27 added lines and one removed line; the original return condition is unchanged. Counts were enabled only for the correctness runs.
Those returns avoid the lock pair and wake call. The corridor is not the heavy open-field view.
CPU comparison: one old/new/old/new set loaded the same open-field save, with call counting off and the game's own processor counters recorded. The loads were:
Not comparable under this load. No CPU difference or FPS claim, and no repeat to chase quieter conditions.
Across all eight starts: no fatal error, crash, device loss or kernel driver fault. Two startup virtual-core escapes occurred, both with old Signal. One known NVIDIA mapping-refusal line overlapped the new story run; no Xid or unknown driver signature.
Checks: all three requested tests pass with GCC 16.2.1 and Clang 23.1.3. Against main
46780503, the own-thread and V-Sync checks fail at their behavioural assertions; the Signal check stops at its absent new extraction anchor, not an executed lost-wake test. Five Windows-target fixture units pass syntax checking, not Windows execution. No full-suite rerun.Pictures inspected: labelled forest/pause/save panels, both field-entrance views, paired dungeon/movie/later-dialogue panels, and all four open-field positions. The new tour's sparse captures missed the Insert overlay itself. Stills do not prove absence of flicker.
Not tested: the merged-main combination above, Windows execution, Intel/AMD Linux, real-window V-Sync, a non-empty filesystem overlay, audible audio continuity, a long soak, or reloading the newly written entrance save. This isolates the Signal switch, not the total CPU saving of all three changes.