feat: basic ppu

This commit is contained in:
EliseZeroTwo 2021-11-24 12:01:25 +01:00
commit 12f97b8d5b
No known key found for this signature in database
GPG key ID: E6D56A6F7B7991DE
15 changed files with 3113 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target
/roms/
config.toml

1913
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

20
Cargo.toml Normal file
View file

@ -0,0 +1,20 @@
[package]
name = "deemgee"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
argh = "0.1.6"
chrono = "0.4.19"
config = "0.11.0"
env_logger = "0.9.0"
log = "0.4.14"
paste = "1.0.6"
pixels = "0.7.0"
serde = { version = "1.0.130", features = ["derive"] }
sha1 = { version = "0.6.0", features = ["std"] }
thiserror = "1.0.30"
winit = { version = "0.25.0", features = ["serde"] }
winit_input_helper = "0.10.0"

11
config.example.toml Normal file
View file

@ -0,0 +1,11 @@
[bindings]
a = "A"
b = "S"
select = "Q"
start = "W"
up = "Up"
down = "Down"
left = "Left"
right = "Right"
pause = "P"
exit = "Escape"

7
rustfmt.toml Normal file
View file

@ -0,0 +1,7 @@
hard_tabs=true
max_width = 100
comment_width = 80
wrap_comments = true
imports_granularity = "Crate"
use_small_heuristics = "Max"
group_imports = "StdExternalCrate"

232
src/gameboy.rs Normal file
View file

