fix(hos): report the game's mapped pages as used memory; commit the heap lazily on Windows #324

Merged
HumanBean merged 3 commits from JosephPursley/Longshot:fix/windows-guest-memory into main 2026-11-14 01:40:36 +00:00
Contributor

Why

On Windows the game's own memory keeps growing as you visit areas, because the emulated OS tells it far more memory is free than really is, so it maps more and more pages for its caches (4.9 GB after three areas, 7.9 GB in a longer play session). On a 32 GB PC this pushed the system to about 1 GB of free commit.

What changed

  • hos memory info (every platform): hos_used_memory() in host/hos_mem.c adds the pages the game mapped through AllocateMemoryPages (counted in a page bitmap, uncounted in FreeMemoryPages) to the used memory that QueryMemoryInfo (+0x08) and svc GetInfo 7/22 (host/hos_svc.c) report, capped at the console's total so the game's free = total − used (at 0x8080f010) can never wrap. The game now sizes its caches against what the console really has. The bitmap is kept a GB of the guest window at a time, allocated on first use (no 16 MB static array charged to Windows' commit).
  • Windows heap: SetMemoryHeapSize no longer commits the whole heap up front. commit_on_touch in host/mkw_glue.c continues when another thread already committed the page (a first-touch race), but not for an execute fault, which still reaches the crash report; that retry is compiled on Windows only (#ifdef _WIN32), so on Linux every fault in the guest window still reaches the crash report as on main. hos_commit now returns whether the range is committed, and read_at/WriteFile in host/nn_fs.c commit guest buffers before ReadFile/WriteFile and return an error, 0xCA01, when that commit is refused, instead of a short read or write. 0xCA01 is the code hos_mem.c and hos_svc.c already return as out of memory; by Horizon's numbering it is svc::ResultInvalidSize (out of memory is 0xD001), and what OoT does with either is unknown.
  • guest imports: in imports_give_back() (gpu/vk/vk_memory.cpp), RX_VK_IMPORT_HIGH defaults to 85 on Windows NVIDIA (it was off on all of Windows); Windows AMD and Intel stay off unless the switch is set, and there the pools it gives back (GRACE mode) leave the budget at once and are freed after RX_VK_RETIRE_SECS. memory_guest_imported() (every platform) also counts pools given back whose memory isn't freed yet.
  • tests: tests/test_commit_limit.py cuts hos_commit by its new signature, and checks that commit_on_touch's retry of a committed page is there and under #ifdef _WIN32; tests/test_texel_buffer_ownership.py cuts imports_give_back at its reworded comment.
  • docs: the switches below in docs/development.md's switch reference.

Flags and settings

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
HOS_MEMINFO_OLD=1 The old used-memory report (program + system only) off Off: for A/B only - - -
HOS_HEAP_COMMIT=1 Commits the whole heap at SetMemoryHeapSize (old Windows behaviour) off Off: for A/B only - - -
RX_VK_IMPORT_HIGH=<percent> Budget use at which idle guest imports are given back 85 on Linux (as before) and Windows NVIDIA (was 0 = off); 0 = off on Windows AMD and Intel (as before) On where it is on by default; AMD/Intel Windows only after a run there Windows NVIDIA - Windows AMD, Intel (with the switch set)
RX_VK_RETIRE_SECS=<s> How long a pool given back waits before its memory is freed on Windows AMD and Intel (already used by the retired pools; now also by this valve when the switch above turns it on there) 30 (unchanged) Keep the default - - Windows AMD, Intel
HOS_PAGES_LIMIT=1 Refuses AllocateMemoryPages past the console's total (diagnostic) off Keep off: the game retries a refused allocation forever - Everything -

All five are in docs/development.md (with RX_VK_IMPORT_LOW, RX_VK_IMPORT_CAP_MB and RX_VK_RETIRE_MIN_SECS, which this PR doesn't change).

Measurements

The numbers below are for 40caf46f, before the review fixes and the rebase; they have not been taken again on 5e8a89b8. On Windows NVIDIA the fixes change nothing these runs reached (the used-memory cap only acts past the console's total, which B2 stayed under; the valve's default there is the same), but they are to be measured again before ready.

main: ae96bad3 (the base 40caf46f was measured on)
this PR: fix/windows-guest-memory @ 40caf46f (now 5e8a89b8 on main 351affd9, not yet measured)
Hardware: NVIDIA GeForce RTX 3060, driver 610.88, AMD Ryzen 7 5800X3D, 32 GB RAM, Windows 11 Pro
Scene: one save (Hyrule Field, adult), then debug teleports to Kokiri Forest, Kakariko Village, Market (and Death Mountain Trail / Zora's Domain, refused in every run: "an event is playing"), ~40 s each, walking; 1920x1080 headless, audio off.
Method: one build; the old behaviour with HOS_MEMINFO_OLD=1 HOS_HEAP_COMMIT=1 RX_VK_IMPORT_HIGH=0 (A), this PR's defaults (B); interleaved A1, B1, A2, B2, ~6 min each, driven over the control bridge. Process memory sampled every ~12 s; mapped pages from the mem: the game's mapped pages log line.
Baseline: this PR @ 40caf46f, old behaviour (A2)
Why not main: the A runs turn this PR's changes off in the same binary, so both sides share the build and caches.
How to reproduce: longshot_ctl.py launch <game> --headless --env HOS_AUDIO=0 [--env HOS_MEMINFO_OLD=1 --env HOS_HEAP_COMMIT=1 --env RX_VK_IMPORT_HIGH=0], press A until play, teleport 0, teleport 2, teleport 3, holding ~40 s each.

Matched pair (A2/B2, same route: Kokiri → Kakariko → Market), end of run:

this PR @ 40caf46f, old behaviour (A2) this PR @ 40caf46f (B2)
Game's mapped pages 4,875 MB 2,560 MB
Private memory 30,227 MB 25,876 MB
Working set 20,416 MB 17,751 MB
fps (Market) 59.7 59.9

First pair (A1 reached only Kokiri because its teleports were refused, so the route is not matched): mapped pages 3,841 vs 2,560 MB, private 28,552 vs 26,402 MB.

Both B runs stop at 2,560 MB of mapped pages whatever the route; the A runs grow with each area. An earlier longer play session by hand (not interleaved) reached 7.9 GB of mapped pages and ~20 GB of working set with the old behaviour, against ~2.6 GB and ~13.9 GB with this PR.

A2: mem: the game's mapped pages 4875 MB; heap 6894 MB; the console's total 9809 MB
B2: mem: the game's mapped pages 2560 MB; heap 6894 MB; the console's total 9809 MB

Testing

Tested: at 40caf46f, RTX 3060 / Windows 11: the four runs above, plus about an hour of hand play (title, Kokiri Forest, Hyrule Field) with no crash or visible problem. tests/run_all.py at 40caf46f: the same 6 failures with and without the change (that PC's setup). The review fixes in 58dc267c (the used-memory cap, the NVIDIA-only default, the fs commit errors, the execute-fault check, the lazily allocated bitmap) and 5e8a89b8 (the first-touch retry on Windows only) were written without building or running anything: not compiled, not run in the game, tests/run_all.py not run on them yet. No test request results yet (none opened).

Not tested:

  • Linux: not unchanged. The used-memory report (hos_used_memory) and memory_guest_imported's count of pools given back but not yet freed apply on every platform; only the lazy heap commit is Windows-only. Not run there at all.
  • Windows AMD and Intel GPUs: the valve stays off there by default now, so their default behaviour only changes through the used-memory report and the lazy heap; the GRACE-mode release runs only with RX_VK_IMPORT_HIGH set, untested.
  • Sessions over 30 minutes; a PC near its commit limit (e.g. a 2 GB page file) with the heap committed lazily.

Needs other testers:

  • Linux: the game boots and plays 30+ minutes across areas, expecting no crash and mapped pages stopping near 2.6 GB (request to open)
  • Windows, AMD GPU: play 30+ minutes across areas with the defaults, expecting no crash and lower memory than main; then the same with RX_VK_IMPORT_HIGH=85, expecting no device loss (request to open)
  • Windows, Intel GPU: the same (request to open)

Screenshots

Nothing on screen.

Risk and rollback

  • Every platform: the game sees less free memory, so it may evict its own caches sooner: more loading or streaming in big areas is possible, though fps was unchanged here. This applies on Linux too, where it hasn't been run.
  • Windows commit charge: with the heap committed lazily, a commit refused at Windows' commit limit no longer shows once at SetMemoryHeapSize; it can come at any first touch, on any thread, mid-play (a 5 s commit_wait stall, then the out-of-memory notice and a crash report). In exchange the heap no longer charges ~6.9 GB of commit up front. A file read or write whose buffer can't be committed now returns an error (0xCA01) to the game, where before it got a short read or write; what the game does with it is untested.
  • A page committed late on Windows can fault inside a host API that writes guest memory and isn't covered yet.
  • Windows NVIDIA: idle imports are given back under pressure, as on Linux. Windows AMD and Intel: unchanged unless RX_VK_IMPORT_HIGH is set.
  • HOS_MEMINFO_OLD=1, HOS_HEAP_COMMIT=1 and RX_VK_IMPORT_HIGH=0 turn each part off; reverting the commits is enough.

Reviewer notes

Overlaps the 5.6 memory row with https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/223 (Linux renderer pressure, separate code apart from imports_give_back()). Coordinating with rogasse's Windows investigation of the same row. The review fixes are in their own commits on top (58dc267c, then 5e8a89b8 for the Linux fault loop) so the author's commit stays as it was apart from the rebase.

Issues

See https://git.magicbean.dev/MagicBeanDev/Longshot/issues/89

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above). (not run on 5e8a89b8; the run at 40caf46f had 6 failures from that PC's setup, the same on main, still to be named one by one)
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. (switches-off baseline in the same binary at 40caf46f, one matched pair; to be measured again on 5e8a89b8)
  • 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.
  • Flags noted, and the switch reference updated (docs/development.md, the README).
  • A row in docs/tested-configurations.md for each measured run. (to do, with the new measurements)
  • 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. (5e8a89b8 not built on either yet; 40caf46f built on Windows only)
  • Docs updated.

🤖 Generated with Claude Code

## Why On Windows the game's own memory keeps growing as you visit areas, because the emulated OS tells it far more memory is free than really is, so it maps more and more pages for its caches (4.9 GB after three areas, 7.9 GB in a longer play session). On a 32 GB PC this pushed the system to about 1 GB of free commit. ## What changed - hos memory info (every platform): `hos_used_memory()` in `host/hos_mem.c` adds the pages the game mapped through `AllocateMemoryPages` (counted in a page bitmap, uncounted in `FreeMemoryPages`) to the used memory that `QueryMemoryInfo` (+0x08) and svc `GetInfo` 7/22 (`host/hos_svc.c`) report, capped at the console's total so the game's free = total − used (at 0x8080f010) can never wrap. The game now sizes its caches against what the console really has. The bitmap is kept a GB of the guest window at a time, allocated on first use (no 16 MB static array charged to Windows' commit). - Windows heap: `SetMemoryHeapSize` no longer commits the whole heap up front. `commit_on_touch` in `host/mkw_glue.c` continues when another thread already committed the page (a first-touch race), but not for an execute fault, which still reaches the crash report; that retry is compiled on Windows only (`#ifdef _WIN32`), so on Linux every fault in the guest window still reaches the crash report as on main. `hos_commit` now returns whether the range is committed, and `read_at`/`WriteFile` in `host/nn_fs.c` commit guest buffers before `ReadFile`/`WriteFile` and return an error, 0xCA01, when that commit is refused, instead of a short read or write. 0xCA01 is the code `hos_mem.c` and `hos_svc.c` already return as out of memory; by Horizon's numbering it is `svc::ResultInvalidSize` (out of memory is 0xD001), and what OoT does with either is unknown. - guest imports: in `imports_give_back()` (`gpu/vk/vk_memory.cpp`), `RX_VK_IMPORT_HIGH` defaults to 85 on Windows NVIDIA (it was off on all of Windows); Windows AMD and Intel stay off unless the switch is set, and there the pools it gives back (GRACE mode) leave the budget at once and are freed after `RX_VK_RETIRE_SECS`. `memory_guest_imported()` (every platform) also counts pools given back whose memory isn't freed yet. - tests: `tests/test_commit_limit.py` cuts `hos_commit` by its new signature, and checks that `commit_on_touch`'s retry of a committed page is there and under `#ifdef _WIN32`; `tests/test_texel_buffer_ownership.py` cuts `imports_give_back` at its reworded comment. - docs: the switches below in `docs/development.md`'s switch reference. ## Flags and settings | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | `HOS_MEMINFO_OLD=1` | The old used-memory report (program + system only) | off | Off: for A/B only | - | - | - | | `HOS_HEAP_COMMIT=1` | Commits the whole heap at `SetMemoryHeapSize` (old Windows behaviour) | off | Off: for A/B only | - | - | - | | `RX_VK_IMPORT_HIGH=<percent>` | Budget use at which idle guest imports are given back | 85 on Linux (as before) and Windows NVIDIA (was 0 = off); 0 = off on Windows AMD and Intel (as before) | On where it is on by default; AMD/Intel Windows only after a run there | Windows NVIDIA | - | Windows AMD, Intel (with the switch set) | | `RX_VK_RETIRE_SECS=<s>` | How long a pool given back waits before its memory is freed on Windows AMD and Intel (already used by the retired pools; now also by this valve when the switch above turns it on there) | 30 (unchanged) | Keep the default | - | - | Windows AMD, Intel | | `HOS_PAGES_LIMIT=1` | Refuses `AllocateMemoryPages` past the console's total (diagnostic) | off | Keep off: the game retries a refused allocation forever | - | Everything | - | All five are in `docs/development.md` (with `RX_VK_IMPORT_LOW`, `RX_VK_IMPORT_CAP_MB` and `RX_VK_RETIRE_MIN_SECS`, which this PR doesn't change). ## Measurements The numbers below are for 40caf46f, before the review fixes and the rebase; they have not been taken again on 5e8a89b8. On Windows NVIDIA the fixes change nothing these runs reached (the used-memory cap only acts past the console's total, which B2 stayed under; the valve's default there is the same), but they are to be measured again before ready. main: ae96bad3 (the base 40caf46f was measured on) this PR: fix/windows-guest-memory @ 40caf46f (now 5e8a89b8 on main 351affd9, not yet measured) Hardware: NVIDIA GeForce RTX 3060, driver 610.88, AMD Ryzen 7 5800X3D, 32 GB RAM, Windows 11 Pro Scene: one save (Hyrule Field, adult), then debug teleports to Kokiri Forest, Kakariko Village, Market (and Death Mountain Trail / Zora's Domain, refused in every run: "an event is playing"), ~40 s each, walking; 1920x1080 headless, audio off. Method: one build; the old behaviour with `HOS_MEMINFO_OLD=1 HOS_HEAP_COMMIT=1 RX_VK_IMPORT_HIGH=0` (A), this PR's defaults (B); interleaved A1, B1, A2, B2, ~6 min each, driven over the control bridge. Process memory sampled every ~12 s; mapped pages from the `mem: the game's mapped pages` log line. Baseline: this PR @ 40caf46f, old behaviour (A2) Why not main: the A runs turn this PR's changes off in the same binary, so both sides share the build and caches. How to reproduce: `longshot_ctl.py launch <game> --headless --env HOS_AUDIO=0 [--env HOS_MEMINFO_OLD=1 --env HOS_HEAP_COMMIT=1 --env RX_VK_IMPORT_HIGH=0]`, press A until `play`, `teleport 0`, `teleport 2`, `teleport 3`, holding ~40 s each. Matched pair (A2/B2, same route: Kokiri → Kakariko → Market), end of run: | | this PR @ 40caf46f, old behaviour (A2) | this PR @ 40caf46f (B2) | |---|---|---| | Game's mapped pages | 4,875 MB | 2,560 MB | | Private memory | 30,227 MB | 25,876 MB | | Working set | 20,416 MB | 17,751 MB | | fps (Market) | 59.7 | 59.9 | First pair (A1 reached only Kokiri because its teleports were refused, so the route is not matched): mapped pages 3,841 vs 2,560 MB, private 28,552 vs 26,402 MB. Both B runs stop at 2,560 MB of mapped pages whatever the route; the A runs grow with each area. An earlier longer play session by hand (not interleaved) reached 7.9 GB of mapped pages and ~20 GB of working set with the old behaviour, against ~2.6 GB and ~13.9 GB with this PR. ``` A2: mem: the game's mapped pages 4875 MB; heap 6894 MB; the console's total 9809 MB B2: mem: the game's mapped pages 2560 MB; heap 6894 MB; the console's total 9809 MB ``` ## Testing **Tested:** at 40caf46f, RTX 3060 / Windows 11: the four runs above, plus about an hour of hand play (title, Kokiri Forest, Hyrule Field) with no crash or visible problem. `tests/run_all.py` at 40caf46f: the same 6 failures with and without the change (that PC's setup). The review fixes in 58dc267c (the used-memory cap, the NVIDIA-only default, the fs commit errors, the execute-fault check, the lazily allocated bitmap) and 5e8a89b8 (the first-touch retry on Windows only) were written without building or running anything: not compiled, not run in the game, `tests/run_all.py` not run on them yet. No test request results yet (none opened). **Not tested:** - Linux: not unchanged. The used-memory report (`hos_used_memory`) and `memory_guest_imported`'s count of pools given back but not yet freed apply on every platform; only the lazy heap commit is Windows-only. Not run there at all. - Windows AMD and Intel GPUs: the valve stays off there by default now, so their default behaviour only changes through the used-memory report and the lazy heap; the GRACE-mode release runs only with `RX_VK_IMPORT_HIGH` set, untested. - Sessions over 30 minutes; a PC near its commit limit (e.g. a 2 GB page file) with the heap committed lazily. **Needs other testers:** - Linux: the game boots and plays 30+ minutes across areas, expecting no crash and mapped pages stopping near 2.6 GB (request to open) - Windows, AMD GPU: play 30+ minutes across areas with the defaults, expecting no crash and lower memory than main; then the same with `RX_VK_IMPORT_HIGH=85`, expecting no device loss (request to open) - Windows, Intel GPU: the same (request to open) ## Screenshots Nothing on screen. ## Risk and rollback - Every platform: the game sees less free memory, so it may evict its own caches sooner: more loading or streaming in big areas is possible, though fps was unchanged here. This applies on Linux too, where it hasn't been run. - Windows commit charge: with the heap committed lazily, a commit refused at Windows' commit limit no longer shows once at `SetMemoryHeapSize`; it can come at any first touch, on any thread, mid-play (a 5 s `commit_wait` stall, then the out-of-memory notice and a crash report). In exchange the heap no longer charges ~6.9 GB of commit up front. A file read or write whose buffer can't be committed now returns an error (0xCA01) to the game, where before it got a short read or write; what the game does with it is untested. - A page committed late on Windows can fault inside a host API that writes guest memory and isn't covered yet. - Windows NVIDIA: idle imports are given back under pressure, as on Linux. Windows AMD and Intel: unchanged unless `RX_VK_IMPORT_HIGH` is set. - `HOS_MEMINFO_OLD=1`, `HOS_HEAP_COMMIT=1` and `RX_VK_IMPORT_HIGH=0` turn each part off; reverting the commits is enough. ## Reviewer notes Overlaps the 5.6 memory row with [https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/223](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/223) (Linux renderer pressure, separate code apart from `imports_give_back()`). Coordinating with rogasse's Windows investigation of the same row. The review fixes are in their own commits on top (58dc267c, then 5e8a89b8 for the Linux fault loop) so the author's commit stays as it was apart from the rebase. ## Issues See [https://git.magicbean.dev/MagicBeanDev/Longshot/issues/89](https://git.magicbean.dev/MagicBeanDev/Longshot/issues/89) ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [ ] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). (not run on 5e8a89b8; the run at 40caf46f had 6 failures from that PC's setup, the same on main, still to be named one by one) - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [ ] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. (switches-off baseline in the same binary at 40caf46f, one matched pair; to be measured again on 5e8a89b8) - [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] Flags noted, and the switch reference updated (docs/development.md, the README). - [ ] A row in `docs/tested-configurations.md` for each measured run. (to do, with the new measurements) - [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. (5e8a89b8 not built on either yet; 40caf46f built on Windows only) - [x] Docs updated. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(hos): report the game's mapped pages as used memory; commit the heap lazily on Windows
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
40caf46f06
QueryMemoryInfo and svc GetInfo 7/22 counted only the program and heap as
used, so the game (free = total - used) kept mapping pages outside the heap:
7.9 GB by Kokiri Forest. Used memory now adds the pages mapped through
AllocateMemoryPages (HOS_MEMINFO_OLD=1 restores the old report).

- Windows: SetMemoryHeapSize no longer commits the heap up front
  (HOS_HEAP_COMMIT=1 restores it); commit_on_touch continues when another
  thread already committed the page (first-touch race), and nn_fs commits
  guest buffers before ReadFile/WriteFile.
- vk_memory: RX_VK_IMPORT_HIGH defaults to 85 on Windows; pools evicted in
  GRACE mode leave the budget at once and are freed after RX_VK_RETIRE_SECS.

Measured on Windows/NVIDIA in play: mapped pages ~2.6 GB (was 7.9 GB),
working set ~13.9 GB (was ~20 GB), no crash. Untested on AMD/Intel and
in sessions over 30 minutes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
HumanBean added this to the v3.0.0 milestone 2026-11-14 00:58:08 +00:00
HumanBean requested changes 2026-11-14 01:02:58 +00:00
Dismissed
HumanBean left a comment

Code review of 40caf46f (read only: nothing built or run here). No comments or test-request results on the PR yet; the CI jobs for this head are still pending (only the description check has run, and passed).

The idea is right and the numbers are convincing for Windows/NVIDIA: telling the game what it really has mapped is the fix for the growth, and lazy heap commit is the right answer to Windows charging the whole 6.9 GB heap against RAM + page file up front. The ReadFile/WriteFile commits cover the only kernel-mode writes into guest memory I can find in host/ (Vulkan imports already commit their range in vk_memory.cpp), and the first-touch race fix is sound.

Must change before merge

  • hos_used_memory() can return more than hos_total_memory (nothing refuses AllocateMemoryPages by default, and B2 already sits ~350 MB under the line). The game's total - used then wraps to a huge free figure and the growth comes back. Clamp it (inline).
  • "Linux unchanged" does not hold: the used-memory report and memory_guest_imported's new evicted-pool check apply on every platform. That needs one Linux run before merge (the PR already lists it; the request isn't opened yet).
  • RX_VK_IMPORT_HIGH=85 on Windows turns the live-import valve on for AMD and Intel, where it was turned off because of device losses (issues 17, 309, 571 per the comment). The GRACE wait is a plausible cure but is untested on those vendors. It needs a Windows AMD and/or Intel run before it ships as a default, or keep it NVIDIA-only for now.
  • Housekeeping the checklist already tracks: rebase on main and re-measure against main @ <hash>, tests/run_all.py, docs for the four switches plus RX_VK_RETIRE_SECS (now read by this valve too and missing from Flags and settings), and the tested-configurations row. Two comments in vk_memory.cpp (lines 3407 and 3425) still say the valve is off on Windows. Note that tests/test_texel_buffer_ownership.py cuts imports_give_back at the text // (longshot) Off by default, so if you reword that comment, update the test in the same commit.

Suggestions (inline): a failed lazy commit in read_at goes on into a ReadFile that will fail; the first-touch retry should not apply to execute faults; the 16 MB static bitmap is charged to commit on Windows.

v3.0.0: not as it stands. It changes what the game sees on every platform and turns on a valve on the two vendors where it caused device losses, with one GPU measured. It can go in if the Linux and AMD/Intel runs come back clean in time. Otherwise, split it: the used-memory report plus the lazy heap after a Linux run, and the Windows valve default after AMD/Intel runs.

Code review of 40caf46f (read only: nothing built or run here). No comments or test-request results on the PR yet; the CI jobs for this head are still pending (only the description check has run, and passed). The idea is right and the numbers are convincing for Windows/NVIDIA: telling the game what it really has mapped is the fix for the growth, and lazy heap commit is the right answer to Windows charging the whole 6.9 GB heap against RAM + page file up front. The ReadFile/WriteFile commits cover the only kernel-mode writes into guest memory I can find in `host/` (Vulkan imports already commit their range in `vk_memory.cpp`), and the first-touch race fix is sound. **Must change before merge** - `hos_used_memory()` can return more than `hos_total_memory` (nothing refuses `AllocateMemoryPages` by default, and B2 already sits ~350 MB under the line). The game's `total - used` then wraps to a huge free figure and the growth comes back. Clamp it (inline). - "Linux unchanged" does not hold: the used-memory report and `memory_guest_imported`'s new evicted-pool check apply on every platform. That needs one Linux run before merge (the PR already lists it; the request isn't opened yet). - `RX_VK_IMPORT_HIGH=85` on Windows turns the live-import valve on for AMD and Intel, where it was turned off because of device losses (issues 17, 309, 571 per the comment). The GRACE wait is a plausible cure but is untested on those vendors. It needs a Windows AMD and/or Intel run before it ships as a default, or keep it NVIDIA-only for now. - Housekeeping the checklist already tracks: rebase on main and re-measure against `main @ <hash>`, `tests/run_all.py`, docs for the four switches plus `RX_VK_RETIRE_SECS` (now read by this valve too and missing from Flags and settings), and the tested-configurations row. Two comments in `vk_memory.cpp` (lines 3407 and 3425) still say the valve is off on Windows. Note that `tests/test_texel_buffer_ownership.py` cuts `imports_give_back` at the text `// (longshot) Off by default`, so if you reword that comment, update the test in the same commit. **Suggestions** (inline): a failed lazy commit in `read_at` goes on into a `ReadFile` that will fail; the first-touch retry should not apply to execute faults; the 16 MB static bitmap is charged to commit on Windows. **v3.0.0:** not as it stands. It changes what the game sees on every platform and turns on a valve on the two vendors where it caused device losses, with one GPU measured. It can go in if the Linux and AMD/Intel runs come back clean in time. Otherwise, split it: the used-memory report plus the lazy heap after a Linux run, and the Windows valve default after AMD/Intel runs.
@ -3429,1 +3435,4 @@
// after a wait there (retired_mode GRACE) without them, so the pools this valve takes wait the same way: held, the table
// already off them, until RX_VK_RETIRE_SECS (30) have passed since, and only then queued for the GPU (retired_value). What
// waits is counted as given back, so the valve does not take more for the same bytes. RX_VK_IMPORT_HIGH=0: off, as before
static const int high = std::clamp(env_int("RX_VK_IMPORT_HIGH", 85), 0, 100);
Owner

This is the default that changes for Windows AMD and Intel, where the valve was off because of device losses right after imports went back (the comment at 3425, still unchanged, says so, as does 3407). The GRACE wait mirrors what fixed the retired pools, but nothing in the PR has run on either vendor. It needs a Windows AMD/Intel run before it ships as a default. Until then, 85 for NVIDIA only (dev.props.vendorID == 0x10DE) would keep the measured gain. Please also update the two stale comments, and the test's // (longshot) Off by default cut marker if you reword 3425.

This is the default that changes for Windows AMD and Intel, where the valve was off because of device losses right after imports went back (the comment at 3425, still unchanged, says so, as does 3407). The GRACE wait mirrors what fixed the retired pools, but nothing in the PR has run on either vendor. It needs a Windows AMD/Intel run before it ships as a default. Until then, `85` for NVIDIA only (`dev.props.vendorID == 0x10DE`) would keep the measured gain. Please also update the two stale comments, and the test's `// (longshot) Off by default` cut marker if you reword 3425.
host/hos_mem.c Outdated
@ -242,0 +247,4 @@
#if defined(_WIN32)
static int commit_all = -1;
if (commit_all < 0) commit_all = env_on("HOS_HEAP_COMMIT");
if (size > hos_heap_size && commit_all) hos_commit(HOS_HEAP_BASE + hos_heap_size, size - hos_heap_size);
Owner

Windows commit semantics are right here: committed-but-untouched pages still count against RAM + page file, and reserve does not. What changes is where a refusal at the commit limit appears. Before, it was one failure at SetMemoryHeapSize. Now it is any first touch, on any thread, mid-play (a 5 s commit_wait stall, then a crash report plus the out-of-memory box). That is a fair trade for ~5 GB less commit, but please state it under Risk and rollback.

Windows commit semantics are right here: committed-but-untouched pages still count against RAM + page file, and reserve does not. What changes is where a refusal at the commit limit appears. Before, it was one failure at `SetMemoryHeapSize`. Now it is any first touch, on any thread, mid-play (a 5 s `commit_wait` stall, then a crash report plus the out-of-memory box). That is a fair trade for ~5 GB less commit, but please state it under Risk and rollback.
host/hos_mem.c Outdated
@ -280,0 +294,4 @@
memory resident, a working set of 19.6 GB, against the console's 12 GB in all). One bit a 4 KB page of the guest window,
so that pages mapped twice or freed twice count once. HOS_PAGES_LIMIT=1: past the console's total (heap + pages),
AllocateMemoryPages fails as it would there; unset, only counted and logged */
static uint64_t mapped_bits[(HOS_GUEST_SPACE >> 12) / 64];
Owner

Nit: 512 GB / 4 KB / 8 = 16 MB of .bss. On Windows, image sections are charged to commit when the image loads, so this adds 16 MB of commit to a PR that is about commit. A two-level map, or one bit per 64 KB granule, would cost almost nothing. Not a blocker.

Nit: 512 GB / 4 KB / 8 = 16 MB of `.bss`. On Windows, image sections are charged to commit when the image loads, so this adds 16 MB of commit to a PR that is about commit. A two-level map, or one bit per 64 KB granule, would cost almost nothing. Not a blocker.
host/hos_mem.c Outdated
@ -280,0 +328,4 @@
LOCK();
const uint64_t mapped = mapped_bytes;
UNLOCK();
return base + hos_heap_size + mapped;
Owner

This can exceed hos_total_memory: AllocateMemoryPages is not refused by default (HOS_PAGES_LIMIT off), and B2 already ends ~350 MB from the line. Past it, the game's free = total - used (0x8080f010) wraps to a huge figure, and it maps without limit again, which is the bug this PR fixes. Clamp it: const uint64_t used = base + hos_heap_size + mapped; return used < hos_total_memory ? used : hos_total_memory; (GetInfo 7/22 get the same value).

This can exceed `hos_total_memory`: `AllocateMemoryPages` is not refused by default (`HOS_PAGES_LIMIT` off), and B2 already ends ~350 MB from the line. Past it, the game's `free = total - used` (0x8080f010) wraps to a huge figure, and it maps without limit again, which is the bug this PR fixes. Clamp it: `const uint64_t used = base + hos_heap_size + mapped; return used < hos_total_memory ? used : hos_total_memory;` (GetInfo 7/22 get the same value).
host/mkw_glue.c Outdated
@ -88,0 +87,4 @@
if (!VirtualQuery(p, &mbi, sizeof mbi)) return EXCEPTION_CONTINUE_SEARCH;
/* (another thread committed this 64 KB between the fault and the look: the access is tried again. The heap is committed on
touch now (hos_mem.c), and two threads touching one new chunk at once crashed the second in the boot's module loads) */
if (mbi.State == MEM_COMMIT && mbi.Protect == PAGE_READWRITE) return EXCEPTION_CONTINUE_EXECUTION;
Owner

Right for the race. One edge: an execute fault (ExceptionInformation[0] == 8, DEP) on a committed RW guest page would now return CONTINUE_EXECUTION forever: a host jump into the guest window spins at 100% instead of reaching report_crash. Check rec->ExceptionInformation[0] != 8 before retrying.

Right for the race. One edge: an execute fault (`ExceptionInformation[0] == 8`, DEP) on a committed RW guest page would now return CONTINUE_EXECUTION forever: a host jump into the guest window spins at 100% instead of reaching `report_crash`. Check `rec->ExceptionInformation[0] != 8` before retrying.
host/nn_fs.c Outdated
@ -420,1 +420,4 @@
const int away = hos_vcore_away(); /* (as OpenFile: no core held through the read) */
/* (the kernel's ReadFile does not fault pages in: a heap page nothing touched yet (hos_mem.c, committed on touch) is
committed for it first, or the read fails with ERROR_NOACCESS) */
if (size && hos_guest_range(buf, size)) hos_commit(buf, size);
Owner

If this commit is refused (the commit limit after commit_wait, or any other error), hos_commit only logs, and the ReadFile below then fails with ERROR_NOACCESS and returns 0 bytes. The game gets a short read rather than a clear error. Consider having hos_commit return whether it succeeded, and returning 0 (or the out-of-memory result) without calling ReadFile when it fails. Same for WriteFile at 475. (On Linux this is a no-op inside the guest window, so it's fine there.)

If this commit is refused (the commit limit after `commit_wait`, or any other error), `hos_commit` only logs, and the `ReadFile` below then fails with ERROR_NOACCESS and returns 0 bytes. The game gets a short read rather than a clear error. Consider having `hos_commit` return whether it succeeded, and returning 0 (or the out-of-memory result) without calling `ReadFile` when it fails. Same for WriteFile at 475. (On Linux this is a no-op inside the guest window, so it's fine there.)
Member

A player's v3.0.0-rc.3 log looks like the case this PR is for, on another 32 GB PC. It ended in a device lost in Kakariko Graveyard (child, after using the magic bean spot), about 110,000 frames into the session.

PC: RTX 3060 Ti 8 GB, NVIDIA 621.01, Windows, 32 GB RAM, 2 GB page file (boot: 32689 MB of memory, 2048 MB of swap), so the commit limit is about 34.7 GB.

Memory over the session (from vk: memory: and vk: commit limit near:):

start middle last reading
Private memory 15,520 MB 24,616 MB 29,436 MB
Working set 7,132 MB 16,549 MB 14,083 MB (peak 20,882)
Commit left (reserve 2,560 MB) 2,544 MB ~2,100 MB 817 MB
VRAM 6.0 of 6.6 GB 6.6 of 6.6 GB
  • The commit warning shows 179 times, from the first minute on. Near the end the textures' budget was cut to 0–766 MB, which still didn't win back commit.
  • fps fell from about 60 to 20 over the session, and GPU time rose to 38.8 ms a frame.
  • gsys::ModelResourceMgr(GpuHeap) reached 0 KB free during area loads (the heap-full retries took 480 blocks, about 3 MB, from parent heaps).

The crash: submit 770398 failed (-4). Both checkpoints (stage 0x1 and 0x2000) stop at the same marker, a write-back of pool 0x103a800000. The next draw never reached the top of the pipe, so no shader in it hung. The log has no driver-side reason for the reset, so the tie to commit is very likely but not proven.

rc.3 has no mem: the game's mapped pages line, so the game's own share can't be split out here. Still, the private memory growth (+14 GB) is the pattern your A runs show. A 2 GB page file is a harsh case worth testing this PR on: with the heap committed lazily, it should keep this PC clear of the limit.

The log stays with us (it has the player's paths); I can share figures from it if you need more.

A player's v3.0.0-rc.3 log looks like the case this PR is for, on another 32 GB PC. It ended in a device lost in Kakariko Graveyard (child, after using the magic bean spot), about 110,000 frames into the session. **PC:** RTX 3060 Ti 8 GB, NVIDIA 621.01, Windows, 32 GB RAM, **2 GB page file** (`boot: 32689 MB of memory, 2048 MB of swap`), so the commit limit is about 34.7 GB. **Memory over the session** (from `vk: memory:` and `vk: commit limit near:`): | | start | middle | last reading | |---|---|---|---| | Private memory | 15,520 MB | 24,616 MB | 29,436 MB | | Working set | 7,132 MB | 16,549 MB | 14,083 MB (peak 20,882) | | Commit left (reserve 2,560 MB) | 2,544 MB | ~2,100 MB | **817 MB** | | VRAM | | 6.0 of 6.6 GB | 6.6 of 6.6 GB | - The commit warning shows 179 times, from the first minute on. Near the end the textures' budget was cut to 0–766 MB, which still didn't win back commit. - fps fell from about 60 to 20 over the session, and GPU time rose to 38.8 ms a frame. - `gsys::ModelResourceMgr(GpuHeap)` reached 0 KB free during area loads (the heap-full retries took 480 blocks, about 3 MB, from parent heaps). **The crash:** `submit 770398 failed (-4)`. Both checkpoints (stage 0x1 and 0x2000) stop at the same marker, a write-back of pool `0x103a800000`. The next draw never reached the top of the pipe, so no shader in it hung. The log has no driver-side reason for the reset, so the tie to commit is very likely but not proven. rc.3 has no `mem: the game's mapped pages` line, so the game's own share can't be split out here. Still, the private memory growth (+14 GB) is the pattern your A runs show. A 2 GB page file is a harsh case worth testing this PR on: with the heap committed lazily, it should keep this PC clear of the limit. The log stays with us (it has the player's paths); I can share figures from it if you need more.
HumanBean force-pushed fix/windows-guest-memory from 40caf46f06
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
to 25ca294064
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
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 8s
Third-party parts / As their notes say (pull_request) Successful in 12s
PR description / Description follows the template (pull_request_target) Successful in 3s
2026-11-14 01:29:33 +00:00
Compare
Owner

Review fixes pushed as 25ca2940 (a commit of its own on top of the author's, both rebased onto main 9fccd0fd; the author's commit is unchanged apart from the rebase). Per point of the review (#324 (comment)):

  1. hos_used_memory() is capped at hos_total_memory, so the game's free = total − used can't wrap (QueryMemoryInfo and GetInfo 7/22 get the same value).
  2. Description: Linux is not unchanged. The used-memory report and memory_guest_imported's count of pools given back but not yet freed apply everywhere; Risk and Not tested say so, and a Linux run is listed as needed.
  3. RX_VK_IMPORT_HIGH defaults to 85 on Windows NVIDIA only (dev.props.vendorID == 0x10DE); Windows AMD and Intel stay off unless the switch is set. Documented in the code, the Flags table and docs/development.md.
  4. Risk now states the commit-charge trade-off: a refused commit can show at any first touch, on any thread, mid-play, not once at SetMemoryHeapSize.
  5. hos_commit returns whether the range is committed; read_at and WriteFile in nn_fs.c return svc::ResultOutOfMemory (0xCA01) when it is refused, without calling ReadFile/WriteFile.
  6. commit_on_touch doesn't retry an execute fault (ExceptionInformation[0] == 8), so a jump into guest memory reaches the crash report.
  7. The mapped-page bitmap is allocated a GB of the window at a time (32 KB leaves, on first map) instead of 16 MB of static data.
  8. Rebased; the five switches (and the related RX_VK_IMPORT_LOW/CAP_MB, RX_VK_RETIRE_MIN_SECS) are in docs/development.md; RX_VK_RETIRE_SECS is in the Flags table; the two comments in vk_memory.cpp that said the valve is off on Windows are updated, and tests/test_texel_buffer_ownership.py cuts at the reworded one. tests/test_commit_limit.py cuts hos_commit by its new int signature.

Nothing was built or run for this push: not compiled, no tests/run_all.py, no game run. The measurements in the description are still 40caf46f's. Still needed before ready: a build on both Windows and Linux, tests/run_all.py, a Linux run, Windows AMD and Intel runs (defaults, and with RX_VK_IMPORT_HIGH=85 if the valve should go on there), and the Windows NVIDIA A/B measured again on 25ca2940 with a tested-configurations row. It stays WIP.

Review fixes pushed as 25ca2940 (a commit of its own on top of the author's, both rebased onto main 9fccd0fd; the author's commit is unchanged apart from the rebase). Per point of the review (https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/324#issuecomment-15681): 1. `hos_used_memory()` is capped at `hos_total_memory`, so the game's free = total − used can't wrap (QueryMemoryInfo and GetInfo 7/22 get the same value). 2. Description: Linux is not unchanged. The used-memory report and `memory_guest_imported`'s count of pools given back but not yet freed apply everywhere; Risk and Not tested say so, and a Linux run is listed as needed. 3. `RX_VK_IMPORT_HIGH` defaults to 85 on Windows NVIDIA only (`dev.props.vendorID == 0x10DE`); Windows AMD and Intel stay off unless the switch is set. Documented in the code, the Flags table and docs/development.md. 4. Risk now states the commit-charge trade-off: a refused commit can show at any first touch, on any thread, mid-play, not once at `SetMemoryHeapSize`. 5. `hos_commit` returns whether the range is committed; `read_at` and `WriteFile` in nn_fs.c return `svc::ResultOutOfMemory` (0xCA01) when it is refused, without calling ReadFile/WriteFile. 6. `commit_on_touch` doesn't retry an execute fault (`ExceptionInformation[0] == 8`), so a jump into guest memory reaches the crash report. 7. The mapped-page bitmap is allocated a GB of the window at a time (32 KB leaves, on first map) instead of 16 MB of static data. 8. Rebased; the five switches (and the related `RX_VK_IMPORT_LOW`/`CAP_MB`, `RX_VK_RETIRE_MIN_SECS`) are in docs/development.md; `RX_VK_RETIRE_SECS` is in the Flags table; the two comments in vk_memory.cpp that said the valve is off on Windows are updated, and `tests/test_texel_buffer_ownership.py` cuts at the reworded one. `tests/test_commit_limit.py` cuts `hos_commit` by its new `int` signature. Nothing was built or run for this push: not compiled, no `tests/run_all.py`, no game run. The measurements in the description are still 40caf46f's. Still needed before ready: a build on both Windows and Linux, `tests/run_all.py`, a Linux run, Windows AMD and Intel runs (defaults, and with `RX_VK_IMPORT_HIGH=85` if the valve should go on there), and the Windows NVIDIA A/B measured again on 25ca2940 with a tested-configurations row. It stays WIP.
HumanBean requested changes 2026-11-14 01:36:03 +00:00
Dismissed
HumanBean left a comment

Re-review of 25ca2940 (our fix commit) and of the rebase, read only: nothing built or run here. Read first: the description, llun's log comment and the fix comment (#324 (comment)). No test-request results yet.

Rebase (4f4502a9): intact. Its diff against 9fccd0fd and 40caf46f's diff against ae96bad3 have the same added and removed lines in each of the six files (sorted per file), and 4f4502a9's parent is 9fccd0fd, so nothing of main is reverted. Main has since moved to 351affd9 (four renderer commits touching other hunks of vk_memory.cpp and docs/development.md): a rebase before ready, not now.

CI on 25ca2940: nothing compiled it. Only the title, actionlint, third-party notes and description checks ran (all passed); the Python source checks, the SPIR-V fixtures and the third-party upstream check were skipped.

The review's points on 25ca2940:

  1. Used memory capped at hos_total_memory: fixed. GetInfo 6/21 and QueryMemoryInfo +0x00 report that same total, so free can't wrap; the HOS_MEMINFO_OLD path is unchanged.
  2. "Linux unchanged": fixed in the description.
  3. Valve default: fixed. 85 on Windows only for dev.props.vendorID == 0x10DE, the same test retired_mode() already uses. dev is set by then because imports_give_back first runs from memory_begin_submission. Linux stays 85, and test_retired_give_back.py's needles still match.
  4. Commit-charge trade-off: it is now under Risk.
  5. The fs commit errors are right. All three ReadFile entry points pass &oom, and read_at has no other callers. The refused path gives the core back (hos_vcore_back) before it returns. WriteFile's explicit return is needed, since RET only sets x0. The other hos_commit callers (hos_mem.c ×5, hos_svc.c, hos_main.c) ignore the new int, which is fine in C. No other declaration of it exists. On Linux, the shim's in-window commit returns the address, so the result is always 1. See the nit inline about which result 0xCA01 is.
  6. Execute faults: fixed on Windows only; on Linux the retry turns every fault in the guest window into a hang (inline, must fix).
  7. Bitmap: fine. Every pages_count_locked call holds the hos_mem lock, so leaves are allocated under it. Nothing touches the bitmap from the exception handler, so nothing is allocated in a vectored handler. On a failed calloc the pages are neither counted nor marked, so mapping and freeing stay consistent and the report errs low. The map == 2 probe counts a missing leaf as all new. Sizes: 512 × 8 B static, 32 KB a leaf.
  8. Housekeeping: done. The docs rows match the code (HOS_HEAP_COMMIT is under #if _WIN32; RX_VK_RETIRE_SECS is read only in GRACE). The two stale comments are reworded. The test cuts match the new source exactly: "int hos_commit(uint64_t addr, uint64_t size)", " // (longshot) Default by OS and vendor" (the #ifdef falls after the cut, so the stubbed compile doesn't need dev), and test_guest_copies.py's two needles in read_at/WriteFile are still present.

Must change: the Linux hang (inline on host/mkw_glue.c). Point 6 was fixed with a test the Linux shim never meets, and the underlying regression is the author's retry, which our first review called sound without looking at Linux. Until it is fixed, "Linux run needed" can't come back clean on a bad fault.

Still needed before ready, once that is fixed: a build on Windows (llvm-mingw) and Linux; tests/run_all.py; a Linux run; Windows AMD and Intel runs (defaults, and with RX_VK_IMPORT_HIGH=85 if the valve is to go on there); the Windows NVIDIA A/B measured again on the new head with a tested-configurations row; the three test requests opened; a rebase on main.

Re-review of 25ca2940 (our fix commit) and of the rebase, read only: nothing built or run here. Read first: the description, llun's log comment and the fix comment (https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/324#issuecomment-15857). No test-request results yet. **Rebase (4f4502a9):** intact. Its diff against 9fccd0fd and 40caf46f's diff against ae96bad3 have the same added and removed lines in each of the six files (sorted per file), and 4f4502a9's parent is 9fccd0fd, so nothing of main is reverted. Main has since moved to 351affd9 (four renderer commits touching other hunks of `vk_memory.cpp` and `docs/development.md`): a rebase before ready, not now. **CI on 25ca2940:** nothing compiled it. Only the title, actionlint, third-party notes and description checks ran (all passed); the Python source checks, the SPIR-V fixtures and the third-party upstream check were skipped. **The review's points on 25ca2940:** 1. Used memory capped at `hos_total_memory`: fixed. GetInfo 6/21 and QueryMemoryInfo +0x00 report that same total, so free can't wrap; the `HOS_MEMINFO_OLD` path is unchanged. 2. "Linux unchanged": fixed in the description. 3. Valve default: fixed. 85 on Windows only for `dev.props.vendorID == 0x10DE`, the same test `retired_mode()` already uses. `dev` is set by then because `imports_give_back` first runs from `memory_begin_submission`. Linux stays 85, and `test_retired_give_back.py`'s needles still match. 4. Commit-charge trade-off: it is now under Risk. 5. The fs commit errors are right. All three `ReadFile` entry points pass `&oom`, and `read_at` has no other callers. The refused path gives the core back (`hos_vcore_back`) before it returns. `WriteFile`'s explicit `return` is needed, since `RET` only sets x0. The other `hos_commit` callers (hos_mem.c ×5, hos_svc.c, hos_main.c) ignore the new `int`, which is fine in C. No other declaration of it exists. On Linux, the shim's in-window commit returns the address, so the result is always 1. See the nit inline about which result 0xCA01 is. 6. Execute faults: **fixed on Windows only; on Linux the retry turns every fault in the guest window into a hang** (inline, must fix). 7. Bitmap: fine. Every `pages_count_locked` call holds the hos_mem lock, so leaves are allocated under it. Nothing touches the bitmap from the exception handler, so nothing is allocated in a vectored handler. On a failed `calloc` the pages are neither counted nor marked, so mapping and freeing stay consistent and the report errs low. The `map == 2` probe counts a missing leaf as all new. Sizes: 512 × 8 B static, 32 KB a leaf. 8. Housekeeping: done. The docs rows match the code (`HOS_HEAP_COMMIT` is under `#if _WIN32`; `RX_VK_RETIRE_SECS` is read only in GRACE). The two stale comments are reworded. The test cuts match the new source exactly: `"int hos_commit(uint64_t addr, uint64_t size)"`, `" // (longshot) Default by OS and vendor"` (the `#ifdef` falls after the cut, so the stubbed compile doesn't need `dev`), and `test_guest_copies.py`'s two needles in `read_at`/`WriteFile` are still present. **Must change:** the Linux hang (inline on `host/mkw_glue.c`). Point 6 was fixed with a test the Linux shim never meets, and the underlying regression is the author's retry, which our first review called sound without looking at Linux. Until it is fixed, "Linux run needed" can't come back clean on a bad fault. **Still needed before ready, once that is fixed:** a build on Windows (llvm-mingw) and Linux; `tests/run_all.py`; a Linux run; Windows AMD and Intel runs (defaults, and with `RX_VK_IMPORT_HIGH=85` if the valve is to go on there); the Windows NVIDIA A/B measured again on the new head with a tested-configurations row; the three test requests opened; a rebase on main.
host/mkw_glue.c Outdated
@ -88,0 +91,4 @@
/* (another thread committed this 64 KB between the fault and the look: the access is tried again. The heap is committed on
touch now (hos_mem.c), and two threads touching one new chunk at once crashed the second in the boot's module loads.
Not an execute fault (8, DEP): a jump into guest memory would fault again at once, forever; it goes on to the crash report) */
if (mbi.State == MEM_COMMIT && mbi.Protect == PAGE_READWRITE && rec->ExceptionInformation[0] != 8)
Owner

Linux: this turns every fault in the guest window into an endless loop. The POSIX shim's VirtualQuery reports every guest page as MEM_COMMIT + PAGE_READWRITE without asking the kernel (host/posix/win32_posix.c, 'the guest window ... which VirtualQuery reports as committed'). on_signal sets ExceptionInformation[0] only to 0 or 1 (REG_ERR & 2), never 8. So on Linux any SIGSEGV or SIGBUS above guest 0x100000 returns CONTINUE_EXECUTION here, and the faulting instruction runs again forever at 100% CPU. That covers a host jump into guest memory (NX), and a SIGBUS from the memfd-backed window when memory runs out. On main, State != MEM_RESERVE sent all of these on to report_crash. The != 8 check fixes it for Windows only.

Linux has no lazy commit, so the race this retry is for can't happen there. Simplest fix: put the retry under #ifdef _WIN32. Optionally, have the shim also set 8 when REG_ERR & 0x10 (an instruction fetch), so the execute test means the same on both, but SIGBUS would still need the #ifdef. If test_commit_limit.py should guard it, a needle on the #ifdef would do.

**Linux: this turns every fault in the guest window into an endless loop.** The POSIX shim's `VirtualQuery` reports every guest page as `MEM_COMMIT` + `PAGE_READWRITE` without asking the kernel (`host/posix/win32_posix.c`, 'the guest window ... which VirtualQuery reports as committed'). `on_signal` sets `ExceptionInformation[0]` only to 0 or 1 (`REG_ERR & 2`), never 8. So on Linux any SIGSEGV or SIGBUS above guest 0x100000 returns CONTINUE_EXECUTION here, and the faulting instruction runs again forever at 100% CPU. That covers a host jump into guest memory (NX), and a SIGBUS from the memfd-backed window when memory runs out. On main, `State != MEM_RESERVE` sent all of these on to `report_crash`. The `!= 8` check fixes it for Windows only. Linux has no lazy commit, so the race this retry is for can't happen there. Simplest fix: put the retry under `#ifdef _WIN32`. Optionally, have the shim also set 8 when `REG_ERR & 0x10` (an instruction fetch), so the execute test means the same on both, but SIGBUS would still need the `#ifdef`. If `test_commit_limit.py` should guard it, a needle on the `#ifdef` would do.
host/nn_fs.c Outdated
@ -87,6 +87,7 @@ const char *hos_romfs_dir, *hos_user_dir;
#define R_PATH_EXISTS 0x402 /* fs::ResultPathAlreadyExists */
#define R_NOT_MOUNTED 0xCA02 /* fs::ResultInvalidMountName-ish: no such mount */
#define R_TARGET_NOT_FOUND 0x7D402 /* fs::ResultTargetNotFound (save data) */
#define R_OUT_OF_MEMORY 0xCA01 /* svc::ResultOutOfMemory: the guest buffer could not be committed (Windows) */
Owner

Nit: by Horizon's numbering, 0xCA01 is kernel (module 1) description 101, svc::ResultInvalidSize; svc::ResultOutOfMemory is description 104, 0xD001 (Atmosphère's svc_results.hpp, yuzu's svc_results.h). The label is inherited: hos_mem.c and hos_svc.c already return 0xCA01 under the same comment. So this follows the house convention, and fixing them all is out of scope. Either way the game gets a failure where before it got a short read (the first ReadFile returned fs::ResultOutOfRange; the other two returned success with fewer bytes). I can't tell from here what OoT does with either. Worth fixing the comment (or using 0xD001) and the description's wording, so nobody relies on the game treating it as out of memory.

Nit: by Horizon's numbering, 0xCA01 is kernel (module 1) description 101, `svc::ResultInvalidSize`; `svc::ResultOutOfMemory` is description 104, 0xD001 (Atmosphère's `svc_results.hpp`, yuzu's `svc_results.h`). The label is inherited: `hos_mem.c` and `hos_svc.c` already return 0xCA01 under the same comment. So this follows the house convention, and fixing them all is out of scope. Either way the game gets a failure where before it got a short read (the first `ReadFile` returned `fs::ResultOutOfRange`; the other two returned success with fewer bytes). I can't tell from here what OoT does with either. Worth fixing the comment (or using 0xD001) and the description's wording, so nobody relies on the game treating it as out of memory.
HumanBean force-pushed fix/windows-guest-memory from 25ca294064
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
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 8s
Third-party parts / As their notes say (pull_request) Successful in 12s
PR description / Description follows the template (pull_request_target) Successful in 3s
to 5e8a89b8fa
Some checks failed
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
CI / Workflow lint (actionlint) (pull_request) Successful in 7s
Third-party parts / As their notes say (pull_request) Successful in 8s
PR description / Description follows the template (pull_request_target) Failing after 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
2026-11-14 01:38:08 +00:00
Compare
Owner

Pushed 5e8a89b8 on main 351affd9 (rebased, linear; the author's commit and 58dc267c, formerly 25ca2940, have the same +/- lines as before). It fixes the Linux hang from the re-review (#324 (comment)): commit_on_touch's retry of a page another thread committed is now under #ifdef _WIN32. On Linux the VirtualQuery stand-in calls every guest page committed and read-write and the signal handler never sets 8, so every SIGSEGV/SIGBUS in the guest window looped at 100% CPU; now they reach report_crash as on main. tests/test_commit_limit.py checks that the retry is there and under #ifdef _WIN32 (no #else/#elif around it). host/nn_fs.c gets a one-line comment that 0xCA01 is the house's out-of-memory code and svc::ResultInvalidSize by Horizon's numbering; the label is unchanged. The description says 0xCA01 rather than svc::ResultOutOfMemory, and names the new head. Nothing was built or run here: not compiled, no tests, not run in the game. Still WIP.

Pushed 5e8a89b8 on main 351affd9 (rebased, linear; the author's commit and 58dc267c, formerly 25ca2940, have the same +/- lines as before). It fixes the Linux hang from the re-review (https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/324#issuecomment-15893): `commit_on_touch`'s retry of a page another thread committed is now under `#ifdef _WIN32`. On Linux the VirtualQuery stand-in calls every guest page committed and read-write and the signal handler never sets 8, so every SIGSEGV/SIGBUS in the guest window looped at 100% CPU; now they reach report_crash as on main. `tests/test_commit_limit.py` checks that the retry is there and under `#ifdef _WIN32` (no #else/#elif around it). `host/nn_fs.c` gets a one-line comment that 0xCA01 is the house's out-of-memory code and `svc::ResultInvalidSize` by Horizon's numbering; the label is unchanged. The description says 0xCA01 rather than svc::ResultOutOfMemory, and names the new head. Nothing was built or run here: not compiled, no tests, not run in the game. Still WIP.
HumanBean left a comment

Light re-review of 5e8a89b8 (the follow-up to review 1225). Both points are fixed; nothing new found.

Linux reaches report_crash again. On Linux the guest window is one read-write mapping, so the VirtualQuery stand-in in host/posix/win32_posix.c always answers MEM_COMMIT/PAGE_READWRITE there. With the retry now inside #ifdef _WIN32, a SIGSEGV or SIGBUS in the window goes past the null-page check and then hits mbi.State != MEM_RESERVE, which returns EXCEPTION_CONTINUE_SEARCH. The signal shim then passes it to the unhandled filter, report_crash. That is the same path as commit_on_touch on main (351affd9), where both checks sat on one line.

Windows is unchanged. The code inside the #ifdef is the same as in 58dc267c; only the comment grew. The preprocessor block doesn't cross any brace. rec and mbi are still used outside it, so Linux gets no unused-variable warning.

The test is tighter, not looser. tests/test_commit_limit.py only adds lines. Its retry string matches the source byte for byte and must appear exactly once. The check fails if the nearest #ifdef _WIN32 above it is followed by #endif, #else or #elif before the retry, or if return EXCEPTION_CONTINUE_EXECUTION; comes after the closing #endif.

The 0xCA01 nit is handled. The new comment above R_OUT_OF_MEMORY names it as Horizon's svc::ResultInvalidSize. Optional: the define's own trailing comment still says svc::ResultOutOfMemory, so the two disagree. Rewording that trailing comment would remove the contradiction.

The rebase onto 351affd9 is clean. The +/- lines of 2e8cbd66 and 58dc267c are identical to those of 4f4502a9 and 25ca2940.

CI on 5e8a89b8 is green, but light. The title, actionlint, third-party notes and description template checks pass. The Python source checks, the SPIR-V fixtures and the third-party upstream check were skipped on this WIP run, so test_commit_limit.py hasn't run in CI yet.

Still to do before this leaves WIP:

  • Builds on both platforms (llvm-mingw Windows, Linux clang/gcc).
  • python -I -B tests/run_all.py, or CI's Python source checks once they run on this PR.
  • A Linux run in the game. It should show no regression, and a deliberate guest fault should still give a CRASH: log line.
  • Windows runs on AMD and Intel: heap committed on touch, the used-memory figure, nothing crashing at boot.
  • An NVIDIA Windows re-measure of the import valve's new default, before and after on the same scene.
  • Test requests for the hardware we don't have (Windows AMD/Intel, Linux).

Light review, code only: nothing was built or run.

Light re-review of 5e8a89b8 (the follow-up to [review 1225](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/324#issuecomment-15893)). **Both points are fixed; nothing new found.** **Linux reaches report_crash again.** On Linux the guest window is one read-write mapping, so the `VirtualQuery` stand-in in `host/posix/win32_posix.c` always answers `MEM_COMMIT`/`PAGE_READWRITE` there. With the retry now inside `#ifdef _WIN32`, a SIGSEGV or SIGBUS in the window goes past the null-page check and then hits `mbi.State != MEM_RESERVE`, which returns `EXCEPTION_CONTINUE_SEARCH`. The signal shim then passes it to the unhandled filter, `report_crash`. That is the same path as `commit_on_touch` on main (351affd9), where both checks sat on one line. **Windows is unchanged.** The code inside the `#ifdef` is the same as in 58dc267c; only the comment grew. The preprocessor block doesn't cross any brace. `rec` and `mbi` are still used outside it, so Linux gets no unused-variable warning. **The test is tighter, not looser.** `tests/test_commit_limit.py` only adds lines. Its retry string matches the source byte for byte and must appear exactly once. The check fails if the nearest `#ifdef _WIN32` above it is followed by `#endif`, `#else` or `#elif` before the retry, or if `return EXCEPTION_CONTINUE_EXECUTION;` comes after the closing `#endif`. **The 0xCA01 nit is handled.** The new comment above `R_OUT_OF_MEMORY` names it as Horizon's `svc::ResultInvalidSize`. Optional: the define's own trailing comment still says `svc::ResultOutOfMemory`, so the two disagree. Rewording that trailing comment would remove the contradiction. **The rebase onto 351affd9 is clean.** The +/- lines of 2e8cbd66 and 58dc267c are identical to those of 4f4502a9 and 25ca2940. **CI on 5e8a89b8 is green, but light.** The title, actionlint, third-party notes and description template checks pass. The Python source checks, the SPIR-V fixtures and the third-party upstream check were skipped on this WIP run, so `test_commit_limit.py` hasn't run in CI yet. **Still to do before this leaves WIP:** - Builds on both platforms (llvm-mingw Windows, Linux clang/gcc). - `python -I -B tests/run_all.py`, or CI's Python source checks once they run on this PR. - A Linux run in the game. It should show no regression, and a deliberate guest fault should still give a `CRASH:` log line. - Windows runs on AMD and Intel: heap committed on touch, the used-memory figure, nothing crashing at boot. - An NVIDIA Windows re-measure of the import valve's new default, before and after on the same scene. - Test requests for the hardware we don't have (Windows AMD/Intel, Linux). Light review, code only: nothing was built or run.
HumanBean dismissed HumanBean's review 2026-11-14 01:40:04 +00:00
Reason:

Superseded by #324 (comment): the points it asked for are fixed; what's left is listed there.

HumanBean dismissed HumanBean's review 2026-11-14 01:40:06 +00:00
Reason:

Superseded by #324 (comment): the points it asked for are fixed; what's left is listed there.

HumanBean changed title from WIP: fix(hos): report the game's mapped pages as used memory; commit the heap lazily on Windows to fix(hos): report the game's mapped pages as used memory; commit the heap lazily on Windows 2026-11-14 01:40:32 +00:00
HumanBean deleted branch fix/windows-guest-memory 2026-11-14 01:40:37 +00:00
First-time contributor

Windows/NVIDIA flicker follow-up from the Zora's Fountain report in issue #366. I am testing the three old-behaviour switches from this PR in the same scene now; this comment records the evidence already obtained, not a result of that new test.

On Longshot nightly-127-cf605c85 (source 8672daee), RTX 4050 Laptop 6 GB / NVIDIA 621.14 / Windows 11 build 26200, with frame generation Off, Link briefly becomes a near-black silhouette in ZoraFountain_Child. A same-save, same-place A-B-A observation was: default RX_VK_LATE_FIX on -> flicker; one 1-2 minute launch with RX_VK_LATE_FIX=0 -> no flicker seen; default on again -> flicker. In a further run, RX_VK_LATE_FIX=1 with RX_VK_MIRROR_SPARSE=0 still flickered. The player did not observe it in Jabu-Jabu's Belly during that session. Details and a three-frame crop are in issue #366.

The late-pages path added by PR #248 is therefore a narrower lead on this PC. These short runs do not establish the defective source line or exclude an interaction with PR #324: disabling late_fix also changes GPU/CPU timing. I will report the result of HOS_MEMINFO_OLD=1, HOS_HEAP_COMMIT=1, RX_VK_IMPORT_HIGH=0 with late_fix left on as a separate follow-up.

Windows/NVIDIA flicker follow-up from the Zora's Fountain report in [issue #366](https://git.magicbean.dev/MagicBeanDev/Longshot/issues/366#issuecomment-18628). I am testing the three old-behaviour switches from this PR in the same scene now; this comment records the evidence already obtained, not a result of that new test. On Longshot nightly-127-cf605c85 (source 8672daee), RTX 4050 Laptop 6 GB / NVIDIA 621.14 / Windows 11 build 26200, with frame generation Off, Link briefly becomes a near-black silhouette in `ZoraFountain_Child`. A same-save, same-place A-B-A observation was: default `RX_VK_LATE_FIX` on -> flicker; one 1-2 minute launch with `RX_VK_LATE_FIX=0` -> no flicker seen; default on again -> flicker. In a further run, `RX_VK_LATE_FIX=1` with `RX_VK_MIRROR_SPARSE=0` still flickered. The player did not observe it in Jabu-Jabu's Belly during that session. Details and a three-frame crop are in issue #366. The late-pages path added by [PR #248](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/248) is therefore a narrower lead on this PC. These short runs do not establish the defective source line or exclude an interaction with PR #324: disabling late_fix also changes GPU/CPU timing. I will report the result of `HOS_MEMINFO_OLD=1`, `HOS_HEAP_COMMIT=1`, `RX_VK_IMPORT_HIGH=0` with late_fix left on as a separate follow-up.
First-time contributor

Follow-up to my note above: the black Link flicker still occurred in the same ZoraFountain_Child save with all three PR #324 old-behaviour switches set together: HOS_MEMINFO_OLD=1, HOS_HEAP_COMMIT=1, and RX_VK_IMPORT_HIGH=0. This launch also had RX_VK_LATE_FIX=1, RX_VK_FRAMEGEN=0, and RX_VK_MIRROR_SPARSE=0. The log confirms nightly-127-cf605c85, late pages' fix on, and the Zora map load. The player observed the near-black Link flicker during the 1-2 minute same-place check.

This is one run, not a per-switch or version bisect. It shows that none of PR #324's three new behaviours is required for this symptom on this PC when all three are reverted together. The late-pages path remains the more specific lead; its exact faulty operation is still unproven. I will continue diagnosis in issue #366.

Follow-up to [my note above](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/324#issuecomment-18713): **the black Link flicker still occurred** in the same `ZoraFountain_Child` save with all three PR #324 old-behaviour switches set together: `HOS_MEMINFO_OLD=1`, `HOS_HEAP_COMMIT=1`, and `RX_VK_IMPORT_HIGH=0`. This launch also had `RX_VK_LATE_FIX=1`, `RX_VK_FRAMEGEN=0`, and `RX_VK_MIRROR_SPARSE=0`. The log confirms nightly-127-cf605c85, `late pages' fix on`, and the Zora map load. The player observed the near-black Link flicker during the 1-2 minute same-place check. This is one run, not a per-switch or version bisect. It shows that none of PR #324's three new behaviours is required for this symptom on this PC when all three are reverted together. The late-pages path remains the more specific lead; its exact faulty operation is still unproven. I will continue diagnosis in [issue #366](https://git.magicbean.dev/MagicBeanDev/Longshot/issues/366).
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
4 participants
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!324
No description provided.