Skip to content

feat(libkernel): map regular files with mmap on Windows - #6542

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
wachi35676:fix/libkernel-windows-shared-file-mmap
Oct 19, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
wachi35676:fix/libkernel-windows-shared-file-mmap

Conversation

@wachi35676

@wachi35676 wachi35676 commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

What

mmap of a regular file now works on Windows. Before, it failed with EOPNOTSUPP (PosixMemory.cpp; libkernel debt: "Windows maps no files"). Linux gained the same feature in #5053.

  • Shared mappings are views of a file section created on the descriptor's handle (CreateFileMappingW), read-write when the descriptor allows writing and read-only otherwise. They are mapped page by page through WindowsMappings::Map, which now takes the section's view protection instead of always using execute-read-write (new GuestArenaMapFile_nid_postfix). Writes through the descriptor and through the mapping see each other.
  • Private mappings are a copy of the file read at mmap time (POSIX leaves it unspecified whether later file changes show). The partial last page is zero-filled, as on FreeBSD.
  • Anonymous shm_open objects and JIT shared-memory descriptors are temporary files on Windows, so mmap now maps them too; the debt entries for both are updated.
  • mprotect of a mapped view above the view's protection fails with EACCES before anything changes, as FreeBSD's vm_map_protect does. This covers writing through a read-only descriptor and adding PROT_EXEC.
  • Cases Windows cannot represent fail with EOPNOTSUPP and are listed in docs/dev/debt/libkernel.md:
    • shared mappings past the end of the file (a section would grow the file);
    • PROT_EXEC on a file view;
    • write-only descriptors;
    • private pages wholly past the end of the file.
  • Also listed in the debt file:
    • ftruncate shrinking a file fails with EINVAL while any shared mapping of it exists;
    • a read-write GPU alias of a read-only file view fails;
    • separate mmap calls make separate sections, so write tracking does not treat them as aliases.