@ -0,0 +1,232 @@
mod interrupts;
mod joypad;
mod mapper;
mod memory;
mod ppu;
mod timer;
use interrupts::Interrupts;
use joypad::Joypad;
use mapper::Mapper;
use memory::Memory;
use ppu::Ppu;
use timer::Timer;
pub struct DmaState {
pub base: u8,
pub remaining_cycles: u8,
pub remaining_delay: u8,
}
impl DmaState {
pub fn new() -> Self {
Self { base: 0, remaining_cycles: 0, remaining_delay: 0 }
}
pub fn init_request(&mut self, base: u8) {
self.base = base;
self.remaining_cycles = 0xA0;
self.remaining_delay = 2;
}
}
pub struct Gameboy {
pub ppu: Ppu,
memory: Memory,
cartridge: Option<Box<dyn Mapper>>,
interrupts: Interrupts,
timer: Timer,
pub joypad: Joypad,
pub dma: DmaState,
}
impl Gameboy {
pub fn new(bootrom: [u8; 0x100]) -> Self {
Self {
memory: Memory::new(bootrom),
cartridge: None,
interrupts: Interrupts::new(),
timer: Timer::new(),
joypad: Joypad::new(),
dma: DmaState::new(),
ppu: Ppu::new(),
}
}
pub fn tick(&mut self) -> bool {
if self.timer.tick() {
self.interrupts.write_if_timer(true);
}
self.tick_cpu();
let redraw_requested = self.ppu.tick(&mut self.interrupts);
self.tick_dma();
redraw_requested
}
fn tick_cpu(&mut self) {}
fn tick_dma(&mut self) {
if self.dma.remaining_delay > 0 {
self.dma.remaining_delay -= 1;
} else if self.dma.remaining_cycles > 0 {
let offset = 0xA0 - self.dma.remaining_cycles;
let value = if self.dma.base <= 0x7F {
match self.cartridge.as_ref() {
Some(cart) => cart.read_rom_u8((self.dma.base as u16) << 8 | offset as u16),
None => 0xFF,
}
} else if self.dma.base <= 0x9F {
self.ppu.dma_read_vram(offset)
} else {
0xFF
};
self.ppu.dma_write_oam(offset, value);
self.dma.remaining_cycles -= 1;
}
}
fn cpu_read_io(&self, address: u16) -> u8 {
match address {
0xFF00 => self.joypad.cpu_read(),
0xFF01..=0xFF02 => unimplemented!("Serial"),
0xFF03 => 0, // Unused
0xFF04 => self.timer.div,
0xFF05 => self.timer.tima,
0xFF06 => self.timer.tma,
0xFF07 => self.timer.read_tac(),
0xFF08..=0xFF0E => 0, // Unused
0xFF0F => self.interrupts.interrupt_enable,
0xFF10..=0xFF3F => unimplemented!("Sound IO"),
0xFF40 => self.ppu.lcdc,
0xFF41 => self.ppu.stat,
0xFF42 => self.ppu.scy,
0xFF43 => self.ppu.scx,
0xFF44 => self.ppu.ly,
0xFF45 => self.ppu.lyc,
0xFF46 => self.dma.base,
0xFF47..=0xFF49 => 0,
0xFF4A => self.ppu.wy,
0xFF4B => self.ppu.wx,
0xFF4C..=0xFF4E => 0, // Unused
0xFF4F => 0, // CGB VRAM Bank Select
0xFF50 => self.memory.bootrom_disabled as u8,
0xFF51..=0xFF55 => 0, // CGB VRAM DMA
0xFF56..=0xFF67 => 0, // Unused
0xFF68..=0xFF69 => 0, // BJ/OBJ Palettes
0xFF6A..=0xFF6F => 0, // Unused
0xFF70 => 0, // CGB WRAM Bank Select
0xFF71..=0xFF7F => 0, // Unused
_ => unreachable!("IO Read Invalid"),
}
}
fn cpu_write_io(&mut self, address: u16, value: u8) {
match address {
0xFF00 => self.joypad.cpu_write(value),
0xFF01..=0xFF02 => unimplemented!("Serial"),
0xFF03 => {} // Unused
0xFF04 => self.timer.div = value,
0xFF05 => self.timer.tima = value,
0xFF06 => self.timer.tma = value,
0xFF07 => self.timer.write_tac(value),
0xFF08..=0xFF0E => {} // Unused
0xFF0F => self.interrupts.interrupt_enable = value & 0b1_1111,
0xFF10..=0xFF3F => unimplemented!("Sound IO"),
0xFF40 => self.ppu.lcdc = value,
0xFF41 => self.ppu.cpu_write_stat(value),
0xFF42 => self.ppu.scy = value,
0xFF43 => self.ppu.scx = value,
0xFF44 => {} // LY is read only
0xFF45 => self.ppu.lyc = value,
0xFF46 => {
if self.dma.remaining_cycles == 0 {
self.dma.init_request(value);
}
}
0xFF47..=0xFF49 => {}
0xFF4A => self.ppu.wy = value,
0xFF4B => self.ppu.wx = value,
0xFF4C..=0xFF4E => {} // Unused
0xFF4F => {} // CGB VRAM Bank Select
0xFF50 => {
if value & 0b1 == 1 {
self.memory.bootrom_disabled = true;
}
}
0xFF51..=0xFF55 => {} // CGB VRAM DMA
0xFF56..=0xFF67 => {} // Unused
0xFF68..=0xFF69 => {} // CGB BG/OBJ Palettes
0xFF6A..=0xFF6F => {} // Unused
0xFF70 => {} // CGB WRAM Bank Select
0xFF71..=0xFF7F => {} // Unused
_ => unreachable!("IO Read Invalid"),
}
}
pub fn cpu_read_u8(&self, address: u16) -> u8 {
if self.dma.remaining_cycles == 0 {
match address {
0..=0xFF if !self.memory.bootrom_disabled => self.memory.bootrom[address as usize],
0..=0x7FFF => match self.cartridge.as_ref() {
Some(mapper) => mapper.read_rom_u8(address),
None => 0,
},
0x8000..=0x9FFF => self.ppu.cpu_read_vram(address),
0xA000..=0xBFFF => match self.cartridge.as_ref() {
Some(mapper) => mapper.read_eram_u8(address),
None => 0,
},
0xC000..=0xDFFF => self.memory.wram[address as usize - 0xC000],
0xE000..=0xFDFF => self.memory.wram[address as usize - 0xE000],
0xFE00..=0xFE9F => self.ppu.cpu_read_oam(address),
0xFEA0..=0xFEFF => 0,
0xFF00..=0xFF7F => self.cpu_read_io(address),
0xFF80..=0xFFFE => self.memory.hram[address as usize - 0xFF80],
0xFFFF => self.interrupts.interrupt_enable,
}
} else {
match address {
0..=0xFEFF => 0,
0xFF00..=0xFF7F => self.cpu_read_io(address),
0xFF80..=0xFFFE => self.memory.hram[address as usize - 0xFF80],
0xFFFF => self.interrupts.interrupt_enable,
}
}
}
pub fn cpu_write_u8(&mut self, address: u16, value: u8) {
if self.dma.remaining_cycles == 0 {
match address {
0..=0xFF if !self.memory.bootrom_disabled => {}
0..=0x7FFF => {
if let Some(mapper) = self.cartridge.as_mut() {
mapper.write_rom_u8(address, value)
}
}
0x8000..=0x9FFF => self.ppu.cpu_write_vram(address, value),
0xA000..=0xBFFF => {
if let Some(mapper) = self.cartridge.as_mut() {
mapper.write_eram_u8(address, value)
}
}
0xC000..=0xDFFF => self.memory.wram[address as usize - 0xC000] = value,
0xE000..=0xFDFF => self.memory.wram[address as usize - 0xE000] = value,
0xFE00..=0xFE9F => self.ppu.cpu_write_oam(address, value),
0xFEA0..=0xFEFF => {}
0xFF00..=0xFF7F => self.cpu_write_io(address, value),
0xFF80..=0xFFFE => self.memory.hram[address as usize - 0xFF80] = value,
0xFFFF => self.interrupts.interrupt_enable = value & 0b1_1111,
}
} else {
match address {
0..=0xFEFF => {}
0xFF00..=0xFF7F => self.cpu_write_io(address, value),
0xFF80..=0xFFFE => self.memory.hram[address as usize - 0xFF80] = value,
0xFFFF => self.interrupts.interrupt_enable = value & 0b1_1111,
}
}
}
}

38
src/gameboy/interrupts.rs Normal file
View file

@ -0,0 +1,38 @@
macro_rules! define_bitfield_u8_gs {
($name:ident, $offset:literal, $loc:ident) => {
paste::paste! {
pub fn [<read_ $name>](&self) -> bool {
((self.$loc >> $offset) & 0b1) == 1
}
pub fn [<write_ $name>](&mut self, value: bool) {
log::debug!(std::concat!("Setting ", std::stringify!($name), " to {}"), value);
self.$loc &= !(0b1 << $offset);
self.$loc |= (value as u8) << $offset;
}
}
};
}
pub struct Interrupts {
pub ime: bool,
pub interrupt_enable: u8,
pub interrupt_flag: u8,
}
impl Interrupts {
pub fn new() -> Self {
Self { ime: true, interrupt_enable: 0b1_1111, interrupt_flag: 0b0_0000 }
}
define_bitfield_u8_gs!(ie_vblank, 0, interrupt_enable);
define_bitfield_u8_gs!(ie_lcd_stat, 1, interrupt_enable);
define_bitfield_u8_gs!(ie_timer, 2, interrupt_enable);
define_bitfield_u8_gs!(ie_serial, 3, interrupt_enable);
define_bitfield_u8_gs!(ie_joypad, 4, interrupt_enable);
define_bitfield_u8_gs!(if_vblank, 0, interrupt_flag);
define_bitfield_u8_gs!(if_lcd_stat, 1, interrupt_flag);
define_bitfield_u8_gs!(if_timer, 2, interrupt_flag);
define_bitfield_u8_gs!(if_serial, 3, interrupt_flag);
define_bitfield_u8_gs!(if_joypad, 4, interrupt_flag);
}

62
src/gameboy/joypad.rs Normal file
View file

@ -0,0 +1,62 @@
#[derive(Debug, PartialEq, Eq)]
pub enum JoypadMode {
Action,
Direction,
}
pub struct Joypad {
mode: JoypadMode,
pub down: bool,
pub up: bool,
pub left: bool,
pub right: bool,
pub start: bool,
pub select: bool,
pub b: bool,
pub a: bool,
}
impl Joypad {
pub fn new() -> Self {
Self {
mode: JoypadMode::Action,
down: false,
up: false,
left: false,
right: false,
start: false,
select: false,
b: false,
a: false,
}
}
pub fn cpu_read(&self) -> u8 {
match self.mode {
JoypadMode::Action => {
(1 << 4)
| ((!self.start as u8) << 3)
| ((!self.select as u8) << 2)
| ((!self.b as u8) << 1)
| (!self.a as u8)
}
JoypadMode::Direction => {
(1 << 5)
| ((!self.down as u8) << 3)
| ((!self.up as u8) << 2)
| ((!self.left as u8) << 1)
| (!self.right as u8)
}
}
}
pub fn cpu_write(&mut self, content: u8) {
if (content >> 5) & 0b1 == 0 {
self.mode = JoypadMode::Action;
}
if (content >> 4) & 0b1 == 0 {
self.mode = JoypadMode::Direction;
}
}
}

37
src/gameboy/mapper.rs Normal file
View file

@ -0,0 +1,37 @@
pub trait Mapper {
fn read_rom_u8(&self, address: u16) -> u8;
fn write_rom_u8(&mut self, address: u16, value: u8);
fn read_eram_u8(&self, address: u16) -> u8;
fn write_eram_u8(&mut self, address: u16, value: u8);
}
pub struct NoMBC {
rom: [u8; 0x8000],
ram: Option<[u8; 0x2000]>,
}
impl Mapper for NoMBC {
fn read_rom_u8(&self, address: u16) -> u8 {
self.rom[address as usize]
}
fn write_rom_u8(&mut self, address: u16, value: u8) {
self.rom[address as usize] = value
}
fn read_eram_u8(&self, address: u16) -> u8 {
let decoded_address = address - 0xA000;
match &self.ram {
Some(ram) => ram[decoded_address as usize],
None => 0,
}
}
fn write_eram_u8(&mut self, address: u16, value: u8) {
let decoded_address = address - 0xA000;
if let Some(ram) = &mut self.ram {
ram[decoded_address as usize] = value;
}
}
}

13
src/gameboy/memory.rs Normal file
View file

@ -0,0 +1,13 @@
pub struct Memory {
pub wram: [u8; 0x2000],
pub hram: [u8; 0xAF],
pub bootrom_disabled: bool,
pub bootrom: [u8; 0x100],
}
impl Memory {
pub fn new(bootrom: [u8; 0x100]) -> Self {
Self { wram: [0; 0x2000], hram: [0; 0xAF], bootrom, bootrom_disabled: false }
}
}

365
src/gameboy/ppu.rs Normal file
View file

@ -0,0 +1,365 @@
use std::ops::{Index, IndexMut};
use crate::{gameboy::Interrupts, window::FB_WIDTH};
pub struct WrappedBuffer<const SIZE: usize>([u8; SIZE]);
impl<const SIZE: usize> Index<usize> for WrappedBuffer<SIZE> {
type Output = u8;
fn index(&self, index: usize) -> &Self::Output {
&self.0[index % SIZE]
}
}
impl<const SIZE: usize> IndexMut<usize> for WrappedBuffer<SIZE> {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.0[index % SIZE]
}
}
impl<const SIZE: usize> WrappedBuffer<SIZE> {
pub fn empty() -> Self {
Self([0; SIZE])
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum PPUMode {
HBlank,
VBlank,
SearchingOAM,
TransferringData,
}
#[derive(Debug, Clone, Copy)]
pub enum Color {
White,
LGray,
DGray,
Black,
Transparent,
}
impl Color {
pub fn rgba(self) -> &'static [u8; 4] {
match self {
Color::White => &[0x7F, 0x86, 0x0F, 0xFF],
Color::LGray => &[0x57, 0x7c, 0x44, 0xFF],
Color::DGray => &[0x36, 0x5d, 0x48, 0xFF],
Color::Black => &[0x2a, 0x45, 0x3b, 0xFF],
Color::Transparent => &[0x00, 0x00, 0x00, 0x00],
}
}
pub fn parse_bgp_color(color: u8) -> Self {
match color & 0b11 {
0 => Self::White,
1 => Self::LGray,
2 => Self::DGray,
3 => Self::Black,
_ => unreachable!(),
}
}
pub fn parse_obp_color(color: u8) -> Self {
match color & 0b11 {
0 => Self::Transparent,
1 => Self::LGray,
2 => Self::DGray,
3 => Self::Black,
_ => unreachable!(),
}
}
pub fn parse_bgp(mut bgp: u8) -> [Self; 4] {
let mut out = [Self::White, Self::White, Self::White, Self::White];
for color in &mut out {
*color = Self::parse_bgp_color(bgp);
bgp >>= 2;
}
out.reverse();
out
}
pub fn parse_obp(mut obp: u8) -> [Self; 4] {
let mut out = [Self::Transparent, Self::Transparent, Self::Transparent, Self::Transparent];
for color in &mut out {
*color = Self::parse_obp_color(obp);
obp >>= 2;
}
out.reverse();
out
}
}
impl PPUMode {
pub fn mode_flag(&self) -> u8 {
match self {
PPUMode::HBlank => 0,
PPUMode::VBlank => 1,
PPUMode::SearchingOAM => 2,
PPUMode::TransferringData => 3,
}
}
pub fn from_mode_flag(value: u8) -> Self {
match value & 0b11 {
0 => Self::HBlank,
1 => Self::VBlank,
2 => Self::SearchingOAM,
3 => Self::TransferringData,
_ => unreachable!(),
}
}
}
pub struct Ppu {
pub lcdc: u8,
pub stat: u8,
pub scy: u8,
pub scx: u8,
pub ly: u8,
pub lyc: u8,
pub wy: u8,
pub wx: u8,
pub vram: [u8; 0x2000],
pub oam: [u8; 0xA0],
pub cycle_counter: u16,
pub bgp: u8,
pub obp: u8,
pub framebuffer: WrappedBuffer<{ 160 * 144 * 4 }>,
pub sprite_framebuffer: WrappedBuffer<{ 160 * 144 * 4 }>,
}
impl Ppu {
pub fn new() -> Self {
Self {
lcdc: 0b1000_0000,
stat: 0b0000_0010,
scy: 0,
scx: 0,
ly: 0,
lyc: 0,
wy: 0,
wx: 0,
vram: [0; 0x2000],
oam: [0; 0xA0],
cycle_counter: 0,
framebuffer: WrappedBuffer::empty(),
sprite_framebuffer: WrappedBuffer::empty(),
bgp: 0,
obp: 0,
}
}
fn set_scanline(&mut self, interrupts: &mut Interrupts, scanline: u8) {
self.ly = scanline;
self.stat &= !(1 << 2);
if self.ly == self.lyc {
self.stat |= 1 << 2;
if (self.stat >> 6) & 0b1 == 1 {
interrupts.write_if_lcd_stat(true);
}
}
}
fn draw_line(&mut self) {
for pixel_idx in 0..FB_WIDTH as u8 {
let scrolled_x = pixel_idx.overflowing_add(self.scx).0 as usize;
let scrolled_y = self.ly.overflowing_add(self.scy).0 as usize;
let tilemap_idx = scrolled_x / 8 + ((scrolled_y as usize / 8) * 32);
let tilemap_value = self.read_tile_map()[tilemap_idx];
let color = Self::parse_tile_color(
self.read_tile(tilemap_value),
scrolled_x % 8,
scrolled_y % 8,
);
let dest_idx_base = ((self.scy as usize * FB_WIDTH as usize) + pixel_idx as usize) * 4;
for (idx, byte) in color.rgba().iter().enumerate() {
self.framebuffer[dest_idx_base + idx] = *byte;
}
}
}
fn parse_tile_color(tile: &[u8], x: usize, y: usize) -> Color {
assert!(x < 8);
if x < 4 {
let bitshift = 6 - x * 2;
Color::parse_bgp_color(tile[y * 2] >> bitshift)
} else {
let x = x - 4;
let bitshift = 6 - x * 2;
Color::parse_bgp_color(tile[(y * 2) + 1] >> bitshift)
}
}
fn set_mode(&mut self, interrupts: &mut Interrupts, mode: PPUMode) {
log::debug!("PPU switching mode to {:?} @ {}", mode, self.cycle_counter);
self.stat &= !0b11;
self.stat |= mode.mode_flag();
self.cycle_counter = 0;
let offset = match mode {
PPUMode::HBlank => 3,
PPUMode::VBlank => 4,
PPUMode::SearchingOAM => 5,
_ => return,
};
if (self.stat >> offset) & 0b1 == 1 {
interrupts.write_if_lcd_stat(true);
}
if mode == PPUMode::VBlank {
interrupts.write_if_vblank(true);
}
}
pub fn write_fb(&self) -> Vec<u8> {
let mut out = self.framebuffer.0.to_vec();
for x in 0..(160 * 144) {
let idx = x * 4;
let (r, g, b, a) = (
self.sprite_framebuffer[idx],
self.sprite_framebuffer[idx + 1],
self.sprite_framebuffer[idx + 2],
self.sprite_framebuffer[idx + 3],
);
if r != 0 || g != 0 || b != 0 || a != 0 {
out[idx] = r;
out[idx + 1] = g;
out[idx + 2] = b;
out[idx + 3] = a;
}
}
out
}
pub fn tick(&mut self, interrupts: &mut Interrupts) -> bool {
let res = match self.mode() {
PPUMode::HBlank => {
if self.cycle_counter >= 120 {
self.set_scanline(interrupts, self.ly + 1);
let next_mode = match self.ly > 143 {
true => PPUMode::VBlank,
false => PPUMode::SearchingOAM,
};
self.set_mode(interrupts, next_mode);
}
false
}
PPUMode::VBlank => {
if self.cycle_counter % 506 == 0 {
if self.ly >= 153 {
self.set_scanline(interrupts, 0);
self.set_mode(interrupts, PPUMode::SearchingOAM);
true
} else {
self.set_scanline(interrupts, self.ly + 1);
false
}
} else {
false
}
}
PPUMode::SearchingOAM => {
if self.cycle_counter >= 80 {
self.set_mode(interrupts, PPUMode::TransferringData);
}
false
}
PPUMode::TransferringData => {
if self.cycle_counter >= 170 {
self.draw_line();
self.set_mode(interrupts, PPUMode::HBlank);
}
false
}
};
self.cycle_counter += 1;
res
}
pub fn mode(&self) -> PPUMode {
PPUMode::from_mode_flag(self.stat)
}
pub fn cpu_read_oam(&self, address: u16) -> u8 {
let decoded_address = address - 0xFE00;
match self.mode() {
PPUMode::HBlank | PPUMode::VBlank => self.oam[decoded_address as usize],
PPUMode::SearchingOAM | PPUMode::TransferringData => 0xFF,
}
}
pub fn dma_write_oam(&mut self, offset: u8, value: u8) {
self.oam[offset as usize] = value;
}
pub fn cpu_write_oam(&mut self, address: u16, value: u8) {
let decoded_address = address - 0xFE00;
match self.mode() {
PPUMode::HBlank | PPUMode::VBlank => self.oam[decoded_address as usize] = value,
_ => {}
}
}
pub fn dma_read_vram(&mut self, offset: u8) -> u8 {
self.vram[offset as usize]
}
pub fn cpu_read_vram(&self, address: u16) -> u8 {
let decoded_address = address - 0x8000;
match self.mode() {
PPUMode::HBlank | PPUMode::VBlank | PPUMode::SearchingOAM => {
self.vram[decoded_address as usize]
}
PPUMode::TransferringData => 0xFF,
}
}
pub fn cpu_write_vram(&mut self, address: u16, value: u8) {
let decoded_address = address - 0x8000;
match self.mode() {
PPUMode::HBlank | PPUMode::VBlank | PPUMode::SearchingOAM => {
self.vram[decoded_address as usize] = value
}
_ => {}
}
}
pub fn cpu_write_stat(&mut self, value: u8) {
self.stat = value & 0b0111_1000;
}
pub fn read_tile(&self, obj: u8) -> &[u8] {
if (self.lcdc >> 4) & 0b1 == 1 {
&self.vram[obj as usize * 16..((obj as usize + 1) * 16)]
} else if obj < 128 {
&self.vram[0x1000 + (obj as usize * 16)..0x1000 + ((obj as usize + 1) * 16)]
} else {
let adjusted_obj = obj - 128;
&self.vram
[0x800 + (adjusted_obj as usize * 16)..0x800 + ((adjusted_obj as usize + 1) * 16)]
}
}
pub fn read_tile_map(&self) -> &[u8] {
match (self.lcdc >> 3) & 0b1 == 1 {
true => &self.vram[0x1C00..=0x1FFF],
false => &self.vram[0x1800..=0x1BFF],
}
}
}

87
src/gameboy/timer.rs Normal file
View file

@ -0,0 +1,87 @@
pub struct Timer {
pub enable: bool,
pub clock: TimerClock,
pub div: u8,
pub div_counter: u8,
pub tima: u8,
pub tima_counter: u16,
pub tma: u8,
}
impl Timer {
pub fn new() -> Self {
Self {
enable: false,
clock: TimerClock::C1024,
tima: 0,
tma: 0,
div: 0,
div_counter: 0,
tima_counter: 0,
}
}
pub fn tick(&mut self) -> bool {
self.div_counter = self.div_counter.overflowing_add(1).0;
if self.div_counter == 0 {
self.div = self.div.overflowing_add(1).0;
}
if self.enable {
self.tima_counter = self.tima_counter.overflowing_add(1).0;
if self.tima_counter >= self.clock.cycles() {
self.tima += self.tima.overflowing_add(1).0;
return self.tima == 0;
}
}
false
}
pub fn read_tac(&self) -> u8 {
((self.enable as u8) << 2) | self.clock.tac_clock()
}
pub fn write_tac(&mut self, value: u8) {
self.enable = (value >> 2) & 0b1 == 1;
self.clock = TimerClock::from_tac_clock(value);
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum TimerClock {
C16,
C64,
C256,
C1024,
}
impl TimerClock {
pub fn cycles(&self) -> u16 {
match self {
Self::C16 => 16,
Self::C64 => 64,
Self::C256 => 256,
Self::C1024 => 1024,
}
}
pub fn tac_clock(&self) -> u8 {
match self {
Self::C16 => 1,
Self::C64 => 2,
Self::C256 => 3,
Self::C1024 => 0,
}
}
pub fn from_tac_clock(value: u8) -> Self {
match value & 0b11 {
1 => Self::C16,
2 => Self::C64,
3 => Self::C256,
4 => Self::C1024,
_ => unreachable!(),
}
}
}

125
src/main.rs Normal file
View file

@ -0,0 +1,125 @@
mod gameboy;
mod settings;
mod window;
use std::{
path::PathBuf,
sync::mpsc::{channel, Receiver, Sender},
};
use argh::FromArgs;
use gameboy::Gameboy;
use settings::DeemgeeConfig;
use window::WindowEvent;
use crate::window::GameboyEvent;
#[derive(Debug, FromArgs)]
/// DMG Emulator
pub struct CliArgs {
/// bootrom path
#[argh(positional)]
pub bootrom: PathBuf,
/// game path
#[argh(positional)]
pub rom: Option<PathBuf>,
}
#[derive(Debug, thiserror::Error)]
pub enum DmgError {
#[error("Bootrom Not Found")]
BootromNotFound,
#[error("Bootrom Incorrect Size (expected 256 bytes, found {0} bytes)")]
BootromInvalidSize(u64),
#[error("Bootrom SHA1 failed (expected 4ed31ec6b0b175bb109c0eb5fd3d193da823339f)")]
BootromInvalidHash,
#[error("Game Not Found")]
GameNotFound,
#[error("IO Error: {0}")]
IO(#[from] std::io::Error),
}
fn main() -> Result<(), DmgError> {
env_logger::init();
let args: CliArgs = argh::from_env();
let config = DeemgeeConfig::from_file();
let (window_side_tx, gb_side_rx) = channel::<WindowEvent>();
let (gb_side_tx, window_side_rx) = channel::<GameboyEvent>();
let jh = std::thread::spawn(move || run_gameboy(config, args, gb_side_rx, gb_side_tx));
window::run_window(config, window_side_rx, window_side_tx);
jh.join().unwrap()
}
pub fn run_gameboy(
_config: DeemgeeConfig,
args: CliArgs,
rx: Receiver<WindowEvent>,
tx: Sender<GameboyEvent>,
) -> Result<(), DmgError> {
if !args.bootrom.is_file() {
return Err(DmgError::BootromNotFound);
}
let brom_md = std::fs::metadata(args.bootrom.as_path())?;
if brom_md.len() != 256 {
return Err(DmgError::BootromInvalidSize(brom_md.len()));
}
let bootrom = std::fs::read(args.bootrom)?;
if bootrom.len() != 256 {
return Err(DmgError::BootromInvalidSize(bootrom.len() as u64));
}
if sha1::Sha1::from(bootrom.as_slice()).hexdigest().as_str()
!= "4ed31ec6b0b175bb109c0eb5fd3d193da823339f"
{
return Err(DmgError::BootromInvalidHash);
}
let mut gameboy = Gameboy::new(bootrom.as_slice().try_into().unwrap());
let mut last = chrono::Local::now();
let mut paused = false;
let mut frame_counter = 0;
'outer: loop {
while let Ok(event) = rx.try_recv() {
match event {
window::WindowEvent::AToggle => gameboy.joypad.a = !gameboy.joypad.a,
window::WindowEvent::BToggle => gameboy.joypad.b = !gameboy.joypad.b,
window::WindowEvent::SelectToggle => gameboy.joypad.select = !gameboy.joypad.select,
window::WindowEvent::StartToggle => gameboy.joypad.start = !gameboy.joypad.start,
window::WindowEvent::UpToggle => gameboy.joypad.up = !gameboy.joypad.up,
window::WindowEvent::DownToggle => gameboy.joypad.down = !gameboy.joypad.down,
window::WindowEvent::LeftToggle => gameboy.joypad.left = !gameboy.joypad.left,
window::WindowEvent::RightToggle => gameboy.joypad.right = !gameboy.joypad.right,
window::WindowEvent::PauseToggle => paused = !paused,
window::WindowEvent::Exit => break 'outer,
}
}
if !paused {
let redraw_needed = gameboy.tick();
if redraw_needed {
frame_counter += 1;
if frame_counter == 60 {
let now = chrono::Local::now();
log::info!("Rendered 60 frames in {}", now - last);
last = now;
frame_counter = 0;
tx.send(GameboyEvent::Framebuffer(gameboy.ppu.write_fb())).unwrap();
}
}
}
}
Ok(())
}

29
src/settings.rs Normal file
View file

@ -0,0 +1,29 @@
use winit::event::VirtualKeyCode;
#[derive(Debug, serde::Deserialize, Clone, Copy)]
pub struct DeemgeeConfig {
pub bindings: Bindings,
}
impl DeemgeeConfig {
pub fn from_file() -> Self {
let mut settings = config::Config::default();
settings.merge(config::File::with_name("config")).unwrap();
settings.try_into().expect("Config Error")
}
}
#[derive(Debug, serde::Deserialize, Clone, Copy)]
pub struct Bindings {
pub a: VirtualKeyCode,
pub b: VirtualKeyCode,
pub select: VirtualKeyCode,
pub start: VirtualKeyCode,
pub up: VirtualKeyCode,
pub down: VirtualKeyCode,
pub left: VirtualKeyCode,
pub right: VirtualKeyCode,
pub pause: VirtualKeyCode,
pub exit: VirtualKeyCode,
}

171
src/window.rs Normal file
View file

@ -0,0 +1,171 @@
use std::sync::mpsc::{Receiver, Sender};
use pixels::{Pixels, SurfaceTexture};
use winit::{
event::{Event, VirtualKeyCode},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
};
use winit_input_helper::WinitInputHelper;
use crate::settings::DeemgeeConfig;
macro_rules! define_keypress {
($input:ident, $config:ident, $keymap:ident, $tx:ident, $key:ident, $event:ident) => {
if $input.key_pressed($config.bindings.$key)
&& !*$keymap.idx(&$config, $config.bindings.$key)
{
$tx.send(WindowEvent::$event).unwrap();
*$keymap.idx(&$config, $config.bindings.$key) = true;
}
if $input.key_released($config.bindings.$key)
&& *$keymap.idx(&$config, $config.bindings.$key)
{
$tx.send(WindowEvent::$event).unwrap();
*$keymap.idx(&$config, $config.bindings.$key) = false;
}
};
}
#[derive(Debug, Clone, Copy)]
pub enum WindowEvent {
AToggle,
BToggle,
SelectToggle,
StartToggle,
UpToggle,
DownToggle,
LeftToggle,
RightToggle,
PauseToggle,
Exit,
}
#[derive(Debug)]
pub enum GameboyEvent {
Framebuffer(Vec<u8>),
}
pub const FB_HEIGHT: u32 = 144;
pub const FB_WIDTH: u32 = 160;
#[derive(Debug, Default)]
pub struct Keymap {
pub down: bool,
pub up: bool,
pub left: bool,
pub right: bool,
pub start: bool,
pub select: bool,
pub b: bool,
pub a: bool,
pub pause: bool,
}
impl Keymap {
pub fn idx(&mut self, config: &DeemgeeConfig, kc: VirtualKeyCode) -> &mut bool {
if kc == config.bindings.a {
&mut self.a
} else if kc == config.bindings.b {
&mut self.b
} else if kc == config.bindings.start {
&mut self.start
} else if kc == config.bindings.select {
&mut self.select
} else if kc == config.bindings.up {
&mut self.up
} else if kc == config.bindings.down {
&mut self.down
} else if kc == config.bindings.left {
&mut self.left
} else if kc == config.bindings.right {
&mut self.right
} else if kc == config.bindings.pause {
&mut self.pause
} else {
unreachable!();
}
}
}
pub fn run_window(config: DeemgeeConfig, rx: Receiver<GameboyEvent>, tx: Sender<WindowEvent>) {
let event_loop = EventLoop::new();
let mut input = WinitInputHelper::new();
let window = { WindowBuilder::new().with_title("OwO").build(&event_loop).unwrap() };
let mut pixels = {
let window_size = window.inner_size();
let surface_texture = SurfaceTexture::new(window_size.width, window_size.height, &window);
Pixels::new(FB_WIDTH, FB_HEIGHT, surface_texture).unwrap()
};
let mut redraw_happened = true;
let mut fb: Option<Vec<u8>> = None;
let mut keymap = Keymap::default();
event_loop.run(move |event, _, control_flow| {
if let Event::RedrawRequested(_) = event {
let frame = pixels.get_frame();
match fb.as_ref() {
Some(fb) => {
redraw_happened = true;
frame.copy_from_slice(fb.as_slice());
}
None => {
let x = vec![0xff; frame.len()];
frame.copy_from_slice(x.as_slice())
}
}
if let Err(why) = pixels.render() {
log::error!("Pixels Error: {}", why);
*control_flow = ControlFlow::Exit;
tx.send(WindowEvent::Exit).unwrap();
return;
}
}
if input.update(&event) {
if input.key_pressed(config.bindings.exit) || input.quit() {
*control_flow = ControlFlow::Exit;
tx.send(WindowEvent::Exit).unwrap();
return;
}
if input.key_pressed(config.bindings.pause) {
tx.send(WindowEvent::PauseToggle).unwrap();
}
define_keypress!(input, config, keymap, tx, a, AToggle);
define_keypress!(input, config, keymap, tx, b, BToggle);
define_keypress!(input, config, keymap, tx, start, StartToggle);
define_keypress!(input, config, keymap, tx, select, SelectToggle);
define_keypress!(input, config, keymap, tx, up, UpToggle);
define_keypress!(input, config, keymap, tx, down, DownToggle);
define_keypress!(input, config, keymap, tx, left, LeftToggle);
define_keypress!(input, config, keymap, tx, right, RightToggle);
}
if let Some(size) = input.window_resized() {
pixels.resize_surface(size.width, size.height);
window.request_redraw();
redraw_happened = false;
}
while let Ok(event) = rx.try_recv() {
match event {
GameboyEvent::Framebuffer(buf) => {
fb = Some(buf);
if redraw_happened {
window.request_redraw();
redraw_happened = false;
}
}
}
}
});
}