Codebase Deep Dive: SameBoy

SameBoy is a cycle-accurate Game Boy (DMG) and Game Boy Color (CGB) emulator written in portable C. This document explains how the core emulation engine, frontends, and tooling fit together — with emphasis on dataflow, timing synchronization, and execution paths. Analysis is based on the repository at commit 208ba4afabffab9edde416f2dbb8ae459e34adb8.

1. Executive mental model

Think of SameBoy as a single monolithic state machine (GB_gameboy_t) advanced by bus-cycle-accurate CPU emulation, with peripheral hardware (PPU, APU, timers, DMA, MBC) synchronized through a central GB_advance_cycles() hub. Frontends are thin hosts that call GB_run() in a loop and receive pixels, audio samples, and events through C callbacks.

┌─────────────────────────────────────────────────────────────┐ │ Frontend (SDL / Cocoa / iOS / libretro) │ │ - main loop calls GB_run() or GB_run_frame() │ │ - registers vblank, audio, input, boot ROM callbacks │ └──────────────────────────┬──────────────────────────────────┘ │ callbacks ↑ GB_set_* ↓ config ┌──────────────────────────▼──────────────────────────────────┐ │ Core (Core/*.c) — portable C, no platform deps │ │ │ │ GB_run() → GB_cpu_run() → cycle_read/write │ │ ↓ │ │ GB_advance_cycles() ← timing hub │ │ ├─ GB_joypad_run │ │ ├─ GB_apu_run │ │ ├─ GB_display_run (PPU) │ │ ├─ GB_dma_run │ │ ├─ ir_run, rtc_run │ │ │ │ GB_gameboy_t = all saved hardware state │ └─────────────────────────────────────────────────────────────┘

Key insight: GB_run() does not emulate a full frame. It runs one CPU quantum (one instruction, interrupt service, halt cycle, or STOP-mode tick). Frontends loop until vblank_just_occured is set, or libretro uses GB_run_frame() which loops internally. Video presentation happens in the vblank_callback, not inside GB_run().

External references: Pan Docs (hardware spec), DeepWiki SameBoy index, BESS save-state format.

2. Repository map

What kind of project this is

SameBoy is a native desktop/mobile emulator — not a web app, not a server. It ships multiple GUI frontends around one shared emulation core, plus a headless tester, libretro core, and open-source boot ROMs.

C Objective-C Makefile rgbds SDL2 Metal/OpenGL

Top-level directories
PathRoleCentral vs peripheral
Core/Emulation engine: CPU, memory, PPU (display.c), APU, MBC, timing, debugger, save states, cheats, rewind, SGB HLECentral
SDL/Cross-platform frontend (Linux, Windows, BSD). Entry: main.cPeripheral (host)
Cocoa/macOS native app with debugger UI, link cable, camera. Entry: main.mGBApp.mPeripheral
iOS/Touch UI, Metal rendering, ROM library. Entry: main.mPeripheral
libretro/RetroArch core. Entry: retro_run() in libretro.cPeripheral
AppleCommon/Shared Metal/GL view, audio client for Cocoa+iOSGlue
BootROMs/SameBoot — rgbds assembly sources for DMG/CGB/SGB boot ROMsSupporting assets
Tester/Headless ROM runner for CI regression (screenshot hashing)Test infra
Shaders/GLSL scaling filters for SDLPeripheral
JoyKit/Game controller abstraction (macOS)Peripheral
Misc/, HexFiend/, QuickLook/, Windows/, FreeDesktop/Utilities, hex editor plugin, Quick Look, Windows helpers, .desktop filesPeripheral
.github/workflows/CI: build all targets + sanity ROM testsInfra
Entry points and configuration
FileType
SDL/main.c:main()SDL CLI/GUI entry
Cocoa/main.mmacOS NSApplicationMain
iOS/main.miOS UIApplicationMain
libretro/libretro.c:retro_init/load_game/runLibretro API
Tester/main.c:main()Headless test runner
MakefilePrimary build system
version.mkVERSION := 1.0.3
sameboy.pc.inpkg-config template for libsameboy

3. Main runtime architecture

Core abstractions
  • GB_gameboy_t — opaque (public) or fully expanded (internal) struct holding all hardware state, split into GB_SECTION blocks for save states.
  • GB_model_t — DMG, MGB, SGB, SGB2, CGB, CGB0, AGB, etc. (Core/model.h).
  • Callback function pointers in the unsaved section — bridge to frontends without platform deps in Core.
  • GB_UNIT state machines in timing.h — batchable per-component timing (display, div, APU channels).

gb.h — GB_gameboy_t sections

Startup lifecycle
  1. GB_init(gb, model) — zero state, allocate WRAM/VRAM by model, set defaults, call GB_reset(), load default border tiles.
  2. Frontend registers callbacks (GB_set_vblank_callback, GB_apu_set_sample_callback, etc.).
  3. GB_load_rom() — mmap/copy ROM, parse header via GB_configure_cart(), set MBC type.
  4. GB_load_battery() — restore MBC RAM + RTC from .sav.
  5. Boot ROM load via boot_rom_load_callback (frontend provides file or embedded ROM).
  6. Main loop: GB_run() until exit.

GB_init · SDL run() setup and loop

4. Key execution flows

Five end-to-end paths with dedicated walkthrough pages:

5. Core modules

Core/ — emulation engine
ModuleResponsibilityKey APIDepends on
gb.cOrchestration, ROM/battery I/O, borders, GB_runGB_init, GB_run, GB_load_romAll subsystems
sm83_cpu.cSM83 CPU, opcode table, bus conflictsGB_cpu_run (internal)memory, timing
memory.cAddress decode, OAM bug, DMAGB_read_memory, GB_write_memorymbc, display, apu
mbc.cCartridge banking MBC1-7, HuC, TPP1, cameraGB_configure_cart, GB_reset_mbc
display.cPPU — LCD timing, FIFO rendering, palettesGB_display_run, GB_display_vblanktiming, memory
apu.c4-channel audio, band-limited synthesis, 96kHz outputGB_apu_run, GB_apu_set_sample_callbacktiming
timing.cGB_advance_cycles hub, DIV/TIMA, serial, turbo syncGB_advance_cyclesjoypad, apu, display, dma
joypad.cKey matrix, debouncing, faux analogGB_set_key_state, GB_joypad_runtiming
save_state.cNative + BESS save/loadGB_save_state, GB_load_stategb.h layout
debugger.cCLI debugger, breakpoints, symbolsGB_debugger_run, GB_debugger_breaksm83_disassembler
sgb.cSuper Game Boy HLE (border, palettes, intro)Internaldisplay, apu
rewind.cCompressed rewind ring bufferGB_rewind_push/popsave_state (no BESS)
Frontends — glue, not business logic

SDL (SDL/main.c, gui.c, audio.c), Cocoa (Cocoa/Document.m, GBView.m), iOS (GBViewController.m), libretro (libretro.c). Each translates OS events → GB_set_key_state and callbacks → OS audio/video APIs. No emulation accuracy logic lives here.

6. Data model and persistence

In-memory state (GB_gameboy_t sections)
SectionContentsSaved?
headerMagic, version (15)Yes
core_stateRegisters AF–PC, IME, flags, modelYes
dmaOAM DMA, HDMA stateYes
mbcBank registers, rumble, camera regsYes
hramHRAM + I/O registers FF80–FFFFYes
timingDIV, TIMA, serial, joypad bounceYes
apuChannel state (not output buffer)Yes
rtcMBC3/TPP1 real-time clockYes
videoVRAM pointer is external; OAM, palettes, PPU state savedPartial + blob
unsavedROM/RAM pointers, callbacks, debugger, rewindNo

External heap blobs: rom, ram, vram, mbc_ram — serialized as trailing blocks in save files.

File formats
  • ROM — raw .gb / .gbc; header parsed at 0x0100–0x014F
  • Battery.sav beside ROM (MBC RAM + RTC); multiple legacy formats supported in gb.c
  • Save state — section-ordered binary + optional BESS footer for cross-emulator compatibility
  • Cheats.cht text format
  • SDL prefs — binary prefs.bin (SDL/configuration.c)
  • Cocoa/iOSNSUserDefaults keys like GBEmulatedModel, GBRight
  • Libretro — core options via RETRO_ENVIRONMENT_GET_VARIABLE

7. External interfaces

What the system talks to
InterfaceUsed byPurpose
FilesystemAllROM, .sav, save states, boot ROMs, cheats, symbols
SDL2 / Metal / OpenGLSDL, Apple frontendsWindow, input, GPU rendering, shaders
Core Audio / SDL Audio / AVAudioEngineCocoa, iOS, SDL96 kHz PCM output
Libretro APIlibretro.cRetroArch integration
Host clock (time())RTC, WorkBoyReal-time clock advancement
Serial callbacksCocoa link cable, printer, WorkBoyBit-banged serial between two GB instances or accessories
Camera callbacksCocoa, iOSGame Boy Camera sensor pixels

No network, database, or cloud services in the core product.

8. Error handling and edge cases

Patterns
  • Return codes: GB_load_rom, GB_save_state, GB_load_battery return 0 on success, negative on failure.
  • Assertions: GB_ASSERT_NOT_RUNNING prevents re-entrant GB_run; debug-only thread checks on callback registration.
  • Logging: GB_log(gb, fmt, ...) → optional log_callback; SDL can capture to message boxes.
  • Tester crash detection: stack overflow, halt-with-IE=0 deadlock, infinite 0xFF at 0x38.
  • Graceful degradation: unreadable .sav path logs warning; RTC reset if timestamp implausible.

Failure modes: corrupt ROM header → GB_configure_cart may default to ROM-only; save state version mismatch → load rejected; audio backpressure in SDL drops samples when queue > freq/8.

9. Concurrency and lifecycle

Threading model
  • Core is single-threaded. running_thread_id tracks the emulation thread; callbacks must not call GB_run re-entrantly.
  • Cocoa: emulation on NSThread (Document.m -run); UI on main thread; atomic blocks for debugger.
  • iOS: serial GCD queue _runQueue for GB_run.
  • SDL: single-threaded main loop; input polled via update_input_hint_callback from timing code.
  • Libretro: host-thread driven; no internal threads.
  • Tester: optional --jobs N forks processes (Unix only).

Turbo mode skips PPU rendering and may skip vblank callbacks unless enable_skipped_frame_vblank_callbacks is set. Rewind pushes state on each vblank (compressed).

10. Tests

Framework and coverage

Framework: Custom headless runner (Tester/main.c), not unit tests. CI compares SHA1 of output BMP screenshots.

CI ROMs (.github/actions/sanity_tests.sh): cgb_sound.gb, cgb-acid2.gbc, dmg-acid2.gb, dmg_sound-2.gb, oam_bug-2.gb.

Run locally:

make tester CONF=release
./build/bin/tester/sameboy_tester --cgb --length 10 path/to/rom.gb

Covered: PPU accuracy (Acid2), APU (blargg sound ROMs), OAM bug, basic crash detection.

Uncovered: Individual MBC edge cases, debugger commands, frontend UI, link cable timing, most cheat paths — no automated tests found.

11. Build, run, and deploy

Commands
# Dependencies: clang, make, rgbds, cppp, libsdl2 (SDL port)
git clone https://github.com/LIJI32/SameBoy.git
cd SameBoy
make                    # sdl on Linux, cocoa on macOS
make all CONF=release   # sdl + tester + libretro + lib (+ cocoa on macOS)
make -C libretro        # libretro core only
./build/bin/SDL/sameboy game.gb

# CI equivalent
make -j all CONF=release
.github/actions/sanity_tests.sh

Deploy: GitHub Actions builds artifacts on push to master; libretro branch auto-synced via update_libretro.sh; macOS releases as .app, iOS as IPA/deb; FreeDesktop make install.

Config: CONF=debug|release|native_release|fat_release, BOOTROMS_DIR=, DATA_DIR=, DISABLE_DEBUGGER=1 (lib only).

12. Most important files to understand first

Ranked list
  1. Core/gb.h — entire public API and state layout
  2. Core/gb.cGB_init, GB_run, ROM/battery lifecycle
  3. Core/timing.cGB_advance_cycles, the synchronization hub
  4. Core/sm83_cpu.c — CPU execution and bus-cycle timing
  5. Core/memory.c — memory map, side effects, OAM bug
  6. Core/display.c — PPU (largest accuracy-critical file)
  7. Core/apu.c — audio generation
  8. Core/mbc.c — cartridge hardware
  9. Core/save_state.c — persistence format
  10. SDL/main.c — reference frontend integration
  11. libretro/libretro.c — alternative frame-driven host model
  12. Makefile — how everything is wired together

13. Things that are confusing or risky

Non-obvious insights
  • 8 MHz cycle units: Internal cycle counters use 8 MHz units; DMG divides by 2 in GB_advance_cycles when not in double-speed CGB mode.
  • GB_run ≠ one frame: Easy to misread; use GB_run_frame or loop on vblank_just_occured.
  • Bus conflicts: sm83_cpu.c has per-register conflict maps for DMG/CGB/SGB — changing I/O timing can break Acid2-style tests.
  • PPU state machine: 20+ states in display.c; STAT mode bits intentionally delayed 4 T-cycles (see TODO in source).
  • Opaque struct: Public builds hide GB_gameboy_t layout; define GB_INTERNAL when hacking Core.
  • Libretro strips features: Built with GB_DISABLE_REWIND, GB_DISABLE_DEBUGGER, etc.
  • Band-limited init: apu.c uses __attribute__((constructor)) — runs at library load.
  • SGB intro hack: Special path in GB_run advances PPU without CPU during intro animation.
  • Save state version: STRUCT_VERSION 15 — bumping requires migration logic in save_state.c.

14. Suggested learning path

Ordered reading list
  1. Read README.md and skim DeepWiki overview
  2. Study Core/gb.hGB_SECTION layout and public functions
  3. Trace GB_initGB_reset in gb.c
  4. Read GB_run and follow into GB_cpu_run (sm83_cpu.c)
  5. Understand GB_advance_cycles in timing.c
  6. Pick one bus operation: GB_read_memory for I/O register side effects
  7. Skim display.c GB_display_run state machine header
  8. Read SDL run() in SDL/main.c for callback wiring
  9. Run make tester on dmg-acid2.gb and inspect output BMP
  10. Explore debugger.c if modifying CPU or memory behavior

15. Open questions

Cannot be determined from code alone
  • Exact hardware behavior for several TODO-marked timing cases (ISR IF read timing, STOP mode DMA, MGB OAM bug variants).
  • Whether Windows tester target is intentionally broken (Makefile links incomplete object set).
  • Production release cadence and signing process for macOS/iOS builds.
  • Which real-hardware tests were used to validate each bitwise_glitch OAM corruption variant.
  • Performance characteristics on low-end ARM without profiling data in repo.

Generated from analysis of LIJI32/SameBoy at commit 208ba4a. See component pages for step-by-step execution traces.