Skip to content
QShen3Public

About

An emulator that support Nintendo Game Boy and Game Boy Color. Under development.

Resources

Stars

7 stars

Watchers

1 watching

Forks

Repository files navigation

QNEmu

CI

QNEmu is an emulator that support Nintendo Game Boy and Game Boy Color. It is still under development and plans to support other consoles in the future. The main purpose of this project is to study computer architecture and software design pattern.

QNEmu running game

Build

Requirements:

  • A C++20 compiler (GCC >= 9, Clang >= 12, or MSVC 2019+)
  • CMake >= 3.16 (>= 3.20 if you want to use the bundled presets)
  • Qt >= 6.2 with Core, Gui, Widgets and Multimedia modules
  • Google Test >= 1.10 (only when building the tests)

Build command:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build

The executable will be available in build/qnemu/qnemu. To run the tests:

ctest --test-dir build --output-on-failure

CMake presets are also available:

cmake --preset debug
cmake --build --preset debug
ctest --preset debug
Preset Description
debug Debug build in build/
release Release build in build-release/
coverage Debug build with coverage instrumentation
analyze Debug build with clang-tidy and cppcheck

Useful options (pass them with -D when configuring):

Option Default Description
BUILD_TESTING ON Build the unit tests (requires Google Test)
QNEMU_WARNINGS_AS_ERRORS ON Treat compiler warnings as errors
QNEMU_ENABLE_CLANG_TIDY OFF Run clang-tidy while compiling
QNEMU_ENABLE_CPPCHECK OFF Run cppcheck while compiling
QNEMU_ENABLE_COVERAGE OFF Instrument the binary and add a coverage target

The coverage report is generated by:

cmake -S . -B build -DQNEMU_ENABLE_COVERAGE=ON
cmake --build build --target coverage

This requires gcovr on GCC/Clang builds, or OpenCppCoverage on MSVC builds.

On macOS with Homebrew, the dependencies can be installed with:

brew install cmake ninja qt googletest

On Debian/Ubuntu, install the Qt and Google Test development packages:

sudo apt install cmake ninja-build qt6-base-dev qt6-multimedia-dev libgtest-dev libgmock-dev

CPU core

The SM83 core lives in qnemu/gb/cpu and is deliberately independent of Qt, the MMU and the GUI thread:

  • GbCpu executes one instruction per step() and talks to the rest of the machine only through CpuBus (read / write / tick).
  • Every memory access is one machine cycle, so instruction timing is expressed directly in the instruction implementation instead of in a separate table.
  • Registers is a portable plain struct; flags are the usual Z N H C byte, with no anonymous-union or endianness assumptions.
  • Interrupts are handled at instruction boundaries: IME, the EI one-instruction delay, IF/IE priority, HALT, the HALT bug and STOP are all part of the core.
  • GbCpuBus adapts GbMmu's 1 T-cycle step() to the CPU's 4 T-cycle bus accesses. The GUI thread lives in Gb, not in the CPU.

Unit tests for the core are in tests/gb/cpu/GbCpuTest.cpp and use a flat TestBus. They cover reset state, flags and arithmetic edge cases, signed jumps, the SP-relative instructions, interrupt dispatch, the EI delay, HALT, the HALT bug and STOP.

PPU core

The PPU is split into three Qt-free pieces under qnemu/gb/gpu:

  • GbPpu owns the dot/line clock, LCDC/STAT/LY/LYC state, OAM DMA, HDMA, palette RAM and the frame boundary. Mode 3 length includes fine-scroll, window and sprite fetch penalties; OAM DMA has a two-M-cycle startup delay and restart handling.
  • PpuMemory owns the two VRAM banks, OAM and the CGB palette RAM.
  • PpuRenderer is a stateless scanline renderer: it reads registers and memory, writes one row, and returns whether the window was visible. This is the part to drive from unit tests with synthetic VRAM/OAM.

STAT interrupts use an edge/blocking line rather than firing on every mode change. GbCpuBus also implements CGB KEY0/KEY1 and double-speed switching, including halving the number of peripheral dots per CPU T-cycle.

The PPU never touches Qt or GbCartridgeInterface. External memory needed by OAM DMA/HDMA is supplied through a callback, and finished frames are pushed to DisplayInterface as a plain 160x144 FrameBuffer. RasterDisplay converts that buffer to a QImage on the GUI side, without blocking the CPU thread.

PPU tests live in tests/gb/gpu/ and cover:

  • background/window tile addressing, signed tile indexes and DMG palettes
  • sprite flips, 8x16 tile selection, the 10-object limit, DMG/CGB priority and the CGB object-priority mode (FF6C)
  • OAM DMA startup/restart/byte timing, HDMA block copies, mode transitions, SCX/window/sprite mode-3 penalties, STAT edge blocking, LYC interrupts and VBlank frame delivery
  • CGB KEY0/KEY1 and double-speed bus timing
  • full-frame image comparison against dmg-acid2 and cgb-acid2

Test ROMs

