Compare commits
	
		
			47 Commits
		
	
	
		
			80a15039e0
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8a352ca807 | |||
| 068e4124a6 | |||
| fedae65cc8 | |||
| 41586d8cc4 | |||
| 79740ac245 | |||
| d6bfde081e | |||
| d2f57ee66b | |||
| 3841d3a98c | |||
| 85940c8744 | |||
| c7adf54976 | |||
| f0f9003247 | |||
| 5eba097ad4 | |||
| 756c806c2a | |||
| 77af5453f0 | |||
| 41bc4f7afe | |||
| 36e46d3780 | |||
| 926c66746d | |||
| cf2357f917 | |||
| 3fb7edd333 | |||
| 3ab512d663 | |||
| 61156ca8a8 | |||
| 2c411d5792 | |||
| 4c657c9832 | |||
| e72b11f946 | |||
| bbcbbd8ce3 | |||
| c4226e6e76 | |||
| 409314a4e5 | |||
| 952bf68bd0 | |||
| 865c11d53c | |||
| 90d2da9272 | |||
| 4a1a21a08f | |||
| 9d23b571fb | |||
| 70e0b5868d | |||
| a0e3c7c602 | |||
| ef4f3d9ec6 | |||
| 7f872a554b | |||
| dda0257655 | |||
| c10816c048 | |||
| 2405fd027f | |||
| 939c25ce1a | |||
| 6f5e863645 | |||
| ce5d58e1d2 | |||
| ebcd5bec5c | |||
| f2c49b398c | |||
| 6f76571d6c | |||
| 55da5a29d8 | |||
| 7dfcd44820 | 
							
								
								
									
										3
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							| @@ -1,8 +1,9 @@ | ||||
| { | ||||
|     "recommendations": [ | ||||
|         "matklad.rust-analyzer", | ||||
|         "rust-lang.rust-analyzer", | ||||
|         "tamasfe.even-better-toml", | ||||
|         "serayuzgur.crates", | ||||
|         "vadimcn.vscode-lldb", | ||||
|         "donaldhays.rgbds-z80" | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										2625
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2625
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										20
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								Cargo.toml
									
									
									
									
									
								
							| @@ -8,17 +8,21 @@ edition = "2021" | ||||
|  | ||||
| [dependencies] | ||||
| anyhow = "1.0" | ||||
| bitfield = "0.13" | ||||
| clap = "2.33" | ||||
| gilrs = "0.8" | ||||
| pixels = "0.7" | ||||
| winit = "0.25" | ||||
| winit_input_helper = "0.10" | ||||
| rodio = "0.14" | ||||
| rtrb = "0.2" | ||||
| bitfield = "0.17" | ||||
| clap = { version = "4.0", features = ["cargo"] } | ||||
| gilrs = "0.11" | ||||
| winit = "0.28" | ||||
| egui = "0.21" | ||||
| egui_wgpu_backend = "0.22" | ||||
| egui_winit_platform = "0.18" | ||||
| pollster = "0.3" | ||||
| rodio = "0.19" | ||||
| rtrb = "0.3" | ||||
| directories-next = "2.0" | ||||
| tracing = "0.1" | ||||
| tracing-subscriber = { version = "0.3", features = ["std", "env-filter"] } | ||||
| thiserror = "1.0" | ||||
| once_cell = "1.10" | ||||
|  | ||||
| [profile.release] | ||||
| debug = true | ||||
|   | ||||
							
								
								
									
										13
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,5 +1,4 @@ | ||||
| # Rekai's Gameboy Emulator | ||||
| [](https://ci.paoda.moe/paoda/gb) | ||||
|  | ||||
| ### Status | ||||
| * From [Blargg Test ROMs](https://github.com/L-P/blargg-test-roms/) | ||||
| @@ -7,7 +6,7 @@ | ||||
|     * [x] instr_timing | ||||
|     * [x] mem_timing | ||||
|     * [x] mem_timing-2 | ||||
|     * [ ] Partially dmg_sound  | ||||
|     * [ ] dmg_sound (partial)  | ||||
| * [x] [dmg-acid2](https://github.com/mattcurrie/dmg-acid2) | ||||
| * From [mooneye-gb](https://github.com/Gekkio/mooneye-gb): | ||||
|     * Cartridges: | ||||
| @@ -16,14 +15,20 @@ | ||||
|         * [x] MBC2 | ||||
|         * [x] MBC5 | ||||
| * Implements a cycle-accurate PPU FIFO | ||||
|     * Doesn't \*exactly\* work right just yet | ||||
|     * Doesn't \*exactly\* work just yet | ||||
|  | ||||
| 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 are defined [here](https://git.musuka.dev/paoda/gb/src/branch/main/src/joypad.rs#L114) | ||||
| Controls are defined [here](https://github.com/paoda/gb/blob/85940c874460b9cb31bf9b8211bf7dda596d114a/src/joypad.rs#L114) | ||||
|  | ||||
| Key | Button | ||||
| --- | --- | ||||
|   | ||||
							
								
								
									
										28
									
								
								src/apu.rs
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								src/apu.rs
									
									
									
									
									
								
							| @@ -30,7 +30,7 @@ pub struct Apu { | ||||
|     fs: FrameSequencer, | ||||
|     div_prev: Option<u16>, | ||||
|  | ||||
|     prod: Option<SampleProducer<f32>>, | ||||
|     pub(crate) prod: Option<SampleProducer<f32>>, | ||||
|     sample_counter: u64, | ||||
|  | ||||
|     cap: f32, | ||||
| @@ -188,10 +188,6 @@ impl Apu { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn attach_producer(&mut self, prod: SampleProducer<f32>) { | ||||
|         self.prod = Some(prod); | ||||
|     } | ||||
|  | ||||
|     fn reset(&mut self) { | ||||
|         self.ch1.sweep = Default::default(); | ||||
|         self.ch1.duty = Default::default(); | ||||
| @@ -215,8 +211,8 @@ impl Apu { | ||||
|         self.ch4.poly = Default::default(); | ||||
|         self.ch4.freq = Default::default(); | ||||
|  | ||||
|         self.ctrl.channel = Default::default(); | ||||
|         self.ctrl.out = Default::default(); | ||||
|         self.ctrl.channel = ChannelControl(0); | ||||
|         self.ctrl.out = SoundOutput(0); | ||||
|  | ||||
|         // Disable the Channels | ||||
|         self.ch1.enabled = Default::default(); | ||||
| @@ -348,7 +344,7 @@ impl Apu { | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Default)] | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct SoundControl { | ||||
|     /// 0xFF24 | NR50 - Channel Control | ||||
|     channel: ChannelControl, | ||||
| @@ -358,6 +354,16 @@ pub(crate) struct SoundControl { | ||||
|     enabled: bool, | ||||
| } | ||||
|  | ||||
| impl Default for SoundControl { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             channel: ChannelControl(0), | ||||
|             out: SoundOutput(0), | ||||
|             enabled: Default::default(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl SoundControl { | ||||
|     /// 0xFF24 | NR50 - Channel Control | ||||
|     pub(crate) fn channel(&self) -> u8 { | ||||
| @@ -581,7 +587,7 @@ impl Channel1 { | ||||
| pub(crate) struct Channel2 { | ||||
|     /// 0xFF16 | NR21 - Channel 2 Sound length / Wave Pattern Duty | ||||
|     duty: SoundDuty, | ||||
|     /// 0xFF17 | NR22 - Channel 2 Volume ENvelope | ||||
|     /// 0xFF17 | NR22 - Channel 2 Volume Envelope | ||||
|     envelope: VolumeEnvelope, | ||||
|     /// 0xFF18 | NR23 - Channel 2 Frequency low (lower 8 bits only) | ||||
|     freq_lo: u8, | ||||
| @@ -613,12 +619,12 @@ impl Channel2 { | ||||
|         self.length_counter = 64 - self.duty.sound_length() as u16; | ||||
|     } | ||||
|  | ||||
|     /// 0xFF17 | NR22 - Channel 2 Volume ENvelope | ||||
|     /// 0xFF17 | NR22 - Channel 2 Volume Envelope | ||||
|     pub(crate) fn envelope(&self) -> u8 { | ||||
|         u8::from(self.envelope) | ||||
|     } | ||||
|  | ||||
|     /// 0xFF17 | NR22 - Channel 2 Volume ENvelope | ||||
|     /// 0xFF17 | NR22 - Channel 2 Volume Envelope | ||||
|     pub(crate) fn set_envelope(&mut self, byte: u8) { | ||||
|         self.envelope = byte.into(); | ||||
|  | ||||
|   | ||||
| @@ -169,7 +169,7 @@ pub(super) mod ch4 { | ||||
|             match byte & 0x01 { | ||||
|                 0b00 => Self::Short, | ||||
|                 0b01 => Self::Long, | ||||
|                 _ => unreachable!("{:#04X} is not a valid value for CounterWidth"), | ||||
|                 _ => unreachable!("{:#04X} is not a valid value for CounterWidth", byte), | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -416,12 +416,6 @@ pub(super) mod common { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl Default for WavePattern { | ||||
|         fn default() -> Self { | ||||
|             Self::OneEighth // Rationale: OneEighth is 0x00 | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl From<WavePattern> for u8 { | ||||
|         fn from(pattern: WavePattern) -> Self { | ||||
|             pattern as Self | ||||
| @@ -479,12 +473,6 @@ impl Clone for SoundOutput { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for SoundOutput { | ||||
|     fn default() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for SoundOutput { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         Self(byte) | ||||
| @@ -523,12 +511,6 @@ impl Clone for ChannelControl { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for ChannelControl { | ||||
|     fn default() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for ChannelControl { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         Self(byte) | ||||
|   | ||||
							
								
								
									
										73
									
								
								src/bus.rs
									
									
									
									
									
								
							
							
						
						
									
										73
									
								
								src/bus.rs
									
									
									
									
									
								
							| @@ -10,37 +10,19 @@ use crate::work_ram::{VariableWorkRam, WorkRam}; | ||||
|  | ||||
| pub(crate) const BOOT_SIZE: usize = 0x100; | ||||
|  | ||||
| #[derive(Debug)] | ||||
| #[derive(Debug, Default)] | ||||
| pub struct Bus { | ||||
|     boot: Option<[u8; BOOT_SIZE]>, // Boot ROM is 256b long | ||||
|     cart: Option<Cartridge>, | ||||
|     ppu: Ppu, | ||||
|     pub(crate) cart: Option<Cartridge>, | ||||
|     pub(crate) ppu: Ppu, | ||||
|     work_ram: WorkRam, | ||||
|     var_ram: VariableWorkRam, | ||||
|     timer: Timer, | ||||
|     int: Interrupt, | ||||
|     apu: Apu, | ||||
|     pub(crate) apu: Apu, | ||||
|     high_ram: HighRam, | ||||
|     serial: Serial, | ||||
|     joypad: 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(), | ||||
|             joypad: Default::default(), | ||||
|         } | ||||
|     } | ||||
|     pub(crate) joyp: Joypad, | ||||
| } | ||||
|  | ||||
| impl Bus { | ||||
| @@ -51,14 +33,6 @@ impl Bus { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn load_cart(&mut self, rom: Vec<u8>) { | ||||
|         self.cart = Some(Cartridge::new(rom)); | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn cart_title(&self) -> Option<&str> { | ||||
|         self.cart.as_ref()?.title() | ||||
|     } | ||||
|  | ||||
|     #[allow(dead_code)] | ||||
|     pub(crate) fn boot_mapped(&self) -> bool { | ||||
|         self.boot.is_some() | ||||
| @@ -73,6 +47,10 @@ impl Bus { | ||||
|     fn tick(&mut self, limit: u8) { | ||||
|         for _ in 0..limit { | ||||
|             self.timer.tick(); | ||||
|  | ||||
|             if let Some(c) = self.cart.as_mut() { | ||||
|                 c.tick() | ||||
|             } | ||||
|             self.ppu.tick(); | ||||
|             self.apu.tick(self.timer.divider); | ||||
|             self.dma_tick() | ||||
| @@ -85,31 +63,6 @@ impl Bus { | ||||
|             self.oam_write_byte(dest_addr, byte); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     pub(crate) fn joyp_mut(&mut self) -> &mut Joypad { | ||||
|         &mut self.joypad | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     pub(crate) fn cart(&self) -> Option<&Cartridge> { | ||||
|         self.cart.as_ref() | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     pub(crate) fn cart_mut(&mut self) -> Option<&mut Cartridge> { | ||||
|         self.cart.as_mut() | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     pub(crate) fn apu_mut(&mut self) -> &mut Apu { | ||||
|         &mut self.apu | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     pub fn ppu(&self) -> &Ppu { | ||||
|         &self.ppu | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Bus { | ||||
| @@ -239,7 +192,7 @@ impl BusIo for Bus { | ||||
|                 // Every address here starts with 0xFF so we can just check the | ||||
|                 // low byte to figure out which register it is | ||||
|                 match addr & 0x00FF { | ||||
|                     0x00 => self.joypad.p1, | ||||
|                     0x00 => self.joyp.p1, | ||||
|                     0x01 => self.serial.next, | ||||
|                     0x02 => self.serial.ctrl.into(), | ||||
|                     0x04 => (self.timer.divider >> 8) as u8, | ||||
| @@ -337,7 +290,7 @@ impl BusIo for Bus { | ||||
|                 // Every address here starts with 0xFF so we can just check the | ||||
|                 // low byte to figure out which register it is | ||||
|                 match addr & 0x00FF { | ||||
|                     0x00 => self.joypad.update(byte), | ||||
|                     0x00 => self.joyp.update(byte), | ||||
|                     0x01 => self.serial.next = byte, | ||||
|                     0x02 => self.serial.ctrl = byte.into(), | ||||
|                     0x04 => self.timer.divider = 0x0000, | ||||
| @@ -401,7 +354,7 @@ impl Bus { | ||||
|         let lcd_stat = self.ppu.int.lcd_stat(); | ||||
|  | ||||
|         // Read the current interrupt information from the Joypad | ||||
|         let joypad = self.joypad.interrupt(); | ||||
|         let joypad = self.joyp.interrupt(); | ||||
|  | ||||
|         // Read the current interrupt information from the Timer | ||||
|         let timer = self.timer.interrupt(); | ||||
| @@ -431,7 +384,7 @@ impl Bus { | ||||
|         self.ppu.int.set_lcd_stat(lcd_stat); | ||||
|  | ||||
|         // Update the Joypad's instance of the following interrupts | ||||
|         self.joypad.set_interrupt(joypad); | ||||
|         self.joyp.set_interrupt(joypad); | ||||
|  | ||||
|         // Update the Timer's instance of the following interrupts | ||||
|         self.timer.set_interrupt(timer); | ||||
|   | ||||
							
								
								
									
										749
									
								
								src/cartridge.rs
									
									
									
									
									
								
							
							
						
						
									
										749
									
								
								src/cartridge.rs
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										92
									
								
								src/cpu.rs
									
									
									
									
									
								
							
							
						
						
									
										92
									
								
								src/cpu.rs
									
									
									
									
									
								
							| @@ -5,7 +5,7 @@ use crate::Cycle; | ||||
| use bitfield::bitfield; | ||||
| use std::fmt::{Display, Formatter, Result as FmtResult}; | ||||
|  | ||||
| #[derive(Debug, Default)] | ||||
| #[derive(Debug)] | ||||
| pub struct Cpu { | ||||
|     pub(crate) bus: Bus, | ||||
|     reg: Registers, | ||||
| @@ -15,29 +15,13 @@ pub struct Cpu { | ||||
| } | ||||
|  | ||||
| impl Cpu { | ||||
|     #[allow(dead_code)] | ||||
|     pub(crate) fn without_boot() -> Self { | ||||
|         Self { | ||||
|             reg: Registers { | ||||
|                 a: 0x01, | ||||
|                 b: 0x00, | ||||
|                 c: 0x13, | ||||
|                 d: 0x00, | ||||
|                 e: 0xD8, | ||||
|                 h: 0x01, | ||||
|                 l: 0x4D, | ||||
|                 sp: 0xFFFE, | ||||
|                 pc: 0x0100, | ||||
|             }, | ||||
|             flags: 0xb0.into(), | ||||
|             ..Default::default() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn with_boot(rom: [u8; BOOT_SIZE]) -> Self { | ||||
|     pub(crate) fn new(rom: [u8; BOOT_SIZE]) -> Self { | ||||
|         Self { | ||||
|             bus: Bus::with_boot(rom), | ||||
|             ..Default::default() | ||||
|             reg: Default::default(), | ||||
|             flags: Flags(0), | ||||
|             ime: ImeState::Disabled, | ||||
|             state: State::Execute, | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -69,6 +53,12 @@ impl Cpu { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for Cpu { | ||||
|     fn default() -> Self { | ||||
|         Cpu::new(*include_bytes!("../bin/bootix_dmg.bin")) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Cpu { | ||||
|     /// Fetch an [Instruction] from the memory bus | ||||
|     /// (4 cycles) | ||||
| @@ -151,16 +141,6 @@ impl BusIo for Cpu { | ||||
| } | ||||
|  | ||||
| impl Cpu { | ||||
|     #[inline] | ||||
|     pub(crate) fn bus(&self) -> &Bus { | ||||
|         &self.bus | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     pub(crate) fn bus_mut(&mut self) -> &mut Bus { | ||||
|         &mut self.bus | ||||
|     } | ||||
|  | ||||
|     fn handle_ei(&mut self) { | ||||
|         match self.ime { | ||||
|             ImeState::EiExecuted => self.ime = ImeState::Pending, | ||||
| @@ -258,12 +238,6 @@ enum State { | ||||
|     // Stop, | ||||
| } | ||||
|  | ||||
| impl Default for State { | ||||
|     fn default() -> Self { | ||||
|         Self::Execute | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Cpu { | ||||
|     pub(crate) fn set_register(&mut self, register: Register, value: u8) { | ||||
|         use Register::*; | ||||
| @@ -457,12 +431,6 @@ impl Clone for Flags { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for Flags { | ||||
|     fn default() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Display for Flags { | ||||
|     fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { | ||||
|         if self.z() { | ||||
| @@ -518,8 +486,38 @@ pub(crate) enum ImeState { | ||||
|     Enabled, | ||||
| } | ||||
|  | ||||
| impl Default for ImeState { | ||||
|     fn default() -> Self { | ||||
|         Self::Disabled | ||||
| 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) | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										244
									
								
								src/emu.rs
									
									
									
									
									
								
							
							
						
						
									
										244
									
								
								src/emu.rs
									
									
									
									
									
								
							| @@ -1,7 +1,7 @@ | ||||
| use crate::apu::gen::SampleProducer; | ||||
| use crate::bus::BOOT_SIZE; | ||||
| use crate::cartridge::Cartridge; | ||||
| use crate::cpu::Cpu; | ||||
| use crate::joypad::{self, Joypad}; | ||||
| use crate::{Cycle, GB_HEIGHT, GB_WIDTH}; | ||||
| use clap::crate_name; | ||||
| use gilrs::Gilrs; | ||||
| @@ -9,137 +9,155 @@ use std::fs::File; | ||||
| use std::io::{Read, Write}; | ||||
| use std::path::{Path, PathBuf}; | ||||
| use std::time::Duration; | ||||
| use winit_input_helper::WinitInputHelper; | ||||
| use thiserror::Error; | ||||
| use winit::event::KeyboardInput; | ||||
| use winit::event_loop::ControlFlow; | ||||
|  | ||||
| pub const SM83_CYCLE_TIME: Duration = Duration::from_nanos(1_000_000_000 / SM83_CLOCK_SPEED); | ||||
| pub const CYCLES_IN_FRAME: Cycle = 456 * 154; // 456 Cycles times 154 scanlines | ||||
| pub(crate) const SM83_CLOCK_SPEED: u64 = 0x40_0000; // Hz which is 4.194304Mhz | ||||
| const DEFAULT_TITLE: &str = "DMG-01 Emulator"; | ||||
| const DEFAULT_TITLE: &str = "Game Boy Screen"; | ||||
|  | ||||
| pub fn run_frame(emu: &mut Emulator, gamepad: &mut Gilrs, key: &WinitInputHelper) -> Cycle { | ||||
| #[inline] | ||||
| 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; | ||||
|  | ||||
|     if let Some(event) = gamepad.next_event() { | ||||
|         joypad::handle_gamepad_input(emu.joyp_mut(), event); | ||||
|         crate::joypad::handle_gamepad_input(&mut cpu.bus.joyp, event); | ||||
|     } | ||||
|     joypad::handle_keyboard_input(emu.joyp_mut(), key); | ||||
|     crate::joypad::handle_keyboard_input(&mut cpu.bus.joyp, key); | ||||
|  | ||||
|     while elapsed < CYCLES_IN_FRAME { | ||||
|         elapsed += emu.step(); | ||||
|     } | ||||
|  | ||||
|     elapsed | ||||
| } | ||||
|  | ||||
| pub fn draw_frame(emu: &Emulator, buf: &mut [u8; GB_HEIGHT * GB_WIDTH * 4]) { | ||||
|     buf.copy_from_slice(emu.cpu.bus().ppu().frame_buf()); | ||||
| } | ||||
|  | ||||
| pub struct Emulator { | ||||
|     cpu: Cpu, | ||||
|     timestamp: Cycle, | ||||
| } | ||||
|  | ||||
| impl Default for Emulator { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     while elapsed < cycles { | ||||
|         elapsed += cpu.step(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Emulator { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             cpu: Cpu::with_boot(*include_bytes!("../bin/bootix_dmg.bin")), | ||||
|             timestamp: Default::default(), | ||||
| pub fn save_and_exit(cpu: &Cpu, control_flow: &mut ControlFlow) { | ||||
|     write_save(cpu); | ||||
|     *control_flow = ControlFlow::Exit; | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| pub fn pixel_buf(cpu: &Cpu) -> &[u8; GB_HEIGHT * GB_WIDTH * 4] { | ||||
|     use crate::ppu::Device; | ||||
|     cpu.bus.ppu.frame_buf.get(Device::Host) | ||||
| } | ||||
|  | ||||
| pub fn from_boot_rom<P: AsRef<Path>>(path: P) -> std::io::Result<Cpu> { | ||||
|     Ok(Cpu::new(read_boot(path)?)) | ||||
| } | ||||
|  | ||||
| pub fn read_game_rom<P: AsRef<Path>>(cpu: &mut Cpu, path: P) -> std::io::Result<()> { | ||||
|     cpu.bus.cart = Some(Cartridge::new(std::fs::read(path.as_ref())?)); | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| pub fn set_audio_prod(cpu: &mut Cpu, prod: SampleProducer<f32>) { | ||||
|     cpu.bus.apu.prod = Some(prod); | ||||
| } | ||||
|  | ||||
| pub fn rom_title(cpu: &Cpu) -> &str { | ||||
|     cpu.bus | ||||
|         .cart | ||||
|         .as_ref() | ||||
|         .and_then(|c| c.title.as_deref()) | ||||
|         .unwrap_or(DEFAULT_TITLE) | ||||
| } | ||||
|  | ||||
| pub fn write_save(cpu: &Cpu) { | ||||
|     match cpu.bus.cart.as_ref() { | ||||
|         Some(cart) => match write_save_to_file(cart) { | ||||
|             Ok(path) => tracing::info!("Wrote to save at {:?}", path), | ||||
|             Err(err @ SaveError::NotApplicable) => tracing::warn!("Unable to Save: {:?}", err), | ||||
|             Err(SaveError::DiffSize) => unreachable!(), | ||||
|             Err(SaveError::Io(err)) => tracing::error!("{:?}", err), | ||||
|         }, | ||||
|         None => tracing::error!("No cartridge is currently present"), | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn load_save(cpu: &mut Cpu) { | ||||
|     match cpu.bus.cart.as_mut() { | ||||
|         Some(cart) => match read_save_from_file(cart) { | ||||
|             Ok(path) => tracing::info!("Loaded save from {:?}", path), | ||||
|             Err(err @ SaveError::NotApplicable) => tracing::warn!("Unable to load save: {}", err), | ||||
|             Err(err @ SaveError::DiffSize) => tracing::error!("Unable to load save: {}", err), | ||||
|             Err(SaveError::Io(err)) => match err.kind() { | ||||
|                 std::io::ErrorKind::NotFound => tracing::warn!("Save not found"), | ||||
|                 _ => tracing::error!("{:?}", err), | ||||
|             }, | ||||
|         }, | ||||
|         None => tracing::error!("No cartridge is currently present"), | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn write_save_to_file(cart: &Cartridge) -> Result<PathBuf, SaveError> { | ||||
|     match cart.title.as_ref().zip(cart.ext_ram()) { | ||||
|         Some((title, ram)) => { | ||||
|             let mut save_path = data_path().unwrap_or_else(|| PathBuf::from(".")); | ||||
|             save_path.push(title); | ||||
|             save_path.set_extension("sav"); | ||||
|  | ||||
|             let mut file = File::create(&save_path)?; | ||||
|             file.write_all(ram)?; | ||||
|             Ok(save_path) | ||||
|         } | ||||
|         None => Err(SaveError::NotApplicable), | ||||
|     } | ||||
| } | ||||
|  | ||||
|     pub fn from_boot_rom<P: AsRef<Path>>(path: P) -> std::io::Result<Self> { | ||||
|         Ok(Self { | ||||
|             cpu: Cpu::with_boot(Self::read_boot(path)?), | ||||
|             timestamp: Default::default(), | ||||
|         }) | ||||
|     } | ||||
| fn read_save_from_file(cart: &mut Cartridge) -> Result<PathBuf, SaveError> { | ||||
|     match cart.title.clone().zip(cart.ext_ram_mut()) { | ||||
|         Some((title, ext_ram)) => { | ||||
|             let mut save_path = data_path().unwrap_or_else(|| PathBuf::from(".")); | ||||
|             save_path.push(title); | ||||
|             save_path.set_extension("sav"); | ||||
|  | ||||
|     fn read_boot<P: AsRef<Path>>(path: P) -> std::io::Result<[u8; BOOT_SIZE]> { | ||||
|         let mut buf = [0; BOOT_SIZE]; | ||||
|         let mut file = File::open(path.as_ref())?; | ||||
|             let mut file = File::open(&save_path)?; | ||||
|             let mut memory = Vec::new(); | ||||
|             file.read_to_end(&mut memory)?; | ||||
|  | ||||
|         file.read_exact(&mut buf)?; | ||||
|         Ok(buf) | ||||
|     } | ||||
|  | ||||
|     fn step(&mut self) -> Cycle { | ||||
|         let cycles = self.cpu.step(); | ||||
|         self.timestamp += cycles; | ||||
|         cycles | ||||
|     } | ||||
|  | ||||
|     pub fn read_game_rom<P: AsRef<Path>>(&mut self, path: P) -> std::io::Result<()> { | ||||
|         self.load_rom(std::fs::read(path.as_ref())?); | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn load_rom(&mut self, rom: Vec<u8>) { | ||||
|         self.cpu.bus_mut().load_cart(rom); | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     fn joyp_mut(&mut self) -> &mut Joypad { | ||||
|         self.cpu.bus_mut().joyp_mut() | ||||
|     } | ||||
|  | ||||
|     pub fn set_prod(&mut self, prod: SampleProducer<f32>) { | ||||
|         self.cpu.bus_mut().apu_mut().attach_producer(prod) | ||||
|     } | ||||
|  | ||||
|     pub fn title(&self) -> &str { | ||||
|         self.cpu.bus().cart_title().unwrap_or(DEFAULT_TITLE) | ||||
|     } | ||||
|  | ||||
|     pub fn try_write_sav(&self) -> std::io::Result<()> { | ||||
|         if let Some(ext_ram) = self.cpu.bus().cart().map(|c| c.ext_ram()).flatten() { | ||||
|             if let Some(title) = self.cpu.bus().cart_title() { | ||||
|                 let mut save_path = Self::data_path().unwrap_or_else(|| PathBuf::from(".")); | ||||
|                 save_path.push(title); | ||||
|                 save_path.set_extension("sav"); | ||||
|  | ||||
|                 let mut file = File::create(save_path)?; | ||||
|                 file.write_all(ext_ram)?; | ||||
|             if ext_ram.len() != memory.len() { | ||||
|                 return Err(SaveError::DiffSize); | ||||
|             } | ||||
|  | ||||
|             ext_ram.copy_from_slice(&memory); | ||||
|             Ok(save_path) | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub fn try_load_sav(&mut self) -> std::io::Result<()> { | ||||
|         if let Some(cart) = self.cpu.bus_mut().cart_mut() { | ||||
|             if let Some(title) = cart.title() { | ||||
|                 let mut save_path = Self::data_path().unwrap_or_else(|| PathBuf::from(".")); | ||||
|                 save_path.push(title); | ||||
|                 save_path.set_extension("sav"); | ||||
|  | ||||
|                 if let Ok(mut file) = File::open(&save_path) { | ||||
|                     tracing::info!("Load {:?}", save_path); | ||||
|  | ||||
|                     let mut memory = Vec::new(); | ||||
|                     file.read_to_end(&mut memory)?; | ||||
|                     cart.write_ext_ram(memory); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn data_path() -> Option<PathBuf> { | ||||
|         match directories_next::ProjectDirs::from("dev", "musuka", crate_name!()) { | ||||
|             Some(dirs) => { | ||||
|                 let data_local = dirs.data_local_dir(); | ||||
|                 std::fs::create_dir_all(data_local).ok()?; | ||||
|                 Some(data_local.to_path_buf()) | ||||
|             } | ||||
|             None => None, | ||||
|         } | ||||
|         None => Err(SaveError::NotApplicable), | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn read_boot<P: AsRef<Path>>(path: P) -> std::io::Result<[u8; BOOT_SIZE]> { | ||||
|     let mut buf = [0; BOOT_SIZE]; | ||||
|     let mut file = File::open(path.as_ref())?; | ||||
|  | ||||
|     file.read_exact(&mut buf)?; | ||||
|     Ok(buf) | ||||
| } | ||||
|  | ||||
| fn data_path() -> Option<PathBuf> { | ||||
|     match directories_next::ProjectDirs::from("dev", "musuka", crate_name!()) { | ||||
|         Some(dirs) => { | ||||
|             let data_local = dirs.data_local_dir(); | ||||
|             std::fs::create_dir_all(data_local).ok()?; | ||||
|             Some(data_local.to_path_buf()) | ||||
|         } | ||||
|         None => None, | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Error)] | ||||
| pub enum SaveError { | ||||
|     #[error("cartridge lacks title and/or external ram")] | ||||
|     NotApplicable, | ||||
|     #[error(transparent)] | ||||
|     Io(#[from] std::io::Error), | ||||
|     #[error("save file size differs from external ram")] | ||||
|     DiffSize, | ||||
| } | ||||
|   | ||||
							
								
								
									
										472
									
								
								src/gui.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										472
									
								
								src/gui.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,472 @@ | ||||
| use egui::{Context, TextureId}; | ||||
| use egui_wgpu_backend::{wgpu, RenderPass, ScreenDescriptor}; | ||||
| use egui_winit_platform::Platform; | ||||
| use wgpu::{ | ||||
|     Adapter, CommandEncoder, Device, Extent3d, FilterMode, Instance, Queue, RequestDeviceError, | ||||
|     Surface, SurfaceCapabilities, SurfaceConfiguration, Texture, TextureView, | ||||
|     TextureViewDescriptor, | ||||
| }; | ||||
|  | ||||
| use winit::dpi::PhysicalSize; | ||||
| use winit::error::OsError; | ||||
| use winit::event::{ElementState, Event, KeyboardInput}; | ||||
| use winit::event_loop::{ControlFlow, EventLoop}; | ||||
| use winit::window::Window; | ||||
|  | ||||
| use crate::cpu::Cpu; | ||||
| use crate::{emu, GB_HEIGHT, GB_WIDTH}; | ||||
|  | ||||
| const EGUI_DIMENSIONS: (usize, usize) = (1280, 720); | ||||
| const FILTER_MODE: FilterMode = FilterMode::Nearest; | ||||
| const WINDOW_TITLE: &str = "DMG-01 Emulator"; | ||||
|  | ||||
| const SCALE: f32 = 3.0; | ||||
|  | ||||
| pub struct Gui { | ||||
|     pub should_quit: bool, | ||||
|     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 { | ||||
|     pub fn new<T>(title: String, event_loop: &EventLoop<T>, cpu: &Cpu) -> 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 { | ||||
|             should_quit: Default::default(), | ||||
|             title, | ||||
|             mode: EmuMode::Running, | ||||
|  | ||||
|             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, | ||||
| /// we can use a "default" value. However, in order for this to work the chosen "default" | ||||
| /// value must be an **unused** key, so that it is ignored by the emulator. | ||||
| pub fn unused_key() -> KeyboardInput { | ||||
|     #![allow(deprecated)] | ||||
|  | ||||
|     KeyboardInput { | ||||
|         scancode: Default::default(), | ||||
|         state: ElementState::Released, | ||||
|         virtual_keycode: Default::default(), | ||||
|         modifiers: Default::default(), // this argument is deprecated | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn build_window<T>(event_loop: &EventLoop<T>) -> Result<Window, OsError> { | ||||
|     use winit::window::WindowBuilder; | ||||
|  | ||||
|     WindowBuilder::new() | ||||
|         .with_decorations(true) | ||||
|         .with_resizable(true) | ||||
|         .with_transparent(false) | ||||
|         .with_title(WINDOW_TITLE) | ||||
|         .with_inner_size(PhysicalSize { | ||||
|             width: EGUI_DIMENSIONS.0 as f32, | ||||
|             height: EGUI_DIMENSIONS.1 as f32, | ||||
|         }) | ||||
|         .build(event_loop) | ||||
| } | ||||
|  | ||||
| fn request_adapter(instance: &Instance, surface: &Surface) -> Option<Adapter> { | ||||
|     use wgpu::{PowerPreference, RequestAdapterOptions}; | ||||
|  | ||||
|     pollster::block_on(instance.request_adapter(&RequestAdapterOptions { | ||||
|         power_preference: PowerPreference::HighPerformance, | ||||
|         compatible_surface: Some(surface), | ||||
|         force_fallback_adapter: false, // TODO: What do I want to do with this? | ||||
|     })) | ||||
| } | ||||
|  | ||||
| fn request_device(adapter: &Adapter) -> Result<(Device, Queue), RequestDeviceError> { | ||||
|     use wgpu::{DeviceDescriptor, Features, Limits}; | ||||
|  | ||||
|     pollster::block_on(adapter.request_device( | ||||
|         &DeviceDescriptor { | ||||
|             features: Features::default(), | ||||
|             limits: Limits::default(), | ||||
|             label: None, | ||||
|         }, | ||||
|         None, | ||||
|     )) | ||||
| } | ||||
|  | ||||
| fn surface_config(window: &Window, capabilities: SurfaceCapabilities) -> SurfaceConfiguration { | ||||
|     use egui_wgpu_backend::wgpu::{PresentMode, TextureFormat, TextureUsages}; | ||||
|  | ||||
|     let size = window.inner_size(); | ||||
|     SurfaceConfiguration { | ||||
|         usage: TextureUsages::RENDER_ATTACHMENT, | ||||
|         format: capabilities.formats[0], | ||||
|         width: size.width as u32, | ||||
|         height: size.height as u32, | ||||
|         present_mode: PresentMode::Fifo, | ||||
|         alpha_mode: capabilities.alpha_modes[0], | ||||
|         view_formats: vec![TextureFormat::Rgba8UnormSrgb], | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn platform(window: &Window) -> Platform { | ||||
|     use egui::FontDefinitions; | ||||
|     use egui_winit_platform::PlatformDescriptor; | ||||
|  | ||||
|     let size = window.inner_size(); | ||||
|     Platform::new(PlatformDescriptor { | ||||
|         physical_width: size.width as u32, | ||||
|         physical_height: size.height as u32, | ||||
|         scale_factor: window.scale_factor(), | ||||
|         font_definitions: FontDefinitions::default(), | ||||
|         ..Default::default() | ||||
|     }) | ||||
| } | ||||
|  | ||||
| fn texture_size() -> Extent3d { | ||||
|     Extent3d { | ||||
|         width: GB_WIDTH as u32, | ||||
|         height: GB_HEIGHT as u32, | ||||
|         ..Default::default() | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn create_texture(device: &Device, size: Extent3d) -> Texture { | ||||
|     use wgpu::{TextureDescriptor, TextureDimension, TextureFormat, TextureUsages}; | ||||
|  | ||||
|     device.create_texture(&TextureDescriptor { | ||||
|         size, | ||||
|         mip_level_count: 1, | ||||
|         sample_count: 1, | ||||
|         dimension: TextureDimension::D2, | ||||
|         format: TextureFormat::Bgra8Unorm, | ||||
|         usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING, | ||||
|         label: Some("gb_pixel_buffer"), | ||||
|         view_formats: &[TextureFormat::Bgra8Unorm], | ||||
|     }) | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| fn write_to_texture( | ||||
|     queue: &Queue, | ||||
|     texture: &Texture, | ||||
|     data: &[u8; GB_WIDTH * 4 * GB_HEIGHT], | ||||
|     size: Extent3d, | ||||
| ) { | ||||
|     use std::num::NonZeroU32; | ||||
|     use wgpu::{ImageCopyTexture, ImageDataLayout, Origin3d, TextureAspect}; | ||||
|  | ||||
|     queue.write_texture( | ||||
|         ImageCopyTexture { | ||||
|             texture, | ||||
|             mip_level: 0, | ||||
|             origin: Origin3d::ZERO, | ||||
|             aspect: TextureAspect::All, | ||||
|         }, | ||||
|         data, | ||||
|         ImageDataLayout { | ||||
|             offset: 0, | ||||
|             bytes_per_row: NonZeroU32::new(4 * GB_WIDTH as u32), | ||||
|             rows_per_image: NonZeroU32::new(GB_HEIGHT as u32), | ||||
|         }, | ||||
|         size, | ||||
|     ); | ||||
| } | ||||
|  | ||||
| fn expose_texture_to_egui( | ||||
|     render_pass: &mut RenderPass, | ||||
|     device: &Device, | ||||
|     view: &TextureView, | ||||
| ) -> TextureId { | ||||
|     render_pass.egui_texture_from_wgpu_texture(device, view, FILTER_MODE) | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| fn create_command_encoder(device: &Device) -> CommandEncoder { | ||||
|     use wgpu::CommandEncoderDescriptor; | ||||
|  | ||||
|     device.create_command_encoder(&CommandEncoderDescriptor { | ||||
|         label: Some("encoder"), | ||||
|     }) | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| fn create_screen_descriptor(window: &Window, config: &SurfaceConfiguration) -> ScreenDescriptor { | ||||
|     ScreenDescriptor { | ||||
|         physical_width: config.width, | ||||
|         physical_height: config.height, | ||||
|         scale_factor: window.scale_factor() as f32, | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub mod kbd { | ||||
|     use winit::event::{ElementState, KeyboardInput, VirtualKeyCode}; | ||||
|  | ||||
|     pub fn space_released(input: &KeyboardInput) -> bool { | ||||
|         let keycode = input.virtual_keycode; | ||||
|         matches!(input.state, ElementState::Released if keycode == Some(VirtualKeyCode::Space)) | ||||
|     } | ||||
| } | ||||
| @@ -17,11 +17,11 @@ impl Default for HighRam { | ||||
| } | ||||
|  | ||||
| impl BusIo for HighRam { | ||||
|     fn write_byte(&mut self, addr: u16, byte: u8) { | ||||
|         self.buf[addr as usize - HIGH_RAM_START_ADDRESS] = byte; | ||||
|     } | ||||
|  | ||||
|     fn read_byte(&self, addr: u16) -> u8 { | ||||
|         self.buf[addr as usize - HIGH_RAM_START_ADDRESS] | ||||
|     } | ||||
|  | ||||
|     fn write_byte(&mut self, addr: u16, byte: u8) { | ||||
|         self.buf[addr as usize - HIGH_RAM_START_ADDRESS] = byte; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| use self::add::{Source as AddSource, Target as AddTarget}; | ||||
| use self::alu::Source as AluSource; | ||||
| use self::jump::{JumpCondition, JumpLocation}; | ||||
| use self::jump::{JpCond, JpLoc}; | ||||
| use self::load::{Source as LDSource, Target as LDTarget}; | ||||
| use self::table::{ | ||||
|     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 { | ||||
|     NOP, | ||||
|     STOP, | ||||
|     JR(JumpCondition), | ||||
|     JR(JpCond), | ||||
|     LD(LDTarget, LDSource), | ||||
|     ADD(AddTarget, AddSource), | ||||
|     LDHL, | ||||
| @@ -38,13 +38,13 @@ pub(crate) enum Instruction { | ||||
|     XOR(AluSource), | ||||
|     OR(AluSource), | ||||
|     CP(AluSource), | ||||
|     RET(JumpCondition), | ||||
|     RET(JpCond), | ||||
|     POP(Group3RegisterPair), | ||||
|     RETI, | ||||
|     JP(JumpCondition, JumpLocation), | ||||
|     JP(JpCond, JpLoc), | ||||
|     DI, | ||||
|     EI, | ||||
|     CALL(JumpCondition), | ||||
|     CALL(JpCond), | ||||
|     PUSH(Group3RegisterPair), | ||||
|     RST(u8), | ||||
|     RLC(Register), | ||||
| @@ -58,6 +58,7 @@ pub(crate) enum Instruction { | ||||
|     BIT(u8, Register), | ||||
|     RES(u8, Register), | ||||
|     SET(u8, Register), | ||||
|     Invalid, | ||||
| } | ||||
|  | ||||
| impl std::fmt::Debug for Instruction { | ||||
| @@ -109,6 +110,7 @@ impl std::fmt::Debug for Instruction { | ||||
|             BIT(b, r) => write!(f, "BIT {}, {:?}", b, r), | ||||
|             RES(b, r) => write!(f, "RES {}, {:?}", b, r), | ||||
|             SET(b, r) => write!(f, "SET {}, {:?}", b, r), | ||||
|             Invalid => f.write_str("???"), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -293,7 +295,7 @@ impl Instruction { | ||||
|                 let addr = pc.wrapping_add(byte as u16); | ||||
|  | ||||
|                 match cond { | ||||
|                     JumpCondition::NotZero => { | ||||
|                     JpCond::NotZero => { | ||||
|                         if !flags.z() { | ||||
|                             Self::jump(cpu, addr); | ||||
|                             12 | ||||
| @@ -301,7 +303,7 @@ impl Instruction { | ||||
|                             8 | ||||
|                         } | ||||
|                     } | ||||
|                     JumpCondition::Zero => { | ||||
|                     JpCond::Zero => { | ||||
|                         if flags.z() { | ||||
|                             Self::jump(cpu, addr); | ||||
|                             12 | ||||
| @@ -309,7 +311,7 @@ impl Instruction { | ||||
|                             8 | ||||
|                         } | ||||
|                     } | ||||
|                     JumpCondition::NotCarry => { | ||||
|                     JpCond::NotCarry => { | ||||
|                         if !flags.c() { | ||||
|                             Self::jump(cpu, addr); | ||||
|                             12 | ||||
| @@ -317,7 +319,7 @@ impl Instruction { | ||||
|                             8 | ||||
|                         } | ||||
|                     } | ||||
|                     JumpCondition::Carry => { | ||||
|                     JpCond::Carry => { | ||||
|                         if flags.c() { | ||||
|                             Self::jump(cpu, addr); | ||||
|                             12 | ||||
| @@ -325,7 +327,7 @@ impl Instruction { | ||||
|                             8 | ||||
|                         } | ||||
|                     } | ||||
|                     JumpCondition::Always => { | ||||
|                     JpCond::Always => { | ||||
|                         Self::jump(cpu, addr); | ||||
|                         12 | ||||
|                     } | ||||
| @@ -837,7 +839,7 @@ impl Instruction { | ||||
|                 let flags: Flags = *cpu.flags(); | ||||
|  | ||||
|                 match cond { | ||||
|                     JumpCondition::NotZero => { | ||||
|                     JpCond::NotZero => { | ||||
|                         cpu.bus.clock(); // internal branch decision | ||||
|  | ||||
|                         if !flags.z() { | ||||
| @@ -848,7 +850,7 @@ impl Instruction { | ||||
|                             8 | ||||
|                         } | ||||
|                     } | ||||
|                     JumpCondition::Zero => { | ||||
|                     JpCond::Zero => { | ||||
|                         cpu.bus.clock(); // internal branch decision | ||||
|  | ||||
|                         if flags.z() { | ||||
| @@ -859,7 +861,7 @@ impl Instruction { | ||||
|                             8 | ||||
|                         } | ||||
|                     } | ||||
|                     JumpCondition::NotCarry => { | ||||
|                     JpCond::NotCarry => { | ||||
|                         cpu.bus.clock(); // internal branch decision | ||||
|  | ||||
|                         if !flags.c() { | ||||
| @@ -870,7 +872,7 @@ impl Instruction { | ||||
|                             8 | ||||
|                         } | ||||
|                     } | ||||
|                     JumpCondition::Carry => { | ||||
|                     JpCond::Carry => { | ||||
|                         cpu.bus.clock(); // internal branch decision | ||||
|  | ||||
|                         if flags.c() { | ||||
| @@ -881,7 +883,7 @@ impl Instruction { | ||||
|                             8 | ||||
|                         } | ||||
|                     } | ||||
|                     JumpCondition::Always => { | ||||
|                     JpCond::Always => { | ||||
|                         let addr = Self::pop(cpu); | ||||
|                         Self::jump(cpu, addr); | ||||
|                         16 | ||||
| @@ -908,13 +910,13 @@ impl Instruction { | ||||
|                 16 | ||||
|             } | ||||
|             Instruction::JP(cond, location) => match location { | ||||
|                 JumpLocation::HL => { | ||||
|                 JpLoc::HL => { | ||||
|                     // JP HL | Store HL in program counter | ||||
|                     let right = cpu.register_pair(RegisterPair::HL); | ||||
|                     cpu.set_register_pair(RegisterPair::PC, right); | ||||
|                     4 | ||||
|                 } | ||||
|                 JumpLocation::ImmediateWord => { | ||||
|                 JpLoc::ImmediateWord => { | ||||
|                     // JP cond u16 | Store u16 in program counter if condition is true | ||||
|                     // JP u16      | Store u16 in program counter | ||||
|                     let flags: Flags = *cpu.flags(); | ||||
| @@ -922,7 +924,7 @@ impl Instruction { | ||||
|                     let addr = Self::imm_word(cpu); | ||||
|  | ||||
|                     match cond { | ||||
|                         JumpCondition::NotZero => { | ||||
|                         JpCond::NotZero => { | ||||
|                             if !flags.z() { | ||||
|                                 Self::jump(cpu, addr); | ||||
|                                 16 | ||||
| @@ -930,7 +932,7 @@ impl Instruction { | ||||
|                                 12 | ||||
|                             } | ||||
|                         } | ||||
|                         JumpCondition::Zero => { | ||||
|                         JpCond::Zero => { | ||||
|                             if flags.z() { | ||||
|                                 Self::jump(cpu, addr); | ||||
|                                 16 | ||||
| @@ -938,7 +940,7 @@ impl Instruction { | ||||
|                                 12 | ||||
|                             } | ||||
|                         } | ||||
|                         JumpCondition::NotCarry => { | ||||
|                         JpCond::NotCarry => { | ||||
|                             if !flags.c() { | ||||
|                                 Self::jump(cpu, addr); | ||||
|                                 16 | ||||
| @@ -946,7 +948,7 @@ impl Instruction { | ||||
|                                 12 | ||||
|                             } | ||||
|                         } | ||||
|                         JumpCondition::Carry => { | ||||
|                         JpCond::Carry => { | ||||
|                             if flags.c() { | ||||
|                                 Self::jump(cpu, addr); | ||||
|                                 16 | ||||
| @@ -954,7 +956,7 @@ impl Instruction { | ||||
|                                 12 | ||||
|                             } | ||||
|                         } | ||||
|                         JumpCondition::Always => { | ||||
|                         JpCond::Always => { | ||||
|                             Self::jump(cpu, addr); | ||||
|                             16 | ||||
|                         } | ||||
| @@ -980,7 +982,7 @@ impl Instruction { | ||||
|                 let return_addr = cpu.register_pair(RegisterPair::PC); | ||||
|  | ||||
|                 match cond { | ||||
|                     JumpCondition::NotZero => { | ||||
|                     JpCond::NotZero => { | ||||
|                         if !flags.z() { | ||||
|                             cpu.bus.clock(); // internal branch decision | ||||
|                             Self::push(cpu, return_addr); | ||||
| @@ -990,7 +992,7 @@ impl Instruction { | ||||
|                             12 | ||||
|                         } | ||||
|                     } | ||||
|                     JumpCondition::Zero => { | ||||
|                     JpCond::Zero => { | ||||
|                         if flags.z() { | ||||
|                             cpu.bus.clock(); // internal branch decision | ||||
|                             Self::push(cpu, return_addr); | ||||
| @@ -1000,7 +1002,7 @@ impl Instruction { | ||||
|                             12 | ||||
|                         } | ||||
|                     } | ||||
|                     JumpCondition::NotCarry => { | ||||
|                     JpCond::NotCarry => { | ||||
|                         if !flags.c() { | ||||
|                             cpu.bus.clock(); // internal branch decision | ||||
|                             Self::push(cpu, return_addr); | ||||
| @@ -1010,7 +1012,7 @@ impl Instruction { | ||||
|                             12 | ||||
|                         } | ||||
|                     } | ||||
|                     JumpCondition::Carry => { | ||||
|                     JpCond::Carry => { | ||||
|                         if flags.c() { | ||||
|                             cpu.bus.clock(); // internal branch decision | ||||
|                             Self::push(cpu, return_addr); | ||||
| @@ -1020,7 +1022,7 @@ impl Instruction { | ||||
|                             12 | ||||
|                         } | ||||
|                     } | ||||
|                     JumpCondition::Always => { | ||||
|                     JpCond::Always => { | ||||
|                         cpu.bus.clock(); // internal branch decision | ||||
|                         Self::push(cpu, return_addr); | ||||
|                         cpu.set_register_pair(RegisterPair::PC, addr); | ||||
| @@ -1294,6 +1296,7 @@ impl Instruction { | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             Instruction::Invalid => panic!("Attempted to execute invalid instruction"), | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -1557,7 +1560,7 @@ impl Instruction { | ||||
|             // STOP | ||||
|             0o020 => STOP, | ||||
|             // JR i8 | ||||
|             0o030 => JR(JumpCondition::Always), | ||||
|             0o030 => JR(JpCond::Always), | ||||
|             // JR cond i8 | ||||
|             0o040 | 0o050 | 0o060 | 0o070 => JR(jump_cond((byte >> 3) & 0x03)), | ||||
|             // LD r16, u16 | ||||
| @@ -1622,16 +1625,16 @@ impl Instruction { | ||||
|             // POP r16 | ||||
|             0o301 | 0o321 | 0o341 | 0o361 => POP(group3((byte >> 4) & 0x03)), | ||||
|             // RET | ||||
|             0o311 => RET(JumpCondition::Always), | ||||
|             0o311 => RET(JpCond::Always), | ||||
|             // RETI | ||||
|             0o331 => RETI, | ||||
|             // JP HL | ||||
|             0o351 => JP(JumpCondition::Always, JumpLocation::HL), | ||||
|             0o351 => JP(JpCond::Always, JpLoc::HL), | ||||
|             // LD SP, HL | ||||
|             0o371 => LD(LDTarget::SP, LDSource::HL), | ||||
|             // JP cond u16 | ||||
|             0o302 | 0o312 | 0o322 | 0o332 => { | ||||
|                 JP(jump_cond((byte >> 3) & 0x03), JumpLocation::ImmediateWord) | ||||
|                 JP(jump_cond((byte >> 3) & 0x03), JpLoc::ImmediateWord) | ||||
|             } | ||||
|             // LD (0xFF00 + C), A | ||||
|             0o342 => LD(LDTarget::IoWithC, LDSource::A), | ||||
| @@ -1642,9 +1645,7 @@ impl Instruction { | ||||
|             // LD A, (u16) | ||||
|             0o372 => LD(LDTarget::A, LDSource::IndirectImmediateWord), | ||||
|             // JP u16 | ||||
|             0o303 => JP(JumpCondition::Always, JumpLocation::ImmediateWord), | ||||
|             // 0xCB Prefix | ||||
|             0o313 => unreachable!("{:#04X} should be handled by the prefixed decoder", byte), | ||||
|             0o303 => JP(JpCond::Always, JpLoc::ImmediateWord), | ||||
|             // DI | ||||
|             0o363 => DI, | ||||
|             // EI | ||||
| @@ -1653,12 +1654,14 @@ impl Instruction { | ||||
|             0o304 | 0o314 | 0o324 | 0o334 => CALL(jump_cond((byte >> 3) & 0x03)), | ||||
|             // PUSH r16 | ||||
|             0o305 | 0o325 | 0o345 | 0o365 => PUSH(group3((byte >> 4) & 0x03)), | ||||
|             0o315 => CALL(JumpCondition::Always), | ||||
|             // CALL u16 | ||||
|             0o315 => CALL(JpCond::Always), | ||||
|             // ADD, ADC, SUB, SBC, AND, XOR, OR, and CP | ||||
|             0o306 | 0o316 | 0o326 | 0o336 | 0o346 | 0o356 | 0o366 | 0o376 => { | ||||
|                 alu_imm_instr((byte >> 3) & 0x07) | ||||
|             } | ||||
|             0o307 | 0o317 | 0o327 | 0o337 | 0o347 | 0o357 | 0o367 | 0o377 => RST(byte & 0b00111000), | ||||
|             _ => panic!("{:#04X} is an illegal opcode", byte), | ||||
|             _ => Invalid, // 0xCB is 0o313 | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -1681,7 +1684,7 @@ impl Instruction { | ||||
| mod jump { | ||||
|  | ||||
|     #[derive(Clone, Copy)] | ||||
|     pub(crate) enum JumpCondition { | ||||
|     pub(crate) enum JpCond { | ||||
|         Always, | ||||
|         NotZero, | ||||
|         Zero, | ||||
| @@ -1689,9 +1692,9 @@ mod jump { | ||||
|         Carry, | ||||
|     } | ||||
|  | ||||
|     impl std::fmt::Debug for JumpCondition { | ||||
|     impl std::fmt::Debug for JpCond { | ||||
|         fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|             use JumpCondition::*; | ||||
|             use JpCond::*; | ||||
|  | ||||
|             match self { | ||||
|                 Always => f.write_str(""), | ||||
| @@ -1704,14 +1707,14 @@ mod jump { | ||||
|     } | ||||
|  | ||||
|     #[derive(Clone, Copy)] | ||||
|     pub(crate) enum JumpLocation { | ||||
|     pub(crate) enum JpLoc { | ||||
|         HL, | ||||
|         ImmediateWord, | ||||
|     } | ||||
|  | ||||
|     impl std::fmt::Debug for JumpLocation { | ||||
|     impl std::fmt::Debug for JpLoc { | ||||
|         fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|             use JumpLocation::*; | ||||
|             use JpLoc::*; | ||||
|  | ||||
|             match *self { | ||||
|                 HL => f.write_str("HL"), | ||||
| @@ -1871,7 +1874,7 @@ mod load { | ||||
| mod table { | ||||
|     use super::add::{Source as AddSource, Target as AddTarget}; | ||||
|     use super::alu::Source as AluSource; | ||||
|     use super::{Instruction, JumpCondition}; | ||||
|     use super::{Instruction, JpCond}; | ||||
|     use crate::cpu::{Register as CpuRegister, RegisterPair}; | ||||
|  | ||||
|     #[derive(Clone, Copy)] | ||||
| @@ -2070,12 +2073,12 @@ mod table { | ||||
|             0b101 => L, | ||||
|             0b110 => IndirectHL, | ||||
|             0b111 => A, | ||||
|             _ => unreachable!("{:#04X} is not a valid Register"), | ||||
|             _ => unreachable!("{:#04X} is not a valid Register", code), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn jump_cond(code: u8) -> JumpCondition { | ||||
|         use JumpCondition::*; | ||||
|     pub(crate) fn jump_cond(code: u8) -> JpCond { | ||||
|         use JpCond::*; | ||||
|  | ||||
|         match code { | ||||
|             0b00 => NotZero, | ||||
| @@ -2150,3 +2153,268 @@ 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,11 +1,20 @@ | ||||
| use bitfield::bitfield; | ||||
|  | ||||
| #[derive(Debug, Default)] | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct Interrupt { | ||||
|     pub(crate) flag: InterruptFlag, | ||||
|     pub(crate) enable: InterruptEnable, | ||||
| } | ||||
|  | ||||
| impl Default for Interrupt { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             flag: InterruptFlag(0), | ||||
|             enable: InterruptEnable(0), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| bitfield! { | ||||
|    pub struct InterruptEnable(u8); | ||||
|     impl Debug; | ||||
| @@ -23,12 +32,6 @@ impl Clone for InterruptEnable { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for InterruptEnable { | ||||
|     fn default() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for InterruptEnable { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         Self(byte) | ||||
| @@ -58,12 +61,6 @@ impl Clone for InterruptFlag { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for InterruptFlag { | ||||
|     fn default() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for InterruptFlag { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         Self(byte) | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| use gilrs::{Button, Event as GamepadEvent, EventType as GamepadEventType}; | ||||
| use winit_input_helper::WinitInputHelper; | ||||
| use winit::event::{ElementState, KeyboardInput, VirtualKeyCode}; | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct Joypad { | ||||
| @@ -111,68 +111,33 @@ impl ButtonEvent { | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| pub(crate) fn handle_keyboard_input(pad: &mut Joypad, input: &WinitInputHelper) { | ||||
|     use winit::event::VirtualKeyCode; | ||||
|  | ||||
|     // TODO: What do I have to do to get a match statement here? | ||||
|  | ||||
| pub(crate) fn handle_keyboard_input(pad: &mut Joypad, key: KeyboardInput) { | ||||
|     let state = &mut pad.ext; | ||||
|     let irq = &mut pad.interrupt; | ||||
|  | ||||
|     if input.key_pressed(VirtualKeyCode::Down) { | ||||
|         state.dpad_down.update(true, irq); | ||||
|     } | ||||
|     if input.key_released(VirtualKeyCode::Down) { | ||||
|         state.dpad_down.update(false, irq); | ||||
|     } | ||||
|  | ||||
|     if input.key_pressed(VirtualKeyCode::Up) { | ||||
|         state.dpad_up.update(true, irq); | ||||
|     } | ||||
|     if input.key_released(VirtualKeyCode::Up) { | ||||
|         state.dpad_up.update(false, irq); | ||||
|     } | ||||
|  | ||||
|     if input.key_pressed(VirtualKeyCode::Left) { | ||||
|         state.dpad_left.update(true, irq); | ||||
|     } | ||||
|     if input.key_released(VirtualKeyCode::Left) { | ||||
|         state.dpad_left.update(false, irq); | ||||
|     } | ||||
|  | ||||
|     if input.key_pressed(VirtualKeyCode::Right) { | ||||
|         state.dpad_right.update(true, irq); | ||||
|     } | ||||
|     if input.key_released(VirtualKeyCode::Right) { | ||||
|         state.dpad_right.update(false, irq); | ||||
|     } | ||||
|  | ||||
|     if input.key_pressed(VirtualKeyCode::Return) { | ||||
|         state.start.update(true, irq); | ||||
|     } | ||||
|     if input.key_released(VirtualKeyCode::Return) { | ||||
|         state.start.update(false, irq); | ||||
|     } | ||||
|  | ||||
|     if input.key_pressed(VirtualKeyCode::RShift) { | ||||
|         state.select.update(true, irq); | ||||
|     } | ||||
|     if input.key_released(VirtualKeyCode::RShift) { | ||||
|         state.select.update(false, irq); | ||||
|     } | ||||
|  | ||||
|     if input.key_pressed(VirtualKeyCode::Z) { | ||||
|         state.south.update(true, irq); | ||||
|     } | ||||
|     if input.key_released(VirtualKeyCode::Z) { | ||||
|         state.south.update(false, irq); | ||||
|     } | ||||
|  | ||||
|     if input.key_pressed(VirtualKeyCode::X) { | ||||
|         state.east.update(true, irq); | ||||
|     } | ||||
|     if input.key_released(VirtualKeyCode::X) { | ||||
|         state.east.update(false, irq); | ||||
|     match key.state { | ||||
|         ElementState::Pressed => match key.virtual_keycode { | ||||
|             Some(VirtualKeyCode::Down) => state.dpad_down.update(true, irq), | ||||
|             Some(VirtualKeyCode::Up) => state.dpad_up.update(true, irq), | ||||
|             Some(VirtualKeyCode::Left) => state.dpad_left.update(true, irq), | ||||
|             Some(VirtualKeyCode::Right) => state.dpad_right.update(true, irq), | ||||
|             Some(VirtualKeyCode::Return) => state.start.update(true, irq), | ||||
|             Some(VirtualKeyCode::RShift) => state.select.update(true, irq), | ||||
|             Some(VirtualKeyCode::Z) => state.south.update(true, irq), | ||||
|             Some(VirtualKeyCode::X) => state.east.update(true, irq), | ||||
|             None | Some(_) => {} | ||||
|         }, | ||||
|         ElementState::Released => match key.virtual_keycode { | ||||
|             Some(VirtualKeyCode::Down) => state.dpad_down.update(false, irq), | ||||
|             Some(VirtualKeyCode::Up) => state.dpad_up.update(false, irq), | ||||
|             Some(VirtualKeyCode::Left) => state.dpad_left.update(false, irq), | ||||
|             Some(VirtualKeyCode::Right) => state.dpad_right.update(false, irq), | ||||
|             Some(VirtualKeyCode::Return) => state.start.update(false, irq), | ||||
|             Some(VirtualKeyCode::RShift) => state.select.update(false, irq), | ||||
|             Some(VirtualKeyCode::Z) => state.south.update(false, irq), | ||||
|             Some(VirtualKeyCode::X) => state.east.update(false, irq), | ||||
|             None | Some(_) => {} | ||||
|         }, | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| #![allow(clippy::derivable_impls)] // Will remove this if bitfield-rs allows default impls | ||||
|  | ||||
| pub use apu::gen::init as spsc_init; | ||||
| pub type Cycle = u64; | ||||
|  | ||||
| @@ -9,6 +11,7 @@ mod bus; | ||||
| mod cartridge; | ||||
| mod cpu; | ||||
| pub mod emu; | ||||
| pub mod gui; | ||||
| mod high_ram; | ||||
| mod instruction; | ||||
| mod interrupt; | ||||
|   | ||||
							
								
								
									
										166
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										166
									
								
								src/main.rs
									
									
									
									
									
								
							| @@ -1,41 +1,28 @@ | ||||
| use anyhow::Result; | ||||
| use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg}; | ||||
| use gb::emu::{Emulator, CYCLES_IN_FRAME}; | ||||
| use gb::{Cycle, GB_HEIGHT, GB_WIDTH}; | ||||
| use std::path::PathBuf; | ||||
| use std::time::Instant; | ||||
|  | ||||
| use clap::{arg, command, value_parser}; | ||||
| use gb::gui::{EmuMode, Gui}; | ||||
| use gb::{emu, gui}; | ||||
| use gilrs::Gilrs; | ||||
| use pixels::{PixelsBuilder, SurfaceTexture}; | ||||
| use rodio::{OutputStream, Sink}; | ||||
| use tracing_subscriber::EnvFilter; | ||||
| use winit::dpi::{LogicalSize, PhysicalSize}; | ||||
| use winit::event::{Event, VirtualKeyCode}; | ||||
| use winit::event_loop::{ControlFlow, EventLoop}; | ||||
| use winit::window::{Window, WindowBuilder}; | ||||
| use winit_input_helper::WinitInputHelper; | ||||
| use winit::event::{Event, WindowEvent}; | ||||
| use winit::event_loop::{EventLoop, EventLoopBuilder}; | ||||
|  | ||||
| const WINDOW_SCALE: usize = 3; | ||||
| const AUDIO_ENABLED: bool = true; | ||||
|  | ||||
| fn main() -> Result<()> { | ||||
|     let app = App::new(crate_name!()) | ||||
|         .version(crate_version!()) | ||||
|         .author(crate_authors!()) | ||||
|         .about(crate_description!()); | ||||
|  | ||||
|     let m = app | ||||
| fn main() { | ||||
|     let m = command!() | ||||
|         .arg( | ||||
|             Arg::with_name("rom") | ||||
|                 .value_name("ROM_FILE") | ||||
|                 .takes_value(true) | ||||
|                 .index(1) | ||||
|                 .help("Path to the Game ROM"), | ||||
|             arg!(-b --boot <FILE> "path to boot ROM") | ||||
|                 .required(false) | ||||
|                 .value_parser(value_parser!(PathBuf)), | ||||
|         ) | ||||
|         .arg( | ||||
|             Arg::with_name("boot") | ||||
|                 .short("b") | ||||
|                 .long("boot") | ||||
|                 .value_name("FILE") | ||||
|                 .takes_value(true) | ||||
|                 .help("Path to Boot ROM"), | ||||
|             arg!([ROM_FILE] "path to game ROM") | ||||
|                 .required(true) | ||||
|                 .value_parser(value_parser!(PathBuf)), | ||||
|         ) | ||||
|         .get_matches(); | ||||
|  | ||||
| @@ -48,56 +35,42 @@ fn main() -> Result<()> { | ||||
|         .with_env_filter(EnvFilter::from_default_env()) | ||||
|         .init(); | ||||
|  | ||||
|     let mut emu = match m.value_of("boot") { | ||||
|     // Init CPU | ||||
|     let mut cpu = match m.get_one::<PathBuf>("boot") { | ||||
|         Some(path) => { | ||||
|             tracing::info!("User-provided boot ROM"); | ||||
|             Emulator::from_boot_rom(path)? | ||||
|             emu::from_boot_rom(path).expect("initialize emulator with custom boot rom") | ||||
|         } | ||||
|         None => { | ||||
|             tracing::info!("Built-in boot ROM"); | ||||
|             Emulator::new() | ||||
|             Default::default() | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     if let Some(path) = m.value_of("rom") { | ||||
|     // Load ROM if filepath was provided | ||||
|     if let Some(path) = m.get_one::<PathBuf>("ROM_FILE") { | ||||
|         tracing::info!("User-provided cartridge ROM"); | ||||
|         emu.read_game_rom(path)?; | ||||
|         emu::read_game_rom(&mut cpu, path).expect("read game rom from path"); | ||||
|     } | ||||
|  | ||||
|     // Load Save file if it exists | ||||
|     emu.try_load_sav().expect("Load save if exists"); | ||||
|     let rom_title = emu.title(); | ||||
|     emu::load_save(&mut cpu); | ||||
|     let rom_title = emu::rom_title(&cpu).to_string(); | ||||
|  | ||||
|     tracing::info!("Initialize Gamepad"); | ||||
|     let mut gamepad = Gilrs::new().expect("Initialize Controller Support"); | ||||
|  | ||||
|     // Initialize GUI | ||||
|     let event_loop = EventLoop::new(); | ||||
|     let mut input = WinitInputHelper::new(); | ||||
|     let window = create_window(&event_loop, rom_title)?; | ||||
|  | ||||
|     let mut pixels = { | ||||
|         let size = window.inner_size(); | ||||
|         let surface_texture = SurfaceTexture::new(size.width, size.height, &window); | ||||
|  | ||||
|         PixelsBuilder::new(GB_WIDTH as u32, GB_HEIGHT as u32, surface_texture) | ||||
|             .enable_vsync(false) | ||||
|             .build()? | ||||
|     }; | ||||
|  | ||||
|     // Initialize Audio | ||||
|     let (_stream, stream_handle) = OutputStream::try_default().expect("Initialized Audio"); | ||||
|  | ||||
|     if AUDIO_ENABLED { | ||||
|         let (prod, cons) = gb::spsc_init(); | ||||
|         let sink = { | ||||
|             let s = Sink::try_new(&stream_handle)?; | ||||
|             let s = Sink::try_new(&stream_handle).expect("create sink from audio stream handle"); | ||||
|             s.append(cons); | ||||
|             s.set_volume(0.1); | ||||
|             s | ||||
|         }; | ||||
|  | ||||
|         emu.set_prod(prod); | ||||
|         emu::set_audio_prod(&mut cpu, prod); | ||||
|  | ||||
|         tracing::info!("Spawn Audio Thread"); | ||||
|         std::thread::spawn(move || { | ||||
| @@ -105,58 +78,49 @@ fn main() -> Result<()> { | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     let mut cycle_count: Cycle = Default::default(); | ||||
|     // Set up state for the Immediate-mode GUI | ||||
|     let event_loop: EventLoop<Event<()>> = EventLoopBuilder::with_user_event().build(); | ||||
|  | ||||
|     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 | ||||
|     let start_time = Instant::now(); | ||||
|  | ||||
|     event_loop.run(move |event, _, control_flow| { | ||||
|         if let Event::RedrawRequested(_) = event { | ||||
|             if pixels.render().is_err() { | ||||
|                 emu.try_write_sav().expect("Write game save if need be"); | ||||
|         app.handle_event(&event); | ||||
|  | ||||
|                 *control_flow = ControlFlow::Exit; | ||||
|                 return; | ||||
|         match event { | ||||
|             Event::MainEventsCleared => { | ||||
|                 app.maybe_quit(&cpu, control_flow); | ||||
|  | ||||
|                 match app.mode { | ||||
|                     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 | ||||
|                 last_key = gui::unused_key(); | ||||
|  | ||||
|                 app.request_redraw(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if input.update(&event) { | ||||
|             if input.key_pressed(VirtualKeyCode::Escape) || input.quit() { | ||||
|                 emu.try_write_sav().expect("Write game save if need be"); | ||||
|  | ||||
|                 *control_flow = ControlFlow::Exit; | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if let Some(size) = input.window_resized() { | ||||
|                 pixels.resize_surface(size.width, size.height); | ||||
|             } | ||||
|  | ||||
|             cycle_count += gb::emu::run_frame(&mut emu, &mut gamepad, &input); | ||||
|  | ||||
|             if cycle_count >= CYCLES_IN_FRAME { | ||||
|                 cycle_count %= CYCLES_IN_FRAME; | ||||
|  | ||||
|                 let buf: &mut [u8; GB_WIDTH * GB_HEIGHT * 4] = pixels | ||||
|                     .get_frame() | ||||
|                     .try_into() | ||||
|                     .expect("Size of Pixel Buffer is GB_WIDTH * GB_HEIGHT * 4"); | ||||
|  | ||||
|                 gb::emu::draw_frame(&emu, buf); | ||||
|                 window.request_redraw(); | ||||
|             Event::RedrawRequested(..) => { | ||||
|                 app.update_time(start_time.elapsed().as_secs_f64()); | ||||
|                 app.paint(&cpu); | ||||
|             } | ||||
|             Event::WindowEvent { event, .. } => match event { | ||||
|                 WindowEvent::Resized(size) => app.resize(size), | ||||
|                 WindowEvent::CloseRequested => emu::save_and_exit(&cpu, control_flow), | ||||
|                 WindowEvent::KeyboardInput { input, .. } => last_key = input, | ||||
|                 _ => {} | ||||
|             }, | ||||
|             _ => {} | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| fn create_window(event_loop: &EventLoop<()>, title: &str) -> Result<Window> { | ||||
|     let logical = LogicalSize::new(GB_WIDTH as f64, GB_HEIGHT as f64); | ||||
|     let physical = PhysicalSize::new( | ||||
|         (GB_WIDTH * WINDOW_SCALE) as f32, | ||||
|         (GB_HEIGHT * WINDOW_SCALE) as f32, | ||||
|     ); | ||||
|  | ||||
|     Ok(WindowBuilder::new() | ||||
|         .with_title(title) | ||||
|         .with_min_inner_size(logical) | ||||
|         .with_inner_size(physical) | ||||
|         .with_resizable(true) | ||||
|         .build(event_loop)?) | ||||
| } | ||||
|   | ||||
							
								
								
									
										208
									
								
								src/ppu.rs
									
									
									
									
									
								
							
							
						
						
									
										208
									
								
								src/ppu.rs
									
									
									
									
									
								
							| @@ -10,6 +10,8 @@ use types::{ | ||||
|     ObjectPaletteKind, ObjectSize, Pixels, RenderPriority, TileDataAddress, | ||||
| }; | ||||
|  | ||||
| use once_cell::sync::Lazy; | ||||
|  | ||||
| mod dma; | ||||
| mod types; | ||||
|  | ||||
| @@ -32,6 +34,14 @@ const LIGHT_GRAY: [u8; 4] = 0xAEBA89FFu32.to_be_bytes(); | ||||
| const DARK_GRAY: [u8; 4] = 0x5E6745FFu32.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)] | ||||
| pub struct Ppu { | ||||
|     pub(crate) int: Interrupt, | ||||
| @@ -42,13 +52,13 @@ pub struct Ppu { | ||||
|     pub(crate) monochrome: Monochrome, | ||||
|     pub(crate) pos: ScreenPosition, | ||||
|     vram: Box<[u8; VRAM_SIZE]>, | ||||
|     pub(crate) oam: ObjectAttributeTable, | ||||
|     pub(crate) oam: ObjectAttrTable, | ||||
|     pub(crate) dma: DirectMemoryAccess, | ||||
|     scan_dot: Cycle, | ||||
|     fetch: PixelFetcher, | ||||
|     fifo: PixelFifo, | ||||
|     obj_buffer: ObjectBuffer, | ||||
|     frame_buf: Box<[u8; GB_WIDTH * GB_HEIGHT * 4]>, | ||||
|     pub(crate) frame_buf: FrameBuffer, | ||||
|     win_stat: WindowStatus, | ||||
|  | ||||
|     scanline_start: bool, | ||||
| @@ -70,15 +80,26 @@ impl BusIo for Ppu { | ||||
|  | ||||
| impl Ppu { | ||||
|     pub(crate) fn tick(&mut self) { | ||||
|         self.dot += 1; | ||||
|  | ||||
|         if !self.ctrl.lcd_enabled() { | ||||
|             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); | ||||
|             self.pos.line_y = 0; | ||||
|             self.dot = 0; | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         self.dot += 1; | ||||
|  | ||||
|         match self.stat.mode() { | ||||
|             PpuMode::OamScan => { | ||||
|                 // Cycles 1 -> 80 | ||||
|  | ||||
|                 if self.dot >= 80 { | ||||
|                     self.x_pos = 0; | ||||
|                     self.scanline_start = true; | ||||
| @@ -104,12 +125,7 @@ impl Ppu { | ||||
|                 self.scan_oam(); | ||||
|             } | ||||
|             PpuMode::Drawing => { | ||||
|                 if self.ctrl.lcd_enabled() { | ||||
|                     // Only Draw when the LCD Is Enabled | ||||
|                     self.draw(); | ||||
|                 } else { | ||||
|                     self.reset(); | ||||
|                 } | ||||
|                 self.draw(); | ||||
|  | ||||
|                 if self.x_pos == 160 { | ||||
|                     if self.stat.hblank_int() { | ||||
| @@ -164,6 +180,9 @@ impl Ppu { | ||||
|                             self.int.set_lcd_stat(true); | ||||
|                         } | ||||
|  | ||||
|                         // Screen is done drawing | ||||
|                         self.frame_buf.swap(); | ||||
|  | ||||
|                         PpuMode::VBlank | ||||
|                     } else { | ||||
|                         if self.stat.oam_int() { | ||||
| @@ -265,7 +284,7 @@ impl Ppu { | ||||
|                 TileLowB => { | ||||
|                     let obj_size = self.ctrl.obj_size(); | ||||
|  | ||||
|                     let addr = PixelFetcher::get_obj_addr(attr, &self.pos, obj_size); | ||||
|                     let addr = PixelFetcher::obj_addr(attr, &self.pos, obj_size); | ||||
|  | ||||
|                     let byte = self.read_byte(addr); | ||||
|                     self.fetch.obj.tile.with_low(byte); | ||||
| @@ -276,7 +295,7 @@ impl Ppu { | ||||
|                 TileHighB => { | ||||
|                     let obj_size = self.ctrl.obj_size(); | ||||
|  | ||||
|                     let addr = PixelFetcher::get_obj_addr(attr, &self.pos, obj_size); | ||||
|                     let addr = PixelFetcher::obj_addr(attr, &self.pos, obj_size); | ||||
|  | ||||
|                     let byte = self.read_byte(addr + 1); | ||||
|                     self.fetch.obj.tile.with_high(byte); | ||||
| @@ -397,8 +416,8 @@ impl Ppu { | ||||
|                 let x = self.x_pos as usize; | ||||
|  | ||||
|                 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; | ||||
|             } | ||||
|  | ||||
| @@ -415,19 +434,6 @@ 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); | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     pub(crate) fn frame_buf(&self) -> &[u8; GB_HEIGHT * GB_WIDTH * 4] { | ||||
|         &self.frame_buf | ||||
|     } | ||||
|  | ||||
|     fn clock_fifo(&mut self) -> Option<GrayShade> { | ||||
|         use RenderPriority::*; | ||||
|  | ||||
| @@ -446,7 +452,7 @@ impl Ppu { | ||||
|  | ||||
|     fn obj_pixel(&self, obj: ObjPixelProperty) -> GrayShade { | ||||
|         use ObjectPaletteKind::*; | ||||
|         assert!(obj.shade_id != 0); | ||||
|         assert_ne!(obj.shade_id, 0); | ||||
|  | ||||
|         let p0 = &self.monochrome.obj_palette_0; | ||||
|         let p1 = &self.monochrome.obj_palette_1; | ||||
| @@ -473,12 +479,12 @@ impl Default for Ppu { | ||||
|         Self { | ||||
|             vram: Box::new([0u8; VRAM_SIZE]), | ||||
|             dot: Default::default(), | ||||
|             frame_buf: Box::new([0; GB_WIDTH * GB_HEIGHT * 4]), | ||||
|             frame_buf: FrameBuffer::new().expect("create frame buffers"), | ||||
|             int: Default::default(), | ||||
|             ctrl: Default::default(), | ||||
|             ctrl: LCDControl(0), | ||||
|             monochrome: Default::default(), | ||||
|             pos: Default::default(), | ||||
|             stat: Default::default(), | ||||
|             stat: LCDStatus(0x80), // bit 7 is always 1 | ||||
|             oam: Default::default(), | ||||
|             scan_dot: Default::default(), | ||||
|             fetch: Default::default(), | ||||
| @@ -533,7 +539,7 @@ pub(crate) struct ScreenPosition { | ||||
|     pub(crate) window_x: u8, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Default)] | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct Monochrome { | ||||
|     /// 0xFF47 | BGP - Background Palette Data | ||||
|     pub(crate) bg_palette: BackgroundPalette, | ||||
| @@ -543,12 +549,22 @@ pub(crate) struct Monochrome { | ||||
|     pub(crate) obj_palette_1: ObjectPalette, | ||||
| } | ||||
|  | ||||
| impl Default for Monochrome { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             bg_palette: BackgroundPalette(0), | ||||
|             obj_palette_0: ObjectPalette(0), | ||||
|             obj_palette_1: ObjectPalette(0), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct ObjectAttributeTable { | ||||
| pub(crate) struct ObjectAttrTable { | ||||
|     buf: Box<[u8; OAM_SIZE]>, | ||||
| } | ||||
|  | ||||
| impl BusIo for ObjectAttributeTable { | ||||
| impl BusIo for ObjectAttrTable { | ||||
|     fn read_byte(&self, addr: u16) -> u8 { | ||||
|         let index = (addr - 0xFE00) as usize; | ||||
|         self.buf[index] | ||||
| @@ -560,8 +576,8 @@ impl BusIo for ObjectAttributeTable { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ObjectAttributeTable { | ||||
|     fn attribute(&self, index: usize) -> ObjectAttribute { | ||||
| impl ObjectAttrTable { | ||||
|     fn attribute(&self, index: usize) -> ObjectAttr { | ||||
|         let start = index * 4; | ||||
|  | ||||
|         let slice: &[u8; 4] = self.buf[start..(start + 4)] | ||||
| @@ -572,7 +588,7 @@ impl ObjectAttributeTable { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for ObjectAttributeTable { | ||||
| impl Default for ObjectAttrTable { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             buf: Box::new([0; OAM_SIZE]), | ||||
| @@ -580,15 +596,15 @@ impl Default for ObjectAttributeTable { | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy, Default)] | ||||
| struct ObjectAttribute { | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| struct ObjectAttr { | ||||
|     y: u8, | ||||
|     x: u8, | ||||
|     tile_index: u8, | ||||
|     flags: ObjectFlags, | ||||
| } | ||||
|  | ||||
| impl From<[u8; 4]> for ObjectAttribute { | ||||
| impl From<[u8; 4]> for ObjectAttr { | ||||
|     fn from(bytes: [u8; 4]) -> Self { | ||||
|         Self { | ||||
|             y: bytes[0], | ||||
| @@ -599,7 +615,7 @@ impl From<[u8; 4]> for ObjectAttribute { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<'a> From<&'a [u8; 4]> for ObjectAttribute { | ||||
| impl<'a> From<&'a [u8; 4]> for ObjectAttr { | ||||
|     fn from(bytes: &'a [u8; 4]) -> Self { | ||||
|         Self { | ||||
|             y: bytes[0], | ||||
| @@ -612,7 +628,7 @@ impl<'a> From<&'a [u8; 4]> for ObjectAttribute { | ||||
|  | ||||
| #[derive(Debug)] | ||||
| struct ObjectBuffer { | ||||
|     inner: [Option<ObjectAttribute>; OBJECT_LIMIT], | ||||
|     inner: [Option<ObjectAttr>; OBJECT_LIMIT], | ||||
|     len: usize, | ||||
| } | ||||
|  | ||||
| @@ -626,7 +642,7 @@ impl ObjectBuffer { | ||||
|         self.len = 0; | ||||
|     } | ||||
|  | ||||
|     fn add(&mut self, attr: ObjectAttribute) { | ||||
|     fn add(&mut self, attr: ObjectAttr) { | ||||
|         self.inner[self.len] = Some(attr); | ||||
|         self.len += 1; | ||||
|     } | ||||
| @@ -678,7 +694,7 @@ impl PixelFetcher { | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn get_obj_addr(attr: &ObjectAttribute, pos: &ScreenPosition, size: ObjectSize) -> u16 { | ||||
|     fn obj_addr(attr: &ObjectAttr, pos: &ScreenPosition, size: ObjectSize) -> u16 { | ||||
|         let line_y = pos.line_y; | ||||
|  | ||||
|         // TODO: Why is the offset 14 and 30 respectively? | ||||
| @@ -792,21 +808,12 @@ impl Default for BackgroundFetcher { | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| #[derive(Debug, Default)] | ||||
| struct ObjectFetcher { | ||||
|     state: FetcherState, | ||||
|     tile: TileBuilder, | ||||
| } | ||||
|  | ||||
| impl Default for ObjectFetcher { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             state: Default::default(), | ||||
|             tile: Default::default(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Fetcher for ObjectFetcher { | ||||
|     fn reset(&mut self) { | ||||
|         self.state = Default::default(); | ||||
| @@ -841,7 +848,7 @@ struct BgPixelProperty { | ||||
|     shade_id: u8, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Default)] | ||||
| #[derive(Debug)] | ||||
| struct ObjPixelProperty { | ||||
|     shade_id: u8, | ||||
|     palette_kind: ObjectPaletteKind, | ||||
| @@ -914,3 +921,96 @@ struct WindowStatus { | ||||
|     /// drawing from the window tile map is true | ||||
|     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, | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| use crate::Cycle; | ||||
|  | ||||
| #[derive(Debug, Default)] | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct DirectMemoryAccess { | ||||
|     pub(crate) state: DmaState, | ||||
|     cycle: Cycle, | ||||
| @@ -8,6 +8,16 @@ pub(crate) struct DirectMemoryAccess { | ||||
|     pub(crate) start: DmaAddress, | ||||
| } | ||||
|  | ||||
| impl Default for DirectMemoryAccess { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             state: DmaState::Disabled, | ||||
|             cycle: Default::default(), | ||||
|             start: Default::default(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl DirectMemoryAccess { | ||||
|     pub(crate) fn tick(&mut self) -> Option<(u16, u16)> { | ||||
|         match self.state { | ||||
| @@ -69,12 +79,6 @@ pub(crate) enum DmaState { | ||||
|     Transferring, | ||||
| } | ||||
|  | ||||
| impl Default for DmaState { | ||||
|     fn default() -> Self { | ||||
|         Self::Disabled | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy, Default)] | ||||
| pub(crate) struct DmaAddress(Option<u16>); | ||||
|  | ||||
|   | ||||
| @@ -29,12 +29,6 @@ impl Clone for LCDStatus { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for LCDStatus { | ||||
|     fn default() -> Self { | ||||
|         Self(0x80) // bit 7 is always 1 | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<LCDStatus> for u8 { | ||||
|     fn from(status: LCDStatus) -> Self { | ||||
|         status.0 | ||||
| @@ -67,12 +61,6 @@ impl From<PpuMode> for u8 { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for PpuMode { | ||||
|     fn default() -> Self { | ||||
|         Self::HBlank | ||||
|     } | ||||
| } | ||||
|  | ||||
| bitfield! { | ||||
|     pub struct LCDControl(u8); | ||||
|     impl Debug; | ||||
| @@ -93,12 +81,6 @@ impl Clone for LCDControl { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for LCDControl { | ||||
|     fn default() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for LCDControl { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         Self(byte) | ||||
| @@ -142,12 +124,6 @@ impl From<TileMapAddress> for u8 { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for TileMapAddress { | ||||
|     fn default() -> Self { | ||||
|         Self::X9800 | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub enum TileDataAddress { | ||||
|     X8800 = 0, | ||||
| @@ -170,12 +146,6 @@ impl From<TileDataAddress> for u8 { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for TileDataAddress { | ||||
|     fn default() -> Self { | ||||
|         Self::X8800 | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub enum ObjectSize { | ||||
|     Eight = 0, | ||||
| @@ -209,12 +179,6 @@ impl From<ObjectSize> for u8 { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for ObjectSize { | ||||
|     fn default() -> Self { | ||||
|         Self::Eight | ||||
|     } | ||||
| } | ||||
|  | ||||
| bitfield! { | ||||
|     pub struct BackgroundPalette(u8); | ||||
|     impl Debug; | ||||
| @@ -243,12 +207,6 @@ impl Clone for BackgroundPalette { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for BackgroundPalette { | ||||
|     fn default() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for BackgroundPalette { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         Self(byte) | ||||
| @@ -288,12 +246,6 @@ impl Clone for ObjectPalette { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for ObjectPalette { | ||||
|     fn default() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for ObjectPalette { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         Self(byte) | ||||
| @@ -361,24 +313,12 @@ impl From<ObjectFlags> for u8 { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for ObjectFlags { | ||||
|     fn default() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub enum ObjectPaletteKind { | ||||
|     Zero = 0, | ||||
|     One = 1, | ||||
| } | ||||
|  | ||||
| impl Default for ObjectPaletteKind { | ||||
|     fn default() -> Self { | ||||
|         Self::Zero | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for ObjectPaletteKind { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         match byte & 0b01 { | ||||
| @@ -395,7 +335,7 @@ impl From<ObjectPaletteKind> for u8 { | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy, PartialEq)] | ||||
| #[derive(Debug, Clone, Copy, Eq, PartialEq)] | ||||
| pub enum RenderPriority { | ||||
|     Object = 0, | ||||
|     BackgroundAndWindow = 1, | ||||
| @@ -417,12 +357,6 @@ impl From<RenderPriority> for u8 { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for RenderPriority { | ||||
|     fn default() -> Self { | ||||
|         Self::Object | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub enum GrayShade { | ||||
|     White = 0, | ||||
| @@ -442,12 +376,6 @@ impl GrayShade { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for GrayShade { | ||||
|     fn default() -> Self { | ||||
|         Self::White | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for GrayShade { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         match byte & 0b11 { | ||||
|   | ||||
| @@ -1,79 +0,0 @@ | ||||
| use crate::Cycle; | ||||
| use std::collections::BinaryHeap; | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct Scheduler { | ||||
|     timestamp: Cycle, | ||||
|     queue: BinaryHeap<Event>, | ||||
| } | ||||
|  | ||||
| impl Scheduler { | ||||
|     pub(crate) fn init() -> Self { | ||||
|         let mut scheduler = Self { | ||||
|             timestamp: Default::default(), | ||||
|             queue: Default::default(), | ||||
|         }; | ||||
|  | ||||
|         scheduler.push(Event { | ||||
|             kind: EventKind::TimestampOverflow, | ||||
|             timestamp: Cycle::MAX, | ||||
|             cb: |_delay| panic!("Reached Cycle::MAX"), | ||||
|         }); | ||||
|  | ||||
|         scheduler | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn push(&mut self, event: Event) { | ||||
|         self.queue.push(event); | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn step(&mut self, cycles: Cycle) { | ||||
|         self.timestamp += cycles; | ||||
|  | ||||
|         loop { | ||||
|             let should_pop = match self.queue.peek() { | ||||
|                 Some(event) => self.timestamp >= event.timestamp, | ||||
|                 None => false, | ||||
|             }; | ||||
|  | ||||
|             if !should_pop { | ||||
|                 break; | ||||
|             } | ||||
|  | ||||
|             let event = self.queue.pop().expect("Pop Event from Scheduler Queue"); | ||||
|  | ||||
|             (event.cb)(self.timestamp - event.timestamp); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct Event { | ||||
|     kind: EventKind, | ||||
|     cb: fn(Cycle), | ||||
|     pub(crate) timestamp: Cycle, | ||||
| } | ||||
|  | ||||
| impl Eq for Event {} | ||||
| impl PartialEq for Event { | ||||
|     fn eq(&self, other: &Self) -> bool { | ||||
|         self.kind == other.kind && self.timestamp == other.timestamp | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl PartialOrd for Event { | ||||
|     fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { | ||||
|         Some(self.cmp(other)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Ord for Event { | ||||
|     fn cmp(&self, other: &Self) -> std::cmp::Ordering { | ||||
|         self.timestamp.cmp(&other.timestamp) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||||
| pub(crate) enum EventKind { | ||||
|     TimestampOverflow, | ||||
| } | ||||
| @@ -1,6 +1,6 @@ | ||||
| use bitfield::bitfield; | ||||
|  | ||||
| #[derive(Debug, Default)] | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct Serial { | ||||
|     /// 0xFF01 | SB - Serial Transfer Data | ||||
|     pub(crate) next: u8, | ||||
| @@ -8,6 +8,15 @@ pub(crate) struct Serial { | ||||
|     pub(crate) ctrl: SerialControl, | ||||
| } | ||||
|  | ||||
| impl Default for Serial { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             next: Default::default(), | ||||
|             ctrl: SerialControl(0), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| bitfield! { | ||||
|     pub struct SerialControl(u8); | ||||
|     impl Debug; | ||||
| @@ -23,12 +32,6 @@ impl Clone for SerialControl { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for SerialControl { | ||||
|     fn default() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for SerialControl { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         Self(byte) | ||||
| @@ -47,12 +50,6 @@ enum ShiftClock { | ||||
|     Internal = 1, | ||||
| } | ||||
|  | ||||
| impl Default for ShiftClock { | ||||
|     fn default() -> Self { | ||||
|         Self::External | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for ShiftClock { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         match byte & 0b01 { | ||||
| @@ -69,12 +66,6 @@ enum ClockSpeed { | ||||
|     Fast = 1, | ||||
| } | ||||
|  | ||||
| impl Default for ClockSpeed { | ||||
|     fn default() -> Self { | ||||
|         Self::Normal | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for ClockSpeed { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         match byte & 0b01 { | ||||
|   | ||||
| @@ -106,7 +106,7 @@ impl Timer { | ||||
| impl Default for Timer { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             ctrl: Default::default(), | ||||
|             ctrl: TimerControl(0), | ||||
|             counter: 0, | ||||
|             modulo: 0, | ||||
|             divider: 0, | ||||
| @@ -157,12 +157,6 @@ impl Clone for TimerControl { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for TimerControl { | ||||
|     fn default() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for TimerControl { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         Self(byte) | ||||
|   | ||||
| @@ -11,13 +11,13 @@ pub(crate) struct WorkRam { | ||||
| } | ||||
|  | ||||
| impl BusIo for WorkRam { | ||||
|     fn write_byte(&mut self, addr: u16, byte: u8) { | ||||
|         self.bank[addr as usize - WORK_RAM_START_ADDRESS] = byte; | ||||
|     } | ||||
|  | ||||
|     fn read_byte(&self, addr: u16) -> u8 { | ||||
|         self.bank[addr as usize - WORK_RAM_START_ADDRESS] | ||||
|     } | ||||
|  | ||||
|     fn write_byte(&mut self, addr: u16, byte: u8) { | ||||
|         self.bank[addr as usize - WORK_RAM_START_ADDRESS] = byte; | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for WorkRam { | ||||
| @@ -42,11 +42,11 @@ impl Default for VariableWorkRam { | ||||
| } | ||||
|  | ||||
| impl BusIo for VariableWorkRam { | ||||
|     fn write_byte(&mut self, addr: u16, byte: u8) { | ||||
|         self.buf[addr as usize - VARIABLE_WORK_RAM_START_ADDRESS] = byte; | ||||
|     } | ||||
|  | ||||
|     fn read_byte(&self, addr: u16) -> u8 { | ||||
|         self.buf[addr as usize - VARIABLE_WORK_RAM_START_ADDRESS] | ||||
|     } | ||||
|  | ||||
|     fn write_byte(&mut self, addr: u16, byte: u8) { | ||||
|         self.buf[addr as usize - VARIABLE_WORK_RAM_START_ADDRESS] = byte; | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user