WIP: fix(macos): serialize guest memory ownership on 16 KiB pages and pin imported backing #480

Draft
mooki wants to merge 10 commits from mooki/Longshot:mac/guest-16k into main
Member

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 VirtualQuery that assume 4 KiB host pages, and a dirty-page scan that
reads 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 with main.
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 head 9c679145 and
merges #436's head 7731dc0c unchanged (merge aeb30801; the one conflict, the Apple include block of
win32_posix.c, keeps both includes). Review aeb30801..647e90e4, this PR's own four commits.

Why

  • A freed range can be handed out while it is still being zeroed. hos_region_free drops 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.
  • Two images can change the guest window. The executable and the renderer each link win32_posix.c. Either copy's
    VirtualAlloc(MEM_RESERVE) or MEM_RELEASE inside the window would make or drop a mapping the other does not know.
  • A GPU import needs a pin, not a question. A free asks the renderer whether the range is imported, then replaces
    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.
  • 16 KiB host pages. VirtualProtect and the RX_NVN_WATCH_TEXELS_SRC write watch work on whole host pages: a
    fault on one 4 KiB guest page is a fault for its three neighbours too. VirtualQuery answered "committed,
    read-write" for the whole window without asking the kernel.
  • The dirty-page scan reads atomic bytes with word loads. Game threads publish write marks one atomic byte at a
    time; the scan's zero-skip read them through memcpy and 64-bit loads, a data race in C's memory model. On arm64's
    weaker ordering that matters more than on x86.

What changed

