From 3d7b7a2d76d0f4c09e8f7d53ec591ac1d3188351 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sat, 24 Oct 2026 08:02:42 -0700 Subject: [PATCH] gba: allocate the frame buffers before the ROM cache; skip only when behind Every GBA game showed a white screen (with sound): the core was running and rendering, but no frame reached the display. gpSP's ROM cache takes 1 MB PSRAM blocks until it has 16 of them or one fails, which left ~320 KB, and the two 150 KB frame buffers video_cb alternates between were allocated on first use after that: the second one failed, and video_cb returned before pushing every frame. - init_gba allocates both frame buffers before the core starts. - The cache's heap blocks stop short of the last 2 MB of PSRAM, for the pause menu's screenshots and a save state's 425 KB buffer (which, with no exceptions, would abort rather than throw). 16 MB ROMs now page their last couple of MB from the card, as gpSP does when the cache is smaller than the ROM. Also: the frame pacing counted a frame that started just after its deadline (the sleep wakes up to a 1 ms tick late) as behind, so every other frame was skipped even at 4 ms a frame (Metroid Fusion: 1194 rendered, 1193 skipped). A frame is now behind only a quarter frame late: 1785 rendered, 4 skipped. Minish Cap 1657 / 135, Fire Red 1647 / 115. Co-Authored-By: Claude Opus 5.5 (1M context) --- components/gba/src/gba.cpp | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/components/gba/src/gba.cpp b/components/gba/src/gba.cpp index 030f800..c771056 100644 --- a/components/gba/src/gba.cpp +++ b/components/gba/src/gba.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include "box-emu.hpp" @@ -183,8 +184,12 @@ size_t gpsp_obj_priority_bytes(void); // HAL's ROM arena first, then the heap. The arena stays reserved at its boot // address for the carts that keep address-bound save states in it (MGS, // TPM); releasing it for the cache let it come back somewhere else. +// The core asks for blocks until it has ROM_BUFFER_SIZE or one fails, so the +// heap ones stop short of the last PSRAM_RESERVE bytes: the frame buffers, +// the pause menu's screenshots and a save state's buffer come after it. namespace { uint32_t g_arena_slices = 0; // bit i: slice i of the ROM arena is a cache block +constexpr size_t PSRAM_RESERVE = 2 * 1024 * 1024; } extern "C" void *gpsp_rom_block_alloc(size_t bytes) { auto &emu = Tab5Emu::get(); @@ -197,6 +202,9 @@ extern "C" void *gpsp_rom_block_alloc(size_t bytes) { } } } + if (heap_caps_get_free_size(MALLOC_CAP_SPIRAM) < bytes + PSRAM_RESERVE) { + return nullptr; // a smaller cache pages the rest of the ROM from the card + } return malloc(bytes); } extern "C" void gpsp_rom_block_free(void *p) { @@ -246,6 +254,15 @@ void init_gba(const std::string &rom_path) { gba_shared_free(); return; } + // the two frame buffers video_cb alternates between, before the ROM cache + // takes what PSRAM is left (allocated on first use, they came after it, and + // with the second one missing no frame reached the screen) + auto &emu = BoxEmu::get(); + if (!emu.frame_buffer0() || !emu.frame_buffer1()) { + fmt::print("gba: no memory for the frame buffers\n"); + gba_shared_free(); + return; + } retro_set_environment(&environ_cb); retro_set_video_refresh(&video_cb); retro_set_audio_sample_batch(&audio_batch_cb); @@ -263,7 +280,6 @@ void init_gba(const std::string &rom_path) { struct retro_system_av_info av = {}; retro_get_system_av_info(&av); g_fps = av.timing.fps; - auto &emu = BoxEmu::get(); emu.palette(nullptr); g_audio_rate = static_cast(av.timing.sample_rate); emu.audio_sample_rate(g_audio_rate); @@ -299,7 +315,11 @@ void run_gba_rom() { if (g_next_due == 0 || start > g_next_due + 2 * frame_us) { g_next_due = start; } - const bool behind = start > g_next_due; + // the sleep below wakes up to a tick (1 ms) after the deadline, so a frame + // that starts just past it is on time; only a quarter frame late is behind + // (with no slack every frame after a sleep was "behind" and every other + // frame was skipped, even at 4 ms a frame) + const bool behind = start > g_next_due + frame_us / 4; // the core's "auto" frameskip skips a frame when the host reports an // audio underrun (up to 30 in a row). Rendering is a small part of a // frame here, so skipping barely catches up but makes the output choppy: