Skip to content

Commit

Permalink
Merge pull request #87 from qoda-dev/beta
Browse files Browse the repository at this point in the history
Beta
  • Loading branch information
nkrs-lab authored Apr 23, 2023
2 parents 68448a5 + accf3d0 commit 71ccb6b
Show file tree
Hide file tree
Showing 21 changed files with 7,881 additions and 15 deletions.
679 changes: 678 additions & 1 deletion Cargo.lock

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
[package]
name = "rustboy"
version = "0.1.0"
name = "qoboy"
version = "1.0.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
minifb = "0.23.0"
120 changes: 120 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Qoboy

A gameboy emulator with an embedded debugger and a video ram viewer.

## Installation and running

Clone and build the projet with the following commands

```shell
git clone https://github.com/qoda-dev/qoboy.git

cd qoboy/

cargo build --release
```

To use the emulator, you have to bring your own **boot rom** and **game rom** files. Then you can run the game with the following command:

```shell
cargo run <boot_rom_path> <game_rom_path>
```

The keyboard mapping is defined as follows:

| Gameboy control | Keyboard |
| ----------------- | ------- |
| A | a |
| B | z |
| start | backspace |
| select | enter |
| left | left arrow |
| right | right arrow |
| up | up arrow |
| down | down arrow |

## Embedded debugger

This emulator comes with an embedded **video ram viewer** and a light **debugger** which can ease the development of your game or your own emulator by using this one as a reference.

To launch the debugger, add **--debug** when running your game rom:

```shell
cargo run <boot_rom_path> <game_rom_path> --debug
```

Till now, the debugger can handle the following commands:

| command | argument | description |
| ----------------- | ------- | ------ |
| run | none | run the cpu until it encounters a breakpoint or a halt command is received |
| halt | none | when the cpu is running, halt its execution to the current program counter |
| step | none | when the cpu is halted, execute the instruction pointed by the program counter and update the PC to the next instruction |
| break_set | address | set a breakpoint to the address |
| break_reset | none | reset the breakpoint |

The emulator can manage only **one breakpoint** and the address passed to the **break_set** command shall meet the following format:

```shell
break_set C012
```

Where **C012** is the program address on which we want to break in **hexadecimal** format.

> When launched with the **--debug** option, the emulator stops at address 0x0000 by default and waits for a command just like after a **halt** command has been typed.
> Type **run** or **step** to run your program.
## Tests

In addition to unit tests for each module, more general functionnal tests are done with blargg's and Acid2 test roms.

### Blargg's tests

Source files can be found [here](https://github.com/retrio/gb-test-roms). These roms are used to test general behaviour of CPU, timer and memory subsystems.

| Blargg's test rom | Comment | Result |
| ----------------- | ------- | ------ |
| cpu_instrs | none | :heavy_check_mark: |
| instr_timing | none | :heavy_check_mark: |
| interrupt_time | need sound to pass | :x: |
| dmg_sound | need sound to pass | :x: |
| oam_bug | not implemented | :x: |
| halt_bug | not implemented | :x: |
| mem_timing | need a clock cycle accurate emulator | :x: |
| mem_timing-2 | need a clock cycle accurate emulator | :x: |

### Acid2 tests

Source files can be found [here](https://github.com/mattcurrie/dmg-acid2). This rom is used to test the PPU unit.

| test rom | Comment | Result |
| -------- | ------- | ------ |
| dmg_acid2 | sprite priority follows GB color behaviour | :x: |

## Features

- [X] implement a gameboy emulator which passes all cpu_instr and instr_timing tests
- [X] add support to no_mbc / mbc1 / mbc3 cartridge types
- [X] implement a lightweight debugger
- [X] implement a vram viewer
- [ ] fix sprite priority to pass ACID2 test
- [ ] add possibility to save a game
- [ ] use winit and softbuffer instead of minifb (which is not as stable as expected)

## Ressources

### General

- https://gbdev.io/pandocs/Specifications.html
- https://gbdev.gg8.se/wiki/articles/Main_Page

### Opcodes

- https://meganesu.github.io/generate-gb-opcodes/
- https://www.pastraiser.com/cpu/gameboy/gameboy_opcodes.html

### Reference emulators

- https://github.com/Gekkio/mooneye-gb
- https://github.com/mohanson/gameboy
- https://github.com/rylev/DMG-01
1 change: 1 addition & 0 deletions assets/ASSET.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Here can be found custom logo for the project. Use the STARGAZE font for the project title.
158 changes: 158 additions & 0 deletions src/cartridge/mbc1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
use crate::cartridge::{MbcType, RomSize, RamSize, Mbc};

const RAM_ENABLE_SPACE_START: u16 = 0x0000;
const RAM_ENABLE_SPACE_END: u16 = 0x1FFF;

const ROM_BANK_NB_SPACE_START: u16 = 0x2000;
const ROM_BANK_NB_SPACE_END: u16 = 0x3FFF;

const RAM_BANK_NB_SPACE_START: u16 = 0x4000;
const RAM_BANK_NB_SPACE_END: u16 = 0x5FFF;

const BANKING_MODE_SPACE_START: u16 = 0x6000;
const BANKING_MODE_SPACE_END: u16 = 0x7FFF;

const ENABLE_RAM_FLAG: u8 = 0x0A;

const GB_ADDR_BIT_MASK: usize = 0x3FFF;
const ROM_BANK_BIT_OFFSET: usize = 14;
const RAM_BANK_BIT_OFFSET: usize = 19;

#[allow(non_camel_case_types)]
enum RomBankMask {
MASK_1_BIT = 0x01,
MASK_2_BIT = 0x03,
MASK_3_BIT = 0x07,
MASK_4_BIT = 0x0F,
MASK_5_BIT = 0x1F,
}

pub struct Mbc1 {
// config
rom_size: RomSize,
// internal registers
ram_enable: bool,
rom_bank_number: u8,
ram_bank_number: u8,
banking_mode: bool,
// memory
rom_bank: Vec<u8>,
ram_bank: Vec<u8>,
}

impl Mbc1 {
pub fn new(_: MbcType, rom_size: RomSize, ram_size: RamSize, rom: &[u8]) -> Mbc1 {
let mut rom_bank: Vec<u8> = vec![0xFF; rom_size.clone() as usize];
let ram_bank: Vec<u8> = vec![0xFF; ram_size.clone() as usize];

// copy all rom data
for rom_index in 0..(rom_size as usize){
rom_bank[rom_index as usize] = rom[rom_index as usize];
}

Mbc1 {
// config
rom_size: rom_size,
// internal registers
ram_enable: false,
rom_bank_number: 1,
ram_bank_number: 0,
banking_mode: false,
// memory
rom_bank: rom_bank,
ram_bank: ram_bank,
}
}
}

impl Mbc for Mbc1 {
fn read_bank_0 (&self, address: usize) -> u8 {
if self.banking_mode {
let gb_addr = ((self.ram_bank_number as usize) << RAM_BANK_BIT_OFFSET) | (address & GB_ADDR_BIT_MASK);
self.rom_bank[gb_addr]
} else {
let gb_addr = address & GB_ADDR_BIT_MASK;
self.rom_bank[gb_addr]
}
}

fn read_bank_n (&self, address: usize) -> u8 {
let gb_addr = ((self.ram_bank_number as usize) << RAM_BANK_BIT_OFFSET)
| ((self.rom_bank_number as usize) << ROM_BANK_BIT_OFFSET)
| (address & GB_ADDR_BIT_MASK);
self.rom_bank[gb_addr]
}

fn read_ram (&self, address: usize) -> u8 {
if self.ram_enable {
if self.banking_mode {
let gb_addr = address & 0x1FFF;
self.ram_bank[gb_addr]
} else {
let gb_addr = ((self.ram_bank_number as usize) << 13)
| (address & 0x1FFF);
self.ram_bank[gb_addr]
}
} else {
// RAM is disabled, returns 0xFF
0xFF
}
}

fn write_bank_0 (&mut self, address: usize, data: u8) {
match address as u16 {
RAM_ENABLE_SPACE_START..=RAM_ENABLE_SPACE_END => {
if data == ENABLE_RAM_FLAG {
self.ram_enable = true;
}
},
ROM_BANK_NB_SPACE_START..=ROM_BANK_NB_SPACE_END => {
let rom_bank_mask = match self.rom_size {
RomSize::SIZE_32_KB => RomBankMask::MASK_1_BIT,
RomSize::SIZE_64_KB => RomBankMask::MASK_2_BIT,
RomSize::SIZE_128_KB => RomBankMask::MASK_3_BIT,
RomSize::SIZE_256_KB => RomBankMask::MASK_4_BIT,
_ => RomBankMask::MASK_5_BIT,
};

self.rom_bank_number = if data != 0 {
data & (rom_bank_mask as u8)
} else {
// if register is set to 0, set it to 1
1
};
},
_ => panic!("mbc 1 bank 0 address {:x} doesn't exists.", address),
}
}

fn write_bank_n (&mut self, address: usize, data: u8) {
match address as u16 {
RAM_BANK_NB_SPACE_START..=RAM_BANK_NB_SPACE_END => {
self.ram_bank_number = data & 0x03;
},
BANKING_MODE_SPACE_START..=BANKING_MODE_SPACE_END => {
self.banking_mode = (data & 0x01) != 0;
},
_ => panic!("mbc 1 bank n address {:x} doesn't exists.", address),
}
}

fn write_ram (&mut self, address: usize, data: u8) {
if self.ram_enable {
if self.banking_mode {
let gb_addr = address & 0x1FFF;
self.ram_bank[gb_addr] = data;
} else {
let gb_addr = ((self.ram_bank_number as usize) << 13)
| (address & 0x1FFF);
self.ram_bank[gb_addr] = data;
}
} else {
// do nothing when ram is disabled
}
}

// not used for this mbc, doesn't do anything
fn run (&mut self, _: u8) {}
}
Loading

0 comments on commit 71ccb6b

Please sign in to comment.