fix(vk): samplers past the driver's allocation limit are reused, not lost #442
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
beanstalk: approved @ 1697603
beanstalk: approved @ 2c5a62e
beanstalk: approved @ 4fd82b7
beanstalk: approved @ 64d6d09
beanstalk: approved @ 6f91fae
beanstalk: approved @ 70a78f9
beanstalk: approved @ 8d361dc
beanstalk: approved @ 9d8356e
beanstalk: approved @ abf2227
beanstalk: approved @ d82f202
beanstalk: approved @ e5f1e40
beanstalk: approved @ ee8ab5f
ci: approved
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
regression
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!442
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:fix/sampler-reuse"
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?
Type: fix · Risk: medium · Tested on: macOS 26.7.1, Apple M4 Max (game-free checks with a modeled driver, ThreadSanitizer, arm64 and x86_64 object compiles; no game run) · Needs: Windows or Linux, NVIDIA GPU
Why
A driver makes only so many samplers (
maxSamplerAllocationCount: NVIDIA's about 4000, NVK's 4096), and the renderernever destroyed one. It makes a sampler for each sampler state it meets, and every Anisotropic filtering, Render
resolution or Upscaler change makes a new set on top. Past the limit
vkCreateSamplerfails, and from then on everydraw that needs a new sampler is refused (
docs/sampler-admission.md), so those objects stop being drawn for the restof the run. Whether a session reaches the limit is not measured.
How to reproduce
Seen on: found by reading the code; not seen in a game (whether a session reaches the limit is not measured). The
game-free test shows it on macOS 26.7.1, Apple M4 Max, with a modeled driver of 80 samplers, against main 6c9edffe's
sources. The samplers are the same code on every OS; the limit is the driver's.
Settings and switches: several Anisotropic filtering, Render resolution or Upscaler changes in one run; each makes a
new set of samplers.
Steps: game-free, from this branch's checkout:
python -I -B tests/test_sampler_admission.py --cxx clang++ --headers <Vulkan include folder> --source <a tree of main>. In the game: play on until the samplers made pass the driver'smaxSamplerAllocationCount.Expected: no draw refused: a sampler the game no longer uses is retired and its room reused.
Actual: past the limit
vkCreateSamplerfails and every draw that needs a new sampler is refused for the rest of therun. In the test on main,
limitstops at the driver's 81st sampler, and 15 of 150 cases fail.What changed
Samplers with the same create info now share one sampler, and the renderer keeps the samplers under the driver's limit:
from 3/4 of the room, each new sampler first retires the least recently used one, destroyed once the GPU is past its
last use. Below that point nothing is retired.
sampler_room,sampler_retireandsamplers_reapwithsampler_forand the top ofsampler_make.gpu/vk/vk_draw.cpp, the samplers' section.SamplerBookreplaces thesamplersmap by key. It owns one sampler for eachSamplerInfo: the create infosampler_makefills in, less what no sample can reach (the anisotropy of a sampler without anisotropic filtering,the border colour of one no address mode clamps to its border). Keys with the same info share that sampler. A
sampler that does not compare keeps its comparison in the info, because NVIDIA's driver compares through it anyway
(the
amd-fixnote insampler_make).sampler_room()keeps the samplers undermaxSamplerAllocationCountless 64 (a quarter of a smaller limit), leftfor the renderer's other samplers and an upscaler SDK's. From 3/4 of that room, each new sampler first retires the
least recently resolved one (
sampler_retire).mine(retired_gen); andsampler_for's tables and the descriptors kept by reuse (samplers_changed()).samplers_reapdestroys it oncethe timeline reaches the
vk_retirement_value()taken at its retirement, asvk_fsr3.cppretires its images.watching), their entries last onepresent, so the book sees each sampler in use once a present, and the one retired is the least recently used.
is retired, and the key fails for that present, as a failed creation does (a log line, 8 at most). No sampler is
made past the room, and none is swapped for a default one.
docs/sampler-admission.mdstates the rule: reuse, retirement, recency, the full room, and the tests' count.tests/sampler_admission_test.cpp,tests/test_sampler_admission.py.sampler_roomand the rest) and gives the book's threecontainers tagged throwing allocators.
maxSamplerAllocationCountis 80, and a GPU timeline. Five new cases, each run three ways(the ID table, the factory itself, the cache off):
limit: 300 recordings on two long-lived threads, as a parallel replay's workers. Each recording draws with 10samplers every recording uses and a window of 20 that moves 5 a recording, with the GPU 2 recordings behind:
1,525 samplers in all. None is refused, each is made once, at most 60 are alive, and the modeled
vkDestroySamplerchecks that none goes before the GPU has passed the last recording that drew with it.limit-full: the GPU stopped. After 60 made (15 of them retired), the 61st key waits; nothing is made past theroom and nothing more is retired. Once the GPU passes and a present comes, the 15 are destroyed, they were the
least recently used, and a retired key is made anew.
limit-reuse: keys apart only in what no sample reaches share one sampler. A comparison the sampler is notenabled for, and border colours that are clamped to, keep theirs apart.
limit-alias-commitandlimit-retire-commit: allocation failure in the key table and in noting a retirement.in a recording before the signal the retirement waits for (
vk_retirement_value's comment: workers are joined beforethe next signal).
submitted, so a wait there could not end; the key waits for a later present instead, as main's failed creation does.
Measurements
Verdict: fixed in the game-free test with a modeled driver: on main the 15 limit cases fail (of 150), here all 150
pass, and the threaded cases run clean under ThreadSanitizer (12 runs, 0 warnings). The full game-free suite has no
status change. No real driver's limit measured: no game run.
Commits: main
6c9edffe5d(2026-11-14); this PR fix/sampler-reuse @99ca543a22.Hardware: Apple M4 Max (16 CPUs), macOS 26.7.1, Apple clang 21. Game-free: no GPU or game involved
Scene: none (the proof is the test, as CONTRIBUTING's
fixrow allows)Method:
tests/test_sampler_admission.pyagainst main'sgpu/vk/vk_draw.cppandgpu/render/rx_nvn.h(6c9edffe)and against this PR; its threaded cases built with ThreadSanitizer; the full game-free suite on this PR, compared by
test name with main's on the same Mac
How to reproduce:
python -I -B tests/test_sampler_admission.py --cxx clang++ --headers <Vulkan include folder>(add--source <a tree of main>for main's side)6c9edffe99ca543atests/test_sampler_admission.pylimit,concurrent,shared-thread,thread-retry, 3 ways each)limitis newtests/run_all.py --jobs 6)On main,
limitstops at the driver's 81st sampler: main destroys none, sovalidfails on the null thatsampler_makereturns oncevkCreateSamplerrefuses.Screenshots: none; nothing on screen in the game-free tests.
Flags and settings
None.
Testing
Tested:
tests/test_sampler_admission.pyfails on main (the 15 limit cases) and passes here (150).-fsanitize=thread:limit,concurrent,shared-threadandthread-retryin all three ways, withno warning.
tests/test_sampler_admission.pypasses on both: main's own version there, the extended one here (its main-side failure is the run above).
requests, some are checks main added or changed since
5e072fc8):test_appimage_package_retry,test_appimage_player_build,test_collect_wide,test_control,test_cpm_discovery,test_crash_dialog,test_crash_record,test_env_on,test_game_layout,test_lift_cleanup_error,test_linux_preflight,test_longshot_ctl,test_low_memory_notice,test_make_game_cache,test_mem_gate,test_mem_gate_children,test_mirror_trim,test_oot_state_tap,test_pcache_header_read,test_pipeline_cache_file,test_pipeline_cache_save,test_pipeline_play_cache,test_pipeline_tour,test_prof_symbols,test_program_cache_cleanup,test_program_cache_one_variant,test_readback_late,test_report_run,test_split_giants,test_vk_pgo,test_vram_budget_row,test_walk_key.recorder_sync_test.exe-2026-11-14-180744.ips).
gpu/vk/vk_draw.cppwith the renderer's flags (nx2/build.py), Apple clang 21, arm64 and x86_64.vk_draw.cppfails the same way on both sides, atposix_fallocate(pipeline_cache_file.h; arm64 checked on both).environ), both arches build on main and here.aa95d35f, at60c3c84a(99ca543abefore the rebases ontoe090cc48andaa95d35f):Sweep3's checks of this head (the C/C++ it changes compiled, and its own tests run, on arm64 and x86_64 where they
apply) gave the same results as before;
python -I -B tests/run_all.pyon wave S1's merge11a5b4e7, which carriesthis head: 568/22/17 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac, no new failure
(
reports/2026-11-15-rebase-sweep-3.md).Not tested:
so play there differs from main only in keys sharing a sampler whose create info is the same in everything a sample
reaches.
vk: samplers: N made, the device allows M: the least recently used retired, and a full room logsvk: samplers: ... fill the N this device allows.std::unordered_map,std::erase_if,std::atomic) and Vulkan calls the file already makes.Must be tested:
60c3c84a) and mainaa95d35fthe same way and play a long session with several Render resolution, Upscaler and Anisotropic filteringchanges, expecting on this PR no
vk: samplers: ... fillline and no textures going missing after avk: samplers: ... retiredline, where main logs novk: samplers:line and, once a session passes the limit, stops drawing whatneeds a new sampler (by hand: test request T-YH69).
Risks
What could break:
timeline reaches
vk_retirement_value(), a signal after every recording made so far, parallel segments included.The modeled driver checks that order in every limit case.
sampler_for, two atomic loads insampler_make, and computing aSamplerInfowhen a new key is made.main6c9edffe(2026-11-14) without a conflict; main had changedvk_draw.cpp(#404, #75)away from this PR's lines. No conflict with the other macOS-port PRs rebased on
6c9edffe(git merge-treeof the newheads, 19:49 CST). The macOS port's TexelDesc branch substitutes a default sampler on Apple where creation fails; under
this rule it drops that substitution.
Turning it off: revert the commit. There is no switch.
Compatibility: Nothing changes:
vk_draw.cppis not in the translator stamp (nx2/translator_files.py), and saves,the options file, the shader and pipeline caches and the vk ABI are unchanged.
Checklist
Everyone
type(scope): description) of the status line's type.main: onaa95d35f(#761, 2026-11-15), head60c3c84a, without a conflict; this PR's owncommits, old → new:
99ca543a→60c3c84a; the commit names elsewhere in this description are from before therebase.
python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). On macOS: 531 passed, 17skipped, and 32 failures that also fail on main
6c9edffeon this Mac, named under Testing. After the rebase onto mainaa95d35f(head60c3c84a): wave S1's merge11a5b4e7, 568/22/17 against main aa95d35f's 543/33/17, no new failure.third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.mainand passing here.tests/that reads changed source updated in this PR, not weakened.8342d01a6799ca543a22WIP: fix(vk): samplers past the driver's allocation limit are reused, not lostto fix(vk): samplers past the driver's allocation limit are reused, not lost99ca543a22fabaf7be5ffabaf7be5f60c3c84adcView command line instructions
Checkout
From your project repository, check out a new branch and test the changes.