feat: basic ppu
This commit is contained in:
commit
12f97b8d5b
15 changed files with 3113 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/target
|
||||||
|
/roms/
|
||||||
|
config.toml
|
1913
Cargo.lock
generated
Normal file
1913
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
20
Cargo.toml
Normal file
20
Cargo.toml
Normal 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
11
config.example.toml
Normal 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
7
rustfmt.toml
Normal 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
232
src/gameboy.rs
Normal 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
38
src/gameboy/interrupts.rs
Normal 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
62
src/gameboy/joypad.rs
Normal 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
37
src/gameboy/mapper.rs
Normal 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
13
src/gameboy/memory.rs
Normal 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
365
src/gameboy/ppu.rs
Normal 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
87
src/gameboy/timer.rs
Normal 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
125
src/main.rs
Normal 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
29
src/settings.rs
Normal 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
171
src/window.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue