Skip to content

Carts play together: the ROM arena stays put, carts borrow it - #5

Merged
finger563 merged 3 commits into
mainfrom
feat/shared-arena
Oct 18, 2026
Merged

finger563 merged 3 commits into
mainfrom
feat/shared-arena

Conversation

@finger563

@finger563 finger563 commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Playing The Phantom Menace and then Jedi Knight crashed JK while it loaded its first level (CORRUPT HEAP … multi_heap_free … (head != NULL) in jk_engine, under sithThing_FreeThing).

Cause. It turned out not to be TPM leaving memory behind. JK crashed the same way when it was the first game after a boot. JK was short of PSRAM:

  • TPM's recompiled game is 3.1 MB of code. Like all code on the P4, it runs from a copy in PSRAM.
  • So the merged firmware's PSRAM heap is 21.8 MB, against 25.2 MB for the JK-only build.
  • On top of that, the bring-up allocation tracker (CONFIG_JK_ESP_DEBUG) holds a 1.5 MB table.
  • JK's level load ran out of memory, and its failure path then corrupted the heap.

JK runs again with that debug option off; it defaults to off.

The soak test found a second problem. After JK or GBA, the free PSRAM was cut up: the largest block fell from 10.5 MB to about 4 MB. TPM then found no 4.9 MB block and ran without save states. The ROM arena also came back at a different address, so MGS and TPM states saved earlier could no longer load.

Changes

The 6 MB ROM arena reserved at boot now stays reserved, at its boot address. Carts borrow it instead of releasing it:

  • JK: a private heap over the arena (multi_heap) for the engine's large allocations. Each block carries its session, so a late free from an earlier session (the engine does these at the next start) is dropped instead of corrupting the new heap.
  • GBA: the first 1 MB ROM cache blocks are slices of the arena, through a new gpSP hook (gpsp_rom_block_alloc / gpsp_rom_block_free); the rest come from the heap as before.
  • MGS and TPM: their arenas are the ROM arena when it's free (Mgs_ArenaBorrow, mgs_reversing 0ac4d5d), with the heap as a fallback. A lent block is never freed.

Results:

  • TPM and MGS get the same arena address on every launch, so their save states survive other games.
  • The largest free PSRAM block stays at 8.9 MB through any order of launches.

Soak test (separate commit): CONFIG_TAB5_EMU_AUTOSTART_MATCH takes a comma-separated list, and CONFIG_TAB5_EMU_AUTOQUIT_SECONDS quits each game after that long (the pause menu's Quit path) and starts the next. Each launch logs free and largest-block memory.

JK texture thrashing (OpenJKDF2 756c012)

Once the player left the start area, the texture (material) cache kept evicting and reloading. Measured on the board with an allocation dump: nine 640×480 menu backgrounds (bkMain, bkEsc (twice), bkSetup, …), 2.7 MB in all, stayed loaded for the whole level. That's more than the first level's entire texture set, 2.2 MB, computed from the GOBs. Menus unload them when they close, but a level started without a menu (-autostart, level changes) never did. jkMain_GameplayShow now unloads them, and they come back on demand through stdBitmap_EnsureData.

After the fix the cache holds every material the level showed (346), with no failed loads and no purges, and 1.3 MB to spare.

The console now reports the cache every 5 s, and the frame's non-zero-pixel check is no longer debug-only.

Testing

On the Tab5, 16 launches with 45 s each:

TPM → JK → MGS → TPM → MGS → JK → SNES (Chrono Trigger) → JK → GBA (Minish Cap) → TPM → Genesis (Sonic 2) → MGS → SNES → TPM → TPM → JK

  • No crashes.
  • JK loaded its level every time.
  • TPM and MGS always got the arena at 0x48cfc3c0.
  • GBA ran at 59.7 fps.

Not tested: loading a save state across another game (the addresses now match, which is what states need).

Note

JK has about 1.9 MB less PSRAM than in the JK-only build, because of TPM's code. The first level loads and plays fine; JK's texture cache is designed to fill whatever is left. I didn't test later, larger levels.

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown

ESP-IDF Size Report for 'Tab5 Emu'

Metric Base PR Delta
FLASH 0 bytes (0.00%) 0 bytes (0.00%) 0 bytes (0.00%)
DRAM 266,582 bytes (60.41%) 266,590 bytes (60.41%) 🔺 +8 bytes (0.00%)
IRAM 0 bytes 0 bytes 0 bytes
RAM (DRAM+IRAM) 266,582 bytes 266,590 bytes 🔺 +8 bytes (0.00%)

FLASH uses app .bin size or json2 flash sum. RAM sums DRAM+IRAM via idf_size. Percentages shown when totals are available.
DRAM/IRAM usage does not include memory used by the heap allocator at runtime.
This report was generated by esp-idf-size-delta.

finger563 and others added 2 commits October 18, 2026 08:15
CONFIG_TAB5_EMU_AUTOSTART_MATCH takes a comma-separated list and
CONFIG_TAB5_EMU_AUTOQUIT_SECONDS quits each auto-started game after that
long, the way the pause menu's Quit does, then starts the next one, round
and round. Each launch logs the free internal RAM and PSRAM (and the
largest blocks) before and after make_cart's releases.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Playing The Phantom Menace and then Jedi Knight could crash JK's level load
with a corrupt heap. JK was short of PSRAM: TPM's recompiled game is 3.1 MB
of code that, like all code on the P4, runs from PSRAM, so the merged
firmware's heap is 3.3 MB smaller than the JK-only build's (and the
bring-up allocation tracker, CONFIG_JK_ESP_DEBUG, took another 1.5 MB).
JK runs again with that option off; the arena changes below keep the PSRAM
in large, stable blocks:

- JK no longer hands the 6 MB ROM arena back to the system heap. It makes
  a private heap of it (multi_heap) for its large allocations; blocks carry
  their session, so the engine's late frees from an earlier session are
  dropped instead of corrupting the next heap.
- GBA takes its first ROM cache blocks from the arena (a gpSP allocation
  hook) instead of releasing it.
- MGS and TPM borrow the arena for their big buffers. Before, they needed
  a 3.8 / 4.9 MB hole in a heap other games had cut up: after JK, TPM found
  no 4.9 MB block and ran without save states.

The arena now stays at its boot address, so MGS and TPM save states keep
loading after other games ran, and the largest free PSRAM block stays at
8.9 MB through any order of launches (it fell to 4 MB).

Tested with the soak test: TPM, JK, MGS, SNES, GBA and Genesis in every
pairing, 16 launches with quits, on the Tab5.

mgs_reversing 0ac4d5d: Mgs_ArenaBorrow.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Nine 640x480 menu backgrounds (2.7 MB of PSRAM) stayed loaded through a
level started without a menu; the material cache thrashed for the room.
Now the level's textures all fit with 1.3 MB to spare.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@finger563
finger563 merged commit 0330936 into main Oct 18, 2026
1 check passed
@finger563
finger563 deleted the feat/shared-arena branch October 18, 2026 19:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant