WIP: fix(macos): serialize guest memory ownership on 16 KiB pages and pin imported backing #480
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!480
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/guest-16k"
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?
What this is for: the macOS port (#420). Native Apple silicon has 16 KiB host pages under the game's 4 KiB ones,
and the executable and the renderer each link their own copy of the POSIX Win32 layer. The guest-memory groundwork
(#478) zeroes freed pages correctly, but leaves the races and lifetimes around them: a free that drops the allocator's
lock before its bytes are gone, two images each able to change the guest window, a GPU import protected only by a
"is it imported?" question, a write watch and a
VirtualQuerythat assume 4 KiB host pages, and a dirty-page scan thatreads atomic byte marks with plain word loads. This PR gives macOS guest memory one owner in the executable, with import
pins and native-page watches. Everything here is macOS only: Windows and Linux compile exactly
main's code(checked file by file below). The version for every platform, which needs timing there first, is issue #432.
What is measured: the guest-memory test (real memory, real faults, two-image dispatch, deterministic races) on arm64
and on x86_64 under Rosetta; the dirty-scan test on both of its branches; the Windows/Linux identity of every changed
host/file; and the full game-free suite on an Apple M4 Max (macOS 26.7.1), compared by test name withmain.Not measured: Windows, Linux, a game run, a GPU import (the renderer's pins are the next PR's), macOS 14.4 itself,
an Intel Mac.
Depends on #478 (
mac/guest-memory) and #436 (mac/fault-contexts): this branch starts at #478's head9c679145andmerges #436's head
7731dc0cunchanged (mergeaeb30801; the one conflict, the Apple include block ofwin32_posix.c, keeps both includes). Reviewaeb30801..647e90e4, this PR's own four commits.Why
hos_region_freedrops the allocator's lock,then discards. A new allocation of the same range in between gets bytes the old free is about to zero or replace.
win32_posix.c. Either copy'sVirtualAlloc(MEM_RESERVE)orMEM_RELEASEinside the window would make or drop a mapping the other does not know.the backing if not. Nothing stops an import starting between the question and the replacement, or a retired import
still being read by the GPU.
VirtualProtectand theRX_NVN_WATCH_TEXELS_SRCwrite watch work on whole host pages: afault on one 4 KiB guest page is a fault for its three neighbours too.
VirtualQueryanswered "committed,read-write" for the whole window without asking the kernel.
time; the scan's zero-skip read them through
memcpyand 64-bit loads, a data race in C's memory model. On arm64'sweaker ordering that matters more than on x86.
What changed
All of it under
#if defined(__APPLE__), withmain's lines in the#else.host/posix/darwin_guest_mem.c(new; linked into the executable only). The one owner of the guest window:hos_posix_guest_reserve,_commit,_discardand_protect, the import pins, and the watch states, under onemutex. No renderer call is made while it is held.
hos_posix_guest_import_pin(lo, hi)/_unpin: half-open guest offsets of the whole host-page backing. Duplicateand overlapping pins each count; a mismatched unpin releases nothing. A discard that overlaps a live pin zeroes the
range in place instead of replacing the backing.
protection, then changed with one call; on failure the watch states are restored.
while a watch fault is pending keeps that fault acknowledged instead of rearming it, and read-only or no-access
stays closed.
host/posix/darwin_mem.c/.h:darwin_mem_query(Mach regions, holes and real protections), stillstateless and shared by both images.
host/posix/win32_posix.c: each copy finds the owner's commit, discard and protect once withdlsym(RTLD_MAIN_ONLY, ...), and refuses guest changes without them. A reservation or release inside the window isrefused.
VirtualQueryreads the Mach regions;VirtualProtectreturns the real previous protection. Host-onlyreservations record their host-rounded size and fail when the table is full.
host/hos_mem.c: the address-region and page frees zero or discard under the allocator's lock, so the rangeis not handed out again until its bytes are gone; a failed region discard is fatal, a failed page free returns 0xCA01.
A page allocation over pages in use keeps their bytes, below 4 GiB too (
HOS_ZERO_ALL_PAGES=1still clears them).hos_region_allocrefuses a bad size or alignment, and a failed bookkeepingreallocis fatal.hos_mark_writtenclamps a length past the window.
host/mkw_glue.c: a failed commit is fatal (#324'sERROR_COMMITMENT_LIMITwait stays, unchanged, in the#else).host/hos_svc.c:MapPhysicalMemoryrefuses a range outside the window;UnmapPhysicalMemoryfrees through theowner (pins included) instead of asking the renderer.
host/hos_main.c: the window is reserved through the owner.host/nvn_native.c: the write watch on native pages. The fault handler records a fixed-size event (address,PC, thread name); the rearm thread logs it later and rearms, logging a page it could not rearm.
host/rx_backend.c: the dirty-page scan reads the marks with byte atomics at every step (256, 32, 8), anempty range consumes no mark, and a failed allocation is fatal. Before each scan, the watched pages are marked dirty
again (
hos_posix_guest_watch_mark), since a fault comes before the store it lets through. The renderer's zeroedguest memory (
host_guest_alloc) is marked written.tests/test_darwin_guest_memory.pywithtests/darwin_guest_memory_test.c(macOS only): compiles the owner, theshared primitives and the real callers cut from
hos_main.c,hos_mem.c,hos_svc.c,mkw_glue.c,nvn_native.c,rx_backend.candwin32_posix.c(its exception dispatch included), loads two copies of the shimas separate dylibs, and runs them on real memory: pins on real shared Mach aliases, frees racing allocations and
pins behind a latch (the test checks the real mutexes are held, not a sleep), real write faults on every subpage, a
fault held inside the real vectored dispatcher while the page is made writable, protection failures before and
after the check (a real maximum-protection change from another thread), the dirty scan at the 255/256 boundary
with two producers.
--behavior-onlyand--watch-protection-onlyrun parts.tests/test_collect_wide.py: the fixture compiles the scan the platform builds (byte atomics on macOS,main'sword loads elsewhere).
docs/development.md, "macOS guest-memory groundwork".Rebase onto
5e072fc8:a3f3001dmet #324's page accounting (below) and #324's commit-limit wait, andtest_collect_wide.py, whose old base had #454's#undef memcpy(not onmain).3366b61band647e90e4are new:the Apple-only gate (the owner's decision for this PR) and the docs.
#324's accounting, kept:
AllocateMemoryPagescounts the pages before the Apple path, as on every platform. OnmacOS
FreeMemoryPagesun-counts them under the same lock as the discard, and only once it has succeeded: a failedfree (0xCA01) leaves its pages counted, so the count no longer errs low there (#478's reviewer note).
Flags and settings
HOS_ZERO_ALL_PAGES=1(macOS: changed)RX_NVN_WATCH_TEXELS_SRC=<w>x<h>,RX_NVN_WATCH_AFTER,RX_NVN_WATCH_ALL(macOS: work now)--arch(repeatable),--source,--out,--cc,--behavior-only,--watch-protection-onlyNo new switch. The three watch switches are
main's debugging switches, described wherenvn_native.creads them andnot in
docs/development.md's list onmain; this PR namesRX_NVN_WATCH_TEXELS_SRCin the guest-memory section andleaves the list as it is.
Measurements
main:
5e072fc8c2adadba5ea610148be1c47800ba0cca(2026-11-14)dependencies alone:
aeb30801(#478 + #436, merged, without this PR's commits)this PR:
mac/guest-16k@647e90e450e8e848b6c35c6dc06a884db29dbb02Hardware: 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 game.Scene: none (a portability fix; its proof is the tests, as CONTRIBUTING's
fixrow allows).Method: two interleaved rounds (PR, main, dependencies, then again).
tests/test_darwin_guest_memory.py --cc clang --arch arm64 --arch x86_64 --source <tree>, this PR's test against each tree. Each tree's owntests/test_collect_wide.py --cc clang, and again with a compiler wrapper that undefines__APPLE__(and sets_FORTIFY_SOURCE=0, so the SDK does not definememcpyas a macro, as glibc does not): the scan Windows and Linuxcompile, built on this Mac. Then the Windows/Linux identity check and the full game-free suite.
How to reproduce:
python -I -B tests/test_darwin_guest_memory.py --cc clang --arch arm64 --arch x86_64, andpython -I -B tests/test_collect_wide.py --cc clang.5e072fc8aeb30801647e90e4host/posix/darwin_mem.c/ the samehost/posix/darwin_guest_mem.c/ the sametest_collect_wide.py, macOS'memcpy' macro redefined, the SDK's) / the sametest_collect_wide.py,__APPLE__undefined(
mem: cannot discard guest region at 0x7ffffff000in the output is the fatal path the test provokes on purpose:region_fatal_exit=23.)Windows and Linux compile
main's code (A-tools/non_apple_identity.shin the port's notes): for eachhost/filethis branch changes,
unifdef -U__APPLE__(only__APPLE__resolved; every other conditional, comment and blank linekept) of
mainand of this PR are byte-identical:hos.h,hos_main.c,hos_mem.c,hos_svc.c,mkw_glue.c,nvn_native.c,rx_backend.c,posix/win32_posix.c,posix/windows.h. Their preprocessed token streams (clang -Efor
x86_64-unknown-linux-gnuwith-D_GNU_SOURCEand forx86_64-w64-windows-gnu,-DA64_WRITE_TRACK -DA64_POLLING, empty system-header stand-ins, line markers and whitespace dropped) are identical too: 7.cfiles × 2targets, 14 of 14. The new
darwin_*files are not in the Windows or Linux build.Earlier, on the old base (
a3f3001dand27719a49on the port's branch; not rerun here): the same test's runs,and
test_collect_wide.pypassing with the byte-atomic scan. The two defects27719a49repairs (a pending faultafter a page was made writable; a protection change failing on a later page after changing earlier ones) were found by
reading the code, not reproduced on the old code; its tests cover both on both routes now.
Testing
Tested:
On an Apple M4 Max with macOS 26.7.1, game-free only, natively (16 KiB pages) and under Rosetta (4 KiB pages), as
above.
tests/test_commit_limit.py(#324's commit-limit wait): passes on this PR.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.pypasses (new tomain), and so does #436'stests/test_darwin_fault_context.py, which this branch carries;tests/test_collect_wide.pygoes from FAIL onmain(the fixture'smemcpymacro meets the SDK's) to pass. Nothing else changed status.main:test_appimage_package_retry,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. The macOS port's other pull requests address them.The same 17 are skipped.
Not tested:
main's code (the identity check above), andtest_collect_wide.py's Windows/Linux branch was built and run on this Mac with__APPLE__undefined, which is not aLinux build.
maindoes not build for macOS yet (#420); the renderer's pins are the nextPR's. No import was pinned by a renderer here; the pin tests use real shared Mach aliases.
Needs other testers:
tests/run_all.py, expecting the same results as onmain(request to open). Issue #432's measurements are separate.
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
main's code, as shown above. No timing was needed for that reason; theevery-platform version waits for #432's measurements.
time its discard takes. A failed commit, a failed region discard and a failed scan allocation are now fatal instead of
logged or ignored.
next PR in this wave (
fix(macos): pin Vulkan host imports until their last owner, and unwind failed startups) pinsevery import until its last owner is gone. Until it lands, a free under an import would replace the backing. macOS
does not build from
mainuntil the port's other PRs land, so no player can meet this; merge the two back to back.LONGSHOT_SHADER_STAMP(nx2/build.py:1176-1182) changes; no renderer ABI field changes.Reviewer notes
darwin_guest_mem.c(hos_posix_guest_discard,_protect,_import_pin,_watch_fault), then theApple branches of
hos_region_freeandFreeMemoryPagesinhos_mem.c.faea2408and37901de8area3f3001dand27719a49from the port's branch, with #324's conflictsresolved and the
Assisted-by/Co-Authored-Bytrailers of this rebase added.3366b61bis new: it puts everychange the first two made outside the Apple path back under
__APPLE__(the scan's byte atomics, the zeroed memorymarked written, the fatal allocation failures,
hos_region_alloc's checks,hos_mark_written's clamp,VirtualQuery's pointer check andlast_error).647e90e4is the docs.darwin_guest_mem.cmust be linked into the executable only. The build PR of this wave (#479) has it in itsexecutable-only list; this PR adds no build-script change.
test_collect_wide.pymeets #454's#undef memcpy(keep this PR's text: themacOS branch no longer defines that macro);
win32_posix.c's Apple include block meets #438 (keep every include); anddocs/development.mdmeets #477 (both add a section after "Log levels": keep both).main(dc390e59,git merge-tree): no conflict. Main has since changed five files this PR's testcuts source from (
hos_main.c,hos_svc.c,mkw_glue.c,rx_backend.c,win32_posix.c), so the test runs againin the rebase; on the merged tree the Windows/Linux identity above still holds against
dc390e59(text 9/9, tokens14/14). #370 changes no claim here: this PR edits no file the stamp hashes, before or after #370.
mach_vm.h,mach/vm_region.h,sys/mman.h,sigaction(2)); nothingcopied.
Issues
See #420 (the macOS port). The every-platform version: #432.
Checklist
For everyone
type(scope): description).main. Branched from5e072fc8, on #478 and #436, which it depends on;mainhas moved since(it merges with
dc390e59without a conflict, under Reviewer notes).python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above).On macOS: 528 passed, 17 skipped, and 18 failures that also fail on
main, 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.tests/that reads changed source updated in this PR, not weakened.Windows and Linux sources equal
main's. Requested under Needs other testers.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.