Mooneye GB is a Game Boy research project and emulator written in Rust.
The main goals of this project are accuracy and documentation. Some existing
emulators are very accurate (Gambatte, BGB >= 1.5) but are not documented very
clearly, so they are not that good references for emulator developers. I want
this project to document as clearly as possible why certain behaviour is
emulated in a certain way. This also means writing a lot of test ROMs to figure
out corner cases and precise behaviour on real hardware.
Looking for the mooneye-gb test ROMs? They are now part of Mooneye Test Suite.
Non-goals:
CGB (Game Boy Color) support. It would be nice, but I want to make the normal
Game Boy support extremely robust first.
A debugger
A good user interface. Building native UIs with Rust is a bit painful at the
moment.
Warning:
Project is WIP
Doesn't work properly without a boot ROM
The emulator is lagging behind hardware research. I don't want to spend time
making short-lived and probably incorrect fixes to the emulator if I'm not
sure about the hardware behaviour.
Performance
Always compile in release mode if you care about performance!
On a i7-3770K desktop machine I can usually run ROMs with 2000 - 4000% speed.
Without optimizations the speed drops to 150 - 200%, which is still fine for
development purposes.
Raspberry Pi with X11 desktop works but is too slow because there is no OpenGL
acceleration.
The emulator is runnable on Android, but cross-compiling and packaging is a
huge pain and touch controls would have to be implemented, so I'm not
supporting Android at the moment.
Running the emulator
Requirements:
Rust 1.26
SDL2 development libraries for your platform must be installed
GUI
cargo run --release
Follow the instructions
Command-line
Acquire a Game Boy bootrom, and put it to $HOME/.local/share/mooneye-gb/bootroms/dmg_boot.bin
cargo build --release
cargo run --release -- PATH_TO_GAMEBOY_ROM
On Windows, also download an SDL2 package containing SDL2.dll, and put it to
target/debug and target/release.
Game Boy keys
Game Boy
Key
Dpad
Arrow keys
A
Z
B
X
Start
Return
Select
Backspace
Other keys
Function
Key
Fast forward
Shift
Toggle performance overlay
F2
Test suite
Blargg's tests
Test
mooneye-gb
cpu instrs
π
dmg sound 2
β
instr timing
π
mem timing 2
π
oam bug 2
β
cgb sound 2
Notes:
cpu_instrs fails on MGB/SGB2 hardware and emulators emulating them correctly.
The ROM incorrectly detects the device as CGB, and attempts to perform a CPU
speed change which causes a freeze (STOP instruction with joypad disabled)
dmg_sound-2 test #10 can fail randomly on real hardware and seems to depend
on non-deterministic behaviour.
oam_bug-2 fails on all CGB, AGB, and AGS devices
cgb_sound-2 test #03 fails on CPU CGB, CPU CGB A, and CPU CGB B
Mooneye GB acceptance tests
Test
mooneye-gb
add sp e timing
π
boot div dmg0
β
boot div dmgABCmgb
β
boot div S
β
boot div2 S
β
boot hwio dmg0
β
boot hwio dmgABCmgb
β
boot hwio S
π
boot regs dmg0
π
boot regs dmgABC
π
boot regs mgb
π
boot regs sgb
π
boot regs sgb2
π
call timing
π
call timing2
π
call cc_timing
π
call cc_timing2
π
di timing GS
π
div timing
π
ei sequence
π
ei timing
π
halt ime0 ei
π
halt ime0 nointr_timing
π
halt ime1 timing
π
halt ime1 timing2 GS
π
if ie registers
π
intr timing
π
jp timing
π
jp cc timing
π
ld hl sp e timing
π
oam dma_restart
π
oam dma start
π
oam dma timing
π
pop timing
π
push timing
π
rapid di ei
π
ret timing
π
ret cc timing
π
reti timing
π
reti intr timing
π
rst timing
π
Bits (unusable bits in memory and registers)
Test
mooneye-gb
mem oam
π
reg f
π
unused_hwio GS
π
Instructions
Test
mooneye-gb
daa
π
Interrupt handling
Test
mooneye-gb
ie push
π
OAM DMA
Test
mooneye-gb
basic
π
reg_read
π
sources GS
π
PPU
Test
mooneye-gb
hblank ly scx timing GS
π
intr 1 2 timing GS
π
intr 2 0 timing
π
intr 2 mode0 timing
π
intr 2 mode3 timing
π
intr 2 oam ok timing
π
intr 2 mode0 timing sprites
β
lcdon timing GS
β
lcdon write timing GS
β
stat irq blocking
β
stat lyc onoff
β
vblank stat intr GS
π
Serial
Test
mooneye-gb
boot sclk align dmgABCmgb
β
Timer
Test
mooneye-gb
div write
π
rapid toggle
π
tim00 div trigger
π
tim00
π
tim01 div trigger
π
tim01Β
π
tim10 div trigger
π
tim10
π
tim11 div trigger
π
tim11
π
tima reload
π
tima write reloading
π
tma write reloading
π
Mooneye GB emulator-only tests
MBC1
Test
mooneye-gb
bits bank1
π
bits bank2
π
bits mode
π
bits ramg
π
rom 512kb
π
rom 1Mb
π
rom 2Mb
π
rom 4Mb
π
rom 8Mb
π
rom 16Mb
π
ram 64kb
π
ram 256kb
π
multicart rom 8Mb
π
MBC2
Test
mooneye-gb
bits ramg
π
bits romb
π
bits unused
π
rom 512kb
π
rom 1Mb
π
rom 2Mb
π
ram
π
MBC5
Test
mooneye-gb
rom 512kb
π
rom 1Mb
π
rom 2Mb
π
rom 4Mb
π
rom 8Mb
π
rom 16Mb
π
rom 32Mb
π
rom 64Mb
π
Mooneye GB manual tests
Test
mooneye-gb
sprite priority
π
Mooneye GB misc tests
Test
mooneye-gb
boot div A
boot div cgb0
boot div cgbABCDE
boot hwio C
boot regs A
boot regs cgb
Bits
Test
mooneye-gb
unused hwio C
PPU
Test
mooneye-gb
vblank stat intr C
License and copyright
Mooneye GB is licensed under GPLv3+.
Copyright (C) 2014-2020 Joonas Javanainen [email protected]
first of all thanks for all the hard work. As an GB homebrew developer accurate emulation lies very close to my heart :)
Now since my evening was rather boring, I decided to check if it was possible to do a wasm port of mooneye-gb and low and behold it turned out to be rather easy with all the rust related tooling we have at hands these days: https://gitlab.com/BonsaiDen/mooneye-wasm
As the title says, I'm requesting a test ROM that can give the details for what is returned from reads from VRAM by the CPU during rendering. The reason for this request is that recently there was an attempt to play back a Tool Assisted Speedrun of Wario Land II on console, but there was a de-sync. I tracked the de-sync down to a VRAM access that happens at the very end of rendering on scanline 22.
Up until now, this case had been dealt with by returning 0xFF. However this is not what console showed. Also, the actual value at the address being read is already 0xFF. So, it has to be returning something else. I think it's returning the last value read by the PPU, and when I put this behaviour in my emulator I get the correct result (the same de-sync displayed on console.) But, this is just speculation.
So, it would be very helpful to have a test that evaluates what is being returned for individual cycles during scanline rendering, especially near the end of a scnaline when the PPU stops accessing VRAM.
(Also thanks for your current tests, they are very helpful and easy to understand.)
Using a standard Gameboy BootRom, load Pokemon blue. Hold shift (turbo mode) and select "New game". Continue to hold shift while professor oak is talking, and the emulator panics with this stack trace:
16:25:30 οΏ½[34m[ INFO] οΏ½[mοΏ½Starting Mooneye GB v0.2.0-pre
16:25:30 οΏ½[36m[DEBUG] οΏ½[mοΏ½(1) mooneye_gb::config::bootrom: Scanning /home/alex/.local/share/mooneye-gb/bootroms/dmg_boot.bin for a boot ROM
16:25:30 οΏ½[34m[ INFO] οΏ½[mοΏ½Using DMG (Game Boy) boot ROM from /home/alex/.local/share/mooneye-gb/bootroms/dmg_boot.bin
16:25:30 οΏ½[36m[DEBUG] οΏ½[mοΏ½(1) gilrs::gamepad: Loaded 276 mappings.
16:25:30 οΏ½[34m[ INFO] οΏ½[mοΏ½Guessed window DPI factor: 1
16:25:30 οΏ½[36m[DEBUG] οΏ½[mοΏ½(1) winit::platform::platform::x11::window: Calculated physical dimensions: 640x576
16:25:30 οΏ½[34m[ INFO] οΏ½[mοΏ½Initialized renderer with OpenGL 4.5
thread '<unnamed>' panicked at 'Undefined opcode 228', core/src/cpu/execute.rs:736:5
stack backtrace:
0: backtrace::backtrace::libunwind::trace
at /cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.40/src/backtrace/libunwind.rs:88
1: backtrace::backtrace::trace_unsynchronized
at /cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.40/src/backtrace/mod.rs:66
2: std::sys_common::backtrace::_print_fmt
at src/libstd/sys_common/backtrace.rs:77
3: <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt
at src/libstd/sys_common/backtrace.rs:61
4: core::fmt::write
at src/libcore/fmt/mod.rs:1028
5: std::io::Write::write_fmt
at src/libstd/io/mod.rs:1412
6: std::sys_common::backtrace::_print
at src/libstd/sys_common/backtrace.rs:65
7: std::sys_common::backtrace::print
at src/libstd/sys_common/backtrace.rs:50
8: std::panicking::default_hook::{{closure}}
at src/libstd/panicking.rs:188
9: std::panicking::default_hook
at src/libstd/panicking.rs:205
10: std::panicking::rust_panic_with_hook
at src/libstd/panicking.rs:464
11: std::panicking::continue_panic_fmt
at src/libstd/panicking.rs:373
12: std::panicking::begin_panic_fmt
at src/libstd/panicking.rs:328
13: mooneye_gb::cpu::execute::<impl mooneye_gb::cpu::Cpu>::undefined
14: mooneye_gb::cpu::decode::<impl mooneye_gb::cpu::Cpu>::decode_exec_fetch
15: mooneye_gb::machine::Machine::emulate
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
16:25:34 οΏ½[31m[ERROR] οΏ½[mοΏ½"SendError(..)"
stack backtrace:
0: failure::backtrace::internal::InternalBacktrace::new
1: failure::backtrace::Backtrace::new
2: mooneye_gb::frontend::emu_thread::EmuThreadHandle::stop
3: mooneye_gb::frontend::SdlFrontend::main
4: mooneye_gb::main
5: std::rt::lang_start::{{closure}}
6: std::rt::lang_start_internal::{{closure}}
at src/libstd/rt.rs:48
std::panicking::try::do_call
at src/libstd/panicking.rs:287
7: __rust_maybe_catch_panic
at src/libpanic_unwind/lib.rs:78
8: std::panicking::try
at src/libstd/panicking.rs:265
std::panic::catch_unwind
at src/libstd/panic.rs:396
std::rt::lang_start_internal
at src/libstd/rt.rs:47
9: main
10: __libc_start_main
11: _start
; the first two bytes of CALL nn will be at $FDFE, so
; the high byte of nn is at the first byte of OAM during testing
; [...]
; the memory read of nn is aligned to happen exactly one cycle
; before the OAM DMA end, so high byte of nn = $FF
; therefore the call becomes:
; call c, $ffca
From what I understand the CPU is only able to access HIRAM during OAM DMA.. so wouldn't that turn into RST $38 ($FF) instead?
Another piece of that test that is confusing to me is the per-cycle timings of CALL.
; CALL cc, nn is expected to have the following timing:
; M = 0: instruction decoding
; M = 1: nn read: memory access for low byte
; M = 2: nn read: memory access for high byte
; M = 3: internal delay
; M = 4: PC push: memory access for high byte
; M = 5: PC push: memory access for low byte
You state above that the low byte is read first. That seems to go against what the test expects if its expecting the low byte to be read correctly and the high byte to be read as $FF.
According to my analysis of the boot ROM, the flags on dmgABC originate from this add a, [hl], which does not overflow if the header checksum is $00, thus setting F to $80 instead of $B0.
https://sameboy.github.io/features/ claims that all mooneye-gbβs acceptance tests are passing, i would like an official test that is added in the README.md
When I tried this test with my emulator, instead of failing or passing, the emulator just stopped after a while. As I was investigating why, it occurred to me that my implementation should have triggered the "R3: unwanted cancel" failure but that it didn't for some reason.
My understanding of the scenario that should lead to this failure is as follows:
from address PC=0x0235 with SP=0x0001, we trigger INTR_SERIAL with "ldh (<IF), a"
while processing this interrupt, if it is cancelled, PC is set to 0
because at the end of ie_push.s we have ".org $0000" followed by "jp hl", when executing the instruction at PC=0, we should jump to the address contained in HL
However, because we trigger the interrupt with SP=1, pushing PC as part of processing the interrupt not only puts 0x35 into IE but it also puts 0x02 at the address 0 which replaces the 0xE9 opcode of the jump instruction that should trigger the failure path.
Is my interpretation correct or did I miss something ?
I tried to build this emulator, but I get the following build error (on MacOS):
--> src/frontend/renderer.rs:57:7
|
57 | FrameState::Even => FrameState::Odd,
| ^^^^^^^^^^^^^^^^ help: consider using a reference: `&FrameState::Even`
error[E0658]: non-reference pattern used to match a reference (see issue #42640)
--> src/frontend/renderer.rs:58:7
|
58 | FrameState::Odd => FrameState::Even,
| ^^^^^^^^^^^^^^^ help: consider using a reference: `&FrameState::Odd
error: aborting due to 2 previous errors
error: Could not compile `mooneye-gb`.
Any ideas what is causing this and how to fix? Thanks! Very interested in and excited about this project.
Perhaps it'd be easier to publish the package, along with the core, on Cargo to simplify installation and make it so others can use the core emulator as a dependency, potentially writing different frontends.
According to the most known Game Boy CPU Manual, as well as the official GB programming manual the half-carry flag is set when there's a carry from bit 11. This would mean a check for 0x0FFF, and not 0x07FF (bit 10).
A test case from the official developer manual (page 97):
When HL = 0x8A23
ADD HL, HL ; HL <- 0x1446, H <- 1, N <- 0, CY <- 1
Please, remove it. The real behaviour of that pseudo-opcode (which, acording to wla-dx, is 0xED) is to hang the CPU forever. Most emulators show error messages when they try to execute that instruction. You should use "ld b,b", which is kind of a standard (no$gmb, bgb) and it doesn't hang emulators or real hardware. In fact, the only reason the test roms can work in real hardware is that just after that instruction there's an infinite loop, so the CPU would be trapped there anyway.
I'm running an emulator against the acceptance/boot_regs-dmg0 rom and was a bit surprised to see that the flag register for DMG is expected to be 00 on boot.
I realize all of these ROMs are tested on hardware, so I'm perplexed as to how the assertions came to be as they are. Is it possible pandocs + the bootstrap are wrong?
Alternatively, is it possible I'm misunderstanding when these registers are expected to be evaluated? E.g. I'm assuming that the registers are snapshotted after the bootstrap sequence, at which point the assumption above of the z flag being set should hold true assuming the bootstrap for DMG is correct.
I'm facing a problem with this test and I need your insight!
At test_fe00: , it appears that OAM is cleared with call clear_oam , then DMA runs and the test compares the result with ram_pattern_1. Since OAM was cleared prior to DMA, it will never have the correct pattern, so you even have to remove the clear or reload the pattern before triggering the DMA.
I hope I didn't get the whole thing wrong!
Keep up the great work!
Hey, would be a cool thing when the test roms not only print the test result on the screen but also output the characters through the serial port of the gb system. Would help with automated testing for emulators i guess becuase you could easier output the errors happent.
But me I am looking for a good remote debugger for gameboy color. I did not find any on the market. MGBA and VisualBoy advance do not support gdbserver for gameboy color.
So I suggest to let me make a pull request to add the feature to debug. Then I will add a remote debuger.
I already tried to start on rusty-boy and oxydbg sadly they were not enough mature or the maintainer did not allow new pull request.