WIP: fix(runtime): preserve guest pages when discarding on macOS #478
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/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
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!478
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/guest-memory"
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?
Why
On macOS the game's memory cannot be set up or given back correctly with the Linux code. The 512 GiB guest window is
reserved with Linux-only calls (
MAP_FIXED_NOREPLACE,memfd_create,MADV_REMOVE,MADV_NOHUGEPAGE), so it does notcompile. And memory the game frees must read as zero afterwards, but Darwin's
MADV_DONTNEEDleaves the old bytes inplace: freed guest pages would come back holding stale data. Native Apple silicon also has 16 KiB host pages under the
game's 4 KiB ones, so a free must not take the neighbouring live 4 KiB pages with it.
What changed
runtime:
host/posix/darwin_mem.canddarwin_mem.h(new).darwin_mem_mapreserves withmach_vm_mapand never replaces what is already mapped there (EEXIST).darwin_mem_discardzeroes exactly the bytes asked for: whole host pages inside the range get a fresh privatemapping over the same address (
mmap(MAP_FIXED), one call, so no gap another thread could take), which also returnstheir memory to the system; partial host pages at either end are zeroed in place, so the guest pages beside them keep
their bytes.
darwin_mem_protectchanges protection on the real host page size (16 KiB native, 4 KiB under Rosetta).runtime:
host/hos_main.creserve_gueston macOS: the window at its fixed address, read-write and paged on demand,with a clear message when something already occupies it.
hos_posix_guest_discardusesdarwin_mem_discardthere.runtime:
host/posix/win32_posix.con macOS:VirtualAlloc,VirtualFreeandVirtualProtectuse the threefunctions above. They keep their 4 KiB guest-page rules:
MEM_DECOMMITrounds out to 4 KiB,MEM_RESETdiscards onlythe whole 4 KiB pages inside the range. Overflowing ranges and failed calls return an error instead of success.
runtime:
host/hos_mem.candhost/hos_svc.con macOS: a failed discard is reported (FreeMemoryPagesandUnmapPhysicalMemory, svc 0x2D, return 0xCA01; a region discard is fatal) instead of being ignored. Ranges under aGPU import are still zeroed in place, as on Linux.
Windows and Linux: unchanged. Every change is under
__APPLE__, withmain's lines in#else.tests:
tests/test_darwin_guest_memory.pywithtests/darwin_guest_memory_test.c(new; macOS only). It compilesdarwin_mem.cwith the real functions cut by text fromhos_main.c,hos_mem.c,hos_svc.c,mkw_glue.candwin32_posix.c, and runs them on real memory:--arch arm64 --arch x86_64runs both routes (x86_64 under Rosetta); without it, the native one.--sourcetestsanother tree.
docs:
docs/development.md, "macOS guest-memory groundwork".Flags and settings
None in the game. The test takes
--arch,--source,--outand--cc.Measurements
main:
5e072fc8c2adadba5ea610148be1c47800ba0cca(2026-11-14)this PR:
mac/guest-memory@9c6791454f8a29c22adcbb1591202a94ec01c1c6Hardware: Apple M4 Max (16 CPUs, 64 GiB), macOS 26.7.1 (25G241), Apple clang 21.0.0, C11,
-O2 -Wall -Wextra -Werror,-mmacosx-version-min=14.4. No GPU, no gameScene: none (a portability fix; its proof is the test, as CONTRIBUTING's
fixrow allows)Method:
tests/test_darwin_guest_memory.py --cc clang --arch arm64 --arch x86_64on this PR and againstmain's source(
git archive 5e072fc8,--source), in two interleaved rounds (PR, main, PR, main). Then the full game-free suite onthis PR, compared by test name with
main's on the same Mac.How to reproduce:
python -I -B tests/test_darwin_guest_memory.py --cc clang --arch arm64 --arch x86_64, then the samewith
--source <a tree of main>.5e072fc89c679145MAP_FIXED_NOREPLACE,MADV_REMOVE,MADV_NOHUGEPAGE,SYS_memfd_createWhy a fresh mapping for whole pages: on this Mac, of the ways to drop a page, only a new private mapping both read back
as zero and returned the memory at once.
MADV_DONTNEED,MADV_FREE,MADV_FREE_REUSABLE,MS_KILLPAGESandmsync(MS_INVALIDATE)left the bytes;memsetandMADV_ZEROzeroed them but kept them resident (andMADV_ZEROisENOTSUPunder Rosetta). That comparison ran on the topic branch before this rebase (macos/guest-memory, 3 runs percandidate and route); the call it chose is unchanged here.
Testing
Tested:
On an Apple M4 Max with macOS 26.7.1, game-free only, natively and under Rosetta, as above.
The test without
--arch, as the suite runs it: native arm64 only, exit 0.The full suite,
tests/run_all.py, 6 at once, on this PR, compared by test name withmainon the same Mac (therunner labels macOS
linux):tests/test_darwin_guest_memory.py(pass): no other check changedstatus, and the same 17 are skipped.
main's own on a Mac, which the macOS port's other pull requestsaddress:
test_appimage_package_retry,test_collect_wide,test_control,test_framegen_resources,test_game_layout,test_low_memory_notice,test_mem_gate,test_mem_gate_children,test_mirror_trim,test_module_ids,test_oot_state_tap,test_pad_layout,test_pcache_header_read,test_pipeline_cache_file,test_pipeline_cache_save,test_pipeline_play_cache,test_prof_symbols,test_program_cache_cleanup,test_program_cache_one_variant.Not tested:
main's, in the#elsebranches.maindoes not build for macOS yet; the other macOS pull requests (#420) do the rest. ThemacOS build links
host/posix/darwin_mem.cinto the executable and the renderer through the build-scripts pull requestof the same wave (
build(macos): discover dual-architecture toolchains and form Mach-O commands), not through this one.memory and serialize its lifetime, as on Linux.
Needs other testers:
tests/run_all.py, expecting the same results as onmain(request to open).
python -I -B tests/test_darwin_guest_memory.py --cc clang --arch arm64 --arch x86_64,expecting
PASS(request to open).Screenshots
Nothing on screen.
Risk and rollback
failed discard is an error instead of a silent success.
(
rx_guest_imported) keep zeroing those in place, unchanged.LONGSHOT_SHADER_STAMP(nx2/build.py:1176-1182) changes.Reviewer notes
darwin_mem_discard, then the 4 KiB rounding kept inVirtualAlloc(MEM_RESET)andVirtualFree(MEM_DECOMMIT).fe35e00aanda350a531aremacos/guest-memory's969eab22andfb4a2c21, cherry-picked ontomainwithout a conflict (the same patch ids), with the
Assisted-byandCo-Authored-Bytrailers of this rebase added.9c679145is new:main's #324 madehos_commitreturnint, and the test cut it by its oldvoidhead, so onmainit stopped with aValueErrorbefore compiling anything. It now cutsint hos_commit(.FreeMemoryPagesun-counts the pages (mapped_bytes) before the discard. On macOS, if that discard fails(the fresh mapping refused), the game gets 0xCA01 with the pages already un-counted, so the count errs low, as #324's
own comment allows for a failed count. It is left as is: changing the order would change #324's code on every
platform.
fix(runtime): compile host jumps and pause hints on macOS arm64merges second meets aone-hunk conflict in
docs/development.md(both add a section after "Log levels"): keep both.mach_vm.h,sys/mman.h, madvise(2), msync(2)); nothing copied.Issues
See #420 (the macOS port).
Checklist
For everyone
type(scope): description).main. Branched from5e072fc8; main has moved since, and it is rebased in one sweep before ready.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above). On macOS:526 passed, 17 skipped, and the same 19 failures as
mainon this Mac, named under Testing.requests are still to open.
the median reported. For a
fix: the test fails onmainand passes here, 2 interleaved rounds, both routes.third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
Assisted-by:/Co-Authored-By:trailers.mainand passing here.Needs other testers.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.