WIP: fix(hos): wait at the commit limit for the host's own allocations, then say out of memory #341
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!341
Loading…
Reference in a new issue
No description provided.
Delete branch "llun/Longshot:fix/host-alloc-commit-limit"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
At Windows' commit limit (RAM plus the page file full), the NVN layer's own
malloc/callocfailing ended the game at once withFATAL: native NVN: recording handle set, with no wait for commit to come back and no out-of-memory notice, so a player saw an unexplained crash (#333: RTX 3060 12 GB, 16 GB RAM with a 16 GB page file, Hyrule Field). With this PR a failed host allocation is treated like a guest commit at the limit: it waits, and if memory doesn't come back the game says it is out of memory, shows the notice and only then stops.The cause, commit growing over play, is #324's; this PR is only about how the game ends at the limit.
What changed
hos_xmalloc/hos_xcalloc/hos_xreallocinhost/mkw_glue.c(declared inhost/hos.h), never NULL. A failed allocation goes tohos_alloc_failed(), which tries again every 50 ms for up to 5 s ascommit_wait()does (#77), while the renderer's textures give commit back. If it gets the memory, one log line (... at the commit limit allocated after N ms). If not: the line,hos_out_of_memory(1), thenhos_fatal("out of memory (RAM plus the page file): <what> (<n> bytes)"). When over 1 GB more than it asked for is still free, it isn't the limit (a size gone wrong), so the fatal says how much was free and shows no notice. Acallocwhose count × size overflows stops at once.hos_out_of_memory(int wait)is the out-of-memory line and notice, once a run, shared bycommit_refused()(guest commits, as before) and the host allocations. The notice is always on a thread of its own: shown on the game thread that ran out (sead::MainThread),MessageBoxAcame back at once and showed nothing (found in the game run, fixed in the second commit). Withwaitthe caller waits until the notice is closed, its own or one already up, before the game stops.HOS_HEADLESSshows no notice. On Linux the line and the notice speak of RAM and swap, not the page file.host/nvn_native.c's five sites from the issue (command buffer table×2,recording buffer,recording handle set,recording copy, throughpool_get()), and alsotexture copies table(pins_grow()), use the helpers instead ofdie()on NULL.host/nn_video.c's decoder table and image pool use them instead ofhos_fatal("video: out of memory"). Its picture buffers that drop a frame on NULL (issue 342) are unchanged.tests/test_host_alloc_wait.py(new) compiles the realhos_out_of_memoryandhos_alloc_failedcut out ofmkw_glue.cwith a fakemalloc, clock and notice (tests/host_alloc_wait_test.c), and checks the call sites by text.tests/test_commit_limit.pyfollowscommit_refused's notice intohos_out_of_memory;tests/test_nvn_host_tables.py's fixture gets stand-ins for the helpers.HOS_TEST_ALLOC_REFUSEindocs/development.md's switches;docs/tested/2026-11-13-host-alloc-oom-llun-run01.md.Flags and settings
HOS_HEADLESS(existing)HOS_TEST_ALLOC_REFUSE=<text>Measurements
main:
cf605c85(2026-11-14)this PR: fix/host-alloc-commit-limit @
d60e26c5(the game was built from50077318:d60e26c5only adds thedocs/testedfile, so the code is the same)Hardware: NVIDIA RTX 5080, driver 621.24, AMD Ryzen 9 9900X3D, 64 GB RAM, 9 GB page file, Windows 11 Pro 26300
Scene: boot, before the title (the game's first recording); 1280x720 window, default settings,
HOS_AUDIO=0,RX_VK_PRECOMPILE=0(shaders not prepared)Method: this is a fix, so the proof is the bug's path before and after, not fps. This PC doesn't reach the commit limit (27 GB of commit was free), so
HOS_TEST_ALLOC_REFUSErefuses the allocation the way Windows does at the limit. That switch isn't onmain, so onmainthe before is the player's log from #333 (nightly-80-7315957c;mainhas the same code at these sites). Two forced runs at two different sites, one headless, and one normal 90 s boot with no switch. A run script watched the game's windows, captured the notice, and closed it 2 s later.How to reproduce: build this branch, then start
longshot.exefrom the game folder withHOS_TEST_ALLOC_REFUSE=recording handle set(orrecording copy). After about 7 s the notice appears; OK ends the game.python -I -B tests/test_host_alloc_wait.py --cc <clang or gcc>for the game-free check.cf605c85(the player's log, #333)d60e26c5FATAL: native NVN: recording handle setmem: ... still refused after 5 s,mem: Windows is out of memory (RAM and page file) ...,FATAL: out of memory (RAM plus the page file): native NVN: recording handle set (524288 bytes)HOS_TEST_ALLOC_REFUSE=recording copynative NVN: recording copy (128 bytes)(forced5)HOS_HEADLESS=1)tests/test_host_alloc_wait.pycommit_refused does not say out of memory through hos_out_of_memory)Testing
Tested:
make_game.py: the four runs above. The first build showed the notice on the game's own thread, whereMessageBoxAshowed nothing: the game waited the 5 s, logged the lines and stopped with no notice. The second commit moves the notice to its own thread, and the runs above are from that build. A normal boot with no switch ran 90 s at 60 fps with no fatal.python -I -B tests/run_all.py: 476 passed, 8 failed, 76 skipped (Windows, llvm-mingw's clang asgcc). All 8 fail the same way onmain@cf605c85on this PC:test_bake_portability,test_fpx_sticky,test_lift_cache,test_module_ids,test_oot_state_tap,test_pad_layout,test_spawn_emit,test_tested_configurations(that one is CRLF line endings in its own CLI output). The newtest_host_alloc_wait.pypasses on this branch and fails onmain. Mutations of the fix are caught: notice not waited for →limitfails; no retries →recoversfails.gcc 13.3(Ubuntu 24.04 under WSL) compileshost/nvn_native.ccleanly, andhost/mkw_glue.cwith only an error thatmain'smkw_glue.chas too (Ctxhas no memberpollata64_poll: a define the lift's own build gives). Nothing is reported in the new code. That is a compile of the changed files with the posixwindows.h, not a full Linux build.Not tested:
mallocreturns NULL, and the switch makes it return NULL at the same call.recovers), not seen in a run.host/nn_video.c's two sites in a movie (two-line changes, built on Windows).Needs other testers:
HOS_TEST_ALLOC_REFUSE="recording handle set". Expected: the linemem: out of memory (RAM and swap), the SDL notice, and the stop once it's closed (T-9A3Y; it waits for thetesting wantedlabel).Screenshots
The notice in forced4 (this PR @
50077318), on the RTX 5080 PC: the comment with the picture. Nothing else on screen changes. Onmain@cf605c85there is no notice on this path.Risk and rollback
cmd_lockorcopy_lock, the video service's table lock). Before, the process ended at that point, so nothing that worked before waits longer; another thread that needs that lock waits too, and if the renderer's give-back needs it, the wait ends in the notice as before.HOS_HEADLESSfor a refused guest commit either (it popped up before). Headless runs are tests, and the log line is unchanged.Reviewer notes
hos_alloc_failed()andhos_out_of_memory()inhost/mkw_glue.c.die()texts behind anative NVN:prefix, so a log from before and after can be grepped for the same name.Issues
Fixes #333
See #324
Checklist
For everyone
type(scope): description).main.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above).main@cf605c85and passes here, on this PC. In the game, two forced runs here, and the player's log formain, where the switch doesn't exist.)third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
Assisted-by:/Co-Authored-By:trailers.docs/tested/for each new measured run (format indocs/tested-configurations.md; no appended row or assembled view).mainand passing here.tests/that reads changed source updated in this PR, not weakened.🤖 Generated with Claude Code
The out-of-memory notice in forced4 (this PR @
50077318, RTX 5080, Windows 11):HOS_TEST_ALLOC_REFUSE=recording handle set, up while the game waits; OK ends it (exit 3, 0.3 s after).Description: the screenshot linked (comment 16766).
Description: the Measurements name the head
d60e26c5(built from50077318, the same code); HOS_HEADLESS noted under Flags. The Linux box stays unticked: a gcc compile of the changed files only, so far.Linux test request opened: T-9A3Y (Linux, any GPU: the forced out-of-memory path and a normal start). It needs the
testing wantedlabel before anyone can claim it; the PR stays WIP until the Linux box is settled.Read head
d60e26c5against maincf605c85: the complete patch, allocation callers, Linux thread and notice code, description, all four comments, issue 333 and the notice picture. Ran the three changed Python checks, with GCC and Clang for both C fixtures; all passed.What's good
reallocblock intact. The check covers recovery, persistent failure, overflow, headless mode and waiting for an existing notice.50077318to this head is the added test record.Must change
host/mkw_glue.c:65-67now callsCreateThreadafter a host allocation has kept failing. On Linux, that callsthread_new()inhost/posix/win32_posix.c:188-192, which callscallocand immediately writest->h.typewithout checking the result. If this allocation also fails, the new error path dereferences NULL before it can reach the notice or the final out-of-memory fatal. The NULL-handle fallback here does not catch that. Previously these host allocation failures went straight todie()orhos_fatal()without creating a thread. Make thread creation return failure safely, or prepare the notice resources before exhaustion, and add a case that refuses this allocation and still reaches the intended fatal without a signal.HOS_TEST_ALLOC_REFUSEdoes not refuse this plaincalloc, and the current fixture replacesCreateThreadwith a version that always succeeds, so neither covers it.Smaller points
host/mkw_glue.c:127still says “RAM plus the page file” on Linux, although the preceding line and notice correctly say swap. Use the same platform wording in the final fatal._WIN32even when compiled on Linux. Its passing Linux runs exercise the Windows branch with substitutes, not thesysinfobranch or the real Linux thread and SDL calls.Readiness and limits
The branch contains current main
cf605c85; there is no intervening-main conflict to resolve. The forge's source-check jobs were skipped, not passed: https://git.magicbean.dev/MagicBeanDev/Longshot/actions/runs/3826/jobs/1.No game, full build, real memory exhaustion or Windows execution was run here. The requested Linux build, ordinary start and notice test in T-9A3Y remain open. A forced-label run would establish that the notice works with memory available; it would not settle the failed thread-allocation case above. I did not test AMD, Intel, Steam Deck or the changed video allocation paths in a movie.
Thanks @mooki for review 1238. Agreed on all three:
thread_new()will return NULL when itscallocfails,CreateThreadpasses that on, and the notice path falls through to the final out-of-memory fatal. A fixture case will refuse that allocation and check the fatal is reached without a signal._WIN32on Linux, so its Linux run covers thesysinfobranch.The fix follows in the next push; the PR stays WIP until then and until T-9A3Y is settled.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.