fix(oot): try a full heap's OptimizeChangeBank allocations again #2451
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!2451
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/heap-retry-1697"
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?
Title: fix(oot): try a full heap's OptimizeChangeBank allocations again
The game can crash a few frames after loading a save when one of its small model heaps runs out of room (#1697, still
crashing on 2.1.0). The game uses the failed allocation anyway and writes through a null pointer.
What changes
The game's OptimizeChangeBank thread rebuilds a model's material block (f_804566e0) with memory from heaps its caller
names: small heaps of their own per model resource ("Model/....bfres", tens of KB), children of a large one. It checks
none of these allocations:
+200, lr 0x80456da4 for +168). A null result makes the bump allocator built on it hand out guest address 0. That is
the crash in #1697's logs: thread OptimizeChangeBanc, a write to guest 0x8 in f_8044cb30, lr 0x80456d48, with the
allocator's base word null in the register dump.
and the block is written to guest 0 two calls later. This is the crash TomShafford found at backflip + attack.
Each of these is now tried again in the heap's parents up to the root, the first with room; the GPU memory then also in
the game's default heap (
host/oot_heap_retry.c). The parents have gigabytes free, so the retry has room.nx2/oot_patches.py, the supported build only). f_80522050 itself andits ~30 other callers are as shipped. Nothing waits or sleeps.
function walks the heap tree for the heap whose range holds the block, so a block from a parent goes back to that
parent. This was read from the lifted code, not traced in a run.
first 60 lines, so a leak over a session would show.
New switches:
HOS_GPU_ALLOC_RETRY=0turns the retry off (as shipped).HOS_GPU_ALLOC_TEST_FAIL=<n>or<gpu>,<cpu>makes the heap refuse the first allocations at these sites, for testing.Credit
The GPU part is ported from TomShafford's Hookshot fork:
55d68e8cd6 and
65c1998222 (GPL-3.0), credited in the commits. Two things
changed from it after review: it is narrowed to these two call sites, and its waits are dropped (its own log showed they
did not help). The CPU part, the place #1697's log points to, is new in this PR.
Open question for #1697
Why the reporter's heap fills, and why only since 2.0.12, is not known. 2.0.12 is when the render target pool's 512 MB
minimum arrived (
hos_rt_pool_sizeinhost/nn_applet.c), and the Graphics heap grows by the same amount. Asking thereporter to try
HOS_RT_POOL_MB=305(2.0.11's size) on 2.1.0 would show whether that alone stops the crash.Verified
The game runs below used the first version: the first CPU buffer, and f_80522050 wrapped for every caller with waits.
The review changes (the second buffer, call-site-only GPU retry, no waits, per-site totals) are checked game-free only.
On Linux, RTX 3090, headless, on a copy of a Kokiri Forest save:
HOS_GPU_ALLOC_TEST_FAIL=50made the heap refuse the first 50 GPU and 50 CPU modelallocations. All of them were made in the parent heap ("ModuleSystem", "NinVirtualAddressHeap"), and the game reached
Kokiri Forest and played normally (screenshot).
HOS_GPU_ALLOC_RETRY=0 HOS_GPU_ALLOC_TEST_FAIL=0,1refuses one model buffer andkills the game at boot. The null buffer is written through, and the game later fails with "indirect call to an
address with no function at 0x698".
tests/test_oot_heap_retry.pyruns the retries on mock sead heaps:nx2/oot_patches.pyedits the four call sites exactly once and leaves f_80522050's other callers and f_80522050itself alone; a second run changes nothing; it runs on the supported build only.
python -I -B tests/run_all.pypasses on this branch alone, apart from 3 tests this PC cannotrun (its Python lacks zstandard and tkinter).
host/oot_heap_retry.ccompiles without warnings for Linux (clang) and Windows (llvm-mingw).Not tested
through allocations refused on purpose. A full heap whose parents are full too would still fail, now with log lines
that name the heaps.
a64.h.github-sync referenced this pull request2026-11-12 06:22:54 +00:00