Title: Dusklight (PPSA99305, v0.3.0, the author's GPL-3.0 release) uses SQLite in WAL mode, which maps its -shm file with mmap(MAP_SHARED, fd).

  • On Windows main: both of its caches fail on every start: Failed to start transaction: disk I/O error, SQLite DB init failed and Failed to begin pipeline cache schema transaction (3 of 3 runs).
  • With this change: SQLite cache init succeeded in 4 of 4 runs, the WAL files fill (dawn_cache.db-wal 1.4 MB), and a second start reuses the cache. The title reaches the same menu as before, since there is no disc image.

Tested

  • Windows 11 (Ryzen 5 9600X, RTX 5070 Ti, NVIDIA 617.42, WinLibs GCC 15.2.0, Release):
    • CheckRegularFileMapping in GuestMemory.cpp now also runs on Windows, with new checks: a pwrite shows through a live shared mapping, a refused mprotect returns EACCES and leaves the view usable, and remove succeeds after munmap. GuestShmOpen.cpp and GuestJitSharedMemory.cpp gain mmap checks.
    • On main, all three tests fail at the first mmap; with this change they pass.
    • Rebuilt on main 44df21632: 424 of 424 guest_*, libkernel*, kernel*, libc* and stream tests pass, excluding agc_/video_out.
    • An mprotect to read-write on a read-only file view followed by a write gives the normal access violation, not the abort that bookkeeping changed before VirtualProtect would cause.
  • Linux (WSL Ubuntu 24.04, GCC 13.3): guest_memory, guest_shm_open, guest_jit_shared_memory, guest_mmap_fixed_anonymous, guest_mprotect_unmapped, guest_mlock and guest_msync pass, both with main's library sources and with this change (the library change is Windows-only; the new assertions also hold on Linux). The full tree builds.
  • Rebased on main 5d9dcc42d (only docs/dev/debt/libkernel.md conflicted, with the kqueue entries from fix(libkernel): serialize Windows vectored file transfers #5833 and feat(libkernel): implement kevent EV_DISABLE, EV_DISPATCH and EV_FORCEONESHOT #5847; the code is unchanged), rebuilt and re-tested on Windows: 424 of 424 again. python tools/check_conventions.py --base origin/main passes.
  • Not tested: macOS; AMD GPUs.

Mappings that end past the end of the file (Windows). A shared mapping may end inside the 16 KiB guest page that holds the end of the file, as POSIX mmap allows. The section keeps the file's size, so the file never grows. In that last guest page:

  • The view ends at the exact end of the file, and Windows zero-fills the rest of its last 4 KiB host page.
  • The rest of the guest page is private zero pages.
  • Every byte of the file stays shared in both directions, read-only and read-write.
  • Bytes past the end of the file read zero and are never written to the file. Bytes written to the private pages are seen only through that mapping.
  • A mapping that reaches a whole page past the end of the file still fails with EOPNOTSUPP, and a read-write alias of such a page throws.

PS5LM Plus (PPSA99581, v0.8.4) needs this. It runs llama.cpp, whose CPU backend maps the whole GGUF model MAP_SHARED, PROT_READ.

  • With Qwen2.5-0.5B-Instruct q4_k_m (491,400,032 bytes, not a multiple of 16 KiB) and -dev none -ngl 0, Windows main and the previous revision both logged llama_model_load: error loading model: mmap failed: Operation not supported, 3 of 3 runs each.
  • With this revision the model loads, and the completion of "The capital of France is" returns " Paris. It is the largest city in Europe…" in 4 of 4 runs.

Tested for this revision (rebased on 5de7a46e0):

  • CheckRegularFileMapping covers the read-only and read-write cases. It fails on the previous revision (mmap returns MAP_FAILED, errno 45) and passes now.
  • Full Windows build; the related ctest set passes 458/458.
  • WSL Ubuntu 24.04: guest_memory, guest_shm_open, guest_jit_shared_memory, guest_mmap_fixed_anonymous, guest_mprotect_unmapped, guest_mlock and guest_msync pass.
  • A separate review probed mprotect across the view/private boundary (refused with EACCES, nothing partly applied), munmap and MAP_FIXED over the last page (no leftover allocations), write tracking on both parts, and the end-of-file bounds at several file sizes and offsets.

Overlap: #7299 (named shared memory on Windows) conflicts with this in docs/dev/debt/libkernel.md. With both, named objects would also take the file-mapping path here, so whichever lands second should reconcile the access checks.

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 under Silent stubs in their library's technical debt file
  • 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
  • Unblocks: none (Dusklight PPSA99305 already reaches its menu; this fixes its SQLite caches on Windows)
  • GPUs tested: n/a (no AGC, shader or VideoOut change; title runs used an NVIDIA RTX 5070 Ti, 617.42, Windows 11)
  • AI-assisted: yes

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 15, 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
PR 6321 core/libs/tests/GuestMemory.cpp L2467
PR 6825 docs/dev/debt/libkernel.md L113-114
PR 7299 docs/dev/debt/libkernel.md L130
PR 7473 core/libs/prx/libc/include/WindowsMappings.hpp L536
PR 7615 core/libs/prx/libc/include/WindowsMappings.hpp L79
PR 7653 docs/dev/debt/libkernel.md L114-115

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 1835 core/libs/prx/libc/src/GuestArena.cpp
PR 4449 core/libs/prx/libc/include/GuestArena.hpp
core/libs/prx/libc/src/GuestArena.cpp
core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp
core/libs/tests/GuestMemory.cpp
PR 5021 core/libs/prx/libc/src/GuestArena.cpp
core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp
core/libs/tests/GuestMemory.cpp
docs/dev/debt/libkernel.md
PR 6044 core/libs/tests/GuestJitSharedMemory.cpp
core/libs/tests/GuestMemory.cpp
core/libs/tests/GuestShmOpen.cpp
PR 6149 docs/dev/debt/libkernel.md
PR 6446 docs/dev/debt/libkernel.md
PR 6469 core/libs/prx/libc/src/GuestArena.cpp
core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp
core/libs/tests/GuestMemory.cpp
docs/dev/debt/libkernel.md
PR 6519 docs/dev/debt/libkernel.md
PR 6879 core/libs/prx/libc/src/GuestArena.cpp
core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp
PR 6908 docs/dev/debt/libkernel.md
PR 6936 docs/dev/debt/libkernel.md
PR 6977 docs/dev/debt/libkernel.md
PR 7039 docs/dev/debt/libkernel.md
PR 7049 docs/dev/debt/libkernel.md
PR 7075 docs/dev/debt/libkernel.md
PR 7088 docs/dev/debt/libkernel.md
PR 7105 core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp
core/libs/tests/GuestMemory.cpp
docs/dev/debt/libkernel.md
PR 7121 docs/dev/debt/libkernel.md
PR 7129 docs/dev/debt/libkernel.md
PR 7149 core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp
docs/dev/debt/libkernel.md
PR 7192 docs/dev/debt/libkernel.md
PR 7201 core/libs/prx/libc/src/GuestArena.cpp
core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp
PR 7332 core/libs/prx/libc/src/GuestArena.cpp
core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp
core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp
core/libs/tests/GuestMemory.cpp
docs/dev/debt/libkernel.md
PR 7509 docs/dev/debt/libkernel.md

@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed libc libkernel docs labels Oct 15, 2026
@wachi35676
wachi35676 force-pushed the fix/libkernel-windows-shared-file-mmap branch from c132955 to 85aece7 Compare October 16, 2026 05:16
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 16, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 17, 2026
@blawesom

Copy link
Copy Markdown
Contributor

Rebase handoff — rebased onto current main (authorship unchanged). One docs-debt conflict, unioned.

Verified on Linux (CachyOS, kernel 7.2.9, GCC 16.2.1, Ninja, Release, BUILD_TESTING=ON, AMD RX 9070 XT / RADV):

To pick it up:

git fetch https://github.com/blawesom/AnyPS5 rebase/6542
git checkout <your branch>
git merge --ff-only FETCH_HEAD

Head is 9006b028e.

@wachi35676

Copy link
Copy Markdown
Contributor Author

Thanks for the rebase, @blawesom. I've pushed my own rebase instead (39d9005fe, on main 66702f1cf), because the docs/dev/debt/libkernel.md union in 9006b028e had left the file inconsistent:

  • the JIT shared memory and kqueue and kevent entries appeared twice;
  • main's EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent entry was gone (196 entries before, 196 with a duplicate pair and one missing after).

The new head keeps main's kqueue entry and this PR's JIT entry, so the only entries that change are the three this PR edits. The code is byte-identical to the previous head.

Windows 11, WinLibs GCC 15.2.0: full build passes, and 454/454 guest_*, libkernel*, nid*, libc* and stream tests pass.

AI-assisted: yes (Claude Code); results above were run on this machine.

@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed and removed needs rebase Conflicts with main, rebase needed labels Oct 17, 2026
@wachi35676
wachi35676 force-pushed the fix/libkernel-windows-shared-file-mmap branch from 39d9005 to c1879fe Compare October 17, 2026 21:30
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 17, 2026
@wachi35676
wachi35676 force-pushed the fix/libkernel-windows-shared-file-mmap branch from c1879fe to 2eabf63 Compare October 17, 2026 23:53
@wachi35676

Copy link
Copy Markdown
Contributor Author

I extended this PR to support shared mappings that end inside the guest page holding the end of the file. It is rebased on 5de7a46e0 and is still one commit, now 2eabf6382.

Found with PS5LM Plus (PPSA99581, v0.8.4). Its llama.cpp CPU backend maps the whole model file shared and read-only. The test model is Qwen2.5-0.5B-Instruct q4_k_m from Qwen's Hugging Face repo (Apache-2.0), 491,400,032 bytes, so the rounded length runs past the end of the file. The previous revision refused that and gave mmap failed: Operation not supported in 3 of 3 runs.

A Windows view can't cover host pages past the section size, and a larger section would grow the file. MapViewOfFile3 does accept the exact remaining size when it replaces a 4 KiB-rounded placeholder, and zero-fills the rest of that host page. So the last guest page is now that view plus private zero pages, and every file byte stays shared.

With this revision the model loads, and "The capital of France is" completes as " Paris. It is the largest city in Europe…" in 4 of 4 runs. The description has the test results and a note on the overlap with #7299.

AI-assisted: yes (Claude Code); results above were run on this machine.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 18, 2026
@wachi35676
wachi35676 force-pushed the fix/libkernel-windows-shared-file-mmap branch from 2eabf63 to 45a702a Compare October 18, 2026 12:50
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 18, 2026
@screamm

screamm commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Windows run of head 2eabf6382, merged onto main 0d078f75d (clean merge). The PR has since been rebased to 45a702a10; its diff against the merge base is line-identical apart from hunk offsets and one unrelated context line in the debt doc, so the results below apply to the new head as well (not rebuilt).

  • Host: Windows 11 Pro 26220, Ryzen 7 5700X, RTX 4070 (driver 596.21), WinLibs MinGW-w64 GCC 15.2.0 r7, Ninja, Release.
  • Build: full build and libs pass, with no warning in the files the PR changes.
  • --repeat until-fail:3: guest_memory (which holds CheckRegularFileMapping), guest_shm_open and guest_jit_shared_memory pass 3/3.
  • Negative control: with PosixMemory.cpp, DirectMemory.cpp, WindowsMappings.hpp and GuestArena.cpp/.hpp from main and the tests kept, all three fail on 3 of 3 runs. guest_memory stops at GuestMemory.cpp:2440, the first shared mmap. With the source restored they pass again.
  • Non-GPU suite (ctest -j8 --timeout 120 -E '^agc_driver_|^video_out_flip'): 928 of 936 pass and 2 are skipped. Six tests timed out in the first pass while the host was busy (hardware_oracle_compare, conventions_pr_template, guest_file_lock_process, conversion_diagnostics, guest_none_relocations, tls_function_coverage_register_address_loads). All six pass serially (hardware_oracle_compare once hit its own 20 s limit, then 11 s and 12 s). I did not run the suite on plain main.
  • tools/check_conventions.py --base origin/main and git diff --check pass. The diff adds no environment variable and no export.
  • Extra checks with a local test executable (not part of the PR), 24 checks, all pass: a 3-page shared read-write mapping of a file that ends 0x1100 bytes into its third page reads zero past the end, the far tail is not seen by a second mapping and is zero again after unmap and remap, and the file size does not change; a 5-page mapping with the middle page and then the end-of-file page unmapped keeps its neighbours and writes (and MAP_FIXED over the hole works); a file mapping MAP_FIXED over anonymous pages at a file offset works; a whole page past the end, PROT_EXEC and an empty file return 45; a write through the view reaches the file when read through another handle. 256 MiB shared read-only: mmap 200-360 ms, munmap 130-200 ms. Against the main sources the mapping checks fail with errno 45.

Titles. PS5LM Plus is not a local title, so I could not run the case in the description. I ran five relinked titles that import mmap for 45 s with a local diagnostic libkernel that logs every mmap with a descriptor: only PPSA99169 RetroArch (its frontend picker) maps files. It calls mmap(len=0x200008, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0) twice, on the Mesa cache files radv-shader-cache/mesa_shader_cache_db/index and radv_builtin_shaders/index (2097160 bytes, so the same read-write end-of-file page case as the model), and both succeed. PPSA85300 VLC, PPSA99997, PPSA99039 and PPSA99420 map no file in that window. PPSA99169, 60 s, two runs each: no stop line on main (4303 and 4428 frames) or with the PR (4800 and 5302 frames), all four killed at 60 s with 12 frame dumps; no FAIL:, FATAL:, not implemented or DEVICE_LOST line. Frame counts do not compare because a build was running on the host. What does change: with the PR the picker goes on to open and write part0/mesa_cache.idx and part0/mesa_cache.db in both cache directories (20929 and 2469 bytes after one run); on main the mmap is refused and those files are never opened. The PR run used main's runtime from 3e3e8b905 with libkernel and libc built from the merge, which is one AGC commit older than the merge base.

Notes on the diff, none blocking:

  • mapShared calls MapViewOfFile3(..., MEM_REPLACE_PLACEHOLDER) with the exact byte count to the end of the file. The MSDN page says the size must be a multiple of the page size and must match the placeholder when replacing one. It works on this host for every end-of-file case above, but it depends on behaviour that is not documented, and I only had one Windows build.
  • The remainder of the last 4 KiB host page (between the end of the file and the next 4 KiB boundary) is part of the view: a second mapping sees a write there, and it survives unmap and remap while the file stays open (I read back 0xcc and 0xaa I had written). It never reaches the file. Only the private zero pages after it are private to the mapping. The debt entry could say so.
  • msync is still only a validity check and never calls FlushViewOfFile, so msync(MS_SYNC) gives no durability on Windows for a mapped file. MSDN also says a mapped file and ReadFile/WriteFile are not necessarily coherent (remote files are not); the local file case behaved as the PR says.
  • A private mapping reads the whole range at mmap time, so a large private mapping costs the read and the memory. Not measured on a large file.
  • fix(libkernel): release flexible memory budget through munmap #6311 (munmap through sceKernelMunmap) merges with this PR without a conflict. I built the pair: the five guest_flexible_memory_* tests, guest_memory, guest_shm_open, guest_jit_shared_memory and guest_truncate pass, and a 256 MiB view unmaps in the same time. File views are not charged to the flexible budget and sceKernelMunmap releases only recorded flexible ranges.
  • fix(libkernel): preserve directory identity across Windows renames #6228 makes directory descriptors real handles and adds File::IsDirectoryDescriptor. This PR rejects directories with File::DirectoryDescriptorPath, which can return nothing if fix(libkernel): preserve directory identity across Windows renames #6228's path lookup fails, and a directory handle reports FILE_TYPE_DISK; whichever lands second should use IsDirectoryDescriptor there. I did not build that combination. perf(libc): start the guest allocation overlap queries at the range that touches the address #6321 conflicts in GuestMemory.cpp.

Not checked: PS5LM Plus, Windows builds other than 10.0.26220, AMD and Intel GPUs, Linux and macOS, a large private mapping, the suite on plain main.

Written with Claude Code assistance (validation by a subagent, reviewed in this session).

@github-actions github-actions Bot added the unblocks title Names a title ID on the Unblocks line label Oct 18, 2026
A shared mapping is a view of a file section created on the descriptor's
handle, read-write when the descriptor is, otherwise read-only. A private
mapping is a copy of the file read at mmap, as POSIX allows. Anonymous
shm_open objects and JIT shared memory descriptors are temporary files on
Windows, so mmap now maps them too (it failed with EOPNOTSUPP before).
mprotect of a mapped view above the view's protection fails with EACCES
before any bookkeeping changes. The cases Windows cannot represent are
listed in the libkernel debt.

Dusklight (PPSA99305) uses SQLite in WAL mode, which maps its -shm file.
On Windows main f063408 every start logged "Failed to start
transaction: disk I/O error" for its Dawn and pipeline caches (3 of 3
runs); with this change the caches initialise and their WAL files fill
(4 of 4 runs). The title reaches its menu either way.

A shared mapping may end inside the 16 KiB page that holds the end of
the file, as POSIX mmap allows: the view ends with the file's last 4 KiB
host page, which Windows zero-fills past the end, the rest of the guest
page is private zero pages, and the file never grows. A mapping that
reaches a whole page past the end still fails with EOPNOTSUPP.

PS5LM Plus (PPSA99581) loads its model with llama.cpp, which maps the
whole GGUF file shared and read-only. With a 491,400,032-byte model,
Windows main 5de7a46 and this change without the last page logged
"llama_model_load: error loading model: mmap failed: Operation not
supported" (3 of 3 runs); now the model loads and the completion of
"The capital of France is" starts with " Paris" (3 of 3 runs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@wachi35676
wachi35676 force-pushed the fix/libkernel-windows-shared-file-mmap branch from 45a702a to ddb774a Compare October 19, 2026 00:11
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 19, 2026
@adriadam10 adriadam10 self-assigned this Oct 19, 2026
@adriadam10
adriadam10 merged commit 2241006 into boykopovar:main Oct 19, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs libc libkernel unblocks title Names a title ID on the Unblocks line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants