WIP: fix(macos): Rosetta host imports at whole hardware pages, and a refused import asked again only when its backing changes #686
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!686
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/rosetta-import"
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 (MoltenVK 1.4.2), game-free at this head on arm64 and x86_64 (Rosetta); GPU probe RI-01 on both routes (history) · Needs: Windows, Linux, macOS x86_64 game run
Why
On a Mac running the x86_64 build under Rosetta, the renderer left about 1,640 draws a frame out once the first scene loaded
(start #29): host-memory imports of guest memory failed (
[mvk-error] … VK_ERROR_OUT_OF_DEVICE_MEMORY … Could not allocate a host-coherent or exportable VkDeviceMemory; the kernel refused the client buffers 6,665 times,IOGPUSysMemory::memoryDescriptorWithClientBuffer … failed). Under Rosetta the process's page is 4 KiB but the GPU maps thehardware's 16 KiB pages. A guest free replaced the freed range's 4 KiB pages with fresh memory, which left 16 KiB pages split
between two memory objects, and IOGPU refuses any import over such a page. Every refused range was then asked of the driver
again every 2 s, and every draw over it was counted and logged as an import (about 22 million asks, 86,833 log lines filling
the runner's 16 MiB log). arm64 never meets it: there both pages are 16 KiB. This PR keeps frees and imports at the hardware's
page and asks a refused import again only when its backing changes. It is part of the macOS port. See #420.
How to reproduce
Seen on: macOS 26.7.1, Apple M4 Max, MoltenVK 1.4.2, the x86_64 build under Rosetta (start #29); not on arm64
Settings and switches: defaults
Steps: start the x86_64 build, load a save and play into the first scene load; or, game-free, the probe RI-01 (reserve as
darwin_mem_mapdoes, replace 4 KiB inside a 16 KiB page asdarwin_mem_discarddid, import the chunk underarch -x86_64),or
python -I -B tests/test_darwin_guest_memory.py --cc clang --arch x86_64with this PR's test on the baseExpected: every import of guest memory succeeds, and no draw is left out
Actual: on the base, under Rosetta, the import of any chunk holding a split page fails with
VK_ERROR_OUT_OF_DEVICE_MEMORYat
vkAllocateMemoryand stays refused (RI-01: refused 3 of 3 when asked again unchanged); the game leaves the draws over itout, about 1,640 a frame (
vk: skips a frame by vk_draw.cpp line)What changed
macOS only; Windows and Linux keep their code paths (their compiled text changes only in comments, an unused type and an
empty report function, see Reviewer notes).
host/posix/darwin_mem.{c,h}:darwin_mem_hardware_page()= the larger ofgetpagesize()andvm_kernel_page_size;darwin_mem_whole_pages().darwin_mem_discardreplaces only the whole hardware pages inside a freeand zeroes the rest in place: the same bytes (zero), and no 16 KiB page ever split between memory objects. Natively the
hardware page is the process's, so nothing changes on arm64.
host/posix/darwin_guest_mem.c: the guest layer's native page is the hardware's (hos_posix_guest_reserve), so guestprotections, watches and pins round to 16 KiB under Rosetta as on arm64 (RI-01: a 16 KiB page with one 4 KiB part read-only
is refused for import too). New
hos_posix_guest_backing_generation(lo, hi): a counter per 2 MiB cell of the guest windowthat a discard's remap or a protection change moves.
gpu/vk/vk_api.h,host/rx_backend.c,host/hos.h: the backing generation is a new host callback,guest_backing_generation, appended after #491's import pins: Apple host API version 25 (#491's pins are 24), offset392, size 400.
gpu/vk/darwin_import.h,gpu/vk/vk_memory.cpp:memory_initaligns imports to the hardware page too(
darwin_import::alignment(minImportedHostPointerAlignment, page, vm_kernel_page_size): 16 KiB under Rosetta, as onarm64), and the start line says so when the process's page is smaller.
gpu/vk/import_hold.h,vk_memory.cpp: on macOS a refused import is remembered inImportRefusalswith itsVkResult and the call that refused it (
import_step), and asked of the driver again only when the host's backinggeneration for its range changes or, for a refusal for lack of memory, after an import was given back. No time-out, no cap.
An ask answered from memory is no longer counted or logged as an import; the every-256th line says
refusedwhen thedriver refused; the refusal line names the call (
vk: pool … not imported (VkResult -2 at vkAllocateMemory over the host pages; asked again once its backing changes)).gpu/vk/vk_replay.cpp,vk_internal.h: with the frame statistics (every 120 presents)vk: imports refused: N ranges remembered (M MB); since the last report R refused, A asked again after their backing changed; a frame: X ranges held, Y bindings answered from memory, each range once a frame; nothing while no range is refused.docs/development.md, the macOS guest-memory paragraph.tests/test_rosetta_import_geometry.py(new, withrosetta_import_geometry_test.cpp: the alignment and freearithmetic under a simulated 4 KiB process page);
ImportRefusalscases inimport_hold_test.cpp;refusals_rememberedand the Rosetta route in
darwin_import_lifetime_test.cpp;hardware_page_discardindarwin_guest_memory_test.c(realVM, both routes); the API-25 layout in
test_vk_darwin_admission.pyanddarwin_renderer_startup_test.cpp; the wiringfilter in
test_darwin_renderer.py;nx2/test_import_windows.pyknowsimport_step.Measurements
Verdict: fixes the cause on the GPU (history: RI-01 on both routes, every replacement inside a 16 KiB page refused under
Rosetta and the fix's whole-page discard imported); game-free at this head, the free and import geometry is 16 KiB on both
routes and a refused range is not asked of the driver again while its backing is unchanged. Not yet measured in the game (the next
x86_64 start decides; see Testing).
Hardware: macOS 26.7.1, Apple M4 Max, 64 GB, Apple clang 21
Scene: none (game-free tests; RI-01 is a standalone probe of the same mapping and import calls, no game data)
Method: the tests below at this head, arm64 and x86_64 (Rosetta), and on the base with this PR's test files, which must fail.
RI-01 (
reports/RosettaImport-files/ri01/, packetb61fed81…), one admission per route, run by the parent at 21:34 CST.Exact checks: one run per route.
How to reproduce:
python -I -B tests/test_darwin_guest_memory.py --cc clang --arch arm64 --arch x86_64;python -I -B tests/test_rosetta_import_geometry.py;python -I -B tests/test_import_hold.pyBaseline: the "before" column is this PR's base, #658 (
11d09b9a), whose import admission this PR changes (main has neither#658's admission nor #491's pins).
11d09b9ab7b509f8VK_ERROR_OUT_OF_DEVICE_MEMORY), 11 kernel refusals for 11 imports; asked again unchanged: refused 3 of 3test_darwin_guest_memory, arm64 and x86_64darwin_mem_hardware_page)HARDWARE_PAGE process=4096 hardware=16384 frees=4 entries=5 edges_on_hardware_pages=1 bytes_exact=1 backing_moved=1under Rosettatest_rosetta_import_geometrydarwin_mem_whole_pages)test_import_holdImportRefusals)test_darwin_renderer, both routesImportRefusalsin the lifetime fixture)test_vk_darwin_admissionApple host API version is 25)Flags and settings
None: the geometry follows the page sizes and the refusals follow the backing, with no switch. The diff against main also
shows the switches of the PRs it stacks on, theirs to list:
HOS_KEEP_FREED_PAGES,HOS_ZERO_ALL_PAGES,RX_NVN_WATCH_AFTER,RX_NVN_WATCH_ALL,RX_NVN_WATCH_TEXELS_SRC(#480),RX_VK_LIBRARY(#438),RX_VK_DLSS_ACTORS,RX_VK_GPU_DUMP,RX_VK_MESH_IND_COUNT_WORDandRX_VK_NO_UPSCALE_STUB(#658).Testing
Tested: on macOS 26.7.1, Apple M4 Max, at this head (main
6c9edffe, #658's stack, this PR), game-free, every run under theshared check slot with a fresh HOME and TMPDIR:
tests/test_darwin_guest_memory.py --arch arm64 --arch x86_64: exit 0 (real VM; the Rosetta route's frees replace whole16 KiB pages and move the backing generation). On the base with this PR's test: fails to compile.
tests/test_rosetta_import_geometry.py: exit 0; its fixture also built with-arch x86_64and run under Rosetta: 18 checks,0 failures. On the base with this PR's test: fails.
tests/test_import_hold.py: exit 0; on the base with this PR's test: fails.tests/test_darwin_renderer.py --arch arm64 --arch x86_64,tests/test_vk_darwin_admission.py(arm64, x86_64),tests/test_import_buffer_binding.py(arm64, x86_64: 37 cases, the Windows/Linux import routes unchanged),nx2/test_import_windows.py: exit 0. On the base with this PR's tests the first two fail.00d0305c, before the API end commit):test_vk_darwin_admissionandtest_darwin_rendererfail on thelayout (
sizeof(AppleHost) == 392,not at exact API24 layout), as its end commit says.vk_memory.cppandvk_replay.cppcompiled whole with main 6c9edffe's renderer flags for arm64 and x86_64 (4 of 4,scratch tree with #476, #492 and #490); host
darwin_guest_mem.c,darwin_mem.candrx_backend.ccompiled whole with#479's macOS host flags for arm64 and x86_64 (6 of 6, scratch tree of this head with #490 and #479; the three files are this
head's byte for byte).
38d42adf): link check 0 of 621 declared names without an externaldefinition, 66 of 66 objects, 1,218 name/arch checks, closure 566 symbols a route, 0 undefined; the suite in the run_all box.
failure. Every changed line is the topic's except the API number (24 there, 25 here) and the docs paragraph, rewritten on
#491's text.
reports/PubWave-files/PubNext/(proof-ri-01/,proof-ri-02/,compile/rosetta-import/,compile/host-rosetta/,offapple-rosetta.txt,wave3/).Not tested: the game at this head on either route (the next x86_64 start on a build carrying this commit decides: see
below); no GPU run at this head (RI-01 is a standalone probe of the mechanism and of the fix's discard, not LS's code).
Windows and Linux: not built there (6 changed files differ from the base's off Apple, in comments, an unused type and an empty
function); its test request covers it.
Must be tested:
vk: guest memory imported in place (alignment 16384, the hardware's page: this process's pages are smaller (Rosetta)), 0[mvk-error] VK_ERROR_OUT_OF_DEVICE_MEMORY … host-coherent or exportable, 0vk: pool … not imported, novk: imports refused:line, 0IOGPU
memoryDescriptorWithClientBuffer … failedin a livelog stream, 0 skipped draws in play (about arm64's 1,910 draws aframe), and the log well under the runner's 16 MiB (by hand: the owner, the log's
vk:and[mvk-error]lines)alignment 16384) and no other difference (byhand: the owner)
b7b509f8, it carries #658 with the PRs it stacks on) and main6c9edffethesame way and start each into play, expecting this PR's changed renderer files to compile (
vk_memory.cpp,vk_replay.cpp,import_hold.h,vk_internal.h) and every line to match main's, a failed import still held for 2 s and logged as before(by hand: test request T-NQ85)
Risks
What could break: on macOS, a free now zeroes the edges of a range in place instead of replacing them (the same bytes);
under Rosetta guest protections and watches cover 16 KiB, as on arm64. A refused import is asked again only when its backing
changes or memory is given back: a refusal for another reason that would have cleared by itself stays refused until then
(counted with the frame statistics, so it is visible). Windows and Linux: unchanged.
Turning it off: revert the PR (no switch).
Compatibility: the Apple host ABI moves to version 25 (size 400): the macOS executable and its renderer library must be
built together, as with every ABI step (on a mismatch
rx_backend.cdoes not take the Vulkan renderer:rx: mkwvk_init version mismatch). Windows and Linux keep version 22, size 360.The translator stamp is unchanged (
ebbf160ce9626041: no stamped file changes). Nothing changes in saves orlongshot_options.ini.Reviewer notes
11d09b9a..b7b509f8, two commits: the topic-xfrommacos/rosetta-import, then the API end commit. Readdarwin_mem_discardandhos_posix_guest_reservefirst, thenImportRefusalsandimport_pool's macOS branch.11d09b9a): the logging it changes is #658'simport_planned, beside its aliasisolation and segments; and through #658 on #491 (the import pins at API 24,
darwin_import.h) with the PRs #491 carries.docs/development.md(the guest-memory paragraph rewritten on #491's text, with this topic's facts),vk_replay.cpp(only this topic's report line, afterdraw_skip_report) andtest_vk_darwin_admission.py(#491's layoutkept in the pick); the end commit appends the generation at version 25.
table renumbers after whichever lands first.
vk_replay.cpp'slog_frame_stats(each adds a report block where thisadds its line): keep both, this PR's line after #680's block, as
macos/integrated187387eb. The conflicts with #490(
win32_posix.c,test_collect_wide.py) and with #667 and #669 (vk_draw.cpp) are #658's own.ImportRefusalsdefined and unused, and an emptymemory_import_refusal_reportcalled once every 120 presents); behaviour unchanged. The same storm exists there in asmaller form (a failed import asked again every 2 s, every 256th ask logged as
importedeven when it failed or was held);changing it for every platform is the owner's decision, not prepared here.
reports/2026-11-14-*.md(the port's design notes), which are not in this repository.Issues
See #420
Checklist
Everyone
type(scope): description) of the status line's type.main(maine090cc48).python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). (On the wave's merge38d42adf(the control-flow bound PR, whose stack carries #658, + this PR): 559 passed, 31 failed, 18 skipped against main 6c9edffe's 531/32/17 on this Mac; the 31 failures are main's own and nothing that passes on main fails;test_collect_widenow passes; 27 new tests pass,test_rosetta_import_geometry,test_darwin_guest_memory,test_darwin_rendererandtest_vk_darwin_admissionamong them;test_darwin_audio_service, Darwin-only, is skipped by the runner;reports/PubWave-files/PubNext/wave3/full-38d42adf/.)third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.mainand passing here. (They fail on this PR's base, #658, with this PR'stest files; on
maintoo, which has neither the guest layer's discard nor #491's import pins.)tests/that reads changed source updated in this PR, not weakened.MKWVK_API_VERSIONbumped (gpu/vk/vk_api.h'sMkwVkHostorMkwVkRendererchanged). (Apple 24 → 25 for the newhost callback; non-Apple unchanged at 22.)
Every scan down vk_memory.cpp's import table by start (pool_lookup's covering import and merge plan, bg_drain, prefetch_chunks, import_in_background, memory_note_gpu_write and memory_guest_imported) stopped once an import ended more than 1 GiB below its address, on the belief that no import is larger than that. pool_lookup's own merge escape leaves an import above 1 GiB over the smaller ones inside it ("left overlapping"), and the device allows imports up to max_import_size: a scan stopped at a small import inside a large one, missed the large one, and the same bytes were imported again, two memories over the same bytes. import_reach.h counts the live imports by span class; its bound is above every live span, within twice the largest. imported_add and imported_take, now the table's only ways in and out, keep the count. A scan stops only where no import further down can reach its address, so it also stops sooner than before when the imports are small. The covering-import checks in pool_lookup and import_in_background look past the last import at or below the range for the same reason. Tests: test_import_reach.py runs the actual scans with a 4 GiB import over a 16 MiB chunk, left by the merge escape (imports are recorded ranges: no payload, no Vulkan, no game). Its lookup, query, note, drain, background and prefetch cases fail on main5e072fc8and pass here, and a source check keeps every change to the table in the two helpers. test_import_windows runs pool_lookup with the helpers; test_mirror_gpu_only names the new publication point. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]>044635ea added vma_startup_rollback (vk_vma.cpp, run after a failed replay_start), which calls vmaDestroyAllocator. vk_vma.cpp includes vk_work.h last so that RX_VK_RECORDER=defer/thread drains before its destructions, and every such boundary call in a vk_work.h file must be in RX_REC_BOUNDARIES with a drain-first macro. This one was not, and test_recorder_sync caught it ("vk_vma.cpp calls ['vmaDestroyAllocator'], which vk_work.h does not drain before"): it passes at 58c5c5aa (044635ea's parent) and fails at 044635ea. Add it to the list and to vk_work.h, next to vmaDestroyBuffer and vmaDestroyImage. With the recorder off (the default) boundary() is a relaxed load and a branch. Rollback runs before any image or buffer exists, so nothing should be held there today, but the allocator is no longer destroyed while a recorder could still hold packets. The test is unchanged (sources: 42 boundaries drained, was 41). Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]>b7b509f85ca0027de2aca0027de2ac6ec9629e59Folded into #848 (feat(vk): the SPIR-V back end on MoltenVK and guest spans past one device allocation)
Pull request closed