gb_test_runner runs a ROM headlessly with a small test bus (ROM/MBC1, RAM, timer, IF/IE, serial and a line counter for VBlank). It understands both common result protocols:

  • blargg serial output and the blargg screen console (Passed / Failed)
  • the Mooneye LD B, B breakpoint with the Fibonacci register signature

Usage:

./build/tests/rom/gb_test_runner path/to/test.gb
./build/tests/rom/gb_test_runner path/to/test.gb --model dmg --max-cycles 300000000

# attach the real PPU (OAM DMA / LY / STAT / VRAM)
./build/tests/rom/gb_test_runner path/to/test.gb --ppu

# attach the APU (sound register/length/sweep tests)
./build/tests/rom/gb_test_runner path/to/test.gb --apu --model dmg

# render frames and compare against a reference PNG
./build/tests/rom/gb_test_runner path/to/acid2.gb --frames 10 \
    --compare-image path/to/reference.png

The ROMs are included as git submodules:

third_party/gb-test-roms/       # blargg, prebuilt ROMs
third_party/mooneye-test-roms/  # Mooneye, prebuilt ROMs
third_party/dmg-acid2/          # source + reference image
third_party/cgb-acid2/          # source + reference image

Initialize them after cloning:

git submodule update --init --recursive

The acid2 reference images are in the submodules, but the release ROMs are not stored in git. Fetch them once (SHA-256 verified):

python3 tests/rom/fetch_acid2_roms.py

The default destination is build/test-roms/acid2; use -DQNEMU_ACID2_ROM_DIR=... if the ROMs live elsewhere.

CMake registers a curated CPU/timing/PPU subset automatically when the submodules are present, so plain ctest covers them as well:

ctest --preset debug -L rom
ctest --preset debug -L blargg
ctest --preset debug -L mooneye
ctest --preset debug -L ppu
ctest --preset debug -L sound
ctest --preset debug -L acid2

The acid2 tests render 10 frames and require an exact 160x144 image match. Use -DQNEMU_ENABLE_ROM_TESTS=OFF to disable the curated tests, or -DQNEMU_TEST_ROM_ROOT=... if the submodules live elsewhere. For an additional directory tree, -DQNEMU_TEST_ROMS_DIR=... registers every .gb below it.

To run a whole directory tree manually and get a summary:

python3 tests/rom/run_test_roms.py third_party/gb-test-roms \
    --include 'cpu_instrs|instr_timing|mem_timing'

python3 tests/rom/run_test_roms.py third_party/mooneye-test-roms/acceptance \
    --include 'ppu/' --ppu

The CPU-only bus still has a tiny LY/VBlank stub; --ppu attaches the real PPU for OAM DMA, VRAM/OAM blocking, STAT/LYC and rendering.

Passing ROM tests

The curated CTest set is currently 249/249, including 98 ROM tests (labels in parentheses):

Blargg (-L blargg):

  • cpu_instrs/individual/01-special … 11-op a,(hl)
  • cpu_instrs/cpu_instrs
  • instr_timing/instr_timing
  • mem_timing/individual/01-read_timing, 02-write_timing, 03-modify_timing
  • mem_timing/mem_timing
  • mem_timing-2/rom_singles/01-read_timing, 02-write_timing, 03-modify_timing
  • mem_timing-2/mem_timing

Blargg sound (-L sound, requires --apu):

  • DMG (--model dmg): dmg_sound/rom_singles/01-registers, 02-len ctr, 03-trigger, 04-sweep, 05-sweep details, 06-overflow on trigger, 07-len sweep period sync, 08-len ctr during power, 11-regs after power
  • CGB (--model cgb): cgb_sound/rom_singles/01-registers, 02-len ctr, 03-trigger, 04-sweep, 05-sweep details, 06-overflow on trigger, 07-len sweep period sync, 08-len ctr during power, 10-wave trigger while on, 11-regs after power, 12-wave

Mooneye CPU/interrupt/timer (-L mooneye, CPU-only bus):

  • bits/reg_f
  • div_timing
  • ei_sequence, ei_timing
  • halt_ime0_ei, halt_ime0_nointr_timing, halt_ime1_timing, halt_ime1_timing2-GS
  • if_ie_registers
  • instr/daa
  • interrupts/ie_push
  • intr_timing
  • pop_timing
  • rapid_di_ei
  • reti_intr_timing
  • timer/div_write
  • timer/rapid_toggle
  • timer/tim00, timer/tim01, timer/tim10, timer/tim11
  • timer/tim00_div_trigger, timer/tim01_div_trigger, timer/tim10_div_trigger, timer/tim11_div_trigger
  • timer/tima_reload

Mooneye PPU/OAM/bus-timing (-L ppu, requires --ppu):

  • add_sp_e_timing
  • call_timing, call_timing2, call_cc_timing, call_cc_timing2
  • di_timing-GS
  • jp_timing, jp_cc_timing
  • ld_hl_sp_e_timing
  • push_timing
  • ret_timing, ret_cc_timing, reti_timing, rst_timing
  • bits/mem_oam
  • oam_dma/basic, oam_dma/reg_read
  • oam_dma_start, oam_dma_restart, oam_dma_timing
  • ppu/hblank_ly_scx_timing-GS
  • ppu/intr_1_2_timing-GS
  • ppu/intr_2_0_timing
  • ppu/intr_2_mode0_timing
  • ppu/intr_2_mode3_timing
  • ppu/intr_2_oam_ok_timing
  • ppu/stat_irq_blocking
  • ppu/stat_lyc_onoff
  • ppu/vblank_stat_intr-GS

