A headless Genesis/Mega Drive test harness built on Genesis Plus GX. gxtest enables fast, automated testing of Genesis ROMs using GoogleTest, with direct memory access for assertions and symbol-based debugging.
- Headless Execution: Run Genesis ROMs at maximum speed without graphics or audio output
- GoogleTest Integration: Write tests using familiar GoogleTest patterns and assertions
- Direct Memory Access: Read/write emulated RAM for state verification and injection
- Symbol-Based Testing: Assert on variable names (
Sym::player_score) instead of magic addresses (0xFF0008) - CPU State Inspection: Access M68K registers (D0-D7, A0-A7, PC, SR)
- Input Simulation: Programmatically control gamepad inputs
- State Save/Load: Capture and restore emulator state
- Conditional Execution: Run until memory conditions are met
mkdir build && cd build
cmake ..
make -j4To enable building test ROMs from source (requires m68k-elf-gcc):
cmake -DGXTEST_BUILD_ROMS=ON ..ctest --output-on-failure#include <gxtest.h>
class MyRomTest : public GX::Test {
protected:
void SetUp() override {
ASSERT_TRUE(emu.LoadRom("path/to/rom.bin"));
}
};
TEST_F(MyRomTest, GameInitializes) {
emu.RunFrames(60); // Run 1 second at 60fps
// Assert on memory values
EXPECT_EQ(ReadWord(0xFF0100), 0x0001);
}// From file
emu.LoadRom("roms/game.bin");
// From embedded byte array
emu.LoadRom(ROM_DATA, ROM_SIZE);// Read memory (handles Genesis byte-swapping automatically)
uint8_t byte = ReadByte(0xFF0000);
uint16_t word = ReadWord(0xFF0000);
uint32_t dword = ReadLong(0xFF0000);
// Write memory
WriteByte(0xFF0000, 0x42);
WriteWord(0xFF0000, 0x1234);
WriteLong(0xFF0000, 0xDEADBEEF);
// Direct RAM access (faster for bulk operations)
uint8_t* work_ram = emu.GetWorkRam(); // 64KB at 0xFF0000
uint8_t* z80_ram = emu.GetZ80Ram(); // 8KB at 0xA00000// Run until memory equals a value (or timeout)
int frames = emu.RunUntilMemoryEquals(0xFF0502, 0xAD, 1000);
ASSERT_GE(frames, 0) << "Condition not met within timeout";
// Run until custom condition
emu.RunUntil([this]() {
return ReadWord(0xFF0100) >= 100;
}, 500);GX::Input input;
input.up = true;
input.a = true;
emu.SetInput(0, input); // Player 1
emu.RunFrames(1);
// Or use convenience method
emu.PressButton(0, GX::Button::Start);For readable tests that use variable names instead of addresses, gxtest supports extracting symbols from ELF files.
- Compile ROM with debug symbols (
m68k-elf-gccproduces.elf) - Extract symbols using
tools/elf2sym.py - Include generated header in your tests
- Assert using symbol names
ROM source (main.c):
volatile uint16_t player_score = 0;
volatile uint8_t player_lives = 3;
void main(void) {
while (!game_over) {
player_score += 10;
// ...
}
}Extract symbols:
m68k-elf-nm -n rom.elf | python3 tools/elf2sym.py > symbols.hGenerated header (symbols.h):
namespace Sym {
constexpr uint32_t player_score = 0xFF0008;
constexpr uint32_t player_lives = 0xFF0018;
}Test using symbols:
#include "symbols.h"
TEST_F(GameTest, ScoreIncrements) {
emu.RunFrames(10);
// Readable: assert on player_score, not 0xFF0008
uint16_t score = ReadWord(Sym::player_score);
EXPECT_GT(score, 0);
EXPECT_EQ(score % 10, 0);
}
TEST_F(GameTest, InjectState) {
// Set score directly for testing edge cases
WriteWord(Sym::player_score, 9990);
WriteByte(Sym::player_lives, 1);
emu.RunFrames(1);
// Verify game-over triggers at 10000 points
EXPECT_EQ(ReadByte(Sym::game_over), 1);
}- Compile-time safety: Renamed variables cause build failures, not runtime crashes
- IDE autocomplete: Symbol names appear in code completion
- Self-documenting:
Sym::player_scoreis clearer than0xFF0008
| Method | Description |
|---|---|
LoadRom(path) |
Load ROM from file |
LoadRom(data, size) |
Load ROM from memory |
Reset() |
Soft reset |
HardReset() |
Hard reset (power cycle) |
RunFrames(n) |
Run n emulator frames |
RunUntilMemoryEquals(addr, val, max) |
Run until memory matches |
RunUntil(condition, max) |
Run until lambda returns true |
ReadByte/Word/Long(addr) |
Read memory |
WriteByte/Word/Long(addr, val) |
Write memory |
GetWorkRam() |
Direct pointer to 68K work RAM |
GetZ80Ram() |
Direct pointer to Z80 RAM |
GetDataRegister(n) |
Read D0-D7 |
GetAddressRegister(n) |
Read A0-A7 |
GetPC() |
Read program counter |
GetSR() |
Read status register |
SetInput(player, state) |
Set controller state |
SaveState() |
Capture emulator state |
LoadState(state) |
Restore emulator state |
Base class for test fixtures. Provides:
emu- Emulator instanceReadByte/Word/Long()- Convenience wrappersWriteByte/Word/Long()- Convenience wrappers
gxtest/
├── include/
│ └── gxtest.h # Public API
├── src/
│ ├── gxtest.cpp # Implementation
│ ├── osd.h # Platform abstraction
│ └── stubs.c # Sega CD stubs
├── tests/
│ ├── example_test.cpp # Basic test patterns
│ ├── prime_sieve_test.cpp
│ └── symbol_example_test.cpp
├── tools/
│ └── elf2sym.py # Symbol extraction
├── roms/
│ ├── prime_sieve/ # Verification ROM
│ └── symbol_example/ # Symbol testing demo
└── vendor/
└── genplusgx/ # Genesis Plus GX core
- CMake 3.14+
- C++17 compiler
- Python 3 (for symbol extraction)
- m68k-elf-gcc (optional, for building test ROMs)
gxtest framework is provided under the MIT license. Genesis Plus GX is licensed under its own terms (see vendor/genplusgx/).