All of it under #if defined(__APPLE__), with main's lines in the #else.

  • runtime: host/posix/darwin_guest_mem.c (new; linked into the executable only). The one owner of the guest window:
    hos_posix_guest_reserve, _commit, _discard and _protect, the import pins, and the watch states, under one
    mutex. 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. Duplicate
      and 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 changes take the whole native range at once: it is checked first against every page's Mach maximum
      protection, then changed with one call; on failure the watch states are restored.
    • Watches: one state byte per host page. The fault handler takes no lock and allocates nothing; a page made writable
      while a watch fault is pending keeps that fault acknowledged instead of rearming it, and read-only or no-access
      stays closed.
  • runtime: host/posix/darwin_mem.c/.h: darwin_mem_query (Mach regions, holes and real protections), still
    stateless and shared by both images.
  • runtime: host/posix/win32_posix.c: each copy finds the owner's commit, discard and protect once with
    dlsym(RTLD_MAIN_ONLY, ...), and refuses guest changes without them. A reservation or release inside the window is
    refused. VirtualQuery reads the Mach regions; VirtualProtect returns the real previous protection. Host-only
    reservations record their host-rounded size and fail when the table is full.
  • runtime: host/hos_mem.c: the address-region and page frees zero or discard under the allocator's lock, so the range
    is 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=1 still clears them).
    hos_region_alloc refuses a bad size or alignment, and a failed bookkeeping realloc is fatal. hos_mark_written
    clamps a length past the window.
  • runtime: host/mkw_glue.c: a failed commit is fatal (#324's ERROR_COMMITMENT_LIMIT wait stays, unchanged, in the
    #else).
    host/hos_svc.c: MapPhysicalMemory refuses a range outside the window; UnmapPhysicalMemory frees through the
    owner (pins included) instead of asking the renderer. host/hos_main.c: the window is reserved through the owner.
  • runtime: 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.
  • runtime: host/rx_backend.c: the dirty-page scan reads the marks with byte atomics at every step (256, 32, 8), an
    empty 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 zeroed
    guest memory (host_guest_alloc) is marked written.
  • tests:
    • tests/test_darwin_guest_memory.py with tests/darwin_guest_memory_test.c (macOS only): compiles the owner, the
      shared primitives and the real callers cut from hos_main.c, hos_mem.c, hos_svc.c, mkw_glue.c,
      nvn_native.c, rx_backend.c and win32_posix.c (its exception dispatch included), loads two copies of the shim
      as 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-only and --watch-protection-only run parts.
    • tests/test_collect_wide.py: the fixture compiles the scan the platform builds (byte atomics on macOS, main's
      word loads elsewhere).
  • docs: docs/development.md, "macOS guest-memory groundwork".

Rebase onto 5e072fc8: a3f3001d met #324's page accounting (below) and #324's commit-limit wait, and
test_collect_wide.py, whose old base had #454's #undef memcpy (not on main). 3366b61b and 647e90e4 are new:
the Apple-only gate (the owner's decision for this PR) and the docs.

#324's accounting, kept: AllocateMemoryPages counts the pages before the Apple path, as on every platform. On
macOS FreeMemoryPages un-counts them under the same lock as the discard, and only once it has succeeded: a failed
free (0xCA01) leaves its pages counted, so the count no longer errs low there (#478's reviewer note).

Flags and settings

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
HOS_ZERO_ALL_PAGES=1 (macOS: changed) A page allocation zeroes the whole range unset: pages in use keep their bytes, at any address Leave unset macOS Windows, Linux: unchanged (the used-page bitmap below 4 GiB)
RX_NVN_WATCH_TEXELS_SRC=<w>x<h>, RX_NVN_WATCH_AFTER, RX_NVN_WATCH_ALL (macOS: work now) The debugging write watch, on whole host pages, logged by the rearm thread with the fault's PC unset Debugging only macOS Windows, Linux: unchanged A game run
Test: --arch (repeatable), --source, --out, --cc, --behavior-only, --watch-protection-only Routes; another tree; keep the fixture; the compiler; part of the run native only the test Not a game switch

No new switch. The three watch switches are main's debugging switches, described where nvn_native.c reads them and
not in docs/development.md's list on main; this PR names RX_NVN_WATCH_TEXELS_SRC in the guest-memory section and
leaves 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 @ 647e90e450e8e848b6c35c6dc06a884db29dbb02
Hardware: 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 fix row 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 own
tests/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 define memcpy as a macro, as glibc does not): the scan Windows and Linux
compile, 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, and
python -I -B tests/test_collect_wide.py --cc clang.

test (round 1 / round 2) main @ 5e072fc8 dependencies @ aeb30801 this PR @ 647e90e4
guest memory, arm64 and Rosetta stops: no host/posix/darwin_mem.c / the same stops: no host/posix/darwin_guest_mem.c / the same exit 0 / exit 0
test_collect_wide.py, macOS fixture does not compile ('memcpy' macro redefined, the SDK's) / the same the same / the same the same pages 256 marks a step as 32 / the same
test_collect_wide.py, __APPLE__ undefined the same pages / the same the same pages / the same the same pages / the same
this PR @ 647e90e4, round 1 (round 2 the same apart from the resident-memory figures):
PIN_LIFETIME duplicate=1 overlap=1 retired_reference=1 full_padding=1 shared_alias_preserved=1 final_unpin_allows_remap=1
CONCURRENT_FREE region_and_page_apis=2 deterministic_latch=1 ownership_held_during_discard=1 reallocated_bytes_preserved=1
CONCURRENT_PIN deterministic_latch=1 pin_waits_for_remap=1 imported_backing_is_new_generation=1 subsequent_free_keeps_backing=1
WATCH host_page=16384 subpages=4 real_faults=4 dirty_granule=4096 neighbor_bytes=preserved restore_error=refused rearm_error=refused
PENDING_WATCH real_vectored_dispatch=1 pending_before_cancel=1 handlers_retained=1 next_real_fault=1 restrictive_controls=3 no_widening=1
PROTECT_FAILURE phase=preflight native_calls=0 error=access_denied prefix_bytes=preserved prefix_protections=preserved watch_states=preserved real_fault_recovery=2
PROTECT_FAILURE phase=after_preflight native_calls=1 error=access_denied prefix_bytes=preserved prefix_protections=preserved watch_states=preserved real_fault_recovery=2
DIRTY concurrent_published=400 acknowledged=400 wide_boundary=255/256 empty_range_preserves=1 overflow_clamped=1
FAILURE_PATHS overflow_unaligned_hole_readonly=refused free_svc_result=0xca01 region_fatal_exit=23
RECLAIM bytes=134217728 rss_before=136888320 rss_after=2719744 drop=134168576 cost_us=410.000
WATCH host_page=4096 subpages=1 real_faults=1 dirty_granule=4096 neighbor_bytes=preserved restore_error=refused rearm_error=refused
(x86_64 under Rosetta: the same lines, 4 KiB host pages)
PASS: Darwin guest memory: arm64, x86_64

(mem: cannot discard guest region at 0x7ffffff000 in 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.sh in the port's notes): for each host/ file
this branch changes, unifdef -U__APPLE__ (only __APPLE__ resolved; every other conditional, comment and blank line
kept) of main and 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 -E
for x86_64-unknown-linux-gnu with -D_GNU_SOURCE and for x86_64-w64-windows-gnu, -DA64_WRITE_TRACK -DA64_POLLING, empty system-header stand-ins, line markers and whitespace dropped) are identical too: 7 .c files × 2
targets, 14 of 14. The new darwin_* files are not in the Windows or Linux build.

Earlier, on the old base (a3f3001d and 27719a49 on the port's branch; not rerun here): the same test's runs,
and test_collect_wide.py passing with the byte-atomic scan. The two defects 27719a49 repairs (a pending fault
after 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 with main on the same Mac (the
    runner labels macOS linux):

    main @ 5e072fc8:     525 passed, 19 failed, 17 skipped in 177 s (linux, 6 at once)
    this PR @ 647e90e4:  528 passed, 18 failed, 17 skipped in 168 s (linux, 6 at once)
    
    • By test name: tests/test_darwin_guest_memory.py passes (new to main), and so does #436's
      tests/test_darwin_fault_context.py, which this branch carries; tests/test_collect_wide.py goes from FAIL on
      main (the fixture's memcpy macro meets the SDK's) to pass. Nothing else changed status.
    • The 18 remaining failures also fail on 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.
    • No crash report was born during the run.

Not tested:

  • Windows and Linux. Nothing was built or run there. They compile main's code (the identity check above), and
    test_collect_wide.py's Windows/Linux branch was built and run on this Mac with __APPLE__ undefined, which is not a
    Linux build.
  • The game on macOS, and a GPU import. main does not build for macOS yet (#420); the renderer's pins are the next
    PR's. No import was pinned by a renderer here; the pin tests use real shared Mach aliases.
  • Timing. The frees now discard under the allocator's lock on macOS; not measured in a game.
  • macOS 14.4 and an Intel Mac. Built for 14.4, run on 26.7.1 on Apple silicon only.

Needs other testers:

  • Windows (llvm-mingw) and Linux: build the host and run tests/run_all.py, expecting the same results as on main
    (request to open). Issue #432's measurements are separate.
  • macOS 14.4 on Apple silicon: 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

  • Windows and Linux: none. They compile main's code, as shown above. No timing was needed for that reason; the
    every-platform version waits for #432's measurements.
  • macOS: frees zero or discard under the allocator's lock (they were outside it); the time a free holds the lock is the
    time its discard takes. A failed commit, a failed region discard and a failed scan allocation are now fatal instead of
    logged or ignored.
  • GPU imports on macOS need the renderer's pins. Frees no longer ask the renderer whether a range is imported; the
    next PR in this wave (fix(macos): pin Vulkan host imports until their last owner, and unwind failed startups) pins
    every import until its last owner is gone. Until it lands, a free under an import would replace the backing. macOS
    does not build from main until the port's other PRs land, so no player can meet this; merge the two back to back.
  • Protection on macOS is per host page (16 KiB native), not per 4 KiB guest page.
  • No file hashed into LONGSHOT_SHADER_STAMP (nx2/build.py:1176-1182) changes; no renderer ABI field changes.
  • Rollback: revert the PR.

Reviewer notes

  • Start with darwin_guest_mem.c (hos_posix_guest_discard, _protect, _import_pin, _watch_fault), then the
    Apple branches of hos_region_free and FreeMemoryPages in hos_mem.c.
  • Commits: faea2408 and 37901de8 are a3f3001d and 27719a49 from the port's branch, with #324's conflicts
    resolved and the Assisted-by / Co-Authored-By trailers of this rebase added. 3366b61b is new: it puts every
    change the first two made outside the Apple path back under __APPLE__ (the scan's byte atomics, the zeroed memory
    marked written, the fatal allocation failures, hos_region_alloc's checks, hos_mark_written's clamp,
    VirtualQuery's pointer check and last_error). 647e90e4 is the docs.
  • darwin_guest_mem.c must be linked into the executable only. The build PR of this wave (#479) has it in its
    executable-only list; this PR adds no build-script change.
  • Merging with other PRs of the series: test_collect_wide.py meets #454's #undef memcpy (keep this PR's text: the
    macOS branch no longer defines that macro); win32_posix.c's Apple include block meets #438 (keep every include); and
    docs/development.md meets #477 (both add a section after "Log levels": keep both).
  • Against current main (dc390e59, git merge-tree): no conflict. Main has since changed five files this PR's test
    cuts source from (hos_main.c, hos_svc.c, mkw_glue.c, rx_backend.c, win32_posix.c), so the test runs again
    in the rebase; on the merged tree the Windows/Linux identity above still holds against dc390e59 (text 9/9, tokens
    14/14). #370 changes no claim here: this PR edits no file the stamp hashes, before or after #370.
  • Written from Apple's SDK headers and manuals (mach_vm.h, mach/vm_region.h, sys/mman.h, sigaction(2)); nothing
    copied.

Issues

See #420 (the macOS port). The every-platform version: #432.

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main. Branched from 5e072fc8, on #478 and #436, which it depends on; main has moved since
    (it merges with dc390e59 without a conflict, under Reviewer notes).
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
    On macOS: 528 passed, 17 skipped, and 18 failures that also fail on main, named under Testing.
  • Tested in the game, or what wasn't is named under Testing. (No game run; named under Testing.)
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). The test
    requests are still to open.
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each,
    the median reported. (For a fix: the test fails on main and passes here, 2 interleaved rounds, both routes.)
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • A game-free test added for the fix, failing on main and passing here.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. Not built there; the preprocessed
    Windows and Linux sources equal main's. Requested under Needs other testers.
  • Docs updated.
**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 `VirtualQuery` that assume 4 KiB host pages, and a dirty-page scan that reads 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 with `main`. **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 head `9c679145` and merges #436's head `7731dc0c` unchanged (merge `aeb30801`; the one conflict, the Apple include block of `win32_posix.c`, keeps both includes). Review `aeb30801..647e90e4`, this PR's own four commits. ## Why - **A freed range can be handed out while it is still being zeroed.** `hos_region_free` drops 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. - **Two images can change the guest window.** The executable and the renderer each link `win32_posix.c`. Either copy's `VirtualAlloc(MEM_RESERVE)` or `MEM_RELEASE` inside the window would make or drop a mapping the other does not know. - **A GPU import needs a pin, not a question.** A free asks the renderer whether the range is imported, then replaces 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. - **16 KiB host pages.** `VirtualProtect` and the `RX_NVN_WATCH_TEXELS_SRC` write watch work on whole host pages: a fault on one 4 KiB guest page is a fault for its three neighbours too. `VirtualQuery` answered "committed, read-write" for the whole window without asking the kernel. - **The dirty-page scan reads atomic bytes with word loads.** Game threads publish write marks one atomic byte at a time; the scan's zero-skip read them through `memcpy` and 64-bit loads, a data race in C's memory model. On arm64's weaker ordering that matters more than on x86. ## What changed All of it under `#if defined(__APPLE__)`, with `main`'s lines in the `#else`. - runtime: `host/posix/darwin_guest_mem.c` (new; linked into the executable only). The one owner of the guest window: `hos_posix_guest_reserve`, `_commit`, `_discard` and `_protect`, the import pins, and the watch states, under one mutex. 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. Duplicate and 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 changes take the whole native range at once: it is checked first against every page's Mach maximum protection, then changed with one call; on failure the watch states are restored. - Watches: one state byte per host page. The fault handler takes no lock and allocates nothing; a page made writable while a watch fault is pending keeps that fault acknowledged instead of rearming it, and read-only or no-access stays closed. - runtime: `host/posix/darwin_mem.c`/`.h`: `darwin_mem_query` (Mach regions, holes and real protections), still stateless and shared by both images. - runtime: `host/posix/win32_posix.c`: each copy finds the owner's commit, discard and protect once with `dlsym(RTLD_MAIN_ONLY, ...)`, and refuses guest changes without them. A reservation or release inside the window is refused. `VirtualQuery` reads the Mach regions; `VirtualProtect` returns the real previous protection. Host-only reservations record their host-rounded size and fail when the table is full. - runtime: `host/hos_mem.c`: the address-region and page frees zero or discard under the allocator's lock, so the range is 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=1` still clears them). `hos_region_alloc` refuses a bad size or alignment, and a failed bookkeeping `realloc` is fatal. `hos_mark_written` clamps a length past the window. - runtime: `host/mkw_glue.c`: a failed commit is fatal (#324's `ERROR_COMMITMENT_LIMIT` wait stays, unchanged, in the `#else`). `host/hos_svc.c`: `MapPhysicalMemory` refuses a range outside the window; `UnmapPhysicalMemory` frees through the owner (pins included) instead of asking the renderer. `host/hos_main.c`: the window is reserved through the owner. - runtime: `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. - runtime: `host/rx_backend.c`: the dirty-page scan reads the marks with byte atomics at every step (256, 32, 8), an empty 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 zeroed guest memory (`host_guest_alloc`) is marked written. - tests: - `tests/test_darwin_guest_memory.py` with `tests/darwin_guest_memory_test.c` (macOS only): compiles the owner, the shared primitives and the real callers cut from `hos_main.c`, `hos_mem.c`, `hos_svc.c`, `mkw_glue.c`, `nvn_native.c`, `rx_backend.c` and `win32_posix.c` (its exception dispatch included), loads two copies of the shim as 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-only` and `--watch-protection-only` run parts. - `tests/test_collect_wide.py`: the fixture compiles the scan the platform builds (byte atomics on macOS, `main`'s word loads elsewhere). - docs: `docs/development.md`, "macOS guest-memory groundwork". **Rebase onto `5e072fc8`:** `a3f3001d` met #324's page accounting (below) and #324's commit-limit wait, and `test_collect_wide.py`, whose old base had #454's `#undef memcpy` (not on `main`). `3366b61b` and `647e90e4` are new: the Apple-only gate (the owner's decision for this PR) and the docs. **#324's accounting, kept:** `AllocateMemoryPages` counts the pages before the Apple path, as on every platform. On macOS `FreeMemoryPages` un-counts them under the same lock as the discard, and only once it has succeeded: a failed free (0xCA01) leaves its pages counted, so the count no longer errs low there (#478's reviewer note). ## Flags and settings | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | `HOS_ZERO_ALL_PAGES=1` (macOS: changed) | A page allocation zeroes the whole range | unset: pages in use keep their bytes, at any address | Leave unset | macOS | Windows, Linux: unchanged (the used-page bitmap below 4 GiB) | | | `RX_NVN_WATCH_TEXELS_SRC=<w>x<h>`, `RX_NVN_WATCH_AFTER`, `RX_NVN_WATCH_ALL` (macOS: work now) | The debugging write watch, on whole host pages, logged by the rearm thread with the fault's PC | unset | Debugging only | macOS | Windows, Linux: unchanged | A game run | | Test: `--arch` (repeatable), `--source`, `--out`, `--cc`, `--behavior-only`, `--watch-protection-only` | Routes; another tree; keep the fixture; the compiler; part of the run | native only | | the test | Not a game switch | | No new switch. The three watch switches are `main`'s debugging switches, described where `nvn_native.c` reads them and not in `docs/development.md`'s list on `main`; this PR names `RX_NVN_WATCH_TEXELS_SRC` in the guest-memory section and leaves 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` @ `647e90e450e8e848b6c35c6dc06a884db29dbb02` Hardware: 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 `fix` row 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 own `tests/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 define `memcpy` as a macro, as glibc does not): the scan Windows and Linux compile, 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`, and `python -I -B tests/test_collect_wide.py --cc clang`. | test (round 1 / round 2) | main @ 5e072fc8 | dependencies @ aeb30801 | this PR @ 647e90e4 | |---|---|---|---| | guest memory, arm64 and Rosetta | stops: no `host/posix/darwin_mem.c` / the same | stops: no `host/posix/darwin_guest_mem.c` / the same | exit 0 / exit 0 | | `test_collect_wide.py`, macOS | fixture does not compile (`'memcpy' macro redefined`, the SDK's) / the same | the same / the same | the same pages 256 marks a step as 32 / the same | | `test_collect_wide.py`, `__APPLE__` undefined | the same pages / the same | the same pages / the same | the same pages / the same | ``` this PR @ 647e90e4, round 1 (round 2 the same apart from the resident-memory figures): PIN_LIFETIME duplicate=1 overlap=1 retired_reference=1 full_padding=1 shared_alias_preserved=1 final_unpin_allows_remap=1 CONCURRENT_FREE region_and_page_apis=2 deterministic_latch=1 ownership_held_during_discard=1 reallocated_bytes_preserved=1 CONCURRENT_PIN deterministic_latch=1 pin_waits_for_remap=1 imported_backing_is_new_generation=1 subsequent_free_keeps_backing=1 WATCH host_page=16384 subpages=4 real_faults=4 dirty_granule=4096 neighbor_bytes=preserved restore_error=refused rearm_error=refused PENDING_WATCH real_vectored_dispatch=1 pending_before_cancel=1 handlers_retained=1 next_real_fault=1 restrictive_controls=3 no_widening=1 PROTECT_FAILURE phase=preflight native_calls=0 error=access_denied prefix_bytes=preserved prefix_protections=preserved watch_states=preserved real_fault_recovery=2 PROTECT_FAILURE phase=after_preflight native_calls=1 error=access_denied prefix_bytes=preserved prefix_protections=preserved watch_states=preserved real_fault_recovery=2 DIRTY concurrent_published=400 acknowledged=400 wide_boundary=255/256 empty_range_preserves=1 overflow_clamped=1 FAILURE_PATHS overflow_unaligned_hole_readonly=refused free_svc_result=0xca01 region_fatal_exit=23 RECLAIM bytes=134217728 rss_before=136888320 rss_after=2719744 drop=134168576 cost_us=410.000 WATCH host_page=4096 subpages=1 real_faults=1 dirty_granule=4096 neighbor_bytes=preserved restore_error=refused rearm_error=refused (x86_64 under Rosetta: the same lines, 4 KiB host pages) PASS: Darwin guest memory: arm64, x86_64 ``` (`mem: cannot discard guest region at 0x7ffffff000` in 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.sh` in the port's notes): for each `host/` file this branch changes, `unifdef -U__APPLE__` (only `__APPLE__` resolved; every other conditional, comment and blank line kept) of `main` and 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 -E` for `x86_64-unknown-linux-gnu` with `-D_GNU_SOURCE` and for `x86_64-w64-windows-gnu`, `-DA64_WRITE_TRACK -DA64_POLLING`, empty system-header stand-ins, line markers and whitespace dropped) are identical too: 7 `.c` files × 2 targets, 14 of 14. The new `darwin_*` files are not in the Windows or Linux build. **Earlier, on the old base** (`a3f3001d` and `27719a49` on the port's branch; not rerun here): the same test's runs, and `test_collect_wide.py` passing with the byte-atomic scan. The two defects `27719a49` repairs (a pending fault after 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 with `main` on the same Mac (the runner labels macOS `linux`): ``` main @ 5e072fc8: 525 passed, 19 failed, 17 skipped in 177 s (linux, 6 at once) this PR @ 647e90e4: 528 passed, 18 failed, 17 skipped in 168 s (linux, 6 at once) ``` - **By test name:** `tests/test_darwin_guest_memory.py` passes (new to `main`), and so does #436's `tests/test_darwin_fault_context.py`, which this branch carries; `tests/test_collect_wide.py` goes from FAIL on `main` (the fixture's `memcpy` macro meets the SDK's) to pass. Nothing else changed status. - **The 18 remaining failures also fail on `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. - No crash report was born during the run. **Not tested:** - **Windows and Linux.** Nothing was built or run there. They compile `main`'s code (the identity check above), and `test_collect_wide.py`'s Windows/Linux branch was built and run on this Mac with `__APPLE__` undefined, which is not a Linux build. - **The game on macOS, and a GPU import.** `main` does not build for macOS yet (#420); the renderer's pins are the next PR's. No import was pinned by a renderer here; the pin tests use real shared Mach aliases. - **Timing.** The frees now discard under the allocator's lock on macOS; not measured in a game. - **macOS 14.4** and an Intel Mac. Built for 14.4, run on 26.7.1 on Apple silicon only. **Needs other testers:** - Windows (llvm-mingw) and Linux: build the host and run `tests/run_all.py`, expecting the same results as on `main` (request to open). Issue #432's measurements are separate. - macOS 14.4 on Apple silicon: `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 - **Windows and Linux: none.** They compile `main`'s code, as shown above. No timing was needed for that reason; the every-platform version waits for #432's measurements. - macOS: frees zero or discard under the allocator's lock (they were outside it); the time a free holds the lock is the time its discard takes. A failed commit, a failed region discard and a failed scan allocation are now fatal instead of logged or ignored. - **GPU imports on macOS need the renderer's pins.** Frees no longer ask the renderer whether a range is imported; the next PR in this wave (`fix(macos): pin Vulkan host imports until their last owner, and unwind failed startups`) pins every import until its last owner is gone. Until it lands, a free under an import would replace the backing. macOS does not build from `main` until the port's other PRs land, so no player can meet this; merge the two back to back. - Protection on macOS is per host page (16 KiB native), not per 4 KiB guest page. - No file hashed into `LONGSHOT_SHADER_STAMP` (`nx2/build.py:1176-1182`) changes; no renderer ABI field changes. - Rollback: revert the PR. ## Reviewer notes - Start with `darwin_guest_mem.c` (`hos_posix_guest_discard`, `_protect`, `_import_pin`, `_watch_fault`), then the Apple branches of `hos_region_free` and `FreeMemoryPages` in `hos_mem.c`. - Commits: `faea2408` and `37901de8` are `a3f3001d` and `27719a49` from the port's branch, with #324's conflicts resolved and the `Assisted-by` / `Co-Authored-By` trailers of this rebase added. `3366b61b` is new: it puts every change the first two made outside the Apple path back under `__APPLE__` (the scan's byte atomics, the zeroed memory marked written, the fatal allocation failures, `hos_region_alloc`'s checks, `hos_mark_written`'s clamp, `VirtualQuery`'s pointer check and `last_error`). `647e90e4` is the docs. - `darwin_guest_mem.c` must be linked into the executable only. The build PR of this wave (#479) has it in its executable-only list; this PR adds no build-script change. - Merging with other PRs of the series: `test_collect_wide.py` meets #454's `#undef memcpy` (keep this PR's text: the macOS branch no longer defines that macro); `win32_posix.c`'s Apple include block meets #438 (keep every include); and `docs/development.md` meets #477 (both add a section after "Log levels": keep both). - Against current `main` (`dc390e59`, `git merge-tree`): no conflict. Main has since changed five files this PR's test cuts source from (`hos_main.c`, `hos_svc.c`, `mkw_glue.c`, `rx_backend.c`, `win32_posix.c`), so the test runs again in the rebase; on the merged tree the Windows/Linux identity above still holds against `dc390e59` (text 9/9, tokens 14/14). #370 changes no claim here: this PR edits no file the stamp hashes, before or after #370. - Written from Apple's SDK headers and manuals (`mach_vm.h`, `mach/vm_region.h`, `sys/mman.h`, `sigaction(2)`); nothing copied. ## Issues See #420 (the macOS port). The every-platform version: #432. ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [ ] Rebased on current `main`. Branched from 5e072fc8, on #478 and #436, which it depends on; `main` has moved since (it merges with `dc390e59` without a conflict, under Reviewer notes). - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). On macOS: 528 passed, 17 skipped, and 18 failures that also fail on `main`, named under Testing. - [x] Tested in the game, or what wasn't is named under Testing. (No game run; named under Testing.) - [ ] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). The test requests are still to open. - [x] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. (For a `fix`: the test fails on `main` and passes here, 2 interleaved rounds, both routes.) - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] A game-free test added for the fix, failing on `main` and passing here. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [ ] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. Not built there; the preprocessed Windows and Linux sources equal `main`'s. Requested under Needs other testers. - [x] Docs updated.
Reserve the fixed guest window with a non-overwriting Mach mapping. Reclaim
complete owned host pages with fresh anonymous mappings and zero only the
partial-page edges, preserving adjacent 4 KiB guest pages on 16 KiB hosts.
Use the actual host granule for non-guest allocation/protection and report
macOS discard failures instead of returning success.

Add game-free source-linked arm64/Rosetta tests covering collisions, repeated
free/recommit, unaligned ranges, failure paths and resident-memory reduction.
Keep Linux and Windows paths unchanged. Build-list integration is separate.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep the POSIX layer's outward 4 KiB MEM_DECOMMIT and inward 4 KiB MEM_RESET
contracts above the byte-exact Darwin helper. Check arithmetic before rounding,
and keep separate real-memory assertions for helper and wrapper ranges.

Default the Darwin regression test to the native architecture. Both CPU routes
remain explicitly selectable for acceptance without requiring Rosetta or arm64
execution from every ordinary macOS suite run.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Decode Darwin signal contexts and access faults for arm64 and x86_64,
copy supported context edits back on signal return, and prevent recursive
callback dispatch when a fault handler itself faults. Keep the non-Apple
bridge text and the x64-shaped CONTEXT layout unchanged.

Add source-linked, game-free owned-memory fault tests for both targets,
including page repair, PC redirection, prior handlers and an inaccessible
handler stack. Document the native PC/SP contract and remaining limits.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
test(runtime): keep Darwin fault checks native by default
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
PR description / Description follows the template (pull_request_target) Successful in 5s
CI / Workflow lint (actionlint) (pull_request) Successful in 7s
Third-party parts / As their notes say (pull_request) Successful in 11s
Website / Website on Cloudflare (pull_request_target) Has been skipped
7731dc0c18
Do not make the ordinary Apple Silicon suite depend on Rosetta. Default
to the running Python process's architecture and retain explicit repeated
--arch options for dual-route qualification. Document that command and
count only files when reporting crash-report directory changes.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
test(runtime): cut main's int hos_commit in the Darwin guest-memory test
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 5s
CI / Conventional Commit title (pull_request) Successful in 5s
CI / Workflow lint (actionlint) (pull_request) Successful in 9s
Third-party parts / As their notes say (pull_request) Successful in 15s
Website / Website on Cloudflare (pull_request_target) Has been skipped
9c6791454f
Main's #324 (a4c77174) made hos_commit in host/mkw_glue.c return whether
the range was committed, so its definition now starts "int hos_commit(".
The Darwin guest-memory test cut the function by its old "void" head and
stopped with a ValueError on main's source, on this branch and on main alike,
before compiling anything. Cut it by the new head.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This PR needs the macOS fault-context PR (#436); its commits are kept as they are.
The one conflict, host/posix/win32_posix.c's Apple include block, keeps both
darwin_mem.h (the guest-memory PR's) and darwin_fault.h (#436's).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep address-region and page frees owned until discard or in-place zeroing
finishes. Route guest mutations from each image to one executable-owned
registry, and pin complete imported backing through duplicate and overlapping
GPU memory lifetimes without querying the renderer under ownership locks.

Use real Mach query ranges and protections, preserve byte-exact 4 KiB guest
ownership and 64 KiB commit semantics, and handle all native watch subpages
with checked fault recovery and conservative dirty coverage. Replace racy
bulk dirty-byte reads with same-granule atomics.

Add game-free native arm64/Rosetta fixtures for two-image dispatch, actual
faults, aliases, failures, and deterministic free/reallocate/import races.
Carry prior RSS evidence rather than repeat unchanged experiments.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep a cancelled-writable watch state so an already pending diagnostic fault
is acknowledged without rearming or changing permissions. Check the real
mapping before acknowledgement; intentional read-only/no-access stays closed.

Freeze affected watch pages once, preflight the complete native range against
Mach maximum permissions, and issue one protection operation. Preserve prior
watch states when either preflight or the native operation fails.

Exercise the real Win32 vectored dispatcher, signal-handler retention and the
next genuine watch fault. Cover permission failures both before preflight and
after a deterministic barrier, with real Mach maximum-protection changes and
no manufactured VM results, on native arm64 and Rosetta.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The two commits before this one also changed Windows and Linux: the
dirty-page scan read its marks with byte atomics, the renderer's zeroed
guest memory was marked written, an empty range consumed no mark,
allocation failures in the scan and in the address-region bookkeeping
were fatal, hos_region_alloc refused a bad size or alignment,
hos_mark_written clamped its length, and VirtualQuery checked its
pointer and set last_error. Nobody has timed the scan there, so each of
these is now inside #if defined(__APPLE__), with main's code in the
#else. unifdef -U__APPLE__ of every host/ file this branch changes
equals main's byte for byte, and so do their preprocessed Linux and
Windows token streams. Issue #432 is the version for every platform.

test_collect_wide's fixture compiles the scan the platform builds: byte
atomics, and an empty range consuming nothing, on macOS; main's word
loads elsewhere.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs(macos): describe the guest window's one owner, import pins and native-page watches
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 5s
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 8s
Website / Website on Cloudflare (pull_request_target) Has been skipped
647e90e450
The guest-memory section still described the groundwork alone: it said
the write watch and VirtualQuery were separate work and that the
region allocator's unlock-before-discard race remained. Describe the
executable's single owner and how each image reaches it, frees that
keep their range until the bytes are gone, import pins, the real
VirtualQuery and VirtualProtect, the native-page watch and the atomic
dirty scan, that all of it is macOS only (issue #432), and the test's
two narrower modes.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 5s
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 8s
Website / Website on Cloudflare (pull_request_target) Has been skipped
This pull request is marked as a work in progress.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'mac/guest-16k:mooki-mac/guest-16k'
git switch 'mooki-mac/guest-16k'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!480
No description provided.