Skip to content

feat(libc): write watch on macOS hosts - #1686

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
Cloud-Multiplayer:feat/macos-write-watch
Oct 10, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
Cloud-Multiplayer:feat/macos-write-watch

Conversation

@Cloud-Multiplayer

Copy link
Copy Markdown
Contributor

What

A GuestWriteWatch backend for macOS hosts, as discussed in #1.

macOS has no counterpart to userfaultfd write-protect or MEM_WRITE_WATCH, so there is no backend today and the driver re-reads every guest buffer on each use. The backend write-protects watched pages: the first write to a page faults, a SIGBUS/SIGSEGV handler records the page and restores its protection, and Collect reports the recorded pages and protects them again. A page is protected before its record is cleared, under the lock the handler takes, so a write that lands in between is recorded.

Because the protection is real, three entry points are added (no-ops on Linux and Windows):

  • GuestWriteWatchBeginHostWrite / EndHostWrite lift the protection around a system call that writes guest memory, which would otherwise fail with EFAULT. GuestArena::HostWrite calls them, next to the Windows equivalent, so the existing file-read sites are covered.
  • GuestWriteWatchProtectionChanged tells the watch the protection given to watched pages by mprotect or by a new mapping.
  • GuestWriteWatchProtection reports that protection, since a watched page is read-only between a collection and the next write.

Nothing in main calls the last two yet, because the macOS memory paths are not in the tree. They are meant for the mprotect/mmap/munmap sites in libkernel's DirectMemory and for the mach_vm_region branch of GuestMemory in the macOS port. This and the other limits are recorded in TechnicalDebt.

Two details that came from running titles rather than from design:

  • Register keeps the recorded protection of a page it already watches. Read from the kernel it would be the watch's own read-only protection, and the page would never be watched again. The driver registers buffers repeatedly, so this showed quickly as stale vertex data.
  • The lock hands out turns in order. With a test-and-set lock, a thread collecting in a loop kept winning it and writers waiting in the handler starved: the four-thread test took 119 s under Rosetta, and takes 1.4 s with turns.

Tested

  • macOS 27.0.1, Apple M3 Pro, AppleClang 17. The new tests (collect, register, host, protection, threads) pass natively (arm64) and as x86-64 through Rosetta, three repetitions each. They build without submodules or the prx libraries; docs/dev/BUILD.md has the commands, and a write-watch job runs them on macos-15-intel and macos-15.
  • One tracked write costs about 19 µs under Rosetta and 4 µs natively on this machine, close to a bare mprotect plus signal (21 µs and 3 µs).
  • The same code runs two commercial titles on my macOS branch (a loader-based port, not this tree) at a 16.6 ms median flip interval; without a write watch the first of them ran at roughly 20 FPS.
  • Linux and Windows: not built here beyond a MinGW syntax check of the two changed files. The Linux and Windows backends are untouched; the new functions are no-ops there.
  • tools/check_conventions.py --base origin/main passes.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed in TechnicalDebt
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source (none added)
  • Generic behaviour, not specific to one title
  • Depends on: none
  • AI-assisted: yes

🤖 Generated with Claude Code

macOS has no counterpart to userfaultfd write-protect or
MEM_WRITE_WATCH, so GuestWriteWatch had no backend there and the driver
re-read every guest buffer on each use.

The macOS backend write-protects watched pages. The first write to a
page faults, the SIGBUS/SIGSEGV handler records the page and restores
its protection, and Collect reports the recorded pages and protects
them again. A page is protected before its record is cleared, under the
lock the handler takes, so a write that lands in between is recorded.
The lock hands out turns in order: an unfair one starved writers under
Rosetta.

The protection is real, which adds three entry points:
- GuestWriteWatchBeginHostWrite/EndHostWrite lift it around a system
  call that writes guest memory, which would otherwise fail with
  EFAULT. GuestArena::HostWrite calls them, as it calls the Windows
  equivalent.
- GuestWriteWatchProtectionChanged tells the watch the protection given
  to watched pages by mprotect or a new mapping.
- GuestWriteWatchProtection reports that protection, since a watched
  page is read-only between a collection and the next write.

Register keeps the recorded protection of a page it already watches:
read from the kernel it would be the watch's own read-only protection,
and the page would never be watched again.

The tests build without the prx libraries and run natively and through
Rosetta.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 10, 2026
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
#921 docs/dev/BUILD.md L76
#1960 core/libs/prx/libc/include/GuestWriteWatch.hpp L15
core/libs/prx/libc/src/GuestWriteWatch.cpp L308

@github-actions github-actions Bot mentioned this pull request Oct 10, 2026
9 tasks done
@adriadam10

Copy link
Copy Markdown
Collaborator

Read the full diff. The new code is all under __APPLE__; Linux and Windows only get the no-op entry points, and the local Linux build and ctest of libs and libc are green. I can't run macOS here, so the five write_watch tests rely on the new write-watch job (macos-15 and macos-15-intel). TechnicalDebt entry and BUILD.md match the code.

Note for later: #1960 also edits GuestWriteWatch.hpp/.cpp and conflicts with this PR, so whichever lands second needs a rebase.

Merging once CI is green.

@adriadam10
adriadam10 merged commit e0526f5 into boykopovar:main Oct 10, 2026
12 checks passed
SleglJan pushed a commit to SleglJan/AnyPS5 that referenced this pull request Oct 10, 2026
…write-watch

feat(libc): write watch on macOS hosts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants