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": [ |     "recommendations": [ | ||||||
|         "matklad.rust-analyzer", |         "rust-lang.rust-analyzer", | ||||||
|         "tamasfe.even-better-toml", |         "tamasfe.even-better-toml", | ||||||
|         "serayuzgur.crates", |         "serayuzgur.crates", | ||||||
|         "vadimcn.vscode-lldb", |         "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" | name = "gb" | ||||||
| version = "0.1.0" | version = "0.1.0" | ||||||
| authors = ["Rekai Musuka <rekai@musuka.dev>"] | 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 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||||
|  |  | ||||||
| [dependencies] | [dependencies] | ||||||
| anyhow = "^1.0" | anyhow = "1.0" | ||||||
| bitfield = "^0.13" | bitfield = "0.17" | ||||||
| clap = "^2.33" | clap = { version = "4.0", features = ["cargo"] } | ||||||
| gilrs = "^0.8" | gilrs = "0.11" | ||||||
| pixels = "^0.5" | winit = "0.28" | ||||||
| winit = "^0.25" | egui = "0.21" | ||||||
| winit_input_helper = "^0.10" | egui_wgpu_backend = "0.22" | ||||||
| rodio = "^0.14" | egui_winit_platform = "0.18" | ||||||
| rtrb = "^0.1.4" | 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] | [profile.release] | ||||||
| debug = true | debug = true | ||||||
|   | |||||||
							
								
								
									
										47
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,14 +1,43 @@ | |||||||
| # Rekai's Gameboy Emulator | # Rekai's Gameboy Emulator | ||||||
| [](https://ci.paoda.moe/paoda/gb) |  | ||||||
|  |  | ||||||
| ### Status | ### Status | ||||||
| * Passes Blargg's cpu_instrs Test | * From [Blargg Test ROMs](https://github.com/L-P/blargg-test-roms/) | ||||||
| * Renders Background & Window Tiles  |     * [x] cpu_instrs | ||||||
| * Implements a PPU FIFO |     * [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 | ### Compiling | ||||||
| * [gameboy-logs](https://github.com/wheremyfoodat/Gameboy-logs) suggests that there are still some underlying problems with the cpu implementation | This project was last successfully built on [Rust 1.64.0](https://github.com/rust-lang/rust/tree/1.64.0) | ||||||
| * 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.  | 1. `git clone https://github.com/paoda/gb` | ||||||
| * Code cleanup is pending completion of some minimum viable product of the emulator | 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.
										
									
								
							
							
								
								
									
										331
									
								
								src/apu.rs
									
									
									
									
									
								
							
							
						
						
									
										331
									
								
								src/apu.rs
									
									
									
									
									
								
							| @@ -1,3 +1,4 @@ | |||||||
|  | use crate::apu::gen::SAMPLE_RATE; | ||||||
| use crate::bus::BusIo; | use crate::bus::BusIo; | ||||||
| use crate::emu::SM83_CLOCK_SPEED; | use crate::emu::SM83_CLOCK_SPEED; | ||||||
| use gen::SampleProducer; | use gen::SampleProducer; | ||||||
| @@ -5,8 +6,8 @@ use types::ch1::{Sweep, SweepDirection}; | |||||||
| use types::ch3::Volume as Ch3Volume; | use types::ch3::Volume as Ch3Volume; | ||||||
| use types::ch4::{CounterWidth, Frequency as Ch4Frequency, PolynomialCounter}; | use types::ch4::{CounterWidth, Frequency as Ch4Frequency, PolynomialCounter}; | ||||||
| use types::common::{EnvelopeDirection, FrequencyHigh, SoundDuty, VolumeEnvelope}; | use types::common::{EnvelopeDirection, FrequencyHigh, SoundDuty, VolumeEnvelope}; | ||||||
| use types::fs::{FrameSequencer, State as FrameSequencerState}; | use types::fs::{FrameSequencer, State as FSState}; | ||||||
| use types::{ChannelControl, SoundOutput}; | use types::{ChannelControl, NRx4, SoundOutput}; | ||||||
|  |  | ||||||
| pub mod gen; | pub mod gen; | ||||||
| mod types; | mod types; | ||||||
| @@ -26,11 +27,13 @@ pub struct Apu { | |||||||
|     /// Noise |     /// Noise | ||||||
|     ch4: Channel4, |     ch4: Channel4, | ||||||
|  |  | ||||||
|     sequencer: FrameSequencer, |     fs: FrameSequencer, | ||||||
|     div_prev: Option<u16>, |     div_prev: Option<u16>, | ||||||
|  |  | ||||||
|     prod: Option<SampleProducer<f32>>, |     pub(crate) prod: Option<SampleProducer<f32>>, | ||||||
|     sample_counter: u64, |     sample_counter: u64, | ||||||
|  |  | ||||||
|  |     cap: f32, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl BusIo for Apu { | impl BusIo for Apu { | ||||||
| @@ -54,7 +57,7 @@ impl BusIo for Apu { | |||||||
|             0x26 => self.ctrl.status(self), |             0x26 => self.ctrl.status(self), | ||||||
|             0x30..=0x3F => self.ch3.read_byte(addr), |             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 |                 0xFF | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @@ -66,29 +69,26 @@ impl BusIo for Apu { | |||||||
|             0x11 if self.ctrl.enabled => self.ch1.set_duty(byte), |             0x11 if self.ctrl.enabled => self.ch1.set_duty(byte), | ||||||
|             0x12 if self.ctrl.enabled => self.ch1.set_envelope(byte), |             0x12 if self.ctrl.enabled => self.ch1.set_envelope(byte), | ||||||
|             0x13 if self.ctrl.enabled => self.ch1.set_freq_lo(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), |             0x16 if self.ctrl.enabled => self.ch2.set_duty(byte), | ||||||
|             0x17 if self.ctrl.enabled => self.ch2.set_envelope(byte), |             0x17 if self.ctrl.enabled => self.ch2.set_envelope(byte), | ||||||
|             0x18 if self.ctrl.enabled => self.ch2.set_freq_lo(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), |             0x1A if self.ctrl.enabled => self.ch3.set_dac_enabled(byte), | ||||||
|             0x1B if self.ctrl.enabled => self.ch3.set_len(byte), |             0x1B if self.ctrl.enabled => self.ch3.set_len(byte), | ||||||
|             0x1C if self.ctrl.enabled => self.ch3.set_volume(byte), |             0x1C if self.ctrl.enabled => self.ch3.set_volume(byte), | ||||||
|             0x1D if self.ctrl.enabled => self.ch3.set_freq_lo(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), |             0x20 if self.ctrl.enabled => self.ch4.set_len(byte), | ||||||
|             0x21 if self.ctrl.enabled => self.ch4.set_envelope(byte), |             0x21 if self.ctrl.enabled => self.ch4.set_envelope(byte), | ||||||
|             0x22 if self.ctrl.enabled => self.ch4.set_poly(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), |             0x24 if self.ctrl.enabled => self.ctrl.set_channel(byte), | ||||||
|             0x25 if self.ctrl.enabled => self.ctrl.set_output(byte), |             0x25 if self.ctrl.enabled => self.ctrl.set_output(byte), | ||||||
|             0x26 => self.set_status(byte), |             0x26 => self.set_status(byte), | ||||||
|             0x30..=0x3F => self.ch3.write_byte(addr, byte), |             0x30..=0x3F => self.ch3.write_byte(addr, byte), | ||||||
|             _ if !self.ctrl.enabled => {} |             _ if !self.ctrl.enabled => {} | ||||||
|             _ => eprintln!( |             _ => tracing::warn!("Attempted write of {:#04X} to {:#06X}", byte, addr), | ||||||
|                 "Wrote {:#04X} to unused IO register {:#06X} [APU]", |  | ||||||
|                 byte, addr |  | ||||||
|             ), |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -99,9 +99,9 @@ impl Apu { | |||||||
|  |  | ||||||
|         // Frame Sequencer (512Hz) |         // Frame Sequencer (512Hz) | ||||||
|         if self.is_falling_edge(12, div) { |         if self.is_falling_edge(12, div) { | ||||||
|             use FrameSequencerState::*; |             use FSState::*; | ||||||
|  |  | ||||||
|             match self.sequencer.state() { |             match self.fs.state() { | ||||||
|                 Length => self.clock_length(), |                 Length => self.clock_length(), | ||||||
|                 LengthAndSweep => { |                 LengthAndSweep => { | ||||||
|                     self.clock_length(); |                     self.clock_length(); | ||||||
| @@ -111,7 +111,7 @@ impl Apu { | |||||||
|                 Nothing => {} |                 Nothing => {} | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             self.sequencer.next(); |             self.fs.next(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         self.div_prev = Some(div); |         self.div_prev = Some(div); | ||||||
| @@ -124,25 +124,33 @@ impl Apu { | |||||||
|         if self.sample_counter >= SM83_CLOCK_SPEED { |         if self.sample_counter >= SM83_CLOCK_SPEED { | ||||||
|             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() { |                 if prod.available_blocking() { | ||||||
|                     // Sample the APU |                     // 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 (left, right) = self.ctrl.out.ch1(); | ||||||
|                     let ch1_left = if left { self.ch1.amplitude() } else { 0.0 }; |                     let ch1_left = if left { ch1_amplitude } else { 0.0 }; | ||||||
|                     let ch1_right = if right { self.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 (left, right) = self.ctrl.out.ch2(); | ||||||
|                     let ch2_left = if left { self.ch2.amplitude() } else { 0.0 }; |                     let ch2_left = if left { ch2_amplitude } else { 0.0 }; | ||||||
|                     let ch2_right = if right { self.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 (left, right) = self.ctrl.out.ch3(); | ||||||
|                     let ch3_left = if left { self.ch3.amplitude() } else { 0.0 }; |                     let ch3_left = if left { ch3_amplitude } else { 0.0 }; | ||||||
|                     let ch3_right = if right { self.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 (left, right) = self.ctrl.out.ch4(); | ||||||
|                     let ch4_left = if left { self.ch4.amplitude() } else { 0.0 }; |                     let ch4_left = if left { ch4_amplitude } else { 0.0 }; | ||||||
|                     let ch4_right = if right { self.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 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; |                     let right_mixed = (ch1_right + ch2_right + ch3_right + ch4_right) / 4.0; | ||||||
| @@ -164,7 +172,7 @@ impl Apu { | |||||||
|  |  | ||||||
|         if self.ctrl.enabled { |         if self.ctrl.enabled { | ||||||
|             // Frame Sequencer reset to Step 0 |             // Frame Sequencer reset to Step 0 | ||||||
|             self.sequencer.reset(); |             self.fs.reset(); | ||||||
|  |  | ||||||
|             // Square Duty units are reset to first step |             // Square Duty units are reset to first step | ||||||
|             self.ch1.duty_pos = 0; |             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) { |     fn reset(&mut self) { | ||||||
|         self.ch1.sweep = Default::default(); |         self.ch1.sweep = Default::default(); | ||||||
|         self.ch1.duty = Default::default(); |         self.ch1.duty = Default::default(); | ||||||
| @@ -207,8 +211,8 @@ impl Apu { | |||||||
|         self.ch4.poly = Default::default(); |         self.ch4.poly = Default::default(); | ||||||
|         self.ch4.freq = Default::default(); |         self.ch4.freq = Default::default(); | ||||||
|  |  | ||||||
|         self.ctrl.channel = Default::default(); |         self.ctrl.channel = ChannelControl(0); | ||||||
|         self.ctrl.out = Default::default(); |         self.ctrl.out = SoundOutput(0); | ||||||
|  |  | ||||||
|         // Disable the Channels |         // Disable the Channels | ||||||
|         self.ch1.enabled = Default::default(); |         self.ch1.enabled = Default::default(); | ||||||
| @@ -217,25 +221,13 @@ impl Apu { | |||||||
|         self.ch4.enabled = Default::default(); |         self.ch4.enabled = Default::default(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fn process_length(freq_hi: &FrequencyHigh, length_timer: &mut u16, enabled: &mut bool) { |     fn process_length(freq: &impl NRx4, counter: &mut u16, enabled: &mut bool) { | ||||||
|         if freq_hi.length_disable() && *length_timer > 0 { |         if freq.length_enable() && *counter > 0 { | ||||||
|             *length_timer -= 1; |             *counter -= 1; | ||||||
|  |  | ||||||
|             // Check in this scope ensures (only) the above subtraction |             // Check in this scope ensures (only) the above subtraction | ||||||
|             // made length_timer 0 |             // made length_timer 0 | ||||||
|             if *length_timer == 0 { |             if *counter == 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 { |  | ||||||
|                 *enabled = false; |                 *enabled = false; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @@ -244,25 +236,25 @@ impl Apu { | |||||||
|     fn clock_length(&mut self) { |     fn clock_length(&mut self) { | ||||||
|         Self::process_length( |         Self::process_length( | ||||||
|             &self.ch1.freq_hi, |             &self.ch1.freq_hi, | ||||||
|             &mut self.ch1.length_timer, |             &mut self.ch1.length_counter, | ||||||
|             &mut self.ch1.enabled, |             &mut self.ch1.enabled, | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         Self::process_length( |         Self::process_length( | ||||||
|             &self.ch2.freq_hi, |             &self.ch2.freq_hi, | ||||||
|             &mut self.ch2.length_timer, |             &mut self.ch2.length_counter, | ||||||
|             &mut self.ch2.enabled, |             &mut self.ch2.enabled, | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         Self::process_length( |         Self::process_length( | ||||||
|             &self.ch3.freq_hi, |             &self.ch3.freq_hi, | ||||||
|             &mut self.ch3.length_timer, |             &mut self.ch3.length_counter, | ||||||
|             &mut self.ch3.enabled, |             &mut self.ch3.enabled, | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         Self::ch4_process_length( |         Self::process_length( | ||||||
|             &self.ch4.freq, |             &self.ch4.freq, | ||||||
|             &mut self.ch4.length_timer, |             &mut self.ch4.length_counter, | ||||||
|             &mut self.ch4.enabled, |             &mut self.ch4.enabled, | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| @@ -279,7 +271,7 @@ impl Apu { | |||||||
|             if self.ch1.sweep_enabled && period != 0 { |             if self.ch1.sweep_enabled && period != 0 { | ||||||
|                 let new_freq = self.ch1.calc_sweep_freq(); |                 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.set_frequency(new_freq); | ||||||
|                     self.ch1.shadow_freq = new_freq; |                     self.ch1.shadow_freq = new_freq; | ||||||
|  |  | ||||||
| @@ -337,9 +329,22 @@ impl Apu { | |||||||
|             None => false, |             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); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| #[derive(Debug, Default)] |         output | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug)] | ||||||
| pub(crate) struct SoundControl { | pub(crate) struct SoundControl { | ||||||
|     /// 0xFF24 | NR50 - Channel Control |     /// 0xFF24 | NR50 - Channel Control | ||||||
|     channel: ChannelControl, |     channel: ChannelControl, | ||||||
| @@ -349,6 +354,16 @@ pub(crate) struct SoundControl { | |||||||
|     enabled: bool, |     enabled: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl Default for SoundControl { | ||||||
|  |     fn default() -> Self { | ||||||
|  |         Self { | ||||||
|  |             channel: ChannelControl(0), | ||||||
|  |             out: SoundOutput(0), | ||||||
|  |             enabled: Default::default(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| impl SoundControl { | impl SoundControl { | ||||||
|     /// 0xFF24 | NR50 - Channel Control |     /// 0xFF24 | NR50 - Channel Control | ||||||
|     pub(crate) fn channel(&self) -> u8 { |     pub(crate) fn channel(&self) -> u8 { | ||||||
| @@ -408,7 +423,7 @@ pub(crate) struct Channel1 { | |||||||
|     sweep_enabled: bool, |     sweep_enabled: bool, | ||||||
|  |  | ||||||
|     // Length Functionality |     // Length Functionality | ||||||
|     length_timer: u16, |     length_counter: u16, | ||||||
|  |  | ||||||
|     freq_timer: u16, |     freq_timer: u16, | ||||||
|     duty_pos: u8, |     duty_pos: u8, | ||||||
| @@ -435,11 +450,11 @@ impl Channel1 { | |||||||
|     /// 0xFF11 | NR11 - Channel 1 Sound length / Wave pattern duty |     /// 0xFF11 | NR11 - Channel 1 Sound length / Wave pattern duty | ||||||
|     pub(crate) fn set_duty(&mut self, byte: u8) { |     pub(crate) fn set_duty(&mut self, byte: u8) { | ||||||
|         self.duty = byte.into(); |         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 |     /// 0xFF12 | NR12 - Channel 1 Volume Envelope | ||||||
|     pub fn envelope(&self) -> u8 { |     pub(crate) fn envelope(&self) -> u8 { | ||||||
|         u8::from(self.envelope) |         u8::from(self.envelope) | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -463,38 +478,54 @@ impl Channel1 { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /// 0xFF14 | NR14 - Channel 1 Frequency high |     /// 0xFF14 | NR14 - Channel 1 Frequency high | ||||||
|     pub(crate) fn set_freq_hi(&mut self, byte: u8) { |     pub(crate) fn set_freq_hi(&mut self, fs: &FrameSequencer, byte: u8) { | ||||||
|         self.freq_hi = byte.into(); |         let mut new_freq: FrequencyHigh = byte.into(); | ||||||
|  |  | ||||||
|         // If this bit is set, a trigger event occurs |         // If this bit is set, a trigger event occurs | ||||||
|         if self.freq_hi.initial() { |         if new_freq.trigger() { | ||||||
|             // Envelope Behaviour during trigger event |             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(); |             self.period_timer = self.envelope.period(); | ||||||
|  |  | ||||||
|  |             // Channel Volume reloaded | ||||||
|             self.current_volume = self.envelope.init_vol(); |             self.current_volume = self.envelope.init_vol(); | ||||||
|  |  | ||||||
|             // Sweep behaviour during trigger event |             // Channel 1 Sweep Behaviour | ||||||
|             let sweep_period = self.sweep.period(); |             let sweep_period = self.sweep.period(); | ||||||
|             let sweep_shift = self.sweep.shift_count(); |             let sweep_shift = self.sweep.shift_count(); | ||||||
|  |  | ||||||
|             self.shadow_freq = self.frequency(); |             self.shadow_freq = self.frequency(); | ||||||
|             self.sweep_timer = if sweep_period == 0 { 8 } else { sweep_period }; |             self.sweep_timer = if sweep_period == 0 { 8 } else { sweep_period }; | ||||||
|  |  | ||||||
|             if sweep_period != 0 || sweep_shift != 0 { |             self.sweep_enabled = sweep_period != 0 || sweep_shift != 0; | ||||||
|                 self.sweep_enabled = true; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if sweep_shift != 0 { |             if sweep_shift != 0 { | ||||||
|                 let _ = self.calc_sweep_freq(); |                 let _ = self.calc_sweep_freq(); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // Length behaviour during trigger event |             self.enabled = self.is_dac_enabled(); | ||||||
|             if self.length_timer == 0 { |  | ||||||
|                 self.length_timer = 64; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|             if self.is_dac_enabled() { |         obscure::nrx4::length_update( | ||||||
|                 self.enabled = true; |             &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) { |     fn tick(&mut self) { | ||||||
| @@ -556,7 +587,7 @@ impl Channel1 { | |||||||
| pub(crate) struct Channel2 { | pub(crate) struct Channel2 { | ||||||
|     /// 0xFF16 | NR21 - Channel 2 Sound length / Wave Pattern Duty |     /// 0xFF16 | NR21 - Channel 2 Sound length / Wave Pattern Duty | ||||||
|     duty: SoundDuty, |     duty: SoundDuty, | ||||||
|     /// 0xFF17 | NR22 - Channel 2 Volume ENvelope |     /// 0xFF17 | NR22 - Channel 2 Volume Envelope | ||||||
|     envelope: VolumeEnvelope, |     envelope: VolumeEnvelope, | ||||||
|     /// 0xFF18 | NR23 - Channel 2 Frequency low (lower 8 bits only) |     /// 0xFF18 | NR23 - Channel 2 Frequency low (lower 8 bits only) | ||||||
|     freq_lo: u8, |     freq_lo: u8, | ||||||
| @@ -568,7 +599,7 @@ pub(crate) struct Channel2 { | |||||||
|     current_volume: u8, |     current_volume: u8, | ||||||
|  |  | ||||||
|     // Length Functionality |     // Length Functionality | ||||||
|     length_timer: u16, |     length_counter: u16, | ||||||
|  |  | ||||||
|     freq_timer: u16, |     freq_timer: u16, | ||||||
|     duty_pos: u8, |     duty_pos: u8, | ||||||
| @@ -585,15 +616,15 @@ impl Channel2 { | |||||||
|     /// 0xFF16 | NR21 - Channel 2 Sound length / Wave Pattern Duty |     /// 0xFF16 | NR21 - Channel 2 Sound length / Wave Pattern Duty | ||||||
|     pub(crate) fn set_duty(&mut self, byte: u8) { |     pub(crate) fn set_duty(&mut self, byte: u8) { | ||||||
|         self.duty = byte.into(); |         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 { |     pub(crate) fn envelope(&self) -> u8 { | ||||||
|         u8::from(self.envelope) |         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) { |     pub(crate) fn set_envelope(&mut self, byte: u8) { | ||||||
|         self.envelope = byte.into(); |         self.envelope = byte.into(); | ||||||
|  |  | ||||||
| @@ -613,22 +644,36 @@ impl Channel2 { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /// 0xFF19 | NR24 - Channel 2 Frequency high |     /// 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(); |         self.freq_hi = byte.into(); | ||||||
|  |  | ||||||
|         if self.freq_hi.initial() { |         obscure::nrx4::length_update( | ||||||
|             // Envelope behaviour during trigger event |             &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(); |             self.period_timer = self.envelope.period(); | ||||||
|  |  | ||||||
|  |             // reload volume | ||||||
|             self.current_volume = self.envelope.init_vol(); |             self.current_volume = self.envelope.init_vol(); | ||||||
|  |  | ||||||
|             // Length behaviour during trigger event |             self.enabled = self.is_dac_enabled(); | ||||||
|             if self.length_timer == 0 { |  | ||||||
|                 self.length_timer = 64; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if self.is_dac_enabled() { |  | ||||||
|                 self.enabled = true; |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -679,7 +724,7 @@ pub(crate) struct Channel3 { | |||||||
|     wave_ram: [u8; WAVE_PATTERN_RAM_LEN], |     wave_ram: [u8; WAVE_PATTERN_RAM_LEN], | ||||||
|  |  | ||||||
|     // Length Functionality |     // Length Functionality | ||||||
|     length_timer: u16, |     length_counter: u16, | ||||||
|  |  | ||||||
|     freq_timer: u16, |     freq_timer: u16, | ||||||
|     offset: u8, |     offset: u8, | ||||||
| @@ -725,7 +770,7 @@ impl Channel3 { | |||||||
|     /// 0xFF1B | NR31 - Sound Length |     /// 0xFF1B | NR31 - Sound Length | ||||||
|     pub(crate) fn set_len(&mut self, byte: u8) { |     pub(crate) fn set_len(&mut self, byte: u8) { | ||||||
|         self.len = byte; |         self.len = byte; | ||||||
|         self.length_timer = 256 - self.len as u16; |         self.length_counter = 256 - self.len as u16; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /// 0xFF1C | NR32 - Channel 3 Volume |     /// 0xFF1C | NR32 - Channel 3 Volume | ||||||
| @@ -757,18 +802,32 @@ impl Channel3 { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /// 0xFF1E | NR34 - Channel 3 Frequency high |     /// 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(); |         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 |             // Length behaviour during trigger event | ||||||
|             if self.length_timer == 0 { |             if self.length_counter == 0 { | ||||||
|                 self.length_timer = 256; |                 self.length_counter = 256; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             if self.dac_enabled { |             self.freq_timer = (2048 - self.frequency()) * 2; | ||||||
|                 self.enabled = true; |  | ||||||
|             } |             // 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, |     current_volume: u8, | ||||||
|  |  | ||||||
|     // Length Functionality |     // Length Functionality | ||||||
|     length_timer: u16, |     length_counter: u16, | ||||||
|  |  | ||||||
|     /// Linear Feedback Shift Register (15-bit) |     /// Linear Feedback Shift Register (15-bit) | ||||||
|     lf_shift: u16, |     lf_shift: u16, | ||||||
| @@ -841,7 +900,7 @@ impl Channel4 { | |||||||
|     /// 0xFF20 | NR41 - Channel 4 Sound Length |     /// 0xFF20 | NR41 - Channel 4 Sound Length | ||||||
|     pub(crate) fn set_len(&mut self, byte: u8) { |     pub(crate) fn set_len(&mut self, byte: u8) { | ||||||
|         self.len = byte & 0x3F; |         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 |     /// 0xFF21 | NR42 - Channel 4 Volume Envelope | ||||||
| @@ -874,25 +933,37 @@ impl Channel4 { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /// 0xFF23 | NR44 - Channel 4 Counter / Consecutive Selector and Restart |     /// 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(); |         self.freq = byte.into(); | ||||||
|  |  | ||||||
|         if self.freq.initial() { |         obscure::nrx4::length_update( | ||||||
|             // Envelope behaviour during trigger event |             &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(); |             self.period_timer = self.envelope.period(); | ||||||
|  |  | ||||||
|  |             //reload volume | ||||||
|             self.current_volume = self.envelope.init_vol(); |             self.current_volume = self.envelope.init_vol(); | ||||||
|  |  | ||||||
|             // Length behaviour during trigger event |             // LFSR reset | ||||||
|             if self.length_timer == 0 { |  | ||||||
|                 self.length_timer = 64; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // LFSR behaviour during trigger event |  | ||||||
|             self.lf_shift = 0x7FFF; |             self.lf_shift = 0x7FFF; | ||||||
|  |  | ||||||
|             if self.is_dac_enabled() { |             self.enabled = self.is_dac_enabled(); | ||||||
|                 self.enabled = true; |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -937,3 +1008,39 @@ impl Channel4 { | |||||||
|         code << 4 |         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,34 +5,13 @@ pub(crate) const SAMPLE_RATE: u32 = 48000; // Hz | |||||||
| const CHANNEL_COUNT: usize = 2; | const CHANNEL_COUNT: usize = 2; | ||||||
| const BUFFER_CAPACITY: usize = 2048 * CHANNEL_COUNT; // # of samples * the # of channels | const BUFFER_CAPACITY: usize = 2048 * CHANNEL_COUNT; // # of samples * the # of channels | ||||||
|  |  | ||||||
| pub struct AudioSPSC<T> { | pub fn init<T>() -> (SampleProducer<T>, SampleConsumer<T>) { | ||||||
|     inner: RingBuffer<T>, |     let (prod, cons) = RingBuffer::new(BUFFER_CAPACITY); | ||||||
| } |  | ||||||
|  |  | ||||||
| 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 }, |         SampleProducer { inner: prod }, | ||||||
|         SampleConsumer { inner: cons }, |         SampleConsumer { inner: cons }, | ||||||
|     ) |     ) | ||||||
| } | } | ||||||
| } |  | ||||||
|  |  | ||||||
| pub struct SampleProducer<T> { | pub struct SampleProducer<T> { | ||||||
|     inner: Producer<T>, |     inner: Producer<T>, | ||||||
|   | |||||||
| @@ -1,5 +1,11 @@ | |||||||
| use bitfield::bitfield; | 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 { | pub(crate) mod ch1 { | ||||||
|     use super::bitfield; |     use super::bitfield; | ||||||
|  |  | ||||||
| @@ -103,7 +109,7 @@ pub(super) mod ch3 { | |||||||
| } | } | ||||||
|  |  | ||||||
| pub(super) mod ch4 { | pub(super) mod ch4 { | ||||||
|     use super::bitfield; |     use super::{bitfield, NRx4}; | ||||||
|  |  | ||||||
|     bitfield! { |     bitfield! { | ||||||
|         pub struct PolynomialCounter(u8); |         pub struct PolynomialCounter(u8); | ||||||
| @@ -163,7 +169,7 @@ pub(super) mod ch4 { | |||||||
|             match byte & 0x01 { |             match byte & 0x01 { | ||||||
|                 0b00 => Self::Short, |                 0b00 => Self::Short, | ||||||
|                 0b01 => Self::Long, |                 0b01 => Self::Long, | ||||||
|                 _ => unreachable!("{:#04X} is not a valid value for CounterWidth"), |                 _ => unreachable!("{:#04X} is not a valid value for CounterWidth", byte), | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -177,17 +183,21 @@ pub(super) mod ch4 { | |||||||
|     bitfield! { |     bitfield! { | ||||||
|         pub struct Frequency(u8); |         pub struct Frequency(u8); | ||||||
|         impl Debug; |         impl Debug; | ||||||
|         _initial, _: 7; |         _trigger, _: 7; | ||||||
|         _length_disable, _: 6; |         _length_enable, _set_length_enable: 6; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     impl Frequency { |     impl NRx4 for Frequency { | ||||||
|         pub(crate) fn length_disable(&self) -> bool { |         fn trigger(&self) -> bool { | ||||||
|             self._length_disable() |             self._trigger() | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         pub(crate) fn initial(&self) -> bool { |         fn length_enable(&self) -> bool { | ||||||
|             self._initial() |             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 { | pub(super) mod common { | ||||||
|     use super::bitfield; |     use super::{bitfield, NRx4}; | ||||||
|  |  | ||||||
|     bitfield! { |     bitfield! { | ||||||
|         pub struct FrequencyHigh(u8); |         pub struct FrequencyHigh(u8); | ||||||
|         impl Debug; |         impl Debug; | ||||||
|         _initial, _: 7; |         _trigger, _: 7; | ||||||
|         _length_disable, _: 6; |         _length_enable, _set_length_enable: 6; | ||||||
|         pub freq_bits, set_freq_bits: 2, 0; |         pub freq_bits, set_freq_bits: 2, 0; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     impl FrequencyHigh { |     impl NRx4 for FrequencyHigh { | ||||||
|         pub(crate) fn initial(&self) -> bool { |         fn trigger(&self) -> bool { | ||||||
|             self._initial() |             self._trigger() | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         pub(crate) fn length_disable(&self) -> bool { |         fn length_enable(&self) -> bool { | ||||||
|             self._length_disable() |             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 { |     impl From<WavePattern> for u8 { | ||||||
|         fn from(pattern: WavePattern) -> Self { |         fn from(pattern: WavePattern) -> Self { | ||||||
|             pattern as 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 { | impl From<u8> for SoundOutput { | ||||||
|     fn from(byte: u8) -> Self { |     fn from(byte: u8) -> Self { | ||||||
|         Self(byte) |         Self(byte) | ||||||
| @@ -509,12 +511,6 @@ impl Clone for ChannelControl { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for ChannelControl { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self(0) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl From<u8> for ChannelControl { | impl From<u8> for ChannelControl { | ||||||
|     fn from(byte: u8) -> Self { |     fn from(byte: u8) -> Self { | ||||||
|         Self(byte) |         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 { |         pub(crate) fn state(&self) -> 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 { |     pub(crate) enum State { | ||||||
|         Length, |         Length, | ||||||
|         Nothing, |         Nothing, | ||||||
|   | |||||||
							
								
								
									
										102
									
								
								src/bus.rs
									
									
									
									
									
								
							
							
						
						
									
										102
									
								
								src/bus.rs
									
									
									
									
									
								
							| @@ -7,63 +7,30 @@ use crate::ppu::{Ppu, PpuMode}; | |||||||
| use crate::serial::Serial; | use crate::serial::Serial; | ||||||
| use crate::timer::Timer; | use crate::timer::Timer; | ||||||
| use crate::work_ram::{VariableWorkRam, WorkRam}; | 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 { | pub struct Bus { | ||||||
|     boot: Option<[u8; BOOT_ROM_SIZE]>, // Boot ROM is 256b long |     boot: Option<[u8; BOOT_SIZE]>, // Boot ROM is 256b long | ||||||
|     cartridge: Option<Cartridge>, |     pub(crate) cart: Option<Cartridge>, | ||||||
|     pub(crate) ppu: Ppu, |     pub(crate) ppu: Ppu, | ||||||
|     work_ram: WorkRam, |     work_ram: WorkRam, | ||||||
|     var_ram: VariableWorkRam, |     var_ram: VariableWorkRam, | ||||||
|     pub(crate) timer: Timer, |     timer: Timer, | ||||||
|     int: Interrupt, |     int: Interrupt, | ||||||
|     pub(crate) apu: Apu, |     pub(crate) apu: Apu, | ||||||
|     high_ram: HighRam, |     high_ram: HighRam, | ||||||
|     serial: Serial, |     serial: Serial, | ||||||
|     pub(crate) joypad: Joypad, |     pub(crate) joyp: 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(), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Bus { | impl Bus { | ||||||
|     pub(crate) fn with_boot(path: &str) -> anyhow::Result<Self> { |     pub(crate) fn with_boot(rom: [u8; 256]) -> Self { | ||||||
|         let mut file = File::open(path)?; |         Self { | ||||||
|         let mut boot_rom = [0u8; 256]; |             boot: Some(rom), | ||||||
|  |  | ||||||
|         file.read_exact(&mut boot_rom)?; |  | ||||||
|  |  | ||||||
|         Ok(Self { |  | ||||||
|             boot: Some(boot_rom), |  | ||||||
|             ..Default::default() |             ..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)] |     #[allow(dead_code)] | ||||||
| @@ -71,13 +38,19 @@ impl Bus { | |||||||
|         self.boot.is_some() |         self.boot.is_some() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     #[inline] | ||||||
|     pub(crate) fn clock(&mut self) { |     pub(crate) fn clock(&mut self) { | ||||||
|         self.tick(4); |         self.tick(4); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     #[inline] | ||||||
|     fn tick(&mut self, limit: u8) { |     fn tick(&mut self, limit: u8) { | ||||||
|         for _ in 0..limit { |         for _ in 0..limit { | ||||||
|             self.timer.tick(); |             self.timer.tick(); | ||||||
|  |  | ||||||
|  |             if let Some(c) = self.cart.as_mut() { | ||||||
|  |                 c.tick() | ||||||
|  |             } | ||||||
|             self.ppu.tick(); |             self.ppu.tick(); | ||||||
|             self.apu.tick(self.timer.divider); |             self.apu.tick(self.timer.divider); | ||||||
|             self.dma_tick() |             self.dma_tick() | ||||||
| @@ -93,7 +66,7 @@ impl Bus { | |||||||
| } | } | ||||||
|  |  | ||||||
| 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 { |         match addr { | ||||||
|             0x0000..=0x7FFF => { |             0x0000..=0x7FFF => { | ||||||
|                 // 16KB ROM bank 00 (ends at 0x3FFF) |                 // 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), |                     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 |             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 |                 // 8KB External RAM | ||||||
|                 Some(cart) => cart.read_byte(addr), |                 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 |             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 |             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); |         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), |                     Some(cart) => cart.read_byte(addr), | ||||||
|                     None => panic!("Tried to read from a non-existent cartridge"), |                     None => 0xFF, | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             0x8000..=0x9FFF => { |             0x8000..=0x9FFF => { | ||||||
| @@ -169,10 +142,10 @@ impl BusIo for Bus { | |||||||
|                     _ => self.ppu.read_byte(addr), |                     _ => self.ppu.read_byte(addr), | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             0xA000..=0xBFFF => match self.cartridge.as_ref() { |             0xA000..=0xBFFF => match self.cart.as_ref() { | ||||||
|                 // 8KB External RAM |                 // 8KB External RAM | ||||||
|                 Some(cart) => cart.read_byte(addr), |                 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 |             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 |             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 |                 // Every address here starts with 0xFF so we can just check the | ||||||
|                 // low byte to figure out which register it is |                 // low byte to figure out which register it is | ||||||
|                 match addr & 0x00FF { |                 match addr & 0x00FF { | ||||||
|                     0x00 => self.joypad.p1, |                     0x00 => self.joyp.p1, | ||||||
|                     0x01 => self.serial.next, |                     0x01 => self.serial.next, | ||||||
|                     0x02 => self.serial.ctrl.into(), |                     0x02 => self.serial.ctrl.into(), | ||||||
|                     0x04 => (self.timer.divider >> 8) as u8, |                     0x04 => (self.timer.divider >> 8) as u8, | ||||||
| @@ -240,8 +213,9 @@ impl BusIo for Bus { | |||||||
|                     0x49 => self.ppu.monochrome.obj_palette_1.into(), |                     0x49 => self.ppu.monochrome.obj_palette_1.into(), | ||||||
|                     0x4A => self.ppu.pos.window_y, |                     0x4A => self.ppu.pos.window_y, | ||||||
|                     0x4B => self.ppu.pos.window_x, |                     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 |                         0xFF | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
| @@ -262,9 +236,8 @@ impl BusIo for Bus { | |||||||
|             0x0000..=0x7FFF => { |             0x0000..=0x7FFF => { | ||||||
|                 // 16KB ROM bank 00 (ends at 0x3FFF) |                 // 16KB ROM bank 00 (ends at 0x3FFF) | ||||||
|                 // and 16KB ROM Bank 01 -> NN (switchable via MB) |                 // and 16KB ROM Bank 01 -> NN (switchable via MB) | ||||||
|                 match self.cartridge.as_mut() { |                 if let Some(cart) = self.cart.as_mut() { | ||||||
|                     Some(cart) => cart.write_byte(addr, byte), |                     cart.write_byte(addr, byte); | ||||||
|                     None => panic!("Tried to write into non-existent cartridge"), |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             0x8000..=0x9FFF => { |             0x8000..=0x9FFF => { | ||||||
| @@ -276,9 +249,8 @@ impl BusIo for Bus { | |||||||
|             } |             } | ||||||
|             0xA000..=0xBFFF => { |             0xA000..=0xBFFF => { | ||||||
|                 // 8KB External RAM |                 // 8KB External RAM | ||||||
|                 match self.cartridge.as_mut() { |                 if let Some(cart) = self.cart.as_mut() { | ||||||
|                     Some(cart) => cart.write_byte(addr, byte), |                     cart.write_byte(addr, byte); | ||||||
|                     None => panic!("Tried to write into non-existent cartridge"), |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             0xC000..=0xCFFF => self.work_ram.write_byte(addr, byte), // 4KB Work RAM Bank 0 |             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 |                 // Every address here starts with 0xFF so we can just check the | ||||||
|                 // low byte to figure out which register it is |                 // low byte to figure out which register it is | ||||||
|                 match addr & 0x00FF { |                 match addr & 0x00FF { | ||||||
|                     0x00 => self.joypad.update(byte), |                     0x00 => self.joyp.update(byte), | ||||||
|                     0x01 => self.serial.next = byte, |                     0x01 => self.serial.next = byte, | ||||||
|                     0x02 => self.serial.ctrl = byte.into(), |                     0x02 => self.serial.ctrl = byte.into(), | ||||||
|                     0x04 => self.timer.divider = 0x0000, |                     0x04 => self.timer.divider = 0x0000, | ||||||
| @@ -352,13 +324,15 @@ impl BusIo for Bus { | |||||||
|                     0x4A => self.ppu.pos.window_y = byte, |                     0x4A => self.ppu.pos.window_y = byte, | ||||||
|                     0x4B => self.ppu.pos.window_x = byte, |                     0x4B => self.ppu.pos.window_x = byte, | ||||||
|                     0x4D => {} // CGB Specific Register |                     0x4D => {} // CGB Specific Register | ||||||
|  |                     0x4F => {} // CGB VRAM Bank Select | ||||||
|                     0x50 => { |                     0x50 => { | ||||||
|                         // Disable Boot ROM |                         // Disable Boot ROM | ||||||
|                         if byte != 0 { |                         if byte != 0 { | ||||||
|                             self.boot = None; |                             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 => { |             0xFF80..=0xFFFE => { | ||||||
| @@ -380,7 +354,7 @@ impl Bus { | |||||||
|         let lcd_stat = self.ppu.int.lcd_stat(); |         let lcd_stat = self.ppu.int.lcd_stat(); | ||||||
|  |  | ||||||
|         // Read the current interrupt information from the Joypad |         // 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 |         // Read the current interrupt information from the Timer | ||||||
|         let timer = self.timer.interrupt(); |         let timer = self.timer.interrupt(); | ||||||
| @@ -410,7 +384,7 @@ impl Bus { | |||||||
|         self.ppu.int.set_lcd_stat(lcd_stat); |         self.ppu.int.set_lcd_stat(lcd_stat); | ||||||
|  |  | ||||||
|         // Update the Joypad's instance of the following interrupts |         // 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 |         // Update the Timer's instance of the following interrupts | ||||||
|         self.timer.set_interrupt(timer); |         self.timer.set_interrupt(timer); | ||||||
|   | |||||||
							
								
								
									
										768
									
								
								src/cartridge.rs
									
									
									
									
									
								
							
							
						
						
									
										768
									
								
								src/cartridge.rs
									
									
									
									
									
								
							| @@ -1,105 +1,131 @@ | |||||||
| use std::fs::File; | use bitfield::bitfield; | ||||||
| use std::io::{self, Read}; |  | ||||||
| use std::path::Path; |  | ||||||
|  |  | ||||||
| use crate::bus::BusIo; | use crate::bus::BusIo; | ||||||
|  | use crate::emu::SM83_CLOCK_SPEED; | ||||||
|  | use crate::Cycle; | ||||||
|  |  | ||||||
| const RAM_SIZE_ADDRESS: usize = 0x0149; | const RAM_SIZE_ADDRESS: usize = 0x0149; | ||||||
| const ROM_SIZE_ADDRESS: usize = 0x0148; | const ROM_SIZE_ADDRESS: usize = 0x0148; | ||||||
| const MBC_TYPE_ADDRESS: usize = 0x0147; | const MBC_KIND_ADDRESS: usize = 0x0147; | ||||||
| const ROM_TITLE_RANGE: std::ops::RangeInclusive<usize> = 0x0134..=0x0143; | const ROM_TITLE_START: usize = 0x134; | ||||||
|  | const ROM_TITLE_MAX_SIZE: usize = 16; | ||||||
|  | const ROM_MANUFACTURER_START: usize = 0x13F; | ||||||
|  |  | ||||||
| #[derive(Debug, Default)] | #[derive(Debug)] | ||||||
| pub(crate) struct Cartridge { | pub(crate) struct Cartridge { | ||||||
|     memory: Vec<u8>, |     mem: Vec<u8>, | ||||||
|     title: Option<String>, |     pub(crate) title: Option<String>, | ||||||
|     mbc: Box<dyn MBCIo>, |     mbc: Box<dyn MbcIo>, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Cartridge { | impl Cartridge { | ||||||
|     pub(crate) fn new<P: AsRef<Path> + ?Sized>(path: &P) -> io::Result<Self> { |     pub(crate) fn new(mem: Vec<u8>) -> Self { | ||||||
|         let mut memory = vec![]; |         let title_mem: &[u8; 16] = mem[ROM_TITLE_START..(ROM_TITLE_START + ROM_TITLE_MAX_SIZE)] | ||||||
|         let mut rom = File::open(path)?; |             .try_into() | ||||||
|         rom.read_to_end(&mut memory)?; |             .expect("coerce slice containing cartridge title from ROM to [u8; 16]"); | ||||||
|  |  | ||||||
|         let title = Self::find_title(&memory); |         let title = Self::detect_title(title_mem); | ||||||
|         eprintln!("Cartridge Title: {:?}", title); |         let mbc = Self::detect_mbc(&mem); | ||||||
|  |         tracing::info!("Title: {:?}", title); | ||||||
|  |  | ||||||
|         Ok(Self { |         Self { mem, title, mbc } | ||||||
|             mbc: Self::detect_mbc(&memory), |  | ||||||
|             title, |  | ||||||
|             memory, |  | ||||||
|         }) |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fn detect_mbc(memory: &[u8]) -> Box<dyn MBCIo> { |     pub(crate) fn ext_ram(&self) -> Option<&[u8]> { | ||||||
|         let ram_size = Self::detect_ram_info(memory); |         self.mbc.ext_ram() | ||||||
|         let rom_size = Self::detect_rom_info(memory); |     } | ||||||
|         let mbc_kind = Self::find_mbc(memory); |  | ||||||
|  |     pub(crate) fn ext_ram_mut(&mut self) -> Option<&mut [u8]> { | ||||||
|  |         self.mbc.ext_ram_mut() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     #[inline] | ||||||
|  |     pub(crate) fn tick(&mut self) { | ||||||
|  |         self.mbc.tick() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn detect_mbc(mem: &[u8]) -> Box<dyn MbcIo> { | ||||||
|  |         let ram_size: RamSize = mem[RAM_SIZE_ADDRESS].into(); | ||||||
|  |         let rom_size: RomSize = mem[ROM_SIZE_ADDRESS].into(); | ||||||
|  |         let mbc_kind = Self::detect_mbc_kind(mem[MBC_KIND_ADDRESS]); | ||||||
|         let ram_cap = ram_size.capacity(); |         let ram_cap = ram_size.capacity(); | ||||||
|         let rom_cap = rom_size.capacity(); |         let rom_cap = rom_size.capacity(); | ||||||
|  |  | ||||||
|         eprintln!("Cartridge Ram Size: {} bytes", ram_cap); |         tracing::info!("RAM size: {} bytes", ram_cap); | ||||||
|         eprintln!("Cartridge ROM Size: {} bytes", rom_size.capacity()); |         tracing::info!("ROM size: {} bytes", rom_cap); | ||||||
|         eprintln!("MBC Type: {:?}", mbc_kind); |         tracing::info!("MBC kind: {:?}", mbc_kind); | ||||||
|  |  | ||||||
|         match mbc_kind { |         match mbc_kind { | ||||||
|             MBCKind::None => Box::new(NoMBC), |             MbcKind::None => Box::new(NoMbc), | ||||||
|             MBCKind::MBC1 => Box::new(MBC1::new(ram_size, rom_size)), |             MbcKind::Mbc1(hw) => Box::new(Mbc1::new(hw, ram_size, rom_size)), | ||||||
|             MBCKind::MBC1WithBattery => Box::new(MBC1::new(ram_size, rom_size)), // TODO: Implement Saving |             MbcKind::Mbc2(hw) => Box::new(Mbc2::new(hw, rom_cap)), | ||||||
|             MBCKind::MBC3 => Box::new(MBC3::new(ram_cap)), |             MbcKind::Mbc3(hw @ Mbc3Hardware::Rtc) => Box::new(Mbc3::new(hw, ram_cap)), | ||||||
|             MBCKind::MBC3WithBattery => Box::new(MBC3::new(ram_cap)), // TODO: Implement Saving |             MbcKind::Mbc3(hw @ Mbc3Hardware::RtcBatteryRam) => Box::new(Mbc3::new(hw, ram_cap)), | ||||||
|             MBCKind::MBC5 => Box::new(MBC5::new(ram_cap, rom_cap)), |             MbcKind::Mbc3(hw @ Mbc3Hardware::BatteryRam) => Box::new(Mbc3::new(hw, ram_cap)), | ||||||
|             MBCKind::MBC5WithBattery => Box::new(MBC5::new(ram_cap, rom_cap)), // TDO: Implement Saving |             MbcKind::Mbc5(hw @ Mbc5Hardware::None) => Box::new(Mbc5::new(hw, ram_cap, rom_cap)), | ||||||
|  |             MbcKind::Mbc5(hw @ Mbc5Hardware::BatteryRam) => { | ||||||
|  |                 Box::new(Mbc5::new(hw, ram_cap, rom_cap)) | ||||||
|  |             } | ||||||
|  |             kind => todo!("ROMS with {:?} are currently unsupported", kind), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fn find_title(memory: &[u8]) -> Option<String> { |     fn detect_title(title_mem: &[u8; ROM_TITLE_MAX_SIZE]) -> Option<String> { | ||||||
|         let slice = &memory[ROM_TITLE_RANGE]; |         use std::str::from_utf8; | ||||||
|         let with_nulls = std::str::from_utf8(slice).ok(); |  | ||||||
|         let trimmed = with_nulls.map(|s| s.trim_matches('\0').trim()); |  | ||||||
|  |  | ||||||
|         match trimmed { |         const ALT_TITLE_LEN: usize = ROM_MANUFACTURER_START - ROM_TITLE_START; | ||||||
|  |  | ||||||
|  |         // ascii byte slie does not have a null-terminator | ||||||
|  |         let ascii = match title_mem.iter().position(|b| *b == 0x00) { | ||||||
|  |             Some(end) => &title_mem[..end], | ||||||
|  |             None => &title_mem[..ROM_TITLE_MAX_SIZE], | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         match from_utf8(ascii).map(str::trim).ok() { | ||||||
|  |             None => match from_utf8(&title_mem[..ALT_TITLE_LEN]).map(str::trim).ok() { | ||||||
|                 Some("") | None => None, |                 Some("") | None => None, | ||||||
|             Some(_) => trimmed.map(String::from), |                 Some(title) => Some(String::from(title)), | ||||||
|  |             }, | ||||||
|  |             Some("") => None, | ||||||
|  |             Some(title) => Some(String::from(title)), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub(crate) fn title(&self) -> Option<&str> { |     fn detect_mbc_kind(id: u8) -> MbcKind { | ||||||
|         self.title.as_deref() |         use MbcKind::*; | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fn detect_ram_info(memory: &[u8]) -> RamSize { |         match id { | ||||||
|         let id = memory[RAM_SIZE_ADDRESS]; |             0x00 => None, | ||||||
|         id.into() |             0x01 => Mbc1(Mbc1Hardware::None), | ||||||
|     } |             0x02 => Mbc1(Mbc1Hardware::Ram), | ||||||
|  |             0x03 => Mbc1(Mbc1Hardware::BatteryRam), | ||||||
|     fn detect_rom_info(memory: &[u8]) -> RomSize { |             0x05 => Mbc2(Mbc2Hardware::None), | ||||||
|         let id = memory[ROM_SIZE_ADDRESS]; |             0x06 => Mbc2(Mbc2Hardware::BatteryRam), | ||||||
|         id.into() |             0x08 | 0x09 => unimplemented!("NoMBC + RAM and NoMBC + Battery unsupported"), | ||||||
|     } |             0x0B | 0x0C | 0x0D => unimplemented!("MM01 unsupported"), | ||||||
|  |             0x0F => Mbc3(Mbc3Hardware::Rtc), | ||||||
|     fn find_mbc(memory: &[u8]) -> MBCKind { |             0x10 => Mbc3(Mbc3Hardware::RtcBatteryRam), | ||||||
|         match memory[MBC_TYPE_ADDRESS] { |             0x11 => Mbc3(Mbc3Hardware::None), | ||||||
|             0x00 => MBCKind::None, |             0x12 => Mbc3(Mbc3Hardware::Ram), | ||||||
|             0x01 | 0x02 => MBCKind::MBC1, |             0x13 => Mbc3(Mbc3Hardware::BatteryRam), | ||||||
|             0x03 => MBCKind::MBC1WithBattery, |             0x19 => Mbc5(Mbc5Hardware::None), | ||||||
|             0x19 | 0x1A => MBCKind::MBC5, |             0x1A => Mbc5(Mbc5Hardware::Ram), | ||||||
|             0x1B => MBCKind::MBC5WithBattery, |             0x1B => Mbc5(Mbc5Hardware::BatteryRam), | ||||||
|             0x13 => MBCKind::MBC3WithBattery, |             0x1C => Mbc5(Mbc5Hardware::Rumble), | ||||||
|             0x11 | 0x12 => MBCKind::MBC3, |             0x1D => Mbc5(Mbc5Hardware::RumbleRam), | ||||||
|             id => unimplemented!("id {:#04X} is an unsupported MBC", id), |             0x1E => Mbc5(Mbc5Hardware::RumbleBatteryRam), | ||||||
|  |             id => unimplemented!("MBC with code {:#04X} is unsupported", id), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl BusIo for Cartridge { | impl BusIo for Cartridge { | ||||||
|     fn read_byte(&self, addr: u16) -> u8 { |     fn read_byte(&self, addr: u16) -> u8 { | ||||||
|         use MBCResult::*; |         use MbcResult::*; | ||||||
|  |  | ||||||
|         match self.mbc.handle_read(addr) { |         match self.mbc.handle_read(addr) { | ||||||
|             Address(addr) => self.memory[addr], |             Addr(addr) => self.mem[addr], | ||||||
|             Value(byte) => byte, |             Byte(byte) => byte, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -109,28 +135,31 @@ impl BusIo for Cartridge { | |||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| struct MBC1 { | struct Mbc1 { | ||||||
|     /// 5-bit number |     /// 5-bit number | ||||||
|     rom_bank: u8, |     rom_bank: u8, | ||||||
|     /// 2-bit number |     /// 2-bit number | ||||||
|     ram_bank: u8, |     ram_bank: u8, | ||||||
|     mode: bool, |     mode: bool, | ||||||
|     ram_size: RamSize, |     ram_size: RamSize, | ||||||
|     memory: Vec<u8>, |     mem: Vec<u8>, | ||||||
|     rom_size: RomSize, |     rom_size: RomSize, | ||||||
|     mem_enabled: bool, |     mem_enabled: bool, | ||||||
|  |  | ||||||
|  |     hw: Mbc1Hardware, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl MBC1 { | impl Mbc1 { | ||||||
|     fn new(ram_size: RamSize, rom_size: RomSize) -> Self { |     fn new(hw: Mbc1Hardware, ram_size: RamSize, rom_size: RomSize) -> Self { | ||||||
|         Self { |         Self { | ||||||
|             rom_bank: 0x01, |             rom_bank: 0x01, | ||||||
|             memory: vec![0; ram_size.capacity() as usize], |             mem: vec![0; ram_size.capacity() as usize], | ||||||
|             ram_size, |             ram_size, | ||||||
|             rom_size, |             rom_size, | ||||||
|             ram_bank: Default::default(), |             ram_bank: Default::default(), | ||||||
|             mode: Default::default(), |             mode: Default::default(), | ||||||
|             mem_enabled: Default::default(), |             mem_enabled: Default::default(), | ||||||
|  |             hw, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -182,16 +211,16 @@ impl MBC1 { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fn ram_addr(&self, addr: u16) -> u16 { |     fn ram_addr(&self, addr: u16) -> usize { | ||||||
|         use RamSize::*; |         use RamSize::*; | ||||||
|  |  | ||||||
|         match self.ram_size { |         match self.ram_size { | ||||||
|             Unused | One => (addr - 0xA000) % self.ram_size.capacity() as u16, |             Unused | One => (addr as usize - 0xA000) % self.ram_size.capacity(), | ||||||
|             Four => { |             Four => { | ||||||
|                 if self.mode { |                 if self.mode { | ||||||
|                     0x2000 * self.ram_bank as u16 + (addr - 0xA000) |                     0x2000 * self.ram_bank as usize + (addr as usize - 0xA000) | ||||||
|                 } else { |                 } else { | ||||||
|                     addr - 0xA000 |                     addr as usize - 0xA000 | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             _ => unreachable!("RAM size can not be greater than 32KB on MBC1"), |             _ => unreachable!("RAM size can not be greater than 32KB on MBC1"), | ||||||
| @@ -199,28 +228,34 @@ impl MBC1 { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl MBCIo for MBC1 { | impl Savable for Mbc1 { | ||||||
|     fn handle_read(&self, addr: u16) -> MBCResult { |     fn ext_ram(&self) -> Option<&[u8]> { | ||||||
|         use MBCResult::*; |         match self.hw { | ||||||
|  |             Mbc1Hardware::BatteryRam => Some(&self.mem), | ||||||
|  |             _ => None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn ext_ram_mut(&mut self) -> Option<&mut [u8]> { | ||||||
|  |         match self.hw { | ||||||
|  |             Mbc1Hardware::BatteryRam => Some(&mut self.mem), | ||||||
|  |             _ => None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl MbcIo for Mbc1 { | ||||||
|  |     fn handle_read(&self, addr: u16) -> MbcResult { | ||||||
|  |         use MbcResult::*; | ||||||
|  |  | ||||||
|         match addr { |         match addr { | ||||||
|             0x0000..=0x3FFF => { |             0x0000..=0x3FFF if self.mode => { | ||||||
|                 if self.mode { |                 Addr(0x4000 * self.zero_bank() as usize + addr as usize) | ||||||
|                     Address(0x4000 * self.zero_bank() as usize + addr as usize) |  | ||||||
|                 } else { |  | ||||||
|                     Address(addr as usize) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             0x4000..=0x7FFF => { |  | ||||||
|                 Address(0x4000 * self.high_bank() as usize + (addr as usize - 0x4000)) |  | ||||||
|             } |  | ||||||
|             0xA000..=0xBFFF => { |  | ||||||
|                 if self.mem_enabled { |  | ||||||
|                     Value(self.memory[self.ram_addr(addr) as usize]) |  | ||||||
|                 } else { |  | ||||||
|                     Value(0xFF) |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|  |             0x0000..=0x3FFF => Addr(addr as usize), | ||||||
|  |             0x4000..=0x7FFF => Addr(0x4000 * self.high_bank() as usize + (addr as usize - 0x4000)), | ||||||
|  |             0xA000..=0xBFFF if self.mem_enabled => Byte(self.mem[self.ram_addr(addr)]), | ||||||
|  |             0xA000..=0xBFFF => Byte(0xFF), | ||||||
|             _ => unreachable!("A read from {:#06X} should not be handled by MBC1", addr), |             _ => unreachable!("A read from {:#06X} should not be handled by MBC1", addr), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -229,19 +264,15 @@ impl MBCIo for MBC1 { | |||||||
|         match addr { |         match addr { | ||||||
|             0x0000..=0x1FFF => self.mem_enabled = (byte & 0x0F) == 0x0A, |             0x0000..=0x1FFF => self.mem_enabled = (byte & 0x0F) == 0x0A, | ||||||
|             0x2000..=0x3FFF => { |             0x2000..=0x3FFF => { | ||||||
|                 self.rom_bank = if byte == 0x00 { |                 let value = byte & 0x1F; | ||||||
|                     0x01 |                 let masked_value = byte & self.rom_size_mask(); | ||||||
|                 } else { |                 self.rom_bank = if value == 0 { 0x01 } else { masked_value }; | ||||||
|                     byte & self.rom_size_mask() |  | ||||||
|                 }; |  | ||||||
|  |  | ||||||
|                 self.rom_bank &= 0x1F; |  | ||||||
|             } |             } | ||||||
|             0x4000..=0x5FFF => self.ram_bank = byte & 0x03, |             0x4000..=0x5FFF => self.ram_bank = byte & 0x03, | ||||||
|             0x6000..=0x7FFF => self.mode = (byte & 0x01) == 0x01, |             0x6000..=0x7FFF => self.mode = (byte & 0x01) == 0x01, | ||||||
|             0xA000..=0xBFFF if self.mem_enabled => { |             0xA000..=0xBFFF if self.mem_enabled => { | ||||||
|                 let ram_addr = self.ram_addr(addr) as usize; |                 let ram_addr = self.ram_addr(addr); | ||||||
|                 self.memory[ram_addr] = byte; |                 self.mem[ram_addr] = byte; | ||||||
|             } |             } | ||||||
|             0xA000..=0xBFFF => {} // Ram isn't enabled, ignored write |             0xA000..=0xBFFF => {} // Ram isn't enabled, ignored write | ||||||
|             _ => unreachable!("A write to {:#06X} should not be handled by MBC1", addr), |             _ => unreachable!("A write to {:#06X} should not be handled by MBC1", addr), | ||||||
| @@ -249,55 +280,198 @@ impl MBCIo for MBC1 { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl RtcTick for Mbc1 { | ||||||
|  |     fn tick(&mut self) {} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Default, Clone, Copy)] | ||||||
|  | struct RtClock { | ||||||
|  |     /// 6-bit unsigned integer | ||||||
|  |     sec: u8, | ||||||
|  |     /// 6-bit unsigned integer | ||||||
|  |     min: u8, | ||||||
|  |     /// 6-bit unsigned integer | ||||||
|  |     hr: u8, | ||||||
|  |     day_low: u8, | ||||||
|  |     day_high: DayHigh, | ||||||
|  |  | ||||||
|  |     cycles: Cycle, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl RtClock { | ||||||
|  |     fn inc_day(&mut self) { | ||||||
|  |         // TODO: Figure out order of operations, the brackets are a bit too defensive here | ||||||
|  |         let days: u16 = (((self.day_high.ninth() as u16) << 8) | self.day_low as u16) + 1; | ||||||
|  |  | ||||||
|  |         if days > 0x1FF { | ||||||
|  |             self.day_high.set_carry(true); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         self.day_high.set_ninth(((days >> 8) & 0x01) == 0x01); | ||||||
|  |         self.day_low = days as u8; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl RtcTick for RtClock { | ||||||
|  |     fn tick(&mut self) { | ||||||
|  |         // This is the sort of situation where you'd want to use a scheduler. | ||||||
|  |         if self.day_high.halt() { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         self.cycles += 1; | ||||||
|  |  | ||||||
|  |         if self.cycles >= SM83_CLOCK_SPEED { | ||||||
|  |             self.cycles %= SM83_CLOCK_SPEED; | ||||||
|  |  | ||||||
|  |             self.sec += 1; | ||||||
|  |  | ||||||
|  |             if self.sec == 60 { | ||||||
|  |                 self.sec = 0; | ||||||
|  |                 self.min += 1; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if self.min == 60 { | ||||||
|  |                 self.min = 0; | ||||||
|  |                 self.hr += 1; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if self.hr == 24 { | ||||||
|  |                 self.hr = 0; | ||||||
|  |                 self.inc_day(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | trait RtcTick { | ||||||
|  |     fn tick(&mut self); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bitfield! { | ||||||
|  |     struct DayHigh(u8); | ||||||
|  |     impl Debug; | ||||||
|  |     _, set_carry: 7; | ||||||
|  |     halt, _: 6; | ||||||
|  |     ninth, set_ninth: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Copy for DayHigh {} | ||||||
|  | impl Clone for DayHigh { | ||||||
|  |     fn clone(&self) -> Self { | ||||||
|  |         *self | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Default for DayHigh { | ||||||
|  |     fn default() -> Self { | ||||||
|  |         Self(0) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl From<u8> for DayHigh { | ||||||
|  |     fn from(byte: u8) -> Self { | ||||||
|  |         Self(byte) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl From<DayHigh> for u8 { | ||||||
|  |     fn from(dh: DayHigh) -> Self { | ||||||
|  |         dh.0 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Copy)] | #[derive(Debug, Clone, Copy)] | ||||||
| enum MBC3Device { | enum Mbc3Device { | ||||||
|     ExternalRam, |     Ram, | ||||||
|     RealTimeClock, |     Rtc(RtcRegister), | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Copy)] | ||||||
|  | enum RtcRegister { | ||||||
|  |     Sec, | ||||||
|  |     Min, | ||||||
|  |     Hr, | ||||||
|  |     DayLow, | ||||||
|  |     DayHigh, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| struct MBC3 { | struct Mbc3 { | ||||||
|     /// 7-bit Number |     /// 7-bit Number | ||||||
|     rom_bank: u8, |     rom_bank: u8, | ||||||
|     /// 2-bit Number |     /// 2-bit Number | ||||||
|     ram_bank: u8, |     ram_bank: u8, | ||||||
|  |  | ||||||
|     devs_enabled: bool, |     devs_enabled: bool, | ||||||
|     mapped: Option<MBC3Device>, |     mapped: Option<Mbc3Device>, | ||||||
|     memory: Vec<u8>, |     mem: Vec<u8>, | ||||||
|  |  | ||||||
|     // RTC Data Latch Previous Write |     // RTC Data Latch Previous Write | ||||||
|     prev_latch_write: Option<u8>, |     prev_latch_write: Option<u8>, | ||||||
|  |  | ||||||
|  |     hw: Mbc3Hardware, | ||||||
|  |     rtc: RtClock, | ||||||
|  |     rtc_latch: Option<RtClock>, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl MBC3 { | impl Mbc3 { | ||||||
|     fn new(ram_cap: usize) -> Self { |     fn new(hw: Mbc3Hardware, ram_cap: usize) -> Self { | ||||||
|         Self { |         Self { | ||||||
|             memory: vec![0; ram_cap], |             mem: vec![0; ram_cap], | ||||||
|             rom_bank: Default::default(), |             rom_bank: Default::default(), | ||||||
|             ram_bank: Default::default(), |             ram_bank: Default::default(), | ||||||
|             devs_enabled: Default::default(), |             devs_enabled: Default::default(), | ||||||
|             mapped: Default::default(), |             mapped: Default::default(), | ||||||
|             prev_latch_write: Default::default(), |             prev_latch_write: Default::default(), | ||||||
|  |             rtc: Default::default(), | ||||||
|  |             rtc_latch: Default::default(), | ||||||
|  |             hw, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl MBCIo for MBC3 { | impl Savable for Mbc3 { | ||||||
|     fn handle_read(&self, addr: u16) -> MBCResult { |     fn ext_ram(&self) -> Option<&[u8]> { | ||||||
|         use MBCResult::*; |         match self.hw { | ||||||
|  |             Mbc3Hardware::BatteryRam | Mbc3Hardware::RtcBatteryRam => Some(&self.mem), | ||||||
|  |             _ => None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn ext_ram_mut(&mut self) -> Option<&mut [u8]> { | ||||||
|  |         match self.hw { | ||||||
|  |             Mbc3Hardware::BatteryRam | Mbc3Hardware::RtcBatteryRam => Some(&mut self.mem), | ||||||
|  |             _ => None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl MbcIo for Mbc3 { | ||||||
|  |     fn handle_read(&self, addr: u16) -> MbcResult { | ||||||
|  |         use MbcResult::*; | ||||||
|  |         use RtcRegister::*; | ||||||
|  |  | ||||||
|         let res = match addr { |         let res = match addr { | ||||||
|             0x0000..=0x3FFF => Address(addr as usize), |             0x0000..=0x3FFF => Addr(addr as usize), | ||||||
|             0x4000..=0x7FFF => Address(0x4000 * self.rom_bank as usize + (addr as usize - 0x4000)), |             0x4000..=0x7FFF => Addr(0x4000 * self.rom_bank as usize + (addr as usize - 0x4000)), | ||||||
|             0xA000..=0xBFFF => match self.mapped { |             0xA000..=0xBFFF => match self.mapped { | ||||||
|                 Some(MBC3Device::ExternalRam) if self.devs_enabled => { |                 Some(Mbc3Device::Ram) if self.devs_enabled => { | ||||||
|                     Value(self.memory[0x2000 * self.ram_bank as usize + (addr as usize - 0xA000)]) |                     Byte(self.mem[0x2000 * self.ram_bank as usize + (addr as usize - 0xA000)]) | ||||||
|                 } |                 } | ||||||
|                 Some(MBC3Device::RealTimeClock) if self.devs_enabled => { |                 Some(Mbc3Device::Rtc(reg)) if self.devs_enabled => Byte( | ||||||
|                     todo!("Return Latched value of register") |                     self.rtc_latch | ||||||
|                 } |                         .as_ref() | ||||||
|                 _ => Value(0xFF), |                         .map(|rtc| match reg { | ||||||
|  |                             Sec => rtc.sec, | ||||||
|  |                             Min => rtc.min, | ||||||
|  |                             Hr => rtc.hr, | ||||||
|  |                             DayLow => rtc.day_low, | ||||||
|  |                             DayHigh => rtc.day_high.into(), | ||||||
|  |                         }) | ||||||
|  |                         .unwrap_or(0xFF), | ||||||
|  |                 ), | ||||||
|  |                 _ => Byte(0xFF), | ||||||
|             }, |             }, | ||||||
|             _ => unreachable!("A read from {:#06X} should not be handled by MBC3", addr), |             _ => unreachable!("A read from {:#06X} should not be handled by MBC3", addr), | ||||||
|         }; |         }; | ||||||
| @@ -306,6 +480,8 @@ impl MBCIo for MBC3 { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     fn handle_write(&mut self, addr: u16, byte: u8) { |     fn handle_write(&mut self, addr: u16, byte: u8) { | ||||||
|  |         use RtcRegister::*; | ||||||
|  |  | ||||||
|         match addr { |         match addr { | ||||||
|             0x000..=0x1FFF => self.devs_enabled = (byte & 0x0F) == 0x0A, // Enable External RAM and Access to RTC if there is one |             0x000..=0x1FFF => self.devs_enabled = (byte & 0x0F) == 0x0A, // Enable External RAM and Access to RTC if there is one | ||||||
|             0x2000..=0x3FFF => { |             0x2000..=0x3FFF => { | ||||||
| @@ -317,28 +493,39 @@ impl MBCIo for MBC3 { | |||||||
|             0x4000..=0x5FFF => match byte { |             0x4000..=0x5FFF => match byte { | ||||||
|                 0x00 | 0x01 | 0x02 | 0x03 => { |                 0x00 | 0x01 | 0x02 | 0x03 => { | ||||||
|                     self.ram_bank = byte & 0x03; |                     self.ram_bank = byte & 0x03; | ||||||
|                     self.mapped = Some(MBC3Device::ExternalRam); |                     self.mapped = Some(Mbc3Device::Ram); | ||||||
|                 } |  | ||||||
|                 0x08 | 0x09 | 0x0A | 0x0B | 0x0C => { |  | ||||||
|                     self.mapped = Some(MBC3Device::RealTimeClock); |  | ||||||
|                 } |                 } | ||||||
|  |                 0x08 => self.mapped = Some(Mbc3Device::Rtc(Sec)), | ||||||
|  |                 0x09 => self.mapped = Some(Mbc3Device::Rtc(Min)), | ||||||
|  |                 0x0A => self.mapped = Some(Mbc3Device::Rtc(Hr)), | ||||||
|  |                 0x0B => self.mapped = Some(Mbc3Device::Rtc(DayLow)), | ||||||
|  |                 0x0C => self.mapped = Some(Mbc3Device::Rtc(DayHigh)), | ||||||
|  |  | ||||||
|                 _ => {} |                 _ => {} | ||||||
|             }, |             }, | ||||||
|             0x6000..=0x7FFF => { |             0x6000..=0x7FFF => { | ||||||
|                 if let Some(0x00) = self.prev_latch_write { |                 if let Some(0x00) = self.prev_latch_write { | ||||||
|                     if byte == 0x01 { |                     if byte == 0x01 { | ||||||
|                         todo!("Perform Data Latch") |                         self.rtc_latch = Some(self.rtc); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 self.prev_latch_write = Some(byte); |                 self.prev_latch_write = Some(byte); | ||||||
|             } |             } | ||||||
|             0xA000..=0xBFFF => match self.mapped { |             0xA000..=0xBFFF => match self.mapped { | ||||||
|                 Some(MBC3Device::ExternalRam) if self.devs_enabled => { |                 Some(Mbc3Device::Ram) if self.devs_enabled => { | ||||||
|                     self.memory[0x2000 * self.ram_bank as usize + (addr as usize - 0xA000)] = byte |                     self.mem[0x2000 * self.ram_bank as usize + (addr as usize - 0xA000)] = byte | ||||||
|                 } |                 } | ||||||
|                 Some(MBC3Device::RealTimeClock) if self.devs_enabled => { |                 Some(Mbc3Device::Rtc(rtc_reg)) if self.devs_enabled => match rtc_reg { | ||||||
|                     todo!("Write to RTC") |                     Sec => { | ||||||
|  |                         self.rtc.sec = byte & 0x3F; | ||||||
|  |                         // Writing to RTC S resets the internal sub-second counter | ||||||
|  |                         self.rtc.cycles = 0; | ||||||
|                     } |                     } | ||||||
|  |                     Min => self.rtc.min = byte & 0x3F, | ||||||
|  |                     Hr => self.rtc.hr = byte & 0x1F, | ||||||
|  |                     DayLow => self.rtc.day_low = byte, | ||||||
|  |                     DayHigh => self.rtc.day_high = (byte & 0xC1).into(), | ||||||
|  |                 }, | ||||||
|                 _ => {} |                 _ => {} | ||||||
|             }, |             }, | ||||||
|             _ => unreachable!("A write to {:#06X} should not be handled by MBC3", addr), |             _ => unreachable!("A write to {:#06X} should not be handled by MBC3", addr), | ||||||
| @@ -346,27 +533,37 @@ impl MBCIo for MBC3 { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl RtcTick for Mbc3 { | ||||||
|  |     fn tick(&mut self) { | ||||||
|  |         if let Mbc3Hardware::RtcBatteryRam | Mbc3Hardware::Rtc = self.hw { | ||||||
|  |             self.rtc.tick(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| struct MBC5 { | struct Mbc5 { | ||||||
|     /// 9-bit number |     /// 9-bit number | ||||||
|     rom_bank: u16, |     rom_bank: u16, | ||||||
|     /// 4-bit number |     /// 4-bit number | ||||||
|     ram_bank: u8, |     ram_bank: u8, | ||||||
|  |  | ||||||
|     rom_cap: usize, |     rom_cap: usize, | ||||||
|  |     mem: Vec<u8>, | ||||||
|     memory: Vec<u8>, |  | ||||||
|     mem_enabled: bool, |     mem_enabled: bool, | ||||||
|  |  | ||||||
|  |     hw: Mbc5Hardware, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl MBC5 { | impl Mbc5 { | ||||||
|     fn new(ram_cap: usize, rom_cap: usize) -> Self { |     fn new(hw: Mbc5Hardware, ram_cap: usize, rom_cap: usize) -> Self { | ||||||
|         Self { |         Self { | ||||||
|             rom_bank: 0x01, |             rom_bank: 0x01, | ||||||
|             memory: vec![0; ram_cap], |             mem: vec![0; ram_cap], | ||||||
|             rom_cap, |             rom_cap, | ||||||
|             ram_bank: Default::default(), |             ram_bank: Default::default(), | ||||||
|             mem_enabled: Default::default(), |             mem_enabled: Default::default(), | ||||||
|  |             hw, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -375,17 +572,33 @@ impl MBC5 { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl MBCIo for MBC5 { | impl Savable for Mbc5 { | ||||||
|     fn handle_read(&self, addr: u16) -> MBCResult { |     fn ext_ram(&self) -> Option<&[u8]> { | ||||||
|         use MBCResult::*; |         match self.hw { | ||||||
|  |             Mbc5Hardware::RumbleBatteryRam | Mbc5Hardware::BatteryRam => Some(&self.mem), | ||||||
|  |             _ => None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn ext_ram_mut(&mut self) -> Option<&mut [u8]> { | ||||||
|  |         match self.hw { | ||||||
|  |             Mbc5Hardware::RumbleBatteryRam | Mbc5Hardware::BatteryRam => Some(&mut self.mem), | ||||||
|  |             _ => None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl MbcIo for Mbc5 { | ||||||
|  |     fn handle_read(&self, addr: u16) -> MbcResult { | ||||||
|  |         use MbcResult::*; | ||||||
|  |  | ||||||
|         match addr { |         match addr { | ||||||
|             0x0000..=0x3FFF => Address(addr as usize), |             0x0000..=0x3FFF => Addr(addr as usize), | ||||||
|             0x4000..=0x7FFF => Address(self.bank_addr(addr)), |             0x4000..=0x7FFF => Addr(self.bank_addr(addr)), | ||||||
|             0xA000..=0xBFFF if self.mem_enabled => { |             0xA000..=0xBFFF if self.mem_enabled => { | ||||||
|                 Value(self.memory[0x2000 * self.ram_bank as usize + (addr as usize - 0xA000)]) |                 Byte(self.mem[0x2000 * self.ram_bank as usize + (addr as usize - 0xA000)]) | ||||||
|             } |             } | ||||||
|             0xA000..=0xBFFF => Value(0xFF), |             0xA000..=0xBFFF => Byte(0xFF), | ||||||
|             _ => unreachable!("A read from {:#06X} should not be handled by MBC5", addr), |             _ => unreachable!("A read from {:#06X} should not be handled by MBC5", addr), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -397,7 +610,7 @@ impl MBCIo for MBC5 { | |||||||
|             0x3000..=0x3FFF => self.rom_bank = (self.rom_bank & 0x00FF) | (byte as u16 & 0x01) << 8, |             0x3000..=0x3FFF => self.rom_bank = (self.rom_bank & 0x00FF) | (byte as u16 & 0x01) << 8, | ||||||
|             0x4000..=0x5FFF => self.ram_bank = byte & 0x0F, |             0x4000..=0x5FFF => self.ram_bank = byte & 0x0F, | ||||||
|             0xA000..=0xBFFF if self.mem_enabled => { |             0xA000..=0xBFFF if self.mem_enabled => { | ||||||
|                 self.memory[0x2000 * self.ram_bank as usize + (addr as usize - 0xA000)] = byte; |                 self.mem[0x2000 * self.ram_bank as usize + (addr as usize - 0xA000)] = byte; | ||||||
|             } |             } | ||||||
|             0xA000..=0xBFFF => {} |             0xA000..=0xBFFF => {} | ||||||
|             _ => unreachable!("A write to {:#06X} should not be handled by MBC5", addr), |             _ => unreachable!("A write to {:#06X} should not be handled by MBC5", addr), | ||||||
| @@ -405,48 +618,173 @@ impl MBCIo for MBC5 { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl RtcTick for Mbc5 { | ||||||
|  |     fn tick(&mut self) {} | ||||||
|  | } | ||||||
|  |  | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| struct NoMBC; | struct Mbc2 { | ||||||
|  |     /// 4-bit number | ||||||
|  |     rom_bank: u8, | ||||||
|  |     mem: Box<[u8; Self::RAM_SIZE]>, | ||||||
|  |  | ||||||
| impl MBCIo for NoMBC { |     mem_enabled: bool, | ||||||
|     fn handle_read(&self, addr: u16) -> MBCResult { |     rom_cap: usize, | ||||||
|         MBCResult::Address(addr as usize) |     hw: Mbc2Hardware, | ||||||
| } | } | ||||||
|  |  | ||||||
|     fn handle_write(&mut self, _addr: u16, _byte: u8) { | impl Mbc2 { | ||||||
|         // eprintln!("Tried to write {:#04X} to a read-only cartridge", byte); |     const RAM_SIZE: usize = 0x0200; | ||||||
|  |  | ||||||
|  |     fn new(hw: Mbc2Hardware, rom_cap: usize) -> Self { | ||||||
|  |         Self { | ||||||
|  |             rom_bank: 0x01, | ||||||
|  |             mem: Box::new([0; Self::RAM_SIZE]), | ||||||
|  |             rom_cap, | ||||||
|  |             mem_enabled: Default::default(), | ||||||
|  |             hw, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| trait MBCIo { |     fn rom_addr(&self, addr: u16) -> usize { | ||||||
|     fn handle_read(&self, addr: u16) -> MBCResult; |         (0x4000 * self.rom_bank as usize + (addr as usize - 0x4000)) % self.rom_cap | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Savable for Mbc2 { | ||||||
|  |     fn ext_ram(&self) -> Option<&[u8]> { | ||||||
|  |         match self.hw { | ||||||
|  |             Mbc2Hardware::BatteryRam => Some(self.mem.as_ref()), | ||||||
|  |             Mbc2Hardware::None => None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn ext_ram_mut(&mut self) -> Option<&mut [u8]> { | ||||||
|  |         match self.hw { | ||||||
|  |             Mbc2Hardware::BatteryRam => Some(self.mem.as_mut()), | ||||||
|  |             Mbc2Hardware::None => None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl MbcIo for Mbc2 { | ||||||
|  |     fn handle_read(&self, addr: u16) -> MbcResult { | ||||||
|  |         use MbcResult::*; | ||||||
|  |  | ||||||
|  |         match addr { | ||||||
|  |             0x0000..=0x3FFF => Addr(addr as usize), | ||||||
|  |             0x4000..=0x7FFF => Addr(self.rom_addr(addr)), | ||||||
|  |             0xA000..=0xBFFF if self.mem_enabled => { | ||||||
|  |                 let mbc2_addr = addr as usize & (Self::RAM_SIZE - 1); | ||||||
|  |                 Byte(self.mem[mbc2_addr] | 0xF0) | ||||||
|  |             } | ||||||
|  |             0xA000..=0xBFFF => Byte(0xFF), | ||||||
|  |             _ => unreachable!("A read from {:#06X} should not be handled by MBC2", addr), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn handle_write(&mut self, addr: u16, byte: u8) { | ||||||
|  |         let nybble = byte & 0x0F; | ||||||
|  |  | ||||||
|  |         match addr { | ||||||
|  |             0x0000..=0x3FFF if addr >> 8 & 0x01 == 0x01 => { | ||||||
|  |                 self.rom_bank = if nybble == 0x00 { 0x01 } else { nybble }; | ||||||
|  |             } | ||||||
|  |             0x0000..=0x3FFF => self.mem_enabled = nybble == 0x0A, | ||||||
|  |             0xA000..=0xBFFF if self.mem_enabled => { | ||||||
|  |                 let mbc2_addr = addr as usize & (Self::RAM_SIZE - 1); | ||||||
|  |                 self.mem[mbc2_addr] = nybble; | ||||||
|  |             } | ||||||
|  |             0x4000..=0x7FFF | 0xA000..=0xBFFF => {} | ||||||
|  |             _ => unreachable!("A write to {:#06X} should not be handled by MBC2", addr), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl RtcTick for Mbc2 { | ||||||
|  |     fn tick(&mut self) {} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug)] | ||||||
|  | struct NoMbc; | ||||||
|  |  | ||||||
|  | impl Savable for NoMbc { | ||||||
|  |     fn ext_ram(&self) -> Option<&[u8]> { | ||||||
|  |         None | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn ext_ram_mut(&mut self) -> Option<&mut [u8]> { | ||||||
|  |         None | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl MbcIo for NoMbc { | ||||||
|  |     fn handle_read(&self, addr: u16) -> MbcResult { | ||||||
|  |         MbcResult::Addr(addr as usize) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn handle_write(&mut self, _: u16, byte: u8) { | ||||||
|  |         tracing::warn!("Attempted write of {:#04X} to cartridge w/out MBC", byte); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl RtcTick for NoMbc { | ||||||
|  |     fn tick(&mut self) {} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | trait MbcIo: Savable + RtcTick { | ||||||
|  |     fn handle_read(&self, addr: u16) -> MbcResult; | ||||||
|     fn handle_write(&mut self, addr: u16, byte: u8); |     fn handle_write(&mut self, addr: u16, byte: u8); | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Copy)] | #[derive(Debug, Clone, Copy)] | ||||||
| enum MBCResult { | enum MbcResult { | ||||||
|     Address(usize), |     Addr(usize), | ||||||
|     Value(u8), |     Byte(u8), | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Copy)] | #[derive(Debug, Clone, Copy)] | ||||||
| enum MBCKind { | enum MbcKind { | ||||||
|     None, |     None, | ||||||
|     MBC1, |     Mbc1(Mbc1Hardware), | ||||||
|     MBC1WithBattery, |     Mbc2(Mbc2Hardware), | ||||||
|     MBC3, |     Mbc3(Mbc3Hardware), | ||||||
|     MBC3WithBattery, |     Mbc5(Mbc5Hardware), | ||||||
|     MBC5, |  | ||||||
|     MBC5WithBattery, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl Default for MBCKind { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self::None |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Copy)] | #[derive(Debug, Clone, Copy)] | ||||||
|  | enum Mbc1Hardware { | ||||||
|  |     None, | ||||||
|  |     Ram, | ||||||
|  |     BatteryRam, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Copy)] | ||||||
|  | enum Mbc2Hardware { | ||||||
|  |     None, | ||||||
|  |     BatteryRam, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Copy)] | ||||||
|  | enum Mbc3Hardware { | ||||||
|  |     Rtc, | ||||||
|  |     RtcBatteryRam, | ||||||
|  |     None, | ||||||
|  |     Ram, | ||||||
|  |     BatteryRam, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Copy)] | ||||||
|  | enum Mbc5Hardware { | ||||||
|  |     None, | ||||||
|  |     Ram, | ||||||
|  |     BatteryRam, | ||||||
|  |     Rumble, | ||||||
|  |     RumbleRam, | ||||||
|  |     RumbleBatteryRam, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Copy, PartialEq)] | ||||||
| enum RamSize { | enum RamSize { | ||||||
|     None = 0x00, |     None = 0x00, | ||||||
|     Unused = 0x01, |     Unused = 0x01, | ||||||
| @@ -471,12 +809,6 @@ impl RamSize { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for RamSize { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self::None |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl From<u8> for RamSize { | impl From<u8> for RamSize { | ||||||
|     fn from(byte: u8) -> Self { |     fn from(byte: u8) -> Self { | ||||||
|         use RamSize::*; |         use RamSize::*; | ||||||
| @@ -545,14 +877,90 @@ impl From<u8> for RomSize { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl std::fmt::Debug for Box<dyn MBCIo> { | impl std::fmt::Debug for Box<dyn MbcIo> { | ||||||
|     fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |     fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||||
|         todo!("Implement Debug for Box<dyn MBC> Trait Object"); |         todo!("Implement Debug for Box<dyn MBC> Trait Object"); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for Box<dyn MBCIo> { | trait Savable { | ||||||
|     fn default() -> Self { |     fn ext_ram(&self) -> Option<&[u8]>; | ||||||
|         Box::new(NoMBC) |     fn ext_ram_mut(&mut self) -> Option<&mut [u8]>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[cfg(test)] | ||||||
|  | mod tests { | ||||||
|  |     use super::Cartridge; | ||||||
|  |  | ||||||
|  |     #[test] | ||||||
|  |     fn empty_rom_title() { | ||||||
|  |         let title = [ | ||||||
|  |             0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||||||
|  |             0x00, 0x00, | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         assert_eq!(None, Cartridge::detect_title(&title)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     #[test] | ||||||
|  |     fn normal_rom_title() { | ||||||
|  |         let title = [ | ||||||
|  |             0x50, 0x4F, 0x4B, 0x45, 0x4D, 0x4F, 0x4E, 0x20, 0x42, 0x4C, 0x55, 0x45, 0x00, 0x00, | ||||||
|  |             0x00, 0x00, | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         assert_eq!( | ||||||
|  |             Some(String::from("POKEMON BLUE")), | ||||||
|  |             Cartridge::detect_title(&title) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     #[test] | ||||||
|  |     fn extra_spaces_title() { | ||||||
|  |         let title = [ | ||||||
|  |             0x54, 0x4f, 0x4b, 0x49, 0x4d, 0x45, 0x4b, 0x49, 0x20, 0x43, 0x55, 0x4c, 0x20, 0x20, 0, | ||||||
|  |             0, | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         assert_eq!( | ||||||
|  |             Some(String::from("TOKIMEKI CUL")), | ||||||
|  |             Cartridge::detect_title(&title) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     #[test] | ||||||
|  |     fn long_title() { | ||||||
|  |         let title = [ | ||||||
|  |             0x54, 0x4f, 0x4b, 0x49, 0x4d, 0x45, 0x4b, 0x49, 0x20, 0x43, 0x55, 0x4c, 0x54, 0x55, | ||||||
|  |             0x52, 0x45, | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         assert_eq!( | ||||||
|  |             Some(String::from("TOKIMEKI CULTURE")), | ||||||
|  |             Cartridge::detect_title(&title), | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     #[test] | ||||||
|  |     fn publisher_code_and_title() { | ||||||
|  |         let title: [u8; 16] = [ | ||||||
|  |             0x47, 0x52, 0x41, 0x4E, 0x44, 0x20, 0x54, 0x48, 0x45, 0x46, 0x54, 0x41, 0x4F, 0x41, | ||||||
|  |             0x45, 0x80, | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         assert_eq!( | ||||||
|  |             Some(String::from("GRAND THEFT")), | ||||||
|  |             Cartridge::detect_title(&title) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     #[test] | ||||||
|  |     fn all_whitespace_title() { | ||||||
|  |         let title = [ | ||||||
|  |             0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||||||
|  |             0x00, 0x00, | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         assert_eq!(None, Cartridge::detect_title(&title)); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										120
									
								
								src/cpu.rs
									
									
									
									
									
								
							
							
						
						
									
										120
									
								
								src/cpu.rs
									
									
									
									
									
								
							| @@ -1,16 +1,13 @@ | |||||||
| use crate::apu::Apu; | use crate::bus::{Bus, BusIo, BOOT_SIZE}; | ||||||
| use crate::bus::{Bus, BusIo}; |  | ||||||
| use crate::instruction::cycle::Cycle; |  | ||||||
| use crate::instruction::Instruction; | use crate::instruction::Instruction; | ||||||
| use crate::interrupt::{InterruptEnable, InterruptFlag}; | use crate::interrupt::{InterruptEnable, InterruptFlag}; | ||||||
| use crate::joypad::Joypad; | use crate::Cycle; | ||||||
| use crate::ppu::Ppu; |  | ||||||
| use bitfield::bitfield; | use bitfield::bitfield; | ||||||
| use std::fmt::{Display, Formatter, Result as FmtResult}; | use std::fmt::{Display, Formatter, Result as FmtResult}; | ||||||
|  |  | ||||||
| #[derive(Debug, Default)] | #[derive(Debug)] | ||||||
| pub struct Cpu { | pub struct Cpu { | ||||||
|     pub bus: Bus, |     pub(crate) bus: Bus, | ||||||
|     reg: Registers, |     reg: Registers, | ||||||
|     flags: Flags, |     flags: Flags, | ||||||
|     ime: ImeState, |     ime: ImeState, | ||||||
| @@ -18,31 +15,16 @@ pub struct Cpu { | |||||||
| } | } | ||||||
|  |  | ||||||
| impl Cpu { | impl Cpu { | ||||||
|     pub fn new() -> Self { |     pub(crate) fn new(rom: [u8; BOOT_SIZE]) -> Self { | ||||||
|         Self { |         Self { | ||||||
|             reg: Registers { |             bus: Bus::with_boot(rom), | ||||||
|                 a: 0x01, |             reg: Default::default(), | ||||||
|                 b: 0x00, |             flags: Flags(0), | ||||||
|                 c: 0x13, |             ime: ImeState::Disabled, | ||||||
|                 d: 0x00, |             state: State::Execute, | ||||||
|                 e: 0xD8, |  | ||||||
|                 h: 0x01, |  | ||||||
|                 l: 0x4D, |  | ||||||
|                 sp: 0xFFFE, |  | ||||||
|                 pc: 0x0100, |  | ||||||
|             }, |  | ||||||
|             flags: 0xb0.into(), |  | ||||||
|             ..Default::default() |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn boot_new(path: &str) -> anyhow::Result<Self> { |  | ||||||
|         Ok(Self { |  | ||||||
|             bus: Bus::with_boot(path)?, |  | ||||||
|             ..Default::default() |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     pub(crate) fn ime(&self) -> ImeState { |     pub(crate) fn ime(&self) -> ImeState { | ||||||
|         self.ime |         self.ime | ||||||
|     } |     } | ||||||
| @@ -60,10 +42,7 @@ impl Cpu { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub(crate) fn is_halted(&self) -> bool { |     pub(crate) fn is_halted(&self) -> bool { | ||||||
|         match self.state { |         matches!(self.state, State::Halt(_)) | ||||||
|             State::Halt(_) => true, |  | ||||||
|             _ => false, |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub(crate) fn halt_kind(&self) -> Option<HaltKind> { |     pub(crate) fn halt_kind(&self) -> Option<HaltKind> { | ||||||
| @@ -72,13 +51,11 @@ impl Cpu { | |||||||
|             _ => None, |             _ => None, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn load_cartridge(&mut self, path: &str) -> std::io::Result<()> { |  | ||||||
|         self.bus.load_cartridge(path) |  | ||||||
| } | } | ||||||
|  |  | ||||||
|     pub fn rom_title(&self) -> Option<&str> { | impl Default for Cpu { | ||||||
|         self.bus.rom_title() |     fn default() -> Self { | ||||||
|  |         Cpu::new(*include_bytes!("../bin/bootix_dmg.bin")) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -116,7 +93,7 @@ impl Cpu { | |||||||
|     /// routine. |     /// routine. | ||||||
|     /// |     /// | ||||||
|     /// Handle HALT and interrupts. |     /// Handle HALT and interrupts. | ||||||
|     pub fn step(&mut self) -> Cycle { |     pub(crate) fn step(&mut self) -> Cycle { | ||||||
|         // Log instructions |         // Log instructions | ||||||
|         // if self.reg.pc > 0xFF { |         // if self.reg.pc > 0xFF { | ||||||
|         //     let out = std::io::stdout(); |         //     let out = std::io::stdout(); | ||||||
| @@ -131,13 +108,10 @@ impl Cpu { | |||||||
|             use HaltKind::*; |             use HaltKind::*; | ||||||
|  |  | ||||||
|             self.bus.clock(); |             self.bus.clock(); | ||||||
|  |             return match kind { | ||||||
|             let elapsed = match kind { |                 ImeEnabled | NonePending => 4, | ||||||
|                 ImeEnabled | NonePending => Cycle::new(4), |  | ||||||
|                 SomePending => todo!("Implement HALT bug"), |                 SomePending => todo!("Implement HALT bug"), | ||||||
|             }; |             }; | ||||||
|  |  | ||||||
|             return elapsed; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         let opcode = self.fetch(); |         let opcode = self.fetch(); | ||||||
| @@ -167,18 +141,6 @@ impl BusIo for Cpu { | |||||||
| } | } | ||||||
|  |  | ||||||
| impl 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) { |     fn handle_ei(&mut self) { | ||||||
|         match self.ime { |         match self.ime { | ||||||
|             ImeState::EiExecuted => self.ime = ImeState::Pending, |             ImeState::EiExecuted => self.ime = ImeState::Pending, | ||||||
| @@ -276,12 +238,6 @@ enum State { | |||||||
|     // Stop, |     // Stop, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for State { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self::Execute |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl Cpu { | impl Cpu { | ||||||
|     pub(crate) fn set_register(&mut self, register: Register, value: u8) { |     pub(crate) fn set_register(&mut self, register: Register, value: u8) { | ||||||
|         use Register::*; |         use Register::*; | ||||||
| @@ -475,12 +431,6 @@ impl Clone for Flags { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for Flags { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self(0) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl Display for Flags { | impl Display for Flags { | ||||||
|     fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { |     fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { | ||||||
|         if self.z() { |         if self.z() { | ||||||
| @@ -536,8 +486,38 @@ pub(crate) enum ImeState { | |||||||
|     Enabled, |     Enabled, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for ImeState { | pub(crate) mod dbg { | ||||||
|     fn default() -> Self { |     use super::{Cpu, ImeState, RegisterPair}; | ||||||
|         Self::Disabled |  | ||||||
|  |     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) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										184
									
								
								src/emu.rs
									
									
									
									
									
								
							
							
						
						
									
										184
									
								
								src/emu.rs
									
									
									
									
									
								
							| @@ -1,73 +1,163 @@ | |||||||
| use crate::cpu::Cpu as SM83; | use crate::apu::gen::SampleProducer; | ||||||
| use crate::instruction::cycle::Cycle; | use crate::bus::BOOT_SIZE; | ||||||
| use crate::joypad; | use crate::cartridge::Cartridge; | ||||||
| use crate::ppu::Ppu; | use crate::cpu::Cpu; | ||||||
| use anyhow::Result; | use crate::{Cycle, GB_HEIGHT, GB_WIDTH}; | ||||||
|  | use clap::crate_name; | ||||||
| use gilrs::Gilrs; | use gilrs::Gilrs; | ||||||
|  | use std::fs::File; | ||||||
|  | use std::io::{Read, Write}; | ||||||
|  | use std::path::{Path, PathBuf}; | ||||||
| use std::time::Duration; | 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 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 | pub(crate) const SM83_CLOCK_SPEED: u64 = 0x40_0000; // Hz which is 4.194304Mhz | ||||||
| const DEFAULT_TITLE: &str = "DMG-01 Emulator"; | const DEFAULT_TITLE: &str = "Game Boy Screen"; | ||||||
| const GAMEPAD_ENABLED: bool = true; |  | ||||||
|  |  | ||||||
| pub fn init(boot_path: Option<&str>, rom_path: &str) -> Result<SM83> { | #[inline] | ||||||
|     let mut cpu = match boot_path { | pub fn run_frame(cpu: &mut Cpu, gamepad: &mut Gilrs, key: KeyboardInput) { | ||||||
|         Some(path) => SM83::boot_new(path)?, |     run(cpu, gamepad, key, CYCLES_IN_FRAME) | ||||||
|         None => SM83::new(), |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     eprintln!("Initialized GB Emulator"); |  | ||||||
|  |  | ||||||
|     cpu.load_cartridge(rom_path)?; |  | ||||||
|     Ok(cpu) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn rom_title(game_boy: &SM83) -> &str { | #[inline] | ||||||
|     game_boy.rom_title().unwrap_or(DEFAULT_TITLE) | pub fn run(cpu: &mut Cpu, gamepad: &mut Gilrs, key: KeyboardInput, cycles: Cycle) { | ||||||
| } |     let mut elapsed = 0; | ||||||
|  |  | ||||||
| pub fn run( |  | ||||||
|     game_boy: &mut SM83, |  | ||||||
|     gamepad: &mut Gilrs, |  | ||||||
|     input: &WinitInputHelper, |  | ||||||
|     target: Cycle, |  | ||||||
| ) -> Cycle { |  | ||||||
|     let mut elapsed = Cycle::new(0); |  | ||||||
|  |  | ||||||
|     if GAMEPAD_ENABLED { |  | ||||||
|     if let Some(event) = gamepad.next_event() { |     if let Some(event) = gamepad.next_event() { | ||||||
|             joypad::handle_gamepad_input(game_boy.joypad_mut(), 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(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|     joypad::handle_keyboard_input(game_boy.joypad_mut(), input); | pub fn save_and_exit(cpu: &Cpu, control_flow: &mut ControlFlow) { | ||||||
|     while elapsed < target { |     write_save(cpu); | ||||||
|         elapsed += game_boy.step(); |     *control_flow = ControlFlow::Exit; | ||||||
| } | } | ||||||
|  |  | ||||||
|     elapsed | #[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 run_frame(game_boy: &mut SM83, gamepad: &mut Gilrs, input: &WinitInputHelper) -> Cycle { | pub fn from_boot_rom<P: AsRef<Path>>(path: P) -> std::io::Result<Cpu> { | ||||||
|     let mut elapsed = Cycle::new(0); |     Ok(Cpu::new(read_boot(path)?)) | ||||||
|  | } | ||||||
|  |  | ||||||
|     if GAMEPAD_ENABLED { | pub fn read_game_rom<P: AsRef<Path>>(cpu: &mut Cpu, path: P) -> std::io::Result<()> { | ||||||
|         if let Some(event) = gamepad.next_event() { |     cpu.bus.cart = Some(Cartridge::new(std::fs::read(path.as_ref())?)); | ||||||
|             joypad::handle_gamepad_input(game_boy.joypad_mut(), event); |     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"), | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|     joypad::handle_keyboard_input(game_boy.joypad_mut(), input); | pub fn load_save(cpu: &mut Cpu) { | ||||||
|     while elapsed < CYCLES_IN_FRAME { |     match cpu.bus.cart.as_mut() { | ||||||
|         elapsed += game_boy.step(); |         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"), | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|     elapsed | fn write_save_to_file(cart: &Cartridge) -> Result<PathBuf, SaveError> { | ||||||
|  |     match cart.title.as_ref().zip(cart.ext_ram()) { | ||||||
|  |         Some((title, ram)) => { | ||||||
|  |             let mut save_path = data_path().unwrap_or_else(|| PathBuf::from(".")); | ||||||
|  |             save_path.push(title); | ||||||
|  |             save_path.set_extension("sav"); | ||||||
|  |  | ||||||
|  |             let mut file = File::create(&save_path)?; | ||||||
|  |             file.write_all(ram)?; | ||||||
|  |             Ok(save_path) | ||||||
|  |         } | ||||||
|  |         None => Err(SaveError::NotApplicable), | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn draw(ppu: &Ppu, frame: &mut [u8]) { | fn read_save_from_file(cart: &mut Cartridge) -> Result<PathBuf, SaveError> { | ||||||
|     ppu.copy_to_gui(frame); |     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"); | ||||||
|  |  | ||||||
|  |             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), | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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 { | 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 { |     fn read_byte(&self, addr: u16) -> u8 { | ||||||
|         self.buf[addr as usize - HIGH_RAM_START_ADDRESS] |         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; | use bitfield::bitfield; | ||||||
|  |  | ||||||
| #[derive(Debug, Default)] | #[derive(Debug)] | ||||||
| pub(crate) struct Interrupt { | pub(crate) struct Interrupt { | ||||||
|     pub(crate) flag: InterruptFlag, |     pub(crate) flag: InterruptFlag, | ||||||
|     pub(crate) enable: InterruptEnable, |     pub(crate) enable: InterruptEnable, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl Default for Interrupt { | ||||||
|  |     fn default() -> Self { | ||||||
|  |         Self { | ||||||
|  |             flag: InterruptFlag(0), | ||||||
|  |             enable: InterruptEnable(0), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| bitfield! { | bitfield! { | ||||||
|    pub struct InterruptEnable(u8); |    pub struct InterruptEnable(u8); | ||||||
|     impl Debug; |     impl Debug; | ||||||
| @@ -23,12 +32,6 @@ impl Clone for InterruptEnable { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for InterruptEnable { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self(0) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl From<u8> for InterruptEnable { | impl From<u8> for InterruptEnable { | ||||||
|     fn from(byte: u8) -> Self { |     fn from(byte: u8) -> Self { | ||||||
|         Self(byte) |         Self(byte) | ||||||
| @@ -58,12 +61,6 @@ impl Clone for InterruptFlag { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for InterruptFlag { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self(0) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl From<u8> for InterruptFlag { | impl From<u8> for InterruptFlag { | ||||||
|     fn from(byte: u8) -> Self { |     fn from(byte: u8) -> Self { | ||||||
|         Self(byte) |         Self(byte) | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| use gilrs::{Button, Event as GamepadEvent, EventType as GamepadEventType}; | use gilrs::{Button, Event as GamepadEvent, EventType as GamepadEventType}; | ||||||
| use winit_input_helper::WinitInputHelper; | use winit::event::{ElementState, KeyboardInput, VirtualKeyCode}; | ||||||
|  |  | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| pub struct Joypad { | pub struct Joypad { | ||||||
| @@ -110,72 +110,39 @@ impl ButtonEvent { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn handle_keyboard_input(pad: &mut Joypad, input: &WinitInputHelper) { | #[inline] | ||||||
|     use winit::event::VirtualKeyCode; | pub(crate) fn handle_keyboard_input(pad: &mut Joypad, key: KeyboardInput) { | ||||||
|  |  | ||||||
|     // TODO: What do I have to do to get a match statement here? |  | ||||||
|  |  | ||||||
|     let state = &mut pad.ext; |     let state = &mut pad.ext; | ||||||
|     let irq = &mut pad.interrupt; |     let irq = &mut pad.interrupt; | ||||||
|  |  | ||||||
|     if input.key_pressed(VirtualKeyCode::Down) { |     match key.state { | ||||||
|         state.dpad_down.update(true, irq); |         ElementState::Pressed => match key.virtual_keycode { | ||||||
|     } |             Some(VirtualKeyCode::Down) => state.dpad_down.update(true, irq), | ||||||
|     if input.key_released(VirtualKeyCode::Down) { |             Some(VirtualKeyCode::Up) => state.dpad_up.update(true, irq), | ||||||
|         state.dpad_down.update(false, 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), | ||||||
|     if input.key_pressed(VirtualKeyCode::Up) { |             Some(VirtualKeyCode::RShift) => state.select.update(true, irq), | ||||||
|         state.dpad_up.update(true, irq); |             Some(VirtualKeyCode::Z) => state.south.update(true, irq), | ||||||
|     } |             Some(VirtualKeyCode::X) => state.east.update(true, irq), | ||||||
|     if input.key_released(VirtualKeyCode::Up) { |             None | Some(_) => {} | ||||||
|         state.dpad_up.update(false, irq); |         }, | ||||||
|     } |         ElementState::Released => match key.virtual_keycode { | ||||||
|  |             Some(VirtualKeyCode::Down) => state.dpad_down.update(false, irq), | ||||||
|     if input.key_pressed(VirtualKeyCode::Left) { |             Some(VirtualKeyCode::Up) => state.dpad_up.update(false, irq), | ||||||
|         state.dpad_left.update(true, irq); |             Some(VirtualKeyCode::Left) => state.dpad_left.update(false, irq), | ||||||
|     } |             Some(VirtualKeyCode::Right) => state.dpad_right.update(false, irq), | ||||||
|     if input.key_released(VirtualKeyCode::Left) { |             Some(VirtualKeyCode::Return) => state.start.update(false, irq), | ||||||
|         state.dpad_left.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), | ||||||
|     if input.key_pressed(VirtualKeyCode::Right) { |             None | Some(_) => {} | ||||||
|         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); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| 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 Button::*; | ||||||
|     use GamepadEventType::*; |     use GamepadEventType::*; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| pub use apu::gen::AudioSPSC; | #![allow(clippy::derivable_impls)] // Will remove this if bitfield-rs allows default impls | ||||||
| pub use instruction::cycle::Cycle; |  | ||||||
|  | pub use apu::gen::init as spsc_init; | ||||||
|  | pub type Cycle = u64; | ||||||
|  |  | ||||||
| pub const GB_WIDTH: usize = 160; | pub const GB_WIDTH: usize = 160; | ||||||
| pub const GB_HEIGHT: usize = 144; | pub const GB_HEIGHT: usize = 144; | ||||||
| @@ -9,6 +11,7 @@ mod bus; | |||||||
| mod cartridge; | mod cartridge; | ||||||
| mod cpu; | mod cpu; | ||||||
| pub mod emu; | pub mod emu; | ||||||
|  | pub mod gui; | ||||||
| mod high_ram; | mod high_ram; | ||||||
| mod instruction; | mod instruction; | ||||||
| mod interrupt; | mod interrupt; | ||||||
|   | |||||||
							
								
								
									
										216
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										216
									
								
								src/main.rs
									
									
									
									
									
								
							| @@ -1,160 +1,126 @@ | |||||||
| use anyhow::{anyhow, Result}; | use std::path::PathBuf; | ||||||
| use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg}; | use std::time::Instant; | ||||||
| use gb::{AudioSPSC, Cycle, GB_HEIGHT, GB_WIDTH}; |  | ||||||
| use gilrs::Gilrs; | use clap::{arg, command, value_parser}; | ||||||
| use pixels::{PixelsBuilder, SurfaceTexture}; | use gb::gui::{EmuMode, Gui}; | ||||||
| use rodio::{OutputStream, Sink}; | use gb::{emu, gui}; | ||||||
| use std::time::{Duration, Instant}; | use gilrs::Gilrs; | ||||||
| use winit::dpi::LogicalSize; | use rodio::{OutputStream, Sink}; | ||||||
| use winit::event::{Event, VirtualKeyCode}; | use tracing_subscriber::EnvFilter; | ||||||
| use winit::event_loop::{ControlFlow, EventLoop}; | use winit::event::{Event, WindowEvent}; | ||||||
| use winit::window::{Window, WindowBuilder}; | use winit::event_loop::{EventLoop, EventLoopBuilder}; | ||||||
| use winit_input_helper::WinitInputHelper; |  | ||||||
|  |  | ||||||
| const WINDOW_SCALE: f64 = 2.0; |  | ||||||
| const AUDIO_ENABLED: bool = true; | const AUDIO_ENABLED: bool = true; | ||||||
|  |  | ||||||
| fn main() -> Result<()> { | fn main() { | ||||||
|     let app = App::new(crate_name!()) |     let m = command!() | ||||||
|         .version(crate_version!()) |  | ||||||
|         .author(crate_authors!()) |  | ||||||
|         .about(crate_description!()); |  | ||||||
|  |  | ||||||
|     let m = app |  | ||||||
|         .arg( |         .arg( | ||||||
|             Arg::with_name("rom") |             arg!(-b --boot <FILE> "path to boot ROM") | ||||||
|                 .value_name("ROM_FILE") |                 .required(false) | ||||||
|                 .takes_value(true) |                 .value_parser(value_parser!(PathBuf)), | ||||||
|                 .required(true) |  | ||||||
|                 .index(1) |  | ||||||
|                 .help("Path to the Game ROM"), |  | ||||||
|         ) |         ) | ||||||
|         .arg( |         .arg( | ||||||
|             Arg::with_name("boot") |             arg!([ROM_FILE] "path to game ROM") | ||||||
|                 .short("b") |                 .required(true) | ||||||
|                 .long("boot") |                 .value_parser(value_parser!(PathBuf)), | ||||||
|                 .value_name("FILE") |  | ||||||
|                 .takes_value(true) |  | ||||||
|                 .help("Path to Boot ROM"), |  | ||||||
|         ) |         ) | ||||||
|         .get_matches(); |         .get_matches(); | ||||||
|  |  | ||||||
|     let rom_path = m |     // Set up subscriber | ||||||
|         .value_of("rom") |     if std::env::var("RUST_LOG").is_err() { | ||||||
|         .expect("Required value 'rom' was provided"); |         std::env::set_var("RUST_LOG", "gb=info"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     let mut game_boy = |     tracing_subscriber::fmt::fmt() | ||||||
|         gb::emu::init(m.value_of("boot"), rom_path).expect("Initialize DMG-01 Emulator"); |         .with_env_filter(EnvFilter::from_default_env()) | ||||||
|     let rom_title = gb::emu::rom_title(&game_boy); |         .init(); | ||||||
|  |  | ||||||
|     let mut gamepad = Gilrs::new().expect("Initialize Controller Support"); |     // Init CPU | ||||||
|  |     let mut cpu = match m.get_one::<PathBuf>("boot") { | ||||||
|     // Initialize GUI |         Some(path) => { | ||||||
|     let event_loop = EventLoop::new(); |             tracing::info!("User-provided boot ROM"); | ||||||
|     let mut input = WinitInputHelper::new(); |             emu::from_boot_rom(path).expect("initialize emulator with custom boot rom") | ||||||
|     let window = create_window(&event_loop, rom_title)?; |         } | ||||||
|  |         None => { | ||||||
|     let mut pixels = { |             tracing::info!("Built-in boot ROM"); | ||||||
|         let size = window.inner_size(); |             Default::default() | ||||||
|         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()? |  | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|  |     // 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 |     // Initialize Audio | ||||||
|     let (_stream, stream_handle) = OutputStream::try_default().expect("Initialized Audio"); |     let (_stream, stream_handle) = OutputStream::try_default().expect("Initialized Audio"); | ||||||
|  |  | ||||||
|     if AUDIO_ENABLED { |     if AUDIO_ENABLED { | ||||||
|         let spsc: AudioSPSC<f32> = Default::default(); |         let (prod, cons) = gb::spsc_init(); | ||||||
|         let (prod, cons) = spsc.init(); |         let sink = { | ||||||
|         let sink = Sink::try_new(&stream_handle)?; |             let s = Sink::try_new(&stream_handle).expect("create sink from audio stream handle"); | ||||||
|         sink.append(cons); |             s.append(cons); | ||||||
|         game_boy.apu_mut().attach_producer(prod); |             s.set_volume(0.1); | ||||||
|  |             s | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         emu::set_audio_prod(&mut cpu, prod); | ||||||
|  |  | ||||||
|  |         tracing::info!("Spawn Audio Thread"); | ||||||
|         std::thread::spawn(move || { |         std::thread::spawn(move || { | ||||||
|             sink.sleep_until_end(); |             sink.sleep_until_end(); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     let mut start = Instant::now(); |     // Set up state for the Immediate-mode GUI | ||||||
|     let frame_time = Duration::from_secs_f64(1.0 / 59.73); // 59.73 Hz on Host |     let event_loop: EventLoop<Event<()>> = EventLoopBuilder::with_user_event().build(); | ||||||
|     let mut cycle_count: Cycle = Default::default(); |  | ||||||
|  |     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| { |     event_loop.run(move |event, _, control_flow| { | ||||||
|         if let Event::RedrawRequested(_) = event { |         app.handle_event(&event); | ||||||
|             if pixels |  | ||||||
|                 .render() |  | ||||||
|                 .map_err(|e| anyhow!("pixels.render() failed: {}", e)) |  | ||||||
|                 .is_err() |  | ||||||
|             { |  | ||||||
|                 *control_flow = ControlFlow::Exit; |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if input.update(&event) { |         match event { | ||||||
|             if input.key_pressed(VirtualKeyCode::Escape) || input.quit() { |             Event::MainEventsCleared => { | ||||||
|                 *control_flow = ControlFlow::Exit; |                 app.maybe_quit(&cpu, control_flow); | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if let Some(size) = input.window_resized() { |                 match app.mode { | ||||||
|                 pixels.resize_surface(size.width, size.height); |                     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) => { | ||||||
|             let mut diff = Instant::now() - start; |                         emu::run(&mut cpu, &mut gamepad, last_key, 4); | ||||||
|             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; |                 // Input has been consumed, reset it | ||||||
|  |                 last_key = gui::unused_key(); | ||||||
|  |  | ||||||
|  |                 app.request_redraw(); | ||||||
|             } |             } | ||||||
|             start = Instant::now(); |             Event::RedrawRequested(..) => { | ||||||
|  |                 app.update_time(start_time.elapsed().as_secs_f64()); | ||||||
|             if cycle_count >= gb::emu::CYCLES_IN_FRAME { |                 app.paint(&cpu); | ||||||
|                 cycle_count %= gb::emu::CYCLES_IN_FRAME; |  | ||||||
|  |  | ||||||
|                 gb::emu::draw(game_boy.ppu(), pixels.get_frame()); |  | ||||||
|                 window.request_redraw(); |  | ||||||
|             } |             } | ||||||
|  |             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)?) |  | ||||||
| } |  | ||||||
|   | |||||||
							
								
								
									
										702
									
								
								src/ppu.rs
									
									
									
									
									
								
							
							
						
						
									
										702
									
								
								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) struct DirectMemoryAccess { | ||||||
|     pub(crate) state: DmaState, |     pub(crate) state: DmaState, | ||||||
|     cycle: Cycle, |     cycle: Cycle, | ||||||
| @@ -8,6 +8,16 @@ pub(crate) struct DirectMemoryAccess { | |||||||
|     pub(crate) start: DmaAddress, |     pub(crate) start: DmaAddress, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl Default for DirectMemoryAccess { | ||||||
|  |     fn default() -> Self { | ||||||
|  |         Self { | ||||||
|  |             state: DmaState::Disabled, | ||||||
|  |             cycle: Default::default(), | ||||||
|  |             start: Default::default(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| impl DirectMemoryAccess { | impl DirectMemoryAccess { | ||||||
|     pub(crate) fn tick(&mut self) -> Option<(u16, u16)> { |     pub(crate) fn tick(&mut self) -> Option<(u16, u16)> { | ||||||
|         match self.state { |         match self.state { | ||||||
| @@ -56,7 +66,7 @@ impl DirectMemoryAccess { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     fn reset(&mut self) { |     fn reset(&mut self) { | ||||||
|         self.cycle = Cycle::new(0); |         self.cycle = 0; | ||||||
|         self.state = DmaState::Disabled; |         self.state = DmaState::Disabled; | ||||||
|         self.start.0 = None; |         self.start.0 = None; | ||||||
|     } |     } | ||||||
| @@ -69,12 +79,6 @@ pub(crate) enum DmaState { | |||||||
|     Transferring, |     Transferring, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for DmaState { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self::Disabled |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Copy, Default)] | #[derive(Debug, Clone, Copy, Default)] | ||||||
| pub(crate) struct DmaAddress(Option<u16>); | 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 { | impl From<LCDStatus> for u8 { | ||||||
|     fn from(status: LCDStatus) -> Self { |     fn from(status: LCDStatus) -> Self { | ||||||
|         status.0 |         status.0 | ||||||
| @@ -67,12 +61,6 @@ impl From<PpuMode> for u8 { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for PpuMode { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self::HBlank |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| bitfield! { | bitfield! { | ||||||
|     pub struct LCDControl(u8); |     pub struct LCDControl(u8); | ||||||
|     impl Debug; |     impl Debug; | ||||||
| @@ -93,12 +81,6 @@ impl Clone for LCDControl { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for LCDControl { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self(0) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl From<u8> for LCDControl { | impl From<u8> for LCDControl { | ||||||
|     fn from(byte: u8) -> Self { |     fn from(byte: u8) -> Self { | ||||||
|         Self(byte) |         Self(byte) | ||||||
| @@ -142,12 +124,6 @@ impl From<TileMapAddress> for u8 { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for TileMapAddress { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self::X9800 |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Copy)] | #[derive(Debug, Clone, Copy)] | ||||||
| pub enum TileDataAddress { | pub enum TileDataAddress { | ||||||
|     X8800 = 0, |     X8800 = 0, | ||||||
| @@ -170,12 +146,6 @@ impl From<TileDataAddress> for u8 { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for TileDataAddress { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self::X8800 |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Copy)] | #[derive(Debug, Clone, Copy)] | ||||||
| pub enum ObjectSize { | pub enum ObjectSize { | ||||||
|     Eight = 0, |     Eight = 0, | ||||||
| @@ -183,7 +153,7 @@ pub enum ObjectSize { | |||||||
| } | } | ||||||
|  |  | ||||||
| impl ObjectSize { | impl ObjectSize { | ||||||
|     pub(crate) fn as_u8(&self) -> u8 { |     pub(crate) fn size(&self) -> u8 { | ||||||
|         use ObjectSize::*; |         use ObjectSize::*; | ||||||
|  |  | ||||||
|         match self { |         match self { | ||||||
| @@ -209,12 +179,6 @@ impl From<ObjectSize> for u8 { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for ObjectSize { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self::Eight |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| bitfield! { | bitfield! { | ||||||
|     pub struct BackgroundPalette(u8); |     pub struct BackgroundPalette(u8); | ||||||
|     impl Debug; |     impl Debug; | ||||||
| @@ -243,12 +207,6 @@ impl Clone for BackgroundPalette { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for BackgroundPalette { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self(0) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl From<u8> for BackgroundPalette { | impl From<u8> for BackgroundPalette { | ||||||
|     fn from(byte: u8) -> Self { |     fn from(byte: u8) -> Self { | ||||||
|         Self(byte) |         Self(byte) | ||||||
| @@ -288,12 +246,6 @@ impl Clone for ObjectPalette { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for ObjectPalette { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self(0) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl From<u8> for ObjectPalette { | impl From<u8> for ObjectPalette { | ||||||
|     fn from(byte: u8) -> Self { |     fn from(byte: u8) -> Self { | ||||||
|         Self(byte) |         Self(byte) | ||||||
| @@ -361,24 +313,12 @@ impl From<ObjectFlags> for u8 { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for ObjectFlags { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self(0) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Copy)] | #[derive(Debug, Clone, Copy)] | ||||||
| pub enum ObjectPaletteKind { | pub enum ObjectPaletteKind { | ||||||
|     Zero = 0, |     Zero = 0, | ||||||
|     One = 1, |     One = 1, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for ObjectPaletteKind { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self::Zero |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl From<u8> for ObjectPaletteKind { | impl From<u8> for ObjectPaletteKind { | ||||||
|     fn from(byte: u8) -> Self { |     fn from(byte: u8) -> Self { | ||||||
|         match byte & 0b01 { |         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 { | pub enum RenderPriority { | ||||||
|     Object = 0, |     Object = 0, | ||||||
|     BackgroundAndWindow = 1, |     BackgroundAndWindow = 1, | ||||||
| @@ -417,12 +357,6 @@ impl From<RenderPriority> for u8 { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for RenderPriority { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self::Object |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Copy)] | #[derive(Debug, Clone, Copy)] | ||||||
| pub enum GrayShade { | pub enum GrayShade { | ||||||
|     White = 0, |     White = 0, | ||||||
| @@ -442,12 +376,6 @@ impl GrayShade { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for GrayShade { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self::White |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl From<u8> for GrayShade { | impl From<u8> for GrayShade { | ||||||
|     fn from(byte: u8) -> Self { |     fn from(byte: u8) -> Self { | ||||||
|         match byte & 0b11 { |         match byte & 0b11 { | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| use bitfield::bitfield; | use bitfield::bitfield; | ||||||
|  |  | ||||||
| #[derive(Debug, Default)] | #[derive(Debug)] | ||||||
| pub(crate) struct Serial { | pub(crate) struct Serial { | ||||||
|     /// 0xFF01 | SB - Serial Transfer Data |     /// 0xFF01 | SB - Serial Transfer Data | ||||||
|     pub(crate) next: u8, |     pub(crate) next: u8, | ||||||
| @@ -8,6 +8,15 @@ pub(crate) struct Serial { | |||||||
|     pub(crate) ctrl: SerialControl, |     pub(crate) ctrl: SerialControl, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl Default for Serial { | ||||||
|  |     fn default() -> Self { | ||||||
|  |         Self { | ||||||
|  |             next: Default::default(), | ||||||
|  |             ctrl: SerialControl(0), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| bitfield! { | bitfield! { | ||||||
|     pub struct SerialControl(u8); |     pub struct SerialControl(u8); | ||||||
|     impl Debug; |     impl Debug; | ||||||
| @@ -23,12 +32,6 @@ impl Clone for SerialControl { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for SerialControl { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self(0) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl From<u8> for SerialControl { | impl From<u8> for SerialControl { | ||||||
|     fn from(byte: u8) -> Self { |     fn from(byte: u8) -> Self { | ||||||
|         Self(byte) |         Self(byte) | ||||||
| @@ -47,12 +50,6 @@ enum ShiftClock { | |||||||
|     Internal = 1, |     Internal = 1, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for ShiftClock { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self::External |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl From<u8> for ShiftClock { | impl From<u8> for ShiftClock { | ||||||
|     fn from(byte: u8) -> Self { |     fn from(byte: u8) -> Self { | ||||||
|         match byte & 0b01 { |         match byte & 0b01 { | ||||||
| @@ -69,12 +66,6 @@ enum ClockSpeed { | |||||||
|     Fast = 1, |     Fast = 1, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Default for ClockSpeed { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Self::Normal |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl From<u8> for ClockSpeed { | impl From<u8> for ClockSpeed { | ||||||
|     fn from(byte: u8) -> Self { |     fn from(byte: u8) -> Self { | ||||||
|         match byte & 0b01 { |         match byte & 0b01 { | ||||||
|   | |||||||
							
								
								
									
										39
									
								
								src/timer.rs
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								src/timer.rs
									
									
									
									
									
								
							| @@ -21,17 +21,6 @@ impl Timer { | |||||||
|         use State::*; |         use State::*; | ||||||
|         use TimerSpeed::*; |         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); |         self.divider = self.divider.wrapping_add(1); | ||||||
|  |  | ||||||
|         // Get Bit Position |         // Get Bit Position | ||||||
| @@ -53,6 +42,17 @@ impl Timer { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         self.and_result = Some(new_result); |         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 |     /// 0xFF05 | TIMA - Timer Counter | ||||||
| @@ -70,7 +70,7 @@ impl Timer { | |||||||
|                 self.counter = byte; |                 self.counter = byte; | ||||||
|                 self.state = AbortedTIMAOverflow(step); |                 self.state = AbortedTIMAOverflow(step); | ||||||
|             } |             } | ||||||
|             LoadTMA => {} |             LoadTIMA => { /* Ignored */ } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -95,9 +95,8 @@ impl Timer { | |||||||
|         use State::*; |         use State::*; | ||||||
|  |  | ||||||
|         self.state = match self.state { |         self.state = match self.state { | ||||||
|             Normal | LoadTMA => Normal, |             Normal | LoadTIMA | AbortedTIMAOverflow(3) => Normal, | ||||||
|             AbortedTIMAOverflow(4) => Normal, |             TIMAOverflow(3) => LoadTIMA, | ||||||
|             TIMAOverflow(4) => LoadTMA, |  | ||||||
|             AbortedTIMAOverflow(step) => AbortedTIMAOverflow(step + 1), |             AbortedTIMAOverflow(step) => AbortedTIMAOverflow(step + 1), | ||||||
|             TIMAOverflow(step) => TIMAOverflow(step + 1), |             TIMAOverflow(step) => TIMAOverflow(step + 1), | ||||||
|         } |         } | ||||||
| @@ -107,7 +106,7 @@ impl Timer { | |||||||
| impl Default for Timer { | impl Default for Timer { | ||||||
|     fn default() -> Self { |     fn default() -> Self { | ||||||
|         Self { |         Self { | ||||||
|             ctrl: Default::default(), |             ctrl: TimerControl(0), | ||||||
|             counter: 0, |             counter: 0, | ||||||
|             modulo: 0, |             modulo: 0, | ||||||
|             divider: 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 { | impl From<u8> for TimerControl { | ||||||
|     fn from(byte: u8) -> Self { |     fn from(byte: u8) -> Self { | ||||||
|         Self(byte) |         Self(byte) | ||||||
| @@ -181,5 +174,5 @@ enum State { | |||||||
|     TIMAOverflow(u8), |     TIMAOverflow(u8), | ||||||
|     AbortedTIMAOverflow(u8), |     AbortedTIMAOverflow(u8), | ||||||
|     Normal, |     Normal, | ||||||
|     LoadTMA, |     LoadTIMA, | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,13 +11,13 @@ pub(crate) struct WorkRam { | |||||||
| } | } | ||||||
|  |  | ||||||
| impl BusIo for 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 { |     fn read_byte(&self, addr: u16) -> u8 { | ||||||
|         self.bank[addr as usize - WORK_RAM_START_ADDRESS] |         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 { | impl Default for WorkRam { | ||||||
| @@ -42,11 +42,11 @@ impl Default for VariableWorkRam { | |||||||
| } | } | ||||||
|  |  | ||||||
| impl BusIo 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 { |     fn read_byte(&self, addr: u16) -> u8 { | ||||||
|         self.buf[addr as usize - VARIABLE_WORK_RAM_START_ADDRESS] |         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