Vulkan: the synchronous replay on the reserved core while it replays, and that core the fastest kind (follow-up to #1034) #1158
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!1158
Loading…
Reference in a new issue
No description provided.
Delete branch "replay-pin"
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?
Follow-up to #1034, after its review (thanks @hckrman101). #1034 pins the "Presentation Thread", by name, to the reserved core for everything it does. That has four problems, all raised in the review: host code names one game's thread, the other queues' submits are left out,
RX_VK_ASYNCis parsed again by hand, and on Intel's hybrid CPUs the reserved core is an E-core.Now the Vulkan back end moves whichever thread is replaying onto the reserved core for as long as it replays, then puts it back on its own processors. That covers
run_submit, plus the synchronous sections ofsubmit_qandpresent, which take the submission's capture. No thread is picked by name, nothing changes withRX_VK_ASYNC=1(the back end's ownasync_on()decides), and a non-Vulkan back end is untouched. The Presentation Thread's pin (HOS_PIN_RENDER) goes.RX_VK_REPLAY_PIN=0turns the new pin off, and the log says once which processors it uses.Windows only. On Linux a new thread inherits the affinity of the thread that creates it. Several of the back end's threads start on first use inside a replay: the capture's copy helpers, the shader workers, the pool imports, and others. They would all have stayed on the reserved core. On Linux nothing is pinned in synchronous mode, as before #1034. That also removes #1034's permanent pin on Linux, which had the same problem.
hos_reserved_cpus()now picks the last core of the fastest kind, usingGetLogicalProcessorInformationEx'sEfficiencyClass:RX_VK_ASYNC=1, whose replay thread is pinned to the same core.tests/test_reserved_core.pychecks the choice for uniform CPUs (including under 8 logical processors), the 12700K and 13900K, E-cores listed first, Lunar Lake, and the 2-P-core laptop CPUs. It's listed inrun_all.py's table (g++).Measured by others on Intel hybrid CPUs (thanks!)
EfficiencyClasschoice took Kokiri Forest from 48.7 fps (reserved core on an E-core) to 60 fps vsync-capped, with 0% of frames over 30 ms.Measured here
Ryzen 9 3900X (12 cores / 24 threads) with an RTX 3080 on Windows 11, on main
8b2e1d3(v2.0.8-rc.1, before #1034 was merged). #1034's pin is measured there asHOS_PIN=Presentation=C00000, which is what it sets. Kokiri Forest after loading a save, headless, input timed in game frames (HOS_INPUT_FRAME_CLOCK=1): standing still, and panning the camera (right stick held). Live numbers over presents 3120-5520, two runs each, in two rounds (the second in reverse order). Stutter is fromRX_VK_SLOW_LOG=35.Over #1034, the average frame rate is a little higher standing (+1.3%) and even while panning. Slow frames go down: fewer frames over 50 ms, and a 1% low of 16.3 fps against 14.8. Only preferring the core, as the review suggested to keep other processors available, gains nothing at all, so that mode isn't kept.
Not measured: Linux, and hybrid CPUs with few P-cores, where no core is now reserved. On the 3900X the reserved core is unchanged (
0xc00000).Merged with main
ac3bd44, the build compiles and a run in game logs the pin on the same core.python -I -B tests/run_all.py: the same failures as main itself on this machine (missing tools, and an older g++ that rejectsvolatilecompound assignment). Everything else passes, includingtest_reserved_core.py.🤖 Generated with Claude Code
Thanks for following up on the #1034 comments, this is a much tighter version.
What checks out (reading it against Windows):
ReplayPinasksasync_on()and is a no-op in async mode.HOS_PIN_RENDERare gone, andhos_thread_pinis back to only keeping threads off the reserved core.host->reserved_cpushook, so noMkwVkHostABI change and the non-Vulkan back ends are untouched.sync_order_mu/replay_muso at most one thread is pinned at a time. ~10SetThreadAffinityMaskpairs a frame is negligible.GetLogicalProcessorInformationExwalk (size query,e->Sizewith the zero guard, fallback to the old call, all-class-0 falling back to the old pick, SMT siblings kept together) and the CPU table in the test look right.The main gaps are on Linux (1 and 2 inline); 3 and 4 are about hybrid CPUs with few P-cores and >64-CPU machines. The hybrid and 8-thread cases are still unmeasured, so it'd be good if someone with such a CPU could give it a try.
Small nit:
tests/test_reserved_core.pydefaults to--cxx g++but isn't added to the table of tests with tool needs intests/run_all.py, so it fails instead of skipping on a machine without g++. It also only coverspick_reserved_core, not the record walk or the 8-processor threshold.@ -1807,0 +1830,4 @@#endif}thread_local int replay_pin_depth = 0;thread_local DWORD_PTR replay_pin_prev = 0;On Linux a thread created with
pthread_create/std::thread(and the shim'sCreateThread) inherits its creator's affinity (Windows gives new threads the process mask, so it's fine there). Several back-end threads start lazily inside pinned sections:sync_watchviacall_onceinsidesubmit_q's pin (~3486)copy_pages, 2 by default,vk_memory.cpp~1293, frommemory_capture_submit()insubmit_q)vk_jobs.cpp:66, on first push) and thejobs_push_serialbuilderbg_mainfor pool importsdisplay_main(vk_framegen.cpp:425, fromdo_present)RX_VK_PARALLEL,par_worker_main(~1379 inrun_submit)None of them resets its affinity, so on Linux they'd all live on the reserved core, competing with the replay it's kept for (the parallel capture copy becomes serial on one core, shader compiles run there). Partly pre-existing (#1034's permanent pin, the async replay thread), but now it applies to whichever thread replays first. Maybe have those threads set their affinity to the process mask minus the reserved core at start, spawn them outside the pin, or have the posix thread-start path clear the pin?
@ -558,12 +558,73 @@ static const NameSpec *match_name_spec(const NameSpec *s, int n, const char *nam/* The last physical core (its logical processors' mask), kept for the Vulkan back end's replay thread (gpu/vkThe 8-logical-processor threshold now counts E-cores, so on hybrids with few P-cores one of very few P-cores gets reserved: i3-1215U (2P+4E = 8 logical) and i5-1235U/1245U (2P+8E = 12) reserve 1 of 2 P-cores, Lunar Lake (4P+4E) 1 of 4. On the 2P parts the game's other threads are left with 1 P-core plus E-cores (before, they only lost one E-core). That may be the right trade for the replay, but it's unmeasured. What about requiring a minimum number of fast-class cores (say 4+) before reserving one, and otherwise no reservation or the old pick?
Edge case: this takes group 0's cores only, while the old
GetLogicalProcessorInformationreturned the calling thread's group.SetThreadAffinityMask/GetProcessAffinityMaskmasks are relative to the thread's/process's own group, and on Windows 11 / Server 2022 the primary group can be 1+. With asymmetric groups (Threadripper/Xeon) the reservation can land on the wrong kind of core or past the end of that group, and the threshold counts only group 0. Filtering by the current group (GetThreadGroupAffinity) instead ofGroup == 0would keep the old behaviour.@ -567,2 +626,4 @@}#endifSYSTEM_LOGICAL_PROCESSOR_INFORMATION info[256];DWORD len = sizeof info;On Linux this still takes the old path: the EfficiencyClass logic is
#if defined(_WIN32)only, sohos_reserved_cpusgoes through the posix shim'sGetLogicalProcessorInformation(host/posix/win32_posix.c~577), which builds masks from sysfscore_idwithout core types and picks the highest mask. On a hybrid Intel CPU under Linux (e.g. a 12700K, CPUs 16-19) the reserved core is still an E-core, and with this PR every synchronous replay, the capture and the present run there, which is wider than #1034 (that pinned only the Presentation Thread).The description says "the fastest kind" without mentioning it's Windows-only. Could it pick by
/sys/devices/system/cpu/cpuN/cpu_capacity(or thecpu_core/cpu_atomPMUcpuslists, orcpuinfo_max_freq), or skip the replay pin on Linux when core types differ?Data point: on an i7-14700KF the reserved core is an E-core
i7-14700KF (P-cores = logical processors 0-15, E-cores = 16-27), RTX 3070, Windows 11, v2.0.8. The log says
threads: Presentation Thread on the reserved core (logical processors 0x8000000; HOS_PIN_RENDER=0: off), so logical processor 27, an E-core.Kokiri Forest, standing then walking, frame generation 2x, ABBA order (pinned,
HOS_PIN_RENDER=0,HOS_PIN_RENDER=0, pinned):HOS_PIN_RENDER=0The mean cost is small (about 1 fps standing, 2.4 walking), but the pinned runs vary much more: an earlier ABBA also had pinned v2.0.8 at 40.5 / 39.8 standing. Pinning to a core of the fastest kind, as this PR does, looks right for Intel's hybrid CPUs.
can confirm this and applied this fix( well ai came up with it I was about to commit this then it found this pr)
Data point: i7-13700K, the fastest-kind choice gets the forest to 60 fps
i7-13700K (P-cores = logical processors 0-15, E-cores = 16-23), Windows 11, on main
85dfa27. SameEfficiencyClassselection as this PR inhos_reserved_cpus()(Presentation Thread pin kept). Kokiri Forest after loading a save, 1600×900 window, frame generation off, 100% resolution,HOS_INPUT_FRAME_CLOCK=1, same spot (~2200 draws a frame):85dfa2785dfa27as is0x800000(LP 23, E-core)85dfa27,HOS_PIN_RENDER=085dfa27+ fastest-kind core0xc000(LP 14-15, P-core)On the E-core the replay took ~17-18 ms a frame while the GPU took ~6.5 ms, so the frame rate was CPU-bound on that one core. With the core picked by
EfficiencyClassit's vsync-capped at 60.Tested on Windows 11, Ryzen (Zen 3, no hybrid cores) + RTX 3070, merged onto v2.0.10 (clean merge). Scripted Castle Town walk: load the save at the town gate, walk into the market for ~30 s. Frame times from
RX_VK_FRAME_LOG, two runs each, alternating:No change in the average or median on this CPU. That is expected without E-cores. The fewer long frames are within run-to-run noise here. No hang, no device lost, the picture matches.
For context, in this scene the replay takes ~14 ms a frame and the GPU ~11 ms (the log's own lines), while frames take ~26 ms. So the town is held back by the game's own threads, not by the replay thread's placement.
@ -567,2 +626,4 @@}#endifSYSTEM_LOGICAL_PROCESSOR_INFORMATION info[256];DWORD len = sizeof info;Right, the
EfficiencyClasschoice is Windows-only, and the description didn't say so. Given your next comment,ffeaed6makes the replay pin itself Windows-only (replay_pin_maskis 0 elsewhere). On Linux nothing is pinned in synchronous mode, as before #1034, so the replay never lands on an E-core there. The Linux pick still matters forRX_VK_ASYNC=1, unchanged from main. Choosing bycpu_capacityor thecpu_corePMU list looks like the right way to do it on Linux, but I can't run it on a Linux hybrid machine, so I'd rather leave it for a follow-up someone there can test. The description now says Windows-only.@ -1807,0 +1830,4 @@#endif}thread_local int replay_pin_depth = 0;thread_local DWORD_PTR replay_pin_prev = 0;Good catch, and it's why the pin is Windows-only now (
ffeaed6). Windows gives a new thread the process mask, so threads started inside a pinned replay are fine there. On Linux every one of those lazily started threads would have kept the reserved core. This also takes away #1034's permanent Presentation Thread pin on Linux, which had the same inheritance problem for everything that thread starts. Resetting the affinity at the start of each back-end thread would make a Linux version possible, again best done by someone who can run it there.@ -558,12 +558,73 @@ static const NameSpec *match_name_spec(const NameSpec *s, int n, const char *nam/* The last physical core (its logical processors' mask), kept for the Vulkan back end's replay thread (gpu/vkDone in
ffeaed6:pick_reserved_corenow keeps a core only with 4+ cores of the fastest kind (besides the 8+ logical processors), and otherwise none. I chose no reservation over the old pick because the two hybrid data points on this PR show the replay pinned to an E-core is slower than not pinned at all. Lunar Lake keeps 1 of 4, per your 4. CPUs of one kind are unchanged, since 8+ logical processors always means 4+ cores.test_reserved_core.pynow covers the i5-1235U, i3-1215U and Lunar Lake layouts, a 6-core CPU without SMT, and a 4-core one with SMT.@ -558,12 +558,73 @@ static const NameSpec *match_name_spec(const NameSpec *s, int n, const char *nam/* The last physical core (its logical processors' mask), kept for the Vulkan back end's replay thread (gpu/vkDone in
ffeaed6: the walk keeps the cores of the calling thread's group (GetThreadGroupAffinity), so the masks match what the affinity calls expect.Thanks for the careful review; all four points and the nit are in
ffeaed6, with details inline. In short: the replay pin is Windows-only, a core is kept only with 4+ of the fastest kind, the cores are the calling thread's processor group's, andtest_reserved_core.pyis inrun_all.py's table and covers the thresholds. The record walk itself still isn't covered, since it's Windows API calls.And thanks @blushOFF, @Brett-D and @Nitnek for testing. The 13700K going from 48.7 to a vsync-capped 60 fps with the fastest-kind core, and the 14700KF numbers for the E-core pin, are exactly the hybrid data this PR was missing. They're credited in the updated description. Merged with main
ac3bd44, it builds, and a run here pins the same core as before on the 3900X.All four earlier points and the test nit are fixed in
ffeaed6:tests/test_reserved_core.pyis in run_all.py.ReplayPinreads correctly: it does nothing in async mode, only the outermost pin applies, and it restores only if the pin worked. Two nits, no action needed:replay_pin_mask()isn't intersected with the process affinity mask, and the reserved core is chosen from the first caller's processor group. The 13700K/14700KF data backs the E-core choice. CI is green. LGTM.