Compare commits
1 Commits
main
...
0f8ee3990c
Author | SHA1 | Date | |
---|---|---|---|
0f8ee3990c |
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -1,9 +1,8 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"rust-lang.rust-analyzer",
|
"matklad.rust-analyzer",
|
||||||
"tamasfe.even-better-toml",
|
"tamasfe.even-better-toml",
|
||||||
"serayuzgur.crates",
|
"serayuzgur.crates",
|
||||||
"vadimcn.vscode-lldb",
|
"vadimcn.vscode-lldb",
|
||||||
"donaldhays.rgbds-z80"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
2586
Cargo.lock
generated
2586
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
24
Cargo.toml
24
Cargo.toml
@@ -8,21 +8,21 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
bitfield = "0.17"
|
bitfield = "0.13"
|
||||||
clap = { version = "4.0", features = ["cargo"] }
|
clap = "2.33"
|
||||||
gilrs = "0.11"
|
gilrs = "0.8"
|
||||||
winit = "0.28"
|
winit = "0.25"
|
||||||
egui = "0.21"
|
egui = "0.15"
|
||||||
egui_wgpu_backend = "0.22"
|
wgpu = "0.11"
|
||||||
egui_winit_platform = "0.18"
|
egui_wgpu_backend = "0.14"
|
||||||
pollster = "0.3"
|
egui_winit_platform = "0.11"
|
||||||
rodio = "0.19"
|
pollster = "0.2"
|
||||||
rtrb = "0.3"
|
rodio = "0.14"
|
||||||
|
rtrb = "0.2"
|
||||||
directories-next = "2.0"
|
directories-next = "2.0"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["std", "env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["std", "env-filter"] }
|
||||||
thiserror = "1.0"
|
thiserror = "1.0.30"
|
||||||
once_cell = "1.10"
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
debug = true
|
debug = true
|
||||||
|
11
README.md
11
README.md
@@ -1,9 +1,10 @@
|
|||||||
# Rekai's Gameboy Emulator
|
# Rekai's Gameboy Emulator
|
||||||
|
[](https://ci.paoda.moe/paoda/gb)
|
||||||
|
|
||||||
### Status
|
### Status
|
||||||
* From [Blargg Test ROMs](https://github.com/L-P/blargg-test-roms/)
|
* From [Blargg Test ROMs](https://github.com/L-P/blargg-test-roms/)
|
||||||
* [x] cpu_instrs
|
* [x] cpu_instrs
|
||||||
* [x] instr_timing
|
* [ ] instr_timing (kind of)
|
||||||
* [x] mem_timing
|
* [x] mem_timing
|
||||||
* [x] mem_timing-2
|
* [x] mem_timing-2
|
||||||
* [ ] dmg_sound (partial)
|
* [ ] dmg_sound (partial)
|
||||||
@@ -20,15 +21,9 @@
|
|||||||
Supports: ROM-only, MBC1, MBC2, MBC3 and MBC5 games.
|
Supports: ROM-only, MBC1, MBC2, MBC3 and MBC5 games.
|
||||||
|
|
||||||
|
|
||||||
### Compiling
|
|
||||||
This project was last successfully built on [Rust 1.64.0](https://github.com/rust-lang/rust/tree/1.64.0)
|
|
||||||
|
|
||||||
1. `git clone https://github.com/paoda/gb`
|
|
||||||
2. `cd gb`
|
|
||||||
3. `cargo run --release`
|
|
||||||
|
|
||||||
### Controls
|
### Controls
|
||||||
Controls are defined [here](https://github.com/paoda/gb/blob/85940c874460b9cb31bf9b8211bf7dda596d114a/src/joypad.rs#L114)
|
Controls are defined [here](https://git.musuka.dev/paoda/gb/src/branch/main/src/joypad.rs#L114)
|
||||||
|
|
||||||
Key | Button
|
Key | Button
|
||||||
--- | ---
|
--- | ---
|
||||||
|
@@ -169,7 +169,7 @@ pub(super) mod ch4 {
|
|||||||
match byte & 0x01 {
|
match byte & 0x01 {
|
||||||
0b00 => Self::Short,
|
0b00 => Self::Short,
|
||||||
0b01 => Self::Long,
|
0b01 => Self::Long,
|
||||||
_ => unreachable!("{:#04X} is not a valid value for CounterWidth", byte),
|
_ => unreachable!("{:#04X} is not a valid value for CounterWidth"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
20
src/bus.rs
20
src/bus.rs
@@ -10,7 +10,7 @@ use crate::work_ram::{VariableWorkRam, WorkRam};
|
|||||||
|
|
||||||
pub(crate) const BOOT_SIZE: usize = 0x100;
|
pub(crate) const BOOT_SIZE: usize = 0x100;
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug)]
|
||||||
pub struct Bus {
|
pub struct Bus {
|
||||||
boot: Option<[u8; BOOT_SIZE]>, // Boot ROM is 256b long
|
boot: Option<[u8; BOOT_SIZE]>, // Boot ROM is 256b long
|
||||||
pub(crate) cart: Option<Cartridge>,
|
pub(crate) cart: Option<Cartridge>,
|
||||||
@@ -25,6 +25,24 @@ pub struct Bus {
|
|||||||
pub(crate) joyp: Joypad,
|
pub(crate) joyp: Joypad,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Bus {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
boot: None,
|
||||||
|
cart: None,
|
||||||
|
ppu: Default::default(),
|
||||||
|
work_ram: Default::default(),
|
||||||
|
var_ram: Default::default(),
|
||||||
|
timer: Default::default(),
|
||||||
|
int: Default::default(),
|
||||||
|
apu: Default::default(),
|
||||||
|
high_ram: Default::default(),
|
||||||
|
serial: Default::default(),
|
||||||
|
joyp: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Bus {
|
impl Bus {
|
||||||
pub(crate) fn with_boot(rom: [u8; 256]) -> Self {
|
pub(crate) fn with_boot(rom: [u8; 256]) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
289
src/cartridge.rs
289
src/cartridge.rs
@@ -15,7 +15,7 @@ const ROM_MANUFACTURER_START: usize = 0x13F;
|
|||||||
pub(crate) struct Cartridge {
|
pub(crate) struct Cartridge {
|
||||||
mem: Vec<u8>,
|
mem: Vec<u8>,
|
||||||
pub(crate) title: Option<String>,
|
pub(crate) title: Option<String>,
|
||||||
mbc: Box<dyn MbcIo>,
|
mbc: Box<dyn MBCIo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cartridge {
|
impl Cartridge {
|
||||||
@@ -44,7 +44,7 @@ impl Cartridge {
|
|||||||
self.mbc.tick()
|
self.mbc.tick()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn detect_mbc(mem: &[u8]) -> Box<dyn MbcIo> {
|
fn detect_mbc(mem: &[u8]) -> Box<dyn MBCIo> {
|
||||||
let ram_size: RamSize = mem[RAM_SIZE_ADDRESS].into();
|
let ram_size: RamSize = mem[RAM_SIZE_ADDRESS].into();
|
||||||
let rom_size: RomSize = mem[ROM_SIZE_ADDRESS].into();
|
let rom_size: RomSize = mem[ROM_SIZE_ADDRESS].into();
|
||||||
let mbc_kind = Self::detect_mbc_kind(mem[MBC_KIND_ADDRESS]);
|
let mbc_kind = Self::detect_mbc_kind(mem[MBC_KIND_ADDRESS]);
|
||||||
@@ -56,64 +56,61 @@ impl Cartridge {
|
|||||||
tracing::info!("MBC kind: {:?}", mbc_kind);
|
tracing::info!("MBC kind: {:?}", mbc_kind);
|
||||||
|
|
||||||
match mbc_kind {
|
match mbc_kind {
|
||||||
MbcKind::None => Box::new(NoMbc),
|
MBCKind::None => Box::new(NoMBC),
|
||||||
MbcKind::Mbc1(hw) => Box::new(Mbc1::new(hw, ram_size, rom_size)),
|
MBCKind::MBC1(hw) => Box::new(MBC1::new(hw, ram_size, rom_size)),
|
||||||
MbcKind::Mbc2(hw) => Box::new(Mbc2::new(hw, rom_cap)),
|
MBCKind::MBC2(hw) => Box::new(MBC2::new(hw, rom_cap)),
|
||||||
MbcKind::Mbc3(hw @ Mbc3Hardware::Rtc) => Box::new(Mbc3::new(hw, ram_cap)),
|
MBCKind::MBC3(hw @ MBC3Hardware::RTC) => Box::new(MBC3::new(hw, ram_cap)),
|
||||||
MbcKind::Mbc3(hw @ Mbc3Hardware::RtcBatteryRam) => Box::new(Mbc3::new(hw, ram_cap)),
|
MBCKind::MBC3(hw @ MBC3Hardware::RTCAndBatteryRAM) => Box::new(MBC3::new(hw, ram_cap)),
|
||||||
MbcKind::Mbc3(hw @ Mbc3Hardware::BatteryRam) => Box::new(Mbc3::new(hw, ram_cap)),
|
MBCKind::MBC5(hw @ MBC5Hardware::None) => Box::new(MBC5::new(hw, ram_cap, rom_cap)),
|
||||||
MbcKind::Mbc5(hw @ Mbc5Hardware::None) => Box::new(Mbc5::new(hw, ram_cap, rom_cap)),
|
MBCKind::MBC5(hw @ MBC5Hardware::BatteryRAM) => {
|
||||||
MbcKind::Mbc5(hw @ Mbc5Hardware::BatteryRam) => {
|
Box::new(MBC5::new(hw, ram_cap, rom_cap))
|
||||||
Box::new(Mbc5::new(hw, ram_cap, rom_cap))
|
|
||||||
}
|
}
|
||||||
kind => todo!("ROMS with {:?} are currently unsupported", kind),
|
kind => todo!("ROMS with {:?} are currently unsupported", kind),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn detect_title(title_mem: &[u8; ROM_TITLE_MAX_SIZE]) -> Option<String> {
|
fn detect_title(title_mem: &[u8; ROM_TITLE_MAX_SIZE]) -> Option<String> {
|
||||||
use std::str::from_utf8;
|
|
||||||
|
|
||||||
const ALT_TITLE_LEN: usize = ROM_MANUFACTURER_START - ROM_TITLE_START;
|
const ALT_TITLE_LEN: usize = ROM_MANUFACTURER_START - ROM_TITLE_START;
|
||||||
|
|
||||||
// ascii byte slie does not have a null-terminator
|
// byte slice we have here is purposely not null terminated
|
||||||
let ascii = match title_mem.iter().position(|b| *b == 0x00) {
|
let ascii = match title_mem.iter().position(|b| *b == 0x00) {
|
||||||
Some(end) => &title_mem[..end],
|
Some(end) => &title_mem[0..end],
|
||||||
None => &title_mem[..ROM_TITLE_MAX_SIZE],
|
None => &title_mem[0..ROM_TITLE_MAX_SIZE],
|
||||||
};
|
};
|
||||||
|
|
||||||
match from_utf8(ascii).map(str::trim).ok() {
|
match std::str::from_utf8(ascii).ok() {
|
||||||
None => match from_utf8(&title_mem[..ALT_TITLE_LEN]).map(str::trim).ok() {
|
None => match std::str::from_utf8(&title_mem[0..ALT_TITLE_LEN]).ok() {
|
||||||
Some("") | None => None,
|
Some("") | None => None,
|
||||||
Some(title) => Some(String::from(title)),
|
Some(title) => Some(String::from(title.trim())),
|
||||||
},
|
},
|
||||||
Some("") => None,
|
Some("") => None,
|
||||||
Some(title) => Some(String::from(title)),
|
Some(title) => Some(String::from(title.trim())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn detect_mbc_kind(id: u8) -> MbcKind {
|
fn detect_mbc_kind(id: u8) -> MBCKind {
|
||||||
use MbcKind::*;
|
use MBCKind::*;
|
||||||
|
|
||||||
match id {
|
match id {
|
||||||
0x00 => None,
|
0x00 => None,
|
||||||
0x01 => Mbc1(Mbc1Hardware::None),
|
0x01 => MBC1(MBC1Hardware::None),
|
||||||
0x02 => Mbc1(Mbc1Hardware::Ram),
|
0x02 => MBC1(MBC1Hardware::RAM),
|
||||||
0x03 => Mbc1(Mbc1Hardware::BatteryRam),
|
0x03 => MBC1(MBC1Hardware::BatteryRAM),
|
||||||
0x05 => Mbc2(Mbc2Hardware::None),
|
0x05 => MBC2(MBC2Hardware::None),
|
||||||
0x06 => Mbc2(Mbc2Hardware::BatteryRam),
|
0x06 => MBC2(MBC2Hardware::BatteryRAM),
|
||||||
0x08 | 0x09 => unimplemented!("NoMBC + RAM and NoMBC + Battery unsupported"),
|
0x08 | 0x09 => unimplemented!("NoMBC + RAM and NoMBC + Battery unsupported"),
|
||||||
0x0B | 0x0C | 0x0D => unimplemented!("MM01 unsupported"),
|
0x0B | 0x0C | 0x0D => unimplemented!("MM01 unsupported"),
|
||||||
0x0F => Mbc3(Mbc3Hardware::Rtc),
|
0x0F => MBC3(MBC3Hardware::RTC),
|
||||||
0x10 => Mbc3(Mbc3Hardware::RtcBatteryRam),
|
0x10 => MBC3(MBC3Hardware::RTCAndBatteryRAM),
|
||||||
0x11 => Mbc3(Mbc3Hardware::None),
|
0x11 => MBC3(MBC3Hardware::None),
|
||||||
0x12 => Mbc3(Mbc3Hardware::Ram),
|
0x12 => MBC3(MBC3Hardware::RAM),
|
||||||
0x13 => Mbc3(Mbc3Hardware::BatteryRam),
|
0x13 => MBC3(MBC3Hardware::BatteryRAM),
|
||||||
0x19 => Mbc5(Mbc5Hardware::None),
|
0x19 => MBC5(MBC5Hardware::None),
|
||||||
0x1A => Mbc5(Mbc5Hardware::Ram),
|
0x1A => MBC5(MBC5Hardware::RAM),
|
||||||
0x1B => Mbc5(Mbc5Hardware::BatteryRam),
|
0x1B => MBC5(MBC5Hardware::BatteryRAM),
|
||||||
0x1C => Mbc5(Mbc5Hardware::Rumble),
|
0x1C => MBC5(MBC5Hardware::Rumble),
|
||||||
0x1D => Mbc5(Mbc5Hardware::RumbleRam),
|
0x1D => MBC5(MBC5Hardware::RumbleRAM),
|
||||||
0x1E => Mbc5(Mbc5Hardware::RumbleBatteryRam),
|
0x1E => MBC5(MBC5Hardware::RumbleBatteryRAM),
|
||||||
id => unimplemented!("MBC with code {:#04X} is unsupported", id),
|
id => unimplemented!("MBC with code {:#04X} is unsupported", id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,7 +118,7 @@ impl Cartridge {
|
|||||||
|
|
||||||
impl BusIo for Cartridge {
|
impl BusIo for Cartridge {
|
||||||
fn read_byte(&self, addr: u16) -> u8 {
|
fn read_byte(&self, addr: u16) -> u8 {
|
||||||
use MbcResult::*;
|
use MBCResult::*;
|
||||||
|
|
||||||
match self.mbc.handle_read(addr) {
|
match self.mbc.handle_read(addr) {
|
||||||
Addr(addr) => self.mem[addr],
|
Addr(addr) => self.mem[addr],
|
||||||
@@ -135,7 +132,7 @@ impl BusIo for Cartridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Mbc1 {
|
struct MBC1 {
|
||||||
/// 5-bit number
|
/// 5-bit number
|
||||||
rom_bank: u8,
|
rom_bank: u8,
|
||||||
/// 2-bit number
|
/// 2-bit number
|
||||||
@@ -146,11 +143,11 @@ struct Mbc1 {
|
|||||||
rom_size: RomSize,
|
rom_size: RomSize,
|
||||||
mem_enabled: bool,
|
mem_enabled: bool,
|
||||||
|
|
||||||
hw: Mbc1Hardware,
|
hw: MBC1Hardware,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mbc1 {
|
impl MBC1 {
|
||||||
fn new(hw: Mbc1Hardware, ram_size: RamSize, rom_size: RomSize) -> Self {
|
fn new(hw: MBC1Hardware, ram_size: RamSize, rom_size: RomSize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
rom_bank: 0x01,
|
rom_bank: 0x01,
|
||||||
mem: vec![0; ram_size.capacity() as usize],
|
mem: vec![0; ram_size.capacity() as usize],
|
||||||
@@ -228,25 +225,25 @@ impl Mbc1 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Savable for Mbc1 {
|
impl Savable for MBC1 {
|
||||||
fn ext_ram(&self) -> Option<&[u8]> {
|
fn ext_ram(&self) -> Option<&[u8]> {
|
||||||
match self.hw {
|
match self.hw {
|
||||||
Mbc1Hardware::BatteryRam => Some(&self.mem),
|
MBC1Hardware::BatteryRAM => Some(&self.mem),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ext_ram_mut(&mut self) -> Option<&mut [u8]> {
|
fn ext_ram_mut(&mut self) -> Option<&mut [u8]> {
|
||||||
match self.hw {
|
match self.hw {
|
||||||
Mbc1Hardware::BatteryRam => Some(&mut self.mem),
|
MBC1Hardware::BatteryRAM => Some(&mut self.mem),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MbcIo for Mbc1 {
|
impl MBCIo for MBC1 {
|
||||||
fn handle_read(&self, addr: u16) -> MbcResult {
|
fn handle_read(&self, addr: u16) -> MBCResult {
|
||||||
use MbcResult::*;
|
use MBCResult::*;
|
||||||
|
|
||||||
match addr {
|
match addr {
|
||||||
0x0000..=0x3FFF if self.mode => {
|
0x0000..=0x3FFF if self.mode => {
|
||||||
@@ -280,7 +277,7 @@ impl MbcIo for Mbc1 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RtcTick for Mbc1 {
|
impl RtClockTick for MBC1 {
|
||||||
fn tick(&mut self) {}
|
fn tick(&mut self) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +309,7 @@ impl RtClock {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RtcTick for RtClock {
|
impl RtClockTick for RtClock {
|
||||||
fn tick(&mut self) {
|
fn tick(&mut self) {
|
||||||
// This is the sort of situation where you'd want to use a scheduler.
|
// This is the sort of situation where you'd want to use a scheduler.
|
||||||
if self.day_high.halt() {
|
if self.day_high.halt() {
|
||||||
@@ -344,7 +341,7 @@ impl RtcTick for RtClock {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait RtcTick {
|
trait RtClockTick {
|
||||||
fn tick(&mut self);
|
fn tick(&mut self);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,41 +379,41 @@ impl From<DayHigh> for u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
enum Mbc3Device {
|
enum MBC3Device {
|
||||||
Ram,
|
ExternalRam,
|
||||||
Rtc(RtcRegister),
|
Clock(RtcRegister),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
enum RtcRegister {
|
enum RtcRegister {
|
||||||
Sec,
|
Second,
|
||||||
Min,
|
Minute,
|
||||||
Hr,
|
Hour,
|
||||||
DayLow,
|
DayLow,
|
||||||
DayHigh,
|
DayHigh,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Mbc3 {
|
struct MBC3 {
|
||||||
/// 7-bit Number
|
/// 7-bit Number
|
||||||
rom_bank: u8,
|
rom_bank: u8,
|
||||||
/// 2-bit Number
|
/// 2-bit Number
|
||||||
ram_bank: u8,
|
ram_bank: u8,
|
||||||
|
|
||||||
devs_enabled: bool,
|
devs_enabled: bool,
|
||||||
mapped: Option<Mbc3Device>,
|
mapped: Option<MBC3Device>,
|
||||||
mem: Vec<u8>,
|
mem: Vec<u8>,
|
||||||
|
|
||||||
// RTC Data Latch Previous Write
|
// RTC Data Latch Previous Write
|
||||||
prev_latch_write: Option<u8>,
|
prev_latch_write: Option<u8>,
|
||||||
|
|
||||||
hw: Mbc3Hardware,
|
hw: MBC3Hardware,
|
||||||
rtc: RtClock,
|
rtc: RtClock,
|
||||||
rtc_latch: Option<RtClock>,
|
rtc_latch: Option<RtClock>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mbc3 {
|
impl MBC3 {
|
||||||
fn new(hw: Mbc3Hardware, ram_cap: usize) -> Self {
|
fn new(hw: MBC3Hardware, ram_cap: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
mem: vec![0; ram_cap],
|
mem: vec![0; ram_cap],
|
||||||
rom_bank: Default::default(),
|
rom_bank: Default::default(),
|
||||||
@@ -431,41 +428,41 @@ impl Mbc3 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Savable for Mbc3 {
|
impl Savable for MBC3 {
|
||||||
fn ext_ram(&self) -> Option<&[u8]> {
|
fn ext_ram(&self) -> Option<&[u8]> {
|
||||||
match self.hw {
|
match self.hw {
|
||||||
Mbc3Hardware::BatteryRam | Mbc3Hardware::RtcBatteryRam => Some(&self.mem),
|
MBC3Hardware::BatteryRAM | MBC3Hardware::RTCAndBatteryRAM => Some(&self.mem),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ext_ram_mut(&mut self) -> Option<&mut [u8]> {
|
fn ext_ram_mut(&mut self) -> Option<&mut [u8]> {
|
||||||
match self.hw {
|
match self.hw {
|
||||||
Mbc3Hardware::BatteryRam | Mbc3Hardware::RtcBatteryRam => Some(&mut self.mem),
|
MBC3Hardware::BatteryRAM | MBC3Hardware::RTCAndBatteryRAM => Some(&mut self.mem),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MbcIo for Mbc3 {
|
impl MBCIo for MBC3 {
|
||||||
fn handle_read(&self, addr: u16) -> MbcResult {
|
fn handle_read(&self, addr: u16) -> MBCResult {
|
||||||
use MbcResult::*;
|
use MBCResult::*;
|
||||||
use RtcRegister::*;
|
use RtcRegister::*;
|
||||||
|
|
||||||
let res = match addr {
|
let res = match addr {
|
||||||
0x0000..=0x3FFF => Addr(addr as usize),
|
0x0000..=0x3FFF => Addr(addr as usize),
|
||||||
0x4000..=0x7FFF => Addr(0x4000 * self.rom_bank as usize + (addr as usize - 0x4000)),
|
0x4000..=0x7FFF => Addr(0x4000 * self.rom_bank as usize + (addr as usize - 0x4000)),
|
||||||
0xA000..=0xBFFF => match self.mapped {
|
0xA000..=0xBFFF => match self.mapped {
|
||||||
Some(Mbc3Device::Ram) if self.devs_enabled => {
|
Some(MBC3Device::ExternalRam) if self.devs_enabled => {
|
||||||
Byte(self.mem[0x2000 * self.ram_bank as usize + (addr as usize - 0xA000)])
|
Byte(self.mem[0x2000 * self.ram_bank as usize + (addr as usize - 0xA000)])
|
||||||
}
|
}
|
||||||
Some(Mbc3Device::Rtc(reg)) if self.devs_enabled => Byte(
|
Some(MBC3Device::Clock(reg)) if self.devs_enabled => Byte(
|
||||||
self.rtc_latch
|
self.rtc_latch
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|rtc| match reg {
|
.map(|rtc| match reg {
|
||||||
Sec => rtc.sec,
|
Second => rtc.sec,
|
||||||
Min => rtc.min,
|
Minute => rtc.min,
|
||||||
Hr => rtc.hr,
|
Hour => rtc.hr,
|
||||||
DayLow => rtc.day_low,
|
DayLow => rtc.day_low,
|
||||||
DayHigh => rtc.day_high.into(),
|
DayHigh => rtc.day_high.into(),
|
||||||
})
|
})
|
||||||
@@ -493,13 +490,13 @@ impl MbcIo for Mbc3 {
|
|||||||
0x4000..=0x5FFF => match byte {
|
0x4000..=0x5FFF => match byte {
|
||||||
0x00 | 0x01 | 0x02 | 0x03 => {
|
0x00 | 0x01 | 0x02 | 0x03 => {
|
||||||
self.ram_bank = byte & 0x03;
|
self.ram_bank = byte & 0x03;
|
||||||
self.mapped = Some(Mbc3Device::Ram);
|
self.mapped = Some(MBC3Device::ExternalRam);
|
||||||
}
|
}
|
||||||
0x08 => self.mapped = Some(Mbc3Device::Rtc(Sec)),
|
0x08 => self.mapped = Some(MBC3Device::Clock(Second)),
|
||||||
0x09 => self.mapped = Some(Mbc3Device::Rtc(Min)),
|
0x09 => self.mapped = Some(MBC3Device::Clock(Minute)),
|
||||||
0x0A => self.mapped = Some(Mbc3Device::Rtc(Hr)),
|
0x0A => self.mapped = Some(MBC3Device::Clock(Hour)),
|
||||||
0x0B => self.mapped = Some(Mbc3Device::Rtc(DayLow)),
|
0x0B => self.mapped = Some(MBC3Device::Clock(DayLow)),
|
||||||
0x0C => self.mapped = Some(Mbc3Device::Rtc(DayHigh)),
|
0x0C => self.mapped = Some(MBC3Device::Clock(DayHigh)),
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
@@ -512,17 +509,17 @@ impl MbcIo for Mbc3 {
|
|||||||
self.prev_latch_write = Some(byte);
|
self.prev_latch_write = Some(byte);
|
||||||
}
|
}
|
||||||
0xA000..=0xBFFF => match self.mapped {
|
0xA000..=0xBFFF => match self.mapped {
|
||||||
Some(Mbc3Device::Ram) if self.devs_enabled => {
|
Some(MBC3Device::ExternalRam) if self.devs_enabled => {
|
||||||
self.mem[0x2000 * self.ram_bank as usize + (addr as usize - 0xA000)] = byte
|
self.mem[0x2000 * self.ram_bank as usize + (addr as usize - 0xA000)] = byte
|
||||||
}
|
}
|
||||||
Some(Mbc3Device::Rtc(rtc_reg)) if self.devs_enabled => match rtc_reg {
|
Some(MBC3Device::Clock(rtc_reg)) if self.devs_enabled => match rtc_reg {
|
||||||
Sec => {
|
Second => {
|
||||||
self.rtc.sec = byte & 0x3F;
|
self.rtc.sec = byte & 0x3F;
|
||||||
// Writing to RTC S resets the internal sub-second counter
|
// Writing to RTC S resets the internal sub-second counter
|
||||||
self.rtc.cycles = 0;
|
self.rtc.cycles = 0;
|
||||||
}
|
}
|
||||||
Min => self.rtc.min = byte & 0x3F,
|
Minute => self.rtc.min = byte & 0x3F,
|
||||||
Hr => self.rtc.hr = byte & 0x1F,
|
Hour => self.rtc.hr = byte & 0x1F,
|
||||||
DayLow => self.rtc.day_low = byte,
|
DayLow => self.rtc.day_low = byte,
|
||||||
DayHigh => self.rtc.day_high = (byte & 0xC1).into(),
|
DayHigh => self.rtc.day_high = (byte & 0xC1).into(),
|
||||||
},
|
},
|
||||||
@@ -533,16 +530,16 @@ impl MbcIo for Mbc3 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RtcTick for Mbc3 {
|
impl RtClockTick for MBC3 {
|
||||||
fn tick(&mut self) {
|
fn tick(&mut self) {
|
||||||
if let Mbc3Hardware::RtcBatteryRam | Mbc3Hardware::Rtc = self.hw {
|
if let MBC3Hardware::RTCAndBatteryRAM | MBC3Hardware::RTC = self.hw {
|
||||||
self.rtc.tick();
|
self.rtc.tick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Mbc5 {
|
struct MBC5 {
|
||||||
/// 9-bit number
|
/// 9-bit number
|
||||||
rom_bank: u16,
|
rom_bank: u16,
|
||||||
/// 4-bit number
|
/// 4-bit number
|
||||||
@@ -552,11 +549,11 @@ struct Mbc5 {
|
|||||||
mem: Vec<u8>,
|
mem: Vec<u8>,
|
||||||
mem_enabled: bool,
|
mem_enabled: bool,
|
||||||
|
|
||||||
hw: Mbc5Hardware,
|
hw: MBC5Hardware,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mbc5 {
|
impl MBC5 {
|
||||||
fn new(hw: Mbc5Hardware, ram_cap: usize, rom_cap: usize) -> Self {
|
fn new(hw: MBC5Hardware, ram_cap: usize, rom_cap: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
rom_bank: 0x01,
|
rom_bank: 0x01,
|
||||||
mem: vec![0; ram_cap],
|
mem: vec![0; ram_cap],
|
||||||
@@ -572,25 +569,25 @@ impl Mbc5 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Savable for Mbc5 {
|
impl Savable for MBC5 {
|
||||||
fn ext_ram(&self) -> Option<&[u8]> {
|
fn ext_ram(&self) -> Option<&[u8]> {
|
||||||
match self.hw {
|
match self.hw {
|
||||||
Mbc5Hardware::RumbleBatteryRam | Mbc5Hardware::BatteryRam => Some(&self.mem),
|
MBC5Hardware::RumbleBatteryRAM | MBC5Hardware::BatteryRAM => Some(&self.mem),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ext_ram_mut(&mut self) -> Option<&mut [u8]> {
|
fn ext_ram_mut(&mut self) -> Option<&mut [u8]> {
|
||||||
match self.hw {
|
match self.hw {
|
||||||
Mbc5Hardware::RumbleBatteryRam | Mbc5Hardware::BatteryRam => Some(&mut self.mem),
|
MBC5Hardware::RumbleBatteryRAM | MBC5Hardware::BatteryRAM => Some(&mut self.mem),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MbcIo for Mbc5 {
|
impl MBCIo for MBC5 {
|
||||||
fn handle_read(&self, addr: u16) -> MbcResult {
|
fn handle_read(&self, addr: u16) -> MBCResult {
|
||||||
use MbcResult::*;
|
use MBCResult::*;
|
||||||
|
|
||||||
match addr {
|
match addr {
|
||||||
0x0000..=0x3FFF => Addr(addr as usize),
|
0x0000..=0x3FFF => Addr(addr as usize),
|
||||||
@@ -618,25 +615,25 @@ impl MbcIo for Mbc5 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RtcTick for Mbc5 {
|
impl RtClockTick for MBC5 {
|
||||||
fn tick(&mut self) {}
|
fn tick(&mut self) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Mbc2 {
|
struct MBC2 {
|
||||||
/// 4-bit number
|
/// 4-bit number
|
||||||
rom_bank: u8,
|
rom_bank: u8,
|
||||||
mem: Box<[u8; Self::RAM_SIZE]>,
|
mem: Box<[u8; Self::RAM_SIZE]>,
|
||||||
|
|
||||||
mem_enabled: bool,
|
mem_enabled: bool,
|
||||||
rom_cap: usize,
|
rom_cap: usize,
|
||||||
hw: Mbc2Hardware,
|
hw: MBC2Hardware,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mbc2 {
|
impl MBC2 {
|
||||||
const RAM_SIZE: usize = 0x0200;
|
const RAM_SIZE: usize = 0x0200;
|
||||||
|
|
||||||
fn new(hw: Mbc2Hardware, rom_cap: usize) -> Self {
|
fn new(hw: MBC2Hardware, rom_cap: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
rom_bank: 0x01,
|
rom_bank: 0x01,
|
||||||
mem: Box::new([0; Self::RAM_SIZE]),
|
mem: Box::new([0; Self::RAM_SIZE]),
|
||||||
@@ -651,25 +648,25 @@ impl Mbc2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Savable for Mbc2 {
|
impl Savable for MBC2 {
|
||||||
fn ext_ram(&self) -> Option<&[u8]> {
|
fn ext_ram(&self) -> Option<&[u8]> {
|
||||||
match self.hw {
|
match self.hw {
|
||||||
Mbc2Hardware::BatteryRam => Some(self.mem.as_ref()),
|
MBC2Hardware::BatteryRAM => Some(self.mem.as_ref()),
|
||||||
Mbc2Hardware::None => None,
|
MBC2Hardware::None => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ext_ram_mut(&mut self) -> Option<&mut [u8]> {
|
fn ext_ram_mut(&mut self) -> Option<&mut [u8]> {
|
||||||
match self.hw {
|
match self.hw {
|
||||||
Mbc2Hardware::BatteryRam => Some(self.mem.as_mut()),
|
MBC2Hardware::BatteryRAM => Some(self.mem.as_mut()),
|
||||||
Mbc2Hardware::None => None,
|
MBC2Hardware::None => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MbcIo for Mbc2 {
|
impl MBCIo for MBC2 {
|
||||||
fn handle_read(&self, addr: u16) -> MbcResult {
|
fn handle_read(&self, addr: u16) -> MBCResult {
|
||||||
use MbcResult::*;
|
use MBCResult::*;
|
||||||
|
|
||||||
match addr {
|
match addr {
|
||||||
0x0000..=0x3FFF => Addr(addr as usize),
|
0x0000..=0x3FFF => Addr(addr as usize),
|
||||||
@@ -701,14 +698,14 @@ impl MbcIo for Mbc2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RtcTick for Mbc2 {
|
impl RtClockTick for MBC2 {
|
||||||
fn tick(&mut self) {}
|
fn tick(&mut self) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct NoMbc;
|
struct NoMBC;
|
||||||
|
|
||||||
impl Savable for NoMbc {
|
impl Savable for NoMBC {
|
||||||
fn ext_ram(&self) -> Option<&[u8]> {
|
fn ext_ram(&self) -> Option<&[u8]> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -718,9 +715,9 @@ impl Savable for NoMbc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MbcIo for NoMbc {
|
impl MBCIo for NoMBC {
|
||||||
fn handle_read(&self, addr: u16) -> MbcResult {
|
fn handle_read(&self, addr: u16) -> MBCResult {
|
||||||
MbcResult::Addr(addr as usize)
|
MBCResult::Addr(addr as usize)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_write(&mut self, _: u16, byte: u8) {
|
fn handle_write(&mut self, _: u16, byte: u8) {
|
||||||
@@ -728,60 +725,60 @@ impl MbcIo for NoMbc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RtcTick for NoMbc {
|
impl RtClockTick for NoMBC {
|
||||||
fn tick(&mut self) {}
|
fn tick(&mut self) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait MbcIo: Savable + RtcTick {
|
trait MBCIo: Savable + RtClockTick {
|
||||||
fn handle_read(&self, addr: u16) -> MbcResult;
|
fn handle_read(&self, addr: u16) -> MBCResult;
|
||||||
fn handle_write(&mut self, addr: u16, byte: u8);
|
fn handle_write(&mut self, addr: u16, byte: u8);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
enum MbcResult {
|
enum MBCResult {
|
||||||
Addr(usize),
|
Addr(usize),
|
||||||
Byte(u8),
|
Byte(u8),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
enum MbcKind {
|
enum MBCKind {
|
||||||
None,
|
None,
|
||||||
Mbc1(Mbc1Hardware),
|
MBC1(MBC1Hardware),
|
||||||
Mbc2(Mbc2Hardware),
|
MBC2(MBC2Hardware),
|
||||||
Mbc3(Mbc3Hardware),
|
MBC3(MBC3Hardware),
|
||||||
Mbc5(Mbc5Hardware),
|
MBC5(MBC5Hardware),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
enum Mbc1Hardware {
|
enum MBC1Hardware {
|
||||||
None,
|
None,
|
||||||
Ram,
|
RAM,
|
||||||
BatteryRam,
|
BatteryRAM,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
enum Mbc2Hardware {
|
enum MBC2Hardware {
|
||||||
None,
|
None,
|
||||||
BatteryRam,
|
BatteryRAM,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
enum Mbc3Hardware {
|
enum MBC3Hardware {
|
||||||
Rtc,
|
RTC,
|
||||||
RtcBatteryRam,
|
RTCAndBatteryRAM,
|
||||||
None,
|
None,
|
||||||
Ram,
|
RAM,
|
||||||
BatteryRam,
|
BatteryRAM,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
enum Mbc5Hardware {
|
enum MBC5Hardware {
|
||||||
None,
|
None,
|
||||||
Ram,
|
RAM,
|
||||||
BatteryRam,
|
BatteryRAM,
|
||||||
Rumble,
|
Rumble,
|
||||||
RumbleRam,
|
RumbleRAM,
|
||||||
RumbleBatteryRam,
|
RumbleBatteryRAM,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
@@ -877,7 +874,7 @@ impl From<u8> for RomSize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Box<dyn MbcIo> {
|
impl std::fmt::Debug for Box<dyn MBCIo> {
|
||||||
fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
todo!("Implement Debug for Box<dyn MBC> Trait Object");
|
todo!("Implement Debug for Box<dyn MBC> Trait Object");
|
||||||
}
|
}
|
||||||
@@ -953,14 +950,4 @@ mod tests {
|
|||||||
Cartridge::detect_title(&title)
|
Cartridge::detect_title(&title)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn all_whitespace_title() {
|
|
||||||
let title = [
|
|
||||||
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00,
|
|
||||||
];
|
|
||||||
|
|
||||||
assert_eq!(None, Cartridge::detect_title(&title));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
36
src/cpu.rs
36
src/cpu.rs
@@ -485,39 +485,3 @@ pub(crate) enum ImeState {
|
|||||||
Pending,
|
Pending,
|
||||||
Enabled,
|
Enabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) mod dbg {
|
|
||||||
use super::{Cpu, ImeState, RegisterPair};
|
|
||||||
|
|
||||||
pub(crate) fn flags(cpu: &Cpu) -> u8 {
|
|
||||||
cpu.flags.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn af(cpu: &Cpu) -> u16 {
|
|
||||||
cpu.register_pair(RegisterPair::AF)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn bc(cpu: &Cpu) -> u16 {
|
|
||||||
cpu.register_pair(RegisterPair::BC)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn de(cpu: &Cpu) -> u16 {
|
|
||||||
cpu.register_pair(RegisterPair::DE)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn hl(cpu: &Cpu) -> u16 {
|
|
||||||
cpu.register_pair(RegisterPair::HL)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn sp(cpu: &Cpu) -> u16 {
|
|
||||||
cpu.register_pair(RegisterPair::SP)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn pc(cpu: &Cpu) -> u16 {
|
|
||||||
cpu.register_pair(RegisterPair::PC)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn ime(cpu: &Cpu) -> bool {
|
|
||||||
matches!(cpu.ime, ImeState::Enabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
18
src/emu.rs
18
src/emu.rs
@@ -18,13 +18,7 @@ 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
|
pub(crate) const SM83_CLOCK_SPEED: u64 = 0x40_0000; // Hz which is 4.194304Mhz
|
||||||
const DEFAULT_TITLE: &str = "Game Boy Screen";
|
const DEFAULT_TITLE: &str = "Game Boy Screen";
|
||||||
|
|
||||||
#[inline]
|
pub fn run_frame(cpu: &mut Cpu, gamepad: &mut Gilrs, key: KeyboardInput) -> Cycle {
|
||||||
pub fn run_frame(cpu: &mut Cpu, gamepad: &mut Gilrs, key: KeyboardInput) {
|
|
||||||
run(cpu, gamepad, key, CYCLES_IN_FRAME)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn run(cpu: &mut Cpu, gamepad: &mut Gilrs, key: KeyboardInput, cycles: Cycle) {
|
|
||||||
let mut elapsed = 0;
|
let mut elapsed = 0;
|
||||||
|
|
||||||
if let Some(event) = gamepad.next_event() {
|
if let Some(event) = gamepad.next_event() {
|
||||||
@@ -32,9 +26,11 @@ pub fn run(cpu: &mut Cpu, gamepad: &mut Gilrs, key: KeyboardInput, cycles: Cycle
|
|||||||
}
|
}
|
||||||
crate::joypad::handle_keyboard_input(&mut cpu.bus.joyp, key);
|
crate::joypad::handle_keyboard_input(&mut cpu.bus.joyp, key);
|
||||||
|
|
||||||
while elapsed < cycles {
|
while elapsed < CYCLES_IN_FRAME {
|
||||||
elapsed += cpu.step();
|
elapsed += cpu.step();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
elapsed
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_and_exit(cpu: &Cpu, control_flow: &mut ControlFlow) {
|
pub fn save_and_exit(cpu: &Cpu, control_flow: &mut ControlFlow) {
|
||||||
@@ -44,8 +40,7 @@ pub fn save_and_exit(cpu: &Cpu, control_flow: &mut ControlFlow) {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn pixel_buf(cpu: &Cpu) -> &[u8; GB_HEIGHT * GB_WIDTH * 4] {
|
pub fn pixel_buf(cpu: &Cpu) -> &[u8; GB_HEIGHT * GB_WIDTH * 4] {
|
||||||
use crate::ppu::Device;
|
cpu.bus.ppu.frame_buf.as_ref()
|
||||||
cpu.bus.ppu.frame_buf.get(Device::Host)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_boot_rom<P: AsRef<Path>>(path: P) -> std::io::Result<Cpu> {
|
pub fn from_boot_rom<P: AsRef<Path>>(path: P) -> std::io::Result<Cpu> {
|
||||||
@@ -65,7 +60,8 @@ pub fn rom_title(cpu: &Cpu) -> &str {
|
|||||||
cpu.bus
|
cpu.bus
|
||||||
.cart
|
.cart
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|c| c.title.as_deref())
|
.map(|c| c.title.as_deref())
|
||||||
|
.flatten()
|
||||||
.unwrap_or(DEFAULT_TITLE)
|
.unwrap_or(DEFAULT_TITLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
395
src/gui.rs
395
src/gui.rs
@@ -1,20 +1,17 @@
|
|||||||
use egui::{Context, TextureId};
|
use egui::{ClippedMesh, CtxRef, TextureId};
|
||||||
use egui_wgpu_backend::{wgpu, RenderPass, ScreenDescriptor};
|
use egui_wgpu_backend::{BackendError, RenderPass, ScreenDescriptor};
|
||||||
use egui_winit_platform::Platform;
|
use egui_winit_platform::Platform;
|
||||||
use wgpu::{
|
use wgpu::{
|
||||||
Adapter, CommandEncoder, Device, Extent3d, FilterMode, Instance, Queue, RequestDeviceError,
|
Adapter, CommandEncoder, Device, Extent3d, FilterMode, Instance, Queue, RequestDeviceError,
|
||||||
Surface, SurfaceCapabilities, SurfaceConfiguration, Texture, TextureView,
|
Surface, SurfaceConfiguration, SurfaceTexture, Texture, TextureFormat, TextureUsages,
|
||||||
TextureViewDescriptor,
|
TextureView,
|
||||||
};
|
};
|
||||||
|
|
||||||
use winit::dpi::PhysicalSize;
|
|
||||||
use winit::error::OsError;
|
use winit::error::OsError;
|
||||||
use winit::event::{ElementState, Event, KeyboardInput};
|
use winit::event::{ElementState, KeyboardInput};
|
||||||
use winit::event_loop::{ControlFlow, EventLoop};
|
use winit::event_loop::EventLoop;
|
||||||
use winit::window::Window;
|
use winit::window::Window;
|
||||||
|
|
||||||
use crate::cpu::Cpu;
|
use crate::{GB_HEIGHT, GB_WIDTH};
|
||||||
use crate::{emu, GB_HEIGHT, GB_WIDTH};
|
|
||||||
|
|
||||||
const EGUI_DIMENSIONS: (usize, usize) = (1280, 720);
|
const EGUI_DIMENSIONS: (usize, usize) = (1280, 720);
|
||||||
const FILTER_MODE: FilterMode = FilterMode::Nearest;
|
const FILTER_MODE: FilterMode = FilterMode::Nearest;
|
||||||
@@ -22,287 +19,21 @@ const WINDOW_TITLE: &str = "DMG-01 Emulator";
|
|||||||
|
|
||||||
const SCALE: f32 = 3.0;
|
const SCALE: f32 = 3.0;
|
||||||
|
|
||||||
pub struct Gui {
|
/// Holds GUI State
|
||||||
pub should_quit: bool,
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GuiState {
|
||||||
|
/// When true, egui winit should exit the application
|
||||||
|
pub quit: bool,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub mode: EmuMode,
|
|
||||||
|
|
||||||
window: Window,
|
|
||||||
platform: Platform,
|
|
||||||
|
|
||||||
surface: Surface,
|
|
||||||
device: Device,
|
|
||||||
queue: Queue,
|
|
||||||
surface_config: SurfaceConfiguration,
|
|
||||||
render_pass: RenderPass,
|
|
||||||
|
|
||||||
texture: Texture,
|
|
||||||
texture_size: Extent3d,
|
|
||||||
texture_id: TextureId,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Gui {
|
impl GuiState {
|
||||||
pub fn new<T>(title: String, event_loop: &EventLoop<T>, cpu: &Cpu) -> Self {
|
pub fn new(title: String) -> Self {
|
||||||
use wgpu::InstanceDescriptor;
|
|
||||||
|
|
||||||
let window = build_window(event_loop).expect("build window");
|
|
||||||
|
|
||||||
let instance = Instance::new(InstanceDescriptor::default());
|
|
||||||
let surface = unsafe {
|
|
||||||
instance
|
|
||||||
.create_surface(&window)
|
|
||||||
.expect("create wgpu instance surface")
|
|
||||||
};
|
|
||||||
|
|
||||||
let adapter = request_adapter(&instance, &surface).expect("request adaptor");
|
|
||||||
let (device, queue) = request_device(&adapter).expect("request device");
|
|
||||||
|
|
||||||
let capabilities = surface.get_capabilities(&adapter);
|
|
||||||
|
|
||||||
let surface_config = surface_config(&window, capabilities);
|
|
||||||
surface.configure(&device, &surface_config);
|
|
||||||
let platform = platform(&window);
|
|
||||||
let mut render_pass = RenderPass::new(&device, surface_config.format, 1);
|
|
||||||
|
|
||||||
let texture_size = texture_size();
|
|
||||||
let texture = create_texture(&device, texture_size);
|
|
||||||
write_to_texture(&queue, &texture, emu::pixel_buf(cpu), texture_size);
|
|
||||||
|
|
||||||
let view = texture.create_view(&TextureViewDescriptor::default());
|
|
||||||
let texture_id = expose_texture_to_egui(&mut render_pass, &device, &view);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
should_quit: Default::default(),
|
|
||||||
title,
|
title,
|
||||||
mode: EmuMode::Running,
|
quit: Default::default(),
|
||||||
|
|
||||||
window,
|
|
||||||
platform,
|
|
||||||
|
|
||||||
surface,
|
|
||||||
device,
|
|
||||||
queue,
|
|
||||||
surface_config,
|
|
||||||
render_pass,
|
|
||||||
|
|
||||||
texture,
|
|
||||||
texture_size,
|
|
||||||
texture_id,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn maybe_quit(&self, cpu: &Cpu, control_flow: &mut ControlFlow) {
|
|
||||||
if self.should_quit {
|
|
||||||
emu::save_and_exit(cpu, control_flow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn request_redraw(&self) {
|
|
||||||
self.window.request_redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_event<T>(&mut self, event: &Event<T>) {
|
|
||||||
self.platform.handle_event(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_time(&mut self, elapsed_seconds: f64) {
|
|
||||||
self.platform.update_time(elapsed_seconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resize(&mut self, size: PhysicalSize<u32>) {
|
|
||||||
// See: https://github.com/rust-windowing/winit/issues/208
|
|
||||||
if size.width > 0 && size.height > 0 {
|
|
||||||
self.surface_config.width = size.width;
|
|
||||||
self.surface_config.height = size.height;
|
|
||||||
self.surface.configure(&self.device, &self.surface_config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn paint(&mut self, cpu: &Cpu) {
|
|
||||||
use wgpu::{Color, SurfaceError};
|
|
||||||
|
|
||||||
let data = emu::pixel_buf(cpu);
|
|
||||||
write_to_texture(&self.queue, &self.texture, data, self.texture_size);
|
|
||||||
|
|
||||||
let output_frame = match self.surface.get_current_texture() {
|
|
||||||
Ok(frame) => frame,
|
|
||||||
Err(SurfaceError::Outdated) => return, // Occurs on minimization on Windows
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Dropped frame with error: {}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let output_view = output_frame
|
|
||||||
.texture
|
|
||||||
.create_view(&TextureViewDescriptor::default());
|
|
||||||
|
|
||||||
// Begin to draw Egui components
|
|
||||||
self.platform.begin_frame();
|
|
||||||
self.draw_egui(cpu, &self.platform.context(), self.texture_id);
|
|
||||||
|
|
||||||
// End the UI frame. We could now handle the output and draw the UI with the backend.
|
|
||||||
let full_output = self.platform.end_frame(Some(&self.window));
|
|
||||||
let paint_jobs = self.platform.context().tessellate(full_output.shapes);
|
|
||||||
|
|
||||||
let mut encoder = create_command_encoder(&self.device);
|
|
||||||
let screen_descriptor = create_screen_descriptor(&self.window, &self.surface_config);
|
|
||||||
let tdelta = full_output.textures_delta;
|
|
||||||
// Upload all resources for the GPU.
|
|
||||||
self.render_pass
|
|
||||||
.add_textures(&self.device, &self.queue, &tdelta)
|
|
||||||
.expect("add texture ok");
|
|
||||||
self.render_pass
|
|
||||||
.update_buffers(&self.device, &self.queue, &paint_jobs, &screen_descriptor);
|
|
||||||
|
|
||||||
// Record all render passes.
|
|
||||||
self.render_pass
|
|
||||||
.execute(
|
|
||||||
&mut encoder,
|
|
||||||
&output_view,
|
|
||||||
&paint_jobs,
|
|
||||||
&screen_descriptor,
|
|
||||||
Some(Color::BLACK),
|
|
||||||
)
|
|
||||||
.expect("execute render pass");
|
|
||||||
|
|
||||||
// Submit the commands.
|
|
||||||
self.queue.submit(std::iter::once(encoder.finish()));
|
|
||||||
|
|
||||||
// Redraw egui
|
|
||||||
output_frame.present();
|
|
||||||
|
|
||||||
self.render_pass
|
|
||||||
.remove_textures(tdelta)
|
|
||||||
.expect("remove texture delta");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn draw_egui(&mut self, cpu: &Cpu, ctx: &Context, texture_id: TextureId) {
|
|
||||||
use crate::{cpu, instruction, ppu};
|
|
||||||
|
|
||||||
fn selectable_text(ui: &mut egui::Ui, mut text: &str) -> egui::Response {
|
|
||||||
ui.add(egui::TextEdit::multiline(&mut text).code_editor())
|
|
||||||
}
|
|
||||||
|
|
||||||
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
|
||||||
ui.menu_button("File", |ui| {
|
|
||||||
if ui.button("Quit").clicked() {
|
|
||||||
self.should_quit = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
egui::Window::new(&self.title).show(ctx, |ui| {
|
|
||||||
ui.image(
|
|
||||||
texture_id,
|
|
||||||
[GB_WIDTH as f32 * SCALE, GB_HEIGHT as f32 * SCALE],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
egui::Window::new("Disassembly").show(ctx, |ui| {
|
|
||||||
selectable_text(ui, &instruction::dbg::tmp_disasm(cpu, 20));
|
|
||||||
});
|
|
||||||
|
|
||||||
egui::Window::new("Settings").show(ctx, |ui| {
|
|
||||||
egui::ComboBox::from_label("Emulation Mode")
|
|
||||||
.selected_text(format!("{:?}", self.mode))
|
|
||||||
.show_ui(ui, |ui| {
|
|
||||||
ui.selectable_value(&mut self.mode, EmuMode::Running, "Running");
|
|
||||||
ui.selectable_value(&mut self.mode, EmuMode::Stopped, "Stopped");
|
|
||||||
ui.selectable_value(&mut self.mode, EmuMode::StepFrame, "Step Frame");
|
|
||||||
ui.selectable_value(&mut self.mode, EmuMode::Step, "Step");
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
egui::Window::new("GB Info").show(ctx, |ui| {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.heading("CPU");
|
|
||||||
|
|
||||||
ui.monospace(format!("AF: {:#06X}", cpu::dbg::af(cpu)));
|
|
||||||
ui.monospace(format!("BC: {:#06X}", cpu::dbg::bc(cpu)));
|
|
||||||
ui.monospace(format!("DE: {:#06X}", cpu::dbg::de(cpu)));
|
|
||||||
ui.monospace(format!("HL: {:#06X}", cpu::dbg::hl(cpu)));
|
|
||||||
ui.add_space(10.0);
|
|
||||||
ui.monospace(format!("SP: {:#06X}", cpu::dbg::sp(cpu)));
|
|
||||||
ui.monospace(format!("PC: {:#06X}", cpu::dbg::pc(cpu)));
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
let ppu = &cpu.bus.ppu;
|
|
||||||
|
|
||||||
ui.heading("PPU");
|
|
||||||
|
|
||||||
ui.monospace(format!("LY: {}", ppu::dbg::ly(ppu)));
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.monospace(format!("SCX: {}", ppu::dbg::scx(ppu)));
|
|
||||||
ui.monospace(format!("SCY: {}", ppu::dbg::scy(ppu)));
|
|
||||||
});
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.monospace(format!("WX: {}", ppu::dbg::wx(ppu)));
|
|
||||||
ui.monospace(format!("WY: {}", ppu::dbg::wy(ppu)));
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.monospace(format!(
|
|
||||||
"Mode: {:?} {}",
|
|
||||||
ppu::dbg::mode(ppu),
|
|
||||||
ppu::dbg::dot(ppu)
|
|
||||||
))
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(10.0);
|
|
||||||
|
|
||||||
let _ = ui.selectable_label(cpu::dbg::ime(cpu), "IME");
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
let irq = cpu.int_request();
|
|
||||||
|
|
||||||
ui.label("IRQ:");
|
|
||||||
let _ = ui.selectable_label(irq & 0b01 == 0x01, "VBlank");
|
|
||||||
let _ = ui.selectable_label(irq >> 1 & 0x01 == 0x01, "LCD STAT");
|
|
||||||
let _ = ui.selectable_label(irq >> 2 & 0x01 == 0x01, "Timer");
|
|
||||||
let _ = ui.selectable_label(irq >> 3 & 0x01 == 0x01, "Serial");
|
|
||||||
let _ = ui.selectable_label(irq >> 4 & 0x01 == 0x01, "Joypad");
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
let ie = cpu.int_enable();
|
|
||||||
|
|
||||||
// TODO: Reimplement this
|
|
||||||
// let r_len = ctx.fonts().glyph_width(egui::TextStyle::Body, 'R');
|
|
||||||
// let e_len = ctx.fonts().glyph_width(egui::TextStyle::Body, 'E');
|
|
||||||
// let q_len = ctx.fonts().glyph_width(egui::TextStyle::Body, 'Q');
|
|
||||||
|
|
||||||
ui.label("IE:");
|
|
||||||
// ui.add_space(q_len - (e_len - r_len));
|
|
||||||
let _ = ui.selectable_label(ie & 0b01 == 0x01, "VBlank");
|
|
||||||
let _ = ui.selectable_label(ie >> 1 & 0x01 == 0x01, "LCD STAT");
|
|
||||||
let _ = ui.selectable_label(ie >> 2 & 0x01 == 0x01, "Timer");
|
|
||||||
let _ = ui.selectable_label(ie >> 3 & 0x01 == 0x01, "Serial");
|
|
||||||
let _ = ui.selectable_label(ie >> 4 & 0x01 == 0x01, "Joypad");
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(10.0);
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
let flags = cpu::dbg::flags(cpu);
|
|
||||||
|
|
||||||
let _ = ui.selectable_label((flags >> 7 & 0x01) == 0x01, "Zero");
|
|
||||||
let _ = ui.selectable_label((flags >> 6 & 0x01) == 0x01, "Negative");
|
|
||||||
let _ = ui.selectable_label((flags >> 5 & 0x01) == 0x01, "Half-Carry");
|
|
||||||
let _ = ui.selectable_label((flags >> 4 & 0x01) == 0x01, "Carry");
|
|
||||||
})
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum EmuMode {
|
|
||||||
Running,
|
|
||||||
StepFrame,
|
|
||||||
Stopped,
|
|
||||||
Step,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// To avoid using an [Option<KeyboardInput>] to keep track of user input from winit,
|
/// To avoid using an [Option<KeyboardInput>] to keep track of user input from winit,
|
||||||
@@ -319,7 +50,8 @@ pub fn unused_key() -> KeyboardInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_window<T>(event_loop: &EventLoop<T>) -> Result<Window, OsError> {
|
pub fn build_window<T>(event_loop: &EventLoop<T>) -> Result<Window, OsError> {
|
||||||
|
use winit::dpi::PhysicalSize;
|
||||||
use winit::window::WindowBuilder;
|
use winit::window::WindowBuilder;
|
||||||
|
|
||||||
WindowBuilder::new()
|
WindowBuilder::new()
|
||||||
@@ -334,45 +66,51 @@ fn build_window<T>(event_loop: &EventLoop<T>) -> Result<Window, OsError> {
|
|||||||
.build(event_loop)
|
.build(event_loop)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request_adapter(instance: &Instance, surface: &Surface) -> Option<Adapter> {
|
pub fn create_surface(window: &Window) -> (Instance, Surface) {
|
||||||
|
use wgpu::Backends;
|
||||||
|
|
||||||
|
let instance = Instance::new(Backends::PRIMARY);
|
||||||
|
let surface = unsafe { instance.create_surface(window) };
|
||||||
|
(instance, surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_adapter(instance: &Instance, surface: &Surface) -> Option<Adapter> {
|
||||||
use wgpu::{PowerPreference, RequestAdapterOptions};
|
use wgpu::{PowerPreference, RequestAdapterOptions};
|
||||||
|
|
||||||
pollster::block_on(instance.request_adapter(&RequestAdapterOptions {
|
pollster::block_on(instance.request_adapter(&RequestAdapterOptions {
|
||||||
power_preference: PowerPreference::HighPerformance,
|
power_preference: PowerPreference::HighPerformance,
|
||||||
compatible_surface: Some(surface),
|
|
||||||
force_fallback_adapter: false, // TODO: What do I want to do with this?
|
force_fallback_adapter: false, // TODO: What do I want to do with this?
|
||||||
|
compatible_surface: Some(surface),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request_device(adapter: &Adapter) -> Result<(Device, Queue), RequestDeviceError> {
|
pub fn request_device(adapter: &Adapter) -> Result<(Device, Queue), RequestDeviceError> {
|
||||||
use wgpu::{DeviceDescriptor, Features, Limits};
|
use wgpu::{DeviceDescriptor, Features, Limits};
|
||||||
|
|
||||||
pollster::block_on(adapter.request_device(
|
pollster::block_on(adapter.request_device(
|
||||||
&DeviceDescriptor {
|
&DeviceDescriptor {
|
||||||
|
label: None,
|
||||||
features: Features::default(),
|
features: Features::default(),
|
||||||
limits: Limits::default(),
|
limits: Limits::default(),
|
||||||
label: None,
|
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn surface_config(window: &Window, capabilities: SurfaceCapabilities) -> SurfaceConfiguration {
|
pub fn surface_config(window: &Window, format: TextureFormat) -> SurfaceConfiguration {
|
||||||
use egui_wgpu_backend::wgpu::{PresentMode, TextureFormat, TextureUsages};
|
use wgpu::PresentMode;
|
||||||
|
|
||||||
let size = window.inner_size();
|
let size = window.inner_size();
|
||||||
SurfaceConfiguration {
|
SurfaceConfiguration {
|
||||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||||
format: capabilities.formats[0],
|
format,
|
||||||
width: size.width as u32,
|
width: size.width as u32,
|
||||||
height: size.height as u32,
|
height: size.height as u32,
|
||||||
present_mode: PresentMode::Fifo,
|
present_mode: PresentMode::Mailbox,
|
||||||
alpha_mode: capabilities.alpha_modes[0],
|
|
||||||
view_formats: vec![TextureFormat::Rgba8UnormSrgb],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn platform(window: &Window) -> Platform {
|
pub fn platform_desc(window: &Window) -> Platform {
|
||||||
use egui::FontDefinitions;
|
use egui::FontDefinitions;
|
||||||
use egui_winit_platform::PlatformDescriptor;
|
use egui_winit_platform::PlatformDescriptor;
|
||||||
|
|
||||||
@@ -386,7 +124,7 @@ fn platform(window: &Window) -> Platform {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn texture_size() -> Extent3d {
|
pub fn texture_size() -> Extent3d {
|
||||||
Extent3d {
|
Extent3d {
|
||||||
width: GB_WIDTH as u32,
|
width: GB_WIDTH as u32,
|
||||||
height: GB_HEIGHT as u32,
|
height: GB_HEIGHT as u32,
|
||||||
@@ -394,23 +132,22 @@ fn texture_size() -> Extent3d {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_texture(device: &Device, size: Extent3d) -> Texture {
|
pub fn create_texture(device: &Device, size: Extent3d) -> Texture {
|
||||||
use wgpu::{TextureDescriptor, TextureDimension, TextureFormat, TextureUsages};
|
use wgpu::{TextureDescriptor, TextureDimension};
|
||||||
|
|
||||||
device.create_texture(&TextureDescriptor {
|
device.create_texture(&TextureDescriptor {
|
||||||
size,
|
size,
|
||||||
mip_level_count: 1,
|
mip_level_count: 1,
|
||||||
sample_count: 1,
|
sample_count: 1,
|
||||||
dimension: TextureDimension::D2,
|
dimension: TextureDimension::D2,
|
||||||
format: TextureFormat::Bgra8Unorm,
|
format: TextureFormat::Rgba8UnormSrgb,
|
||||||
usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING,
|
usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING,
|
||||||
label: Some("gb_pixel_buffer"),
|
label: Some("gb_pixel_buffer"),
|
||||||
view_formats: &[TextureFormat::Bgra8Unorm],
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn write_to_texture(
|
pub fn write_to_texture(
|
||||||
queue: &Queue,
|
queue: &Queue,
|
||||||
texture: &Texture,
|
texture: &Texture,
|
||||||
data: &[u8; GB_WIDTH * 4 * GB_HEIGHT],
|
data: &[u8; GB_WIDTH * 4 * GB_HEIGHT],
|
||||||
@@ -436,16 +173,23 @@ fn write_to_texture(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expose_texture_to_egui(
|
pub fn expose_texture_to_egui(
|
||||||
render_pass: &mut RenderPass,
|
render_pass: &mut RenderPass,
|
||||||
device: &Device,
|
device: &Device,
|
||||||
view: &TextureView,
|
texture: &Texture,
|
||||||
) -> TextureId {
|
) -> TextureId {
|
||||||
render_pass.egui_texture_from_wgpu_texture(device, view, FILTER_MODE)
|
render_pass.egui_texture_from_wgpu_texture(device, texture, FILTER_MODE)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn create_command_encoder(device: &Device) -> CommandEncoder {
|
pub fn create_view(frame: &SurfaceTexture) -> TextureView {
|
||||||
|
use wgpu::TextureViewDescriptor;
|
||||||
|
|
||||||
|
frame.texture.create_view(&TextureViewDescriptor::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn create_command_encoder(device: &Device) -> CommandEncoder {
|
||||||
use wgpu::CommandEncoderDescriptor;
|
use wgpu::CommandEncoderDescriptor;
|
||||||
|
|
||||||
device.create_command_encoder(&CommandEncoderDescriptor {
|
device.create_command_encoder(&CommandEncoderDescriptor {
|
||||||
@@ -454,7 +198,10 @@ fn create_command_encoder(device: &Device) -> CommandEncoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn create_screen_descriptor(window: &Window, config: &SurfaceConfiguration) -> ScreenDescriptor {
|
pub fn create_screen_descriptor(
|
||||||
|
window: &Window,
|
||||||
|
config: &SurfaceConfiguration,
|
||||||
|
) -> ScreenDescriptor {
|
||||||
ScreenDescriptor {
|
ScreenDescriptor {
|
||||||
physical_width: config.width,
|
physical_width: config.width,
|
||||||
physical_height: config.height,
|
physical_height: config.height,
|
||||||
@@ -462,11 +209,31 @@ fn create_screen_descriptor(window: &Window, config: &SurfaceConfiguration) -> S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod kbd {
|
#[inline]
|
||||||
use winit::event::{ElementState, KeyboardInput, VirtualKeyCode};
|
pub fn execute_render_pass(
|
||||||
|
render_pass: &mut RenderPass,
|
||||||
pub fn space_released(input: &KeyboardInput) -> bool {
|
encoder: &mut CommandEncoder,
|
||||||
let keycode = input.virtual_keycode;
|
view: &TextureView,
|
||||||
matches!(input.state, ElementState::Released if keycode == Some(VirtualKeyCode::Space))
|
jobs: Vec<ClippedMesh>,
|
||||||
}
|
descriptor: &ScreenDescriptor,
|
||||||
|
) -> Result<(), BackendError> {
|
||||||
|
render_pass.execute(encoder, view, &jobs, descriptor, Some(wgpu::Color::BLACK))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn draw_egui(app: &mut GuiState, ctx: &CtxRef, texture_id: TextureId) {
|
||||||
|
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||||
|
egui::menu::menu(ui, "File", |ui| {
|
||||||
|
if ui.button("Quit").clicked() {
|
||||||
|
app.quit = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
egui::Window::new(&app.title).show(ctx, |ui| {
|
||||||
|
ui.image(
|
||||||
|
texture_id,
|
||||||
|
[GB_WIDTH as f32 * SCALE, GB_HEIGHT as f32 * SCALE],
|
||||||
|
);
|
||||||
|
})
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
use self::add::{Source as AddSource, Target as AddTarget};
|
use self::add::{Source as AddSource, Target as AddTarget};
|
||||||
use self::alu::Source as AluSource;
|
use self::alu::Source as AluSource;
|
||||||
use self::jump::{JpCond, JpLoc};
|
use self::jump::{JumpCondition, JumpLocation};
|
||||||
use self::load::{Source as LDSource, Target as LDTarget};
|
use self::load::{Source as LDSource, Target as LDTarget};
|
||||||
use self::table::{
|
use self::table::{
|
||||||
alu_imm_instr, alu_reg_instr, flag_instr, group1, group2, group3, jump_cond, prefix_alu,
|
alu_imm_instr, alu_reg_instr, flag_instr, group1, group2, group3, jump_cond, prefix_alu,
|
||||||
@@ -16,7 +16,7 @@ use crate::Cycle;
|
|||||||
pub(crate) enum Instruction {
|
pub(crate) enum Instruction {
|
||||||
NOP,
|
NOP,
|
||||||
STOP,
|
STOP,
|
||||||
JR(JpCond),
|
JR(JumpCondition),
|
||||||
LD(LDTarget, LDSource),
|
LD(LDTarget, LDSource),
|
||||||
ADD(AddTarget, AddSource),
|
ADD(AddTarget, AddSource),
|
||||||
LDHL,
|
LDHL,
|
||||||
@@ -38,13 +38,13 @@ pub(crate) enum Instruction {
|
|||||||
XOR(AluSource),
|
XOR(AluSource),
|
||||||
OR(AluSource),
|
OR(AluSource),
|
||||||
CP(AluSource),
|
CP(AluSource),
|
||||||
RET(JpCond),
|
RET(JumpCondition),
|
||||||
POP(Group3RegisterPair),
|
POP(Group3RegisterPair),
|
||||||
RETI,
|
RETI,
|
||||||
JP(JpCond, JpLoc),
|
JP(JumpCondition, JumpLocation),
|
||||||
DI,
|
DI,
|
||||||
EI,
|
EI,
|
||||||
CALL(JpCond),
|
CALL(JumpCondition),
|
||||||
PUSH(Group3RegisterPair),
|
PUSH(Group3RegisterPair),
|
||||||
RST(u8),
|
RST(u8),
|
||||||
RLC(Register),
|
RLC(Register),
|
||||||
@@ -58,7 +58,6 @@ pub(crate) enum Instruction {
|
|||||||
BIT(u8, Register),
|
BIT(u8, Register),
|
||||||
RES(u8, Register),
|
RES(u8, Register),
|
||||||
SET(u8, Register),
|
SET(u8, Register),
|
||||||
Invalid,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Instruction {
|
impl std::fmt::Debug for Instruction {
|
||||||
@@ -110,7 +109,6 @@ impl std::fmt::Debug for Instruction {
|
|||||||
BIT(b, r) => write!(f, "BIT {}, {:?}", b, r),
|
BIT(b, r) => write!(f, "BIT {}, {:?}", b, r),
|
||||||
RES(b, r) => write!(f, "RES {}, {:?}", b, r),
|
RES(b, r) => write!(f, "RES {}, {:?}", b, r),
|
||||||
SET(b, r) => write!(f, "SET {}, {:?}", b, r),
|
SET(b, r) => write!(f, "SET {}, {:?}", b, r),
|
||||||
Invalid => f.write_str("???"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -295,7 +293,7 @@ impl Instruction {
|
|||||||
let addr = pc.wrapping_add(byte as u16);
|
let addr = pc.wrapping_add(byte as u16);
|
||||||
|
|
||||||
match cond {
|
match cond {
|
||||||
JpCond::NotZero => {
|
JumpCondition::NotZero => {
|
||||||
if !flags.z() {
|
if !flags.z() {
|
||||||
Self::jump(cpu, addr);
|
Self::jump(cpu, addr);
|
||||||
12
|
12
|
||||||
@@ -303,7 +301,7 @@ impl Instruction {
|
|||||||
8
|
8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JpCond::Zero => {
|
JumpCondition::Zero => {
|
||||||
if flags.z() {
|
if flags.z() {
|
||||||
Self::jump(cpu, addr);
|
Self::jump(cpu, addr);
|
||||||
12
|
12
|
||||||
@@ -311,7 +309,7 @@ impl Instruction {
|
|||||||
8
|
8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JpCond::NotCarry => {
|
JumpCondition::NotCarry => {
|
||||||
if !flags.c() {
|
if !flags.c() {
|
||||||
Self::jump(cpu, addr);
|
Self::jump(cpu, addr);
|
||||||
12
|
12
|
||||||
@@ -319,7 +317,7 @@ impl Instruction {
|
|||||||
8
|
8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JpCond::Carry => {
|
JumpCondition::Carry => {
|
||||||
if flags.c() {
|
if flags.c() {
|
||||||
Self::jump(cpu, addr);
|
Self::jump(cpu, addr);
|
||||||
12
|
12
|
||||||
@@ -327,7 +325,7 @@ impl Instruction {
|
|||||||
8
|
8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JpCond::Always => {
|
JumpCondition::Always => {
|
||||||
Self::jump(cpu, addr);
|
Self::jump(cpu, addr);
|
||||||
12
|
12
|
||||||
}
|
}
|
||||||
@@ -839,7 +837,7 @@ impl Instruction {
|
|||||||
let flags: Flags = *cpu.flags();
|
let flags: Flags = *cpu.flags();
|
||||||
|
|
||||||
match cond {
|
match cond {
|
||||||
JpCond::NotZero => {
|
JumpCondition::NotZero => {
|
||||||
cpu.bus.clock(); // internal branch decision
|
cpu.bus.clock(); // internal branch decision
|
||||||
|
|
||||||
if !flags.z() {
|
if !flags.z() {
|
||||||
@@ -850,7 +848,7 @@ impl Instruction {
|
|||||||
8
|
8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JpCond::Zero => {
|
JumpCondition::Zero => {
|
||||||
cpu.bus.clock(); // internal branch decision
|
cpu.bus.clock(); // internal branch decision
|
||||||
|
|
||||||
if flags.z() {
|
if flags.z() {
|
||||||
@@ -861,7 +859,7 @@ impl Instruction {
|
|||||||
8
|
8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JpCond::NotCarry => {
|
JumpCondition::NotCarry => {
|
||||||
cpu.bus.clock(); // internal branch decision
|
cpu.bus.clock(); // internal branch decision
|
||||||
|
|
||||||
if !flags.c() {
|
if !flags.c() {
|
||||||
@@ -872,7 +870,7 @@ impl Instruction {
|
|||||||
8
|
8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JpCond::Carry => {
|
JumpCondition::Carry => {
|
||||||
cpu.bus.clock(); // internal branch decision
|
cpu.bus.clock(); // internal branch decision
|
||||||
|
|
||||||
if flags.c() {
|
if flags.c() {
|
||||||
@@ -883,7 +881,7 @@ impl Instruction {
|
|||||||
8
|
8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JpCond::Always => {
|
JumpCondition::Always => {
|
||||||
let addr = Self::pop(cpu);
|
let addr = Self::pop(cpu);
|
||||||
Self::jump(cpu, addr);
|
Self::jump(cpu, addr);
|
||||||
16
|
16
|
||||||
@@ -910,13 +908,13 @@ impl Instruction {
|
|||||||
16
|
16
|
||||||
}
|
}
|
||||||
Instruction::JP(cond, location) => match location {
|
Instruction::JP(cond, location) => match location {
|
||||||
JpLoc::HL => {
|
JumpLocation::HL => {
|
||||||
// JP HL | Store HL in program counter
|
// JP HL | Store HL in program counter
|
||||||
let right = cpu.register_pair(RegisterPair::HL);
|
let right = cpu.register_pair(RegisterPair::HL);
|
||||||
cpu.set_register_pair(RegisterPair::PC, right);
|
cpu.set_register_pair(RegisterPair::PC, right);
|
||||||
4
|
4
|
||||||
}
|
}
|
||||||
JpLoc::ImmediateWord => {
|
JumpLocation::ImmediateWord => {
|
||||||
// JP cond u16 | Store u16 in program counter if condition is true
|
// JP cond u16 | Store u16 in program counter if condition is true
|
||||||
// JP u16 | Store u16 in program counter
|
// JP u16 | Store u16 in program counter
|
||||||
let flags: Flags = *cpu.flags();
|
let flags: Flags = *cpu.flags();
|
||||||
@@ -924,7 +922,7 @@ impl Instruction {
|
|||||||
let addr = Self::imm_word(cpu);
|
let addr = Self::imm_word(cpu);
|
||||||
|
|
||||||
match cond {
|
match cond {
|
||||||
JpCond::NotZero => {
|
JumpCondition::NotZero => {
|
||||||
if !flags.z() {
|
if !flags.z() {
|
||||||
Self::jump(cpu, addr);
|
Self::jump(cpu, addr);
|
||||||
16
|
16
|
||||||
@@ -932,7 +930,7 @@ impl Instruction {
|
|||||||
12
|
12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JpCond::Zero => {
|
JumpCondition::Zero => {
|
||||||
if flags.z() {
|
if flags.z() {
|
||||||
Self::jump(cpu, addr);
|
Self::jump(cpu, addr);
|
||||||
16
|
16
|
||||||
@@ -940,7 +938,7 @@ impl Instruction {
|
|||||||
12
|
12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JpCond::NotCarry => {
|
JumpCondition::NotCarry => {
|
||||||
if !flags.c() {
|
if !flags.c() {
|
||||||
Self::jump(cpu, addr);
|
Self::jump(cpu, addr);
|
||||||
16
|
16
|
||||||
@@ -948,7 +946,7 @@ impl Instruction {
|
|||||||
12
|
12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JpCond::Carry => {
|
JumpCondition::Carry => {
|
||||||
if flags.c() {
|
if flags.c() {
|
||||||
Self::jump(cpu, addr);
|
Self::jump(cpu, addr);
|
||||||
16
|
16
|
||||||
@@ -956,7 +954,7 @@ impl Instruction {
|
|||||||
12
|
12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JpCond::Always => {
|
JumpCondition::Always => {
|
||||||
Self::jump(cpu, addr);
|
Self::jump(cpu, addr);
|
||||||
16
|
16
|
||||||
}
|
}
|
||||||
@@ -982,7 +980,7 @@ impl Instruction {
|
|||||||
let return_addr = cpu.register_pair(RegisterPair::PC);
|
let return_addr = cpu.register_pair(RegisterPair::PC);
|
||||||
|
|
||||||
match cond {
|
match cond {
|
||||||
JpCond::NotZero => {
|
JumpCondition::NotZero => {
|
||||||
if !flags.z() {
|
if !flags.z() {
|
||||||
cpu.bus.clock(); // internal branch decision
|
cpu.bus.clock(); // internal branch decision
|
||||||
Self::push(cpu, return_addr);
|
Self::push(cpu, return_addr);
|
||||||
@@ -992,7 +990,7 @@ impl Instruction {
|
|||||||
12
|
12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JpCond::Zero => {
|
JumpCondition::Zero => {
|
||||||
if flags.z() {
|
if flags.z() {
|
||||||
cpu.bus.clock(); // internal branch decision
|
cpu.bus.clock(); // internal branch decision
|
||||||
Self::push(cpu, return_addr);
|
Self::push(cpu, return_addr);
|
||||||
@@ -1002,7 +1000,7 @@ impl Instruction {
|
|||||||
12
|
12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JpCond::NotCarry => {
|
JumpCondition::NotCarry => {
|
||||||
if !flags.c() {
|
if !flags.c() {
|
||||||
cpu.bus.clock(); // internal branch decision
|
cpu.bus.clock(); // internal branch decision
|
||||||
Self::push(cpu, return_addr);
|
Self::push(cpu, return_addr);
|
||||||
@@ -1012,7 +1010,7 @@ impl Instruction {
|
|||||||
12
|
12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JpCond::Carry => {
|
JumpCondition::Carry => {
|
||||||
if flags.c() {
|
if flags.c() {
|
||||||
cpu.bus.clock(); // internal branch decision
|
cpu.bus.clock(); // internal branch decision
|
||||||
Self::push(cpu, return_addr);
|
Self::push(cpu, return_addr);
|
||||||
@@ -1022,7 +1020,7 @@ impl Instruction {
|
|||||||
12
|
12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
JpCond::Always => {
|
JumpCondition::Always => {
|
||||||
cpu.bus.clock(); // internal branch decision
|
cpu.bus.clock(); // internal branch decision
|
||||||
Self::push(cpu, return_addr);
|
Self::push(cpu, return_addr);
|
||||||
cpu.set_register_pair(RegisterPair::PC, addr);
|
cpu.set_register_pair(RegisterPair::PC, addr);
|
||||||
@@ -1296,7 +1294,6 @@ impl Instruction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Instruction::Invalid => panic!("Attempted to execute invalid instruction"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1560,7 +1557,7 @@ impl Instruction {
|
|||||||
// STOP
|
// STOP
|
||||||
0o020 => STOP,
|
0o020 => STOP,
|
||||||
// JR i8
|
// JR i8
|
||||||
0o030 => JR(JpCond::Always),
|
0o030 => JR(JumpCondition::Always),
|
||||||
// JR cond i8
|
// JR cond i8
|
||||||
0o040 | 0o050 | 0o060 | 0o070 => JR(jump_cond((byte >> 3) & 0x03)),
|
0o040 | 0o050 | 0o060 | 0o070 => JR(jump_cond((byte >> 3) & 0x03)),
|
||||||
// LD r16, u16
|
// LD r16, u16
|
||||||
@@ -1625,16 +1622,16 @@ impl Instruction {
|
|||||||
// POP r16
|
// POP r16
|
||||||
0o301 | 0o321 | 0o341 | 0o361 => POP(group3((byte >> 4) & 0x03)),
|
0o301 | 0o321 | 0o341 | 0o361 => POP(group3((byte >> 4) & 0x03)),
|
||||||
// RET
|
// RET
|
||||||
0o311 => RET(JpCond::Always),
|
0o311 => RET(JumpCondition::Always),
|
||||||
// RETI
|
// RETI
|
||||||
0o331 => RETI,
|
0o331 => RETI,
|
||||||
// JP HL
|
// JP HL
|
||||||
0o351 => JP(JpCond::Always, JpLoc::HL),
|
0o351 => JP(JumpCondition::Always, JumpLocation::HL),
|
||||||
// LD SP, HL
|
// LD SP, HL
|
||||||
0o371 => LD(LDTarget::SP, LDSource::HL),
|
0o371 => LD(LDTarget::SP, LDSource::HL),
|
||||||
// JP cond u16
|
// JP cond u16
|
||||||
0o302 | 0o312 | 0o322 | 0o332 => {
|
0o302 | 0o312 | 0o322 | 0o332 => {
|
||||||
JP(jump_cond((byte >> 3) & 0x03), JpLoc::ImmediateWord)
|
JP(jump_cond((byte >> 3) & 0x03), JumpLocation::ImmediateWord)
|
||||||
}
|
}
|
||||||
// LD (0xFF00 + C), A
|
// LD (0xFF00 + C), A
|
||||||
0o342 => LD(LDTarget::IoWithC, LDSource::A),
|
0o342 => LD(LDTarget::IoWithC, LDSource::A),
|
||||||
@@ -1645,7 +1642,9 @@ impl Instruction {
|
|||||||
// LD A, (u16)
|
// LD A, (u16)
|
||||||
0o372 => LD(LDTarget::A, LDSource::IndirectImmediateWord),
|
0o372 => LD(LDTarget::A, LDSource::IndirectImmediateWord),
|
||||||
// JP u16
|
// JP u16
|
||||||
0o303 => JP(JpCond::Always, JpLoc::ImmediateWord),
|
0o303 => JP(JumpCondition::Always, JumpLocation::ImmediateWord),
|
||||||
|
// 0xCB Prefix
|
||||||
|
0o313 => unreachable!("{:#04X} should be handled by the prefixed decoder", byte),
|
||||||
// DI
|
// DI
|
||||||
0o363 => DI,
|
0o363 => DI,
|
||||||
// EI
|
// EI
|
||||||
@@ -1654,14 +1653,12 @@ impl Instruction {
|
|||||||
0o304 | 0o314 | 0o324 | 0o334 => CALL(jump_cond((byte >> 3) & 0x03)),
|
0o304 | 0o314 | 0o324 | 0o334 => CALL(jump_cond((byte >> 3) & 0x03)),
|
||||||
// PUSH r16
|
// PUSH r16
|
||||||
0o305 | 0o325 | 0o345 | 0o365 => PUSH(group3((byte >> 4) & 0x03)),
|
0o305 | 0o325 | 0o345 | 0o365 => PUSH(group3((byte >> 4) & 0x03)),
|
||||||
// CALL u16
|
0o315 => CALL(JumpCondition::Always),
|
||||||
0o315 => CALL(JpCond::Always),
|
|
||||||
// ADD, ADC, SUB, SBC, AND, XOR, OR, and CP
|
|
||||||
0o306 | 0o316 | 0o326 | 0o336 | 0o346 | 0o356 | 0o366 | 0o376 => {
|
0o306 | 0o316 | 0o326 | 0o336 | 0o346 | 0o356 | 0o366 | 0o376 => {
|
||||||
alu_imm_instr((byte >> 3) & 0x07)
|
alu_imm_instr((byte >> 3) & 0x07)
|
||||||
}
|
}
|
||||||
0o307 | 0o317 | 0o327 | 0o337 | 0o347 | 0o357 | 0o367 | 0o377 => RST(byte & 0b00111000),
|
0o307 | 0o317 | 0o327 | 0o337 | 0o347 | 0o357 | 0o367 | 0o377 => RST(byte & 0b00111000),
|
||||||
_ => Invalid, // 0xCB is 0o313
|
_ => panic!("{:#04X} is an illegal opcode", byte),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1684,7 +1681,7 @@ impl Instruction {
|
|||||||
mod jump {
|
mod jump {
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub(crate) enum JpCond {
|
pub(crate) enum JumpCondition {
|
||||||
Always,
|
Always,
|
||||||
NotZero,
|
NotZero,
|
||||||
Zero,
|
Zero,
|
||||||
@@ -1692,9 +1689,9 @@ mod jump {
|
|||||||
Carry,
|
Carry,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for JpCond {
|
impl std::fmt::Debug for JumpCondition {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
use JpCond::*;
|
use JumpCondition::*;
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
Always => f.write_str(""),
|
Always => f.write_str(""),
|
||||||
@@ -1707,14 +1704,14 @@ mod jump {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub(crate) enum JpLoc {
|
pub(crate) enum JumpLocation {
|
||||||
HL,
|
HL,
|
||||||
ImmediateWord,
|
ImmediateWord,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for JpLoc {
|
impl std::fmt::Debug for JumpLocation {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
use JpLoc::*;
|
use JumpLocation::*;
|
||||||
|
|
||||||
match *self {
|
match *self {
|
||||||
HL => f.write_str("HL"),
|
HL => f.write_str("HL"),
|
||||||
@@ -1874,7 +1871,7 @@ mod load {
|
|||||||
mod table {
|
mod table {
|
||||||
use super::add::{Source as AddSource, Target as AddTarget};
|
use super::add::{Source as AddSource, Target as AddTarget};
|
||||||
use super::alu::Source as AluSource;
|
use super::alu::Source as AluSource;
|
||||||
use super::{Instruction, JpCond};
|
use super::{Instruction, JumpCondition};
|
||||||
use crate::cpu::{Register as CpuRegister, RegisterPair};
|
use crate::cpu::{Register as CpuRegister, RegisterPair};
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
@@ -2073,12 +2070,12 @@ mod table {
|
|||||||
0b101 => L,
|
0b101 => L,
|
||||||
0b110 => IndirectHL,
|
0b110 => IndirectHL,
|
||||||
0b111 => A,
|
0b111 => A,
|
||||||
_ => unreachable!("{:#04X} is not a valid Register", code),
|
_ => unreachable!("{:#04X} is not a valid Register"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn jump_cond(code: u8) -> JpCond {
|
pub(crate) fn jump_cond(code: u8) -> JumpCondition {
|
||||||
use JpCond::*;
|
use JumpCondition::*;
|
||||||
|
|
||||||
match code {
|
match code {
|
||||||
0b00 => NotZero,
|
0b00 => NotZero,
|
||||||
@@ -2153,268 +2150,3 @@ mod table {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) mod dbg {
|
|
||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
use super::add::{Source as AddSource, Target as AddTarget};
|
|
||||||
use super::alu::Source as AluSource;
|
|
||||||
use super::jump::{JpCond, JpLoc};
|
|
||||||
use super::load::{Source as LDSource, Target as LDTarget};
|
|
||||||
use super::{AllRegisters, BusIo, Cpu, Instruction, RegisterPair};
|
|
||||||
|
|
||||||
pub(crate) fn tmp_disasm(cpu: &Cpu, limit: u8) -> String {
|
|
||||||
let mut sm83_asm = String::new();
|
|
||||||
let mut pc = cpu.register_pair(RegisterPair::PC);
|
|
||||||
|
|
||||||
for _ in 0..limit {
|
|
||||||
let opcode = cpu.read_byte(pc);
|
|
||||||
pc += 1;
|
|
||||||
|
|
||||||
let maybe_instr = if opcode == 0xCB {
|
|
||||||
let opcode = cpu.read_byte(pc);
|
|
||||||
pc += 1;
|
|
||||||
|
|
||||||
Instruction::prefixed(opcode)
|
|
||||||
} else {
|
|
||||||
Instruction::unprefixed(opcode)
|
|
||||||
};
|
|
||||||
|
|
||||||
match maybe_instr {
|
|
||||||
Instruction::Invalid => {}
|
|
||||||
instr => {
|
|
||||||
let asm = format!("{:04X} {:?}\n", pc - 1, instr);
|
|
||||||
sm83_asm.push_str(&asm);
|
|
||||||
pc += delta::pc_inc_count(instr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sm83_asm
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn new_disasm(cpu: &Cpu, limit: u8) -> String {
|
|
||||||
let mut assembly = String::new();
|
|
||||||
let mut pc = cpu.register_pair(RegisterPair::PC);
|
|
||||||
|
|
||||||
for _ in 0..limit {
|
|
||||||
let opcode = cpu.read_byte(pc);
|
|
||||||
pc += 1;
|
|
||||||
|
|
||||||
let maybe_instr = if opcode == 0xCB {
|
|
||||||
let opcode = cpu.read_byte(pc);
|
|
||||||
pc += 1;
|
|
||||||
|
|
||||||
Instruction::prefixed(opcode)
|
|
||||||
} else {
|
|
||||||
Instruction::unprefixed(opcode)
|
|
||||||
};
|
|
||||||
|
|
||||||
match maybe_instr {
|
|
||||||
Instruction::Invalid => {}
|
|
||||||
instr => {
|
|
||||||
let output = format!("${:04X} {}\n", pc - 1, disasm(cpu, pc, instr));
|
|
||||||
assembly.push_str(&output);
|
|
||||||
pc += delta::pc_inc_count(instr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assembly
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: It might be better if I pass in a mutable writer instead of usnig a String
|
|
||||||
fn disasm(cpu: &Cpu, pc: u16, instr: Instruction) -> Cow<str> {
|
|
||||||
use Instruction::*;
|
|
||||||
|
|
||||||
let imm_byte = cpu.read_byte(pc + 1);
|
|
||||||
let imm_word = (cpu.read_byte(pc + 2) as u16) << 8 | imm_byte as u16;
|
|
||||||
|
|
||||||
match instr {
|
|
||||||
// Unprefixed Instructions
|
|
||||||
NOP => "NOP".into(),
|
|
||||||
LD(LDTarget::IndirectImmediateWord, LDSource::SP) => {
|
|
||||||
format!("LD ({:#06X}), SP", imm_word).into()
|
|
||||||
}
|
|
||||||
STOP => "STOP".into(),
|
|
||||||
JR(JpCond::Always) => format!("JR {}", imm_byte as i8).into(),
|
|
||||||
JR(cond) => format!("JR {:?} {}", cond, imm_byte as i8).into(),
|
|
||||||
LD(LDTarget::Group1(rp), LDSource::ImmediateWord) => {
|
|
||||||
format!("LD {:?} {:#06X}", rp, imm_word).into()
|
|
||||||
}
|
|
||||||
ADD(AddTarget::HL, AddSource::Group1(rp)) => format!("ADD HL, {:?}", rp).into(),
|
|
||||||
LD(LDTarget::IndirectGroup2(rp), LDSource::A) => format!("LD ({:?}), A", rp).into(),
|
|
||||||
LD(LDTarget::A, LDSource::IndirectGroup2(rp)) => format!("LD A, ({:?})", rp).into(),
|
|
||||||
INC(AllRegisters::Group1(rp)) => format!("INC {:?}", rp).into(),
|
|
||||||
DEC(AllRegisters::Group1(rp)) => format!("DEC {:?}", rp).into(),
|
|
||||||
INC(AllRegisters::Register(reg)) => format!("INC {:?}", reg).into(),
|
|
||||||
DEC(AllRegisters::Register(reg)) => format!("DEC {:?}", reg).into(),
|
|
||||||
LD(LDTarget::Register(reg), LDSource::ImmediateByte) => {
|
|
||||||
format!("LD {:?}, {:#04X}", reg, imm_byte).into()
|
|
||||||
}
|
|
||||||
RLCA => "RLCA".into(),
|
|
||||||
RRCA => "RRCA".into(),
|
|
||||||
RLA => "RLA".into(),
|
|
||||||
RRA => "RRA".into(),
|
|
||||||
DAA => "DAA".into(),
|
|
||||||
CPL => "CPL".into(),
|
|
||||||
SCF => "SCF".into(),
|
|
||||||
CCF => "CCF".into(),
|
|
||||||
HALT => "HALT".into(),
|
|
||||||
LD(LDTarget::Register(left), LDSource::Register(right)) => {
|
|
||||||
format!("LD {:?}, {:?}", left, right).into()
|
|
||||||
}
|
|
||||||
ADD(AddTarget::A, AddSource::Register(reg)) => format!("ADD A, {:?}", reg).into(),
|
|
||||||
ADC(AluSource::Register(reg)) => format!("ADC {:?}", reg).into(),
|
|
||||||
SUB(AluSource::Register(reg)) => format!("SUB {:?}", reg).into(),
|
|
||||||
SBC(AluSource::Register(reg)) => format!("SBC {:?}", reg).into(),
|
|
||||||
AND(AluSource::Register(reg)) => format!("AND {:?}", reg).into(),
|
|
||||||
XOR(AluSource::Register(reg)) => format!("XOR {:?}", reg).into(),
|
|
||||||
OR(AluSource::Register(reg)) => format!("OR {:?}", reg).into(),
|
|
||||||
CP(AluSource::Register(reg)) => format!("CP {:?}", reg).into(),
|
|
||||||
RET(JpCond::Always) => "RET".into(),
|
|
||||||
RET(cond) => format!("RET {:?}", cond).into(),
|
|
||||||
LD(LDTarget::IoWithImmediateOffset, LDSource::A) => {
|
|
||||||
format!("LD ({:#06X}) , A", 0xFF00 + imm_byte as u16).into()
|
|
||||||
}
|
|
||||||
ADD(AddTarget::SP, AddSource::ImmediateSignedByte) => {
|
|
||||||
format!("ADD SP, {}", imm_byte as i8).into()
|
|
||||||
}
|
|
||||||
LD(LDTarget::A, LDSource::IoWithImmediateOffset) => {
|
|
||||||
format!("LD A, ({:#06X})", 0xFF00 + imm_byte as u16).into()
|
|
||||||
}
|
|
||||||
LDHL => format!("LD HL, SP + {}", imm_byte as i8).into(),
|
|
||||||
POP(rp) => format!("POP {:?}", rp).into(),
|
|
||||||
RETI => "RETI".into(),
|
|
||||||
JP(JpCond::Always, JpLoc::HL) => "JP HL".into(),
|
|
||||||
LD(LDTarget::SP, LDSource::HL) => "LD SP, HL".into(),
|
|
||||||
JP(JpCond::Always, JpLoc::ImmediateWord) => format!("JP {:#06X}", imm_word).into(),
|
|
||||||
JP(cond, JpLoc::ImmediateWord) => format!("JP {:?} {:#06X}", cond, imm_word).into(),
|
|
||||||
LD(LDTarget::IoWithC, LDSource::A) => "LD (0xFF00 + C), A".into(),
|
|
||||||
LD(LDTarget::IndirectImmediateWord, LDSource::A) => {
|
|
||||||
format!("LD ({:#06X}), A", imm_byte).into()
|
|
||||||
}
|
|
||||||
LD(LDTarget::A, LDSource::IoWithC) => "LD A, (0xFF00 + C)".into(),
|
|
||||||
LD(LDTarget::A, LDSource::IndirectImmediateWord) => {
|
|
||||||
format!("LD A, ({:#06X})", imm_word).into()
|
|
||||||
}
|
|
||||||
DI => "DI".into(),
|
|
||||||
EI => "EI".into(),
|
|
||||||
CALL(cond) => format!("CALL {:?} {:#06X}", cond, imm_word).into(),
|
|
||||||
PUSH(rp) => format!("PUSH {:?}", rp).into(),
|
|
||||||
ADD(AddTarget::A, AddSource::ImmediateByte) => {
|
|
||||||
format!("ADD A, {:#04X}", imm_byte).into()
|
|
||||||
}
|
|
||||||
ADC(AluSource::ImmediateByte) => format!("ADC {:#04X}", imm_byte).into(),
|
|
||||||
SUB(AluSource::ImmediateByte) => format!("SUB {:#04X}", imm_byte).into(),
|
|
||||||
SBC(AluSource::ImmediateByte) => format!("SBC {:#04X}", imm_byte).into(),
|
|
||||||
AND(AluSource::ImmediateByte) => format!("AND {:#04X}", imm_byte).into(),
|
|
||||||
XOR(AluSource::ImmediateByte) => format!("XOR {:#04X}", imm_byte).into(),
|
|
||||||
OR(AluSource::ImmediateByte) => format!("OR {:#04X}", imm_byte).into(),
|
|
||||||
CP(AluSource::ImmediateByte) => format!("CP {:#04X}", imm_byte).into(),
|
|
||||||
RST(v) => format!("RST {:#04X}", v).into(),
|
|
||||||
|
|
||||||
// Prefixed Instructions
|
|
||||||
RLC(reg) => format!("RLC {:?}", reg).into(),
|
|
||||||
RRC(reg) => format!("RRC {:?}", reg).into(),
|
|
||||||
RL(reg) => format!("RL {:?}", reg).into(),
|
|
||||||
RR(reg) => format!("RR {:?}", reg).into(),
|
|
||||||
SLA(reg) => format!("SLA {:?}", reg).into(),
|
|
||||||
SRA(reg) => format!("SRA {:?}", reg).into(),
|
|
||||||
SWAP(reg) => format!("SWAP {:?}", reg).into(),
|
|
||||||
SRL(reg) => format!("SRL {:?}", reg).into(),
|
|
||||||
BIT(bit, reg) => format!("BIT {}, {:?}", bit, reg).into(),
|
|
||||||
RES(bit, reg) => format!("RES {}, {:?}", bit, reg).into(),
|
|
||||||
SET(bit, reg) => format!("SET {}, {:?}", bit, reg).into(),
|
|
||||||
_ => unreachable!("{:?} is an illegal instruction", instr),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod delta {
|
|
||||||
use super::super::add::{Source as AddSource, Target as AddTarget};
|
|
||||||
use super::super::alu::Source as AluSource;
|
|
||||||
use super::super::jump::{JpCond, JpLoc};
|
|
||||||
use super::super::load::{Source as LDSource, Target as LDTarget};
|
|
||||||
use super::super::{AllRegisters, Instruction};
|
|
||||||
|
|
||||||
pub(super) fn pc_inc_count(instr: Instruction) -> u16 {
|
|
||||||
use Instruction::*;
|
|
||||||
|
|
||||||
match instr {
|
|
||||||
// Unprefixed
|
|
||||||
NOP => 0,
|
|
||||||
LD(LDTarget::IndirectImmediateWord, LDSource::SP) => 2,
|
|
||||||
STOP => 0,
|
|
||||||
JR(_) => 1,
|
|
||||||
LD(LDTarget::Group1(_), LDSource::ImmediateWord) => 2,
|
|
||||||
ADD(AddTarget::HL, AddSource::Group1(_)) => 0,
|
|
||||||
LD(LDTarget::IndirectGroup2(_), LDSource::A) => 0,
|
|
||||||
LD(LDTarget::A, LDSource::IndirectGroup2(_)) => 0,
|
|
||||||
INC(AllRegisters::Group1(_)) => 0,
|
|
||||||
DEC(AllRegisters::Group1(_)) => 0,
|
|
||||||
INC(AllRegisters::Register(_)) => 0,
|
|
||||||
DEC(AllRegisters::Register(_)) => 0,
|
|
||||||
LD(LDTarget::Register(_), LDSource::ImmediateByte) => 1,
|
|
||||||
RLCA => 0,
|
|
||||||
RRCA => 0,
|
|
||||||
RLA => 0,
|
|
||||||
RRA => 0,
|
|
||||||
DAA => 0,
|
|
||||||
CPL => 0,
|
|
||||||
SCF => 0,
|
|
||||||
CCF => 0,
|
|
||||||
HALT => 0,
|
|
||||||
LD(LDTarget::Register(_), LDSource::Register(_)) => 0,
|
|
||||||
ADD(AddTarget::A, AddSource::Register(_)) => 0,
|
|
||||||
ADC(AluSource::Register(_)) => 0,
|
|
||||||
SUB(AluSource::Register(_)) => 0,
|
|
||||||
SBC(AluSource::Register(_)) => 0,
|
|
||||||
AND(AluSource::Register(_)) => 0,
|
|
||||||
XOR(AluSource::Register(_)) => 0,
|
|
||||||
OR(AluSource::Register(_)) => 0,
|
|
||||||
CP(AluSource::Register(_)) => 0,
|
|
||||||
RET(_) => 0,
|
|
||||||
LD(LDTarget::IoWithImmediateOffset, LDSource::A) => 1,
|
|
||||||
ADD(AddTarget::SP, AddSource::ImmediateSignedByte) => 1,
|
|
||||||
LD(LDTarget::A, LDSource::IoWithImmediateOffset) => 1,
|
|
||||||
LDHL => 1,
|
|
||||||
POP(_) => 0,
|
|
||||||
RETI => 0,
|
|
||||||
JP(JpCond::Always, JpLoc::HL) => 0,
|
|
||||||
LD(LDTarget::SP, LDSource::HL) => 0,
|
|
||||||
JP(_, JpLoc::ImmediateWord) => 2,
|
|
||||||
LD(LDTarget::IoWithC, LDSource::A) => 0,
|
|
||||||
LD(LDTarget::IndirectImmediateWord, LDSource::A) => 2,
|
|
||||||
LD(LDTarget::A, LDSource::IoWithC) => 0,
|
|
||||||
LD(LDTarget::A, LDSource::IndirectImmediateWord) => 2,
|
|
||||||
DI => 0,
|
|
||||||
EI => 0,
|
|
||||||
CALL(_) => 0,
|
|
||||||
PUSH(_) => 0,
|
|
||||||
ADD(AddTarget::A, AddSource::ImmediateByte) => 1,
|
|
||||||
ADC(AluSource::ImmediateByte) => 1,
|
|
||||||
SUB(AluSource::ImmediateByte) => 1,
|
|
||||||
SBC(AluSource::ImmediateByte) => 1,
|
|
||||||
AND(AluSource::ImmediateByte) => 1,
|
|
||||||
XOR(AluSource::ImmediateByte) => 1,
|
|
||||||
OR(AluSource::ImmediateByte) => 1,
|
|
||||||
CP(AluSource::ImmediateByte) => 1,
|
|
||||||
RST(_) => 0,
|
|
||||||
|
|
||||||
// Prefixed
|
|
||||||
RLC(_) => 0,
|
|
||||||
RRC(_) => 0,
|
|
||||||
RL(_) => 0,
|
|
||||||
RR(_) => 0,
|
|
||||||
SLA(_) => 0,
|
|
||||||
SRA(_) => 0,
|
|
||||||
SWAP(_) => 0,
|
|
||||||
SRL(_) => 0,
|
|
||||||
BIT(_, _) => 0,
|
|
||||||
RES(_, _) => 0,
|
|
||||||
SET(_, _) => 0,
|
|
||||||
_ => unreachable!("{:?} is an illegal instruction", instr),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
#![allow(clippy::derivable_impls)] // Will remove this if bitfield-rs allows default impls
|
|
||||||
|
|
||||||
pub use apu::gen::init as spsc_init;
|
pub use apu::gen::init as spsc_init;
|
||||||
pub type Cycle = u64;
|
pub type Cycle = u64;
|
||||||
|
|
||||||
|
149
src/main.rs
149
src/main.rs
@@ -1,28 +1,38 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use clap::{arg, command, value_parser};
|
use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg};
|
||||||
use gb::gui::{EmuMode, Gui};
|
use egui_wgpu_backend::RenderPass;
|
||||||
use gb::{emu, gui};
|
use gb::emu;
|
||||||
|
use gb::gui::GuiState;
|
||||||
use gilrs::Gilrs;
|
use gilrs::Gilrs;
|
||||||
use rodio::{OutputStream, Sink};
|
use rodio::{OutputStream, Sink};
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
use winit::event::{Event, WindowEvent};
|
use winit::event::{Event, WindowEvent};
|
||||||
use winit::event_loop::{EventLoop, EventLoopBuilder};
|
use winit::event_loop::EventLoop;
|
||||||
|
|
||||||
const AUDIO_ENABLED: bool = true;
|
const AUDIO_ENABLED: bool = true;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let m = command!()
|
let app = App::new(crate_name!())
|
||||||
|
.version(crate_version!())
|
||||||
|
.author(crate_authors!())
|
||||||
|
.about(crate_description!());
|
||||||
|
|
||||||
|
let m = app
|
||||||
.arg(
|
.arg(
|
||||||
arg!(-b --boot <FILE> "path to boot ROM")
|
Arg::with_name("rom")
|
||||||
.required(false)
|
.value_name("ROM_FILE")
|
||||||
.value_parser(value_parser!(PathBuf)),
|
.takes_value(true)
|
||||||
|
.index(1)
|
||||||
|
.help("Path to the Game ROM"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
arg!([ROM_FILE] "path to game ROM")
|
Arg::with_name("boot")
|
||||||
.required(true)
|
.short("b")
|
||||||
.value_parser(value_parser!(PathBuf)),
|
.long("boot")
|
||||||
|
.value_name("FILE")
|
||||||
|
.takes_value(true)
|
||||||
|
.help("Path to Boot ROM"),
|
||||||
)
|
)
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|
||||||
@@ -35,8 +45,25 @@ fn main() {
|
|||||||
.with_env_filter(EnvFilter::from_default_env())
|
.with_env_filter(EnvFilter::from_default_env())
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
// Init CPU
|
// --Here lies a lot of winit + wgpu Boilerplate--
|
||||||
let mut cpu = match m.get_one::<PathBuf>("boot") {
|
let event_loop: EventLoop<Event<()>> = EventLoop::with_user_event();
|
||||||
|
let window = gb::gui::build_window(&event_loop).expect("build window");
|
||||||
|
|
||||||
|
let (instance, surface) = gb::gui::create_surface(&window);
|
||||||
|
let adapter = gb::gui::request_adapter(&instance, &surface).expect("request adaptor");
|
||||||
|
let (device, queue) = gb::gui::request_device(&adapter).expect("request device");
|
||||||
|
let format = surface
|
||||||
|
.get_preferred_format(&adapter)
|
||||||
|
.expect("get surface format");
|
||||||
|
|
||||||
|
let mut config = gb::gui::surface_config(&window, format);
|
||||||
|
surface.configure(&device, &config);
|
||||||
|
let mut platform = gb::gui::platform_desc(&window);
|
||||||
|
let mut render_pass = RenderPass::new(&device, format, 1);
|
||||||
|
|
||||||
|
// We interrupt your boiler plate to initialize the emulator so that
|
||||||
|
// we can copy it's empty pixel buffer to the GPU
|
||||||
|
let mut cpu = match m.value_of("boot") {
|
||||||
Some(path) => {
|
Some(path) => {
|
||||||
tracing::info!("User-provided boot ROM");
|
tracing::info!("User-provided boot ROM");
|
||||||
emu::from_boot_rom(path).expect("initialize emulator with custom boot rom")
|
emu::from_boot_rom(path).expect("initialize emulator with custom boot rom")
|
||||||
@@ -47,12 +74,20 @@ fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Set up the wgpu (and then EGUI) texture we'll be working with.
|
||||||
|
let texture_size = gb::gui::texture_size();
|
||||||
|
let texture = gb::gui::create_texture(&device, texture_size);
|
||||||
|
gb::gui::write_to_texture(&queue, &texture, gb::emu::pixel_buf(&cpu), texture_size);
|
||||||
|
let texture_id = gb::gui::expose_texture_to_egui(&mut render_pass, &device, &texture);
|
||||||
|
|
||||||
// Load ROM if filepath was provided
|
// Load ROM if filepath was provided
|
||||||
if let Some(path) = m.get_one::<PathBuf>("ROM_FILE") {
|
if let Some(path) = m.value_of("rom") {
|
||||||
tracing::info!("User-provided cartridge ROM");
|
tracing::info!("User-provided cartridge ROM");
|
||||||
emu::read_game_rom(&mut cpu, path).expect("read game rom from path");
|
emu::read_game_rom(&mut cpu, path).expect("read game rom from path");
|
||||||
}
|
}
|
||||||
|
|
||||||
emu::load_save(&mut cpu);
|
emu::load_save(&mut cpu);
|
||||||
|
|
||||||
let rom_title = emu::rom_title(&cpu).to_string();
|
let rom_title = emu::rom_title(&cpu).to_string();
|
||||||
|
|
||||||
tracing::info!("Initialize Gamepad");
|
tracing::info!("Initialize Gamepad");
|
||||||
@@ -79,44 +114,80 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set up state for the Immediate-mode GUI
|
// Set up state for the Immediate-mode GUI
|
||||||
let event_loop: EventLoop<Event<()>> = EventLoopBuilder::with_user_event().build();
|
let mut app = GuiState::new(rom_title);
|
||||||
|
let mut last_key = gb::gui::unused_key();
|
||||||
let mut app = Gui::new(rom_title, &event_loop, &cpu);
|
|
||||||
let mut last_key = gui::unused_key(); // TODO: Fix this awful impl
|
|
||||||
|
|
||||||
// used for egui animations
|
// used for egui animations
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
|
|
||||||
event_loop.run(move |event, _, control_flow| {
|
event_loop.run(move |event, _, control_flow| {
|
||||||
app.handle_event(&event);
|
platform.handle_event(&event);
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
Event::MainEventsCleared => {
|
Event::MainEventsCleared => {
|
||||||
app.maybe_quit(&cpu, control_flow);
|
if app.quit {
|
||||||
|
emu::save_and_exit(&cpu, control_flow);
|
||||||
|
}
|
||||||
|
|
||||||
match app.mode {
|
gb::emu::run_frame(&mut cpu, &mut gamepad, last_key);
|
||||||
EmuMode::Running => emu::run_frame(&mut cpu, &mut gamepad, last_key),
|
|
||||||
EmuMode::StepFrame if gui::kbd::space_released(&last_key) => {
|
|
||||||
emu::run_frame(&mut cpu, &mut gamepad, last_key)
|
|
||||||
}
|
|
||||||
EmuMode::Step if gui::kbd::space_released(&last_key) => {
|
|
||||||
emu::run(&mut cpu, &mut gamepad, last_key, 4);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Input has been consumed, reset it
|
window.request_redraw();
|
||||||
last_key = gui::unused_key();
|
|
||||||
|
|
||||||
app.request_redraw();
|
|
||||||
}
|
}
|
||||||
Event::RedrawRequested(..) => {
|
Event::RedrawRequested(..) => {
|
||||||
app.update_time(start_time.elapsed().as_secs_f64());
|
platform.update_time(start_time.elapsed().as_secs_f64());
|
||||||
app.paint(&cpu);
|
|
||||||
|
let data = gb::emu::pixel_buf(&cpu);
|
||||||
|
gb::gui::write_to_texture(&queue, &texture, data, texture_size);
|
||||||
|
|
||||||
|
let output_frame = match surface.get_current_texture() {
|
||||||
|
Ok(frame) => frame,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Dropped frame with error: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let output_view = gb::gui::create_view(&output_frame);
|
||||||
|
|
||||||
|
// Begin to draw Egui components
|
||||||
|
platform.begin_frame();
|
||||||
|
gb::gui::draw_egui(&mut app, &platform.context(), texture_id);
|
||||||
|
// End the UI frame. We could now handle the output and draw the UI with the backend.
|
||||||
|
let (_, paint_commands) = platform.end_frame(Some(&window));
|
||||||
|
let paint_jobs = platform.context().tessellate(paint_commands);
|
||||||
|
|
||||||
|
let mut encoder = gb::gui::create_command_encoder(&device);
|
||||||
|
let screen_descriptor = gb::gui::create_screen_descriptor(&window, &config);
|
||||||
|
|
||||||
|
// Upload all resources for the GPU.
|
||||||
|
render_pass.update_texture(&device, &queue, &platform.context().texture());
|
||||||
|
render_pass.update_user_textures(&device, &queue);
|
||||||
|
render_pass.update_buffers(&device, &queue, &paint_jobs, &screen_descriptor);
|
||||||
|
|
||||||
|
// Record all render passes.
|
||||||
|
gb::gui::execute_render_pass(
|
||||||
|
&mut render_pass,
|
||||||
|
&mut encoder,
|
||||||
|
&output_view,
|
||||||
|
paint_jobs,
|
||||||
|
&screen_descriptor,
|
||||||
|
)
|
||||||
|
.expect("record render passes");
|
||||||
|
|
||||||
|
// Submit the commands.
|
||||||
|
queue.submit(std::iter::once(encoder.finish()));
|
||||||
|
|
||||||
|
// Redraw egui
|
||||||
|
output_frame.present();
|
||||||
}
|
}
|
||||||
Event::WindowEvent { event, .. } => match event {
|
Event::WindowEvent { event, .. } => match event {
|
||||||
WindowEvent::Resized(size) => app.resize(size),
|
WindowEvent::Resized(size) => {
|
||||||
WindowEvent::CloseRequested => emu::save_and_exit(&cpu, control_flow),
|
config.width = size.width;
|
||||||
|
config.height = size.height;
|
||||||
|
surface.configure(&device, &config);
|
||||||
|
}
|
||||||
|
WindowEvent::CloseRequested => {
|
||||||
|
emu::save_and_exit(&cpu, control_flow);
|
||||||
|
}
|
||||||
WindowEvent::KeyboardInput { input, .. } => last_key = input,
|
WindowEvent::KeyboardInput { input, .. } => last_key = input,
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
|
153
src/ppu.rs
153
src/ppu.rs
@@ -10,8 +10,6 @@ use types::{
|
|||||||
ObjectPaletteKind, ObjectSize, Pixels, RenderPriority, TileDataAddress,
|
ObjectPaletteKind, ObjectSize, Pixels, RenderPriority, TileDataAddress,
|
||||||
};
|
};
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
|
|
||||||
mod dma;
|
mod dma;
|
||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
@@ -34,14 +32,6 @@ const LIGHT_GRAY: [u8; 4] = 0xAEBA89FFu32.to_be_bytes();
|
|||||||
const DARK_GRAY: [u8; 4] = 0x5E6745FFu32.to_be_bytes();
|
const DARK_GRAY: [u8; 4] = 0x5E6745FFu32.to_be_bytes();
|
||||||
const BLACK: [u8; 4] = 0x202020FFu32.to_be_bytes();
|
const BLACK: [u8; 4] = 0x202020FFu32.to_be_bytes();
|
||||||
|
|
||||||
static BLANK_SCREEN: Lazy<Box<[u8; (GB_WIDTH * 4) * GB_HEIGHT]>> = Lazy::new(|| {
|
|
||||||
WHITE
|
|
||||||
.repeat(GB_WIDTH * GB_HEIGHT)
|
|
||||||
.into_boxed_slice()
|
|
||||||
.try_into()
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Ppu {
|
pub struct Ppu {
|
||||||
pub(crate) int: Interrupt,
|
pub(crate) int: Interrupt,
|
||||||
@@ -58,7 +48,7 @@ pub struct Ppu {
|
|||||||
fetch: PixelFetcher,
|
fetch: PixelFetcher,
|
||||||
fifo: PixelFifo,
|
fifo: PixelFifo,
|
||||||
obj_buffer: ObjectBuffer,
|
obj_buffer: ObjectBuffer,
|
||||||
pub(crate) frame_buf: FrameBuffer,
|
pub(crate) frame_buf: Box<[u8; GB_WIDTH * GB_HEIGHT * 4]>,
|
||||||
win_stat: WindowStatus,
|
win_stat: WindowStatus,
|
||||||
|
|
||||||
scanline_start: bool,
|
scanline_start: bool,
|
||||||
@@ -80,26 +70,15 @@ impl BusIo for Ppu {
|
|||||||
|
|
||||||
impl Ppu {
|
impl Ppu {
|
||||||
pub(crate) fn tick(&mut self) {
|
pub(crate) fn tick(&mut self) {
|
||||||
if !self.ctrl.lcd_enabled() {
|
self.dot += 1;
|
||||||
if self.dot > 0 {
|
|
||||||
// Check ensures this expensive operation only happens once
|
|
||||||
self.frame_buf
|
|
||||||
.get_mut(Device::Guest)
|
|
||||||
.copy_from_slice(BLANK_SCREEN.as_ref());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.stat.set_mode(PpuMode::HBlank);
|
if !self.ctrl.lcd_enabled() {
|
||||||
self.pos.line_y = 0;
|
|
||||||
self.dot = 0;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.dot += 1;
|
|
||||||
|
|
||||||
match self.stat.mode() {
|
match self.stat.mode() {
|
||||||
PpuMode::OamScan => {
|
PpuMode::OamScan => {
|
||||||
// Cycles 1 -> 80
|
// Cycles 1 -> 80
|
||||||
|
|
||||||
if self.dot >= 80 {
|
if self.dot >= 80 {
|
||||||
self.x_pos = 0;
|
self.x_pos = 0;
|
||||||
self.scanline_start = true;
|
self.scanline_start = true;
|
||||||
@@ -125,7 +104,12 @@ impl Ppu {
|
|||||||
self.scan_oam();
|
self.scan_oam();
|
||||||
}
|
}
|
||||||
PpuMode::Drawing => {
|
PpuMode::Drawing => {
|
||||||
self.draw();
|
if self.ctrl.lcd_enabled() {
|
||||||
|
// Only Draw when the LCD Is Enabled
|
||||||
|
self.draw();
|
||||||
|
} else {
|
||||||
|
self.reset();
|
||||||
|
}
|
||||||
|
|
||||||
if self.x_pos == 160 {
|
if self.x_pos == 160 {
|
||||||
if self.stat.hblank_int() {
|
if self.stat.hblank_int() {
|
||||||
@@ -180,9 +164,6 @@ impl Ppu {
|
|||||||
self.int.set_lcd_stat(true);
|
self.int.set_lcd_stat(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Screen is done drawing
|
|
||||||
self.frame_buf.swap();
|
|
||||||
|
|
||||||
PpuMode::VBlank
|
PpuMode::VBlank
|
||||||
} else {
|
} else {
|
||||||
if self.stat.oam_int() {
|
if self.stat.oam_int() {
|
||||||
@@ -416,8 +397,8 @@ impl Ppu {
|
|||||||
let x = self.x_pos as usize;
|
let x = self.x_pos as usize;
|
||||||
|
|
||||||
let i = (GB_WIDTH * 4) * y + (x * 4);
|
let i = (GB_WIDTH * 4) * y + (x * 4);
|
||||||
|
self.frame_buf[i..(i + rgba.len())].copy_from_slice(&rgba);
|
||||||
|
|
||||||
self.frame_buf.get_mut(Device::Guest)[i..(i + rgba.len())].copy_from_slice(&rgba);
|
|
||||||
self.x_pos += 1;
|
self.x_pos += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,6 +415,14 @@ impl Ppu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.pos.line_y = 0;
|
||||||
|
self.stat.set_mode(PpuMode::HBlank);
|
||||||
|
|
||||||
|
let mut blank = WHITE.repeat(self.frame_buf.len() / 4);
|
||||||
|
self.frame_buf.swap_with_slice(&mut blank);
|
||||||
|
}
|
||||||
|
|
||||||
fn clock_fifo(&mut self) -> Option<GrayShade> {
|
fn clock_fifo(&mut self) -> Option<GrayShade> {
|
||||||
use RenderPriority::*;
|
use RenderPriority::*;
|
||||||
|
|
||||||
@@ -479,7 +468,7 @@ impl Default for Ppu {
|
|||||||
Self {
|
Self {
|
||||||
vram: Box::new([0u8; VRAM_SIZE]),
|
vram: Box::new([0u8; VRAM_SIZE]),
|
||||||
dot: Default::default(),
|
dot: Default::default(),
|
||||||
frame_buf: FrameBuffer::new().expect("create frame buffers"),
|
frame_buf: Box::new([0; GB_WIDTH * GB_HEIGHT * 4]),
|
||||||
int: Default::default(),
|
int: Default::default(),
|
||||||
ctrl: LCDControl(0),
|
ctrl: LCDControl(0),
|
||||||
monochrome: Default::default(),
|
monochrome: Default::default(),
|
||||||
@@ -808,12 +797,21 @@ impl Default for BackgroundFetcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug)]
|
||||||
struct ObjectFetcher {
|
struct ObjectFetcher {
|
||||||
state: FetcherState,
|
state: FetcherState,
|
||||||
tile: TileBuilder,
|
tile: TileBuilder,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for ObjectFetcher {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
state: Default::default(),
|
||||||
|
tile: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Fetcher for ObjectFetcher {
|
impl Fetcher for ObjectFetcher {
|
||||||
fn reset(&mut self) {
|
fn reset(&mut self) {
|
||||||
self.state = Default::default();
|
self.state = Default::default();
|
||||||
@@ -921,96 +919,3 @@ struct WindowStatus {
|
|||||||
/// drawing from the window tile map is true
|
/// drawing from the window tile map is true
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) mod dbg {
|
|
||||||
use super::{Ppu, PpuMode};
|
|
||||||
use crate::Cycle;
|
|
||||||
|
|
||||||
pub(crate) fn ly(ppu: &Ppu) -> u8 {
|
|
||||||
ppu.pos.line_y
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn scx(ppu: &Ppu) -> u8 {
|
|
||||||
ppu.pos.scroll_x
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn scy(ppu: &Ppu) -> u8 {
|
|
||||||
ppu.pos.scroll_y
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn mode(ppu: &Ppu) -> PpuMode {
|
|
||||||
ppu.stat.mode()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn wx(ppu: &Ppu) -> i16 {
|
|
||||||
ppu.pos.window_x as i16
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn wy(ppu: &Ppu) -> i16 {
|
|
||||||
ppu.pos.window_y as i16
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn dot(ppu: &Ppu) -> Cycle {
|
|
||||||
ppu.dot
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct FrameBuffer {
|
|
||||||
buf: [Box<[u8; Self::FRAME_LEN]>; 2],
|
|
||||||
current: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
pub enum Device {
|
|
||||||
Guest,
|
|
||||||
Host,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FrameBuffer {
|
|
||||||
const FRAME_LEN: usize = GB_WIDTH * std::mem::size_of::<u32>() * GB_HEIGHT;
|
|
||||||
|
|
||||||
pub fn new() -> Result<Self, FrameBufferError> {
|
|
||||||
Ok(Self {
|
|
||||||
buf: [
|
|
||||||
vec![0; Self::FRAME_LEN]
|
|
||||||
.into_boxed_slice()
|
|
||||||
.try_into()
|
|
||||||
.map_err(|_| FrameBufferError::TryFrom)?,
|
|
||||||
vec![0; Self::FRAME_LEN]
|
|
||||||
.into_boxed_slice()
|
|
||||||
.try_into()
|
|
||||||
.map_err(|_| FrameBufferError::TryFrom)?,
|
|
||||||
],
|
|
||||||
current: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn swap(&mut self) {
|
|
||||||
self.current = !self.current;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_mut(&mut self, device: Device) -> &mut [u8; Self::FRAME_LEN] {
|
|
||||||
let idx = match device {
|
|
||||||
Device::Guest => self.current,
|
|
||||||
Device::Host => !self.current,
|
|
||||||
};
|
|
||||||
|
|
||||||
&mut *self.buf[idx as usize]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&self, device: Device) -> &[u8; Self::FRAME_LEN] {
|
|
||||||
let idx = match device {
|
|
||||||
Device::Guest => self.current,
|
|
||||||
Device::Host => !self.current,
|
|
||||||
};
|
|
||||||
|
|
||||||
&*self.buf[idx as usize]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum FrameBufferError {
|
|
||||||
#[error("Failed to coerce boxed slice to boxed array")]
|
|
||||||
TryFrom,
|
|
||||||
}
|
|
||||||
|
@@ -335,7 +335,7 @@ impl From<ObjectPaletteKind> for u8 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum RenderPriority {
|
pub enum RenderPriority {
|
||||||
Object = 0,
|
Object = 0,
|
||||||
BackgroundAndWindow = 1,
|
BackgroundAndWindow = 1,
|
||||||
|
Reference in New Issue
Block a user