Image tests (-L acid2):

  • dmg-acid2: exact 160x144 match against reference-dmg.png
  • cgb-acid2: exact 160x144 match against reference.png

Known failures at the moment are concentrated in:

  • ppu/intr_2_mode0_timing_sprites (multi-object fetch-penalty edge case)
  • ppu/lcdon_timing-GS, ppu/lcdon_write_timing-GS
  • oam_dma/sources-GS
  • timer/tima_write_reloading, timer/tma_write_reloading (reload-boundary write timing)
  • interrupt_time
  • dmg_sound/rom_singles/09-wave read while on, 10-wave trigger while on, 12-wave write while on and cgb_sound/rom_singles/09-wave read while on (wave-RAM accesses while CH3 is active need cycle-exact CPU/APU bus alignment; the other sound tests pass)
  • boot_* (no boot ROM) and emulator-only MBC2/MBC3/MBC5 suites (those mappers are not implemented)

Play

Just click the executable and load Rom file.

Hardware model

QNEmu starts as CGB by default. The model can be selected from the command line:

./build/qnemu/qnemu
./build/qnemu/qnemu --model cgb   # explicit default
./build/qnemu/qnemu --model dmg
./build/qnemu/qnemu --model sgb

-m is accepted as a shorthand. With cgb, a CGB cartridge runs in CGB mode and a DMG cartridge runs in DMG compatibility mode. dmg and sgb force the DMG-compatible CPU/PPU path. SGB-specific features (border, SGB command protocol, SGB palettes) are not implemented yet, so sgb currently behaves as the DMG hardware baseline.

Audio

The APU implements all four hardware channels:

  • CH1: pulse wave with duty, envelope, length timer and frequency sweep
  • CH2: pulse wave with duty, envelope and length timer
  • CH3: 32-sample 4-bit wave-table channel with 100/50/25% output levels
  • CH4: LFSR noise with 15-bit/7-bit modes, envelope and length timer

A 512 Hz frame sequencer clocks the length timers at 256 Hz, the CH1 sweep at 128 Hz and the volume envelopes at 64 Hz. It is driven by the DIV-APU falling edge, so writing DIV synchronises the APU exactly like the hardware (including the power-on DIV glitch). The mixer applies NR51 panning and NR50 master volume, models the DACs' negative slope, high-pass filters the DC offset like the hardware's output capacitor, and downsamples to 44100 Hz stereo signed 16-bit samples.

Two small quality-of-life filters run on the final output: a 20 ms fade-in when the DACs are enabled (which suppresses the power-on click) and a gentle 2nd-order 10 kHz Butterworth low-pass (which takes the harsh edge off the raw square waves). The sound buffer is flushed whenever a ROM is loaded, so audio from the previous game cannot leak into the next one. Qt Multimedia plays the samples through a small ring buffer; when the buffer is full the emulation thread blocks, which also acts as the emulator's speed limiter.

Unit tests for the channels, frame sequencer, mixer, panning, volume and sample output live in tests/gb/apu/ (-L filtering is not needed; run ./build/tests/qnemu_test --gtest_filter='GbChannel*:GbApu*').

The headless runner can attach the APU with --apu; the curated dmg_sound/cgb_sound register, length, trigger, sweep and power tests run with ctest --preset debug -L sound. Wave-RAM accesses while CH3 is actively playing (tests 09/10/12) still depend on cycle-exact bus alignment and are not part of the curated set.

Input

Host input is separate from rendering. RasterDisplay implements InputInterface and maps Qt keys to GbButton values; GbJoypad only models the FF00/P1 matrix and its edge-triggered interrupt. The joypad is therefore Qt-free and a future gamepad, replay or network input backend can feed it without touching the renderer.

Key binding

Joypad Key
↑ W
↓ S
← A
→ D
A J
B K
Start 3
Select 2

Functions & Features

GameBoy & GameBoy Color

Function Implement Test
CPU ✅ unit + ROM tests
PPU ✅ unit + acid2
Sound ✅ unit + blargg sound
Interrupt ✅ ✅
Joypad ✅ unit
Timer ✅ ✅
no MBC ✅ ✅
MBC1 ✅ ✅
MBC2 ❌ ❌
MBC3 ❌ ❌
MBC5 ❌ ❌
MBC6 ❌ ❌
MBC7 ❌ ❌
HuC1 ❌ ❌
HDMA ✅ unit
Feature Implement Test
model selection ✅ CLI
key remap ❌ ❌
Save & Load ❌ ❌
Filtering ❌ ❌
Vulkan backend ❌ ❌
DirectX backend ❌ ❌
Speed setting ❌ ❌

Thanks

About

An emulator that support Nintendo Game Boy and Game Boy Color. Under development.

Resources

Stars

7 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages