fix(vk): release pages ending at a GPU import boundary #469
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 @ 8015c9a
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!469
Loading…
Reference in a new issue
No description provided.
Delete branch "zeldaootr/Longshot:import-exclusive-end"
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: low · Tested on: Steam Deck OLED, SteamOS 3.9.5, RADV; Linux x86-64 (game-free) · Needs: Windows, any GPU
Why
A free that ends exactly where a live GPU import starts keeps the entire range backed: the renderer counts the touching import as an overlap. This happens during the Kokiri Forest to Hyrule Field transition on the Steam Deck (twice per transition in an instrumented run, with 8 KiB and 64 KiB frees).
How to reproduce
Seen on: Steam Deck OLED (RADV), an instrumented RC3 renderer at the Field transition; Linux x86-64, game-free
Settings and switches: defaults
Steps: on
mainaa95d35fwith this PR's test added (d30476db), runpython -I -B tests/test_guest_imported.py --cxx g++: it asks about[0x1000, 0x2000)with an import starting at0x2000Expected: not imported: the ranges only touch
Actual:
Assertion '!memory_guest_imported(0x1000, 0x2000)' failedWhat changed
The ownership check now treats a freed range as half-open, so an import that starts exactly where the free ends no longer keeps the freed pages backed.
memory_guest_imported()ingpu/vk/vk_memory.cppstarts its backwards search atimported.lower_bound(hi)for the half-open range [lo, hi), so an import starting athino longer counts as an overlap; an empty range answers 0 before the pool lock is taken.tests/test_guest_imported.py(new) cuts the function out of the source and checks both touching boundaries and true overlaps.Measurements
Verdict: passes: no change in fps, frame times or memory on a Steam Deck at the Field transition (20.60 against 20.56 fps, inside the run-to-run spread; no fault in 4 runs each): the fix frees too little there to show; the game-free test fails on
mainand passes here.Hardware: Steam Deck OLED, AMD Custom APU 0932 (RADV VANGOGH), Mesa 26.2.0-devel (git-5c65d4fc31), 16 GB shared, SteamOS 3.9.5 (kernel 7.2.7-valve2)
Scene: Hyrule Field (child), arriving from Kokiri Forest by teleport to the Lost Woods bridge entrance, day; 1280×720 output, render resolution option 4, no frame generation, precompile off, 1536 MiB texture budget, 256 MiB mirror budget, headless
Method: one
scripts/dev_packs.pybase at main9c9d69ed(release optimisation, x86-64-v3); main is the base's runtime with no pack, this PR a renderer pack (longshot_vk.soonly; the host binary is byte-identical). One warm-up pass of main made the caches, copied fresh into every run. Each run: child Kokiri save settled 20 s, teleport to HyruleField_Child, 20 s warm-up, 45 s stationary sample. Four rounds in one session, interleaved main, #468, #469, #472; medians of the 4 runs per arm. FPS and present intervals fromHOS_PRESENT_TRACEin the 45 s window; GPU clock and busy from sysfs every 0.5 s; guest memory backing from the guest memfd's allocated blocks (st_blocks).Rebased since: the branch is now on main
aa95d35f, the fix at075e9c24(the same change as bed7d855, which 76382425 carries,git patch-id). Not measured again onaa95d35f. Since9c9d69edmainchangedgpu/vk/vk_memory.cpponly in how the mirror budget is set (#735), not inmemory_guest_imported(); everything else it changed both arms would take alike, so these numbers still compare the change. The game-free test was run again onaa95d35f(below).How to reproduce: build a base at main and a pack of this branch (
python -I scripts/dev_packs.py base|pack|rt, docs/development.md "Testing a change against a base"), start each runtime withHOS_CONTROLand a child save at Kokiri spawn, thenlongshot_ctl.py teleport HyruleField_Child, wait 20 s and sample 45 s.9c9d69edPer run, FPS: main 20.56, 20.54, 20.69, 20.64; this PR 20.59, 20.66, 20.52, 20.38. Guest memory backed at the end: main 5861-5909 MiB, this PR 5832-5900 MiB. Both differences are inside the run-to-run spread: at this scene the fix frees too little to show in the backing or the frame rate. That matches the earlier instrumented runs (below), which saw the boundary hit twice per transition, with 8 KiB and 64 KiB frees: the fix is for correctness, not for a measurable saving here.
Earlier, an instrumented v3.0.0-rc.3 renderer (SHA-256 prefix ce5646666a7e) with a test switch choosing the old or new decision (
RX_TEST_IMPORT_EXCLUSIVE_END, not in this PR), one run each on the same Deck: 20.09 FPS old, 20.33 FPS new, the boundary hit twice per transition (docs/tested/2026-11-14-field-renderer-zeldaootr-import-old.md,-import-fixed.md). On a Linux sparse shared memfd, freeing a 64 MiB range next to a simulated live import left 64 MiB backed with the old predicate and released it with the fix (three densities, three repeats, adjacent and one-page-overlap controls, 36 trials).Flags and settings
None
Testing
Tested: On the Steam Deck OLED above, 4 runs of this PR interleaved with 4 of main through the Kokiri Forest to Hyrule Field transition: Field reached every time (status map
HyruleField_Child, 918-930 presents in each 45 s sample), no device fault, noVK_ERROR, no crash record, the run's low-memory guard never tripped, every run exited 0. In one run of this PR (round 3) the renderer gave back 16 idle imports at its system-heap limit (vk: system heap 2648 of 3072 MB: 16 idle imports given back (352 MB)); main's runs did not log that line, and the run's frame rate was not affected (20.52 FPS). Game-free, on this branch rebased ontomainaa95d35f:tests/test_guest_imported.pyfails at the test-only commitd30476dband passes atafd5ea15, twice each, alternating, and undertests/run_all.pywith g++ 13.3 and with clang 23.1.5 at both commits; the 43 checks that readgpu/vk/vk_memory.cppor the new test pass at the tip, 43 of 43. With #468 and #472, as rebased, the branch merges cleanly (git merge-tree).Not tested: Unique pages reclaimed during gameplay (the backing measured includes everything the game holds); Windows and other GPUs and drivers in the game.
Must be tested:
VK_ERROR, no crash record inlongshot_log.txtand fps like main at the same spot (by hand, test request T-S2R3: post the fps, anyvk: system heaplines and the GPU, driver and OS)Risks
What could break: a free that only touches a live import is now released where it was kept; the existing synchronization between the ownership check and the host discard remains. Review alongside the pending-import lifetime fix, #468.
Turning it off: revert the fix commit
Compatibility: Nothing changes
Issues
None filed. Overlaps #468 (the same empty-range guard; the two merge cleanly) and #431 (the 1 GiB stop two lines below).
Checklist
Everyone
type(scope): description) of the status line's type.main. Onto main94abdd57(head33ca425e) with no conflict, with the commit subjects made Conventional Commits, which CI's lint job now checks for every commit, on a base that has CI's config (#417); each commit's change is the same as before bygit patch-id. The commits named in this description are from before this rebase:d30476dbis now351d28ba,075e9c24is now5adc36c1,7508a3f2is now00e03697,afd5ea15is now33ca425e. Re-run on94abdd57:test_guest_importedfails at the test-only commit351d28baand passes at the head, with g++ 13.3 and clang 23.1.5; of the 46 checks that are this PR's or read its files (g++ 13.3), 46 pass.python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). The full suite on mainaa95d35f(Linux, g++ 13.3, the Vulkan headers set): 528 passed, 15 failed, 50 skipped; on this branch the new test and the 43 checks that read the changed files ran, with no failure thatmaindoes not have (see Testing). The 15 that fail on main:test_appimage_player_build,test_bake_portability,test_cpm_discovery,test_crash_dialog,test_env_on,test_lift_cache,test_lift_cleanup_error,test_make_game_cache,test_mods_size_table,test_options_budget_once,test_pipeline_tour,test_readback_late,test_vk_pgo,test_walk_keyandnx2/test_texture_init_barrier.py.third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.docs/tested/for each new measured run (format indocs/tested-configurations.md).mainand passing here.RX_VK_ONLY=1 RX_RELEASE=1 RX_TARGET=windowswith llvm-mingw 20261104 linkedlongshot_vk.dll, and the same withoutRX_TARGETlinkedlongshot_vk.so; after the rebase,gpu/vk/vk_memory.cppatafd5ea15checked whole (-fsyntax-only -Wall -Wextra) with clang 23.1.5 for Linux and llvm-mingw 20261104 for Windows: the same diagnostics asmainaa95d35f's (54 and 43), none new. The diff has no path handling.a6337d73ed3046b83159Rebased onto main
1be60eae, the run records renamed to the tester handle, and measured on a Steam Deck OLED against main: four interleaved rounds, medians 20.60 fps on main and 20.56 here, guest memory backing the same within spread, no fault. One run here logged 'system heap 2648 of 3072 MB: 16 idle imports given back', which no main run did; it is noted in the description. Still WIP: not built for Windows yet; the test request asks for that.Built for Windows from Linux with llvm-mingw 20261104 (the renderer alone, RX_VK_ONLY, as the CI job does) and for Linux: both link, and the changed file has no new warning under -Wall -Wextra on either. The Windows box is ticked; the Windows test request stays open for a run on real hardware.
WIP: fix(vk): release pages ending at a GPU import boundaryto fix(vk): release pages ending at a GPU import boundaryRebased onto
mainaa95d35fwith no conflict (the same four changes,git patch-id; commits now under the noreply identity) and the description moved to the current template.The game-free test was run again on the new base (fails at the test-only commit, passes at the tip, two rounds, g++ and clang; the 43 checks that read the changed files pass); the Field numbers still hold because
mainchangedvk_memory.cppsince9c9d69edonly in how the mirror budget is set (#735), not inmemory_guest_imported(). T-S2R3 (Windows) still waits for thetesting wantedlabel.3046b83159afd5ea15aaCode review only (nothing run). The change is correct.
importedis keyed bylo, so withupper_bound(hi)the backwards walk started at a pool whoselo == hi, andit->second.hi > lothen counted a pool that only touches the freed range.lower_bound(hi)starts at the last pool withlo < hi, which is the half-open answer, and the 1 GB early-break is unaffected. The empty-range early return is fine. The pools inretired_poolsandevicted_poolsalready use strict<againsthi, so only this loop had the bug.Fitting with #468: the two are complementary, since 468 adds the pending-import sources (all half-open) and this fixes the
importedloop. They touch the same lines, so the second one to merge needs a rebase. Both add the sameif (hi <= lo) return 0;, and this PR rewrites the neighbouringupper_boundline. Each PR's test also builds the same fixture (test_guest_imported.pyhere,test_pending_guest_import.pythere). They are compatible, since the fixtures define the samebg_*anddead_importsshapes, and once both land the touching-boundary cases are covered twice. That is harmless.Approving. The description matches the diff and is honest that the fix shows no measurable change on the Deck. The test fails on main and passes here.
afd5ea15aa33ca425e46Rebased onto
main94abdd57(head33ca425e), with no conflict; the commit subjects are now Conventional Commits, which CircleCI's lint job checks for every commit of a pull request (#417), and the base has the CI's config, which a fork's pull request needs at its own head. No other change: each commit's change is the same as before bygit patch-id.Re-run on
94abdd57:test_guest_importedfails at the test-only commit351d28baand passes at the head, with g++ 13.3 and clang 23.1.5; of the 46 checks that are this PR's or read its files (g++ 13.3), 46 pass. The description names the new commits.New commits pushed, approval review dismissed automatically according to repository settings
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.