diff --git a/CMakeLists.txt b/CMakeLists.txt index ed29d25..e2bc063 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -241,6 +241,143 @@ target_include_directories(gxtest_prime_sieve PRIVATE gtest_discover_tests(gxtest_prime_sieve) +# ----------------------------------------------------------------------------- +# Symbol Example Test (demonstrates symbol-based testing) +# ----------------------------------------------------------------------------- + +# Option to build ROMs from source (requires m68k-elf toolchain) +option(GXTEST_BUILD_ROMS "Build test ROMs from source (requires m68k-elf-gcc)" OFF) + +# Find Python for symbol extraction +find_package(Python3 COMPONENTS Interpreter) + +if(GXTEST_BUILD_ROMS) + # Find m68k cross-compiler + find_program(M68K_GCC m68k-elf-gcc) + find_program(M68K_AS m68k-elf-as) + find_program(M68K_LD m68k-elf-ld) + find_program(M68K_NM m68k-elf-nm) + find_program(M68K_OBJCOPY m68k-elf-objcopy) + + if(NOT Python3_FOUND) + message(WARNING "Python3 not found, ROM builds disabled (needed for symbol extraction)") + elseif(M68K_GCC AND M68K_AS AND M68K_LD AND M68K_NM AND M68K_OBJCOPY) + message(STATUS "m68k-elf toolchain and Python3 found, enabling ROM builds") + + set(SYMBOL_EXAMPLE_DIR ${CMAKE_SOURCE_DIR}/roms/symbol_example) + set(SYMBOL_EXAMPLE_ROM ${SYMBOL_EXAMPLE_DIR}/symbol_example.bin) + set(SYMBOL_EXAMPLE_ELF ${SYMBOL_EXAMPLE_DIR}/symbol_example.elf) + set(SYMBOL_EXAMPLE_SYMBOLS_TXT ${SYMBOL_EXAMPLE_DIR}/symbol_example_symbols.txt) + set(GENERATED_INCLUDE_DIR ${CMAKE_BINARY_DIR}/generated) + + # Create generated include directory + file(MAKE_DIRECTORY ${GENERATED_INCLUDE_DIR}) + + # Build the ROM using the Makefile + add_custom_command( + OUTPUT ${SYMBOL_EXAMPLE_ROM} ${SYMBOL_EXAMPLE_ELF} + COMMAND make -C ${SYMBOL_EXAMPLE_DIR} ${SYMBOL_EXAMPLE_ROM} + DEPENDS + ${SYMBOL_EXAMPLE_DIR}/main.c + ${SYMBOL_EXAMPLE_DIR}/crt0.s + ${SYMBOL_EXAMPLE_DIR}/genesis.ld + ${SYMBOL_EXAMPLE_DIR}/Makefile + COMMENT "Building symbol_example ROM..." + ) + + # Extract symbols from ELF + add_custom_command( + OUTPUT ${SYMBOL_EXAMPLE_SYMBOLS_TXT} + COMMAND ${M68K_NM} -n ${SYMBOL_EXAMPLE_ELF} > ${SYMBOL_EXAMPLE_SYMBOLS_TXT} + DEPENDS ${SYMBOL_EXAMPLE_ELF} + COMMENT "Extracting symbols from symbol_example.elf..." + ) + + # Generate C++ symbol header + add_custom_command( + OUTPUT ${GENERATED_INCLUDE_DIR}/symbol_example_symbols.h + COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/elf2sym.py + ${SYMBOL_EXAMPLE_SYMBOLS_TXT} > ${GENERATED_INCLUDE_DIR}/symbol_example_symbols.h + DEPENDS + ${SYMBOL_EXAMPLE_SYMBOLS_TXT} + ${CMAKE_SOURCE_DIR}/tools/elf2sym.py + COMMENT "Generating symbol_example_symbols.h..." + ) + + # Generate C++ ROM header with embedded binary + add_custom_command( + OUTPUT ${GENERATED_INCLUDE_DIR}/symbol_example_rom.h + COMMAND make -C ${SYMBOL_EXAMPLE_DIR} symbol_example_rom.h + COMMAND ${CMAKE_COMMAND} -E copy + ${SYMBOL_EXAMPLE_DIR}/symbol_example_rom.h + ${GENERATED_INCLUDE_DIR}/symbol_example_rom.h + DEPENDS ${SYMBOL_EXAMPLE_ROM} + COMMENT "Generating symbol_example_rom.h..." + ) + + # Custom target for ROM generation + add_custom_target(symbol_example_rom_gen + DEPENDS + ${GENERATED_INCLUDE_DIR}/symbol_example_symbols.h + ${GENERATED_INCLUDE_DIR}/symbol_example_rom.h + ) + + # Symbol-based test executable + add_executable(gxtest_symbol_example + tests/symbol_example_test.cpp + ) + + add_dependencies(gxtest_symbol_example symbol_example_rom_gen) + + target_link_libraries(gxtest_symbol_example + gxtest + genplusgx_core + GTest::gtest_main + ) + + target_include_directories(gxtest_symbol_example PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${GENERATED_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/vendor/genplusgx + ) + + gtest_discover_tests(gxtest_symbol_example) + + message(STATUS "Symbol example test enabled") + else() + message(WARNING "m68k-elf toolchain not found, ROM builds disabled") + endif() +else() + # When GXTEST_BUILD_ROMS is OFF, check for pre-built headers + set(PREBUILT_SYMBOLS ${CMAKE_SOURCE_DIR}/tests/symbol_example_symbols.h) + set(PREBUILT_ROM ${CMAKE_SOURCE_DIR}/tests/symbol_example_rom.h) + + if(EXISTS ${PREBUILT_SYMBOLS} AND EXISTS ${PREBUILT_ROM}) + message(STATUS "Using pre-built symbol_example headers") + + add_executable(gxtest_symbol_example + tests/symbol_example_test.cpp + ) + + target_link_libraries(gxtest_symbol_example + gxtest + genplusgx_core + GTest::gtest_main + ) + + target_include_directories(gxtest_symbol_example PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/tests + ${CMAKE_SOURCE_DIR}/vendor/genplusgx + ) + + gtest_discover_tests(gxtest_symbol_example) + else() + message(STATUS "Symbol example test disabled (no pre-built headers and GXTEST_BUILD_ROMS=OFF)") + message(STATUS "To enable: cmake -DGXTEST_BUILD_ROMS=ON or run 'make install' in roms/symbol_example/") + endif() +endif() + # ----------------------------------------------------------------------------- # Installation # ----------------------------------------------------------------------------- diff --git a/README.md b/README.md new file mode 100644 index 0000000..32b0641 --- /dev/null +++ b/README.md @@ -0,0 +1,249 @@ +# gxtest + +A headless Genesis/Mega Drive test harness built on [Genesis Plus GX](https://github.com/ekeeke/Genesis-Plus-GX). gxtest enables fast, automated testing of Genesis ROMs using GoogleTest, with direct memory access for assertions and symbol-based debugging. + +## Features + +- **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 + +## Quick Start + +### Building + +```bash +mkdir build && cd build +cmake .. +make -j4 +``` + +To enable building test ROMs from source (requires `m68k-elf-gcc`): + +```bash +cmake -DGXTEST_BUILD_ROMS=ON .. +``` + +### Running Tests + +```bash +ctest --output-on-failure +``` + +## Writing Tests + +### Basic Test Fixture + +```cpp +#include + +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); +} +``` + +### Loading ROMs + +```cpp +// From file +emu.LoadRom("roms/game.bin"); + +// From embedded byte array +emu.LoadRom(ROM_DATA, ROM_SIZE); +``` + +### Memory Access + +```cpp +// 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 +``` + +### Conditional Execution + +```cpp +// 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); +``` + +### Input Simulation + +```cpp +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); +``` + +## Symbol-Based Testing + +For readable tests that use variable names instead of addresses, gxtest supports extracting symbols from ELF files. + +### Pipeline Overview + +1. **Compile ROM** with debug symbols (`m68k-elf-gcc` produces `.elf`) +2. **Extract symbols** using `tools/elf2sym.py` +3. **Include generated header** in your tests +4. **Assert using symbol names** + +### Example + +ROM source (`main.c`): +```c +volatile uint16_t player_score = 0; +volatile uint8_t player_lives = 3; + +void main(void) { + while (!game_over) { + player_score += 10; + // ... + } +} +``` + +Extract symbols: +```bash +m68k-elf-nm -n rom.elf | python3 tools/elf2sym.py > symbols.h +``` + +Generated header (`symbols.h`): +```cpp +namespace Sym { + constexpr uint32_t player_score = 0xFF0008; + constexpr uint32_t player_lives = 0xFF0018; +} +``` + +Test using symbols: +```cpp +#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); +} +``` + +### Benefits + +- **Compile-time safety**: Renamed variables cause build failures, not runtime crashes +- **IDE autocomplete**: Symbol names appear in code completion +- **Self-documenting**: `Sym::player_score` is clearer than `0xFF0008` + +## API Reference + +### GX::Emulator + +| 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 | + +### GX::Test + +Base class for test fixtures. Provides: +- `emu` - Emulator instance +- `ReadByte/Word/Long()` - Convenience wrappers +- `WriteByte/Word/Long()` - Convenience wrappers + +## Project Structure + +``` +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 +``` + +## Requirements + +- CMake 3.14+ +- C++17 compiler +- Python 3 (for symbol extraction) +- m68k-elf-gcc (optional, for building test ROMs) + +## License + +gxtest framework is provided under the MIT license. Genesis Plus GX is licensed under its own terms (see `vendor/genplusgx/`). diff --git a/roms/symbol_example/Makefile b/roms/symbol_example/Makefile new file mode 100644 index 0000000..256415f --- /dev/null +++ b/roms/symbol_example/Makefile @@ -0,0 +1,120 @@ +# Symbol Example ROM Makefile +# Cross-compiles C code to a Genesis/Mega Drive ROM with symbol extraction + +# Cross-compiler toolchain +PREFIX ?= m68k-elf- +CC = $(PREFIX)gcc +AS = $(PREFIX)as +LD = $(PREFIX)ld +NM = $(PREFIX)nm +OBJCOPY = $(PREFIX)objcopy + +# Flags +CFLAGS = -m68000 -Os -fomit-frame-pointer -fno-builtin -nostdlib -Wall -Wno-main +ASFLAGS = -m68000 +LDFLAGS = -T genesis.ld -nostdlib + +# Output +TARGET = symbol_example +ROM = $(TARGET).bin +ELF = $(TARGET).elf +ROM_HEADER = symbol_example_rom.h +SYM_HEADER = symbol_example_symbols.h +SYM_TXT = $(TARGET)_symbols.txt + +# Tools +ELF2SYM = ../../tools/elf2sym.py + +# Source files +CSRC = main.c +ASRC = crt0.s + +# Object files +OBJS = crt0.o main.o + +.PHONY: all clean header symbols install + +all: $(ROM) $(ROM_HEADER) $(SYM_HEADER) + +# Link object files into ELF +$(ELF): $(OBJS) genesis.ld + $(LD) $(LDFLAGS) -o $@ $(OBJS) + +# Convert ELF to binary ROM +$(ROM): $(ELF) + $(OBJCOPY) -O binary $< $@ + @echo "ROM size: $$(wc -c < $@) bytes" + +# Compile C sources +%.o: %.c + $(CC) $(CFLAGS) -c -o $@ $< + +# Assemble startup code +%.o: %.s + $(AS) $(ASFLAGS) -o $@ $< + +# Extract symbols from ELF +$(SYM_TXT): $(ELF) + $(NM) -n $< > $@ + +# Generate C++ symbol header using elf2sym.py +$(SYM_HEADER): $(SYM_TXT) $(ELF2SYM) + python3 $(ELF2SYM) $< > $@ + @echo "Generated $(SYM_HEADER)" + +# Generate C++ header with embedded ROM data +$(ROM_HEADER): $(ROM) + @echo "// Auto-generated from $(ROM)" > $@ + @echo "// Built from C source code" >> $@ + @echo "" >> $@ + @echo "#ifndef SYMBOL_EXAMPLE_ROM_H" >> $@ + @echo "#define SYMBOL_EXAMPLE_ROM_H" >> $@ + @echo "" >> $@ + @echo "#include " >> $@ + @echo "#include " >> $@ + @echo "" >> $@ + @echo "namespace GX {" >> $@ + @echo "namespace TestRoms {" >> $@ + @echo "" >> $@ + @echo "// Symbol Example ROM ($$(wc -c < $(ROM) | tr -d ' ') bytes)" >> $@ + @echo "// A simple game simulation demonstrating symbol-based testing" >> $@ + @echo "//" >> $@ + @echo "// Global variables available via generated symbols header:" >> $@ + @echo "// player_score, player_lives, player_x, player_y" >> $@ + @echo "// game_state, game_over, frame_count, level" >> $@ + @echo "// enemy_x, enemy_y, enemy_active" >> $@ + @echo "// init_complete, done_flag" >> $@ + @echo "" >> $@ + @echo "constexpr size_t SYMBOL_EXAMPLE_ROM_SIZE = $$(wc -c < $(ROM) | tr -d ' ');" >> $@ + @echo "" >> $@ + @echo "constexpr uint8_t SYMBOL_EXAMPLE_ROM[] = {" >> $@ + @xxd -i < $(ROM) | sed 's/^/ /' >> $@ + @echo "};" >> $@ + @echo "" >> $@ + @echo "// Sentinel values for synchronization" >> $@ + @echo "constexpr uint16_t INIT_SENTINEL = 0xBEEF;" >> $@ + @echo "constexpr uint16_t DONE_SENTINEL = 0xDEAD;" >> $@ + @echo "" >> $@ + @echo "// Game state values" >> $@ + @echo "constexpr uint8_t STATE_INIT = 0;" >> $@ + @echo "constexpr uint8_t STATE_PLAYING = 1;" >> $@ + @echo "constexpr uint8_t STATE_PAUSED = 2;" >> $@ + @echo "constexpr uint8_t STATE_GAME_OVER = 3;" >> $@ + @echo "" >> $@ + @echo "} // namespace TestRoms" >> $@ + @echo "} // namespace GX" >> $@ + @echo "" >> $@ + @echo "#endif // SYMBOL_EXAMPLE_ROM_H" >> $@ + @echo "Generated $(ROM_HEADER)" + +symbols: $(SYM_HEADER) + @echo "Symbols:" + @cat $(SYM_HEADER) + +clean: + rm -f $(OBJS) $(ELF) $(ROM) $(ROM_HEADER) $(SYM_HEADER) $(SYM_TXT) + +# Install headers to tests directory +install: $(ROM_HEADER) $(SYM_HEADER) + cp $(ROM_HEADER) $(SYM_HEADER) ../../tests/ + @echo "Installed headers to tests/" diff --git a/roms/symbol_example/crt0.s b/roms/symbol_example/crt0.s new file mode 100644 index 0000000..02f896c --- /dev/null +++ b/roms/symbol_example/crt0.s @@ -0,0 +1,58 @@ +/* + * Genesis startup code for gxtest ROMs + * Minimal crt0 - initializes BSS, copies .data from ROM, and calls main() + */ + + .section .header + .ascii "SEGA MEGA DRIVE " /* Console name (16 bytes) */ + .ascii "(C)GXTEST 2026 " /* Copyright (16 bytes) */ + .ascii "SYMBOL EXAMPLE ROM " /* Domestic name (48 bytes) */ + .ascii "SYMBOL EXAMPLE ROM " /* Overseas name (48 bytes) */ + .ascii "GM 00000001-00" /* Serial/version (14 bytes) */ + .word 0x0000 /* Checksum placeholder */ + .ascii "J " /* I/O support (16 bytes) */ + .long 0x00000000 /* ROM start */ + .long 0x000007FF /* ROM end (rounded up to 2KB) */ + .long 0x00FF0000 /* RAM start */ + .long 0x00FFFFFF /* RAM end */ + .ascii " " /* SRAM info (12 bytes) */ + .ascii " " /* Notes (12 bytes) */ + .ascii "JUE " /* Region + padding to 0x1FF */ + + .section .text + .global _start + .type _start, @function + +_start: + /* Initialize stack pointer */ + lea 0x00FFFE00, %sp + + /* Clear BSS section */ + lea __bss_start, %a0 + lea __bss_end, %a1 + cmp.l %a0, %a1 + beq.s .Lbss_done +.Lbss_clear: + clr.b (%a0)+ + cmp.l %a0, %a1 + bne.s .Lbss_clear +.Lbss_done: + + /* Copy .data section from ROM (LMA) to RAM (VMA) */ + lea __data_load, %a0 /* Source: ROM load address */ + lea __data_start, %a1 /* Destination: RAM start of .data */ + lea __data_end, %a2 /* End of .data in RAM */ + cmp.l %a1, %a2 + beq.s .Ldata_done +.Ldata_copy: + move.b (%a0)+, (%a1)+ + cmp.l %a1, %a2 + bne.s .Ldata_copy +.Ldata_done: + + /* Call main */ + jsr main + + /* Infinite loop after main returns */ +.Lhang: + bra.s .Lhang diff --git a/roms/symbol_example/genesis.ld b/roms/symbol_example/genesis.ld new file mode 100644 index 0000000..c223c71 --- /dev/null +++ b/roms/symbol_example/genesis.ld @@ -0,0 +1,58 @@ +/* Genesis/Mega Drive Linker Script for gxtest ROMs */ + +OUTPUT_FORMAT("elf32-m68k") +OUTPUT_ARCH(m68k) +ENTRY(_start) + +MEMORY +{ + rom (rx) : ORIGIN = 0x000000, LENGTH = 4M + ram (rwx) : ORIGIN = 0xFF0000, LENGTH = 64K +} + +SECTIONS +{ + /* Vector table at start of ROM */ + .vectors 0x000000 : + { + LONG(0x00FFFE00) /* Initial SSP (stack at end of RAM) */ + LONG(_start) /* Initial PC (entry point) */ + + /* Exception vectors - point to infinite loop */ + . = 0x100; + } > rom + + /* ROM header at 0x100 */ + .header 0x000100 : + { + *(.header) + . = 0x100; /* Header is exactly 256 bytes */ + } > rom + + /* Code starts at 0x200 */ + .text 0x000200 : + { + *(.text*) + *(.rodata*) + . = ALIGN(2); + } > rom + + /* Work RAM */ + .bss 0xFF0000 (NOLOAD) : + { + __bss_start = .; + *(.bss*) + *(COMMON) + __bss_end = .; + } > ram + + .data __bss_end : + { + __data_start = .; + *(.data*) + __data_end = .; + } > ram AT > rom + + /* Symbol for .data load address in ROM */ + __data_load = LOADADDR(.data); +} diff --git a/roms/symbol_example/main.c b/roms/symbol_example/main.c new file mode 100644 index 0000000..4fc6473 --- /dev/null +++ b/roms/symbol_example/main.c @@ -0,0 +1,213 @@ +/** + * Symbol Example ROM for gxtest + * + * Demonstrates symbol-based testing by exposing global variables + * that can be extracted from the ELF and used in test assertions. + * + * This ROM simulates a simple game loop that: + * - Increments a score counter each frame + * - Tracks player lives + * - Sets a game_over flag when conditions are met + * - Maintains a game_state enum + * + * The test harness can inject values and verify state transitions. + */ + +/* Bare-metal type definitions (no standard library) */ +typedef unsigned char uint8_t; +typedef unsigned short uint16_t; +typedef unsigned int uint32_t; + +/* ========================================================================== + * GLOBAL VARIABLES (these will appear in the ELF symbol table) + * ========================================================================== + * Note: We use 'volatile' to prevent the compiler from optimizing away + * reads/writes, since the test harness will be poking at these externally. + */ + +/* Player state */ +volatile uint16_t player_score = 0; /* Current score (increments by 10 each frame) */ +volatile uint8_t player_lives = 3; /* Lives remaining */ +volatile uint16_t player_x = 160; /* X position (center of 320-wide screen) */ +volatile uint16_t player_y = 200; /* Y position */ + +/* Game state */ +volatile uint8_t game_state = 0; /* 0=init, 1=playing, 2=paused, 3=game_over */ +volatile uint8_t game_over = 0; /* Non-zero when game has ended */ +volatile uint16_t frame_count = 0; /* Frames elapsed */ +volatile uint16_t level = 1; /* Current level */ + +/* Enemy state (for testing array-like access) */ +volatile uint16_t enemy_x = 50; +volatile uint16_t enemy_y = 50; +volatile uint8_t enemy_active = 1; + +/* Sentinel values for test synchronization */ +volatile uint16_t init_complete = 0; /* Set to 0xBEEF when init done */ +volatile uint16_t done_flag = 0; /* Set to 0xDEAD when game ends */ + +/* ========================================================================== + * GAME STATE CONSTANTS + * ========================================================================== */ +#define STATE_INIT 0 +#define STATE_PLAYING 1 +#define STATE_PAUSED 2 +#define STATE_GAME_OVER 3 + +#define INIT_SENTINEL 0xBEEF +#define DONE_SENTINEL 0xDEAD + +#define SCORE_PER_FRAME 10 +#define SCORE_FOR_LEVEL 1000 +#define MAX_LEVEL 5 + +/* ========================================================================== + * GAME LOGIC + * ========================================================================== */ + +/** + * Initialize game state + */ +static void game_init(void) +{ + player_score = 0; + player_lives = 3; + player_x = 160; + player_y = 200; + game_state = STATE_INIT; + game_over = 0; + frame_count = 0; + level = 1; + enemy_x = 50; + enemy_y = 50; + enemy_active = 1; + done_flag = 0; + + /* Signal initialization complete */ + init_complete = INIT_SENTINEL; + game_state = STATE_PLAYING; +} + +/** + * Update score and check for level advancement + */ +static void update_score(void) +{ + player_score += SCORE_PER_FRAME; + + /* Level up every 1000 points, capped at MAX_LEVEL */ + uint16_t expected_level = (player_score / SCORE_FOR_LEVEL) + 1; + if (expected_level > MAX_LEVEL) { + expected_level = MAX_LEVEL; + } + if (expected_level > level) { + level = expected_level; + } +} + +/** + * Simple enemy movement (bounces around) + */ +static void update_enemy(void) +{ + if (!enemy_active) { + return; + } + + /* Move enemy based on frame count */ + enemy_x = 50 + (frame_count % 200); + enemy_y = 50 + ((frame_count / 2) % 150); +} + +/** + * Check for collision between player and enemy + * Returns 1 if collision detected + */ +static int check_collision(void) +{ + if (!enemy_active) { + return 0; + } + + /* Simple bounding box collision (32x32 sprites assumed) */ + int dx = (int)player_x - (int)enemy_x; + int dy = (int)player_y - (int)enemy_y; + + if (dx < 0) dx = -dx; /* abs */ + if (dy < 0) dy = -dy; + + return (dx < 32 && dy < 32); +} + +/** + * Handle player taking damage + */ +static void player_hit(void) +{ + if (player_lives > 0) { + player_lives--; + } + + if (player_lives == 0) { + game_over = 1; + game_state = STATE_GAME_OVER; + done_flag = DONE_SENTINEL; + } + + /* Brief invincibility: deactivate enemy temporarily */ + enemy_active = 0; +} + +/** + * Main game loop iteration + */ +static void game_update(void) +{ + if (game_state != STATE_PLAYING) { + return; + } + + frame_count++; + + /* Update game objects */ + update_score(); + update_enemy(); + + /* Reactivate enemy after some frames */ + if (!enemy_active && (frame_count % 60) == 0) { + enemy_active = 1; + } + + /* Check win condition: reach max level with high score */ + if (level >= MAX_LEVEL && player_score >= 5000) { + game_over = 1; + game_state = STATE_GAME_OVER; + done_flag = DONE_SENTINEL; + } + + /* Check collision (lose condition handled in player_hit) */ + if (check_collision()) { + player_hit(); + } +} + +/** + * Main entry point + */ +void main(void) +{ + /* Initialize game */ + game_init(); + + /* Run game loop until game over or max frames reached */ + while (!game_over && frame_count < 1000) { + game_update(); + } + + /* Ensure done_flag is set if we exited due to frame limit */ + if (!done_flag) { + done_flag = DONE_SENTINEL; + } + + /* The startup code will loop forever after main returns */ +} diff --git a/roms/symbol_example/symbol_example_rom.h b/roms/symbol_example/symbol_example_rom.h new file mode 100644 index 0000000..4dc201a --- /dev/null +++ b/roms/symbol_example/symbol_example_rom.h @@ -0,0 +1,135 @@ +// Auto-generated from symbol_example.bin +// Built from C source code + +#ifndef SYMBOL_EXAMPLE_ROM_H +#define SYMBOL_EXAMPLE_ROM_H + +#include +#include + +namespace GX { +namespace TestRoms { + +// Symbol Example ROM (1139 bytes) +// A simple game simulation demonstrating symbol-based testing +// +// Global variables available via generated symbols header: +// player_score, player_lives, player_x, player_y +// game_state, game_over, frame_count, level +// enemy_x, enemy_y, enemy_active +// init_complete, done_flag + +constexpr size_t SYMBOL_EXAMPLE_ROM_SIZE = 1139; + +constexpr uint8_t SYMBOL_EXAMPLE_ROM[] = { + 0x00, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x53, 0x45, 0x47, 0x41, 0x20, 0x4d, 0x45, 0x47, + 0x41, 0x20, 0x44, 0x52, 0x49, 0x56, 0x45, 0x20, 0x28, 0x43, 0x29, 0x47, + 0x58, 0x54, 0x45, 0x53, 0x54, 0x20, 0x32, 0x30, 0x32, 0x36, 0x20, 0x20, + 0x53, 0x59, 0x4d, 0x42, 0x4f, 0x4c, 0x20, 0x45, 0x58, 0x41, 0x4d, 0x50, + 0x4c, 0x45, 0x20, 0x52, 0x4f, 0x4d, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x53, 0x59, 0x4d, 0x42, 0x4f, 0x4c, 0x20, 0x45, 0x58, 0x41, 0x4d, 0x50, + 0x4c, 0x45, 0x20, 0x52, 0x4f, 0x4d, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x47, 0x4d, 0x20, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x31, 0x2d, + 0x30, 0x30, 0x00, 0x00, 0x4a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x07, 0xff, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x4a, 0x55, 0x45, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4f, 0xf9, 0x00, 0xff, + 0xfe, 0x00, 0x41, 0xf9, 0x00, 0xff, 0x00, 0x00, 0x43, 0xf9, 0x00, 0xff, + 0x00, 0x0a, 0xb3, 0xc8, 0x67, 0x06, 0x42, 0x18, 0xb3, 0xc8, 0x66, 0xfa, + 0x41, 0xf9, 0x00, 0x00, 0x04, 0x64, 0x43, 0xf9, 0x00, 0xff, 0x00, 0x0a, + 0x45, 0xf9, 0x00, 0xff, 0x00, 0x19, 0xb5, 0xc9, 0x67, 0x06, 0x12, 0xd8, + 0xb5, 0xc9, 0x66, 0xfa, 0x4e, 0xb9, 0x00, 0x00, 0x02, 0x40, 0x60, 0xfe, + 0x2f, 0x02, 0x33, 0xfc, 0x00, 0x00, 0x00, 0xff, 0x00, 0x08, 0x13, 0xfc, + 0x00, 0x03, 0x00, 0xff, 0x00, 0x18, 0x33, 0xfc, 0x00, 0xa0, 0x00, 0xff, + 0x00, 0x16, 0x33, 0xfc, 0x00, 0xc8, 0x00, 0xff, 0x00, 0x14, 0x13, 0xfc, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x07, 0x13, 0xfc, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x06, 0x33, 0xfc, 0x00, 0x00, 0x00, 0xff, 0x00, 0x04, 0x33, 0xfc, + 0x00, 0x01, 0x00, 0xff, 0x00, 0x12, 0x33, 0xfc, 0x00, 0x32, 0x00, 0xff, + 0x00, 0x10, 0x33, 0xfc, 0x00, 0x32, 0x00, 0xff, 0x00, 0x0e, 0x13, 0xfc, + 0x00, 0x01, 0x00, 0xff, 0x00, 0x0c, 0x33, 0xfc, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x00, 0x33, 0xfc, 0xbe, 0xef, 0x00, 0xff, 0x00, 0x02, 0x13, 0xfc, + 0x00, 0x01, 0x00, 0xff, 0x00, 0x07, 0x10, 0x39, 0x00, 0xff, 0x00, 0x06, + 0x66, 0x0c, 0x30, 0x39, 0x00, 0xff, 0x00, 0x04, 0x0c, 0x40, 0x03, 0xe7, + 0x63, 0x14, 0x30, 0x39, 0x00, 0xff, 0x00, 0x00, 0x66, 0x08, 0x33, 0xfc, + 0xde, 0xad, 0x00, 0xff, 0x00, 0x00, 0x24, 0x1f, 0x4e, 0x75, 0x10, 0x39, + 0x00, 0xff, 0x00, 0x07, 0x0c, 0x00, 0x00, 0x01, 0x66, 0xcc, 0x30, 0x39, + 0x00, 0xff, 0x00, 0x04, 0x52, 0x40, 0x33, 0xc0, 0x00, 0xff, 0x00, 0x04, + 0x30, 0x39, 0x00, 0xff, 0x00, 0x08, 0x06, 0x40, 0x00, 0x0a, 0x33, 0xc0, + 0x00, 0xff, 0x00, 0x08, 0x30, 0x39, 0x00, 0xff, 0x00, 0x08, 0xe6, 0x48, + 0xc0, 0xfc, 0x20, 0xc5, 0x42, 0x40, 0x48, 0x40, 0xe8, 0x48, 0x52, 0x40, + 0x0c, 0x40, 0x00, 0x05, 0x63, 0x02, 0x70, 0x05, 0x32, 0x39, 0x00, 0xff, + 0x00, 0x12, 0xb2, 0x40, 0x64, 0x06, 0x33, 0xc0, 0x00, 0xff, 0x00, 0x12, + 0x10, 0x39, 0x00, 0xff, 0x00, 0x0c, 0x67, 0x40, 0x32, 0x39, 0x00, 0xff, + 0x00, 0x04, 0x02, 0x81, 0x00, 0x00, 0xff, 0xff, 0x82, 0xfc, 0x00, 0xc8, + 0x20, 0x01, 0x48, 0x40, 0x06, 0x40, 0x00, 0x32, 0x33, 0xc0, 0x00, 0xff, + 0x00, 0x10, 0x30, 0x39, 0x00, 0xff, 0x00, 0x04, 0xe2, 0x48, 0x02, 0x80, + 0x00, 0x00, 0xff, 0xff, 0x80, 0xfc, 0x00, 0x96, 0x22, 0x00, 0x48, 0x41, + 0x30, 0x01, 0x06, 0x40, 0x00, 0x32, 0x33, 0xc0, 0x00, 0xff, 0x00, 0x0e, + 0x10, 0x39, 0x00, 0xff, 0x00, 0x0c, 0x66, 0x20, 0x30, 0x39, 0x00, 0xff, + 0x00, 0x04, 0x74, 0x3c, 0x02, 0x80, 0x00, 0x00, 0xff, 0xff, 0x80, 0xc2, + 0x22, 0x00, 0x48, 0x41, 0x4a, 0x41, 0x66, 0x08, 0x13, 0xfc, 0x00, 0x01, + 0x00, 0xff, 0x00, 0x0c, 0x30, 0x39, 0x00, 0xff, 0x00, 0x12, 0x0c, 0x40, + 0x00, 0x04, 0x63, 0x24, 0x30, 0x39, 0x00, 0xff, 0x00, 0x08, 0x0c, 0x40, + 0x13, 0x87, 0x63, 0x18, 0x13, 0xfc, 0x00, 0x01, 0x00, 0xff, 0x00, 0x06, + 0x13, 0xfc, 0x00, 0x03, 0x00, 0xff, 0x00, 0x07, 0x33, 0xfc, 0xde, 0xad, + 0x00, 0xff, 0x00, 0x00, 0x10, 0x39, 0x00, 0xff, 0x00, 0x0c, 0x67, 0x00, + 0xfe, 0xda, 0x32, 0x39, 0x00, 0xff, 0x00, 0x16, 0x32, 0x79, 0x00, 0xff, + 0x00, 0x10, 0x30, 0x39, 0x00, 0xff, 0x00, 0x14, 0x30, 0x79, 0x00, 0xff, + 0x00, 0x0e, 0x02, 0x81, 0x00, 0x00, 0xff, 0xff, 0x74, 0x00, 0x34, 0x09, + 0x92, 0x82, 0x6a, 0x02, 0x44, 0x81, 0x74, 0x1f, 0xb4, 0x81, 0x6d, 0x00, + 0xfe, 0xaa, 0x02, 0x80, 0x00, 0x00, 0xff, 0xff, 0x72, 0x00, 0x32, 0x08, + 0x90, 0x81, 0x6a, 0x02, 0x44, 0x80, 0x72, 0x1f, 0xb2, 0x80, 0x6d, 0x00, + 0xfe, 0x92, 0x10, 0x39, 0x00, 0xff, 0x00, 0x18, 0x67, 0x0e, 0x10, 0x39, + 0x00, 0xff, 0x00, 0x18, 0x53, 0x00, 0x13, 0xc0, 0x00, 0xff, 0x00, 0x18, + 0x10, 0x39, 0x00, 0xff, 0x00, 0x18, 0x66, 0x18, 0x13, 0xfc, 0x00, 0x01, + 0x00, 0xff, 0x00, 0x06, 0x13, 0xfc, 0x00, 0x03, 0x00, 0xff, 0x00, 0x07, + 0x33, 0xfc, 0xde, 0xad, 0x00, 0xff, 0x00, 0x00, 0x13, 0xfc, 0x00, 0x00, + 0x00, 0xff, 0x00, 0x0c, 0x60, 0x00, 0xfe, 0x50, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x32, 0x00, 0x32, 0x00, 0x01, 0x00, 0xc8, 0x00, 0xa0, 0x03 +}; + +// Sentinel values for synchronization +constexpr uint16_t INIT_SENTINEL = 0xBEEF; +constexpr uint16_t DONE_SENTINEL = 0xDEAD; + +// Game state values +constexpr uint8_t STATE_INIT = 0; +constexpr uint8_t STATE_PLAYING = 1; +constexpr uint8_t STATE_PAUSED = 2; +constexpr uint8_t STATE_GAME_OVER = 3; + +} // namespace TestRoms +} // namespace GX + +#endif // SYMBOL_EXAMPLE_ROM_H diff --git a/roms/symbol_example/symbol_example_symbols.h b/roms/symbol_example/symbol_example_symbols.h new file mode 100644 index 0000000..9ff65b8 --- /dev/null +++ b/roms/symbol_example/symbol_example_symbols.h @@ -0,0 +1,26 @@ +#pragma once + +// Auto-generated symbol table from Genesis ELF +// Generated: 2026-01-20T08:21:22.652378 +// Source: symbol_example_symbols.txt + +#include + +namespace Sym { + + constexpr uint32_t done_flag = 0xFF0000; + constexpr uint32_t init_complete = 0xFF0002; + constexpr uint32_t frame_count = 0xFF0004; + constexpr uint32_t game_over = 0xFF0006; + constexpr uint32_t game_state = 0xFF0007; + constexpr uint32_t player_score = 0xFF0008; + constexpr uint32_t enemy_active = 0xFF000C; + constexpr uint32_t enemy_y = 0xFF000E; + constexpr uint32_t enemy_x = 0xFF0010; + constexpr uint32_t level = 0xFF0012; + constexpr uint32_t player_y = 0xFF0014; + constexpr uint32_t player_x = 0xFF0016; + constexpr uint32_t player_lives = 0xFF0018; + +} // namespace Sym + diff --git a/roms/symbol_example/symbol_example_symbols.txt b/roms/symbol_example/symbol_example_symbols.txt new file mode 100644 index 0000000..0cf461d --- /dev/null +++ b/roms/symbol_example/symbol_example_symbols.txt @@ -0,0 +1,20 @@ +00000200 T _start +00000240 T main +00000464 A __data_load +00ff0000 B __bss_start +00ff0000 B done_flag +00ff0002 B init_complete +00ff0004 B frame_count +00ff0006 B game_over +00ff0007 B game_state +00ff0008 B player_score +00ff000a B __bss_end +00ff000a D __data_start +00ff000c D enemy_active +00ff000e D enemy_y +00ff0010 D enemy_x +00ff0012 D level +00ff0014 D player_y +00ff0016 D player_x +00ff0018 D player_lives +00ff0019 D __data_end diff --git a/tests/symbol_example_rom.h b/tests/symbol_example_rom.h new file mode 100644 index 0000000..4dc201a --- /dev/null +++ b/tests/symbol_example_rom.h @@ -0,0 +1,135 @@ +// Auto-generated from symbol_example.bin +// Built from C source code + +#ifndef SYMBOL_EXAMPLE_ROM_H +#define SYMBOL_EXAMPLE_ROM_H + +#include +#include + +namespace GX { +namespace TestRoms { + +// Symbol Example ROM (1139 bytes) +// A simple game simulation demonstrating symbol-based testing +// +// Global variables available via generated symbols header: +// player_score, player_lives, player_x, player_y +// game_state, game_over, frame_count, level +// enemy_x, enemy_y, enemy_active +// init_complete, done_flag + +constexpr size_t SYMBOL_EXAMPLE_ROM_SIZE = 1139; + +constexpr uint8_t SYMBOL_EXAMPLE_ROM[] = { + 0x00, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x53, 0x45, 0x47, 0x41, 0x20, 0x4d, 0x45, 0x47, + 0x41, 0x20, 0x44, 0x52, 0x49, 0x56, 0x45, 0x20, 0x28, 0x43, 0x29, 0x47, + 0x58, 0x54, 0x45, 0x53, 0x54, 0x20, 0x32, 0x30, 0x32, 0x36, 0x20, 0x20, + 0x53, 0x59, 0x4d, 0x42, 0x4f, 0x4c, 0x20, 0x45, 0x58, 0x41, 0x4d, 0x50, + 0x4c, 0x45, 0x20, 0x52, 0x4f, 0x4d, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x53, 0x59, 0x4d, 0x42, 0x4f, 0x4c, 0x20, 0x45, 0x58, 0x41, 0x4d, 0x50, + 0x4c, 0x45, 0x20, 0x52, 0x4f, 0x4d, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x47, 0x4d, 0x20, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x31, 0x2d, + 0x30, 0x30, 0x00, 0x00, 0x4a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x07, 0xff, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x4a, 0x55, 0x45, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4f, 0xf9, 0x00, 0xff, + 0xfe, 0x00, 0x41, 0xf9, 0x00, 0xff, 0x00, 0x00, 0x43, 0xf9, 0x00, 0xff, + 0x00, 0x0a, 0xb3, 0xc8, 0x67, 0x06, 0x42, 0x18, 0xb3, 0xc8, 0x66, 0xfa, + 0x41, 0xf9, 0x00, 0x00, 0x04, 0x64, 0x43, 0xf9, 0x00, 0xff, 0x00, 0x0a, + 0x45, 0xf9, 0x00, 0xff, 0x00, 0x19, 0xb5, 0xc9, 0x67, 0x06, 0x12, 0xd8, + 0xb5, 0xc9, 0x66, 0xfa, 0x4e, 0xb9, 0x00, 0x00, 0x02, 0x40, 0x60, 0xfe, + 0x2f, 0x02, 0x33, 0xfc, 0x00, 0x00, 0x00, 0xff, 0x00, 0x08, 0x13, 0xfc, + 0x00, 0x03, 0x00, 0xff, 0x00, 0x18, 0x33, 0xfc, 0x00, 0xa0, 0x00, 0xff, + 0x00, 0x16, 0x33, 0xfc, 0x00, 0xc8, 0x00, 0xff, 0x00, 0x14, 0x13, 0xfc, + 0x00, 0x00, 0x00, 0xff, 0x00, 0x07, 0x13, 0xfc, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x06, 0x33, 0xfc, 0x00, 0x00, 0x00, 0xff, 0x00, 0x04, 0x33, 0xfc, + 0x00, 0x01, 0x00, 0xff, 0x00, 0x12, 0x33, 0xfc, 0x00, 0x32, 0x00, 0xff, + 0x00, 0x10, 0x33, 0xfc, 0x00, 0x32, 0x00, 0xff, 0x00, 0x0e, 0x13, 0xfc, + 0x00, 0x01, 0x00, 0xff, 0x00, 0x0c, 0x33, 0xfc, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x00, 0x33, 0xfc, 0xbe, 0xef, 0x00, 0xff, 0x00, 0x02, 0x13, 0xfc, + 0x00, 0x01, 0x00, 0xff, 0x00, 0x07, 0x10, 0x39, 0x00, 0xff, 0x00, 0x06, + 0x66, 0x0c, 0x30, 0x39, 0x00, 0xff, 0x00, 0x04, 0x0c, 0x40, 0x03, 0xe7, + 0x63, 0x14, 0x30, 0x39, 0x00, 0xff, 0x00, 0x00, 0x66, 0x08, 0x33, 0xfc, + 0xde, 0xad, 0x00, 0xff, 0x00, 0x00, 0x24, 0x1f, 0x4e, 0x75, 0x10, 0x39, + 0x00, 0xff, 0x00, 0x07, 0x0c, 0x00, 0x00, 0x01, 0x66, 0xcc, 0x30, 0x39, + 0x00, 0xff, 0x00, 0x04, 0x52, 0x40, 0x33, 0xc0, 0x00, 0xff, 0x00, 0x04, + 0x30, 0x39, 0x00, 0xff, 0x00, 0x08, 0x06, 0x40, 0x00, 0x0a, 0x33, 0xc0, + 0x00, 0xff, 0x00, 0x08, 0x30, 0x39, 0x00, 0xff, 0x00, 0x08, 0xe6, 0x48, + 0xc0, 0xfc, 0x20, 0xc5, 0x42, 0x40, 0x48, 0x40, 0xe8, 0x48, 0x52, 0x40, + 0x0c, 0x40, 0x00, 0x05, 0x63, 0x02, 0x70, 0x05, 0x32, 0x39, 0x00, 0xff, + 0x00, 0x12, 0xb2, 0x40, 0x64, 0x06, 0x33, 0xc0, 0x00, 0xff, 0x00, 0x12, + 0x10, 0x39, 0x00, 0xff, 0x00, 0x0c, 0x67, 0x40, 0x32, 0x39, 0x00, 0xff, + 0x00, 0x04, 0x02, 0x81, 0x00, 0x00, 0xff, 0xff, 0x82, 0xfc, 0x00, 0xc8, + 0x20, 0x01, 0x48, 0x40, 0x06, 0x40, 0x00, 0x32, 0x33, 0xc0, 0x00, 0xff, + 0x00, 0x10, 0x30, 0x39, 0x00, 0xff, 0x00, 0x04, 0xe2, 0x48, 0x02, 0x80, + 0x00, 0x00, 0xff, 0xff, 0x80, 0xfc, 0x00, 0x96, 0x22, 0x00, 0x48, 0x41, + 0x30, 0x01, 0x06, 0x40, 0x00, 0x32, 0x33, 0xc0, 0x00, 0xff, 0x00, 0x0e, + 0x10, 0x39, 0x00, 0xff, 0x00, 0x0c, 0x66, 0x20, 0x30, 0x39, 0x00, 0xff, + 0x00, 0x04, 0x74, 0x3c, 0x02, 0x80, 0x00, 0x00, 0xff, 0xff, 0x80, 0xc2, + 0x22, 0x00, 0x48, 0x41, 0x4a, 0x41, 0x66, 0x08, 0x13, 0xfc, 0x00, 0x01, + 0x00, 0xff, 0x00, 0x0c, 0x30, 0x39, 0x00, 0xff, 0x00, 0x12, 0x0c, 0x40, + 0x00, 0x04, 0x63, 0x24, 0x30, 0x39, 0x00, 0xff, 0x00, 0x08, 0x0c, 0x40, + 0x13, 0x87, 0x63, 0x18, 0x13, 0xfc, 0x00, 0x01, 0x00, 0xff, 0x00, 0x06, + 0x13, 0xfc, 0x00, 0x03, 0x00, 0xff, 0x00, 0x07, 0x33, 0xfc, 0xde, 0xad, + 0x00, 0xff, 0x00, 0x00, 0x10, 0x39, 0x00, 0xff, 0x00, 0x0c, 0x67, 0x00, + 0xfe, 0xda, 0x32, 0x39, 0x00, 0xff, 0x00, 0x16, 0x32, 0x79, 0x00, 0xff, + 0x00, 0x10, 0x30, 0x39, 0x00, 0xff, 0x00, 0x14, 0x30, 0x79, 0x00, 0xff, + 0x00, 0x0e, 0x02, 0x81, 0x00, 0x00, 0xff, 0xff, 0x74, 0x00, 0x34, 0x09, + 0x92, 0x82, 0x6a, 0x02, 0x44, 0x81, 0x74, 0x1f, 0xb4, 0x81, 0x6d, 0x00, + 0xfe, 0xaa, 0x02, 0x80, 0x00, 0x00, 0xff, 0xff, 0x72, 0x00, 0x32, 0x08, + 0x90, 0x81, 0x6a, 0x02, 0x44, 0x80, 0x72, 0x1f, 0xb2, 0x80, 0x6d, 0x00, + 0xfe, 0x92, 0x10, 0x39, 0x00, 0xff, 0x00, 0x18, 0x67, 0x0e, 0x10, 0x39, + 0x00, 0xff, 0x00, 0x18, 0x53, 0x00, 0x13, 0xc0, 0x00, 0xff, 0x00, 0x18, + 0x10, 0x39, 0x00, 0xff, 0x00, 0x18, 0x66, 0x18, 0x13, 0xfc, 0x00, 0x01, + 0x00, 0xff, 0x00, 0x06, 0x13, 0xfc, 0x00, 0x03, 0x00, 0xff, 0x00, 0x07, + 0x33, 0xfc, 0xde, 0xad, 0x00, 0xff, 0x00, 0x00, 0x13, 0xfc, 0x00, 0x00, + 0x00, 0xff, 0x00, 0x0c, 0x60, 0x00, 0xfe, 0x50, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x32, 0x00, 0x32, 0x00, 0x01, 0x00, 0xc8, 0x00, 0xa0, 0x03 +}; + +// Sentinel values for synchronization +constexpr uint16_t INIT_SENTINEL = 0xBEEF; +constexpr uint16_t DONE_SENTINEL = 0xDEAD; + +// Game state values +constexpr uint8_t STATE_INIT = 0; +constexpr uint8_t STATE_PLAYING = 1; +constexpr uint8_t STATE_PAUSED = 2; +constexpr uint8_t STATE_GAME_OVER = 3; + +} // namespace TestRoms +} // namespace GX + +#endif // SYMBOL_EXAMPLE_ROM_H diff --git a/tests/symbol_example_symbols.h b/tests/symbol_example_symbols.h new file mode 100644 index 0000000..9ff65b8 --- /dev/null +++ b/tests/symbol_example_symbols.h @@ -0,0 +1,26 @@ +#pragma once + +// Auto-generated symbol table from Genesis ELF +// Generated: 2026-01-20T08:21:22.652378 +// Source: symbol_example_symbols.txt + +#include + +namespace Sym { + + constexpr uint32_t done_flag = 0xFF0000; + constexpr uint32_t init_complete = 0xFF0002; + constexpr uint32_t frame_count = 0xFF0004; + constexpr uint32_t game_over = 0xFF0006; + constexpr uint32_t game_state = 0xFF0007; + constexpr uint32_t player_score = 0xFF0008; + constexpr uint32_t enemy_active = 0xFF000C; + constexpr uint32_t enemy_y = 0xFF000E; + constexpr uint32_t enemy_x = 0xFF0010; + constexpr uint32_t level = 0xFF0012; + constexpr uint32_t player_y = 0xFF0014; + constexpr uint32_t player_x = 0xFF0016; + constexpr uint32_t player_lives = 0xFF0018; + +} // namespace Sym + diff --git a/tests/symbol_example_test.cpp b/tests/symbol_example_test.cpp new file mode 100644 index 0000000..fb4cf33 --- /dev/null +++ b/tests/symbol_example_test.cpp @@ -0,0 +1,344 @@ +/** + * gxtest - Symbol Example Test + * + * Demonstrates symbol-based testing where assertions use variable names + * extracted from the ROM's ELF file instead of hardcoded addresses. + * + * This approach provides: + * 1. Compile-time safety: Renamed variables cause build failures, not runtime crashes + * 2. IDE autocomplete: Symbol names are available for code completion + * 3. Self-documenting tests: ReadWord(Sym::player_score) is clearer than ReadWord(0xFF0008) + * + * The symbol header is generated at build time by tools/elf2sym.py from + * the ROM's ELF symbol table. + */ + +#include +#include "symbol_example_rom.h" // Embedded ROM binary +#include "symbol_example_symbols.h" // Generated symbol addresses (Sym::player_score, etc.) + +namespace { + +using namespace GX::TestRoms; + +/** + * Test fixture that loads the symbol example ROM + */ +class SymbolExampleTest : public GX::Test { +protected: + void SetUp() override { + ASSERT_TRUE(emu.LoadRom(SYMBOL_EXAMPLE_ROM, SYMBOL_EXAMPLE_ROM_SIZE)) + << "Failed to load symbol example ROM"; + } + + /** + * Helper: Wait for game initialization to complete + * Returns true if init completed, false if timeout + */ + bool WaitForInit(int max_frames = 60) { + int result = emu.RunUntil([this]() { + return ReadWord(Sym::init_complete) == INIT_SENTINEL; + }, max_frames); + return result >= 0; + } + + /** + * Helper: Wait for game to end + * Returns true if game ended, false if timeout + */ + bool WaitForGameOver(int max_frames = 1000) { + int result = emu.RunUntil([this]() { + return ReadWord(Sym::done_flag) == DONE_SENTINEL; + }, max_frames); + return result >= 0; + } +}; + +// ============================================================================= +// Basic Initialization Tests +// ============================================================================= + +/** + * Verify the ROM loads and initializes correctly + */ +TEST_F(SymbolExampleTest, RomLoadsAndInitializes) { + ASSERT_TRUE(WaitForInit()) << "Game failed to initialize"; + + // Check init sentinel was set + EXPECT_EQ(ReadWord(Sym::init_complete), INIT_SENTINEL); + + // Verify initial game state using SYMBOLS, not magic addresses + EXPECT_EQ(ReadByte(Sym::game_state), STATE_PLAYING); + EXPECT_EQ(ReadByte(Sym::game_over), 0); +} + +/** + * Test initial player state values + * Note: We reset and check immediately since the game loop runs during WaitForInit + */ +TEST_F(SymbolExampleTest, InitialPlayerState) { + ASSERT_TRUE(WaitForInit()); + + // Player position should remain at initial values + EXPECT_EQ(ReadWord(Sym::player_x), 160); // Center of 320-wide screen + EXPECT_EQ(ReadWord(Sym::player_y), 200); + + // Score increases every frame, so just verify it's a reasonable value + // and is incrementing in multiples of 10 + uint16_t score = ReadWord(Sym::player_score); + EXPECT_EQ(score % 10, 0) << "Score should be a multiple of 10"; + + // Level should be at least 1 + EXPECT_GE(ReadWord(Sym::level), 1); +} + +// ============================================================================= +// Score and Progression Tests +// ============================================================================= + +/** + * Test that score increments over time + */ +TEST_F(SymbolExampleTest, ScoreIncrementsCorrectly) { + ASSERT_TRUE(WaitForInit()); + + // Run for a few frames + emu.RunFrames(10); + + // Score should have increased (10 points per frame) + uint16_t score = ReadWord(Sym::player_score); + EXPECT_GT(score, 0) << "Score should increase over time"; + EXPECT_EQ(score % 10, 0) << "Score should increment in multiples of 10"; +} + +/** + * Test level progression based on score + * Note: Since the game runs many iterations per emulator frame (no vsync), + * we verify that level advancement happens by checking the relationship + * between score and level rather than exact values. + */ +TEST_F(SymbolExampleTest, LevelProgressionBasedOnScore) { + ASSERT_TRUE(WaitForInit()); + + // Read current state + uint16_t score = ReadWord(Sym::player_score); + uint16_t level = ReadWord(Sym::level); + + // Level should correspond to score (level = score/1000 + 1, max 5) + uint16_t expected_level = (score / 1000) + 1; + if (expected_level > 5) expected_level = 5; + + EXPECT_EQ(level, expected_level) + << "Level should match score-based calculation (score=" << score << ")"; + + // Verify level stays bounded + EXPECT_GE(level, 1); + EXPECT_LE(level, 5); +} + +// ============================================================================= +// Memory Injection Tests (demonstrates test harness power) +// ============================================================================= + +/** + * Test injecting player position + */ +TEST_F(SymbolExampleTest, InjectPlayerPosition) { + ASSERT_TRUE(WaitForInit()); + + // Move player to corner + WriteWord(Sym::player_x, 10); + WriteWord(Sym::player_y, 10); + + // Verify the write took effect + EXPECT_EQ(ReadWord(Sym::player_x), 10); + EXPECT_EQ(ReadWord(Sym::player_y), 10); +} + +/** + * Test injecting score directly + */ +TEST_F(SymbolExampleTest, InjectScore) { + ASSERT_TRUE(WaitForInit()); + + // Set score directly + WriteWord(Sym::player_score, 4242); + + EXPECT_EQ(ReadWord(Sym::player_score), 4242); +} + +// ============================================================================= +// Game State Transition Tests +// ============================================================================= + +/** + * Test that game ends when player loses all lives + */ +TEST_F(SymbolExampleTest, GameOverWhenNoLives) { + ASSERT_TRUE(WaitForInit()); + + // Set lives to 0 directly + WriteByte(Sym::player_lives, 0); + + // Trigger a collision by moving player to enemy position + uint16_t enemy_x = ReadWord(Sym::enemy_x); + uint16_t enemy_y = ReadWord(Sym::enemy_y); + WriteWord(Sym::player_x, enemy_x); + WriteWord(Sym::player_y, enemy_y); + + // Ensure enemy is active + WriteByte(Sym::enemy_active, 1); + + // Run frame to process collision + emu.RunFrames(1); + + // Game should end since lives were already 0 + EXPECT_EQ(ReadByte(Sym::game_over), 1); + EXPECT_EQ(ReadByte(Sym::game_state), STATE_GAME_OVER); +} + +/** + * Test win condition: high score at max level + */ +TEST_F(SymbolExampleTest, WinConditionHighScoreMaxLevel) { + ASSERT_TRUE(WaitForInit()); + + // Set up win condition: max level with high score + WriteWord(Sym::level, 5); // MAX_LEVEL + WriteWord(Sym::player_score, 4990); // Just below win threshold + + // Run one frame to add 10 points and trigger win check + emu.RunFrames(1); + + // Game should end with win + EXPECT_EQ(ReadWord(Sym::player_score), 5000); + EXPECT_EQ(ReadByte(Sym::game_over), 1); + EXPECT_EQ(ReadWord(Sym::done_flag), DONE_SENTINEL); +} + +// ============================================================================= +// Frame Count and Timing Tests +// ============================================================================= + +/** + * Test that frame counter increments over time + * Note: The ROM runs in a tight loop without vsync, so frame_count + * increments many times per emulator frame. We just verify it increases. + */ +TEST_F(SymbolExampleTest, FrameCounterIncrements) { + // Don't wait for init - just start fresh and check frame_count increases + emu.Reset(); + + // Run one emulator frame + emu.RunFrames(1); + + // The game loop runs many iterations per emulator frame + // Just verify frame_count has started incrementing + uint16_t frame_count = ReadWord(Sym::frame_count); + EXPECT_GT(frame_count, 0) << "Frame counter should have incremented"; + + // Run more and verify it continues increasing + uint16_t prev = frame_count; + emu.RunFrames(1); + frame_count = ReadWord(Sym::frame_count); + + // Either game is still running (frame_count increased) or game ended + // (frame_count stopped at terminal value) + EXPECT_GE(frame_count, prev) << "Frame counter should not decrease"; +} + +// ============================================================================= +// Enemy State Tests +// ============================================================================= + +/** + * Test enemy position updates based on frame count + */ +TEST_F(SymbolExampleTest, EnemyPositionUpdates) { + ASSERT_TRUE(WaitForInit()); + + uint16_t initial_x = ReadWord(Sym::enemy_x); + uint16_t initial_y = ReadWord(Sym::enemy_y); + + // Run some frames + emu.RunFrames(10); + + uint16_t new_x = ReadWord(Sym::enemy_x); + uint16_t new_y = ReadWord(Sym::enemy_y); + + // Enemy should have moved (pattern: x = 50 + frame%200, y = 50 + (frame/2)%150) + EXPECT_NE(new_x, initial_x) << "Enemy X should change"; +} + +/** + * Test enemy deactivation on collision + */ +TEST_F(SymbolExampleTest, EnemyDeactivatesOnCollision) { + ASSERT_TRUE(WaitForInit()); + + // Ensure player has lives + ASSERT_GT(ReadByte(Sym::player_lives), 0); + + // Force collision + WriteByte(Sym::enemy_active, 1); + WriteWord(Sym::player_x, ReadWord(Sym::enemy_x)); + WriteWord(Sym::player_y, ReadWord(Sym::enemy_y)); + + // Run frame to process collision + emu.RunFrames(1); + + // Enemy should be deactivated (invincibility period) + EXPECT_EQ(ReadByte(Sym::enemy_active), 0) + << "Enemy should deactivate after collision (invincibility)"; +} + +// ============================================================================= +// Full Game Run Test +// ============================================================================= + +/** + * Test running the game to natural completion + */ +TEST_F(SymbolExampleTest, GameRunsToCompletion) { + ASSERT_TRUE(WaitForInit()); + + // Game should complete within 1000 frames (its internal limit) + ASSERT_TRUE(WaitForGameOver(1100)) + << "Game should complete within frame limit"; + + // Verify game ended + EXPECT_EQ(ReadWord(Sym::done_flag), DONE_SENTINEL); + EXPECT_EQ(ReadByte(Sym::game_over), 1); + + // Log final state + std::cout << "Final score: " << ReadWord(Sym::player_score) << std::endl; + std::cout << "Final level: " << ReadWord(Sym::level) << std::endl; + std::cout << "Lives remaining: " << (int)ReadByte(Sym::player_lives) << std::endl; + std::cout << "Frames elapsed: " << ReadWord(Sym::frame_count) << std::endl; +} + +// ============================================================================= +// Comparison: Symbol vs Address (demonstration only) +// ============================================================================= + +/** + * This test demonstrates the readability difference between symbol-based + * and address-based assertions. Both approaches work, but symbols are clearer. + */ +TEST_F(SymbolExampleTest, SymbolsVsAddressesComparison) { + ASSERT_TRUE(WaitForInit()); + + // Symbol-based (recommended): Clear what we're testing + uint16_t score_by_symbol = ReadWord(Sym::player_score); + uint8_t lives_by_symbol = ReadByte(Sym::player_lives); + + // Address-based (legacy): Requires checking documentation + uint16_t score_by_address = ReadWord(0xFF0008); // What's at this address? + uint8_t lives_by_address = ReadByte(0xFF0018); // Magic number, unclear + + // Both give same result, but symbol version is self-documenting + EXPECT_EQ(score_by_symbol, score_by_address); + EXPECT_EQ(lives_by_symbol, lives_by_address); +} + +} // namespace diff --git a/tools/elf2sym.py b/tools/elf2sym.py new file mode 100644 index 0000000..1c6fd8a --- /dev/null +++ b/tools/elf2sym.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +""" +elf2sym.py - Extract symbols from Genesis ELF files and generate C++ headers + +Reads nm output and generates a C++ header with symbol addresses for use +in gxtest fixtures. Only exports symbols in Genesis work RAM (0xFF0000-0xFFFFFF). + +Usage: + nm -n rom.elf | python3 elf2sym.py > symbols.h + # or + python3 elf2sym.py rom_symbols.txt > symbols.h +""" + +import sys +import re +import argparse +from datetime import datetime + + +def parse_nm_output(lines): + """Parse nm output and extract symbols with addresses.""" + # nm output format: ADDRESS TYPE NAME + # Example: 00ff0000 D game_score + # Types: D=data, B=bss, T=text, etc. + regex = re.compile(r'^([0-9a-fA-F]+)\s+([a-zA-Z])\s+(\S+)') + + symbols = [] + for line in lines: + line = line.strip() + if not line: + continue + match = regex.match(line) + if match: + addr_hex, sym_type, name = match.groups() + addr = int(addr_hex, 16) + symbols.append({ + 'address': addr, + 'type': sym_type, + 'name': name + }) + return symbols + + +def filter_ram_symbols(symbols, ram_start=0xFF0000, ram_end=0xFFFFFF): + """Filter to only include symbols in Genesis work RAM range.""" + return [s for s in symbols if ram_start <= s['address'] <= ram_end] + + +# Linker-generated symbols to exclude from output +LINKER_SYMBOLS = { + '__bss_start', '__bss_end', + '__data_start', '__data_end', '__data_load', + '__text_start', '__text_end', + '__stack', '__heap_start', '__heap_end', + '_etext', '_edata', '_end', +} + + +def is_linker_symbol(name): + """Check if symbol is a linker-generated marker.""" + return name in LINKER_SYMBOLS or name.startswith('__') + + +def sanitize_name(name): + """Sanitize symbol name for C++ identifier use.""" + # Remove single leading underscore (common C compiler convention) + # but preserve multiple underscores to avoid collisions + # e.g., '_foo' -> 'foo', '__foo' -> '_foo' + if name.startswith('_') and not name.startswith('__'): + name = name[1:] + elif name.startswith('__'): + name = name[1:] # '__foo' -> '_foo' + # Replace any non-identifier characters + name = re.sub(r'[^a-zA-Z0-9_]', '_', name) + # Ensure it doesn't start with a digit + if name and name[0].isdigit(): + name = '_' + name + return name + + +def generate_header(symbols, namespace='Sym', source_file=None): + """Generate C++ header content from symbol list.""" + lines = [] + + # Header guard and includes + lines.append('#pragma once') + lines.append('') + lines.append('// Auto-generated symbol table from Genesis ELF') + lines.append(f'// Generated: {datetime.now().isoformat()}') + if source_file: + lines.append(f'// Source: {source_file}') + lines.append('') + lines.append('#include ') + lines.append('') + lines.append(f'namespace {namespace} {{') + lines.append('') + + # Sort by address for readability + symbols_sorted = sorted(symbols, key=lambda s: s['address']) + + # Track seen names to avoid duplicates + seen_names = set() + + for sym in symbols_sorted: + # Skip linker-generated symbols + if is_linker_symbol(sym['name']): + continue + clean_name = sanitize_name(sym['name']) + if not clean_name: + continue + if clean_name in seen_names: + print(f'Warning: Skipping duplicate symbol {clean_name} ' + f'(from {sym["name"]})', file=sys.stderr) + continue + seen_names.add(clean_name) + + addr = sym['address'] + + # Add comment with original name if different + comment = '' + if clean_name != sym['name']: + comment = f' // original: {sym["name"]}' + + lines.append(f' constexpr uint32_t {clean_name} = 0x{addr:06X};{comment}') + + lines.append('') + lines.append(f'}} // namespace {namespace}') + lines.append('') + + return '\n'.join(lines) + + +def main(): + parser = argparse.ArgumentParser( + description='Generate C++ symbol header from nm output', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Examples: + nm -n rom.elf | python3 elf2sym.py + python3 elf2sym.py symbols.txt + python3 elf2sym.py symbols.txt --namespace GameSymbols + ''' + ) + parser.add_argument('input', nargs='?', default='-', + help='Input file (nm output), or - for stdin') + parser.add_argument('--namespace', '-n', default='Sym', + help='C++ namespace name (default: Sym)') + parser.add_argument('--ram-start', type=lambda x: int(x, 0), default=0xFF0000, + help='RAM region start address (default: 0xFF0000)') + parser.add_argument('--ram-end', type=lambda x: int(x, 0), default=0xFFFFFF, + help='RAM region end address (default: 0xFFFFFF)') + + args = parser.parse_args() + + # Read input + if args.input == '-': + lines = sys.stdin.readlines() + source_file = 'stdin' + else: + try: + with open(args.input, 'r') as f: + lines = f.readlines() + source_file = args.input + except FileNotFoundError: + print(f'Error: File not found: {args.input}', file=sys.stderr) + sys.exit(1) + except IOError as e: + print(f'Error reading file: {e}', file=sys.stderr) + sys.exit(1) + + # Parse and filter symbols + symbols = parse_nm_output(lines) + ram_symbols = filter_ram_symbols(symbols, args.ram_start, args.ram_end) + + if not ram_symbols: + print('Warning: No symbols found in RAM range ' + f'0x{args.ram_start:06X}-0x{args.ram_end:06X}', file=sys.stderr) + + # Generate and output header + header = generate_header(ram_symbols, args.namespace, source_file) + print(header) + + +if __name__ == '__main__': + main()