Compare commits
	
		
			98 Commits
		
	
	
		
			01064bab69
			...
			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 | |||
| 80a15039e0 | |||
| 32405c0734 | |||
| 2ef8fefbb7 | |||
| 548945a4ee | |||
| 1526b4a09d | |||
| 4125ea5c74 | |||
| b9519d9b7a | |||
| 01278ca83f | |||
| 2bb8bd6d8f | |||
| 080c1e7518 | |||
| 765f9d8288 | |||
| 8780c4a59b | |||
| e19a540650 | |||
| 4cfd951ede | |||
| de8fe1a367 | |||
| c5fa41b20d | |||
| 552cfd4a18 | |||
| 4c516804e4 | |||
| 9b3ab73bb1 | |||
| d9f1d661ae | |||
| da83032e24 | |||
| 67e2447d3f | |||
| 37cf3d92e4 | |||
| 293e5762c3 | |||
| 999f661e6b | |||
| e573411890 | |||
| 352a65b705 | |||
| 64230973f1 | |||
| dbbf87af52 | |||
| 1440cd1fc7 | |||
| 9964b49ce1 | |||
| 142231d355 | |||
| 227928e8ca | |||
| 1001b0b124 | |||
| 71ce3f43e0 | |||
| ce121864d2 | |||
| e1fe00ab64 | |||
| 5882678bc5 | |||
| bcd67cb317 | |||
| 4516ca8477 | |||
| 6087e3b20b | |||
| 10ac579c40 | |||
| ee5504111b | |||
| a628f64d28 | |||
| 318a6e0386 | |||
| db012c7f4b | |||
| e42c87aeb7 | |||
| 9113e95fa0 | |||
| 9973dc8714 | |||
| e128025208 | |||
| 44ac0c8ebd | 
							
								
								
									
										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" | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										2910
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2910
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										27
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								Cargo.toml
									
									
									
									
									
								
							| @@ -2,20 +2,27 @@ | ||||
| name = "gb" | ||||
| version = "0.1.0" | ||||
| authors = ["Rekai Musuka <rekai@musuka.dev>"] | ||||
| edition = "2018" | ||||
| edition = "2021" | ||||
|  | ||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
|  | ||||
| [dependencies] | ||||
| anyhow = "^1.0" | ||||
| bitfield = "^0.13" | ||||
| clap = "^2.33" | ||||
| gilrs = "^0.8" | ||||
| pixels = "^0.5" | ||||
| winit = "^0.25" | ||||
| winit_input_helper = "^0.10" | ||||
| rodio = "^0.14" | ||||
| rtrb = "^0.1.4" | ||||
| anyhow = "1.0" | ||||
| 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 | ||||
|   | ||||
							
								
								
									
										47
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,14 +1,43 @@ | ||||
| # Rekai's Gameboy Emulator | ||||
| [](https://ci.paoda.moe/paoda/gb) | ||||
|  | ||||
| ### Status | ||||
| * Passes Blargg's cpu_instrs Test | ||||
| * Renders Background & Window Tiles  | ||||
| * Implements a PPU FIFO | ||||
| * From [Blargg Test ROMs](https://github.com/L-P/blargg-test-roms/) | ||||
|     * [x] cpu_instrs | ||||
|     * [x] instr_timing | ||||
|     * [x] mem_timing | ||||
|     * [x] mem_timing-2 | ||||
|     * [ ] dmg_sound (partial)  | ||||
| * [x] [dmg-acid2](https://github.com/mattcurrie/dmg-acid2) | ||||
| * From [mooneye-gb](https://github.com/Gekkio/mooneye-gb): | ||||
|     * Cartridges: | ||||
|         * [x] MBC1 | ||||
|         * [ ] MBC1M | ||||
|         * [x] MBC2 | ||||
|         * [x] MBC5 | ||||
| * Implements a cycle-accurate PPU FIFO | ||||
|     * Doesn't \*exactly\* work just yet | ||||
|  | ||||
| Supports: ROM-only, MBC1, MBC2, MBC3 and MBC5 games. | ||||
|  | ||||
|  | ||||
| ### Notes | ||||
| * [gameboy-logs](https://github.com/wheremyfoodat/Gameboy-logs) suggests that there are still some underlying problems with the cpu implementation | ||||
| * The Sprite FIFO does not work as expected yet | ||||
| * Sound is neither emulated nor stubbed. Upon writing / reading to a APU related register the emulator will panic.  | ||||
| * Code cleanup is pending completion of some minimum viable product of the emulator | ||||
| ### 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://github.com/paoda/gb/blob/85940c874460b9cb31bf9b8211bf7dda596d114a/src/joypad.rs#L114) | ||||
|  | ||||
| Key | Button | ||||
| --- | --- | ||||
| <kbd>X</kbd> | B | ||||
| <kbd>Z</kbd> | A | ||||
| <kbd>Enter</kbd> | START | ||||
| <kbd>Shift</kbd> | SELECT | ||||
|  | ||||
| Then use the Arrow keys for the D-Pad | ||||
|  | ||||
| ### Credits | ||||
| The Boot ROM found in the `bin/` directory was made by [Optix](https://github.com/Hacktix) over [here](https://github.com/Hacktix/Bootix) | ||||
							
								
								
									
										
											BIN
										
									
								
								bin/bootix_dmg.bin
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								bin/bootix_dmg.bin
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										333
									
								
								src/apu.rs
									
									
									
									
									
								
							
							
						
						
									
										333
									
								
								src/apu.rs
									
									
									
									
									
								
							| @@ -1,3 +1,4 @@ | ||||
| use crate::apu::gen::SAMPLE_RATE; | ||||
| use crate::bus::BusIo; | ||||
| use crate::emu::SM83_CLOCK_SPEED; | ||||
| use gen::SampleProducer; | ||||
| @@ -5,8 +6,8 @@ use types::ch1::{Sweep, SweepDirection}; | ||||
| use types::ch3::Volume as Ch3Volume; | ||||
| use types::ch4::{CounterWidth, Frequency as Ch4Frequency, PolynomialCounter}; | ||||
| use types::common::{EnvelopeDirection, FrequencyHigh, SoundDuty, VolumeEnvelope}; | ||||
| use types::fs::{FrameSequencer, State as FrameSequencerState}; | ||||
| use types::{ChannelControl, SoundOutput}; | ||||
| use types::fs::{FrameSequencer, State as FSState}; | ||||
| use types::{ChannelControl, NRx4, SoundOutput}; | ||||
|  | ||||
| pub mod gen; | ||||
| mod types; | ||||
| @@ -26,11 +27,13 @@ pub struct Apu { | ||||
|     /// Noise | ||||
|     ch4: Channel4, | ||||
|  | ||||
|     sequencer: FrameSequencer, | ||||
|     fs: FrameSequencer, | ||||
|     div_prev: Option<u16>, | ||||
|  | ||||
|     prod: Option<SampleProducer<f32>>, | ||||
|     pub(crate) prod: Option<SampleProducer<f32>>, | ||||
|     sample_counter: u64, | ||||
|  | ||||
|     cap: f32, | ||||
| } | ||||
|  | ||||
| impl BusIo for Apu { | ||||
| @@ -54,7 +57,7 @@ impl BusIo for Apu { | ||||
|             0x26 => self.ctrl.status(self), | ||||
|             0x30..=0x3F => self.ch3.read_byte(addr), | ||||
|             _ => { | ||||
|                 eprintln!("Read 0xFF from unused IO register {:#06X} [APU]", addr); | ||||
|                 tracing::warn!("Attempted read from {:#06X}", addr); | ||||
|                 0xFF | ||||
|             } | ||||
|         } | ||||
| @@ -66,29 +69,26 @@ impl BusIo for Apu { | ||||
|             0x11 if self.ctrl.enabled => self.ch1.set_duty(byte), | ||||
|             0x12 if self.ctrl.enabled => self.ch1.set_envelope(byte), | ||||
|             0x13 if self.ctrl.enabled => self.ch1.set_freq_lo(byte), | ||||
|             0x14 if self.ctrl.enabled => self.ch1.set_freq_hi(byte), | ||||
|             0x14 if self.ctrl.enabled => self.ch1.set_freq_hi(&self.fs, byte), | ||||
|             0x16 if self.ctrl.enabled => self.ch2.set_duty(byte), | ||||
|             0x17 if self.ctrl.enabled => self.ch2.set_envelope(byte), | ||||
|             0x18 if self.ctrl.enabled => self.ch2.set_freq_lo(byte), | ||||
|             0x19 if self.ctrl.enabled => self.ch2.set_freq_hi(byte), | ||||
|             0x19 if self.ctrl.enabled => self.ch2.set_freq_hi(&self.fs, byte), | ||||
|             0x1A if self.ctrl.enabled => self.ch3.set_dac_enabled(byte), | ||||
|             0x1B if self.ctrl.enabled => self.ch3.set_len(byte), | ||||
|             0x1C if self.ctrl.enabled => self.ch3.set_volume(byte), | ||||
|             0x1D if self.ctrl.enabled => self.ch3.set_freq_lo(byte), | ||||
|             0x1E if self.ctrl.enabled => self.ch3.set_freq_hi(byte), | ||||
|             0x1E if self.ctrl.enabled => self.ch3.set_freq_hi(&self.fs, byte), | ||||
|             0x20 if self.ctrl.enabled => self.ch4.set_len(byte), | ||||
|             0x21 if self.ctrl.enabled => self.ch4.set_envelope(byte), | ||||
|             0x22 if self.ctrl.enabled => self.ch4.set_poly(byte), | ||||
|             0x23 if self.ctrl.enabled => self.ch4.set_frequency(byte), | ||||
|             0x23 if self.ctrl.enabled => self.ch4.set_frequency(&self.fs, byte), | ||||
|             0x24 if self.ctrl.enabled => self.ctrl.set_channel(byte), | ||||
|             0x25 if self.ctrl.enabled => self.ctrl.set_output(byte), | ||||
|             0x26 => self.set_status(byte), | ||||
|             0x30..=0x3F => self.ch3.write_byte(addr, byte), | ||||
|             _ if !self.ctrl.enabled => {} | ||||
|             _ => eprintln!( | ||||
|                 "Wrote {:#04X} to unused IO register {:#06X} [APU]", | ||||
|                 byte, addr | ||||
|             ), | ||||
|             _ => tracing::warn!("Attempted write of {:#04X} to {:#06X}", byte, addr), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -99,9 +99,9 @@ impl Apu { | ||||
|  | ||||
|         // Frame Sequencer (512Hz) | ||||
|         if self.is_falling_edge(12, div) { | ||||
|             use FrameSequencerState::*; | ||||
|             use FSState::*; | ||||
|  | ||||
|             match self.sequencer.state() { | ||||
|             match self.fs.state() { | ||||
|                 Length => self.clock_length(), | ||||
|                 LengthAndSweep => { | ||||
|                     self.clock_length(); | ||||
| @@ -111,7 +111,7 @@ impl Apu { | ||||
|                 Nothing => {} | ||||
|             } | ||||
|  | ||||
|             self.sequencer.next(); | ||||
|             self.fs.next(); | ||||
|         } | ||||
|  | ||||
|         self.div_prev = Some(div); | ||||
| @@ -124,25 +124,33 @@ impl Apu { | ||||
|         if self.sample_counter >= SM83_CLOCK_SPEED { | ||||
|             self.sample_counter %= SM83_CLOCK_SPEED; | ||||
|  | ||||
|             if let Some(ref mut prod) = self.prod { | ||||
|             if let Some(prod) = self.prod.as_mut() { | ||||
|                 if prod.available_blocking() { | ||||
|                     // Sample the APU | ||||
|  | ||||
|                     let ch1_amplitude = | ||||
|                         Self::high_pass(&mut self.cap, self.ch1.amplitude(), self.ch1.enabled); | ||||
|                     let (left, right) = self.ctrl.out.ch1(); | ||||
|                     let ch1_left = if left { self.ch1.amplitude() } else { 0.0 }; | ||||
|                     let ch1_right = if right { self.ch1.amplitude() } else { 0.0 }; | ||||
|                     let ch1_left = if left { ch1_amplitude } else { 0.0 }; | ||||
|                     let ch1_right = if right { ch1_amplitude } else { 0.0 }; | ||||
|  | ||||
|                     let ch2_amplitude = | ||||
|                         Self::high_pass(&mut self.cap, self.ch2.amplitude(), self.ch2.enabled); | ||||
|                     let (left, right) = self.ctrl.out.ch2(); | ||||
|                     let ch2_left = if left { self.ch2.amplitude() } else { 0.0 }; | ||||
|                     let ch2_right = if right { self.ch2.amplitude() } else { 0.0 }; | ||||
|                     let ch2_left = if left { ch2_amplitude } else { 0.0 }; | ||||
|                     let ch2_right = if right { ch2_amplitude } else { 0.0 }; | ||||
|  | ||||
|                     let ch3_amplitude = | ||||
|                         Self::high_pass(&mut self.cap, self.ch3.amplitude(), self.ch3.enabled); | ||||
|                     let (left, right) = self.ctrl.out.ch3(); | ||||
|                     let ch3_left = if left { self.ch3.amplitude() } else { 0.0 }; | ||||
|                     let ch3_right = if right { self.ch3.amplitude() } else { 0.0 }; | ||||
|                     let ch3_left = if left { ch3_amplitude } else { 0.0 }; | ||||
|                     let ch3_right = if right { ch3_amplitude } else { 0.0 }; | ||||
|  | ||||
|                     let ch4_amplitude = | ||||
|                         Self::high_pass(&mut self.cap, self.ch4.amplitude(), self.ch4.enabled); | ||||
|                     let (left, right) = self.ctrl.out.ch4(); | ||||
|                     let ch4_left = if left { self.ch4.amplitude() } else { 0.0 }; | ||||
|                     let ch4_right = if right { self.ch4.amplitude() } else { 0.0 }; | ||||
|                     let ch4_left = if left { ch4_amplitude } else { 0.0 }; | ||||
|                     let ch4_right = if right { ch4_amplitude } else { 0.0 }; | ||||
|  | ||||
|                     let left_mixed = (ch1_left + ch2_left + ch3_left + ch4_left) / 4.0; | ||||
|                     let right_mixed = (ch1_right + ch2_right + ch3_right + ch4_right) / 4.0; | ||||
| @@ -164,7 +172,7 @@ impl Apu { | ||||
|  | ||||
|         if self.ctrl.enabled { | ||||
|             // Frame Sequencer reset to Step 0 | ||||
|             self.sequencer.reset(); | ||||
|             self.fs.reset(); | ||||
|  | ||||
|             // Square Duty units are reset to first step | ||||
|             self.ch1.duty_pos = 0; | ||||
| @@ -180,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(); | ||||
| @@ -207,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(); | ||||
| @@ -217,25 +221,13 @@ impl Apu { | ||||
|         self.ch4.enabled = Default::default(); | ||||
|     } | ||||
|  | ||||
|     fn process_length(freq_hi: &FrequencyHigh, length_timer: &mut u16, enabled: &mut bool) { | ||||
|         if freq_hi.length_disable() && *length_timer > 0 { | ||||
|             *length_timer -= 1; | ||||
|     fn process_length(freq: &impl NRx4, counter: &mut u16, enabled: &mut bool) { | ||||
|         if freq.length_enable() && *counter > 0 { | ||||
|             *counter -= 1; | ||||
|  | ||||
|             // Check in this scope ensures (only) the above subtraction | ||||
|             // made length_timer 0 | ||||
|             if *length_timer == 0 { | ||||
|                 *enabled = false; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn ch4_process_length(freq: &Ch4Frequency, length_timer: &mut u16, enabled: &mut bool) { | ||||
|         if freq.length_disable() && *length_timer > 0 { | ||||
|             *length_timer -= 1; | ||||
|  | ||||
|             // Check in this scope ensures (only) the above subtraction | ||||
|             // made length_timer 0 | ||||
|             if *length_timer == 0 { | ||||
|             if *counter == 0 { | ||||
|                 *enabled = false; | ||||
|             } | ||||
|         } | ||||
| @@ -244,25 +236,25 @@ impl Apu { | ||||
|     fn clock_length(&mut self) { | ||||
|         Self::process_length( | ||||
|             &self.ch1.freq_hi, | ||||
|             &mut self.ch1.length_timer, | ||||
|             &mut self.ch1.length_counter, | ||||
|             &mut self.ch1.enabled, | ||||
|         ); | ||||
|  | ||||
|         Self::process_length( | ||||
|             &self.ch2.freq_hi, | ||||
|             &mut self.ch2.length_timer, | ||||
|             &mut self.ch2.length_counter, | ||||
|             &mut self.ch2.enabled, | ||||
|         ); | ||||
|  | ||||
|         Self::process_length( | ||||
|             &self.ch3.freq_hi, | ||||
|             &mut self.ch3.length_timer, | ||||
|             &mut self.ch3.length_counter, | ||||
|             &mut self.ch3.enabled, | ||||
|         ); | ||||
|  | ||||
|         Self::ch4_process_length( | ||||
|         Self::process_length( | ||||
|             &self.ch4.freq, | ||||
|             &mut self.ch4.length_timer, | ||||
|             &mut self.ch4.length_counter, | ||||
|             &mut self.ch4.enabled, | ||||
|         ); | ||||
|     } | ||||
| @@ -279,7 +271,7 @@ impl Apu { | ||||
|             if self.ch1.sweep_enabled && period != 0 { | ||||
|                 let new_freq = self.ch1.calc_sweep_freq(); | ||||
|  | ||||
|                 if new_freq <= 2047 && self.ch1.sweep.shift_count() != 0 { | ||||
|                 if new_freq <= 0x7FF && self.ch1.sweep.shift_count() != 0 { | ||||
|                     self.ch1.set_frequency(new_freq); | ||||
|                     self.ch1.shadow_freq = new_freq; | ||||
|  | ||||
| @@ -337,9 +329,22 @@ impl Apu { | ||||
|             None => false, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn high_pass(capacitor: &mut f32, input: f32, enabled: bool) -> f32 { | ||||
|         const CHARGE_FACTOR: f32 = 0.999958; | ||||
|  | ||||
|         let mut output = 0.0; | ||||
|         if enabled { | ||||
|             output = input - *capacitor; | ||||
|             *capacitor = | ||||
|                 input - output * CHARGE_FACTOR.powi(SM83_CLOCK_SPEED as i32 / SAMPLE_RATE as i32); | ||||
|         } | ||||
|  | ||||
|         output | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Default)] | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct SoundControl { | ||||
|     /// 0xFF24 | NR50 - Channel Control | ||||
|     channel: ChannelControl, | ||||
| @@ -349,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 { | ||||
| @@ -408,7 +423,7 @@ pub(crate) struct Channel1 { | ||||
|     sweep_enabled: bool, | ||||
|  | ||||
|     // Length Functionality | ||||
|     length_timer: u16, | ||||
|     length_counter: u16, | ||||
|  | ||||
|     freq_timer: u16, | ||||
|     duty_pos: u8, | ||||
| @@ -435,11 +450,11 @@ impl Channel1 { | ||||
|     /// 0xFF11 | NR11 - Channel 1 Sound length / Wave pattern duty | ||||
|     pub(crate) fn set_duty(&mut self, byte: u8) { | ||||
|         self.duty = byte.into(); | ||||
|         self.length_timer = 64 - self.duty.sound_length() as u16; | ||||
|         self.length_counter = 64 - self.duty.sound_length() as u16; | ||||
|     } | ||||
|  | ||||
|     /// 0xFF12 | NR12 - Channel 1 Volume Envelope | ||||
|     pub fn envelope(&self) -> u8 { | ||||
|     pub(crate) fn envelope(&self) -> u8 { | ||||
|         u8::from(self.envelope) | ||||
|     } | ||||
|  | ||||
| @@ -463,38 +478,54 @@ impl Channel1 { | ||||
|     } | ||||
|  | ||||
|     /// 0xFF14 | NR14 - Channel 1 Frequency high | ||||
|     pub(crate) fn set_freq_hi(&mut self, byte: u8) { | ||||
|         self.freq_hi = byte.into(); | ||||
|     pub(crate) fn set_freq_hi(&mut self, fs: &FrameSequencer, byte: u8) { | ||||
|         let mut new_freq: FrequencyHigh = byte.into(); | ||||
|  | ||||
|         // If this bit is set, a trigger event occurs | ||||
|         if self.freq_hi.initial() { | ||||
|             // Envelope Behaviour during trigger event | ||||
|         if new_freq.trigger() { | ||||
|             self.enabled = true; | ||||
|  | ||||
|             if self.length_counter == 0 { | ||||
|                 self.length_counter = 64; | ||||
|  | ||||
|                 // length_counter was 0 so length evidently wasn't enabled | ||||
|                 self.freq_hi.set_length_enable(false); | ||||
|             }; | ||||
|  | ||||
|             // reload freq_timer but last two bits are unmodified | ||||
|             self.freq_timer = obscure::square::freq_timer_reload(self.freq_timer, self.frequency()); | ||||
|  | ||||
|             // Volume Envelope loaded w/ period | ||||
|             self.period_timer = self.envelope.period(); | ||||
|  | ||||
|             // Channel Volume reloaded | ||||
|             self.current_volume = self.envelope.init_vol(); | ||||
|  | ||||
|             // Sweep behaviour during trigger event | ||||
|             // Channel 1 Sweep Behaviour | ||||
|             let sweep_period = self.sweep.period(); | ||||
|             let sweep_shift = self.sweep.shift_count(); | ||||
|  | ||||
|             self.shadow_freq = self.frequency(); | ||||
|             self.sweep_timer = if sweep_period == 0 { 8 } else { sweep_period }; | ||||
|  | ||||
|             if sweep_period != 0 || sweep_shift != 0 { | ||||
|                 self.sweep_enabled = true; | ||||
|             } | ||||
|             self.sweep_enabled = sweep_period != 0 || sweep_shift != 0; | ||||
|  | ||||
|             if sweep_shift != 0 { | ||||
|                 let _ = self.calc_sweep_freq(); | ||||
|             } | ||||
|  | ||||
|             // Length behaviour during trigger event | ||||
|             if self.length_timer == 0 { | ||||
|                 self.length_timer = 64; | ||||
|             } | ||||
|  | ||||
|             if self.is_dac_enabled() { | ||||
|                 self.enabled = true; | ||||
|             } | ||||
|             self.enabled = self.is_dac_enabled(); | ||||
|         } | ||||
|  | ||||
|         obscure::nrx4::length_update( | ||||
|             &mut new_freq, | ||||
|             fs, | ||||
|             &mut self.length_counter, | ||||
|             &mut self.enabled, | ||||
|             self.freq_hi.length_enable(), | ||||
|         ); | ||||
|  | ||||
|         self.freq_hi = new_freq; | ||||
|     } | ||||
|  | ||||
|     fn tick(&mut self) { | ||||
| @@ -556,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, | ||||
| @@ -568,7 +599,7 @@ pub(crate) struct Channel2 { | ||||
|     current_volume: u8, | ||||
|  | ||||
|     // Length Functionality | ||||
|     length_timer: u16, | ||||
|     length_counter: u16, | ||||
|  | ||||
|     freq_timer: u16, | ||||
|     duty_pos: u8, | ||||
| @@ -585,15 +616,15 @@ impl Channel2 { | ||||
|     /// 0xFF16 | NR21 - Channel 2 Sound length / Wave Pattern Duty | ||||
|     pub(crate) fn set_duty(&mut self, byte: u8) { | ||||
|         self.duty = byte.into(); | ||||
|         self.length_timer = 64 - self.duty.sound_length() as u16; | ||||
|         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(); | ||||
|  | ||||
| @@ -613,22 +644,36 @@ impl Channel2 { | ||||
|     } | ||||
|  | ||||
|     /// 0xFF19 | NR24 - Channel 2 Frequency high | ||||
|     pub(crate) fn set_freq_hi(&mut self, byte: u8) { | ||||
|     pub(crate) fn set_freq_hi(&mut self, fs: &FrameSequencer, byte: u8) { | ||||
|         let prev_le = self.freq_hi.length_enable(); | ||||
|         self.freq_hi = byte.into(); | ||||
|  | ||||
|         if self.freq_hi.initial() { | ||||
|             // Envelope behaviour during trigger event | ||||
|         obscure::nrx4::length_update( | ||||
|             &mut self.freq_hi, | ||||
|             fs, | ||||
|             &mut self.length_counter, | ||||
|             &mut self.enabled, | ||||
|             prev_le, | ||||
|         ); | ||||
|  | ||||
|         if self.freq_hi.trigger() { | ||||
|             self.enabled = true; | ||||
|  | ||||
|             // Reload length counter if need be | ||||
|             if self.length_counter == 0 { | ||||
|                 self.length_counter = 64; | ||||
|             } | ||||
|  | ||||
|             // reload frequency timer | ||||
|             self.freq_timer = obscure::square::freq_timer_reload(self.freq_timer, self.frequency()); | ||||
|  | ||||
|             // reload envelope | ||||
|             self.period_timer = self.envelope.period(); | ||||
|  | ||||
|             // reload volume | ||||
|             self.current_volume = self.envelope.init_vol(); | ||||
|  | ||||
|             // Length behaviour during trigger event | ||||
|             if self.length_timer == 0 { | ||||
|                 self.length_timer = 64; | ||||
|             } | ||||
|  | ||||
|             if self.is_dac_enabled() { | ||||
|                 self.enabled = true; | ||||
|             } | ||||
|             self.enabled = self.is_dac_enabled(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -679,7 +724,7 @@ pub(crate) struct Channel3 { | ||||
|     wave_ram: [u8; WAVE_PATTERN_RAM_LEN], | ||||
|  | ||||
|     // Length Functionality | ||||
|     length_timer: u16, | ||||
|     length_counter: u16, | ||||
|  | ||||
|     freq_timer: u16, | ||||
|     offset: u8, | ||||
| @@ -725,7 +770,7 @@ impl Channel3 { | ||||
|     /// 0xFF1B | NR31 - Sound Length | ||||
|     pub(crate) fn set_len(&mut self, byte: u8) { | ||||
|         self.len = byte; | ||||
|         self.length_timer = 256 - self.len as u16; | ||||
|         self.length_counter = 256 - self.len as u16; | ||||
|     } | ||||
|  | ||||
|     /// 0xFF1C | NR32 - Channel 3 Volume | ||||
| @@ -757,18 +802,32 @@ impl Channel3 { | ||||
|     } | ||||
|  | ||||
|     /// 0xFF1E | NR34 - Channel 3 Frequency high | ||||
|     pub(crate) fn set_freq_hi(&mut self, byte: u8) { | ||||
|     pub(crate) fn set_freq_hi(&mut self, fs: &FrameSequencer, byte: u8) { | ||||
|         let prev_le = self.freq_hi.length_enable(); | ||||
|         self.freq_hi = byte.into(); | ||||
|  | ||||
|         if self.freq_hi.initial() { | ||||
|         obscure::nrx4::length_update( | ||||
|             &mut self.freq_hi, | ||||
|             fs, | ||||
|             &mut self.length_counter, | ||||
|             &mut self.enabled, | ||||
|             prev_le, | ||||
|         ); | ||||
|  | ||||
|         if self.freq_hi.trigger() { | ||||
|             self.enabled = true; | ||||
|  | ||||
|             // Length behaviour during trigger event | ||||
|             if self.length_timer == 0 { | ||||
|                 self.length_timer = 256; | ||||
|             if self.length_counter == 0 { | ||||
|                 self.length_counter = 256; | ||||
|             } | ||||
|  | ||||
|             if self.dac_enabled { | ||||
|                 self.enabled = true; | ||||
|             } | ||||
|             self.freq_timer = (2048 - self.frequency()) * 2; | ||||
|  | ||||
|             // reset wave channel's ptr into wave RAM | ||||
|             self.offset = 0; | ||||
|  | ||||
|             self.enabled = self.dac_enabled; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -827,7 +886,7 @@ pub(crate) struct Channel4 { | ||||
|     current_volume: u8, | ||||
|  | ||||
|     // Length Functionality | ||||
|     length_timer: u16, | ||||
|     length_counter: u16, | ||||
|  | ||||
|     /// Linear Feedback Shift Register (15-bit) | ||||
|     lf_shift: u16, | ||||
| @@ -841,7 +900,7 @@ impl Channel4 { | ||||
|     /// 0xFF20 | NR41 - Channel 4 Sound Length | ||||
|     pub(crate) fn set_len(&mut self, byte: u8) { | ||||
|         self.len = byte & 0x3F; | ||||
|         self.length_timer = 64 - self.len as u16; | ||||
|         self.length_counter = 64 - self.len as u16; | ||||
|     } | ||||
|  | ||||
|     /// 0xFF21 | NR42 - Channel 4 Volume Envelope | ||||
| @@ -874,25 +933,37 @@ impl Channel4 { | ||||
|     } | ||||
|  | ||||
|     /// 0xFF23 | NR44 - Channel 4 Counter / Consecutive Selector and Restart | ||||
|     pub(crate) fn set_frequency(&mut self, byte: u8) { | ||||
|     pub(crate) fn set_frequency(&mut self, fs: &FrameSequencer, byte: u8) { | ||||
|         let prev_le = self.freq.length_enable(); | ||||
|         self.freq = byte.into(); | ||||
|  | ||||
|         if self.freq.initial() { | ||||
|             // Envelope behaviour during trigger event | ||||
|         obscure::nrx4::length_update( | ||||
|             &mut self.freq, | ||||
|             fs, | ||||
|             &mut self.length_counter, | ||||
|             &mut self.enabled, | ||||
|             prev_le, | ||||
|         ); | ||||
|  | ||||
|         if self.freq.trigger() { | ||||
|             self.enabled = true; | ||||
|  | ||||
|             if self.length_counter == 0 { | ||||
|                 self.length_counter = 64; | ||||
|             } | ||||
|  | ||||
|             // FIXME: Frequency Timer reloaded? | ||||
|  | ||||
|             // reload envelope | ||||
|             self.period_timer = self.envelope.period(); | ||||
|  | ||||
|             //reload volume | ||||
|             self.current_volume = self.envelope.init_vol(); | ||||
|  | ||||
|             // Length behaviour during trigger event | ||||
|             if self.length_timer == 0 { | ||||
|                 self.length_timer = 64; | ||||
|             } | ||||
|  | ||||
|             // LFSR behaviour during trigger event | ||||
|             // LFSR reset | ||||
|             self.lf_shift = 0x7FFF; | ||||
|  | ||||
|             if self.is_dac_enabled() { | ||||
|                 self.enabled = true; | ||||
|             } | ||||
|             self.enabled = self.is_dac_enabled(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -937,3 +1008,39 @@ impl Channel4 { | ||||
|         code << 4 | ||||
|     } | ||||
| } | ||||
|  | ||||
| mod obscure { | ||||
|     pub(super) mod square { | ||||
|         pub(crate) fn freq_timer_reload(freq_timer: u16, frequency: u16) -> u16 { | ||||
|             (freq_timer & 0x0003) | (((2048 - frequency) * 4) & 0xFFFC) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub(super) mod nrx4 { | ||||
|         use super::super::{FrameSequencer, NRx4}; | ||||
|  | ||||
|         /// Implements the obscure behaviour when writing to NRX4 under certain | ||||
|         /// conditions | ||||
|         /// | ||||
|         /// # Arguments | ||||
|         /// * `freq_hi` - mutable reference to a channel's frequency high register | ||||
|         /// * `fs` - reference to the APU's frame sequencer | ||||
|         /// * `counter` - mutable reference to a channel's internal enabled flag | ||||
|         /// * `prev_le` - what length_enable was before NRx4 was written with a new value | ||||
|         pub(crate) fn length_update( | ||||
|             freq: &mut impl NRx4, | ||||
|             fs: &FrameSequencer, | ||||
|             counter: &mut u16, | ||||
|             enabled: &mut bool, | ||||
|             prev_le: bool, | ||||
|         ) { | ||||
|             if !fs.next_clocks_length() && !prev_le && freq.length_enable() && *counter != 0 { | ||||
|                 *counter -= 1; | ||||
|  | ||||
|                 if *counter == 0 && !freq.trigger() { | ||||
|                     *enabled = false; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -5,33 +5,12 @@ pub(crate) const SAMPLE_RATE: u32 = 48000; // Hz | ||||
| const CHANNEL_COUNT: usize = 2; | ||||
| const BUFFER_CAPACITY: usize = 2048 * CHANNEL_COUNT; // # of samples * the # of channels | ||||
|  | ||||
| pub struct AudioSPSC<T> { | ||||
|     inner: RingBuffer<T>, | ||||
| } | ||||
|  | ||||
| impl<T> Default for AudioSPSC<T> { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             inner: RingBuffer::new(BUFFER_CAPACITY), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<T> AudioSPSC<T> { | ||||
|     pub fn new(capacity: usize) -> Self { | ||||
|         Self { | ||||
|             inner: RingBuffer::new(capacity), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn init(self) -> (SampleProducer<T>, SampleConsumer<T>) { | ||||
|         let (prod, cons) = self.inner.split(); | ||||
|  | ||||
|         ( | ||||
|             SampleProducer { inner: prod }, | ||||
|             SampleConsumer { inner: cons }, | ||||
|         ) | ||||
|     } | ||||
| pub fn init<T>() -> (SampleProducer<T>, SampleConsumer<T>) { | ||||
|     let (prod, cons) = RingBuffer::new(BUFFER_CAPACITY); | ||||
|     ( | ||||
|         SampleProducer { inner: prod }, | ||||
|         SampleConsumer { inner: cons }, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| pub struct SampleProducer<T> { | ||||
|   | ||||
| @@ -1,5 +1,11 @@ | ||||
| use bitfield::bitfield; | ||||
|  | ||||
| pub(crate) trait NRx4 { | ||||
|     fn trigger(&self) -> bool; | ||||
|     fn length_enable(&self) -> bool; | ||||
|     fn set_length_enable(&mut self, value: bool); | ||||
| } | ||||
|  | ||||
| pub(crate) mod ch1 { | ||||
|     use super::bitfield; | ||||
|  | ||||
| @@ -103,7 +109,7 @@ pub(super) mod ch3 { | ||||
| } | ||||
|  | ||||
| pub(super) mod ch4 { | ||||
|     use super::bitfield; | ||||
|     use super::{bitfield, NRx4}; | ||||
|  | ||||
|     bitfield! { | ||||
|         pub struct PolynomialCounter(u8); | ||||
| @@ -163,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), | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -177,17 +183,21 @@ pub(super) mod ch4 { | ||||
|     bitfield! { | ||||
|         pub struct Frequency(u8); | ||||
|         impl Debug; | ||||
|         _initial, _: 7; | ||||
|         _length_disable, _: 6; | ||||
|         _trigger, _: 7; | ||||
|         _length_enable, _set_length_enable: 6; | ||||
|     } | ||||
|  | ||||
|     impl Frequency { | ||||
|         pub(crate) fn length_disable(&self) -> bool { | ||||
|             self._length_disable() | ||||
|     impl NRx4 for Frequency { | ||||
|         fn trigger(&self) -> bool { | ||||
|             self._trigger() | ||||
|         } | ||||
|  | ||||
|         pub(crate) fn initial(&self) -> bool { | ||||
|             self._initial() | ||||
|         fn length_enable(&self) -> bool { | ||||
|             self._length_enable() | ||||
|         } | ||||
|  | ||||
|         fn set_length_enable(&mut self, value: bool) { | ||||
|             self._set_length_enable(value); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -218,23 +228,27 @@ pub(super) mod ch4 { | ||||
| } | ||||
|  | ||||
| pub(super) mod common { | ||||
|     use super::bitfield; | ||||
|     use super::{bitfield, NRx4}; | ||||
|  | ||||
|     bitfield! { | ||||
|         pub struct FrequencyHigh(u8); | ||||
|         impl Debug; | ||||
|         _initial, _: 7; | ||||
|         _length_disable, _: 6; | ||||
|         _trigger, _: 7; | ||||
|         _length_enable, _set_length_enable: 6; | ||||
|         pub freq_bits, set_freq_bits: 2, 0; | ||||
|     } | ||||
|  | ||||
|     impl FrequencyHigh { | ||||
|         pub(crate) fn initial(&self) -> bool { | ||||
|             self._initial() | ||||
|     impl NRx4 for FrequencyHigh { | ||||
|         fn trigger(&self) -> bool { | ||||
|             self._trigger() | ||||
|         } | ||||
|  | ||||
|         pub(crate) fn length_disable(&self) -> bool { | ||||
|             self._length_disable() | ||||
|         fn length_enable(&self) -> bool { | ||||
|             self._length_enable() | ||||
|         } | ||||
|  | ||||
|         fn set_length_enable(&mut self, value: bool) { | ||||
|             self._set_length_enable(value); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -402,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 | ||||
| @@ -465,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) | ||||
| @@ -509,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) | ||||
| @@ -557,6 +553,15 @@ pub(super) mod fs { | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         pub(crate) fn next_clocks_length(&self) -> bool { | ||||
|             use State::*; | ||||
|  | ||||
|             match self.state { | ||||
|                 Length | LengthAndSweep => true, | ||||
|                 Nothing | Envelope => false, | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         pub(crate) fn state(&self) -> State { | ||||
|             self.state | ||||
|         } | ||||
| @@ -567,7 +572,7 @@ pub(super) mod fs { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[derive(Debug, Clone, Copy)] | ||||
|     #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||||
|     pub(crate) enum State { | ||||
|         Length, | ||||
|         Nothing, | ||||
|   | ||||
							
								
								
									
										104
									
								
								src/bus.rs
									
									
									
									
									
								
							
							
						
						
									
										104
									
								
								src/bus.rs
									
									
									
									
									
								
							| @@ -7,63 +7,30 @@ use crate::ppu::{Ppu, PpuMode}; | ||||
| use crate::serial::Serial; | ||||
| use crate::timer::Timer; | ||||
| use crate::work_ram::{VariableWorkRam, WorkRam}; | ||||
| use std::{fs::File, io::Read}; | ||||
|  | ||||
| const BOOT_ROM_SIZE: usize = 0x100; | ||||
| pub(crate) const BOOT_SIZE: usize = 0x100; | ||||
|  | ||||
| #[derive(Debug)] | ||||
| #[derive(Debug, Default)] | ||||
| pub struct Bus { | ||||
|     boot: Option<[u8; BOOT_ROM_SIZE]>, // Boot ROM is 256b long | ||||
|     cartridge: Option<Cartridge>, | ||||
|     boot: Option<[u8; BOOT_SIZE]>, // Boot ROM is 256b long | ||||
|     pub(crate) cart: Option<Cartridge>, | ||||
|     pub(crate) ppu: Ppu, | ||||
|     work_ram: WorkRam, | ||||
|     var_ram: VariableWorkRam, | ||||
|     pub(crate) timer: Timer, | ||||
|     timer: Timer, | ||||
|     int: Interrupt, | ||||
|     pub(crate) apu: Apu, | ||||
|     high_ram: HighRam, | ||||
|     serial: Serial, | ||||
|     pub(crate) joypad: Joypad, | ||||
| } | ||||
|  | ||||
| impl Default for Bus { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             boot: None, | ||||
|             cartridge: 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 { | ||||
|     pub(crate) fn with_boot(path: &str) -> anyhow::Result<Self> { | ||||
|         let mut file = File::open(path)?; | ||||
|         let mut boot_rom = [0u8; 256]; | ||||
|  | ||||
|         file.read_exact(&mut boot_rom)?; | ||||
|  | ||||
|         Ok(Self { | ||||
|             boot: Some(boot_rom), | ||||
|     pub(crate) fn with_boot(rom: [u8; 256]) -> Self { | ||||
|         Self { | ||||
|             boot: Some(rom), | ||||
|             ..Default::default() | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn load_cartridge(&mut self, path: &str) -> std::io::Result<()> { | ||||
|         self.cartridge = Some(Cartridge::new(path)?); | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn rom_title(&self) -> Option<&str> { | ||||
|         self.cartridge.as_ref()?.title() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[allow(dead_code)] | ||||
| @@ -71,13 +38,19 @@ impl Bus { | ||||
|         self.boot.is_some() | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     pub(crate) fn clock(&mut self) { | ||||
|         self.tick(4); | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     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() | ||||
| @@ -93,7 +66,7 @@ impl Bus { | ||||
| } | ||||
|  | ||||
| impl Bus { | ||||
|     pub fn oam_read_byte(&self, addr: u16) -> u8 { | ||||
|     pub(crate) fn oam_read_byte(&self, addr: u16) -> u8 { | ||||
|         match addr { | ||||
|             0x0000..=0x7FFF => { | ||||
|                 // 16KB ROM bank 00 (ends at 0x3FFF) | ||||
| @@ -104,16 +77,16 @@ impl Bus { | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 match self.cartridge.as_ref() { | ||||
|                 match self.cart.as_ref() { | ||||
|                     Some(cart) => cart.read_byte(addr), | ||||
|                     None => panic!("Tried to read from a non-existent cartridge"), | ||||
|                     None => 0xFF, | ||||
|                 } | ||||
|             } | ||||
|             0x8000..=0x9FFF => self.ppu.read_byte(addr), // 8KB Video RAM | ||||
|             0xA000..=0xBFFF => match self.cartridge.as_ref() { | ||||
|             0xA000..=0xBFFF => match self.cart.as_ref() { | ||||
|                 // 8KB External RAM | ||||
|                 Some(cart) => cart.read_byte(addr), | ||||
|                 None => panic!("Tried to read from a non-existent cartridge"), | ||||
|                 None => 0xFF, | ||||
|             }, | ||||
|             0xC000..=0xCFFF => self.work_ram.read_byte(addr), // 4KB Work RAM Bank 0 | ||||
|             0xD000..=0xDFFF => self.var_ram.read_byte(addr),  // 4KB Work RAM Bank 1 -> N | ||||
| @@ -140,7 +113,7 @@ impl Bus { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn oam_write_byte(&mut self, addr: u16, byte: u8) { | ||||
|     pub(crate) fn oam_write_byte(&mut self, addr: u16, byte: u8) { | ||||
|         self.ppu.oam.write_byte(addr, byte); | ||||
|     } | ||||
| } | ||||
| @@ -157,9 +130,9 @@ impl BusIo for Bus { | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 match self.cartridge.as_ref() { | ||||
|                 match self.cart.as_ref() { | ||||
|                     Some(cart) => cart.read_byte(addr), | ||||
|                     None => panic!("Tried to read from a non-existent cartridge"), | ||||
|                     None => 0xFF, | ||||
|                 } | ||||
|             } | ||||
|             0x8000..=0x9FFF => { | ||||
| @@ -169,10 +142,10 @@ impl BusIo for Bus { | ||||
|                     _ => self.ppu.read_byte(addr), | ||||
|                 } | ||||
|             } | ||||
|             0xA000..=0xBFFF => match self.cartridge.as_ref() { | ||||
|             0xA000..=0xBFFF => match self.cart.as_ref() { | ||||
|                 // 8KB External RAM | ||||
|                 Some(cart) => cart.read_byte(addr), | ||||
|                 None => panic!("Tried to read from a non-existent cartridge"), | ||||
|                 None => 0xFF, | ||||
|             }, | ||||
|             0xC000..=0xCFFF => self.work_ram.read_byte(addr), // 4KB Work RAM Bank 0 | ||||
|             0xD000..=0xDFFF => self.var_ram.read_byte(addr),  // 4KB Work RAM Bank 1 -> N | ||||
| @@ -219,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, | ||||
| @@ -240,8 +213,9 @@ impl BusIo for Bus { | ||||
|                     0x49 => self.ppu.monochrome.obj_palette_1.into(), | ||||
|                     0x4A => self.ppu.pos.window_y, | ||||
|                     0x4B => self.ppu.pos.window_x, | ||||
|                     0x4F => 0xFF, // CGB VRAM Bank Select | ||||
|                     _ => { | ||||
|                         eprintln!("Read 0xFF from unused IO register {:#06X}.", addr); | ||||
|                         tracing::warn!("Attempted read from {:#06X} on IO", addr); | ||||
|                         0xFF | ||||
|                     } | ||||
|                 } | ||||
| @@ -262,9 +236,8 @@ impl BusIo for Bus { | ||||
|             0x0000..=0x7FFF => { | ||||
|                 // 16KB ROM bank 00 (ends at 0x3FFF) | ||||
|                 // and 16KB ROM Bank 01 -> NN (switchable via MB) | ||||
|                 match self.cartridge.as_mut() { | ||||
|                     Some(cart) => cart.write_byte(addr, byte), | ||||
|                     None => panic!("Tried to write into non-existent cartridge"), | ||||
|                 if let Some(cart) = self.cart.as_mut() { | ||||
|                     cart.write_byte(addr, byte); | ||||
|                 } | ||||
|             } | ||||
|             0x8000..=0x9FFF => { | ||||
| @@ -276,9 +249,8 @@ impl BusIo for Bus { | ||||
|             } | ||||
|             0xA000..=0xBFFF => { | ||||
|                 // 8KB External RAM | ||||
|                 match self.cartridge.as_mut() { | ||||
|                     Some(cart) => cart.write_byte(addr, byte), | ||||
|                     None => panic!("Tried to write into non-existent cartridge"), | ||||
|                 if let Some(cart) = self.cart.as_mut() { | ||||
|                     cart.write_byte(addr, byte); | ||||
|                 } | ||||
|             } | ||||
|             0xC000..=0xCFFF => self.work_ram.write_byte(addr, byte), // 4KB Work RAM Bank 0 | ||||
| @@ -318,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, | ||||
| @@ -352,13 +324,15 @@ impl BusIo for Bus { | ||||
|                     0x4A => self.ppu.pos.window_y = byte, | ||||
|                     0x4B => self.ppu.pos.window_x = byte, | ||||
|                     0x4D => {} // CGB Specific Register | ||||
|                     0x4F => {} // CGB VRAM Bank Select | ||||
|                     0x50 => { | ||||
|                         // Disable Boot ROM | ||||
|                         if byte != 0 { | ||||
|                             self.boot = None; | ||||
|                         } | ||||
|                     } | ||||
|                     _ => eprintln!("Wrote {:#04X} to unused IO register {:#06X}.", byte, addr), | ||||
|                     0x70 => {} // CGB WRAM Bank Select | ||||
|                     _ => tracing::warn!("Attempted write of {:#04X} to {:#06X} on IO", byte, addr), | ||||
|                 }; | ||||
|             } | ||||
|             0xFF80..=0xFFFE => { | ||||
| @@ -380,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(); | ||||
| @@ -410,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); | ||||
|   | ||||
							
								
								
									
										774
									
								
								src/cartridge.rs
									
									
									
									
									
								
							
							
						
						
									
										774
									
								
								src/cartridge.rs
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										122
									
								
								src/cpu.rs
									
									
									
									
									
								
							
							
						
						
									
										122
									
								
								src/cpu.rs
									
									
									
									
									
								
							| @@ -1,16 +1,13 @@ | ||||
| use crate::apu::Apu; | ||||
| use crate::bus::{Bus, BusIo}; | ||||
| use crate::instruction::cycle::Cycle; | ||||
| use crate::bus::{Bus, BusIo, BOOT_SIZE}; | ||||
| use crate::instruction::Instruction; | ||||
| use crate::interrupt::{InterruptEnable, InterruptFlag}; | ||||
| use crate::joypad::Joypad; | ||||
| use crate::ppu::Ppu; | ||||
| use crate::Cycle; | ||||
| use bitfield::bitfield; | ||||
| use std::fmt::{Display, Formatter, Result as FmtResult}; | ||||
|  | ||||
| #[derive(Debug, Default)] | ||||
| #[derive(Debug)] | ||||
| pub struct Cpu { | ||||
|     pub bus: Bus, | ||||
|     pub(crate) bus: Bus, | ||||
|     reg: Registers, | ||||
|     flags: Flags, | ||||
|     ime: ImeState, | ||||
| @@ -18,31 +15,16 @@ pub struct Cpu { | ||||
| } | ||||
|  | ||||
| impl Cpu { | ||||
|     pub fn new() -> Self { | ||||
|     pub(crate) fn new(rom: [u8; BOOT_SIZE]) -> 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() | ||||
|             bus: Bus::with_boot(rom), | ||||
|             reg: Default::default(), | ||||
|             flags: Flags(0), | ||||
|             ime: ImeState::Disabled, | ||||
|             state: State::Execute, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn boot_new(path: &str) -> anyhow::Result<Self> { | ||||
|         Ok(Self { | ||||
|             bus: Bus::with_boot(path)?, | ||||
|             ..Default::default() | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn ime(&self) -> ImeState { | ||||
|         self.ime | ||||
|     } | ||||
| @@ -60,10 +42,7 @@ impl Cpu { | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn is_halted(&self) -> bool { | ||||
|         match self.state { | ||||
|             State::Halt(_) => true, | ||||
|             _ => false, | ||||
|         } | ||||
|         matches!(self.state, State::Halt(_)) | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn halt_kind(&self) -> Option<HaltKind> { | ||||
| @@ -72,13 +51,11 @@ impl Cpu { | ||||
|             _ => None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|     pub fn load_cartridge(&mut self, path: &str) -> std::io::Result<()> { | ||||
|         self.bus.load_cartridge(path) | ||||
|     } | ||||
|  | ||||
|     pub fn rom_title(&self) -> Option<&str> { | ||||
|         self.bus.rom_title() | ||||
| impl Default for Cpu { | ||||
|     fn default() -> Self { | ||||
|         Cpu::new(*include_bytes!("../bin/bootix_dmg.bin")) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -116,7 +93,7 @@ impl Cpu { | ||||
|     /// routine. | ||||
|     /// | ||||
|     /// Handle HALT and interrupts. | ||||
|     pub fn step(&mut self) -> Cycle { | ||||
|     pub(crate) fn step(&mut self) -> Cycle { | ||||
|         // Log instructions | ||||
|         // if self.reg.pc > 0xFF { | ||||
|         //     let out = std::io::stdout(); | ||||
| @@ -131,13 +108,10 @@ impl Cpu { | ||||
|             use HaltKind::*; | ||||
|  | ||||
|             self.bus.clock(); | ||||
|  | ||||
|             let elapsed = match kind { | ||||
|                 ImeEnabled | NonePending => Cycle::new(4), | ||||
|             return match kind { | ||||
|                 ImeEnabled | NonePending => 4, | ||||
|                 SomePending => todo!("Implement HALT bug"), | ||||
|             }; | ||||
|  | ||||
|             return elapsed; | ||||
|         } | ||||
|  | ||||
|         let opcode = self.fetch(); | ||||
| @@ -167,18 +141,6 @@ impl BusIo for Cpu { | ||||
| } | ||||
|  | ||||
| impl Cpu { | ||||
|     pub fn ppu(&mut self) -> &Ppu { | ||||
|         &self.bus.ppu | ||||
|     } | ||||
|  | ||||
|     pub fn apu_mut(&mut self) -> &mut Apu { | ||||
|         &mut self.bus.apu | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn joypad_mut(&mut self) -> &mut Joypad { | ||||
|         &mut self.bus.joypad | ||||
|     } | ||||
|  | ||||
|     fn handle_ei(&mut self) { | ||||
|         match self.ime { | ||||
|             ImeState::EiExecuted => self.ime = ImeState::Pending, | ||||
| @@ -276,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::*; | ||||
| @@ -475,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() { | ||||
| @@ -536,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) | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										194
									
								
								src/emu.rs
									
									
									
									
									
								
							
							
						
						
									
										194
									
								
								src/emu.rs
									
									
									
									
									
								
							| @@ -1,73 +1,163 @@ | ||||
| use crate::cpu::Cpu as SM83; | ||||
| use crate::instruction::cycle::Cycle; | ||||
| use crate::joypad; | ||||
| use crate::ppu::Ppu; | ||||
| use anyhow::Result; | ||||
| use crate::apu::gen::SampleProducer; | ||||
| use crate::bus::BOOT_SIZE; | ||||
| use crate::cartridge::Cartridge; | ||||
| use crate::cpu::Cpu; | ||||
| use crate::{Cycle, GB_HEIGHT, GB_WIDTH}; | ||||
| use clap::crate_name; | ||||
| use gilrs::Gilrs; | ||||
| 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 = Cycle::new(456 * 154); // 456 Cycles times 154 scanlines | ||||
| pub const CYCLES_IN_FRAME: Cycle = 456 * 154; // 456 Cycles times 154 scanlines | ||||
| pub(crate) const SM83_CLOCK_SPEED: u64 = 0x40_0000; // Hz which is 4.194304Mhz | ||||
| const DEFAULT_TITLE: &str = "DMG-01 Emulator"; | ||||
| const GAMEPAD_ENABLED: bool = true; | ||||
| const DEFAULT_TITLE: &str = "Game Boy Screen"; | ||||
|  | ||||
| pub fn init(boot_path: Option<&str>, rom_path: &str) -> Result<SM83> { | ||||
|     let mut cpu = match boot_path { | ||||
|         Some(path) => SM83::boot_new(path)?, | ||||
|         None => SM83::new(), | ||||
|     }; | ||||
|  | ||||
|     eprintln!("Initialized GB Emulator"); | ||||
|  | ||||
|     cpu.load_cartridge(rom_path)?; | ||||
|     Ok(cpu) | ||||
| #[inline] | ||||
| pub fn run_frame(cpu: &mut Cpu, gamepad: &mut Gilrs, key: KeyboardInput) { | ||||
|     run(cpu, gamepad, key, CYCLES_IN_FRAME) | ||||
| } | ||||
|  | ||||
| pub fn rom_title(game_boy: &SM83) -> &str { | ||||
|     game_boy.rom_title().unwrap_or(DEFAULT_TITLE) | ||||
| #[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() { | ||||
|         crate::joypad::handle_gamepad_input(&mut cpu.bus.joyp, event); | ||||
|     } | ||||
|     crate::joypad::handle_keyboard_input(&mut cpu.bus.joyp, key); | ||||
|  | ||||
|     while elapsed < cycles { | ||||
|         elapsed += cpu.step(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn run( | ||||
|     game_boy: &mut SM83, | ||||
|     gamepad: &mut Gilrs, | ||||
|     input: &WinitInputHelper, | ||||
|     target: Cycle, | ||||
| ) -> Cycle { | ||||
|     let mut elapsed = Cycle::new(0); | ||||
| pub fn save_and_exit(cpu: &Cpu, control_flow: &mut ControlFlow) { | ||||
|     write_save(cpu); | ||||
|     *control_flow = ControlFlow::Exit; | ||||
| } | ||||
|  | ||||
|     if GAMEPAD_ENABLED { | ||||
|         if let Some(event) = gamepad.next_event() { | ||||
|             joypad::handle_gamepad_input(game_boy.joypad_mut(), event); | ||||
| #[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), | ||||
|     } | ||||
|  | ||||
|     joypad::handle_keyboard_input(game_boy.joypad_mut(), input); | ||||
|     while elapsed < target { | ||||
|         elapsed += game_boy.step(); | ||||
|     } | ||||
|  | ||||
|     elapsed | ||||
| } | ||||
|  | ||||
| pub fn run_frame(game_boy: &mut SM83, gamepad: &mut Gilrs, input: &WinitInputHelper) -> Cycle { | ||||
|     let mut elapsed = Cycle::new(0); | ||||
| 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"); | ||||
|  | ||||
|     if GAMEPAD_ENABLED { | ||||
|         if let Some(event) = gamepad.next_event() { | ||||
|             joypad::handle_gamepad_input(game_boy.joypad_mut(), event); | ||||
|             let mut file = File::open(&save_path)?; | ||||
|             let mut memory = Vec::new(); | ||||
|             file.read_to_end(&mut memory)?; | ||||
|  | ||||
|             if ext_ram.len() != memory.len() { | ||||
|                 return Err(SaveError::DiffSize); | ||||
|             } | ||||
|  | ||||
|             ext_ram.copy_from_slice(&memory); | ||||
|             Ok(save_path) | ||||
|         } | ||||
|         None => Err(SaveError::NotApplicable), | ||||
|     } | ||||
|  | ||||
|     joypad::handle_keyboard_input(game_boy.joypad_mut(), input); | ||||
|     while elapsed < CYCLES_IN_FRAME { | ||||
|         elapsed += game_boy.step(); | ||||
|     } | ||||
|  | ||||
|     elapsed | ||||
| } | ||||
|  | ||||
| pub fn draw(ppu: &Ppu, frame: &mut [u8]) { | ||||
|     ppu.copy_to_gui(frame); | ||||
| 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; | ||||
|     } | ||||
| } | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -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 { | ||||
| @@ -110,72 +110,39 @@ impl ButtonEvent { | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub 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? | ||||
|  | ||||
| #[inline] | ||||
| 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::T) { | ||||
|         state.start.update(true, irq); | ||||
|     } | ||||
|     if input.key_released(VirtualKeyCode::T) { | ||||
|         state.start.update(false, irq); | ||||
|     } | ||||
|  | ||||
|     if input.key_pressed(VirtualKeyCode::Y) { | ||||
|         state.select.update(true, irq); | ||||
|     } | ||||
|     if input.key_released(VirtualKeyCode::Y) { | ||||
|         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(_) => {} | ||||
|         }, | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn handle_gamepad_input(pad: &mut Joypad, event: GamepadEvent) { | ||||
| #[inline] | ||||
| pub(crate) fn handle_gamepad_input(pad: &mut Joypad, event: GamepadEvent) { | ||||
|     use Button::*; | ||||
|     use GamepadEventType::*; | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| pub use apu::gen::AudioSPSC; | ||||
| pub use instruction::cycle::Cycle; | ||||
| #![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; | ||||
|  | ||||
| pub const GB_WIDTH: usize = 160; | ||||
| pub const GB_HEIGHT: usize = 144; | ||||
| @@ -9,6 +11,7 @@ mod bus; | ||||
| mod cartridge; | ||||
| mod cpu; | ||||
| pub mod emu; | ||||
| pub mod gui; | ||||
| mod high_ram; | ||||
| mod instruction; | ||||
| mod interrupt; | ||||
|   | ||||
							
								
								
									
										226
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										226
									
								
								src/main.rs
									
									
									
									
									
								
							| @@ -1,160 +1,126 @@ | ||||
| use anyhow::{anyhow, Result}; | ||||
| use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg}; | ||||
| use gb::{AudioSPSC, Cycle, GB_HEIGHT, GB_WIDTH}; | ||||
| use gilrs::Gilrs; | ||||
| use pixels::{PixelsBuilder, SurfaceTexture}; | ||||
| use rodio::{OutputStream, Sink}; | ||||
| use std::time::{Duration, Instant}; | ||||
| use winit::dpi::LogicalSize; | ||||
| use winit::event::{Event, VirtualKeyCode}; | ||||
| use winit::event_loop::{ControlFlow, EventLoop}; | ||||
| use winit::window::{Window, WindowBuilder}; | ||||
| use winit_input_helper::WinitInputHelper; | ||||
| 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 rodio::{OutputStream, Sink}; | ||||
| use tracing_subscriber::EnvFilter; | ||||
| use winit::event::{Event, WindowEvent}; | ||||
| use winit::event_loop::{EventLoop, EventLoopBuilder}; | ||||
|  | ||||
| const WINDOW_SCALE: f64 = 2.0; | ||||
| 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) | ||||
|                 .required(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(); | ||||
|  | ||||
|     let rom_path = m | ||||
|         .value_of("rom") | ||||
|         .expect("Required value 'rom' was provided"); | ||||
|     // Set up subscriber | ||||
|     if std::env::var("RUST_LOG").is_err() { | ||||
|         std::env::set_var("RUST_LOG", "gb=info"); | ||||
|     } | ||||
|  | ||||
|     let mut game_boy = | ||||
|         gb::emu::init(m.value_of("boot"), rom_path).expect("Initialize DMG-01 Emulator"); | ||||
|     let rom_title = gb::emu::rom_title(&game_boy); | ||||
|     tracing_subscriber::fmt::fmt() | ||||
|         .with_env_filter(EnvFilter::from_default_env()) | ||||
|         .init(); | ||||
|  | ||||
|     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()? | ||||
|     // Init CPU | ||||
|     let mut cpu = match m.get_one::<PathBuf>("boot") { | ||||
|         Some(path) => { | ||||
|             tracing::info!("User-provided boot ROM"); | ||||
|             emu::from_boot_rom(path).expect("initialize emulator with custom boot rom") | ||||
|         } | ||||
|         None => { | ||||
|             tracing::info!("Built-in boot ROM"); | ||||
|             Default::default() | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     // 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(&mut cpu, path).expect("read game rom from path"); | ||||
|     } | ||||
|     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 Audio | ||||
|     let (_stream, stream_handle) = OutputStream::try_default().expect("Initialized Audio"); | ||||
|  | ||||
|     if AUDIO_ENABLED { | ||||
|         let spsc: AudioSPSC<f32> = Default::default(); | ||||
|         let (prod, cons) = spsc.init(); | ||||
|         let sink = Sink::try_new(&stream_handle)?; | ||||
|         sink.append(cons); | ||||
|         game_boy.apu_mut().attach_producer(prod); | ||||
|         let (prod, cons) = gb::spsc_init(); | ||||
|         let sink = { | ||||
|             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_audio_prod(&mut cpu, prod); | ||||
|  | ||||
|         tracing::info!("Spawn Audio Thread"); | ||||
|         std::thread::spawn(move || { | ||||
|             sink.sleep_until_end(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     let mut start = Instant::now(); | ||||
|     let frame_time = Duration::from_secs_f64(1.0 / 59.73); // 59.73 Hz on Host | ||||
|     let mut cycle_count: Cycle = Default::default(); | ||||
|     // 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() | ||||
|                 .map_err(|e| anyhow!("pixels.render() failed: {}", e)) | ||||
|                 .is_err() | ||||
|             { | ||||
|                 *control_flow = ControlFlow::Exit; | ||||
|                 return; | ||||
|         app.handle_event(&event); | ||||
|  | ||||
|         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() { | ||||
|                 *control_flow = ControlFlow::Exit; | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if let Some(size) = input.window_resized() { | ||||
|                 pixels.resize_surface(size.width, size.height); | ||||
|             } | ||||
|  | ||||
|             let mut diff = Instant::now() - start; | ||||
|             while diff.subsec_nanos() < frame_time.subsec_nanos() { | ||||
|                 if cycle_count < gb::emu::CYCLES_IN_FRAME { | ||||
|                     cycle_count += gb::emu::run_frame(&mut game_boy, &mut gamepad, &input); | ||||
|                 } | ||||
|  | ||||
|                 diff = Instant::now() - start; | ||||
|             } | ||||
|             start = Instant::now(); | ||||
|  | ||||
|             if cycle_count >= gb::emu::CYCLES_IN_FRAME { | ||||
|                 cycle_count %= gb::emu::CYCLES_IN_FRAME; | ||||
|  | ||||
|                 gb::emu::draw(game_boy.ppu(), pixels.get_frame()); | ||||
|                 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, | ||||
|                 _ => {} | ||||
|             }, | ||||
|             _ => {} | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| #[cfg(not(windows))] | ||||
| fn create_window(event_loop: &EventLoop<()>, title: &str) -> Result<Window> { | ||||
|     let size = LogicalSize::new( | ||||
|         (GB_WIDTH as f64) * WINDOW_SCALE, | ||||
|         (GB_HEIGHT as f64) * WINDOW_SCALE, | ||||
|     ); | ||||
|     Ok(WindowBuilder::new() | ||||
|         .with_title(title) | ||||
|         .with_inner_size(size) | ||||
|         .with_min_inner_size(size) | ||||
|         .with_resizable(true) | ||||
|         .with_decorations(true) | ||||
|         .with_transparent(false) | ||||
|         .build(event_loop)?) | ||||
| } | ||||
|  | ||||
| #[cfg(windows)] | ||||
| fn create_window(event_loop: &EventLoop<()>, title: &str) -> Result<Window> { | ||||
|     use winit::platform::windows::WindowBuilderExtWindows; | ||||
|  | ||||
|     let size = LogicalSize::new( | ||||
|         (GB_WIDTH as f64) * WINDOW_SCALE, | ||||
|         (GB_HEIGHT as f64) * WINDOW_SCALE, | ||||
|     ); | ||||
|     Ok(WindowBuilder::new() | ||||
|         .with_title(title) | ||||
|         .with_inner_size(size) | ||||
|         .with_min_inner_size(size) | ||||
|         .with_resizable(true) | ||||
|         .with_decorations(true) | ||||
|         .with_transparent(false) | ||||
|         .with_drag_and_drop(false) | ||||
|         .build(event_loop)?) | ||||
| } | ||||
|   | ||||
							
								
								
									
										742
									
								
								src/ppu.rs
									
									
									
									
									
								
							
							
						
						
									
										742
									
								
								src/ppu.rs
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,6 +1,6 @@ | ||||
| use crate::instruction::cycle::Cycle; | ||||
| 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 { | ||||
| @@ -56,7 +66,7 @@ impl DirectMemoryAccess { | ||||
|     } | ||||
|  | ||||
|     fn reset(&mut self) { | ||||
|         self.cycle = Cycle::new(0); | ||||
|         self.cycle = 0; | ||||
|         self.state = DmaState::Disabled; | ||||
|         self.start.0 = None; | ||||
|     } | ||||
| @@ -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, | ||||
| @@ -183,7 +153,7 @@ pub enum ObjectSize { | ||||
| } | ||||
|  | ||||
| impl ObjectSize { | ||||
|     pub(crate) fn as_u8(&self) -> u8 { | ||||
|     pub(crate) fn size(&self) -> u8 { | ||||
|         use ObjectSize::*; | ||||
|  | ||||
|         match self { | ||||
| @@ -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,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 { | ||||
|   | ||||
							
								
								
									
										39
									
								
								src/timer.rs
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								src/timer.rs
									
									
									
									
									
								
							| @@ -21,17 +21,6 @@ impl Timer { | ||||
|         use State::*; | ||||
|         use TimerSpeed::*; | ||||
|  | ||||
|         match self.state { | ||||
|             TIMAOverflow(_) | AbortedTIMAOverflow(_) => self.next(), | ||||
|             LoadTMA => { | ||||
|                 self.counter = self.modulo; | ||||
|                 self.interrupt = true; | ||||
|  | ||||
|                 self.next(); | ||||
|             } | ||||
|             Normal => {} | ||||
|         } | ||||
|  | ||||
|         self.divider = self.divider.wrapping_add(1); | ||||
|  | ||||
|         // Get Bit Position | ||||
| @@ -53,6 +42,17 @@ impl Timer { | ||||
|         } | ||||
|  | ||||
|         self.and_result = Some(new_result); | ||||
|  | ||||
|         match self.state { | ||||
|             TIMAOverflow(_) | AbortedTIMAOverflow(_) => self.next(), | ||||
|             LoadTIMA => { | ||||
|                 self.counter = self.modulo; | ||||
|                 self.interrupt = true; | ||||
|  | ||||
|                 self.next(); | ||||
|             } | ||||
|             Normal => {} | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// 0xFF05 | TIMA - Timer Counter | ||||
| @@ -70,7 +70,7 @@ impl Timer { | ||||
|                 self.counter = byte; | ||||
|                 self.state = AbortedTIMAOverflow(step); | ||||
|             } | ||||
|             LoadTMA => {} | ||||
|             LoadTIMA => { /* Ignored */ } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -95,9 +95,8 @@ impl Timer { | ||||
|         use State::*; | ||||
|  | ||||
|         self.state = match self.state { | ||||
|             Normal | LoadTMA => Normal, | ||||
|             AbortedTIMAOverflow(4) => Normal, | ||||
|             TIMAOverflow(4) => LoadTMA, | ||||
|             Normal | LoadTIMA | AbortedTIMAOverflow(3) => Normal, | ||||
|             TIMAOverflow(3) => LoadTIMA, | ||||
|             AbortedTIMAOverflow(step) => AbortedTIMAOverflow(step + 1), | ||||
|             TIMAOverflow(step) => TIMAOverflow(step + 1), | ||||
|         } | ||||
| @@ -107,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, | ||||
| @@ -158,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) | ||||
| @@ -181,5 +174,5 @@ enum State { | ||||
|     TIMAOverflow(u8), | ||||
|     AbortedTIMAOverflow(u8), | ||||
|     Normal, | ||||
|     LoadTMA, | ||||
|     LoadTIMA, | ||||
| } | ||||
|   | ||||
| @@ -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