Skip to content

fix(libc): track writes to singly mapped direct memory without faults on Windows - #5596

Merged
oneandonlydean merged 4 commits into
boykopovar:mainfrom
tibzejoker:port/pr-637-main
Oct 14, 2026
Merged

oneandonlydean merged 4 commits into
boykopovar:mainfrom
tibzejoker:port/pr-637-main

Conversation

@tibzejoker

Copy link
Copy Markdown
Contributor

What

Port of @DotDebian's #637 onto current main, as asked on #637. DotDebian stays the author of the main commit, and our changes are separate commits.

On Windows, AnyPS5 tracked CPU writes to direct memory by arming its section views read-only and taking a write fault. The exception frame Windows builds for such a fault, with live AVX state, overwrites the System V red zone below RSP, so guest leaf functions that keep values there read back garbage (#361). GTA V's zlib inflate_fast was one of them.

With this change, a direct memory page mapped at a single guest address is private MEM_WRITE_WATCH memory at that address, and its writes are collected with GetWriteWatch, like flexible memory: no fault. A page mapped at a second address moves its whole resident run into the section behind a read-only fence while it is copied, and every address then becomes a view, tracked by write protection as before.

Commits:

  • fix(libc): track writes to singly mapped direct memory without faults on Windows (author DotDebian): fix(libc): track writes to singly mapped direct memory without faults on Windows #637's two commits squashed and merged by hand onto main's 4 KiB subpage tracking of shared views. Resident runs keep a collected bit per page and enter the fresh ranges, so Fresh and Armed collections select them like shared views. A demoted run hands its collected bits to the new views and reports every page once. The fixes from the 2026-10-07 review are kept: fenced copies in demote and evict, demoting the whole run, unmapped pages kept out of the section, and the watchResident guard in Collect.
  • fix(libc): keep resident mappings to direct memory backings on Windows: only direct memory goes resident, through GuestArenaMapDirect. Other single-mapped sections (file-backed JIT and shm memory, whose aliases use duplicated handles) keep main's shared-view path; making them resident broke guest_jit_shared_memory.
  • test(libc): require resident direct memory in the resident run tests: CheckBreakingAResidentRunKeepsConcurrentAccesses and CheckRemappedDirectMemoryIsChargedOnce now check that the first mapping is MEM_PRIVATE, so they fail on main instead of passing there.
  • docs(libc): move the Windows direct memory debt entry to libc.md.

The 2026-10-11 review points:

  • MapAlias throws through refuse(...) when a resident run extends outside the aliased range. importAllocation keeps main's caller and fallback, with no entry.alias == nullptr branch.
  • tests/Recorder.cpp keeps main's switches and adds --host-import-second-mapping-only.
  • The two tests that passed on main now fail there, as above.

Tested

Windows 11, i9-13900K, RTX 4090, MinGW-w64 GCC 15.2.0 (WinLibs r7), Release, BUILD_TESTING=ON, on main ce5c839.

Not tested on Linux: the change is in the Windows mapping code.

Checklist

🤖 Generated with Claude Code

https://claude.ai/code/session_01V1W4jdNSKTV7xwuS6xQPJV

DotDebian and others added 4 commits October 14, 2026 02:39
… on Windows

A direct memory page mapped at one guest address now lives in private
MEM_WRITE_WATCH memory at that address, and its writes are collected with
GetWriteWatch like flexible memory: no fault is raised. A page mapped at a
second address moves its whole resident run into the section, behind a
read-only fence while it is copied, and every address becomes a view of it,
tracked by write protection. Unmapped resident pages that are not blank are
kept in a private reservation until they are mapped again, not in the
section.

Ported onto main's 4 KiB subpage tracking of shared views: resident runs
keep a per-page collected bit and enter the fresh ranges, so Fresh and
Armed collections select them like shared views; a demoted run carries its
collected bits into the views and reports every page once. MapAlias throws
through refuse() when a resident run extends outside the aliased range, and
the caller keeps main's fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1W4jdNSKTV7xwuS6xQPJV
Map made every singly mapped section page private resident memory. A JIT
or shm_open section is file backed and committed: its bytes live in the
file, and its alias descriptors map duplicated handles, which the physical
page slots do not match, so a resident copy diverged from the file, and
committing its view on a demote failed with access denied
(guest_jit_shared_memory). Only direct memory backings, mapped through the
new GuestArenaMapDirect, now become resident; other sections keep main's
shared views. Shared pages are committed only while their section page is
reserved, as on main.

The socket host write tests map their direct memory a second time, so the
write-protected shared path they check is still the one they exercise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1W4jdNSKTV7xwuS6xQPJV
CheckBreakingAResidentRunKeepsConcurrentAccesses and
CheckRemappedDirectMemoryIsChargedOnce passed on main, where a singly
mapped direct memory page is a shared view and no resident run exists.
Both now require their first mapping to be private committed memory, so
on main they fail there instead of passing without exercising a run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1W4jdNSKTV7xwuS6xQPJV
The entry links WindowsMappings.hpp, so the debt-file convention puts it in
docs/dev/debt/libc.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1W4jdNSKTV7xwuS6xQPJV
@github-actions

github-actions Bot commented Oct 14, 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
#3864 core/libs/prx/libc/include/WindowsMappings.hpp L263-264, L366-367, L410
#3965 docs/dev/debt/libc.md L30
#5062 core/libs/prx/libc/include/WindowsMappings.hpp L77-78, L263-264, L366-367, L596-597
#5599 docs/dev/debt/libc.md L30

@oneandonlydean

Copy link
Copy Markdown
Collaborator

@tibzejoker thanks, this covers the 10-11 points. Since it changes the Windows memory model, could you run GTA V (PPSA04264) on this head alone on top of main and post the result? The 150 s run in #5024 was on a combined stack.

@tibzejoker

Copy link
Copy Markdown
Contributor Author

@oneandonlydean GTA V (PPSA04264) on this head alone. Windows 11, i9-13900K, RTX 4090 (NVIDIA 616.92), MinGW-w64 GCC 15.2.0, Release. Write watch on, no other PR and no local change, one run each:

So, on its own, this head removes the write-watch crash and nothing new shows up before the next known stop. With #448 (fdfawef324's port) on top, the Story prologue ran for 150 s with correct rendering, as posted in #5024.

AI-assisted (Claude Code).

@oneandonlydean
oneandonlydean merged commit 3e4fe4c into boykopovar:main Oct 14, 2026
15 checks passed
oneandonlydean added a commit that referenced this pull request Oct 14, 2026
…range leaves the write watch

On Windows, singly mapped direct memory is private since #5596, so its host
import is a direct import that leaves the write watch instead of a shared
alias that stays watched. A storage image whose results were pending when
its memory was imported had no saved guest bytes, and storing them threw.

GuestMemory now calls a hook before Unwatch and before an ImportWatched
that does not stay watched. Every image with results pending over the
range saves the guest bytes of its pending units that have none, then
collects and records the pieces the keep rule leaves to the CPU for the
writes the watch saw. The next store and Refresh leave those pieces to the
CPU and compare the rest with the saved bytes. A map-time import made
without the GPU lock is deferred to the locked retry when pending results
overlap the range.
@tibzejoker

Copy link
Copy Markdown
Contributor Author

A correction to my run above: "removes the write-watch crash" was based on one run per tree. Since then I've run GTA more often with this change in (main ee9037d + #757 and the other Story PRs, write watch on). In 1 run of 6, the same crash came back 15 s into Story: [RAGE] HDD Streamer, a null write at eboot 0x28aacee. The other 5 ran 60-180 s without it. Without this change, both of my runs crashed there.

So this change makes the crash rare in GTA, but it doesn't remove it. One possible reason is that only direct memory goes resident, so writes to other shared views can still fault. I haven't traced that one crash, so I can't tell whether it was red-zone damage or something else at the same site.

AI-assisted (Claude Code).

@EagleBearerYS

Copy link
Copy Markdown
Contributor

Resident Evil Village (PPSA01556, 01.210.000) aborts at boot since this PR, 19 s in and before any dialog:

FATAL: an address-based build copies 419 MiB of 1 registered ranges, past 64 MiB (APS5_ADDRESS_COPY_MAX_MIB); the largest: 0x260400000+0x1a300000 (419.0 MiB committed, outside the write-watched arena)

RTX 3070 Laptop GPU (8 GB), driver 617.42, Windows 11 Home 10.0.26300, Ryzen 9 5900HS, 15.4 GB RAM. main 0b6d033, relinked with main's relinker. Plain main can't boot Village here for an unrelated reason: it prepares every shader at registration, and after 10 minutes private memory was at 21.9 GB with 6.8 GB of commit free. So this run adds two local commits that prepare shaders at their first draw (APS5_RE8_LAZY_PREPARE=1, APS5_NO_GPL=1). They don't touch the import or mirror paths.

That range is a 419 MiB direct-memory heap mapped once. Before this PR it was shared views, served by a heap mirror, and Village booted to gameplay on our older branch. With this PR it's resident private memory. The address-based build's 64 MiB window import leaves the write watch for private memory, and the heap mirror refuses ranges with unwatched blocks, so the whole range would be copied on each build, past the 64 MiB guard. Raising the guard (APS5_ADDRESS_COPY_MAX_MIB=512) gets past it.

#5688 fixes it: heap mirrors keep such ranges and refresh the blocks outside the write watch. With it the abort is gone and the range is host-imported:

[gpu] host import of 0x260400000+0x1a300000 ok (3444 live, 5377.7 MiB live, 6511.3 MiB ever)

adriadam10 pushed a commit to Pipodi/AnyPS5 that referenced this pull request Oct 14, 2026
…indows

Since boykopovar#5596 a direct memory page mapped at one guest address is private
write-watched memory. The AGC driver imported such ranges as plain
read-write memory, without an alias, and Windows host imports without an
alias leave guest write tracking: every use of the range was compared and
refilled on the CPU. A range that holds resident direct memory now takes the
alias path: MapAlias moves every resident run the range touches into the
section (as a second mapping does) instead of refusing runs that extend past
the range, and the import stays write-tracked through the guest views.
Direct memory the GPU never imports keeps fault-free write watching.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
poonasor pushed a commit to poonasor/AnyPS5 that referenced this pull request Oct 15, 2026
…indows

Since boykopovar#5596 a direct memory page mapped at one guest address is private
write-watched memory. The AGC driver imported such ranges as plain
read-write memory, without an alias, and Windows host imports without an
alias leave guest write tracking: every use of the range was compared and
refilled on the CPU. A range that holds resident direct memory now takes the
alias path: MapAlias moves every resident run the range touches into the
section (as a second mapping does) instead of refusing runs that extend past
the range, and the import stays write-tracked through the guest views.
Direct memory the GPU never imports keeps fault-free write watching.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
poonasor pushed a commit to poonasor/AnyPS5 that referenced this pull request Oct 15, 2026
…indows

Since boykopovar#5596 a direct memory page mapped at one guest address is private
write-watched memory. The AGC driver imported such ranges as plain
read-write memory, without an alias, and Windows host imports without an
alias leave guest write tracking: every use of the range was compared and
refilled on the CPU. A range that holds resident direct memory now takes the
alias path: MapAlias moves every resident run the range touches into the
section (as a second mapping does) instead of refusing runs that extend past
the range, and the import stays write-tracked through the guest views.
Direct memory the GPU never imports keeps fault-free write watching.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SoRadGaming added a commit to SoRadGaming/AnyPS5 that referenced this pull request Oct 19, 2026
Linux widens a collect of at most one 64 KiB block to the whole block within an epoch (boykopovar#1310), so
the memo serves the block's other ranges. Windows kept the requested range because it was not
tested there. On Windows with boykopovar#5596's resident direct memory every walk is a GetWriteWatch call,
and ASTRO BOT's hub walks about 50k ranges of at most 64 KiB per 10 s on queue 0.

Windows coverage does not know which pages of a block are committed or inaccessible, and a walk
refuses such a range before it collects anything. So a widened walk that fails is retried over the
requested pages; without that, a collect beside uncommitted pages of its block fails.

agc_driver_image_table_reuse expected one more table resolution on Windows than on Linux, from
the narrower collects; it now expects Linux's count on both.

The new uncommitted-pages test resets its range before releasing it, so the arena does not hand
the next test an address inside memory that is still committed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

Windows: resuming a write fault overwrites the guest red zone when AVX state is live

4 participants