perf(prepare): the segment heap, so the driver's compiles run side by side on Windows #15
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/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/macos
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
ci: approved
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!15
Loading…
Reference in a new issue
No description provided.
Delete branch "madebyjordan/Longshot:perf/prepare-segment-heap"
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?
Summary
On AMD's Windows driver, a first start's shader preparation takes hours, and giving it more CPU threads made it slower, not faster: the driver's compiler threads all wait on one lock in Windows' default memory heap. This makes the game use Windows' segment heap, which has no such lock, so the preparation's first part can use every CPU. Preparation gets about a third faster; play is unchanged.
Before and after
RX 6700 XT, Ryzen 5 5600, 16 GB, Windows 11, AMD 26.10.1. The same build both ways (one exe with the manifest's segment heap line, a copy without it), from cold caches,
--prepare-shaders.Only Windows changes: on Linux, or a Windows that ignores the manifest, the worker count stays as before. Not measured yet: NVIDIA, Intel, Windows 10.
Details
Why
On a Ryzen 5 5600 (6 cores, 12 threads) with an RX 6700 XT, a cold
--prepare-shadersrun of main spent 1,466 s in its first part (prepare_run: every program translated, and AMD's compile of 9,691 compute variants), most of it in the driver. More workers did not help:amdvlk64.dll -> RtlAllocateHeap / RtlFreeHeap -> RtlEnterCriticalSection -> NtWaitForAlertByThreadId: the NT heap's lock.VkAllocationCallbacksdid not help: the driver does not use them for its compiler's memory.What changed
nx2/win_manifest.py: the game's manifest asks for<heapType>SegmentHeap</heapType>(Windows 10 2004 and newer; older Windows ignores it).gpu/vk/prepare_workers.h(new),vk_progcache.cppprepare_cpus():prepare_run's pool before the game ishalf_cpus()as before, unless the process heap really is the segment heap (read from the heap's header, so a Windows that ignored the manifest, or Linux, keeps the old count). Then every usable CPU gets a worker, one more for each 768 MB of free physical memory past 4 GB (each worker past half the CPUs took 0.4-0.5 GB more at the peak). The log says which:vk: preparation: 12 workers for the shader programs (12 CPUs usable; the segment heap: ...).RX_VK_PREPARE_BACKGROUND), Linux, and every other pool.Measured
Windows 11 (26200), Ryzen 5 5600, 16 GB, RX 6700 XT, AMD 26.10.1. Cold runs: the game's caches emptied, and a new folder path each run, which gives the AMD driver an empty cache too (a new path got a new file in
%LOCALAPPDATA%\AMD\VkCacheeach time).--prepare-shaders, no window.Same build (this branch,
v2.1.0-rc.3-299-gdae19300): its own exe against a copy of the same exe whose manifest has theheapTypeline taken out:vk: prepared ... in)Earlier, on main
51da0464with the manifest patched into a copy of the exe: first part 1,466 s (NT heap, 6 workers) against 904 s (segment heap, 12 workers); predicted pipelines in the first 300 s of a fresh process 1,495 (NT heap) against 2,358 (segment heap, after its first part in the same process).In play (main's renderer and exe, the same exe with and without the manifest line; the field bench save: Hyrule Field by day, 2560x1440 output with AMD FSR 4, standing, 150 s each, four alternated pairs): fps median, NT heap / segment heap: 50.8 / 50.1, 51.2 / 50.7, 49.5 / 51.3, 47.3 / 49.2 (pair differences -1.4%, -1.0%, +3.6%, +4.0%: run-to-run noise, no direction). GPU time 13.0-13.6 ms and replay draw time 9.7-10.9 ms move with the fps in both; peak private memory 21.1-21.4 GB in both. (Three earlier runs, both kinds among them, were all at 45 fps while the PC was busy, and are left out.)
Not measured: NVIDIA and Intel on Windows (they get the heap, and on the segment heap the same worker count change), a full end-to-end preparation (hours on this PC either way), Windows 10.
Overlap
prepare_run/predict_run/precompile_run. On the NT heap, more workers ran slower here, so those counts are worth measuring again on the segment heap.See NotRexed/Hookshot#2696, NotRexed/Hookshot#2505.
Those are Hookshot pull requests from before the move; whichever of them come to Longshot overlap the same way.
Tests
Carried over from NotRexed/Hookshot#2895, unchanged apart from the rename: rebased onto Longshot's main
7afe6ae1, with(hookshot)comments now(longshot). The measurements above are from that pull request's Windows builds (on Hookshot maindae19300); the change's code is the same, but it has not been measured again on Longshot's main.tests/test_prepare_workers.py(new): compilesprepare_workers.h: the NT heap's count, one worker per 768 MB past the reserve, the usable CPUs and 32 as limits, the heap signatures; and checksprepare_runand the manifest use them.tests/test_utf8_paths.py: the manifest also hasheapType SegmentHeap; its probe exe still starts from a folder named in Japanese and emoji with the new manifest (UTF-8 code page as before).tests/test_prepare_cpus.py:prepare_runis sized byprepare_cpus, which starts fromhalf_cpusandusable_cpus.tests/run_all.pyon Linux after the rebase (CachyOS, GCC 16.2, Python 3.14): 453 passed, 4 failed, 72 skipped; main7afe6ae1alone fails the same 4 (numpy, zstandard and Tk not installed here, and a GCC 16 internal compiler error intest_lower_tld_ms.py), and alsotest_ship_state_list.pyonce.tests/run_all.pyon Windows (llvm-mingw clang, the Vulkan headers from the builder's CPM cache): 469 passed, 11 failed, 41 skipped; the same 11 fail on maindae19300alone on this PC (builder, portable-archive and readme checks, andtest_program_cache_one_variant.py, which does not compile NotRexed/Hookshot#1630'sdisk_zstdhere).🤖 Generated with Claude Code
HumanBean referenced this pull request2026-11-13 00:12:55 +00:00