Skip to content

Commit 7624c0d

Browse files
committed
GBA support
1 parent 0f0297d commit 7624c0d

17 files changed

Lines changed: 592 additions & 22 deletions

‎CMakeLists.txt‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ dpf_add_plugin(${NAME}
184184
src/system/sameboy/roles/MgbPassthroughRole.cpp
185185
src/system/mesen/MesenSystem.cpp
186186
src/system/mesen/MesenVideoDevice.cpp
187+
src/system/mesen/GbaSystem.cpp
187188
src/system/mesen/roles/NesN8MidiRole.cpp
188189
FILES_UI
189190
src/PluginUI.cpp
@@ -204,6 +205,7 @@ dpf_add_plugin(${NAME}
204205
src/system/sameboy/roles/MgbPassthroughRole.cpp
205206
src/system/mesen/MesenSystem.cpp
206207
src/system/mesen/MesenVideoDevice.cpp
208+
src/system/mesen/GbaSystem.cpp
207209
src/system/mesen/roles/NesN8MidiRole.cpp
208210
${UI_BUNDLE_C}
209211
deps/dpf-widgets/generic/LVGL.cpp)
@@ -283,6 +285,19 @@ if(BUILD_CLI)
283285
COMMENT "Rendering examples/scripts/nes_smoke.json -> /tmp/nes-smoke.wav + boot.png"
284286
)
285287

288+
# `make cli-gba-smoke` -> boot the nanoloop demo GBA ROM under GbaSystem,
289+
# render 5 s, capture two screenshots. End-to-end verification of the
290+
# Mesen-backed GBA dispatch (audio + video + boot).
291+
add_custom_target(cli-gba-smoke
292+
COMMAND ${CMAKE_BINARY_DIR}/bin/retroplug-cli
293+
--script ${CMAKE_SOURCE_DIR}/examples/scripts/gba_smoke.json
294+
--screenshot-dir ${CMAKE_BINARY_DIR}/gba-smoke
295+
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
296+
DEPENDS retroplug-cli
297+
USES_TERMINAL
298+
COMMENT "Rendering examples/scripts/gba_smoke.json -> /tmp/gba-smoke.wav + boot.png"
299+
)
300+
286301
# `make cli-n8-midi` -> end-to-end MIDI-input test for the n8-midi ROM.
287302
# Pumps note-ons across the four standard NES channels (pulse1=ch0,
288303
# pulse2=ch1, triangle=ch2, noise=ch3) into the FIFO and captures the

‎cli/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ add_executable(retroplug-cli
1414
${PROJECT_SOURCE_DIR}/src/system/sameboy/roles/MgbPassthroughRole.cpp
1515
${PROJECT_SOURCE_DIR}/src/system/mesen/MesenSystem.cpp
1616
${PROJECT_SOURCE_DIR}/src/system/mesen/MesenVideoDevice.cpp
17+
${PROJECT_SOURCE_DIR}/src/system/mesen/GbaSystem.cpp
1718
${PROJECT_SOURCE_DIR}/src/system/mesen/roles/NesN8MidiRole.cpp
1819
# lodepng (vendored under lv_binding_js). Compiled directly so the CLI
1920
# doesn't have to pull in the full lvgl-js-native static lib for one

‎cli/Script.hpp‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ struct ScriptSystem {
6666
// "KeyboardMidi", "MidiPassthrough", "ArduinoboyMaster"). Only meaningful
6767
// when the ROM is sniffed as LSDJ; ignored otherwise.
6868
std::optional<std::string> lsdj_sync_mode;
69+
// GBA-only: path to a real GBA BIOS file (16384 bytes; filename can be
70+
// anything — Mesen requires it to be named gba_bios.bin so GbaSystem
71+
// copies it into its firmware folder on activate). Without it most ROMs
72+
// hang at the first BIOS SWI; the Cult-of-GBA open-source BIOS works as
73+
// a drop-in for the smoke test. Ignored for SameBoy / NES ROMs.
74+
std::optional<std::string> bios_path;
6975
};
7076

7177
struct Script {
@@ -90,7 +96,7 @@ struct Script {
9096
struct TimedButton {
9197
std::uint64_t sample;
9298
std::uint32_t systemIndex;
93-
std::uint8_t button; // SameBoy: GameboyButton; Mesen: NesButton (cast)
99+
std::uint8_t button; // SameBoy: GameboyButton; Mesen: NesButton; Gba: GbaButton (cast)
94100
bool down;
95101
};
96102

@@ -114,9 +120,13 @@ struct TimedTransport {
114120
std::optional<double> setBpm;
115121
};
116122

117-
// Returns a button opcode as a raw uint8_t. Both GameboyButton and NesButton
118-
// are position-aligned (Right=0..Start=7) so the same name table works for
119-
// either system kind; the receiving system reinterprets the byte.
123+
// Returns a button opcode as a raw uint8_t. GameboyButton, NesButton, and
124+
// GbaButton all use the same position-aligned encoding (Right=0..Start=7)
125+
// for the shared eight buttons, so a single name table works for any
126+
// system kind; the receiving system reinterprets the byte. L and R are
127+
// GBA-only (wire bytes 8 and 9 from GbaButton); pressing them at a
128+
// SameBoy or Mesen system is a no-op (their toXButton switches return A
129+
// for unknown values).
120130
inline std::uint8_t parseButtonName(const std::string& s) {
121131
std::string lower(s.size(), '\0');
122132
std::transform(s.begin(), s.end(), lower.begin(),
@@ -129,6 +139,8 @@ inline std::uint8_t parseButtonName(const std::string& s) {
129139
if (lower == "b") return static_cast<std::uint8_t>(GameboyButton::B);
130140
if (lower == "select") return static_cast<std::uint8_t>(GameboyButton::Select);
131141
if (lower == "start") return static_cast<std::uint8_t>(GameboyButton::Start);
142+
if (lower == "l") return static_cast<std::uint8_t>(GbaButton::L);
143+
if (lower == "r") return static_cast<std::uint8_t>(GbaButton::R);
132144
throw std::runtime_error("unknown button name: " + s);
133145
}
134146

‎cli/main.cpp‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
#include "project/Project.hpp"
3838
#include "system/RomFormat.hpp"
3939
#include "system/SystemBase.hpp"
40+
#include "system/mesen/GbaConfig.hpp"
41+
#include "system/mesen/GbaSystem.hpp"
4042
#include "system/mesen/MesenConfig.hpp"
4143
#include "system/mesen/MesenSystem.hpp"
4244
#include "system/sameboy/SameBoyConfig.hpp"
@@ -278,7 +280,7 @@ int main(int argc, char** argv) try {
278280
std::unique_ptr<SystemBase> sys;
279281
if (fmt == RomFormat::Unknown) {
280282
std::fprintf(stderr,
281-
"script: systems[%u].rom '%s' is not a recognised Game Boy or NES ROM\n",
283+
"script: systems[%u].rom '%s' is not a recognised Game Boy, NES, or GBA ROM\n",
282284
i, s.rom.c_str());
283285
return 1;
284286
}
@@ -295,6 +297,20 @@ int main(int argc, char** argv) try {
295297
cfg.romPath = s.rom;
296298
sys = std::make_unique<MesenSystem>(
297299
project.nextSystemId(), cfg, std::move(bytes));
300+
} else if (fmt == RomFormat::Gba) {
301+
if (s.link_group.value_or(0) != 0) {
302+
std::fprintf(stderr, "script: systems[%u] is GBA; link_group not supported\n", i);
303+
return 1;
304+
}
305+
if (s.lsdj_sync_mode) {
306+
std::fprintf(stderr, "script: systems[%u] is GBA; lsdj_sync_mode not applicable\n", i);
307+
return 1;
308+
}
309+
GbaSystemConfig cfg;
310+
cfg.romPath = s.rom;
311+
if (s.bios_path) cfg.biosPath = *s.bios_path;
312+
sys = std::make_unique<GbaSystem>(
313+
project.nextSystemId(), cfg, std::move(bytes));
298314
} else {
299315
SameBoyConfig cfg;
300316
cfg.romPath = s.rom;

‎deps/mesen/Core/GBA/GbaConsole.cpp‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,11 @@ void GbaConsole::ProcessEndOfFrame()
354354
{
355355
_controlManager->UpdateControlDevices();
356356
_controlManager->UpdateInputState();
357+
// RetroPlug: GbaControlManager::UpdateInputState() has been modified to
358+
// skip the BaseControlManager::UpdateInputState() clobbering call (which
359+
// would wipe SetBitValue from the host) but still refresh _state.
360+
// ActiveKeys from the controller bits, so the CPU's read of $4000130
361+
// sees host-driven button changes.
357362
}
358363

359364
BaseControlManager* GbaConsole::GetControlManager()

‎deps/mesen/Core/GBA/GbaControlManager.cpp‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,15 @@ GbaControlManagerState& GbaControlManager::GetState()
2323

2424
void GbaControlManager::UpdateInputState()
2525
{
26-
BaseControlManager::UpdateInputState();
26+
// RetroPlug: BaseControlManager::UpdateInputState() calls
27+
// device->ClearState() + SetStateFromInput() on every poll, which
28+
// reads from host KeyMappings and would wipe any bits the host set
29+
// via SetBitValue. The host (CLI script / JS bridge / DAW plugin)
30+
// is the source of truth for input; Mesen's keyboard-polling path
31+
// is unused. Skip the base call and just refresh the ActiveKeys
32+
// cache from the controller's current bits so the CPU's read of
33+
// $4000130 sees host-driven button changes.
34+
//BaseControlManager::UpdateInputState();
2735
_state.ActiveKeys = (ReadController(0) | (ReadController(1) << 8));
2836
CheckForIrq();
2937
}

‎examples/scripts/gba_smoke.json‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"systems": [
3+
{
4+
"rom": "resources/roms/nanoloop287d.gba",
5+
"bios_path": "build/firmware/gba_bios.bin"
6+
}
7+
],
8+
"duration_ms": 8000,
9+
"sample_rate": 44100,
10+
"block_size": 1024,
11+
"out_wav": "/tmp/gba-smoke.wav",
12+
"events": [
13+
{ "at_ms": 2000, "screenshot": "boot", "system": 0 },
14+
{ "at_ms": 4000, "tap": "Start", "system": 0, "hold_ms": 80 },
15+
{ "at_ms": 6000, "screenshot": "post_start", "system": 0 },
16+
{ "at_ms": 7500, "screenshot": "final", "system": 0 }
17+
]
18+
}

‎src/PluginJsBridge.cpp‎

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ extern "C" {
2020
#include "system/InputTypes.hpp"
2121
#include "system/RomFormat.hpp"
2222
#include "system/SystemBase.hpp"
23+
#include "system/mesen/GbaConfig.hpp"
24+
#include "system/mesen/GbaSystem.hpp"
2325
#include "system/mesen/MesenConfig.hpp"
2426
#include "system/mesen/MesenSystem.hpp"
2527
#include "system/sameboy/SameBoyConfig.hpp"
@@ -178,19 +180,17 @@ SystemBase* PluginJsBridge::buildSystemFromPath(const std::string& path) {
178180
return nullptr;
179181
}
180182

181-
// Content-based dispatch: iNES magic → MesenSystem, Game Boy Nintendo
182-
// logo → SameBoySystem, anything else → reject. Matching by magic bytes
183-
// (not extension) means a mislabelled ROM still goes to the right
184-
// backend, and totally unrelated files (a .sh script, a JPEG, …)
185-
// surface as "rom-error" instead of being executed as instructions
186-
// by whichever backend caught them. The pre-detector code fell through
187-
// to SameBoy for anything not ending in `.nes`, which produced a stream
188-
// of "Wrote XX to YYYY (RAM Mirror)" log spam from the GB CPU
189-
// executing garbage.
183+
// Content-based dispatch: iNES magic → MesenSystem, GBA Nintendo logo
184+
// at $0004 → GbaSystem, Game Boy Nintendo logo at $0104 → SameBoySystem,
185+
// anything else → reject. Matching by magic bytes (not extension) means
186+
// a mislabelled ROM still goes to the right backend, and totally
187+
// unrelated files (a .sh script, a JPEG, …) surface as "rom-error"
188+
// instead of being executed as instructions by whichever backend caught
189+
// them.
190190
const RomFormat fmt = detectRomFormat(bytes);
191191
if (fmt == RomFormat::Unknown) {
192192
std::fprintf(stderr,
193-
"buildSystemFromPath: '%s' is not a recognised Game Boy or NES ROM\n",
193+
"buildSystemFromPath: '%s' is not a recognised Game Boy, NES, or GBA ROM\n",
194194
path.c_str());
195195
emitRomEvent(engine, "rom-error", path);
196196
return nullptr;
@@ -207,6 +207,20 @@ SystemBase* PluginJsBridge::buildSystemFromPath(const std::string& path) {
207207
return sys.release();
208208
}
209209

210+
if (fmt == RomFormat::Gba) {
211+
GbaSystemConfig cfg;
212+
cfg.romPath = path;
213+
// BIOS lookup: pass through anything the host has dropped into the
214+
// build firmware dir. If absent, GbaSystem falls back to HLE
215+
// (Mesen-zeroed boot ROM) and most non-trivial ROMs will hang on
216+
// their first BIOS SWI. Plumbing a UI-side BIOS picker is a
217+
// follow-up.
218+
cfg.biosPath = "build/firmware/gba_bios.bin";
219+
auto sys = std::make_unique<GbaSystem>(id, cfg, std::move(bytes));
220+
sys->onActivate(sr);
221+
return sys.release();
222+
}
223+
210224
SameBoyConfig cfg;
211225
cfg.romPath = path;
212226
cfg.model = SameBoyModel::CgbC;

‎src/project/Project.cpp‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include "rfl/Variant.hpp"
99

10+
#include "system/mesen/GbaSystem.hpp"
1011
#include "system/mesen/MesenSystem.hpp"
1112
#include "system/sameboy/SameBoySystem.hpp"
1213

@@ -76,6 +77,22 @@ SystemId Project::addSystem(const SystemConfig& config) {
7677
return id;
7778
}
7879

80+
if (const auto* gb = rfl::get_if<GbaSystemConfig>(&config.variant())) {
81+
std::vector<std::uint8_t> rom = gb->romBytes.bytes();
82+
if (rom.empty())
83+
rom = slurpFile(gb->romPath);
84+
if (rom.empty()) {
85+
std::fprintf(stderr, "[Project] no ROM bytes/path for GBA id=%u path='%s'\n",
86+
id, gb->romPath.c_str());
87+
return 0;
88+
}
89+
auto sys = std::make_unique<GbaSystem>(id, *gb, std::move(rom));
90+
systems_.push_back(std::move(sys));
91+
config_.systems.push_back(*gb);
92+
rebuildLinkGroups();
93+
return id;
94+
}
95+
7996
return 0;
8097
}
8198

‎src/system/InputTypes.hpp‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,26 @@ enum class NesButton : std::uint8_t {
3939
Start = 7,
4040
Count = 8,
4141
};
42+
43+
// GBA buttons. The shared 8 names keep the position-aligned wire bytes used
44+
// across all kinds (Right=0..Start=7) so the CLI script parser, JS bridge,
45+
// and any other "name → button" path can stay system-agnostic. L and R are
46+
// GBA-only and live at the end of the table.
47+
//
48+
// Note: Mesen's own GbaController::Buttons enum has a different order
49+
// (Up=0, Down=1, Left=2, Right=3, Start=4, Select=5, B=6, A=7, L=8, R=9 —
50+
// see deps/mesen/Core/GBA/Input/GbaController.h). GbaSystem::pressButton
51+
// does the explicit remap from this wire byte to Mesen's native enum.
52+
enum class GbaButton : std::uint8_t {
53+
Right = 0,
54+
Left = 1,
55+
Up = 2,
56+
Down = 3,
57+
A = 4,
58+
B = 5,
59+
Select = 6,
60+
Start = 7,
61+
L = 8,
62+
R = 9,
63+
Count = 10,
64+
};

0 commit comments

Comments
 (0)