Compare commits
17 Commits
3c2456611e
...
refactor
Author | SHA1 | Date | |
---|---|---|---|
9baa15050e | |||
4516ca8477 | |||
6087e3b20b | |||
10ac579c40 | |||
ee5504111b | |||
a628f64d28 | |||
318a6e0386 | |||
db012c7f4b | |||
e42c87aeb7 | |||
9113e95fa0 | |||
9973dc8714 | |||
e128025208 | |||
44ac0c8ebd | |||
01064bab69 | |||
634bc2d2c0 | |||
d794a94b68 | |||
b87e31d3f4 |
569
Cargo.lock
generated
569
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ name = "gb"
|
||||
version = "0.1.0"
|
||||
authors = ["Rekai Musuka <rekai@musuka.dev>"]
|
||||
edition = "2018"
|
||||
resolver = "2"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -11,7 +12,7 @@ anyhow = "^1.0"
|
||||
bitfield = "^0.13"
|
||||
clap = "^2.33"
|
||||
gilrs = "^0.8"
|
||||
pixels = "^0.5"
|
||||
pixels = "^0.6"
|
||||
winit = "^0.25"
|
||||
winit_input_helper = "^0.10"
|
||||
rodio = "^0.14"
|
||||
|
35
src/apu.rs
35
src/apu.rs
@@ -468,22 +468,8 @@ impl Channel1 {
|
||||
|
||||
// If this bit is set, a trigger event occurs
|
||||
if self.freq_hi.initial() {
|
||||
// Envelope Behaviour during trigger event
|
||||
self.period_timer = self.envelope.period();
|
||||
self.current_volume = self.envelope.init_vol();
|
||||
|
||||
// Sweep behaviour during trigger event
|
||||
let sweep_period = self.sweep.period();
|
||||
let sweep_shift = self.sweep.shift_count();
|
||||
self.shadow_freq = self.frequency();
|
||||
self.sweep_timer = if sweep_period == 0 { 8 } else { sweep_period };
|
||||
|
||||
if sweep_period != 0 || sweep_shift != 0 {
|
||||
self.sweep_enabled = true;
|
||||
}
|
||||
|
||||
if sweep_shift != 0 {
|
||||
let _ = self.calc_sweep_freq();
|
||||
if self.is_dac_enabled() {
|
||||
self.enabled = true;
|
||||
}
|
||||
|
||||
// Length behaviour during trigger event
|
||||
@@ -491,8 +477,21 @@ impl Channel1 {
|
||||
self.length_timer = 64;
|
||||
}
|
||||
|
||||
if self.is_dac_enabled() {
|
||||
self.enabled = true;
|
||||
// Envelope Behaviour during trigger event
|
||||
self.period_timer = self.envelope.period();
|
||||
self.current_volume = self.envelope.init_vol();
|
||||
|
||||
// Sweep behaviour during trigger event
|
||||
let sweep_period = self.sweep.period();
|
||||
let sweep_shift = self.sweep.shift_count();
|
||||
|
||||
self.shadow_freq = self.frequency();
|
||||
self.sweep_timer = if sweep_period == 0 { 8 } else { sweep_period };
|
||||
|
||||
self.sweep_enabled = sweep_period != 0 || sweep_shift != 0;
|
||||
|
||||
if sweep_shift != 0 {
|
||||
let _ = self.calc_sweep_freq();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
43
src/bus.rs
43
src/bus.rs
@@ -7,14 +7,13 @@ use crate::ppu::{Ppu, PpuMode};
|
||||
use crate::serial::Serial;
|
||||
use crate::timer::Timer;
|
||||
use crate::work_ram::{VariableWorkRam, WorkRam};
|
||||
use std::{fs::File, io::Read};
|
||||
|
||||
const BOOT_ROM_SIZE: usize = 0x100;
|
||||
pub(crate) const BOOT_SIZE: usize = 0x100;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Bus {
|
||||
boot: Option<[u8; BOOT_ROM_SIZE]>, // Boot ROM is 256b long
|
||||
cartridge: Option<Cartridge>,
|
||||
boot: Option<[u8; BOOT_SIZE]>, // Boot ROM is 256b long
|
||||
cart: Option<Cartridge>,
|
||||
pub(crate) ppu: Ppu,
|
||||
work_ram: WorkRam,
|
||||
var_ram: VariableWorkRam,
|
||||
@@ -30,7 +29,7 @@ impl Default for Bus {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
boot: None,
|
||||
cartridge: None,
|
||||
cart: None,
|
||||
ppu: Default::default(),
|
||||
work_ram: Default::default(),
|
||||
var_ram: Default::default(),
|
||||
@@ -45,25 +44,19 @@ impl Default for Bus {
|
||||
}
|
||||
|
||||
impl Bus {
|
||||
pub(crate) fn with_boot(path: &str) -> anyhow::Result<Self> {
|
||||
let mut file = File::open(path)?;
|
||||
let mut boot_rom = [0u8; 256];
|
||||
|
||||
file.read_exact(&mut boot_rom)?;
|
||||
|
||||
Ok(Self {
|
||||
boot: Some(boot_rom),
|
||||
pub(crate) fn with_boot(rom: [u8; 256]) -> Self {
|
||||
Self {
|
||||
boot: Some(rom),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn load_cartridge(&mut self, path: &str) -> std::io::Result<()> {
|
||||
self.cartridge = Some(Cartridge::new(path)?);
|
||||
Ok(())
|
||||
pub(crate) fn load_cart(&mut self, rom: Vec<u8>) {
|
||||
self.cart = Some(Cartridge::new(rom));
|
||||
}
|
||||
|
||||
pub(crate) fn rom_title(&self) -> Option<&str> {
|
||||
self.cartridge.as_ref()?.title()
|
||||
pub(crate) fn cart_title(&self) -> Option<&str> {
|
||||
self.cart.as_ref()?.title()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -104,13 +97,13 @@ impl Bus {
|
||||
}
|
||||
}
|
||||
|
||||
match self.cartridge.as_ref() {
|
||||
match self.cart.as_ref() {
|
||||
Some(cart) => cart.read_byte(addr),
|
||||
None => panic!("Tried to read from a non-existent cartridge"),
|
||||
}
|
||||
}
|
||||
0x8000..=0x9FFF => self.ppu.read_byte(addr), // 8KB Video RAM
|
||||
0xA000..=0xBFFF => match self.cartridge.as_ref() {
|
||||
0xA000..=0xBFFF => match self.cart.as_ref() {
|
||||
// 8KB External RAM
|
||||
Some(cart) => cart.read_byte(addr),
|
||||
None => panic!("Tried to read from a non-existent cartridge"),
|
||||
@@ -157,7 +150,7 @@ impl BusIo for Bus {
|
||||
}
|
||||
}
|
||||
|
||||
match self.cartridge.as_ref() {
|
||||
match self.cart.as_ref() {
|
||||
Some(cart) => cart.read_byte(addr),
|
||||
None => panic!("Tried to read from a non-existent cartridge"),
|
||||
}
|
||||
@@ -169,7 +162,7 @@ impl BusIo for Bus {
|
||||
_ => self.ppu.read_byte(addr),
|
||||
}
|
||||
}
|
||||
0xA000..=0xBFFF => match self.cartridge.as_ref() {
|
||||
0xA000..=0xBFFF => match self.cart.as_ref() {
|
||||
// 8KB External RAM
|
||||
Some(cart) => cart.read_byte(addr),
|
||||
None => panic!("Tried to read from a non-existent cartridge"),
|
||||
@@ -262,7 +255,7 @@ impl BusIo for Bus {
|
||||
0x0000..=0x7FFF => {
|
||||
// 16KB ROM bank 00 (ends at 0x3FFF)
|
||||
// and 16KB ROM Bank 01 -> NN (switchable via MB)
|
||||
match self.cartridge.as_mut() {
|
||||
match self.cart.as_mut() {
|
||||
Some(cart) => cart.write_byte(addr, byte),
|
||||
None => panic!("Tried to write into non-existent cartridge"),
|
||||
}
|
||||
@@ -276,7 +269,7 @@ impl BusIo for Bus {
|
||||
}
|
||||
0xA000..=0xBFFF => {
|
||||
// 8KB External RAM
|
||||
match self.cartridge.as_mut() {
|
||||
match self.cart.as_mut() {
|
||||
Some(cart) => cart.write_byte(addr, byte),
|
||||
None => panic!("Tried to write into non-existent cartridge"),
|
||||
}
|
||||
|
141
src/cartridge.rs
141
src/cartridge.rs
@@ -1,7 +1,3 @@
|
||||
use std::fs::File;
|
||||
use std::io::{self, Read};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::bus::BusIo;
|
||||
|
||||
const RAM_SIZE_ADDRESS: usize = 0x0149;
|
||||
@@ -17,19 +13,15 @@ pub(crate) struct Cartridge {
|
||||
}
|
||||
|
||||
impl Cartridge {
|
||||
pub(crate) fn new<P: AsRef<Path> + ?Sized>(path: &P) -> io::Result<Self> {
|
||||
let mut memory = vec![];
|
||||
let mut rom = File::open(path)?;
|
||||
rom.read_to_end(&mut memory)?;
|
||||
|
||||
pub(crate) fn new(memory: Vec<u8>) -> Self {
|
||||
let title = Self::find_title(&memory);
|
||||
eprintln!("Cartridge Title: {:?}", title);
|
||||
|
||||
Ok(Self {
|
||||
Self {
|
||||
mbc: Self::detect_mbc(&memory),
|
||||
title,
|
||||
memory,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_mbc(memory: &[u8]) -> Box<dyn MBCIo> {
|
||||
@@ -47,6 +39,8 @@ impl Cartridge {
|
||||
MBCKind::None => Box::new(NoMBC),
|
||||
MBCKind::MBC1 => Box::new(MBC1::new(ram_size, rom_size)),
|
||||
MBCKind::MBC1WithBattery => Box::new(MBC1::new(ram_size, rom_size)), // TODO: Implement Saving
|
||||
MBCKind::MBC2 => Box::new(MBC2::new(rom_cap)),
|
||||
MBCKind::MBC2WithBattery => Box::new(MBC2::new(rom_cap)), // TODO: Implement Saving
|
||||
MBCKind::MBC3 => Box::new(MBC3::new(ram_cap)),
|
||||
MBCKind::MBC3WithBattery => Box::new(MBC3::new(ram_cap)), // TODO: Implement Saving
|
||||
MBCKind::MBC5 => Box::new(MBC5::new(ram_cap, rom_cap)),
|
||||
@@ -75,19 +69,23 @@ impl Cartridge {
|
||||
}
|
||||
|
||||
fn detect_rom_info(memory: &[u8]) -> RomSize {
|
||||
let id = dbg!(memory[ROM_SIZE_ADDRESS]);
|
||||
let id = memory[ROM_SIZE_ADDRESS];
|
||||
id.into()
|
||||
}
|
||||
|
||||
fn find_mbc(memory: &[u8]) -> MBCKind {
|
||||
use MBCKind::*;
|
||||
|
||||
match memory[MBC_TYPE_ADDRESS] {
|
||||
0x00 => MBCKind::None,
|
||||
0x01 | 0x02 => MBCKind::MBC1,
|
||||
0x03 => MBCKind::MBC1WithBattery,
|
||||
0x19 | 0x1A => MBCKind::MBC5,
|
||||
0x1B => MBCKind::MBC5WithBattery,
|
||||
0x13 => MBCKind::MBC3WithBattery,
|
||||
0x11 | 0x12 => MBCKind::MBC3,
|
||||
0x00 => None,
|
||||
0x01 | 0x02 => MBC1,
|
||||
0x03 => MBC1WithBattery,
|
||||
0x05 => MBC2,
|
||||
0x06 => MBC2WithBattery,
|
||||
0x19 | 0x1A => MBC5,
|
||||
0x1B => MBC5WithBattery,
|
||||
0x13 => MBC3WithBattery,
|
||||
0x11 | 0x12 => MBC3,
|
||||
id => unimplemented!("id {:#04X} is an unsupported MBC", id),
|
||||
}
|
||||
}
|
||||
@@ -182,16 +180,16 @@ impl MBC1 {
|
||||
}
|
||||
}
|
||||
|
||||
fn ram_addr(&self, addr: u16) -> u16 {
|
||||
fn ram_addr(&self, addr: u16) -> usize {
|
||||
use RamSize::*;
|
||||
|
||||
match self.ram_size {
|
||||
Unused | One => (addr - 0xA000) % self.ram_size.capacity() as u16,
|
||||
Unused | One => (addr as usize - 0xA000) % self.ram_size.capacity(),
|
||||
Four => {
|
||||
if self.mode {
|
||||
0x2000 * self.ram_bank as u16 + (addr - 0xA000)
|
||||
0x2000 * self.ram_bank as usize + (addr as usize - 0xA000)
|
||||
} else {
|
||||
addr - 0xA000
|
||||
addr as usize - 0xA000
|
||||
}
|
||||
}
|
||||
_ => unreachable!("RAM size can not be greater than 32KB on MBC1"),
|
||||
@@ -204,23 +202,17 @@ impl MBCIo for MBC1 {
|
||||
use MBCResult::*;
|
||||
|
||||
match addr {
|
||||
0x0000..=0x3FFF => {
|
||||
if self.mode {
|
||||
Address(0x4000 * self.zero_bank() as usize + addr as usize)
|
||||
} else {
|
||||
Address(addr as usize)
|
||||
}
|
||||
0x0000..=0x3FFF if self.mode => {
|
||||
Address(0x4000 * self.zero_bank() as usize + addr as usize)
|
||||
}
|
||||
0x0000..=0x3FFF => Address(addr as usize),
|
||||
0x4000..=0x7FFF => {
|
||||
Address(0x4000 * self.high_bank() as usize + (addr as usize - 0x4000))
|
||||
}
|
||||
0xA000..=0xBFFF => {
|
||||
if self.mem_enabled {
|
||||
Value(self.memory[self.ram_addr(addr) as usize])
|
||||
} else {
|
||||
Value(0xFF)
|
||||
}
|
||||
0xA000..=0xBFFF if self.mem_enabled && self.ram_size != RamSize::None => {
|
||||
Value(self.memory[self.ram_addr(addr)])
|
||||
}
|
||||
0xA000..=0xBFFF => Value(0xFF),
|
||||
_ => unreachable!("A read from {:#06X} should not be handled by MBC1", addr),
|
||||
}
|
||||
}
|
||||
@@ -229,18 +221,14 @@ impl MBCIo for MBC1 {
|
||||
match addr {
|
||||
0x0000..=0x1FFF => self.mem_enabled = (byte & 0x0F) == 0x0A,
|
||||
0x2000..=0x3FFF => {
|
||||
self.rom_bank = if byte == 0x00 {
|
||||
0x01
|
||||
} else {
|
||||
byte & self.rom_size_mask()
|
||||
};
|
||||
|
||||
self.rom_bank &= 0x1F;
|
||||
let value = byte & 0x1F;
|
||||
let masked_value = byte & self.rom_size_mask();
|
||||
self.rom_bank = if value == 0 { 0x01 } else { masked_value };
|
||||
}
|
||||
0x4000..=0x5FFF => self.ram_bank = byte & 0x03,
|
||||
0x6000..=0x7FFF => self.mode = (byte & 0x01) == 0x01,
|
||||
0xA000..=0xBFFF if self.mem_enabled => {
|
||||
let ram_addr = self.ram_addr(addr) as usize;
|
||||
0xA000..=0xBFFF if self.mem_enabled && self.ram_size != RamSize::None => {
|
||||
let ram_addr = self.ram_addr(addr);
|
||||
self.memory[ram_addr] = byte;
|
||||
}
|
||||
0xA000..=0xBFFF => {} // Ram isn't enabled, ignored write
|
||||
@@ -405,6 +393,67 @@ impl MBCIo for MBC5 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MBC2 {
|
||||
/// 4-bit number
|
||||
rom_bank: u8,
|
||||
memory: Box<[u8; Self::RAM_SIZE]>,
|
||||
mem_enabled: bool,
|
||||
|
||||
rom_cap: usize,
|
||||
}
|
||||
|
||||
impl MBC2 {
|
||||
const RAM_SIZE: usize = 0x0200;
|
||||
|
||||
fn new(rom_cap: usize) -> Self {
|
||||
Self {
|
||||
rom_bank: 0x01,
|
||||
memory: Box::new([0; Self::RAM_SIZE]),
|
||||
mem_enabled: Default::default(),
|
||||
rom_cap,
|
||||
}
|
||||
}
|
||||
|
||||
fn rom_addr(&self, addr: u16) -> usize {
|
||||
(0x4000 * self.rom_bank as usize + (addr as usize - 0x4000)) % self.rom_cap
|
||||
}
|
||||
}
|
||||
|
||||
impl MBCIo for MBC2 {
|
||||
fn handle_read(&self, addr: u16) -> MBCResult {
|
||||
use MBCResult::*;
|
||||
|
||||
match addr {
|
||||
0x0000..=0x3FFF => Address(addr as usize),
|
||||
0x4000..=0x7FFF => Address(self.rom_addr(addr)),
|
||||
0xA000..=0xBFFF if self.mem_enabled => {
|
||||
let mbc2_addr = addr as usize & (Self::RAM_SIZE - 1);
|
||||
Value(self.memory[mbc2_addr] | 0xF0)
|
||||
}
|
||||
0xA000..=0xBFFF => Value(0xFF),
|
||||
_ => unreachable!("A read from {:#06X} should not be handled by MBC2", addr),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_write(&mut self, addr: u16, byte: u8) {
|
||||
let nybble = byte & 0x0F;
|
||||
|
||||
match addr {
|
||||
0x0000..=0x3FFF if addr >> 8 & 0x01 == 0x01 => {
|
||||
self.rom_bank = if nybble == 0x00 { 0x01 } else { nybble };
|
||||
}
|
||||
0x0000..=0x3FFF => self.mem_enabled = nybble == 0x0A,
|
||||
0xA000..=0xBFFF if self.mem_enabled => {
|
||||
let mbc2_addr = addr as usize & (Self::RAM_SIZE - 1);
|
||||
self.memory[mbc2_addr] = nybble;
|
||||
}
|
||||
0x4000..=0x7FFF | 0xA000..=0xBFFF => {}
|
||||
_ => unreachable!("A write to {:#06X} should not be handled by MBC2", addr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct NoMBC;
|
||||
|
||||
@@ -434,6 +483,8 @@ enum MBCKind {
|
||||
None,
|
||||
MBC1,
|
||||
MBC1WithBattery,
|
||||
MBC2,
|
||||
MBC2WithBattery,
|
||||
MBC3,
|
||||
MBC3WithBattery,
|
||||
MBC5,
|
||||
@@ -446,7 +497,7 @@ impl Default for MBCKind {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum RamSize {
|
||||
None = 0x00,
|
||||
Unused = 0x01,
|
||||
|
39
src/cpu.rs
39
src/cpu.rs
@@ -1,10 +1,7 @@
|
||||
use crate::apu::Apu;
|
||||
use crate::bus::{Bus, BusIo};
|
||||
use crate::instruction::cycle::Cycle;
|
||||
use crate::bus::{Bus, BusIo, BOOT_SIZE};
|
||||
use crate::instruction::Instruction;
|
||||
use crate::interrupt::{InterruptEnable, InterruptFlag};
|
||||
use crate::joypad::Joypad;
|
||||
use crate::ppu::Ppu;
|
||||
use crate::Cycle;
|
||||
use bitfield::bitfield;
|
||||
use std::fmt::{Display, Formatter, Result as FmtResult};
|
||||
|
||||
@@ -18,7 +15,7 @@ pub struct Cpu {
|
||||
}
|
||||
|
||||
impl Cpu {
|
||||
pub fn new() -> Self {
|
||||
pub(crate) fn without_boot() -> Self {
|
||||
Self {
|
||||
reg: Registers {
|
||||
a: 0x01,
|
||||
@@ -36,11 +33,11 @@ impl Cpu {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn boot_new(path: &str) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
bus: Bus::with_boot(path)?,
|
||||
pub(crate) fn with_boot(rom: [u8; BOOT_SIZE]) -> Self {
|
||||
Self {
|
||||
bus: Bus::with_boot(rom),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ime(&self) -> ImeState {
|
||||
@@ -72,14 +69,6 @@ impl Cpu {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_cartridge(&mut self, path: &str) -> std::io::Result<()> {
|
||||
self.bus.load_cartridge(path)
|
||||
}
|
||||
|
||||
pub fn rom_title(&self) -> Option<&str> {
|
||||
self.bus.rom_title()
|
||||
}
|
||||
}
|
||||
|
||||
impl Cpu {
|
||||
@@ -133,7 +122,7 @@ impl Cpu {
|
||||
self.bus.clock();
|
||||
|
||||
let elapsed = match kind {
|
||||
ImeEnabled | NonePending => Cycle::new(4),
|
||||
ImeEnabled | NonePending => 4,
|
||||
SomePending => todo!("Implement HALT bug"),
|
||||
};
|
||||
|
||||
@@ -167,16 +156,12 @@ impl BusIo for Cpu {
|
||||
}
|
||||
|
||||
impl Cpu {
|
||||
pub fn ppu(&mut self) -> &Ppu {
|
||||
&self.bus.ppu
|
||||
pub(crate) fn bus(&self) -> &Bus {
|
||||
&self.bus
|
||||
}
|
||||
|
||||
pub fn apu_mut(&mut self) -> &mut Apu {
|
||||
&mut self.bus.apu
|
||||
}
|
||||
|
||||
pub(crate) fn joypad_mut(&mut self) -> &mut Joypad {
|
||||
&mut self.bus.joypad
|
||||
pub(crate) fn bus_mut(&mut self) -> &mut Bus {
|
||||
&mut self.bus
|
||||
}
|
||||
|
||||
fn handle_ei(&mut self) {
|
||||
|
158
src/emu.rs
158
src/emu.rs
@@ -1,73 +1,121 @@
|
||||
use crate::cpu::Cpu as SM83;
|
||||
use crate::instruction::cycle::Cycle;
|
||||
use crate::joypad;
|
||||
use crate::ppu::Ppu;
|
||||
use anyhow::Result;
|
||||
use crate::apu::gen::SampleProducer;
|
||||
use crate::cpu::Cpu;
|
||||
use crate::joypad::{self, Joypad};
|
||||
use crate::{Cycle, GB_HEIGHT, GB_WIDTH};
|
||||
use gilrs::Gilrs;
|
||||
use std::time::Duration;
|
||||
use winit_input_helper::WinitInputHelper;
|
||||
|
||||
pub const SM83_CYCLE_TIME: Duration = Duration::from_nanos(1_000_000_000 / SM83_CLOCK_SPEED);
|
||||
pub const CYCLES_IN_FRAME: Cycle = Cycle::new(456 * 154); // 456 Cycles times 154 scanlines
|
||||
pub const CYCLES_IN_FRAME: Cycle = 456 * 154; // 456 Cycles times 154 scanlines
|
||||
pub(crate) const SM83_CLOCK_SPEED: u64 = 0x40_0000; // Hz which is 4.194304Mhz
|
||||
const DEFAULT_TITLE: &str = "DMG-01 Emulator";
|
||||
const GAMEPAD_ENABLED: bool = true;
|
||||
|
||||
pub fn init(boot_path: Option<&str>, rom_path: &str) -> Result<SM83> {
|
||||
let mut cpu = match boot_path {
|
||||
Some(path) => SM83::boot_new(path)?,
|
||||
None => SM83::new(),
|
||||
};
|
||||
pub fn run_frame(emu: &mut Emulator, gamepad: &mut Gilrs, key: &WinitInputHelper) -> Cycle {
|
||||
let mut elapsed = 0;
|
||||
|
||||
eprintln!("Initialized GB Emulator");
|
||||
|
||||
cpu.load_cartridge(rom_path)?;
|
||||
Ok(cpu)
|
||||
}
|
||||
|
||||
pub fn rom_title(game_boy: &SM83) -> &str {
|
||||
game_boy.rom_title().unwrap_or(DEFAULT_TITLE)
|
||||
}
|
||||
|
||||
pub fn run(
|
||||
game_boy: &mut SM83,
|
||||
gamepad: &mut Gilrs,
|
||||
input: &WinitInputHelper,
|
||||
target: Cycle,
|
||||
) -> Cycle {
|
||||
let mut elapsed = Cycle::new(0);
|
||||
|
||||
if GAMEPAD_ENABLED {
|
||||
if let Some(event) = gamepad.next_event() {
|
||||
joypad::handle_gamepad_input(game_boy.joypad_mut(), event);
|
||||
}
|
||||
if let Some(event) = gamepad.next_event() {
|
||||
joypad::handle_gamepad_input(emu.joyp_mut(), event);
|
||||
}
|
||||
|
||||
joypad::handle_keyboard_input(game_boy.joypad_mut(), input);
|
||||
while elapsed < target {
|
||||
elapsed += game_boy.step();
|
||||
}
|
||||
|
||||
elapsed
|
||||
}
|
||||
|
||||
pub fn run_frame(game_boy: &mut SM83, gamepad: &mut Gilrs, input: &WinitInputHelper) -> Cycle {
|
||||
let mut elapsed = Cycle::new(0);
|
||||
|
||||
if GAMEPAD_ENABLED {
|
||||
if let Some(event) = gamepad.next_event() {
|
||||
joypad::handle_gamepad_input(game_boy.joypad_mut(), event);
|
||||
}
|
||||
}
|
||||
|
||||
joypad::handle_keyboard_input(game_boy.joypad_mut(), input);
|
||||
joypad::handle_keyboard_input(emu.joyp_mut(), key);
|
||||
while elapsed < CYCLES_IN_FRAME {
|
||||
elapsed += game_boy.step();
|
||||
elapsed += emu.step();
|
||||
}
|
||||
|
||||
elapsed
|
||||
}
|
||||
|
||||
pub fn draw(ppu: &Ppu, frame: &mut [u8]) {
|
||||
ppu.copy_to_gui(frame);
|
||||
pub fn draw_frame(emu: &Emulator, buf: &mut [u8; GB_HEIGHT * GB_WIDTH * 4]) {
|
||||
buf.copy_from_slice(emu.cpu.bus().ppu.frame_buf());
|
||||
}
|
||||
|
||||
pub struct Emulator {
|
||||
cpu: Cpu,
|
||||
timestamp: Cycle,
|
||||
}
|
||||
|
||||
impl Emulator {
|
||||
fn new(cpu: Cpu) -> Self {
|
||||
Self {
|
||||
cpu,
|
||||
timestamp: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn step(&mut self) -> Cycle {
|
||||
self.cpu.step()
|
||||
}
|
||||
|
||||
fn load_cart(&mut self, rom: Vec<u8>) {
|
||||
self.cpu.bus_mut().load_cart(rom)
|
||||
}
|
||||
|
||||
fn joyp_mut(&mut self) -> &mut Joypad {
|
||||
&mut self.cpu.bus_mut().joypad
|
||||
}
|
||||
|
||||
pub fn set_prod(&mut self, prod: SampleProducer<f32>) {
|
||||
self.cpu.bus_mut().apu.attach_producer(prod)
|
||||
}
|
||||
|
||||
pub fn title(&self) -> &str {
|
||||
self.cpu.bus().cart_title().unwrap_or(DEFAULT_TITLE)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod build {
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Result};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::bus::BOOT_SIZE;
|
||||
use crate::cpu::Cpu;
|
||||
|
||||
use super::Emulator;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct EmulatorBuilder {
|
||||
boot: Option<[u8; BOOT_SIZE]>,
|
||||
cart: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl EmulatorBuilder {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn with_boot<P: AsRef<Path>>(mut self, path: P) -> Result<Self> {
|
||||
let mut file = File::open(path.as_ref())?;
|
||||
|
||||
let mut buf = [0x00; BOOT_SIZE];
|
||||
file.read_exact(&mut buf)?;
|
||||
|
||||
self.boot = Some(buf);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn with_cart<P: AsRef<Path>>(mut self, path: P) -> Result<Self> {
|
||||
let mut file = File::open(path.as_ref())?;
|
||||
|
||||
let mut buf = Vec::new();
|
||||
file.read_to_end(&mut buf)?;
|
||||
|
||||
self.cart = Some(buf);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn finish(mut self) -> Emulator {
|
||||
let mut emu = Emulator::new(match self.boot {
|
||||
Some(rom) => Cpu::with_boot(rom),
|
||||
None => Cpu::without_boot(),
|
||||
});
|
||||
|
||||
if let Some(rom) = self.cart.take() {
|
||||
emu.load_cart(rom)
|
||||
}
|
||||
|
||||
emu
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
pub use apu::gen::AudioSPSC;
|
||||
pub use instruction::cycle::Cycle;
|
||||
pub type Cycle = u64;
|
||||
|
||||
pub const GB_WIDTH: usize = 160;
|
||||
pub const GB_HEIGHT: usize = 144;
|
||||
|
88
src/main.rs
88
src/main.rs
@@ -1,18 +1,20 @@
|
||||
use std::convert::TryInto;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg};
|
||||
use gb::emu::build::EmulatorBuilder;
|
||||
use gb::{AudioSPSC, Cycle, GB_HEIGHT, GB_WIDTH};
|
||||
use gilrs::Gilrs;
|
||||
use pixels::{PixelsBuilder, SurfaceTexture};
|
||||
use rodio::{OutputStream, Sink};
|
||||
use std::time::{Duration, Instant};
|
||||
use winit::dpi::LogicalSize;
|
||||
use winit::dpi::{LogicalSize, PhysicalSize};
|
||||
use winit::event::{Event, VirtualKeyCode};
|
||||
use winit::event_loop::{ControlFlow, EventLoop};
|
||||
use winit::window::{Window, WindowBuilder};
|
||||
use winit_input_helper::WinitInputHelper;
|
||||
|
||||
const WINDOW_SCALE: f64 = 2.0;
|
||||
const AUDIO_ENABLED: bool = true;
|
||||
const WINDOW_SCALE: usize = 3;
|
||||
const AUDIO_ENABLED: bool = false;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let app = App::new(crate_name!())
|
||||
@@ -39,13 +41,15 @@ fn main() -> Result<()> {
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
let rom_path = m
|
||||
.value_of("rom")
|
||||
.expect("Required value 'rom' was provided");
|
||||
let mut emu_build =
|
||||
EmulatorBuilder::new().with_cart(m.value_of("rom").expect("ROM path provided"))?;
|
||||
|
||||
let mut game_boy =
|
||||
gb::emu::init(m.value_of("boot"), rom_path).expect("Initialize DMG-01 Emulator");
|
||||
let rom_title = gb::emu::rom_title(&game_boy);
|
||||
if let Some(path) = m.value_of("boot") {
|
||||
emu_build = emu_build.with_boot(path)?;
|
||||
}
|
||||
|
||||
let mut emu = emu_build.finish();
|
||||
let rom_title = emu.title();
|
||||
|
||||
let mut gamepad = Gilrs::new().expect("Initialize Controller Support");
|
||||
|
||||
@@ -69,17 +73,20 @@ fn main() -> Result<()> {
|
||||
if AUDIO_ENABLED {
|
||||
let spsc: AudioSPSC<f32> = Default::default();
|
||||
let (prod, cons) = spsc.init();
|
||||
let sink = Sink::try_new(&stream_handle)?;
|
||||
sink.append(cons);
|
||||
game_boy.apu_mut().attach_producer(prod);
|
||||
let sink = {
|
||||
let s = Sink::try_new(&stream_handle)?;
|
||||
s.append(cons);
|
||||
s.set_volume(0.1);
|
||||
s
|
||||
};
|
||||
|
||||
emu.set_prod(prod);
|
||||
|
||||
std::thread::spawn(move || {
|
||||
sink.sleep_until_end();
|
||||
});
|
||||
}
|
||||
|
||||
let mut start = Instant::now();
|
||||
let frame_time = Duration::from_secs_f64(1.0 / 59.73); // 59.73 Hz on Host
|
||||
let mut cycle_count: Cycle = Default::default();
|
||||
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
@@ -104,57 +111,34 @@ fn main() -> Result<()> {
|
||||
pixels.resize_surface(size.width, size.height);
|
||||
}
|
||||
|
||||
let mut diff = Instant::now() - start;
|
||||
while diff.subsec_nanos() < frame_time.subsec_nanos() {
|
||||
if cycle_count < gb::emu::CYCLES_IN_FRAME {
|
||||
cycle_count += gb::emu::run_frame(&mut game_boy, &mut gamepad, &input);
|
||||
}
|
||||
|
||||
diff = Instant::now() - start;
|
||||
}
|
||||
start = Instant::now();
|
||||
cycle_count += gb::emu::run_frame(&mut emu, &mut gamepad, &input);
|
||||
|
||||
if cycle_count >= gb::emu::CYCLES_IN_FRAME {
|
||||
cycle_count %= gb::emu::CYCLES_IN_FRAME;
|
||||
|
||||
gb::emu::draw(game_boy.ppu(), pixels.get_frame());
|
||||
let buf: &mut [u8; GB_WIDTH * GB_HEIGHT * 4] = pixels
|
||||
.get_frame()
|
||||
.try_into()
|
||||
.expect("Size of Pixel Buffer is GB_WIDTH * GB_HEIGHT * 4");
|
||||
|
||||
gb::emu::draw_frame(&emu, buf);
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn create_window(event_loop: &EventLoop<()>, title: &str) -> Result<Window> {
|
||||
let size = LogicalSize::new(
|
||||
(GB_WIDTH as f64) * WINDOW_SCALE,
|
||||
(GB_HEIGHT as f64) * WINDOW_SCALE,
|
||||
let logical = LogicalSize::new(GB_WIDTH as f64, GB_HEIGHT as f64);
|
||||
let physical = PhysicalSize::new(
|
||||
(GB_WIDTH * WINDOW_SCALE) as f32,
|
||||
(GB_HEIGHT * WINDOW_SCALE) as f32,
|
||||
);
|
||||
|
||||
Ok(WindowBuilder::new()
|
||||
.with_title(title)
|
||||
.with_inner_size(size)
|
||||
.with_min_inner_size(size)
|
||||
.with_min_inner_size(logical)
|
||||
.with_inner_size(physical)
|
||||
.with_resizable(true)
|
||||
.with_decorations(true)
|
||||
.with_transparent(false)
|
||||
.build(event_loop)?)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn create_window(event_loop: &EventLoop<()>, title: &str) -> Result<Window> {
|
||||
use winit::platform::windows::WindowBuilderExtWindows;
|
||||
|
||||
let size = LogicalSize::new(
|
||||
(GB_WIDTH as f64) * WINDOW_SCALE,
|
||||
(GB_HEIGHT as f64) * WINDOW_SCALE,
|
||||
);
|
||||
Ok(WindowBuilder::new()
|
||||
.with_title(title)
|
||||
.with_inner_size(size)
|
||||
.with_min_inner_size(size)
|
||||
.with_resizable(true)
|
||||
.with_decorations(true)
|
||||
.with_transparent(false)
|
||||
.with_drag_and_drop(false)
|
||||
.build(event_loop)?)
|
||||
}
|
||||
|
18
src/ppu.rs
18
src/ppu.rs
@@ -1,5 +1,5 @@
|
||||
use crate::bus::BusIo;
|
||||
use crate::instruction::cycle::Cycle;
|
||||
use crate::Cycle;
|
||||
use crate::GB_HEIGHT;
|
||||
use crate::GB_WIDTH;
|
||||
use dma::DirectMemoryAccess;
|
||||
@@ -79,7 +79,7 @@ impl Ppu {
|
||||
|
||||
match self.stat.mode() {
|
||||
PpuMode::OamScan => {
|
||||
if self.cycle >= 80.into() {
|
||||
if self.cycle >= 80 {
|
||||
self.stat.set_mode(PpuMode::Drawing);
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ impl Ppu {
|
||||
PpuMode::Drawing => {
|
||||
if self.ctrl.lcd_enabled() {
|
||||
// Only Draw when the LCD Is Enabled
|
||||
self.draw(self.cycle.into());
|
||||
self.draw(self.cycle);
|
||||
} else {
|
||||
self.reset();
|
||||
}
|
||||
@@ -125,7 +125,7 @@ impl Ppu {
|
||||
PpuMode::HBlank => {
|
||||
// This mode will always end at 456 cycles
|
||||
|
||||
if self.cycle >= 456.into() {
|
||||
if self.cycle >= 456 {
|
||||
self.cycle %= 456;
|
||||
self.pos.line_y += 1;
|
||||
|
||||
@@ -167,7 +167,7 @@ impl Ppu {
|
||||
}
|
||||
}
|
||||
PpuMode::VBlank => {
|
||||
if self.cycle > 456.into() {
|
||||
if self.cycle > 456 {
|
||||
self.cycle %= 456;
|
||||
self.pos.line_y += 1;
|
||||
|
||||
@@ -228,7 +228,7 @@ impl Ppu {
|
||||
self.scan_state.next();
|
||||
}
|
||||
|
||||
fn draw(&mut self, _cycle: u32) {
|
||||
fn draw(&mut self, _cycle: Cycle) {
|
||||
use FetcherState::*;
|
||||
|
||||
let mut iter = self.obj_buffer.iter_mut();
|
||||
@@ -418,8 +418,8 @@ impl Ppu {
|
||||
self.frame_buf.swap_with_slice(&mut blank);
|
||||
}
|
||||
|
||||
pub fn copy_to_gui(&self, frame: &mut [u8]) {
|
||||
frame.copy_from_slice(self.frame_buf.as_ref());
|
||||
pub(crate) fn frame_buf(&self) -> &[u8; GB_HEIGHT * GB_WIDTH * 4] {
|
||||
&self.frame_buf
|
||||
}
|
||||
|
||||
fn clock_fifo(&mut self) -> Option<GrayShade> {
|
||||
@@ -470,7 +470,7 @@ impl Default for Ppu {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
vram: Box::new([0u8; VRAM_SIZE]),
|
||||
cycle: Cycle::new(0),
|
||||
cycle: Default::default(),
|
||||
frame_buf: Box::new([0; GB_WIDTH * GB_HEIGHT * 4]),
|
||||
int: Default::default(),
|
||||
ctrl: Default::default(),
|
||||
|
@@ -1,4 +1,4 @@
|
||||
use crate::instruction::cycle::Cycle;
|
||||
use crate::Cycle;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct DirectMemoryAccess {
|
||||
@@ -56,7 +56,7 @@ impl DirectMemoryAccess {
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.cycle = Cycle::new(0);
|
||||
self.cycle = 0;
|
||||
self.state = DmaState::Disabled;
|
||||
self.start.0 = None;
|
||||
}
|
||||
|
79
src/scheduler.rs
Normal file
79
src/scheduler.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use crate::Cycle;
|
||||
use std::collections::BinaryHeap;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Scheduler {
|
||||
timestamp: Cycle,
|
||||
queue: BinaryHeap<Event>,
|
||||
}
|
||||
|
||||
impl Scheduler {
|
||||
pub(crate) fn init() -> Self {
|
||||
let mut scheduler = Self {
|
||||
timestamp: Default::default(),
|
||||
queue: Default::default(),
|
||||
};
|
||||
|
||||
scheduler.push(Event {
|
||||
kind: EventKind::TimestampOverflow,
|
||||
timestamp: Cycle::MAX,
|
||||
cb: |_delay| panic!("Reached Cycle::MAX"),
|
||||
});
|
||||
|
||||
scheduler
|
||||
}
|
||||
|
||||
pub(crate) fn push(&mut self, event: Event) {
|
||||
self.queue.push(event);
|
||||
}
|
||||
|
||||
pub(crate) fn step(&mut self, cycles: Cycle) {
|
||||
self.timestamp += cycles;
|
||||
|
||||
loop {
|
||||
let should_pop = match self.queue.peek() {
|
||||
Some(event) => self.timestamp >= event.timestamp,
|
||||
None => false,
|
||||
};
|
||||
|
||||
if !should_pop {
|
||||
break;
|
||||
}
|
||||
|
||||
let event = self.queue.pop().expect("Pop Event from Scheduler Queue");
|
||||
|
||||
(event.cb)(self.timestamp - event.timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Event {
|
||||
kind: EventKind,
|
||||
cb: fn(Cycle),
|
||||
pub(crate) timestamp: Cycle,
|
||||
}
|
||||
|
||||
impl Eq for Event {}
|
||||
impl PartialEq for Event {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.kind == other.kind && self.timestamp == other.timestamp
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Event {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Event {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.timestamp.cmp(&other.timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum EventKind {
|
||||
TimestampOverflow,
|
||||
}
|
37
src/timer.rs
37
src/timer.rs
@@ -21,17 +21,15 @@ impl Timer {
|
||||
use State::*;
|
||||
use TimerSpeed::*;
|
||||
|
||||
if let TIMAOverflow(step) | AbortedTIMAOverflow(step) = self.state {
|
||||
if step < 4 {
|
||||
self.state = TIMAOverflow(step + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if self.state == TIMAOverflow(step) {
|
||||
match self.state {
|
||||
TIMAOverflow(_) | AbortedTIMAOverflow(_) => self.next(),
|
||||
LoadTMA => {
|
||||
self.counter = self.modulo;
|
||||
self.interrupt = true;
|
||||
|
||||
self.next();
|
||||
}
|
||||
self.state = Normal;
|
||||
Normal => {}
|
||||
}
|
||||
|
||||
self.divider = self.divider.wrapping_add(1);
|
||||
@@ -67,14 +65,12 @@ impl Timer {
|
||||
use State::*;
|
||||
|
||||
match self.state {
|
||||
Normal => self.counter = byte,
|
||||
Normal | AbortedTIMAOverflow(_) => self.counter = byte,
|
||||
TIMAOverflow(step) => {
|
||||
if step < 4 {
|
||||
self.counter = byte;
|
||||
self.state = AbortedTIMAOverflow(step);
|
||||
}
|
||||
self.counter = byte;
|
||||
self.state = AbortedTIMAOverflow(step);
|
||||
}
|
||||
AbortedTIMAOverflow(_) => self.counter = byte,
|
||||
LoadTMA => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +90,18 @@ impl Timer {
|
||||
self.state = State::TIMAOverflow(0);
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self) {
|
||||
use State::*;
|
||||
|
||||
self.state = match self.state {
|
||||
Normal | LoadTMA => Normal,
|
||||
AbortedTIMAOverflow(4) => Normal,
|
||||
TIMAOverflow(4) => LoadTMA,
|
||||
AbortedTIMAOverflow(step) => AbortedTIMAOverflow(step + 1),
|
||||
TIMAOverflow(step) => TIMAOverflow(step + 1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Timer {
|
||||
@@ -173,4 +181,5 @@ enum State {
|
||||
TIMAOverflow(u8),
|
||||
AbortedTIMAOverflow(u8),
|
||||
Normal,
|
||||
LoadTMA,
|
||||
}
|
||||
|
Reference in New Issue
Block a user