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.
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
The SM83 core lives in qnemu/gb/cpu and is deliberately independent of Qt,
the MMU and the GUI thread:
GbCpuexecutes one instruction perstep()and talks to the rest of the machine only throughCpuBus(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.
Registersis a portable plain struct; flags are the usualZ N H Cbyte, 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.
GbCpuBusadaptsGbMmu's 1 T-cyclestep()to the CPU's 4 T-cycle bus accesses. The GUI thread lives inGb, 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.
The PPU is split into three Qt-free pieces under qnemu/gb/gpu:
GbPpuowns 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.PpuMemoryowns the two VRAM banks, OAM and the CGB palette RAM.PpuRendereris 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
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, Bbreakpoint 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.
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_instrsinstr_timing/instr_timingmem_timing/individual/01-read_timing,02-write_timing,03-modify_timingmem_timing/mem_timingmem_timing-2/rom_singles/01-read_timing,02-write_timing,03-modify_timingmem_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_fdiv_timingei_sequence,ei_timinghalt_ime0_ei,halt_ime0_nointr_timing,halt_ime1_timing,halt_ime1_timing2-GSif_ie_registersinstr/daainterrupts/ie_pushintr_timingpop_timingrapid_di_eireti_intr_timingtimer/div_writetimer/rapid_toggletimer/tim00,timer/tim01,timer/tim10,timer/tim11timer/tim00_div_trigger,timer/tim01_div_trigger,timer/tim10_div_trigger,timer/tim11_div_triggertimer/tima_reload
Mooneye PPU/OAM/bus-timing (-L ppu, requires --ppu):
add_sp_e_timingcall_timing,call_timing2,call_cc_timing,call_cc_timing2di_timing-GSjp_timing,jp_cc_timingld_hl_sp_e_timingpush_timingret_timing,ret_cc_timing,reti_timing,rst_timingbits/mem_oamoam_dma/basic,oam_dma/reg_readoam_dma_start,oam_dma_restart,oam_dma_timingppu/hblank_ly_scx_timing-GSppu/intr_1_2_timing-GSppu/intr_2_0_timingppu/intr_2_mode0_timingppu/intr_2_mode3_timingppu/intr_2_oam_ok_timingppu/stat_irq_blockingppu/stat_lyc_onoffppu/vblank_stat_intr-GS
Image tests (-L acid2):
dmg-acid2: exact 160x144 match againstreference-dmg.pngcgb-acid2: exact 160x144 match againstreference.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-GSoam_dma/sources-GStimer/tima_write_reloading,timer/tma_write_reloading(reload-boundary write timing)interrupt_timedmg_sound/rom_singles/09-wave read while on,10-wave trigger while on,12-wave write while onandcgb_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) andemulator-onlyMBC2/MBC3/MBC5 suites (those mappers are not implemented)
Just click the executable and load Rom file.
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.
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.
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.
| Joypad | Key |
|---|---|
| ↑ | W |
| ↓ | S |
| ← | A |
| → | D |
| A | J |
| B | K |
| Start | 3 |
| Select | 2 |
| 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 | ❌ | ❌ |
