Skip to content

fix(libkernel): map direct memory as write-watched private pages until it is aliased - #2054

Closed
esatturan wants to merge 1 commit into
boykopovar:mainfrom
esatturan:fix/direct-memory-write-watch
Closed

esatturan wants to merge 1 commit into
boykopovar:mainfrom
esatturan:fix/direct-memory-write-watch

Conversation

@esatturan

@esatturan esatturan commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

What

On Windows, direct memory was always mapped as views of a section. Views have no page write watch, so the GPU driver tracked CPU writes to them by arming pages read-only and taking a write fault. With live AVX state, the exception frame Windows builds for such a fault overwrites the stack from RSP-0x20 down: most of the System V red zone, where guest leaf functions keep live values. Measured on Windows 11 with AVX2; without live AVX state the frame starts at RSP-0x120.

PPSA29141 took about 5,000 such faults a second and crashed in roughly a quarter of 90-second runs, in a Unity job, on a red-zone slot read back as 0.

  • A physical page mapped at one address is now private MEM_WRITE_WATCH memory, one allocation per mapping, with the section as its store.
    • Its content goes to the section when the mapping ends while the page stays allocated, and comes back when it is mapped again.
    • A second mapping turns the private one into views first.
    • Unmapping part of a private mapping commits the kept parts again as their own allocations, with their content and protections.
    • During either switch, accesses to the range wait in the vectored handler (GuestArenaAwaitRetarget) and retry.
  • WindowsMappings::Collect finds Commit's write-watched allocations without VirtualQuery.
  • HostWrite also refuses private pages the guest left read-only (reads into them return EFAULT).
  • Recorded in TechnicalDebt.

Tested

Windows 11, i7-13650HX, RTX 4070 Laptop, MinGW-w64 GCC 15.2.

  • guest_memory gains tests for: a page mapped once is private and never armed; content survives an unmap and remap; a second mapping converts to views while another thread keeps writing; a partial unmap while another thread writes. The writer waits are bounded (5 s) and guest_memory has a 20 s CTest timeout. The existing view tests now alias their pages.
  • Full ctest on current main (c6e3aa76) merged with this branch, the other LEGO Party! PRs and feat(libkernel): implement _umtx_op wait and wake operations #1482: 547/547 pass. On Windows, main's libs step currently fails with duplicate patched export name: LibwuIonIBw: feat(videoout): implement sceVideoOutGetResolutionStatus #1714 added APS5_EXPORTs for sceVideoOutAddVrrActiveStatusEvent and sceVideoOutAddVrrStatusFlagsPrivilege, whose names the NID table already maps. Those two lines were removed locally for the run.
  • PPSA29141: 0 write faults (was about 383,000 in 75 s); four two-minute runs and a five-minute run without a crash; frame rate unchanged (15-35 fps). Title runs were on the base these PRs had before review. PPSA29141 was converted with feat(relinker): add --module-dir for modules loaded at run time #846's --module-dir for its plugins, and feat(relinker): add --module-dir for modules loaded at run time #846 no longer merges with today's main, so the title was not converted and run again on it.

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
  • Generic behaviour, not specific to one title
  • Depends on: none
  • AI-assisted: yes (Claude Code)

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed the retargeting and direct-memory changes. One CI-blocking test issue: the new Windows-only writer tests have unbounded while (stores.load() < ...) std::this_thread::yield() waits (GuestMemory.cpp around lines 801/804 and 876/880). If a regression leaves a writer waiting in the AV handler, the test can hang indefinitely, contrary to CONTRIBUTING.md’s 30-second per-test limit. Please give guest_memory a hard CTest timeout below 30 seconds (and bound the progress waits where possible), so a regression fails the job instead of occupying a runner indefinitely. I have not approved this head; I could not independently run the Windows-only path here.

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One contribution-policy issue as well: this diff adds many explanatory comments in code (WindowsMappings.hpp, DirectMemory.cpp, CrashReport.cpp, and GuestMemory.cpp). docs/dev/CONVENTIONS.md allows code comments only for technical-debt notes, #endif, or namespace endings. The new TechnicalDebt entry is appropriate, but it does not make the implementation walkthroughs and test comments compliant. Please remove those comments or keep only comments that point to the specific documented debt; the PR checklist currently says there are no code comments, which does not match the diff.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Two things before this can go in:

  • Remove the explanatory comments (WindowsMappings.hpp, DirectMemory.cpp, CrashReport.cpp, GuestMemory.cpp) as MarcZX100 said; only technical-debt markers are allowed, and the TechnicalDebt entry already covers this. The bounded waits he asked for in the writer tests too.
  • Keep one mapping path: drop APS5_DIRECT_MEMORY_VIEWS. The APS5_TRACE_FAULTS counter and the crash-report register dump are separate topics; send them in their own PR if you want them.

It conflicts with main in TechnicalDebt now, so please rebase when you push.

@Mouhand-Kaddo

Mouhand-Kaddo commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Tested this PR on Arch Linux in WSL2 on Windows 11 (Intel Core i7-13700K), using GCC 16.2.1 20260810, CMake 4.4.4, Ninja, Python 3.14.7 and Mesa lavapipe (VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json). This is software Vulkan validation, not a native Linux hardware-GPU result.

I applied head 46d6acb8a641a17bd92b57ec4ccdb3427e397f22 to main 0fdda7be in an isolated native WSL checkout. There was a documentation-only conflict in TechnicalDebt, resolved by retaining both entries; the code was unchanged. Release, BUILD_TESTING=ON, SPIRV-Tools disabled; built the default target and explicitly built libs.

  • Full CTest, parallel 4, 30-second per-test timeout: 485 passed, 27 skipped, 1 timed out, 513 total (70.74 s). guest_memory passed. The skips are feature-dependent GPU tests on lavapipe.
  • agc_driver_flat_store timed out at 30 s; an isolated retry also timed out. I checked out unchanged main 0fdda7be, rebuilt both the default target and libs, and reran that test: the same timeout, with the batch still running at 25 s. This failure reproduces without this PR; I am not reporting a clean full suite.

Additional Windows integration check (WinLibs GCC 15.2.0 r7, Release with -g, SPIRV-Tools disabled): guest_memory, fiber, and windows_exceptions all passed, 3/3. That branch also includes #2310 and #1477, so this is an integration result rather than isolated Windows validation of this PR. A Demon's Souls dump still faults during startup with those changes; this is not a title compatibility success claim.

Validation performed with OpenAI Codex assistance. No game binaries or proprietary code are attached.

…l it is aliased

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@esatturan

Copy link
Copy Markdown
Contributor Author

Done, rebased on current main:

  • Removed the explanatory comments from WindowsMappings.hpp, DirectMemory.cpp, CrashReport.cpp and GuestMemory.cpp.
  • One mapping path: APS5_DIRECT_MEMORY_VIEWS is gone. APS5_TRACE_FAULTS and the crash-report stack and register dumps are out too, so CrashReport.cpp only adds the GuestArenaAwaitRetarget check now.
  • @MarcZX100's bounded waits: the writer tests wait at most 5 s for the writer's progress and fail through Require, and guest_memory has a 20 s CTest timeout.
  • The TechnicalDebt hunk carried other PRs' entries by mistake; it now holds only the direct-memory entry.

guest_memory, windows_exceptions, fiber and the full suite pass on Windows 11. @Mouhand-Kaddo, thanks for the Linux run.

@github-actions github-actions Bot added build needs rebase Conflicts with main, rebase needed labels Oct 9, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

Please rebase onto current main: core/libs/tests/GuestMemory.cpp conflicts after #2236 and #2328.

#2236 changed sceKernelReleaseDirectMemory and sceKernelCheckedReleaseDirectMemory to unmap the released memory's mappings after DirectMemoryFree. With a page mapped once now being private, check that this unmap doesn't write the page back into a section range that was just freed, and add a case that releases a page mapped once.

@TheAggie

Copy link
Copy Markdown
Contributor

@esatturan I retested the current head (630b18f) against the crash I described on #361 (RetroArch's Dolphin core, a red-zone clobber during write-watch faults on direct memory).

Startup works again. An earlier version of this PR stopped RetroArch at startup with a heap mirror's range is no longer write-watched (RADV's read+execute shader range). With 630b18f that's gone: Dolphin boots the GameCube 240p Suite and renders its menu. Three 150 s runs with shader compilation mode 0 had no crash.

The red-zone crash: I can't tell whether this fixes it. On my current base the adler32 crash no longer happens even without this PR. The crash used to need shader compilation mode 2. In that mode, all three runs with the PR and all three without it now stop at an unrelated recompiler error (GetImageResource dword 0 is not a valid runtime value), and none of them hit adler32 before that. So these runs don't show anything either way about the fix.

One test interaction with main. CheckFailedCollectKeepsWrites (added in bbdb35b, after this PR's base) fails with the PR merged:

Guest memory check failed at core/libs/tests/GuestMemory.cpp (the Require(!collect(mapped, page * 2, count)) after the sceKernelMprotect)

Its second half:

  • maps two pages of direct memory once;
  • writes to the first;
  • sets the second to PROT_NONE;
  • expects a reset collect over both pages to fail, keeping the write.

With this PR that mapping is a private write-watched allocation, and the collect over both pages succeeds. Without the PR, on the same build, the whole guest_memory test passes. Since a collect that fails is supposed to keep the writes for the next one, a no-access page in the range now gets the opposite result from before. If the private path is meant to tolerate that, the test should probably say so; if not, the private path may need the same range check.

How I ran it: guest_memory stops earlier on my machine at CheckGuestModuleImageProtection (a dlopen that fails here, on main too). I skipped that one check locally for both runs so the later checks would run. Your two new checks (CheckDirectMemoryMappedOnceIsPrivate, CheckPartialUnmapOfPrivateDirectMemory) run after CheckFailedCollectKeepsWrites, so I couldn't see their result.

Caveats:

  • Not current main: this is my integration branch, a main from 2026-10-07 plus my open PRs, feat(agc): vertex clip and cull distances #423 and a few local spikes. 630b18f was cherry-picked onto it, with conflicts in DirectMemory.cpp (static_cast<void>(start) next to your held.assign) and the GuestMemory.cpp test list, resolved by keeping both sides.
  • Setup: Windows 11, MinGW-w64 GCC 15.2.0, RTX 5070 Ti, Ryzen 7 7800X3D.

AI-assisted (Claude Code).

@Mouhand-Kaddo

Copy link
Copy Markdown
Contributor

Tested head 630b18f04 applied to current main 7159f7f34 (local validation commit 0c4ef6a09). The cherry-pick required two mechanical conflict resolutions: retain both constructor initializers and retain both main's CheckFailedCollectKeepsWrites call and this PR's added test calls.

Windows 11, MinGW-w64 GCC 15.2.0, Release:

  • windows_exceptions and fiber passed.
  • guest_memory failed at GuestMemory.cpp:1024, in CheckFailedCollectKeepsWrites: after mapping direct memory once, writing its first half, and changing the second half to no access, Require(!collect(mapped, page * 2, count)) fails. Collection returns success for the mixed-protection private mapping.
  • Rebuilt and ran guest_memory on unchanged main 7159f7f34: passed (1.29 s).

Arch Linux under WSL2, GCC 16.2.1, Release: guest_memory and fiber passed, 2/2. The Windows-only private-page checks are not exercised by that Linux result.

This is focused validation, not a full-suite or game compatibility claim. The new main regression appears to need reconciliation with private direct-memory collection before merging.

@Mouhand-Kaddo

Copy link
Copy Markdown
Contributor

Follow-up to my Windows result above: the failure comes from private-region collection accepting PAGE_NOACCESS during preflight. The same issue also occurs on main for an ordinary private allocation with an inaccessible middle region.

I prepared a separate focused fix and regression in the fork: https://github.com/Mouhand-Kaddo/AnyPS5/tree/prep/private-write-watch-inaccessible (commit 82336b697). It rejects inaccessible private regions before any write-watch reset, and the regression verifies that a failed collection preserves earlier writes. The existing direct-memory assertion also distinguishes the 4 KiB private write-watch granularity from shared-view tracking.

With this PR's head 630b18f04 applied on top of that fix and main 29380369a (local validation commit 1fa8ba9a2), Windows 11 / MinGW-w64 GCC 15.2.0 Release passed guest_memory, fiber, and windows_exceptions: 3/3, 1.48 s. This reconciles the focused failure I reported; it is not a full-suite validation. The generic fix alone also passed guest_memory and fiber on Arch WSL2 (2/2); those Linux results do not exercise the Windows-only checks.

No upstream PR opened for the separate fix while intake is paused.

@Mouhand-Kaddo

Copy link
Copy Markdown
Contributor

Additional Windows runtime evidence, separate from the isolated regression checks reported above:

I tested a local integration of current main 4a747c487, the #757/#758 shader-table stack, the focused shader-stack and private-range collection fixes, and this PR's head 630b18f04. guest_memory, fiber, and windows_exceptions passed 3/3 (1.98 s).

During the bounded runtime comparison, host committed memory rose from 24.68 GB to 46.23 GB in 9.5 s; the commit limit was 46.99 GB at that point. I stopped the process when headroom fell below 1 GB. The process had about 7.51 GB of private committed memory then. This run ended before the engine loop, so it does not establish a boot or rendering result.

The physical allocator had also created a shared pagefile-backed section for a 12 GiB physical allocation. The implementation currently creates that section eagerly, before private views are mapped. I am investigating delaying shared backing creation until a shared view or retained private data needs it, and will keep that separate from this PR's mapping behavior. No payloads or executable data are attached.

Windows 11 x64, GCC 15.2.0 / MinGW-w64, Release; original pagefile settings retained. This is a commit-pressure observation on an integration, not an isolated functional failure of the PR.

@Mouhand-Kaddo

Copy link
Copy Markdown
Contributor

Follow-up on the host-commit observation: I prepared a separate, single-topic fork change that defers Windows shared backing creation until it is first mapped:
https://github.com/Mouhand-Kaddo/AnyPS5/tree/prep/lazy-direct-memory-backing

On main 262c03169, the focused memory/fiber/exception checks passed 3/3 on Windows and memory/fiber passed 2/2 on Arch WSL2. A local 1 GiB allocation probe confirms that an unmapped allocation creates no section handle or approximately-1-GiB commit charge, while shared aliases still preserve writes in both directions and release returns the handle count to baseline.

I also tested a local integration with this PR's head 630b18f04 plus the previously reported inaccessible-range collection fix. The Windows checks passed 3/3 (1.74 s). Its first private mapping leaves the backing section uncreated; creating an alias then materializes the section and preserves both views' contents. Adapting the lazy helper requires using it in MapView and the Window copy paths.

Bounded runtime probes of the combined changes progress through substantially more shader work than the earlier run and now stop at an unsupported depth-maintenance draw. This does not isolate the earlier startup fault's cause or establish rendering success, since the integration also includes newer main and shader work. No new upstream PR or payload upload.

@johnmaia

Copy link
Copy Markdown
Contributor

Runtime result on Windows 11 10.0.26300, i7-13700K, RTX 4080, WinLibs GCC 15.2.0 r7: with 630b18f0 cherry-picked, Disney Epic Mickey: Rebrushed (PPSA15657) no longer hits the #361 null write at eboot 0x55eac49 (a red-zone pointer in a leaf function) and ran more than 14 minutes past that point. Details on #361.

On my branch CheckFailedCollectKeepsWrites needed its expectation changed for the private pages, the same failure @Mouhand-Kaddo reported above; their separate fix looks like the cleaner way to handle it.

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates. Still open:

  • CheckFailedCollectKeepsWrites (already on main) fails on Windows with this PR, as reported by @Mouhand-Kaddo and @johnmaia: private-region collection accepts PAGE_NOACCESS in preflight, so a failed collect no longer preserves earlier writes. The fix is the separate change @Mouhand-Kaddo prepared (reject inaccessible private regions before any write-watch reset, https://github.com/Mouhand-Kaddo/AnyPS5/tree/prep/private-write-watch-inaccessible); please include it here or land it first, and keep the test expectation as main has it.
  • Rebase onto current main (DirectMemory.cpp constructor and GuestMemory.cpp test list conflict). It rebases cleanly apart from those two spots.
  • Release of a page mapped once: checked in the code, not run. ForgetDirectMemory erases g_physPages and the mappings before UnmapDirectMemoryViews, so EraseMappings finds no private mapping and SavePrivate is not called on the freed range. Please add a test for it: map once, write, sceKernelReleaseDirectMemory, allocate the same range again, map it, and require zeroes (the old address must no longer be committed).
  • PR CI no longer builds Windows: please post a guest_memory run of the rebased head with the first point fixed.

@xXPBSXx

xXPBSXx commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Test data for this PR with DREDGE (PPSA09076, Unity), which showed the red-zone crash described in #361 and #3915 on main a11c8f7 (Windows 10, Ryzen 7 5800X3D).

Rebase. 630b18f04 cherry-picked onto main a11c8f7 has two small conflicts, both from tests that main gained after this PR was written: in DirectMemory.cpp (PhysicalBacking, main has static_cast<void>(start);, this PR has held.assign(...); I kept both) and in GuestMemory.cpp (the call list in main; I kept main's CheckFailedCollectKeepsWrites() and this PR's two new checks). After that libs builds, but ctest -R "^guest_memory$" fails on that rebase:

Guest memory check failed at core/libs/tests/GuestMemory.cpp:1159

Line 1159 is Require(collect(mapped, page, count) && count == 4) in CheckFailedCollectKeepsWrites, a check on a direct mapping that is mapped once (sceKernelMapDirectMemory of one range, then sceKernelMprotect of its second page, then GuestArenaCollectWrites). With this PR such a mapping is private write-watched memory, so the number of pages collect returns may differ from what that test, written for views, expects. I did not look at whether the test or the PR is wrong; guest_memory passes on plain main.

Crash rate (inconclusive). 10 alternating pairs of 120 s runs, plain main libs against main + this commit, same relinked app.exe: 0 of 10 faults on either side (no FATAL in any of the 20). The same title faulted in 2 of 8 earlier runs on main (see #3915), so the rate was lower in this batch and these 20 runs cannot show whether the PR removes the crash. A second batch with 60 s runs is running.

AI-assisted (Claude Code).

@xXPBSXx

xXPBSXx commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Second batch for the DREDGE comparison above, same method: 10 alternating pairs, 60 s each, the same relinked app.exe and run_title (Windows 10, Ryzen 7 5800X3D, main a11c8f7 libs against the same libs plus the rebased 630b18f04; no other change).

plain main main + this PR
60 s runs 3 of 10 faulted 0 of 10
120 s runs (earlier batch) 0 of 10 0 of 10
all 40 runs 3 of 20 0 of 20

The three faults on main are all in a Job.Worker thread (Job.Worker 1, 10, 8), at app.exe+0x17f05ed (twice, a read of 0x00007ff74d268000, an address inside the executable's own mapping) and +0x17f0843 (a read of 0x40), in the same routine area as the faults in #3915 (+0x17f0796, +0x17efe13). None of the 20 runs with this PR faulted.

What this does and does not show: a one-sided Fisher exact test on 3 of 20 against 0 of 20 gives p of about 0.12, so it is consistent with the PR removing the crash but is not significant, and the rate on main was clearly not constant (2 of 8 earlier, then 0 of 10 at 120 s, then 3 of 10 at 60 s), so the groups are not equally exposed. More runs on both sides would be needed. The unit test failure of the rebase (GuestMemory.cpp:1159, above) is separate and unresolved.

AI-assisted (Claude Code).

@xXPBSXx

xXPBSXx commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

@esatturan thanks for this PR. On the guest_memory failure after rebasing it onto current main (a11c8f7), which I reported above: it is not a mistake in the rebase, and it has two separate causes. This is from reading and building only; I changed nothing in the PR and did not push anything.

CheckFailedCollectKeepsWrites was added to main by bbdb35b after this PR was written. It maps a direct-memory range once (sceKernelMapDirectMemory, two 0x4000 pages), writes one byte in the first page, makes the second page inaccessible with sceKernelMprotect(..., 0), and then expects (a) a collect over both pages to fail without losing the write, and (b) a collect of the first page to return 4 four-KiB pages. With this PR such a mapping is private MEM_WRITE_WATCH memory, not shared views, and:

  1. Line 1159, Require(!collect(mapped, page * 2, count)). The whole-range pre-check collectable() returns false for a PAGE_NOACCESS view, but for private memory it only tests MEM_COMMIT and MEM_PRIVATE, so the inaccessible private page passes and collect returns true. A one-line local change ((memory.Protect & 0xffu) == PAGE_NOACCESS also returns false in the private branch of collectable()) makes line 1159 pass. Without it, a caller that gets true for such a range would go on to read a page the guest made inaccessible.
  2. Line 1160, count == 4. With that change, collect(mapped, page, count) returns ok = 1, count = 1: GetWriteWatch reports the one 4 KiB page that was written, while shared views report the whole 16 KiB guest page as 4 pages. So this expectation describes views only. Since a once-mapped range is private now, the test would need to alias the mapping (map the same physical range a second time) to keep exercising the view path, or the expected count has to depend on the mapping kind.

I did not run anything beyond guest_memory for this. Whether (2) matters to the GPU side, which is the consumer of the collected page list, I do not know.

AI-assisted (Claude Code).

@AlxcBTW

AlxcBTW commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

@esatturan a rendering regression with this PR under host import, found and bisected together with @parl0v.

What we see. Sword of the Sea (PPSA25344, Unreal Engine 5) on Windows. Our integration branch is main a11c8f7f plus #2054 (0f25943c7) plus a stack of other open PRs. With host import on, its main menu renders saturated red with tiled garbage rectangles. Two changes bring back a correct menu on the same branch: APS5_NO_HOST_IMPORT=1, or keeping direct memory as shared views by forcing privatePages = false in AddMapping (libkernel/DirectMemory/DirectMemory.cpp) and changing nothing else.

Same branch, host import on RTX 5070 Laptop (ours) RTX 4060, i5-14400F (parl0v)
as is (#2054 active) red + tiled garbage, frames 35 and 87 red + tiled garbage, frame 43
privatePages = false correct menu, frame 751 correct menu, frame 104
APS5_NO_HOST_IMPORT=1 correct, frames 43 and 75 correct, frame 43

Reverting the R_X XOR fixes, #3372 and #3425 left it red (#3425 is a separate slowdown).

Mechanism. @parl0v traced it in the code. ShareMappedPages (DirectMemory.cpp:436) runs when a new mapping aliases a private range. It calls SavePrivate, remaps the VA as shared section views and sets privatePages = false (lines 455–462), but nothing re-imports host memory. A VkDeviceMemory imported while the range was still private keeps the old pages, so the GPU reads pages the title no longer writes. That gives stale tiles, and the same stale data feeds UE's colour-grading LUT, hence the red.

Possible fixes. Invalidate and re-import any host import over the range in ShareMappedPages, or don't keep host-imported direct memory private. I haven't tried either yet.

AI-assisted: yes (Claude Code)

@xXPBSXx

xXPBSXx commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Second 60 s batch of the DREDGE comparison (10 more alternating pairs, same setup as the two comments above). The result is less clear than the first batch:

plain main main + this PR
batch 2 (60 s) 2 of 10 faulted 1 of 10 faulted
batch 1 (60 s) 3 of 10 0 of 10
60 s runs together 5 of 20 1 of 20
120 s runs 0 of 10 0 of 10
all 60 and 120 s runs 5 of 30 1 of 30

One-sided Fisher exact test: p of about 0.09 for 5 of 20 against 1 of 20, and about 0.10 for 5 of 30 against 1 of 30. So the rate with this PR was lower in these runs but the difference is not significant, and the crash did not go away: the one fault with this PR is in Job.Worker 4 at app.exe+0x17efe13 with a read of 0x0000000600000004, the same site and the same bad value as one of the original faults on main (#3915). The main faults in this batch were both at app.exe+0x17f0796, a read of 0x0000000600000030, again the site and value from #3915.

So this data does not support "this PR removes the DREDGE crash"; it is compatible with it removing some of the write faults that overwrite the red zone while others remain. A guess that I did not check: write-watched memory that is not direct memory mapped once (the guest arena's watched commits, or direct memory that is aliased) still takes resumed write faults. I did not check which memory the faulting write in DREDGE targets.

AI-assisted (Claude Code).

@AlxcBTW

AlxcBTW commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

@esatturan a correction to our red-menu report above (Sword of the Sea, PPSA25344): the mechanism we gave there is wrong for this title. The red is real and comes with this PR plus host import, but it is not caused by ShareMappedPages.

What we got wrong. We said ShareMappedPages turns a private range into views and leaves an import of the old pages behind. This title never takes that path. With APS5_TRACE_MEMORY=1, a 180 s menu run logs 3708 [memory] lines and no shared ... (mapped again) line, so no direct memory range is mapped a second time. We also wrote a fix for that path (wait for GPU work and retire the imports before the switch). On one build with that fix switchable, the menu is just as red with it as without it:

Row (same build, 180 s) blue / red / other at t60, t120, t170 Frame / GPU / worker ms
this PR 0/46/54, 0/45/55, 0/45/55 697 / 213 / 693
this PR + our ShareMappedPages fix 0/45/54, 0/46/54, 0/45/55 647 / 206 / 643
repeat, this PR 0/46/54, 0/46/54, 0/46/54 664 / 206 / 661
repeat, with the fix 0/46/54, 0/46/54, 0/46/54 718 / 212 / 714
with the fix, host imports kept write-watched 91/0/9, 91/0/9, 91/0/9 510 / 27 / 509

What it is instead, as far as we can tell. The last row keeps Windows host imports write-watched. That takes a local switch of ours (APS5_WRITE_WATCH_IMPORTS=watch, not on main), and with it the menu is correct. On Windows the driver unwatches every range it imports ([write-watch] Windows direct host imports use comparisons; separate shared aliases retain guest write tracking):

  • On main, direct memory is shared views, so an import goes through a separate alias and the guest's own views keep their write tracking.
  • With this PR, a page mapped once is private, so it is imported directly and loses its write tracking.

So some CPU write that the driver relies on write tracking to see is missed. That fits both changes from our first report: APS5_NO_HOST_IMPORT=1, and forcing privatePages = false. I haven't found yet which consumer misses the write.

Cost. The comparisons are also expensive. In the four red rows mem.compare is a top cost at 215-231 ms per frame, with about 1 TB compared per run. Keeping the imports watched cuts that to 22 GB, and our shared-view rows on the same base compare 3-5 GB per run at 289-332 ms per frame.

The gap we described in ShareMappedPages is still there in the code: an import made over private pages outlives a second mapping of the same memory. A mock test shows it, but this title doesn't reach it. I can send that fix and its test separately if you want them. Sorry for the wrong lead.

Windows 11, RTX 5070 Laptop (616.92). The base is main a11c8f7 plus this PR (rebased, 02c898e) and our local patches for this title, upstream default env (APS5_RESOURCE_CACHE_ENTRIES=8192, APS5_PROFILE_DRAW=1).

AI-assisted: yes (Claude Code)

@xXPBSXx

xXPBSXx commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Question on testing: DREDGE (PPSA09076, Windows) faults intermittently in a function whose loop variables live below rsp (details and offsets in #3915 , comment of today), which looks like the red-zone case of #361. Should I run DREDGE with this PR branch (5 runs of 150 s against plain main, library hash checked) to see whether the fault rate changes? In my earlier test of this approach on this title (#3915) the fault rate dropped but the fault did not disappear. I will not start it until someone says it is wanted.

AI-assisted (Claude Code).

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks for this. Closing in favour of #637, which handles singly mapped direct memory the same way and is being ported onto main. The points from the 10-11 review (inaccessible private pages in collect, the release test) apply there too. If you want to continue this design, please reopen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build docs libc libkernel needs rebase Conflicts with main, rebase needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants