perf(host): keep the game's threads on the 3D V-Cache cores of two-CCD X3D CPUs #1638
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!1638
Loading…
Reference in a new issue
No description provided.
Delete branch "perf/x3d-cache-cores"
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?
On Ryzen X3D CPUs with two CCDs (7900X3D, 7950X3D, 9900X3D, 9950X3D), busy places such as Castle Town's market run under 60 fps. The game's threads are spread over both halves of the CPU and only one half has the big cache.
What happens now
A 7900X3D, 7950X3D, 9900X3D or 9950X3D has the big L3 on one CCD only (96 MB against 32 MB). Windows moves a game there only when its Game Bar knows the program as a game. It does not know Hookshot, so the game's threads run on both CCDs, and the replay's reserved core (the last core of the fastest kind) is on the CCD without the cache.
The change
Where the CPU's L3s differ in size and the largest is shared by 8 or more logical processors, the game's threads are kept to those cores (
hos_cache_cpus,pick_cache_cores), and no core is kept apart for the replay there.HOS_CACHE_CORES=0: every core, as before.HOS_RESERVE_CORE=1keeps the replay's core all the same.Numbers
Ryzen 9 9950X3D (SMT off), RTX 4070 SUPER, 2560x1440, unattended runs with the same scripted input in every run. Built from
4a7ae1b,HOS_CACHE_CORES=0against the default.Castle Town's market, DLSS Quality, about 2,400 draws, 23-25 two-second windows a run:
Hyrule Field, DLAA, about 2,150 draws (the frame rate is at its 60 cap in both): the replay takes 8.09 ms a frame on every core and 6.61 ms with this.
On v2.0.11's code the same comparison gave 56.9-58.0 fps against 59.8-59.9 over four runs each, and 59.6-59.9 with the process held to the first CCD by hand.
Not measured
SMT on (16 logical processors on the cache CCD), the 12-core parts, and whether the replay would do better on a core of its own when the cache CCD has more than 8.
Test
python -I -B tests/test_reserved_core.py(new cases forpick_cache_cores: 9950X3D with and without SMT, 7900X3D, the cache on the second CCD, a 7950X, one L3 for all, two L3s of the largest size).🤖 Generated with Claude Code
Thanks, the numbers are convincing for the 9950X3D and the detection code itself looks right to me: L3 entries filtered to the calling thread's group, a process mask that doesn't overlap the cache cores falls back to the old behaviour, HOS_PIN / HOS_PIN_BY_CORE still win because they're applied after, and the renderer's own workers are left alone. CI is green and the new cases run through the existing
tests/test_reserved_core.pyentry in run_all.py.The main thing I'd want changed before merging is the trigger condition, because "L3s of different sizes, largest shared by 8+ logical processors" also matches CPUs that aren't X3D (inline). Smaller points:
cache = 0off_WIN32), so Linux on an X3D still reserves the last core, which is on the non-cache CCD. That's fine as a scope, but please say so in the title or description, since right now it reads as if it applies everywhere.HOS_RESERVE_CORE=1on one of these CPUs you get the last fastest core, which is normally on the other CCD, not the cache-CCD core you measured at 9.8-10.0 ms. That's fine if that's what you want, but the comment makes it sound like the measured setup.last_fastest_core/pick_reserved_corearea and also touchestests/test_reserved_core.py, so whichever lands second will need a rebase.This condition also fires on Strix Point. A Ryzen AI 9 HX 370 has 4 Zen 5 cores on a 16 MB L3 and 8 Zen 5c cores on an 8 MB L3. With SMT on, the big L3 is shared by 8 logical processors, so every game thread on a 12-core laptop gets squeezed onto 4 cores, and the reserved core goes away too. The other hybrid layouts with a separate smaller L3 hit the same case.
On the X3D parts the L3s cover the same number of logical processors (one CCD each), and only the size differs. Something like "every L3 shares as many logical processors as the largest" would separate the two cases (or require the cache set to be at least half of all the processors). Could you add an HX 370 case to the test,
{{0xFF, 16*MB}, {0xFFFF00, 8*MB}} == 0?@ -627,0 +673,4 @@const SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX *e = (const SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX *)(buf + at);if (!e->Size) break;if (e->Relationship == RelationCache && e->Cache.Level == 3 && e->Cache.GroupMask.Group == group) {masks[n] = (uint64_t)e->Cache.GroupMask.Mask;Nit: "8 at most" only holds with SMT off. With SMT on, the cache CCD of a 9950X3D is 16 logical processors, which the PR lists as not measured. Maybe say "8 cores" and note that the SMT-on case is untested.
Thanks, this covers what I raised.
Fixed:
pick_cache_coresnow requires every L3 to be shared by the same number of logical processors, so the HX 370 layout (8 vs 16 logical) returns 0. The new tests cover it with SMT on and off, plus the case where the widths match but the largest L3 has only 4 cores.HOS_RESERVE_CORE=1behaviour (the last fastest core is normally on the other CCD) are now spelled out in the comments.I re-read the new Windows code (group filter, the two buffer walks, the fallback when the process mask doesn't overlap the cache cores, the HOS_PIN paths still applied after) and found nothing wrong.
CI: the two red Python source checks (
nx2/test_draw_parameters.py,tests/test_antilag.py, and on Windowstest_wayland_libdecor_hint.py) are in code this PR doesn't touch, and main's own recent runs of that workflow are failing too. The Windows back end build, AppImage and builder jobs pass. Still worth a rebase if #1569 lands first, as it touches the same reserved-core area and test file.