Compare commits
	
		
			180 Commits
		
	
	
		
			192bdffd64
			...
			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 | |||
| 01064bab69 | |||
| 634bc2d2c0 | |||
| d794a94b68 | |||
| b87e31d3f4 | |||
| 3c2456611e | |||
| b829f05a34 | |||
| afd2d16371 | |||
| 1f8fa48168 | |||
| c6fbb79189 | |||
| 8b78b2943e | |||
| 0af95a1dd3 | |||
| 9fa40f8584 | |||
| b10bc7b4fd | |||
| 4658a0d106 | |||
| f92b9d61ef | |||
| 360a9a7b65 | |||
| 5d64e539a7 | |||
| 22f96a10e7 | |||
| 8fea8eb1ff | |||
| 9b2c91445a | |||
| b9046bceba | |||
| aa22e93049 | |||
| 6215eccb2f | |||
| a77d0a0f62 | |||
| c2f2e2194b | |||
| d68257bb29 | |||
| e27d6dc25b | |||
| 1acb5de19d | |||
| 1b78b248a3 | |||
| 5d6df46a2d | |||
| 7e65d82fef | |||
| 8c9567b610 | |||
| 53dfaf0de2 | |||
| 16c2dd81fc | |||
| 79be38a1e6 | |||
| 8625bec059 | |||
| 0637b771e3 | |||
| 0107fa04c9 | |||
| 6265c8af04 | |||
| 5482a8e75f | |||
| 002dae6826 | |||
| c863dc835c | |||
| d4407cf849 | |||
| de0d147685 | |||
| dd8339e8de | |||
| 05d6475015 | |||
| 32b597a328 | |||
| 33be2e0e83 | |||
| 832e1b7633 | |||
| a549b9feef | |||
| 4d6fc95130 | |||
| 7112cd15e3 | |||
| 2af673d7c5 | |||
| e43cac8041 | |||
| 8724da824e | |||
| 903cfacad3 | |||
| 9d0e099a97 | |||
| d9a3a7b0dd | |||
| 1a4546f4a4 | |||
| bd685a99a4 | |||
| 31abd6dc5c | |||
| c7e3cb5b35 | |||
| e4d77d66ba | |||
| 9e36e86c14 | |||
| 6f6c308d84 | |||
| f429d72882 | |||
| 4fda7587ce | |||
| 3fb182b9cb | |||
| c1430594cf | |||
| 8e2ef58b81 | |||
| b4b84d0cc5 | |||
| 1d8fb8d32a | |||
| cc0cb0dd8a | |||
| fd9a5b51ba | |||
| b9a798d0ad | |||
| a1eadc0bc4 | |||
| ea53f97312 | |||
| 08bd33b4d4 | |||
| 2b5926e533 | |||
| 1e27875dc3 | |||
| 1566d60fd4 | |||
| ab99e24f64 | 
							
								
								
									
										9
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| { | ||||
|     "recommendations": [ | ||||
|         "rust-lang.rust-analyzer", | ||||
|         "tamasfe.even-better-toml", | ||||
|         "serayuzgur.crates", | ||||
|         "vadimcn.vscode-lldb", | ||||
|         "donaldhays.rgbds-z80" | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										2937
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2937
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										30
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								Cargo.toml
									
									
									
									
									
								
							| @@ -2,23 +2,27 @@ | ||||
| name = "gb" | ||||
| version = "0.1.0" | ||||
| authors = ["Rekai Musuka <rekai@musuka.dev>"] | ||||
| edition = "2018" | ||||
| edition = "2021" | ||||
|  | ||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
|  | ||||
| [dependencies] | ||||
| anyhow = "^1.0" | ||||
| bitfield = "^0.13" | ||||
| clap = "^2.33" | ||||
| gilrs = "^0.8" | ||||
| pixels = "^0.3" | ||||
| winit = "^0.24" | ||||
| winit_input_helper = "^0.9" | ||||
| egui = "^0.10" | ||||
| egui_wgpu_backend = { git="https://github.com/hasenbanck/egui_wgpu_backend.git", rev="9d03ad345d15d1e44165849b242d3562fdf3e859" } | ||||
| egui_winit_platform = { git="https://github.com/hasenbanck/egui_winit_platform.git", rev="17298250e9721e8bf2c1d4a17b3e22777f8cb2e8" } | ||||
| rodio = "^0.14" | ||||
| crossbeam-channel = "^0.5" | ||||
| anyhow = "1.0" | ||||
| bitfield = "0.17" | ||||
| clap = { version = "4.0", features = ["cargo"] } | ||||
| gilrs = "0.11" | ||||
| winit = "0.28" | ||||
| egui = "0.21" | ||||
| egui_wgpu_backend = "0.22" | ||||
| egui_winit_platform = "0.18" | ||||
| pollster = "0.3" | ||||
| rodio = "0.19" | ||||
| rtrb = "0.3" | ||||
| directories-next = "2.0" | ||||
| tracing = "0.1" | ||||
| tracing-subscriber = { version = "0.3", features = ["std", "env-filter"] } | ||||
| thiserror = "1.0" | ||||
| once_cell = "1.10" | ||||
|  | ||||
| [profile.release] | ||||
| debug = true | ||||
|   | ||||
							
								
								
									
										47
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,14 +1,43 @@ | ||||
| # Rekai's Gameboy Emulator | ||||
| [](https://ci.paoda.moe/paoda/gb) | ||||
|  | ||||
| ### Status | ||||
| * Passes Blargg's cpu_instrs Test | ||||
| * Renders Background & Window Tiles  | ||||
| * Implements a PPU FIFO | ||||
| * From [Blargg Test ROMs](https://github.com/L-P/blargg-test-roms/) | ||||
|     * [x] cpu_instrs | ||||
|     * [x] instr_timing | ||||
|     * [x] mem_timing | ||||
|     * [x] mem_timing-2 | ||||
|     * [ ] dmg_sound (partial)  | ||||
| * [x] [dmg-acid2](https://github.com/mattcurrie/dmg-acid2) | ||||
| * From [mooneye-gb](https://github.com/Gekkio/mooneye-gb): | ||||
|     * Cartridges: | ||||
|         * [x] MBC1 | ||||
|         * [ ] MBC1M | ||||
|         * [x] MBC2 | ||||
|         * [x] MBC5 | ||||
| * Implements a cycle-accurate PPU FIFO | ||||
|     * Doesn't \*exactly\* work just yet | ||||
|  | ||||
| Supports: ROM-only, MBC1, MBC2, MBC3 and MBC5 games. | ||||
|  | ||||
|  | ||||
| ### Notes | ||||
| * [gameboy-logs](https://github.com/wheremyfoodat/Gameboy-logs) suggests that there are still some underlying problems with the cpu implementation | ||||
| * The Sprite FIFO does not work as expected yet | ||||
| * Sound is neither emulated nor stubbed. Upon writing / reading to a APU related register the emulator will panic.  | ||||
| * Code cleanup is pending completion of some minimum viable product of the emulator | ||||
| ### Compiling | ||||
| This project was last successfully built on [Rust 1.64.0](https://github.com/rust-lang/rust/tree/1.64.0) | ||||
|  | ||||
| 1. `git clone https://github.com/paoda/gb` | ||||
| 2. `cd gb` | ||||
| 3. `cargo run --release` | ||||
|  | ||||
| ### Controls | ||||
| Controls are defined [here](https://github.com/paoda/gb/blob/85940c874460b9cb31bf9b8211bf7dda596d114a/src/joypad.rs#L114) | ||||
|  | ||||
| Key | Button | ||||
| --- | --- | ||||
| <kbd>X</kbd> | B | ||||
| <kbd>Z</kbd> | A | ||||
| <kbd>Enter</kbd> | START | ||||
| <kbd>Shift</kbd> | SELECT | ||||
|  | ||||
| Then use the Arrow keys for the D-Pad | ||||
|  | ||||
| ### Credits | ||||
| The Boot ROM found in the `bin/` directory was made by [Optix](https://github.com/Hacktix) over [here](https://github.com/Hacktix/Bootix) | ||||
							
								
								
									
										
											BIN
										
									
								
								bin/bootix_dmg.bin
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								bin/bootix_dmg.bin
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1046
									
								
								src/apu.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1046
									
								
								src/apu.rs
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										80
									
								
								src/apu/gen.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/apu/gen.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| use rodio::Source; | ||||
| use rtrb::{Consumer, Producer, PushError, RingBuffer}; | ||||
|  | ||||
| pub(crate) const SAMPLE_RATE: u32 = 48000; // Hz | ||||
| const CHANNEL_COUNT: usize = 2; | ||||
| const BUFFER_CAPACITY: usize = 2048 * CHANNEL_COUNT; // # of samples * the # of channels | ||||
|  | ||||
| pub fn init<T>() -> (SampleProducer<T>, SampleConsumer<T>) { | ||||
|     let (prod, cons) = RingBuffer::new(BUFFER_CAPACITY); | ||||
|     ( | ||||
|         SampleProducer { inner: prod }, | ||||
|         SampleConsumer { inner: cons }, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| pub struct SampleProducer<T> { | ||||
|     inner: Producer<T>, | ||||
| } | ||||
|  | ||||
| impl<T> SampleProducer<T> { | ||||
|     pub(crate) fn push(&mut self, value: T) -> Result<(), PushError<T>> { | ||||
|         self.inner.push(value) | ||||
|     } | ||||
|  | ||||
|     #[allow(dead_code)] | ||||
|     pub(crate) fn available(&self) -> bool { | ||||
|         self.inner.slots() > 2 | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn available_blocking(&self) -> bool { | ||||
|         loop { | ||||
|             if self.inner.slots() > 2 { | ||||
|                 break true; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<T> std::fmt::Debug for SampleProducer<T> { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         f.debug_struct(&format!("SampleProducer<{}>", std::any::type_name::<T>())) | ||||
|             .finish_non_exhaustive() | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct SampleConsumer<T> { | ||||
|     inner: Consumer<T>, | ||||
| } | ||||
|  | ||||
| impl Iterator for SampleConsumer<f32> { | ||||
|     type Item = f32; | ||||
|  | ||||
|     fn next(&mut self) -> Option<Self::Item> { | ||||
|         // As of 2021-07-28, PopError can only be Empty | ||||
|         Some(self.inner.pop().unwrap_or_default()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Source for SampleConsumer<f32> { | ||||
|     fn current_frame_len(&self) -> Option<usize> { | ||||
|         // A frame changes when the samples rate or | ||||
|         // number of channels change. This will never happen, so | ||||
|         // we return | ||||
|         None | ||||
|     } | ||||
|  | ||||
|     fn channels(&self) -> u16 { | ||||
|         // The Gameboy supports two channels | ||||
|         CHANNEL_COUNT as u16 | ||||
|     } | ||||
|  | ||||
|     fn sample_rate(&self) -> u32 { | ||||
|         SAMPLE_RATE | ||||
|     } | ||||
|  | ||||
|     fn total_duration(&self) -> Option<std::time::Duration> { | ||||
|         // The duration of this source is infinite | ||||
|         None | ||||
|     } | ||||
| } | ||||
							
								
								
									
										582
									
								
								src/apu/types.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										582
									
								
								src/apu/types.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,582 @@ | ||||
| use bitfield::bitfield; | ||||
|  | ||||
| pub(crate) trait NRx4 { | ||||
|     fn trigger(&self) -> bool; | ||||
|     fn length_enable(&self) -> bool; | ||||
|     fn set_length_enable(&mut self, value: bool); | ||||
| } | ||||
|  | ||||
| pub(crate) mod ch1 { | ||||
|     use super::bitfield; | ||||
|  | ||||
|     bitfield! { | ||||
|         pub struct Sweep(u8); | ||||
|         impl Debug; | ||||
|         _period, _: 6, 4; | ||||
|         from into SweepDirection, _direction, _: 3, 3; | ||||
|         _shift_count, _: 2, 0; | ||||
|     } | ||||
|  | ||||
|     impl Sweep { | ||||
|         pub(crate) fn period(&self) -> u8 { | ||||
|             self._period() | ||||
|         } | ||||
|  | ||||
|         pub(crate) fn shift_count(&self) -> u8 { | ||||
|             self._shift_count() | ||||
|         } | ||||
|  | ||||
|         pub(crate) fn direction(&self) -> SweepDirection { | ||||
|             self._direction() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl Copy for Sweep {} | ||||
|     impl Clone for Sweep { | ||||
|         fn clone(&self) -> Self { | ||||
|             *self | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl Default for Sweep { | ||||
|         fn default() -> Self { | ||||
|             Self(0) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl From<u8> for Sweep { | ||||
|         fn from(byte: u8) -> Self { | ||||
|             Self(byte) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl From<Sweep> for u8 { | ||||
|         fn from(sweep: Sweep) -> Self { | ||||
|             sweep.0 | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[derive(Debug, Clone, Copy)] | ||||
|     pub(crate) enum SweepDirection { | ||||
|         Increase = 0, | ||||
|         Decrease = 1, | ||||
|     } | ||||
|  | ||||
|     impl From<u8> for SweepDirection { | ||||
|         fn from(byte: u8) -> Self { | ||||
|             match byte & 0x01 { | ||||
|                 0b00 => Self::Increase, | ||||
|                 0b01 => Self::Decrease, | ||||
|                 _ => unreachable!("{:04X} is not a valid value for SweepChange", byte), | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl From<SweepDirection> for u8 { | ||||
|         fn from(change: SweepDirection) -> Self { | ||||
|             change as u8 | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub(super) mod ch3 { | ||||
|     #[derive(Debug, Clone, Copy)] | ||||
|     pub(crate) enum Volume { | ||||
|         Mute = 0, | ||||
|         Full = 1, | ||||
|         Half = 2, | ||||
|         Quarter = 3, | ||||
|     } | ||||
|  | ||||
|     impl Volume { | ||||
|         pub(crate) fn shift_count(&self) -> u8 { | ||||
|             use Volume::*; | ||||
|  | ||||
|             match *self { | ||||
|                 Mute => 4, | ||||
|                 Full => 0, | ||||
|                 Half => 1, | ||||
|                 Quarter => 2, | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl Default for Volume { | ||||
|         fn default() -> Self { | ||||
|             Self::Mute | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub(super) mod ch4 { | ||||
|     use super::{bitfield, NRx4}; | ||||
|  | ||||
|     bitfield! { | ||||
|         pub struct PolynomialCounter(u8); | ||||
|         impl Debug; | ||||
|         _shift_count, _: 7, 4; | ||||
|         from into CounterWidth, _counter_width, _: 3, 3; | ||||
|         _divisor_code, _: 2, 0; | ||||
|     } | ||||
|  | ||||
|     impl PolynomialCounter { | ||||
|         pub(crate) fn divisor_code(&self) -> u8 { | ||||
|             self._divisor_code() | ||||
|         } | ||||
|  | ||||
|         pub(crate) fn counter_width(&self) -> CounterWidth { | ||||
|             self._counter_width() | ||||
|         } | ||||
|  | ||||
|         pub(crate) fn shift_count(&self) -> u8 { | ||||
|             self._shift_count() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl Copy for PolynomialCounter {} | ||||
|     impl Clone for PolynomialCounter { | ||||
|         fn clone(&self) -> Self { | ||||
|             *self | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl Default for PolynomialCounter { | ||||
|         fn default() -> Self { | ||||
|             Self(0) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl From<u8> for PolynomialCounter { | ||||
|         fn from(byte: u8) -> Self { | ||||
|             Self(byte) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl From<PolynomialCounter> for u8 { | ||||
|         fn from(poly: PolynomialCounter) -> Self { | ||||
|             poly.0 | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[derive(Debug, Clone, Copy)] | ||||
|     pub(crate) enum CounterWidth { | ||||
|         Long,  // 15 bits long | ||||
|         Short, // 7 bits long | ||||
|     } | ||||
|  | ||||
|     impl From<u8> for CounterWidth { | ||||
|         fn from(byte: u8) -> Self { | ||||
|             match byte & 0x01 { | ||||
|                 0b00 => Self::Short, | ||||
|                 0b01 => Self::Long, | ||||
|                 _ => unreachable!("{:#04X} is not a valid value for CounterWidth", byte), | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl From<CounterWidth> for u8 { | ||||
|         fn from(counter_width: CounterWidth) -> Self { | ||||
|             counter_width as u8 | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     bitfield! { | ||||
|         pub struct Frequency(u8); | ||||
|         impl Debug; | ||||
|         _trigger, _: 7; | ||||
|         _length_enable, _set_length_enable: 6; | ||||
|     } | ||||
|  | ||||
|     impl NRx4 for Frequency { | ||||
|         fn trigger(&self) -> bool { | ||||
|             self._trigger() | ||||
|         } | ||||
|  | ||||
|         fn length_enable(&self) -> bool { | ||||
|             self._length_enable() | ||||
|         } | ||||
|  | ||||
|         fn set_length_enable(&mut self, value: bool) { | ||||
|             self._set_length_enable(value); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl Copy for Frequency {} | ||||
|     impl Clone for Frequency { | ||||
|         fn clone(&self) -> Self { | ||||
|             *self | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl Default for Frequency { | ||||
|         fn default() -> Self { | ||||
|             Self(0) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl From<u8> for Frequency { | ||||
|         fn from(byte: u8) -> Self { | ||||
|             Self(byte & 0xC0) // Only bits 7 and 6 hold anything of value | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl From<Frequency> for u8 { | ||||
|         fn from(state: Frequency) -> Self { | ||||
|             state.0 & 0x40 // Only bit 6 holds anything of value | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub(super) mod common { | ||||
|     use super::{bitfield, NRx4}; | ||||
|  | ||||
|     bitfield! { | ||||
|         pub struct FrequencyHigh(u8); | ||||
|         impl Debug; | ||||
|         _trigger, _: 7; | ||||
|         _length_enable, _set_length_enable: 6; | ||||
|         pub freq_bits, set_freq_bits: 2, 0; | ||||
|     } | ||||
|  | ||||
|     impl NRx4 for FrequencyHigh { | ||||
|         fn trigger(&self) -> bool { | ||||
|             self._trigger() | ||||
|         } | ||||
|  | ||||
|         fn length_enable(&self) -> bool { | ||||
|             self._length_enable() | ||||
|         } | ||||
|  | ||||
|         fn set_length_enable(&mut self, value: bool) { | ||||
|             self._set_length_enable(value); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl Copy for FrequencyHigh {} | ||||
|     impl Clone for FrequencyHigh { | ||||
|         fn clone(&self) -> Self { | ||||
|             *self | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl Default for FrequencyHigh { | ||||
|         fn default() -> Self { | ||||
|             Self(0) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl From<u8> for FrequencyHigh { | ||||
|         fn from(byte: u8) -> Self { | ||||
|             Self(byte) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl From<FrequencyHigh> for u8 { | ||||
|         fn from(freq: FrequencyHigh) -> Self { | ||||
|             freq.0 & 0x40 // Only bit 6 can be read | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     bitfield! { | ||||
|         pub struct VolumeEnvelope(u8); | ||||
|         impl Debug; | ||||
|         _init_vol, _: 7, 4; | ||||
|         from into EnvelopeDirection, _direction, _: 3, 3; | ||||
|         _period, _: 2, 0; | ||||
|     } | ||||
|  | ||||
|     impl VolumeEnvelope { | ||||
|         pub(crate) fn init_vol(&self) -> u8 { | ||||
|             self._init_vol() | ||||
|         } | ||||
|  | ||||
|         pub(crate) fn direction(&self) -> EnvelopeDirection { | ||||
|             self._direction() | ||||
|         } | ||||
|  | ||||
|         pub(crate) fn period(&self) -> u8 { | ||||
|             self._period() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl Copy for VolumeEnvelope {} | ||||
|     impl Clone for VolumeEnvelope { | ||||
|         fn clone(&self) -> Self { | ||||
|             *self | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl Default for VolumeEnvelope { | ||||
|         fn default() -> Self { | ||||
|             Self(0) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl From<u8> for VolumeEnvelope { | ||||
|         fn from(byte: u8) -> Self { | ||||
|             Self(byte) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl From<VolumeEnvelope> for u8 { | ||||
|         fn from(envelope: VolumeEnvelope) -> Self { | ||||
|             envelope.0 | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[derive(Debug, Clone, Copy)] | ||||
|     pub(crate) enum EnvelopeDirection { | ||||
|         Decrease = 0, | ||||
|         Increase = 1, | ||||
|     } | ||||
|  | ||||
|     impl From<u8> for EnvelopeDirection { | ||||
|         fn from(byte: u8) -> Self { | ||||
|             match byte & 0b01 { | ||||
|                 0b00 => Self::Decrease, | ||||
|                 0b01 => Self::Increase, | ||||
|                 _ => unreachable!("{:#04X} is not a valid value for EnvelopeDirection", byte), | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl From<EnvelopeDirection> for u8 { | ||||
|         fn from(direction: EnvelopeDirection) -> Self { | ||||
|             direction as u8 | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl Default for EnvelopeDirection { | ||||
|         fn default() -> Self { | ||||
|             Self::Decrease | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     bitfield! { | ||||
|         pub struct SoundDuty(u8); | ||||
|         impl Debug; | ||||
|         from into WavePattern, _wave_pattern, _: 7, 6; | ||||
|         _sound_length, _: 5, 0; | ||||
|     } | ||||
|  | ||||
|     impl SoundDuty { | ||||
|         pub(crate) fn wave_pattern(&self) -> WavePattern { | ||||
|             self._wave_pattern() | ||||
|         } | ||||
|  | ||||
|         pub(crate) fn sound_length(&self) -> u8 { | ||||
|             self._sound_length() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl Copy for SoundDuty {} | ||||
|     impl Clone for SoundDuty { | ||||
|         fn clone(&self) -> Self { | ||||
|             *self | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl Default for SoundDuty { | ||||
|         fn default() -> Self { | ||||
|             Self(0) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl From<u8> for SoundDuty { | ||||
|         fn from(byte: u8) -> Self { | ||||
|             Self(byte) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl From<SoundDuty> for u8 { | ||||
|         fn from(duty: SoundDuty) -> Self { | ||||
|             duty.0 & 0xC0 // Only bits 7 and 6 can be read | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[derive(Debug, Clone, Copy)] | ||||
|     pub(crate) enum WavePattern { | ||||
|         OneEighth = 0,     // 12.5% ( _-------_-------_------- ) | ||||
|         OneQuarter = 1,    // 25%   ( __------__------__------ ) | ||||
|         OneHalf = 2,       // 50%   ( ____----____----____---- ) (normal) | ||||
|         ThreeQuarters = 3, // 75%   ( ______--______--______-- ) | ||||
|     } | ||||
|  | ||||
|     impl WavePattern { | ||||
|         pub(crate) const fn amplitude(&self, index: u8) -> u8 { | ||||
|             use WavePattern::*; | ||||
|             let i = 7 - index; // an index of 0 should get the highest bit | ||||
|  | ||||
|             match *self { | ||||
|                 OneEighth => (0b00000001 >> i) & 0x01, | ||||
|                 OneQuarter => (0b10000001 >> i) & 0x01, | ||||
|                 OneHalf => (0b10000111 >> i) & 0x01, | ||||
|                 ThreeQuarters => (0b01111110 >> i) & 0x01, | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl From<WavePattern> for u8 { | ||||
|         fn from(pattern: WavePattern) -> Self { | ||||
|             pattern as Self | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl From<u8> for WavePattern { | ||||
|         fn from(byte: u8) -> Self { | ||||
|             match byte & 0b11 { | ||||
|                 0b00 => Self::OneEighth, | ||||
|                 0b01 => Self::OneQuarter, | ||||
|                 0b10 => Self::OneHalf, | ||||
|                 0b11 => Self::ThreeQuarters, | ||||
|                 _ => unreachable!("{:#04X} is not a valid value for WavePattern", byte), | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| bitfield! { | ||||
|     pub struct SoundOutput(u8); | ||||
|     impl Debug; | ||||
|     pub ch4_left, _: 7; | ||||
|     pub ch3_left, _: 6; | ||||
|     pub ch2_left, _: 5; | ||||
|     pub ch1_left, _: 4; | ||||
|     pub ch4_right, _: 3; | ||||
|     pub ch3_right, _: 2; | ||||
|     pub ch2_right, _: 1; | ||||
|     pub ch1_right, _: 0; | ||||
| } | ||||
|  | ||||
| impl SoundOutput { | ||||
|     pub(super) fn ch1(&self) -> (bool, bool) { | ||||
|         (self.ch1_left(), self.ch1_right()) | ||||
|     } | ||||
|  | ||||
|     pub(super) fn ch2(&self) -> (bool, bool) { | ||||
|         (self.ch2_left(), self.ch2_right()) | ||||
|     } | ||||
|  | ||||
|     pub(super) fn ch3(&self) -> (bool, bool) { | ||||
|         (self.ch3_left(), self.ch3_right()) | ||||
|     } | ||||
|  | ||||
|     pub(super) fn ch4(&self) -> (bool, bool) { | ||||
|         (self.ch4_left(), self.ch4_right()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Copy for SoundOutput {} | ||||
| impl Clone for SoundOutput { | ||||
|     fn clone(&self) -> Self { | ||||
|         *self | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for SoundOutput { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         Self(byte) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<SoundOutput> for u8 { | ||||
|     fn from(output: SoundOutput) -> Self { | ||||
|         output.0 | ||||
|     } | ||||
| } | ||||
|  | ||||
| bitfield! { | ||||
|     pub struct ChannelControl(u8); | ||||
|     impl Debug; | ||||
|     vin_left, _: 7; | ||||
|     _left_volume, _: 6, 4; | ||||
|     vin_right, _: 3; | ||||
|     _right_volume, _: 2, 0; | ||||
| } | ||||
|  | ||||
| impl ChannelControl { | ||||
|     pub(crate) fn left_volume(&self) -> f32 { | ||||
|         self._left_volume() as f32 | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn right_volume(&self) -> f32 { | ||||
|         self._right_volume() as f32 | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Copy for ChannelControl {} | ||||
| impl Clone for ChannelControl { | ||||
|     fn clone(&self) -> Self { | ||||
|         *self | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for ChannelControl { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         Self(byte) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<ChannelControl> for u8 { | ||||
|     fn from(ctrl: ChannelControl) -> Self { | ||||
|         ctrl.0 | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub(super) mod fs { | ||||
|     #[derive(Debug)] | ||||
|     pub(crate) struct FrameSequencer { | ||||
|         step: u8, | ||||
|         state: State, | ||||
|     } | ||||
|  | ||||
|     impl Default for FrameSequencer { | ||||
|         fn default() -> Self { | ||||
|             Self { | ||||
|                 step: Default::default(), | ||||
|                 state: State::Length, | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     impl FrameSequencer { | ||||
|         pub(crate) fn next(&mut self) { | ||||
|             use State::*; | ||||
|  | ||||
|             self.step = (self.step + 1) % 8; | ||||
|             self.state = match self.step { | ||||
|                 1 | 3 | 5 => Nothing, | ||||
|                 0 | 4 => Length, | ||||
|                 2 | 6 => LengthAndSweep, | ||||
|                 7 => Envelope, | ||||
|                 _ => unreachable!("Step {} is invalid for the Frame Sequencer", self.step), | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         pub(crate) fn next_clocks_length(&self) -> bool { | ||||
|             use State::*; | ||||
|  | ||||
|             match self.state { | ||||
|                 Length | LengthAndSweep => true, | ||||
|                 Nothing | Envelope => false, | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         pub(crate) fn state(&self) -> State { | ||||
|             self.state | ||||
|         } | ||||
|  | ||||
|         pub(crate) fn reset(&mut self) { | ||||
|             self.step = Default::default(); | ||||
|             self.state = State::Length; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||||
|     pub(crate) enum State { | ||||
|         Length, | ||||
|         Nothing, | ||||
|         LengthAndSweep, | ||||
|         Envelope, | ||||
|     } | ||||
| } | ||||
							
								
								
									
										202
									
								
								src/bus.rs
									
									
									
									
									
								
							
							
						
						
									
										202
									
								
								src/bus.rs
									
									
									
									
									
								
							| @@ -1,88 +1,64 @@ | ||||
| use crate::apu::Apu; | ||||
| use crate::cartridge::Cartridge; | ||||
| use crate::high_ram::HighRam; | ||||
| use crate::interrupt::{Interrupt, InterruptFlag}; | ||||
| use crate::joypad::Joypad; | ||||
| use crate::ppu::{Ppu, PpuMode}; | ||||
| use crate::serial::Serial; | ||||
| use crate::sound::{AudioSender, Sound}; | ||||
| use crate::timer::Timer; | ||||
| use crate::work_ram::{VariableWorkRam, WorkRam}; | ||||
| use std::{fs::File, io::Read}; | ||||
|  | ||||
| const BOOT_ROM_SIZE: usize = 0x100; | ||||
| pub(crate) const BOOT_SIZE: usize = 0x100; | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| #[derive(Debug, Default)] | ||||
| pub struct Bus { | ||||
|     boot: Option<[u8; BOOT_ROM_SIZE]>, // Boot ROM is 256b long | ||||
|     cartridge: Option<Cartridge>, | ||||
|     pub ppu: Ppu, | ||||
|     boot: Option<[u8; BOOT_SIZE]>, // Boot ROM is 256b long | ||||
|     pub(crate) cart: Option<Cartridge>, | ||||
|     pub(crate) ppu: Ppu, | ||||
|     work_ram: WorkRam, | ||||
|     var_ram: VariableWorkRam, | ||||
|     pub(crate) timer: Timer, | ||||
|     timer: Timer, | ||||
|     int: Interrupt, | ||||
|     snd: Sound, | ||||
|     pub(crate) apu: Apu, | ||||
|     high_ram: HighRam, | ||||
|     serial: Serial, | ||||
|     pub(crate) joypad: Joypad, | ||||
| } | ||||
|  | ||||
| impl Default for Bus { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             boot: None, | ||||
|             cartridge: None, | ||||
|             ppu: Default::default(), | ||||
|             work_ram: Default::default(), | ||||
|             var_ram: Default::default(), | ||||
|             timer: Default::default(), | ||||
|             int: Default::default(), | ||||
|             snd: Default::default(), | ||||
|             high_ram: Default::default(), | ||||
|             serial: Default::default(), | ||||
|             joypad: Default::default(), | ||||
|         } | ||||
|     } | ||||
|     pub(crate) joyp: Joypad, | ||||
| } | ||||
|  | ||||
| impl Bus { | ||||
|     pub(crate) fn with_boot(path: &str) -> anyhow::Result<Self> { | ||||
|         let mut file = File::open(path)?; | ||||
|         let mut boot_rom = [0u8; 256]; | ||||
|  | ||||
|         file.read_exact(&mut boot_rom)?; | ||||
|  | ||||
|         Ok(Self { | ||||
|             boot: Some(boot_rom), | ||||
|     pub(crate) fn with_boot(rom: [u8; 256]) -> Self { | ||||
|         Self { | ||||
|             boot: Some(rom), | ||||
|             ..Default::default() | ||||
|         }) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn load_cartridge(&mut self, path: &str) -> std::io::Result<()> { | ||||
|         self.cartridge = Some(Cartridge::new(path)?); | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn rom_title(&self) -> Option<&str> { | ||||
|         self.cartridge.as_ref()?.title() | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn pass_audio_src(&mut self, sender: AudioSender) { | ||||
|         self.snd.set_audio_src(sender) | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn is_mpsc_still_full(&mut self) -> bool { | ||||
|         self.snd.is_mpsc_still_full() | ||||
|     #[allow(dead_code)] | ||||
|     pub(crate) fn boot_mapped(&self) -> bool { | ||||
|         self.boot.is_some() | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     pub(crate) fn clock(&mut self) { | ||||
|         self.ppu.clock(); | ||||
|         self.timer.clock(); | ||||
|         self.snd.clock(self.timer.divider); | ||||
|         self.clock_dma(); | ||||
|         self.tick(4); | ||||
|     } | ||||
|  | ||||
|     fn clock_dma(&mut self) { | ||||
|         if let Some((src_addr, dest_addr)) = self.ppu.dma.clock() { | ||||
|     #[inline] | ||||
|     fn tick(&mut self, limit: u8) { | ||||
|         for _ in 0..limit { | ||||
|             self.timer.tick(); | ||||
|  | ||||
|             if let Some(c) = self.cart.as_mut() { | ||||
|                 c.tick() | ||||
|             } | ||||
|             self.ppu.tick(); | ||||
|             self.apu.tick(self.timer.divider); | ||||
|             self.dma_tick() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn dma_tick(&mut self) { | ||||
|         if let Some((src_addr, dest_addr)) = self.ppu.dma.tick() { | ||||
|             let byte = self.oam_read_byte(src_addr); | ||||
|             self.oam_write_byte(dest_addr, byte); | ||||
|         } | ||||
| @@ -90,7 +66,7 @@ impl Bus { | ||||
| } | ||||
|  | ||||
| impl Bus { | ||||
|     pub fn oam_read_byte(&self, addr: u16) -> u8 { | ||||
|     pub(crate) fn oam_read_byte(&self, addr: u16) -> u8 { | ||||
|         match addr { | ||||
|             0x0000..=0x7FFF => { | ||||
|                 // 16KB ROM bank 00 (ends at 0x3FFF) | ||||
| @@ -101,16 +77,16 @@ impl Bus { | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 match self.cartridge.as_ref() { | ||||
|                 match self.cart.as_ref() { | ||||
|                     Some(cart) => cart.read_byte(addr), | ||||
|                     None => panic!("Tried to read from a non-existent cartridge"), | ||||
|                     None => 0xFF, | ||||
|                 } | ||||
|             } | ||||
|             0x8000..=0x9FFF => self.ppu.read_byte(addr), // 8KB Video RAM | ||||
|             0xA000..=0xBFFF => match self.cartridge.as_ref() { | ||||
|             0xA000..=0xBFFF => match self.cart.as_ref() { | ||||
|                 // 8KB External RAM | ||||
|                 Some(cart) => cart.read_byte(addr), | ||||
|                 None => panic!("Tried to read from a non-existent cartridge"), | ||||
|                 None => 0xFF, | ||||
|             }, | ||||
|             0xC000..=0xCFFF => self.work_ram.read_byte(addr), // 4KB Work RAM Bank 0 | ||||
|             0xD000..=0xDFFF => self.var_ram.read_byte(addr),  // 4KB Work RAM Bank 1 -> N | ||||
| @@ -137,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); | ||||
|     } | ||||
| } | ||||
| @@ -154,9 +130,9 @@ impl BusIo for Bus { | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 match self.cartridge.as_ref() { | ||||
|                 match self.cart.as_ref() { | ||||
|                     Some(cart) => cart.read_byte(addr), | ||||
|                     None => panic!("Tried to read from a non-existent cartridge"), | ||||
|                     None => 0xFF, | ||||
|                 } | ||||
|             } | ||||
|             0x8000..=0x9FFF => { | ||||
| @@ -166,10 +142,10 @@ impl BusIo for Bus { | ||||
|                     _ => self.ppu.read_byte(addr), | ||||
|                 } | ||||
|             } | ||||
|             0xA000..=0xBFFF => match self.cartridge.as_ref() { | ||||
|             0xA000..=0xBFFF => match self.cart.as_ref() { | ||||
|                 // 8KB External RAM | ||||
|                 Some(cart) => cart.read_byte(addr), | ||||
|                 None => panic!("Tried to read from a non-existent cartridge"), | ||||
|                 None => 0xFF, | ||||
|             }, | ||||
|             0xC000..=0xCFFF => self.work_ram.read_byte(addr), // 4KB Work RAM Bank 0 | ||||
|             0xD000..=0xDFFF => self.var_ram.read_byte(addr),  // 4KB Work RAM Bank 1 -> N | ||||
| @@ -216,33 +192,15 @@ impl BusIo for Bus { | ||||
|                 // Every address here starts with 0xFF so we can just check the | ||||
|                 // low byte to figure out which register it is | ||||
|                 match addr & 0x00FF { | ||||
|                     0x00 => self.joypad.p1, | ||||
|                     0x00 => self.joyp.p1, | ||||
|                     0x01 => self.serial.next, | ||||
|                     0x02 => self.serial.ctrl.into(), | ||||
|                     0x04 => (self.timer.divider >> 8) as u8, | ||||
|                     0x05 => self.timer.counter, | ||||
|                     0x05 => self.timer.tima(), | ||||
|                     0x06 => self.timer.modulo, | ||||
|                     0x07 => self.timer.ctrl.into(), | ||||
|                     0x0F => self.interrupt_flag().into(), | ||||
|                     0x10 => self.snd.ch1.sweep.into(), | ||||
|                     0x11 => self.snd.ch1.duty(), | ||||
|                     0x12 => self.snd.ch1.envelope.into(), | ||||
|                     0x14 => self.snd.ch1.freq_hi(), | ||||
|                     0x16 => self.snd.ch2.duty(), | ||||
|                     0x17 => self.snd.ch2.envelope.into(), | ||||
|                     0x19 => self.snd.ch2.freq_hi(), | ||||
|                     0x1A => self.snd.ch3.enabled(), | ||||
|                     0x1B => self.snd.ch3.len(), | ||||
|                     0x1C => self.snd.ch3.volume(), | ||||
|                     0x1E => self.snd.ch3.freq_hi(), | ||||
|                     0x20 => self.snd.ch4.len(), | ||||
|                     0x21 => self.snd.ch4.envelope.into(), | ||||
|                     0x22 => self.snd.ch4.poly.into(), | ||||
|                     0x23 => self.snd.ch4.freq_data(), | ||||
|                     0x24 => self.snd.ctrl.channel.into(), | ||||
|                     0x25 => self.snd.ctrl.output.into(), | ||||
|                     0x26 => self.snd.ctrl.status(&self.snd), | ||||
|                     0x30..=0x3F => self.snd.ch3.wave_ram[addr as usize - 0xFF30], | ||||
|                     0x10..=0x3F => self.apu.read_byte(addr), | ||||
|                     0x40 => self.ppu.ctrl.into(), | ||||
|                     0x41 => self.ppu.stat.into(), | ||||
|                     0x42 => self.ppu.pos.scroll_y, | ||||
| @@ -255,8 +213,11 @@ impl BusIo for Bus { | ||||
|                     0x49 => self.ppu.monochrome.obj_palette_1.into(), | ||||
|                     0x4A => self.ppu.pos.window_y, | ||||
|                     0x4B => self.ppu.pos.window_x, | ||||
|                     0x4D => 0xFF, // CGB Specific Register | ||||
|                     _ => unimplemented!("Unable to read {:#06X} in I/O Registers", addr), | ||||
|                     0x4F => 0xFF, // CGB VRAM Bank Select | ||||
|                     _ => { | ||||
|                         tracing::warn!("Attempted read from {:#06X} on IO", addr); | ||||
|                         0xFF | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             0xFF80..=0xFFFE => { | ||||
| @@ -275,9 +236,8 @@ impl BusIo for Bus { | ||||
|             0x0000..=0x7FFF => { | ||||
|                 // 16KB ROM bank 00 (ends at 0x3FFF) | ||||
|                 // and 16KB ROM Bank 01 -> NN (switchable via MB) | ||||
|                 match self.cartridge.as_mut() { | ||||
|                     Some(cart) => cart.write_byte(addr, byte), | ||||
|                     None => panic!("Tried to write into non-existent cartridge"), | ||||
|                 if let Some(cart) = self.cart.as_mut() { | ||||
|                     cart.write_byte(addr, byte); | ||||
|                 } | ||||
|             } | ||||
|             0x8000..=0x9FFF => { | ||||
| @@ -289,9 +249,8 @@ impl BusIo for Bus { | ||||
|             } | ||||
|             0xA000..=0xBFFF => { | ||||
|                 // 8KB External RAM | ||||
|                 match self.cartridge.as_mut() { | ||||
|                     Some(cart) => cart.write_byte(addr, byte), | ||||
|                     None => panic!("Tried to write into non-existent cartridge"), | ||||
|                 if let Some(cart) = self.cart.as_mut() { | ||||
|                     cart.write_byte(addr, byte); | ||||
|                 } | ||||
|             } | ||||
|             0xC000..=0xCFFF => self.work_ram.write_byte(addr, byte), // 4KB Work RAM Bank 0 | ||||
| @@ -324,43 +283,22 @@ impl BusIo for Bus { | ||||
|                     _ => {} | ||||
|                 } | ||||
|             } | ||||
|             0xFEA0..=0xFEFF => {} // TODO: As far as I know, writes to here do nothing. | ||||
|             0xFEA0..=0xFEFF => {} // FIXME: As far as I know, writes to here do nothing. | ||||
|             0xFF00..=0xFF7F => { | ||||
|                 // IO Registers | ||||
|  | ||||
|                 // Every address here starts with 0xFF so we can just check the | ||||
|                 // low byte to figure out which register it is | ||||
|                 match addr & 0x00FF { | ||||
|                     0x00 => self.joypad.update(byte), | ||||
|                     0x00 => self.joyp.update(byte), | ||||
|                     0x01 => self.serial.next = byte, | ||||
|                     0x02 => self.serial.ctrl = byte.into(), | ||||
|                     0x04 => self.timer.divider = 0x0000, | ||||
|                     0x05 => self.timer.counter = byte, | ||||
|                     0x05 => self.timer.set_tima(byte), | ||||
|                     0x06 => self.timer.modulo = byte, | ||||
|                     0x07 => self.timer.ctrl = byte.into(), | ||||
|                     0x0F => self.set_interrupt_flag(byte), | ||||
|                     0x10 => self.snd.ch1.sweep = byte.into(), | ||||
|                     0x11 => self.snd.ch1.set_duty(byte), | ||||
|                     0x12 => self.snd.ch1.envelope = byte.into(), | ||||
|                     0x13 => self.snd.ch1.set_freq_lo(byte), | ||||
|                     0x14 => self.snd.ch1.set_freq_hi(byte), | ||||
|                     0x16 => self.snd.ch2.set_duty(byte), | ||||
|                     0x17 => self.snd.ch2.envelope = byte.into(), | ||||
|                     0x18 => self.snd.ch2.set_freq_lo(byte), | ||||
|                     0x19 => self.snd.ch2.set_freq_hi(byte), | ||||
|                     0x1A => self.snd.ch3.set_enabled(byte), | ||||
|                     0x1B => self.snd.ch3.set_len(byte), | ||||
|                     0x1C => self.snd.ch3.set_volume(byte), | ||||
|                     0x1D => self.snd.ch3.set_freq_lo(byte), | ||||
|                     0x1E => self.snd.ch3.set_freq_hi(byte), | ||||
|                     0x20 => self.snd.ch4.set_len(byte), | ||||
|                     0x21 => self.snd.ch4.envelope = byte.into(), | ||||
|                     0x22 => self.snd.ch4.poly = byte.into(), | ||||
|                     0x23 => self.snd.ch4.set_freq_data(byte), | ||||
|                     0x24 => self.snd.ctrl.channel = byte.into(), | ||||
|                     0x25 => self.snd.ctrl.output = byte.into(), | ||||
|                     0x26 => self.snd.ctrl.set_status(byte), // FIXME: Should we control which bytes are written to here? | ||||
|                     0x30..=0x3F => self.snd.ch3.wave_ram[addr as usize - 0xFF30] = byte, | ||||
|                     0x10..=0x3F => self.apu.write_byte(addr, byte), | ||||
|                     0x40 => self.ppu.ctrl = byte.into(), | ||||
|                     0x41 => self.ppu.stat.update(byte), | ||||
|                     0x42 => self.ppu.pos.scroll_y = byte, | ||||
| @@ -386,14 +324,15 @@ impl BusIo for Bus { | ||||
|                     0x4A => self.ppu.pos.window_y = byte, | ||||
|                     0x4B => self.ppu.pos.window_x = byte, | ||||
|                     0x4D => {} // CGB Specific Register | ||||
|                     0x4F => {} // CGB VRAM Bank Select | ||||
|                     0x50 => { | ||||
|                         // Disable Boot ROM | ||||
|                         if byte != 0 { | ||||
|                             self.boot = None; | ||||
|                         } | ||||
|                     } | ||||
|                     0x7F => {} // Tetris tries to write to this non-existent IO Address | ||||
|                     _ => unimplemented!("Unable to write to {:#06X} in I/O Registers", addr), | ||||
|                     0x70 => {} // CGB WRAM Bank Select | ||||
|                     _ => tracing::warn!("Attempted write of {:#04X} to {:#06X} on IO", byte, addr), | ||||
|                 }; | ||||
|             } | ||||
|             0xFF80..=0xFFFE => { | ||||
| @@ -408,17 +347,6 @@ impl BusIo for Bus { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Bus { | ||||
|     pub(crate) fn read_word(&self, addr: u16) -> u16 { | ||||
|         (self.read_byte(addr + 1) as u16) << 8 | self.read_byte(addr) as u16 | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn write_word(&mut self, addr: u16, word: u16) { | ||||
|         self.write_byte(addr + 1, (word >> 8) as u8); | ||||
|         self.write_byte(addr, (word & 0x00FF) as u8); | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Bus { | ||||
|     fn interrupt_flag(&self) -> InterruptFlag { | ||||
|         // Read the current interrupt information from the PPU | ||||
| @@ -426,7 +354,7 @@ impl Bus { | ||||
|         let lcd_stat = self.ppu.int.lcd_stat(); | ||||
|  | ||||
|         // Read the current interrupt information from the Joypad | ||||
|         let joypad = self.joypad.interrupt(); | ||||
|         let joypad = self.joyp.interrupt(); | ||||
|  | ||||
|         // Read the current interrupt information from the Timer | ||||
|         let timer = self.timer.interrupt(); | ||||
| @@ -456,7 +384,7 @@ impl Bus { | ||||
|         self.ppu.int.set_lcd_stat(lcd_stat); | ||||
|  | ||||
|         // Update the Joypad's instance of the following interrupts | ||||
|         self.joypad.set_interrupt(joypad); | ||||
|         self.joyp.set_interrupt(joypad); | ||||
|  | ||||
|         // Update the Timer's instance of the following interrupts | ||||
|         self.timer.set_interrupt(timer); | ||||
|   | ||||
							
								
								
									
										1020
									
								
								src/cartridge.rs
									
									
									
									
									
								
							
							
						
						
									
										1020
									
								
								src/cartridge.rs
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										384
									
								
								src/cpu.rs
									
									
									
									
									
								
							
							
						
						
									
										384
									
								
								src/cpu.rs
									
									
									
									
									
								
							| @@ -1,54 +1,30 @@ | ||||
| use crate::bus::{Bus, BusIo}; | ||||
| use crate::instruction::{Cycle, Instruction}; | ||||
| use crate::bus::{Bus, BusIo, BOOT_SIZE}; | ||||
| use crate::instruction::Instruction; | ||||
| use crate::interrupt::{InterruptEnable, InterruptFlag}; | ||||
| use crate::joypad::Joypad; | ||||
| use crate::ppu::Ppu; | ||||
| use crate::sound::AudioSender; | ||||
| use crate::timer::Timer; | ||||
| use crate::Cycle; | ||||
| use bitfield::bitfield; | ||||
| use std::fmt::{Display, Formatter, Result as FmtResult}; | ||||
|  | ||||
| #[derive(Debug, Clone, Default)] | ||||
| #[derive(Debug)] | ||||
| pub struct Cpu { | ||||
|     pub bus: Bus, | ||||
|     pub(crate) bus: Bus, | ||||
|     reg: Registers, | ||||
|     flags: Flags, | ||||
|     ime: ImeState, | ||||
|     // TODO: Merge halted and state properties | ||||
|     halted: Option<HaltState>, | ||||
|     state: State, | ||||
| } | ||||
|  | ||||
| impl Cpu { | ||||
|     pub fn new() -> Self { | ||||
|     pub(crate) fn new(rom: [u8; BOOT_SIZE]) -> Self { | ||||
|         Self { | ||||
|             reg: Registers { | ||||
|                 a: 0x01, | ||||
|                 b: 0x00, | ||||
|                 c: 0x13, | ||||
|                 d: 0x00, | ||||
|                 e: 0xD8, | ||||
|                 h: 0x01, | ||||
|                 l: 0x4D, | ||||
|                 sp: 0xFFFE, | ||||
|                 pc: 0x0100, | ||||
|             }, | ||||
|             flags: 0xb0.into(), | ||||
|             ..Default::default() | ||||
|             bus: Bus::with_boot(rom), | ||||
|             reg: Default::default(), | ||||
|             flags: Flags(0), | ||||
|             ime: ImeState::Disabled, | ||||
|             state: State::Execute, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn boot_new(path: &str) -> anyhow::Result<Self> { | ||||
|         Ok(Self { | ||||
|             bus: Bus::with_boot(path)?, | ||||
|             ..Default::default() | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub fn set_audio_src(&mut self, sender: AudioSender) { | ||||
|         self.bus.pass_audio_src(sender) | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn ime(&self) -> ImeState { | ||||
|         self.ime | ||||
|     } | ||||
| @@ -57,89 +33,100 @@ impl Cpu { | ||||
|         self.ime = state; | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn halt(&mut self, state: HaltState) { | ||||
|         self.halted = Some(state); | ||||
|     pub(crate) fn halt_cpu(&mut self, kind: HaltKind) { | ||||
|         self.state = State::Halt(kind); | ||||
|     } | ||||
|  | ||||
|     fn resume(&mut self) { | ||||
|         self.halted = None; | ||||
|     fn resume_execution(&mut self) { | ||||
|         self.state = State::Execute; | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn halted(&self) -> Option<HaltState> { | ||||
|         self.halted | ||||
|     pub(crate) fn is_halted(&self) -> bool { | ||||
|         matches!(self.state, State::Halt(_)) | ||||
|     } | ||||
|  | ||||
|     fn inc_pc(&mut self) { | ||||
|         self.reg.pc += 1; | ||||
|     pub(crate) fn halt_kind(&self) -> Option<HaltKind> { | ||||
|         match self.state { | ||||
|             State::Halt(kind) => Some(kind), | ||||
|             _ => None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|     pub fn load_cartridge(&mut self, path: &str) -> std::io::Result<()> { | ||||
|         self.bus.load_cartridge(path) | ||||
|     } | ||||
|  | ||||
|     pub fn rom_title(&self) -> Option<&str> { | ||||
|         self.bus.rom_title() | ||||
| impl Default for Cpu { | ||||
|     fn default() -> Self { | ||||
|         Cpu::new(*include_bytes!("../bin/bootix_dmg.bin")) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Cpu { | ||||
|     fn fetch(&self) -> u8 { | ||||
|         self.bus.read_byte(self.reg.pc) | ||||
|     /// Fetch an [Instruction] from the memory bus | ||||
|     /// (4 cycles) | ||||
|     fn fetch(&mut self) -> u8 { | ||||
|         let byte = self.read_byte(self.reg.pc); | ||||
|         self.bus.clock(); | ||||
|         self.reg.pc += 1; | ||||
|         byte | ||||
|     } | ||||
|  | ||||
|     /// Decode a byte into an [SM83](Cpu) [Instruction] | ||||
|     /// | ||||
|     /// If opcode == 0xCB, then decoding costs 4 cycles. | ||||
|     /// Otherwise, decoding is free | ||||
|     pub(crate) fn decode(&mut self, opcode: u8) -> Instruction { | ||||
|         Instruction::from_byte(self, opcode) | ||||
|         if opcode == 0xCB { | ||||
|             Instruction::decode(self.fetch(), true) | ||||
|         } else { | ||||
|             Instruction::decode(opcode, false) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Execute an [Instruction]. | ||||
|     /// | ||||
|     /// The amount of cycles necessary to execute an instruction range from | ||||
|     /// 0 to 20 T-cycles | ||||
|     fn execute(&mut self, instruction: Instruction) -> Cycle { | ||||
|         Instruction::execute(self, instruction) | ||||
|     } | ||||
|  | ||||
|     pub fn step(&mut self) -> Cycle { | ||||
|         // if !self.bus.boot_enabled() { | ||||
|     /// Perform the [`Cpu::fetch()`] [`Cpu::decode()`] [`Cpu::execute()`] | ||||
|     /// routine. | ||||
|     /// | ||||
|     /// Handle HALT and interrupts. | ||||
|     pub(crate) fn step(&mut self) -> Cycle { | ||||
|         // Log instructions | ||||
|         // if self.reg.pc > 0xFF { | ||||
|         //     let out = std::io::stdout(); | ||||
|         //     let handle = out.lock(); | ||||
|  | ||||
|         //     self.log_state(handle).unwrap(); | ||||
|         //     let _ = self._print_logs(out.lock()); | ||||
|         // } | ||||
|  | ||||
|         let cycles = match self.halted() { | ||||
|             Some(state) => { | ||||
|                 use HaltState::*; | ||||
|  | ||||
|                 match state { | ||||
|                     ImeEnabled | NonePending => Cycle::new(4), | ||||
|                     SomePending => todo!("Implement HALT bug"), | ||||
|         if let Some(elapsed) = self.handle_interrupt() { | ||||
|             return elapsed; | ||||
|         } | ||||
|             } | ||||
|             None => { | ||||
|                 let opcode = self.fetch(); | ||||
|                 self.inc_pc(); | ||||
|  | ||||
|                 let instr = self.decode(opcode); | ||||
|                 let cycles = self.execute(instr); | ||||
|         if let Some(kind) = self.halt_kind() { | ||||
|             use HaltKind::*; | ||||
|  | ||||
|                 self.check_ime(); | ||||
|  | ||||
|                 cycles | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         // FIXME: Regression, this is not a proper fix, | ||||
|         // the FIFO behaves weirdly using this code | ||||
|         let pending: u32 = cycles.into(); | ||||
|         let mut offset = 0; | ||||
|         for _ in 0..(pending + offset) { | ||||
|             if !self.bus.is_mpsc_still_full() { | ||||
|             self.bus.clock(); | ||||
|             } else { | ||||
|                 offset += 1; | ||||
|             } | ||||
|             return match kind { | ||||
|                 ImeEnabled | NonePending => 4, | ||||
|                 SomePending => todo!("Implement HALT bug"), | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         self.handle_interrupts(); | ||||
|         let opcode = self.fetch(); | ||||
|         let instr = self.decode(opcode); | ||||
|         let elapsed = self.execute(instr); | ||||
|         self.handle_ei(); | ||||
|  | ||||
|         cycles | ||||
|         // For use in Blargg's Test ROMs | ||||
|         // if self.read_byte(0xFF02) == 0x81 { | ||||
|         //     let c = self.read_byte(0xFF01) as char; | ||||
|         //     self.write_byte(0xFF02, 0x00); | ||||
|         //     eprint!("{}", c); | ||||
|         // } | ||||
|  | ||||
|         elapsed | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -154,98 +141,72 @@ impl BusIo for Cpu { | ||||
| } | ||||
|  | ||||
| impl Cpu { | ||||
|     pub(crate) fn read_imm_byte(&mut self, addr: u16) -> u8 { | ||||
|         self.inc_pc(); // NB: the addr read in the line below will be equal to PC - 1 after this function call | ||||
|         self.bus.read_byte(addr) | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn read_imm_word(&mut self, addr: u16) -> u16 { | ||||
|         self.inc_pc(); | ||||
|         self.inc_pc(); // NB: the addr read in the line below will be equal to PC - 2 after this function call | ||||
|         self.bus.read_word(addr) | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn write_word(&mut self, addr: u16, word: u16) { | ||||
|         self.bus.write_word(addr, word) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Cpu { | ||||
|     pub fn ppu(&mut self) -> &Ppu { | ||||
|         &self.bus.ppu | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn joypad_mut(&mut self) -> &mut Joypad { | ||||
|         &mut self.bus.joypad | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn timer(&self) -> &Timer { | ||||
|         &self.bus.timer | ||||
|     } | ||||
|  | ||||
|     fn check_ime(&mut self) { | ||||
|     fn handle_ei(&mut self) { | ||||
|         match self.ime { | ||||
|             ImeState::Pending => { | ||||
|                 // This is within the context of the EI instruction, we need to not update EI until the end of the | ||||
|                 // next executed Instruction | ||||
|                 self.ime = ImeState::PendingEnd; | ||||
|             } | ||||
|             ImeState::PendingEnd => { | ||||
|                 // The Instruction after EI has now been executed, so we want to enable the IME flag here | ||||
|                 self.ime = ImeState::Enabled; | ||||
|             } | ||||
|             ImeState::Disabled | ImeState::Enabled => {} // Do Nothing | ||||
|             ImeState::EiExecuted => self.ime = ImeState::Pending, | ||||
|             ImeState::Pending => self.ime = ImeState::Enabled, | ||||
|             ImeState::Disabled | ImeState::Enabled => {} | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn handle_interrupts(&mut self) { | ||||
|         let req = self.read_byte(0xFF0F); | ||||
|         let enabled = self.read_byte(0xFFFF); | ||||
|     pub(crate) fn int_request(&self) -> u8 { | ||||
|         self.read_byte(0xFF0F) | ||||
|     } | ||||
|  | ||||
|         if self.halted.is_some() { | ||||
|     pub(crate) fn int_enable(&self) -> u8 { | ||||
|         self.read_byte(0xFFFF) | ||||
|     } | ||||
|  | ||||
|     fn handle_interrupt(&mut self) -> Option<Cycle> { | ||||
|         let irq = self.int_request(); | ||||
|         let enable = self.int_enable(); | ||||
|  | ||||
|         // TODO: Ensure that this behaviour is correct | ||||
|         if self.is_halted() { | ||||
|             // When we're here either a HALT with IME set or | ||||
|             // a HALT with IME not set and No pending Interrupts was called | ||||
|  | ||||
|             if req & enabled != 0 { | ||||
|             if irq & enable != 0 { | ||||
|                 // The if self.ime() below correctly follows the "resuming from HALT" behaviour so | ||||
|                 // nothing actually needs to be added here. This is just documentation | ||||
|                 // since it's a bit weird why nothing is being done | ||||
|  | ||||
|                 self.resume() | ||||
|                 self.resume_execution(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if let ImeState::Enabled = self.ime() { | ||||
|             let mut req: InterruptFlag = req.into(); | ||||
|             let enabled: InterruptEnable = enabled.into(); | ||||
|         match self.ime() { | ||||
|             ImeState::Enabled => { | ||||
|                 let mut irq: InterruptFlag = irq.into(); | ||||
|                 let enable: InterruptEnable = enable.into(); | ||||
|  | ||||
|             let vector = if req.vblank() && enabled.vblank() { | ||||
|                 let rst_vector = if irq.vblank() && enable.vblank() { | ||||
|                     // Handle VBlank Interrupt | ||||
|                 req.set_vblank(false); | ||||
|                     irq.set_vblank(false); | ||||
|  | ||||
|                     // INT 40h | ||||
|                     Some(0x40) | ||||
|             } else if req.lcd_stat() && enabled.lcd_stat() { | ||||
|                 } else if irq.lcd_stat() && enable.lcd_stat() { | ||||
|                     // Handle LCD STAT Interrupt | ||||
|                 req.set_lcd_stat(false); | ||||
|                     irq.set_lcd_stat(false); | ||||
|  | ||||
|                     // INT 48h | ||||
|                     Some(0x48) | ||||
|             } else if req.timer() && enabled.timer() { | ||||
|                 } else if irq.timer() && enable.timer() { | ||||
|                     // Handle Timer Interrupt | ||||
|                 req.set_timer(false); | ||||
|                     irq.set_timer(false); | ||||
|  | ||||
|                     // INT 50h | ||||
|                     Some(0x50) | ||||
|             } else if req.serial() && enabled.serial() { | ||||
|                 } else if irq.serial() && enable.serial() { | ||||
|                     // Handle Serial Interrupt | ||||
|                 req.set_serial(false); | ||||
|                     irq.set_serial(false); | ||||
|  | ||||
|                     // INT 58h | ||||
|                     Some(0x58) | ||||
|             } else if req.joypad() && enabled.joypad() { | ||||
|                 } else if irq.joypad() && enable.joypad() { | ||||
|                     // Handle Joypad Interrupt | ||||
|                 req.set_joypad(false); | ||||
|                     irq.set_joypad(false); | ||||
|  | ||||
|                     // INT 60h | ||||
|                     Some(0x60) | ||||
| @@ -253,34 +214,30 @@ impl Cpu { | ||||
|                     None | ||||
|                 }; | ||||
|  | ||||
|             let _ = match vector { | ||||
|                 Some(address) => { | ||||
|                 match rst_vector { | ||||
|                     Some(vector) => { | ||||
|                         //  Write the Changes to 0xFF0F and 0xFFFF registers | ||||
|                     self.write_byte(0xFF0F, req.into()); | ||||
|                         self.write_byte(0xFF0F, irq.into()); | ||||
|  | ||||
|                         // Disable all future interrupts | ||||
|                         self.set_ime(ImeState::Disabled); | ||||
|                     Instruction::reset(self, address) | ||||
|                         Some(Instruction::reset(self, vector)) | ||||
|                     } | ||||
|                 None => Cycle::new(0), // NO Interrupts were enabled and / or requested | ||||
|             }; | ||||
|                     None => None, | ||||
|                 } | ||||
|             } | ||||
|             _ => None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Copy, Clone)] | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| enum State { | ||||
|     Execute, | ||||
|     // Halt, | ||||
|     Halt(HaltKind), | ||||
|     // Stop, | ||||
| } | ||||
|  | ||||
| impl Default for State { | ||||
|     fn default() -> Self { | ||||
|         Self::Execute | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Cpu { | ||||
|     pub(crate) fn set_register(&mut self, register: Register, value: u8) { | ||||
|         use Register::*; | ||||
| @@ -293,7 +250,6 @@ impl Cpu { | ||||
|             E => self.reg.e = value, | ||||
|             H => self.reg.h = value, | ||||
|             L => self.reg.l = value, | ||||
|             Flag => self.flags = value.into(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -308,7 +264,6 @@ impl Cpu { | ||||
|             E => self.reg.e, | ||||
|             H => self.reg.h, | ||||
|             L => self.reg.l, | ||||
|             Flag => self.flags.into(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -357,13 +312,20 @@ impl Cpu { | ||||
|         &self.flags | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn update_flags(&mut self, z: bool, n: bool, h: bool, c: bool) { | ||||
|         self.flags.set_z(z); | ||||
|         self.flags.set_n(n); | ||||
|         self.flags.set_h(h); | ||||
|         self.flags.set_c(c); | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn set_flags(&mut self, flags: Flags) { | ||||
|         self.flags = flags; | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Cpu { | ||||
|     fn _debug_log(&self, mut w: impl std::io::Write, instr: &Instruction) -> std::io::Result<()> { | ||||
|     fn _print_debug(&self, mut w: impl std::io::Write) -> std::io::Result<()> { | ||||
|         write!(w, "A: {:02X} ", self.reg.a)?; | ||||
|         write!(w, "F: {:02X} ", u8::from(self.flags))?; | ||||
|         write!(w, "B: {:02X} ", self.reg.b)?; | ||||
| @@ -378,30 +340,39 @@ impl Cpu { | ||||
|         write!(w, "{:02X} ", self.read_byte(self.reg.pc + 1))?; | ||||
|         write!(w, "{:02X} ", self.read_byte(self.reg.pc + 2))?; | ||||
|         write!(w, "{:02X})", self.read_byte(self.reg.pc + 3))?; | ||||
|         writeln!(w, "| {:?}", instr)?; | ||||
|         writeln!(w, "| {:?}", self._dbg_instr())?; | ||||
|         w.flush() | ||||
|     } | ||||
|  | ||||
|     fn _log_state(&self, mut writer: impl std::io::Write) -> std::io::Result<()> { | ||||
|         write!(writer, "A: {:02X} ", self.reg.a)?; | ||||
|         write!(writer, "F: {:02X} ", u8::from(self.flags))?; | ||||
|         write!(writer, "B: {:02X} ", self.reg.b)?; | ||||
|         write!(writer, "C: {:02X} ", self.reg.c)?; | ||||
|         write!(writer, "D: {:02X} ", self.reg.d)?; | ||||
|         write!(writer, "E: {:02X} ", self.reg.e)?; | ||||
|         write!(writer, "H: {:02X} ", self.reg.h)?; | ||||
|         write!(writer, "L: {:02X} ", self.reg.l)?; | ||||
|         write!(writer, "SP: {:04X} ", self.reg.sp)?; | ||||
|         write!(writer, "PC: 00:{:04X} ", self.reg.pc)?; | ||||
|         write!(writer, "({:02X} ", self.read_byte(self.reg.pc))?; | ||||
|         write!(writer, "{:02X} ", self.read_byte(self.reg.pc + 1))?; | ||||
|         write!(writer, "{:02X} ", self.read_byte(self.reg.pc + 2))?; | ||||
|         writeln!(writer, "{:02X})", self.read_byte(self.reg.pc + 3))?; | ||||
|         writer.flush() | ||||
|     fn _print_logs(&self, mut w: impl std::io::Write) -> std::io::Result<()> { | ||||
|         write!(w, "A: {:02X} ", self.reg.a)?; | ||||
|         write!(w, "F: {:02X} ", u8::from(self.flags))?; | ||||
|         write!(w, "B: {:02X} ", self.reg.b)?; | ||||
|         write!(w, "C: {:02X} ", self.reg.c)?; | ||||
|         write!(w, "D: {:02X} ", self.reg.d)?; | ||||
|         write!(w, "E: {:02X} ", self.reg.e)?; | ||||
|         write!(w, "H: {:02X} ", self.reg.h)?; | ||||
|         write!(w, "L: {:02X} ", self.reg.l)?; | ||||
|         write!(w, "SP: {:04X} ", self.reg.sp)?; | ||||
|         write!(w, "PC: 00:{:04X} ", self.reg.pc)?; | ||||
|         write!(w, "({:02X} ", self.read_byte(self.reg.pc))?; | ||||
|         write!(w, "{:02X} ", self.read_byte(self.reg.pc + 1))?; | ||||
|         write!(w, "{:02X} ", self.read_byte(self.reg.pc + 2))?; | ||||
|         writeln!(w, "{:02X})", self.read_byte(self.reg.pc + 3))?; | ||||
|         w.flush() | ||||
|     } | ||||
|  | ||||
|     fn _dbg_instr(&self) -> Instruction { | ||||
|         let byte = self.read_byte(self.reg.pc); | ||||
|         if byte == 0xCB { | ||||
|             Instruction::decode(self.read_byte(self.reg.pc + 1), true) | ||||
|         } else { | ||||
|             Instruction::decode(byte, false) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Copy, Clone)] | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub(crate) enum Register { | ||||
|     A, | ||||
|     B, | ||||
| @@ -410,10 +381,9 @@ pub(crate) enum Register { | ||||
|     E, | ||||
|     H, | ||||
|     L, | ||||
|     Flag, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Copy, Clone)] | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub(crate) enum RegisterPair { | ||||
|     AF, | ||||
|     BC, | ||||
| @@ -423,7 +393,7 @@ pub(crate) enum RegisterPair { | ||||
|     PC, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Copy, Clone, Default)] | ||||
| #[derive(Debug, Default)] | ||||
| struct Registers { | ||||
|     a: u8, | ||||
|     b: u8, | ||||
| @@ -461,12 +431,6 @@ impl Clone for Flags { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for Flags { | ||||
|     fn default() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Display for Flags { | ||||
|     fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { | ||||
|         if self.z() { | ||||
| @@ -508,7 +472,7 @@ impl From<u8> for Flags { | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub(crate) enum HaltState { | ||||
| pub(crate) enum HaltKind { | ||||
|     ImeEnabled, | ||||
|     NonePending, | ||||
|     SomePending, | ||||
| @@ -517,13 +481,43 @@ pub(crate) enum HaltState { | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub(crate) enum ImeState { | ||||
|     Disabled, | ||||
|     EiExecuted, | ||||
|     Pending, | ||||
|     PendingEnd, | ||||
|     Enabled, | ||||
| } | ||||
|  | ||||
| impl Default for ImeState { | ||||
|     fn default() -> Self { | ||||
|         Self::Disabled | ||||
| pub(crate) mod dbg { | ||||
|     use super::{Cpu, ImeState, RegisterPair}; | ||||
|  | ||||
|     pub(crate) fn flags(cpu: &Cpu) -> u8 { | ||||
|         cpu.flags.into() | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn af(cpu: &Cpu) -> u16 { | ||||
|         cpu.register_pair(RegisterPair::AF) | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn bc(cpu: &Cpu) -> u16 { | ||||
|         cpu.register_pair(RegisterPair::BC) | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn de(cpu: &Cpu) -> u16 { | ||||
|         cpu.register_pair(RegisterPair::DE) | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn hl(cpu: &Cpu) -> u16 { | ||||
|         cpu.register_pair(RegisterPair::HL) | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn sp(cpu: &Cpu) -> u16 { | ||||
|         cpu.register_pair(RegisterPair::SP) | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn pc(cpu: &Cpu) -> u16 { | ||||
|         cpu.register_pair(RegisterPair::PC) | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn ime(cpu: &Cpu) -> bool { | ||||
|         matches!(cpu.ime, ImeState::Enabled) | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										191
									
								
								src/emu.rs
									
									
									
									
									
								
							
							
						
						
									
										191
									
								
								src/emu.rs
									
									
									
									
									
								
							| @@ -1,60 +1,163 @@ | ||||
| use crate::cpu::Cpu as SM83; | ||||
| use crate::instruction::Cycle; | ||||
| use crate::joypad; | ||||
| use crate::ppu::Ppu; | ||||
| use anyhow::Result; | ||||
| use crate::apu::gen::SampleProducer; | ||||
| use crate::bus::BOOT_SIZE; | ||||
| use crate::cartridge::Cartridge; | ||||
| use crate::cpu::Cpu; | ||||
| use crate::{Cycle, GB_HEIGHT, GB_WIDTH}; | ||||
| use clap::crate_name; | ||||
| use gilrs::Gilrs; | ||||
| use std::fs::File; | ||||
| use std::io::{Read, Write}; | ||||
| use std::path::{Path, PathBuf}; | ||||
| use std::time::Duration; | ||||
| use winit_input_helper::WinitInputHelper; | ||||
| use thiserror::Error; | ||||
| use winit::event::KeyboardInput; | ||||
| use winit::event_loop::ControlFlow; | ||||
|  | ||||
| pub const SM83_CYCLE_TIME: Duration = Duration::from_nanos(1_000_000_000 / SM83_CLOCK_SPEED); | ||||
| pub const CYCLES_IN_FRAME: Cycle = Cycle::new(456 * 154); // 456 Cycles times 154 scanlines | ||||
| pub const CYCLES_IN_FRAME: Cycle = 456 * 154; // 456 Cycles times 154 scanlines | ||||
| pub(crate) const SM83_CLOCK_SPEED: u64 = 0x40_0000; // Hz which is 4.194304Mhz | ||||
| const DEFAULT_TITLE: &str = "DMG-01 Emulator"; | ||||
| const GAMEPAD_ENABLED: bool = false; | ||||
| const DEFAULT_TITLE: &str = "Game Boy Screen"; | ||||
|  | ||||
| pub fn init(boot_path: Option<&str>, rom_path: &str) -> Result<SM83> { | ||||
|     let mut cpu = match boot_path { | ||||
|         Some(path) => SM83::boot_new(path)?, | ||||
|         None => SM83::new(), | ||||
|     }; | ||||
|  | ||||
|     eprintln!("Initialized GB Emulator"); | ||||
|  | ||||
|     cpu.load_cartridge(rom_path)?; | ||||
|     Ok(cpu) | ||||
| #[inline] | ||||
| pub fn run_frame(cpu: &mut Cpu, gamepad: &mut Gilrs, key: KeyboardInput) { | ||||
|     run(cpu, gamepad, key, CYCLES_IN_FRAME) | ||||
| } | ||||
|  | ||||
| pub fn rom_title(game_boy: &SM83) -> &str { | ||||
|     game_boy.rom_title().unwrap_or(DEFAULT_TITLE) | ||||
| } | ||||
| #[inline] | ||||
| pub fn run(cpu: &mut Cpu, gamepad: &mut Gilrs, key: KeyboardInput, cycles: Cycle) { | ||||
|     let mut elapsed = 0; | ||||
|  | ||||
| pub fn run( | ||||
|     game_boy: &mut SM83, | ||||
|     gamepad: &mut Gilrs, | ||||
|     input: &WinitInputHelper, | ||||
|     pending: Cycle, | ||||
| ) -> Cycle { | ||||
|     let mut elapsed = Cycle::new(0); | ||||
|  | ||||
|     while elapsed < pending { | ||||
|         elapsed += run_unsynced(game_boy, gamepad, input); | ||||
|     } | ||||
|  | ||||
|     elapsed | ||||
| } | ||||
|  | ||||
| pub fn run_unsynced(game_boy: &mut SM83, gamepad: &mut Gilrs, input: &WinitInputHelper) -> Cycle { | ||||
|     if GAMEPAD_ENABLED { | ||||
|     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); | ||||
|     game_boy.step() | ||||
| pub fn save_and_exit(cpu: &Cpu, control_flow: &mut ControlFlow) { | ||||
|     write_save(cpu); | ||||
|     *control_flow = ControlFlow::Exit; | ||||
| } | ||||
|  | ||||
| pub fn draw(ppu: &Ppu, frame: &mut [u8]) { | ||||
|     ppu.copy_to_gui(frame); | ||||
| #[inline] | ||||
| pub fn pixel_buf(cpu: &Cpu) -> &[u8; GB_HEIGHT * GB_WIDTH * 4] { | ||||
|     use crate::ppu::Device; | ||||
|     cpu.bus.ppu.frame_buf.get(Device::Host) | ||||
| } | ||||
|  | ||||
| pub fn from_boot_rom<P: AsRef<Path>>(path: P) -> std::io::Result<Cpu> { | ||||
|     Ok(Cpu::new(read_boot(path)?)) | ||||
| } | ||||
|  | ||||
| pub fn read_game_rom<P: AsRef<Path>>(cpu: &mut Cpu, path: P) -> std::io::Result<()> { | ||||
|     cpu.bus.cart = Some(Cartridge::new(std::fs::read(path.as_ref())?)); | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| pub fn set_audio_prod(cpu: &mut Cpu, prod: SampleProducer<f32>) { | ||||
|     cpu.bus.apu.prod = Some(prod); | ||||
| } | ||||
|  | ||||
| pub fn rom_title(cpu: &Cpu) -> &str { | ||||
|     cpu.bus | ||||
|         .cart | ||||
|         .as_ref() | ||||
|         .and_then(|c| c.title.as_deref()) | ||||
|         .unwrap_or(DEFAULT_TITLE) | ||||
| } | ||||
|  | ||||
| pub fn write_save(cpu: &Cpu) { | ||||
|     match cpu.bus.cart.as_ref() { | ||||
|         Some(cart) => match write_save_to_file(cart) { | ||||
|             Ok(path) => tracing::info!("Wrote to save at {:?}", path), | ||||
|             Err(err @ SaveError::NotApplicable) => tracing::warn!("Unable to Save: {:?}", err), | ||||
|             Err(SaveError::DiffSize) => unreachable!(), | ||||
|             Err(SaveError::Io(err)) => tracing::error!("{:?}", err), | ||||
|         }, | ||||
|         None => tracing::error!("No cartridge is currently present"), | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn load_save(cpu: &mut Cpu) { | ||||
|     match cpu.bus.cart.as_mut() { | ||||
|         Some(cart) => match read_save_from_file(cart) { | ||||
|             Ok(path) => tracing::info!("Loaded save from {:?}", path), | ||||
|             Err(err @ SaveError::NotApplicable) => tracing::warn!("Unable to load save: {}", err), | ||||
|             Err(err @ SaveError::DiffSize) => tracing::error!("Unable to load save: {}", err), | ||||
|             Err(SaveError::Io(err)) => match err.kind() { | ||||
|                 std::io::ErrorKind::NotFound => tracing::warn!("Save not found"), | ||||
|                 _ => tracing::error!("{:?}", err), | ||||
|             }, | ||||
|         }, | ||||
|         None => tracing::error!("No cartridge is currently present"), | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn write_save_to_file(cart: &Cartridge) -> Result<PathBuf, SaveError> { | ||||
|     match cart.title.as_ref().zip(cart.ext_ram()) { | ||||
|         Some((title, ram)) => { | ||||
|             let mut save_path = data_path().unwrap_or_else(|| PathBuf::from(".")); | ||||
|             save_path.push(title); | ||||
|             save_path.set_extension("sav"); | ||||
|  | ||||
|             let mut file = File::create(&save_path)?; | ||||
|             file.write_all(ram)?; | ||||
|             Ok(save_path) | ||||
|         } | ||||
|         None => Err(SaveError::NotApplicable), | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn read_save_from_file(cart: &mut Cartridge) -> Result<PathBuf, SaveError> { | ||||
|     match cart.title.clone().zip(cart.ext_ram_mut()) { | ||||
|         Some((title, ext_ram)) => { | ||||
|             let mut save_path = data_path().unwrap_or_else(|| PathBuf::from(".")); | ||||
|             save_path.push(title); | ||||
|             save_path.set_extension("sav"); | ||||
|  | ||||
|             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, | ||||
| } | ||||
|   | ||||
							
								
								
									
										729
									
								
								src/gui.rs
									
									
									
									
									
								
							
							
						
						
									
										729
									
								
								src/gui.rs
									
									
									
									
									
								
							| @@ -1,321 +1,472 @@ | ||||
| use crate::bus::BusIo; | ||||
| use crate::cpu::{Cpu as SM83, Register, RegisterPair}; | ||||
| use egui::{ClippedMesh, FontDefinitions}; | ||||
| use egui_wgpu_backend::{RenderPass, ScreenDescriptor}; | ||||
| use egui_winit_platform::{Platform, PlatformDescriptor}; | ||||
| use pixels::{wgpu, PixelsContext}; | ||||
| use std::time::Instant; | ||||
| use wgpu::TextureFormat::Bgra8UnormSrgb; | ||||
|  | ||||
| // Boilerplate code from: https://github.com/parasyte/pixels/blob/0.3.0/examples/egui-winit/src/gui.rs | ||||
|  | ||||
| /// Manages all state required for rendering egui over `Pixels`. | ||||
| pub struct Egui { | ||||
|     // State for egui. | ||||
|     start_time: Instant, | ||||
|     platform: Platform, | ||||
|     screen_desc: ScreenDescriptor, | ||||
|     render_pass: RenderPass, | ||||
|     paint_jobs: Vec<ClippedMesh>, | ||||
|  | ||||
|     pub config: Configuration, | ||||
|  | ||||
|     show_flags: bool, | ||||
|     show_cpu_info: bool, | ||||
|     show_registers: bool, | ||||
|     show_int: bool, | ||||
|     show_timer: bool, | ||||
| } | ||||
|  | ||||
| impl Egui { | ||||
|     /// Create egui. | ||||
|     pub fn new(width: u32, height: u32, scale_factor: f64, context: &PixelsContext) -> Self { | ||||
|         let platform = Platform::new(PlatformDescriptor { | ||||
|             physical_width: width, | ||||
|             physical_height: height, | ||||
|             scale_factor, | ||||
|             font_definitions: FontDefinitions::default(), | ||||
|             style: Default::default(), | ||||
|         }); | ||||
|  | ||||
|         let screen_desc = ScreenDescriptor { | ||||
|             physical_width: width, | ||||
|             physical_height: height, | ||||
|             scale_factor: scale_factor as f32, | ||||
| 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, | ||||
| }; | ||||
|  | ||||
|         let render_pass = RenderPass::new(&context.device, Bgra8UnormSrgb); | ||||
| 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 { | ||||
|             start_time: Instant::now(), | ||||
|             should_quit: Default::default(), | ||||
|             title, | ||||
|             mode: EmuMode::Running, | ||||
|  | ||||
|             window, | ||||
|             platform, | ||||
|             screen_desc, | ||||
|  | ||||
|             surface, | ||||
|             device, | ||||
|             queue, | ||||
|             surface_config, | ||||
|             render_pass, | ||||
|             paint_jobs: Vec::new(), | ||||
|             config: Default::default(), | ||||
|             show_flags: false, | ||||
|             show_cpu_info: false, | ||||
|             show_registers: false, | ||||
|             show_int: false, | ||||
|             show_timer: false, | ||||
|  | ||||
|             texture, | ||||
|             texture_size, | ||||
|             texture_id, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Handle input events from the window manager. | ||||
|     pub fn handle_event(&mut self, event: &winit::event::Event<'_, ()>) { | ||||
|     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); | ||||
|     } | ||||
|  | ||||
|     /// Resize egui. | ||||
|     pub fn resize(&mut self, width: u32, height: u32) { | ||||
|         self.screen_desc.physical_width = width; | ||||
|         self.screen_desc.physical_height = height; | ||||
|     pub fn update_time(&mut self, elapsed_seconds: f64) { | ||||
|         self.platform.update_time(elapsed_seconds); | ||||
|     } | ||||
|  | ||||
|     /// Update scaling factor. | ||||
|     pub fn scale_factor(&mut self, scale_factor: f64) { | ||||
|         self.screen_desc.scale_factor = scale_factor as f32; | ||||
|     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); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Prepare egui. | ||||
|     pub fn prepare(&mut self, game_boy: &SM83) { | ||||
|         self.platform | ||||
|             .update_time(self.start_time.elapsed().as_secs_f64()); | ||||
|     pub fn paint(&mut self, cpu: &Cpu) { | ||||
|         use wgpu::{Color, SurfaceError}; | ||||
|  | ||||
|         // Begin the egui frame. | ||||
|         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); | ||||
|  | ||||
|         // Draw the demo application. | ||||
|         self.ui(&self.platform.context(), game_boy); | ||||
|         // 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); | ||||
|  | ||||
|         // End the egui frame and create all paint jobs to prepare for rendering. | ||||
|         let (_output, paint_commands) = self.platform.end_frame(); | ||||
|         self.paint_jobs = self.platform.context().tessellate(paint_commands); | ||||
|     } | ||||
|  | ||||
|     /// Create the UI using egui. | ||||
|     fn ui(&mut self, ctx: &egui::CtxRef, game_boy: &SM83) { | ||||
|         egui::TopPanel::top("menubar_container").show(ctx, |ui| { | ||||
|             egui::menu::bar(ui, |ui| { | ||||
|                 egui::menu::menu(ui, "File", |ui| { | ||||
|                     if ui.button("Configuration").clicked() { | ||||
|                         self.config.show = true; | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|                 egui::menu::menu(ui, "Status", |ui| { | ||||
|                     if ui.button("Flags").clicked() { | ||||
|                         self.show_flags = true; | ||||
|                     } | ||||
|  | ||||
|                     if ui.button("CPU Information").clicked() { | ||||
|                         self.show_cpu_info = true; | ||||
|                     } | ||||
|  | ||||
|                     if ui.button("Registers").clicked() { | ||||
|                         self.show_registers = true; | ||||
|                     } | ||||
|  | ||||
|                     if ui.button("Interrupts").clicked() { | ||||
|                         self.show_int = true; | ||||
|                     } | ||||
|  | ||||
|                     if ui.button("Timer").clicked() { | ||||
|                         self.show_timer = true; | ||||
|                     } | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         egui::Window::new("Cpu Flags") | ||||
|             .open(&mut self.show_flags) | ||||
|             .show(ctx, |ui| { | ||||
|                 let flags = game_boy.flags(); | ||||
|  | ||||
|                 ui.horizontal(|ui| { | ||||
|                     let _ = ui.selectable_label(flags.z(), "Zero"); | ||||
|                     let _ = ui.selectable_label(flags.n(), "Negative"); | ||||
|                     let _ = ui.selectable_label(flags.h(), "Half-Carry"); | ||||
|                     let _ = ui.selectable_label(flags.c(), "Carry"); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|         egui::Window::new("Timer") | ||||
|             .open(&mut self.show_timer) | ||||
|             .show(ctx, |ui| { | ||||
|                 let timer = game_boy.timer(); | ||||
|  | ||||
|                 ui.horizontal(|ui| { | ||||
|                     ui.label("DIV"); | ||||
|                     ui.monospace(format!("{:#06X}", timer.divider)); | ||||
|                 }); | ||||
|  | ||||
|                 ui.horizontal(|ui| { | ||||
|                     ui.label("TIMA"); | ||||
|                     ui.monospace(format!("{:#04X}", timer.counter)); | ||||
|                 }); | ||||
|  | ||||
|                 ui.horizontal(|ui| { | ||||
|                     ui.label("TMA"); | ||||
|                     ui.monospace(format!("{:#04X}", timer.modulo)); | ||||
|                 }); | ||||
|  | ||||
|                 ui.horizontal(|ui| { | ||||
|                     ui.label("TAC"); | ||||
|                     ui.monospace(format!("{:?}", timer.ctrl)); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|         egui::Window::new("Registers") | ||||
|             .open(&mut self.show_registers) | ||||
|             .show(ctx, |ui| { | ||||
|                 ui.horizontal(|ui| { | ||||
|                     ui.label("A"); | ||||
|                     ui.monospace(format!("{:#04X}", game_boy.register(Register::A))); | ||||
|  | ||||
|                     ui.label("F"); | ||||
|                     ui.monospace(format!("{:#04X}", game_boy.register(Register::Flag))); | ||||
|                 }); | ||||
|  | ||||
|                 ui.horizontal(|ui| { | ||||
|                     ui.label("B"); | ||||
|                     ui.monospace(format!("{:#04X}", game_boy.register(Register::B))); | ||||
|  | ||||
|                     ui.label("C"); | ||||
|                     ui.monospace(format!("{:#04X}", game_boy.register(Register::C))); | ||||
|                 }); | ||||
|  | ||||
|                 ui.horizontal(|ui| { | ||||
|                     ui.label("D"); | ||||
|                     ui.monospace(format!("{:#04X}", game_boy.register(Register::D))); | ||||
|  | ||||
|                     ui.label("E"); | ||||
|                     ui.monospace(format!("{:#04X}", game_boy.register(Register::E))); | ||||
|                 }); | ||||
|  | ||||
|                 ui.horizontal(|ui| { | ||||
|                     ui.label("H"); | ||||
|                     ui.monospace(format!("{:#04X}", game_boy.register(Register::H))); | ||||
|  | ||||
|                     ui.label("L"); | ||||
|                     ui.monospace(format!("{:#04X}", game_boy.register(Register::L))); | ||||
|                 }); | ||||
|  | ||||
|                 ui.horizontal(|ui| { | ||||
|                     ui.label("AF"); | ||||
|                     ui.monospace(format!("{:#06X}", game_boy.register_pair(RegisterPair::AF))); | ||||
|                 }); | ||||
|  | ||||
|                 ui.horizontal(|ui| { | ||||
|                     ui.label("BC"); | ||||
|                     ui.monospace(format!("{:#06X}", game_boy.register_pair(RegisterPair::BC))); | ||||
|                 }); | ||||
|  | ||||
|                 ui.horizontal(|ui| { | ||||
|                     ui.label("DE"); | ||||
|                     ui.monospace(format!("{:#06X}", game_boy.register_pair(RegisterPair::DE))); | ||||
|                 }); | ||||
|  | ||||
|                 ui.horizontal(|ui| { | ||||
|                     ui.label("HL"); | ||||
|                     ui.monospace(format!("{:#06X}", game_boy.register_pair(RegisterPair::HL))); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|         egui::Window::new("Cpu Information") | ||||
|             .open(&mut self.show_cpu_info) | ||||
|             .show(ctx, |ui| { | ||||
|                 ui.horizontal(|ui| { | ||||
|                     ui.label("PC"); | ||||
|                     ui.monospace(format!("{:#06X}", game_boy.register_pair(RegisterPair::PC))); | ||||
|                 }); | ||||
|  | ||||
|                 ui.horizontal(|ui| { | ||||
|                     ui.label("SP"); | ||||
|                     ui.monospace(format!("{:#06X}", game_boy.register_pair(RegisterPair::SP))); | ||||
|                 }); | ||||
|  | ||||
|                 ui.horizontal(|ui| { | ||||
|                     ui.label("IME"); | ||||
|                     ui.label(format!("{:?}", game_boy.ime())); | ||||
|                 }); | ||||
|  | ||||
|                 ui.horizontal(|ui| { | ||||
|                     ui.label("HALT"); | ||||
|                     ui.label(format!("{:?}", game_boy.halted())); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|         egui::Window::new("IRQ Information") | ||||
|             .open(&mut self.show_int) | ||||
|             .show(ctx, |ui| { | ||||
|                 let req = game_boy.read_byte(0xFF0F); | ||||
|                 let enabled = game_boy.read_byte(0xFFFF); | ||||
|  | ||||
|                 ui.heading("Interrupt Requests"); | ||||
|                 ui.horizontal(|ui| { | ||||
|                     let _ = ui.selectable_label(req & 0x01 == 0x01, "VBLANK"); | ||||
|                     let _ = ui.selectable_label((req >> 1) & 0x01 == 0x01, "LCD STAT"); | ||||
|                     let _ = ui.selectable_label((req >> 2) & 0x01 == 0x01, "TIMER"); | ||||
|                     let _ = ui.selectable_label((req >> 3) & 0x01 == 0x01, "SERIAL"); | ||||
|                     let _ = ui.selectable_label((req >> 4) & 0x01 == 0x01, "JOYPAD"); | ||||
|                 }); | ||||
|  | ||||
|                 ui.heading("Interrupt Enable"); | ||||
|                 ui.horizontal(|ui| { | ||||
|                     let _ = ui.selectable_label(enabled & 0x01 == 0x01, "VBLANK"); | ||||
|                     let _ = ui.selectable_label((enabled >> 1) & 0x01 == 0x01, "LCD STAT"); | ||||
|                     let _ = ui.selectable_label((enabled >> 2) & 0x01 == 0x01, "TIMER"); | ||||
|                     let _ = ui.selectable_label((enabled >> 3) & 0x01 == 0x01, "SERIAL"); | ||||
|                     let _ = ui.selectable_label((enabled >> 4) & 0x01 == 0x01, "JOYPAD"); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|         egui::Window::new("Configuration") | ||||
|             .open(&mut self.config.show) | ||||
|             .show(ctx, |_ui| {}); | ||||
|     } | ||||
|  | ||||
|     /// Render egui. | ||||
|     pub fn render( | ||||
|         &mut self, | ||||
|         encoder: &mut wgpu::CommandEncoder, | ||||
|         render_target: &wgpu::TextureView, | ||||
|         context: &PixelsContext, | ||||
|     ) { | ||||
|         // Upload all resources to the GPU. | ||||
|         self.render_pass.update_texture( | ||||
|             &context.device, | ||||
|             &context.queue, | ||||
|             &self.platform.context().texture(), | ||||
|         ); | ||||
|         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 | ||||
|             .update_user_textures(&context.device, &context.queue); | ||||
|         self.render_pass.update_buffers( | ||||
|             &context.device, | ||||
|             &context.queue, | ||||
|             &self.paint_jobs, | ||||
|             &self.screen_desc, | ||||
|         ); | ||||
|             .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( | ||||
|             encoder, | ||||
|             render_target, | ||||
|             &self.paint_jobs, | ||||
|             &self.screen_desc, | ||||
|         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) | ||||
| } | ||||
|  | ||||
| pub struct Configuration { | ||||
|     /// Show Configuration egui menu | ||||
|     show: bool, | ||||
| #[inline] | ||||
| fn create_command_encoder(device: &Device) -> CommandEncoder { | ||||
|     use wgpu::CommandEncoderDescriptor; | ||||
|  | ||||
|     device.create_command_encoder(&CommandEncoderDescriptor { | ||||
|         label: Some("encoder"), | ||||
|     }) | ||||
| } | ||||
|  | ||||
| impl Default for Configuration { | ||||
|     fn default() -> Self { | ||||
|         Self { show: false } | ||||
| #[inline] | ||||
| fn create_screen_descriptor(window: &Window, config: &SurfaceConfiguration) -> ScreenDescriptor { | ||||
|     ScreenDescriptor { | ||||
|         physical_width: config.width, | ||||
|         physical_height: config.height, | ||||
|         scale_factor: window.scale_factor() as f32, | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub mod kbd { | ||||
|     use winit::event::{ElementState, KeyboardInput, VirtualKeyCode}; | ||||
|  | ||||
|     pub fn space_released(input: &KeyboardInput) -> bool { | ||||
|         let keycode = input.virtual_keycode; | ||||
|         matches!(input.state, ElementState::Released if keycode == Some(VirtualKeyCode::Space)) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -17,11 +17,11 @@ impl Default for HighRam { | ||||
| } | ||||
|  | ||||
| impl BusIo for HighRam { | ||||
|     fn write_byte(&mut self, addr: u16, byte: u8) { | ||||
|         self.buf[addr as usize - HIGH_RAM_START_ADDRESS] = byte; | ||||
|     } | ||||
|  | ||||
|     fn read_byte(&self, addr: u16) -> u8 { | ||||
|         self.buf[addr as usize - HIGH_RAM_START_ADDRESS] | ||||
|     } | ||||
|  | ||||
|     fn write_byte(&mut self, addr: u16, byte: u8) { | ||||
|         self.buf[addr as usize - HIGH_RAM_START_ADDRESS] = byte; | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										3171
									
								
								src/instruction.rs
									
									
									
									
									
								
							
							
						
						
									
										3171
									
								
								src/instruction.rs
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,11 +1,20 @@ | ||||
| use bitfield::bitfield; | ||||
|  | ||||
| #[derive(Debug, Clone, Copy, Default)] | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct Interrupt { | ||||
|     pub(crate) flag: InterruptFlag, | ||||
|     pub(crate) enable: InterruptEnable, | ||||
| } | ||||
|  | ||||
| impl Default for Interrupt { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             flag: InterruptFlag(0), | ||||
|             enable: InterruptEnable(0), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| bitfield! { | ||||
|    pub struct InterruptEnable(u8); | ||||
|     impl Debug; | ||||
| @@ -23,12 +32,6 @@ impl Clone for InterruptEnable { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for InterruptEnable { | ||||
|     fn default() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for InterruptEnable { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         Self(byte) | ||||
| @@ -58,12 +61,6 @@ impl Clone for InterruptFlag { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for InterruptFlag { | ||||
|     fn default() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for InterruptFlag { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         Self(byte) | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| use gilrs::{Button, Event as GamepadEvent, EventType as GamepadEventType}; | ||||
| use winit_input_helper::WinitInputHelper; | ||||
| use winit::event::{ElementState, KeyboardInput, VirtualKeyCode}; | ||||
|  | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| #[derive(Debug)] | ||||
| pub struct Joypad { | ||||
|     /// 0xFF00 | P1/JOYP - Player 1 Joypad | ||||
|     pub(crate) p1: u8, | ||||
| @@ -48,7 +48,7 @@ impl Joypad { | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy, Default)] | ||||
| #[derive(Debug, Default)] | ||||
| struct JoypadState { | ||||
|     // Direction Row | ||||
|     dpad_down: ButtonEvent, | ||||
| @@ -110,64 +110,39 @@ impl ButtonEvent { | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn handle_keyboard_input(pad: &mut Joypad, input: &WinitInputHelper) { | ||||
|     use winit::event::VirtualKeyCode; | ||||
|  | ||||
|     // TODO: What do I have to do to get a match statement here? | ||||
|  | ||||
| #[inline] | ||||
| pub(crate) fn handle_keyboard_input(pad: &mut Joypad, key: KeyboardInput) { | ||||
|     let state = &mut pad.ext; | ||||
|     let irq = &mut pad.interrupt; | ||||
|  | ||||
|     if input.key_pressed(VirtualKeyCode::Down) { | ||||
|         state.dpad_down.update(true, irq); | ||||
|     } else if input.key_released(VirtualKeyCode::Down) { | ||||
|         state.dpad_down.update(false, irq); | ||||
|     } | ||||
|  | ||||
|     if input.key_pressed(VirtualKeyCode::Up) { | ||||
|         state.dpad_up.update(true, irq); | ||||
|     } else if input.key_released(VirtualKeyCode::Up) { | ||||
|         state.dpad_up.update(false, irq); | ||||
|     } | ||||
|  | ||||
|     if input.key_pressed(VirtualKeyCode::Left) { | ||||
|         state.dpad_left.update(true, irq); | ||||
|     } else if input.key_released(VirtualKeyCode::Left) { | ||||
|         state.dpad_left.update(false, irq); | ||||
|     } | ||||
|  | ||||
|     if input.key_pressed(VirtualKeyCode::Right) { | ||||
|         state.dpad_right.update(true, irq); | ||||
|     } else if input.key_released(VirtualKeyCode::Right) { | ||||
|         state.dpad_right.update(false, irq); | ||||
|     } | ||||
|  | ||||
|     if input.key_pressed(VirtualKeyCode::T) { | ||||
|         state.start.update(true, irq); | ||||
|     } else if input.key_released(VirtualKeyCode::T) { | ||||
|         state.start.update(false, irq); | ||||
|     } | ||||
|  | ||||
|     if input.key_pressed(VirtualKeyCode::Y) { | ||||
|         state.select.update(true, irq); | ||||
|     } else if input.key_released(VirtualKeyCode::Y) { | ||||
|         state.select.update(false, irq); | ||||
|     } | ||||
|  | ||||
|     if input.key_pressed(VirtualKeyCode::Z) { | ||||
|         state.south.update(true, irq); | ||||
|     } else if input.key_released(VirtualKeyCode::Z) { | ||||
|         state.south.update(false, irq); | ||||
|     } | ||||
|  | ||||
|     if input.key_pressed(VirtualKeyCode::X) { | ||||
|         state.east.update(true, irq); | ||||
|     } else if input.key_released(VirtualKeyCode::X) { | ||||
|         state.east.update(false, irq); | ||||
|     match key.state { | ||||
|         ElementState::Pressed => match key.virtual_keycode { | ||||
|             Some(VirtualKeyCode::Down) => state.dpad_down.update(true, irq), | ||||
|             Some(VirtualKeyCode::Up) => state.dpad_up.update(true, irq), | ||||
|             Some(VirtualKeyCode::Left) => state.dpad_left.update(true, irq), | ||||
|             Some(VirtualKeyCode::Right) => state.dpad_right.update(true, irq), | ||||
|             Some(VirtualKeyCode::Return) => state.start.update(true, irq), | ||||
|             Some(VirtualKeyCode::RShift) => state.select.update(true, irq), | ||||
|             Some(VirtualKeyCode::Z) => state.south.update(true, irq), | ||||
|             Some(VirtualKeyCode::X) => state.east.update(true, irq), | ||||
|             None | Some(_) => {} | ||||
|         }, | ||||
|         ElementState::Released => match key.virtual_keycode { | ||||
|             Some(VirtualKeyCode::Down) => state.dpad_down.update(false, irq), | ||||
|             Some(VirtualKeyCode::Up) => state.dpad_up.update(false, irq), | ||||
|             Some(VirtualKeyCode::Left) => state.dpad_left.update(false, irq), | ||||
|             Some(VirtualKeyCode::Right) => state.dpad_right.update(false, irq), | ||||
|             Some(VirtualKeyCode::Return) => state.start.update(false, irq), | ||||
|             Some(VirtualKeyCode::RShift) => state.select.update(false, irq), | ||||
|             Some(VirtualKeyCode::Z) => state.south.update(false, irq), | ||||
|             Some(VirtualKeyCode::X) => state.east.update(false, irq), | ||||
|             None | Some(_) => {} | ||||
|         }, | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn handle_gamepad_input(pad: &mut Joypad, event: GamepadEvent) { | ||||
| #[inline] | ||||
| pub(crate) fn handle_gamepad_input(pad: &mut Joypad, event: GamepadEvent) { | ||||
|     use Button::*; | ||||
|     use GamepadEventType::*; | ||||
|  | ||||
|   | ||||
							
								
								
									
										11
									
								
								src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								src/lib.rs
									
									
									
									
									
								
							| @@ -1,21 +1,22 @@ | ||||
| pub use gui::Egui; | ||||
| pub use instruction::Cycle; | ||||
| pub use sound::AudioMPSC; | ||||
| #![allow(clippy::derivable_impls)] // Will remove this if bitfield-rs allows default impls | ||||
|  | ||||
| pub use apu::gen::init as spsc_init; | ||||
| pub type Cycle = u64; | ||||
|  | ||||
| pub const GB_WIDTH: usize = 160; | ||||
| pub const GB_HEIGHT: usize = 144; | ||||
|  | ||||
| mod apu; | ||||
| mod bus; | ||||
| mod cartridge; | ||||
| mod cpu; | ||||
| pub mod emu; | ||||
| mod gui; | ||||
| pub mod gui; | ||||
| mod high_ram; | ||||
| mod instruction; | ||||
| mod interrupt; | ||||
| mod joypad; | ||||
| mod ppu; | ||||
| mod serial; | ||||
| mod sound; | ||||
| mod timer; | ||||
| mod work_ram; | ||||
|   | ||||
							
								
								
									
										212
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										212
									
								
								src/main.rs
									
									
									
									
									
								
							| @@ -1,154 +1,126 @@ | ||||
| use anyhow::{anyhow, Result}; | ||||
| use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg}; | ||||
| use gb::{AudioMPSC, Cycle, Egui, GB_HEIGHT, GB_WIDTH}; | ||||
| use gilrs::Gilrs; | ||||
| use pixels::{Pixels, SurfaceTexture}; | ||||
| use rodio::{OutputStream, Sink}; | ||||
| use std::path::PathBuf; | ||||
| use std::time::Instant; | ||||
| use winit::dpi::LogicalSize; | ||||
| use winit::event::{Event, VirtualKeyCode}; | ||||
| use winit::event_loop::{ControlFlow, EventLoop}; | ||||
| use winit::window::{Window, WindowBuilder}; | ||||
| use winit_input_helper::WinitInputHelper; | ||||
|  | ||||
| const SCALE: f64 = 2.0; | ||||
| use clap::{arg, command, value_parser}; | ||||
| use gb::gui::{EmuMode, Gui}; | ||||
| use gb::{emu, gui}; | ||||
| use gilrs::Gilrs; | ||||
| use rodio::{OutputStream, Sink}; | ||||
| use tracing_subscriber::EnvFilter; | ||||
| use winit::event::{Event, WindowEvent}; | ||||
| use winit::event_loop::{EventLoop, EventLoopBuilder}; | ||||
|  | ||||
| fn main() -> Result<()> { | ||||
|     let app = App::new(crate_name!()) | ||||
|         .version(crate_version!()) | ||||
|         .author(crate_authors!()) | ||||
|         .about(crate_description!()); | ||||
| const AUDIO_ENABLED: bool = true; | ||||
|  | ||||
|     let m = app | ||||
| fn main() { | ||||
|     let m = command!() | ||||
|         .arg( | ||||
|             Arg::with_name("rom") | ||||
|                 .value_name("ROM_FILE") | ||||
|                 .takes_value(true) | ||||
|                 .required(true) | ||||
|                 .index(1) | ||||
|                 .help("Path to the Game ROM"), | ||||
|             arg!(-b --boot <FILE> "path to boot ROM") | ||||
|                 .required(false) | ||||
|                 .value_parser(value_parser!(PathBuf)), | ||||
|         ) | ||||
|         .arg( | ||||
|             Arg::with_name("boot") | ||||
|                 .short("b") | ||||
|                 .long("boot") | ||||
|                 .value_name("FILE") | ||||
|                 .takes_value(true) | ||||
|                 .help("Path to Boot ROM"), | ||||
|             arg!([ROM_FILE] "path to game ROM") | ||||
|                 .required(true) | ||||
|                 .value_parser(value_parser!(PathBuf)), | ||||
|         ) | ||||
|         .get_matches(); | ||||
|  | ||||
|     // `rom` is a required value in every situation so this will | ||||
|     // always exist. | ||||
|     let rom_path = m | ||||
|         .value_of("rom") | ||||
|         .expect("Required value 'rom' was provided"); | ||||
|     // Set up subscriber | ||||
|     if std::env::var("RUST_LOG").is_err() { | ||||
|         std::env::set_var("RUST_LOG", "gb=info"); | ||||
|     } | ||||
|  | ||||
|     let mut game_boy = | ||||
|         gb::emu::init(m.value_of("boot"), rom_path).expect("Initialized DMG-01 Emulator"); | ||||
|     let cartridge_title = gb::emu::rom_title(&game_boy); | ||||
|     tracing_subscriber::fmt::fmt() | ||||
|         .with_env_filter(EnvFilter::from_default_env()) | ||||
|         .init(); | ||||
|  | ||||
|     // Initialize Gamepad Support | ||||
|     let mut gamepad = Gilrs::new().expect("Initialized Gilrs for Controller Input"); | ||||
|  | ||||
|     // Initialize GUI | ||||
|     let event_loop = EventLoop::new(); | ||||
|     let mut input = WinitInputHelper::new(); | ||||
|     let window = create_window(&event_loop, cartridge_title)?; | ||||
|  | ||||
|     let (mut pixels, mut egui) = { | ||||
|         let size = window.inner_size(); | ||||
|         let scale_factor = window.scale_factor(); | ||||
|         let surface_texture = SurfaceTexture::new(size.width, size.height, &window); | ||||
|         let pixels = Pixels::new(GB_WIDTH as u32, GB_HEIGHT as u32, surface_texture)?; | ||||
|         let egui = Egui::new(size.width, size.height, scale_factor, pixels.context()); | ||||
|  | ||||
|         (pixels, egui) | ||||
|     // Init CPU | ||||
|     let mut cpu = match m.get_one::<PathBuf>("boot") { | ||||
|         Some(path) => { | ||||
|             tracing::info!("User-provided boot ROM"); | ||||
|             emu::from_boot_rom(path).expect("initialize emulator with custom boot rom") | ||||
|         } | ||||
|         None => { | ||||
|             tracing::info!("Built-in boot ROM"); | ||||
|             Default::default() | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     let (send, recv) = AudioMPSC::new(); | ||||
|     game_boy.set_audio_src(send); | ||||
|     // Load ROM if filepath was provided | ||||
|     if let Some(path) = m.get_one::<PathBuf>("ROM_FILE") { | ||||
|         tracing::info!("User-provided cartridge ROM"); | ||||
|         emu::read_game_rom(&mut cpu, path).expect("read game rom from path"); | ||||
|     } | ||||
|     emu::load_save(&mut cpu); | ||||
|     let rom_title = emu::rom_title(&cpu).to_string(); | ||||
|  | ||||
|     tracing::info!("Initialize Gamepad"); | ||||
|     let mut gamepad = Gilrs::new().expect("Initialize Controller Support"); | ||||
|  | ||||
|     // Initialize Audio | ||||
|     let (_stream, stream_handle) = OutputStream::try_default().expect("Initialized Audio"); | ||||
|     let sink = Sink::try_new(&stream_handle).expect("Initialize Audio Sink"); | ||||
|  | ||||
|     if AUDIO_ENABLED { | ||||
|         let (prod, cons) = gb::spsc_init(); | ||||
|         let sink = { | ||||
|             let s = Sink::try_new(&stream_handle).expect("create sink from audio stream handle"); | ||||
|             s.append(cons); | ||||
|             s.set_volume(0.1); | ||||
|             s | ||||
|         }; | ||||
|  | ||||
|         emu::set_audio_prod(&mut cpu, prod); | ||||
|  | ||||
|         tracing::info!("Spawn Audio Thread"); | ||||
|         std::thread::spawn(move || { | ||||
|         sink.append(recv); | ||||
|  | ||||
|             sink.sleep_until_end(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     let mut now = Instant::now(); | ||||
|     let mut cycle_count: Cycle = Default::default(); | ||||
|     // Set up state for the Immediate-mode GUI | ||||
|     let event_loop: EventLoop<Event<()>> = EventLoopBuilder::with_user_event().build(); | ||||
|  | ||||
|     let mut app = Gui::new(rom_title, &event_loop, &cpu); | ||||
|     let mut last_key = gui::unused_key(); // TODO: Fix this awful impl | ||||
|  | ||||
|     // used for egui animations | ||||
|     let start_time = Instant::now(); | ||||
|  | ||||
|     event_loop.run(move |event, _, control_flow| { | ||||
|         // Update egui | ||||
|         egui.handle_event(&event); | ||||
|         app.handle_event(&event); | ||||
|  | ||||
|         if let Event::RedrawRequested(_) = event { | ||||
|             // Prepare egui | ||||
|             egui.prepare(&game_boy); | ||||
|         match event { | ||||
|             Event::MainEventsCleared => { | ||||
|                 app.maybe_quit(&cpu, control_flow); | ||||
|  | ||||
|             // Render everything together | ||||
|             let render_result = pixels.render_with(|encoder, target, ctx| { | ||||
|                 // Render the texture | ||||
|                 ctx.scaling_renderer.render(encoder, target); | ||||
|  | ||||
|                 // Render egui | ||||
|                 egui.render(encoder, target, ctx); | ||||
|             }); | ||||
|  | ||||
|             if render_result | ||||
|                 .map_err(|e| anyhow!("pixels.render() failed: {}", e)) | ||||
|                 .is_err() | ||||
|             { | ||||
|                 *control_flow = ControlFlow::Exit; | ||||
|                 return; | ||||
|                 match app.mode { | ||||
|                     EmuMode::Running => emu::run_frame(&mut cpu, &mut gamepad, last_key), | ||||
|                     EmuMode::StepFrame if gui::kbd::space_released(&last_key) => { | ||||
|                         emu::run_frame(&mut cpu, &mut gamepad, last_key) | ||||
|                     } | ||||
|                     EmuMode::Step if gui::kbd::space_released(&last_key) => { | ||||
|                         emu::run(&mut cpu, &mut gamepad, last_key, 4); | ||||
|                     } | ||||
|                     _ => {} | ||||
|                 }; | ||||
|  | ||||
|         if input.update(&event) { | ||||
|             if input.key_pressed(VirtualKeyCode::Escape) || input.quit() { | ||||
|                 *control_flow = ControlFlow::Exit; | ||||
|                 return; | ||||
|                 // Input has been consumed, reset it | ||||
|                 last_key = gui::unused_key(); | ||||
|  | ||||
|                 app.request_redraw(); | ||||
|             } | ||||
|  | ||||
|             if let Some(scale_factor) = input.scale_factor() { | ||||
|                 egui.scale_factor(scale_factor); | ||||
|             } | ||||
|  | ||||
|             if let Some(size) = input.window_resized() { | ||||
|                 pixels.resize_surface(size.width, size.height); | ||||
|                 egui.resize(size.width, size.height); | ||||
|             } | ||||
|  | ||||
|             let delta = now.elapsed().subsec_nanos(); | ||||
|             now = Instant::now(); | ||||
|  | ||||
|             let pending = Cycle::new(delta / gb::emu::SM83_CYCLE_TIME.subsec_nanos()); | ||||
|             cycle_count += gb::emu::run(&mut game_boy, &mut gamepad, &input, pending); | ||||
|  | ||||
|             if cycle_count >= gb::emu::CYCLES_IN_FRAME { | ||||
|                 // Draw Frame | ||||
|                 cycle_count = Cycle::new(0); | ||||
|  | ||||
|                 gb::emu::draw(game_boy.ppu(), pixels.get_frame()); | ||||
|                 window.request_redraw(); | ||||
|             Event::RedrawRequested(..) => { | ||||
|                 app.update_time(start_time.elapsed().as_secs_f64()); | ||||
|                 app.paint(&cpu); | ||||
|             } | ||||
|             Event::WindowEvent { event, .. } => match event { | ||||
|                 WindowEvent::Resized(size) => app.resize(size), | ||||
|                 WindowEvent::CloseRequested => emu::save_and_exit(&cpu, control_flow), | ||||
|                 WindowEvent::KeyboardInput { input, .. } => last_key = input, | ||||
|                 _ => {} | ||||
|             }, | ||||
|             _ => {} | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| fn create_window(event_loop: &EventLoop<()>, title: &str) -> Result<Window> { | ||||
|     let size = LogicalSize::new((GB_WIDTH as f64) * SCALE, (GB_HEIGHT as f64) * 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) // OleInitialize failed error if this is set to true | ||||
|         .build(event_loop)?) | ||||
| } | ||||
|   | ||||
							
								
								
									
										854
									
								
								src/ppu.rs
									
									
									
									
									
								
							
							
						
						
									
										854
									
								
								src/ppu.rs
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,6 +1,6 @@ | ||||
| use crate::instruction::Cycle; | ||||
| use crate::Cycle; | ||||
|  | ||||
| #[derive(Debug, Default, Clone)] | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct DirectMemoryAccess { | ||||
|     pub(crate) state: DmaState, | ||||
|     cycle: Cycle, | ||||
| @@ -8,8 +8,18 @@ pub(crate) struct DirectMemoryAccess { | ||||
|     pub(crate) start: DmaAddress, | ||||
| } | ||||
|  | ||||
| impl Default for DirectMemoryAccess { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             state: DmaState::Disabled, | ||||
|             cycle: Default::default(), | ||||
|             start: Default::default(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl DirectMemoryAccess { | ||||
|     pub(crate) fn clock(&mut self) -> Option<(u16, u16)> { | ||||
|     pub(crate) fn tick(&mut self) -> Option<(u16, u16)> { | ||||
|         match self.state { | ||||
|             DmaState::Pending => { | ||||
|                 self.cycle += 1; | ||||
| @@ -28,7 +38,7 @@ impl DirectMemoryAccess { | ||||
|  | ||||
|                 let src_addr = self | ||||
|                     .start | ||||
|                     .addr | ||||
|                     .0 | ||||
|                     .as_mut() | ||||
|                     .expect("Source Address present during DMA Transfer"); | ||||
|  | ||||
| @@ -56,9 +66,9 @@ impl DirectMemoryAccess { | ||||
|     } | ||||
|  | ||||
|     fn reset(&mut self) { | ||||
|         self.cycle = Cycle::new(0); | ||||
|         self.cycle = 0; | ||||
|         self.state = DmaState::Disabled; | ||||
|         self.start.addr = None; | ||||
|         self.start.0 = None; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -69,32 +79,21 @@ pub(crate) enum DmaState { | ||||
|     Transferring, | ||||
| } | ||||
|  | ||||
| impl Default for DmaState { | ||||
|     fn default() -> Self { | ||||
|         Self::Disabled | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Default, Clone, Copy)] | ||||
| pub(crate) struct DmaAddress { | ||||
|     /// The current *source* address of the DMA Transfer | ||||
|     /// | ||||
|     /// NB: Will be None if no DMA Transfer is in progress | ||||
|     addr: Option<u16>, | ||||
| } | ||||
| #[derive(Debug, Clone, Copy, Default)] | ||||
| pub(crate) struct DmaAddress(Option<u16>); | ||||
|  | ||||
| impl DmaAddress { | ||||
|     pub(crate) fn update(&mut self, byte: u8, state: &mut DmaState) { | ||||
|         let start = (byte as u16) << 8; | ||||
|  | ||||
|         self.addr = Some(start); | ||||
|         self.0 = Some(start); | ||||
|         *state = DmaState::Pending; | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<DmaAddress> for u8 { | ||||
|     fn from(ctrl: DmaAddress) -> Self { | ||||
|         match ctrl.addr { | ||||
|         match ctrl.0 { | ||||
|             Some(addr) => (addr >> 8) as u8, | ||||
|             None => 0xFF, // TODO: What garbage value should be here? | ||||
|         } | ||||
|   | ||||
| @@ -29,12 +29,6 @@ impl Clone for LCDStatus { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for LCDStatus { | ||||
|     fn default() -> Self { | ||||
|         Self(0x80) // bit 7 is always 1 | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<LCDStatus> for u8 { | ||||
|     fn from(status: LCDStatus) -> Self { | ||||
|         status.0 | ||||
| @@ -67,12 +61,6 @@ impl From<PpuMode> for u8 { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for PpuMode { | ||||
|     fn default() -> Self { | ||||
|         Self::HBlank | ||||
|     } | ||||
| } | ||||
|  | ||||
| bitfield! { | ||||
|     pub struct LCDControl(u8); | ||||
|     impl Debug; | ||||
| @@ -93,12 +81,6 @@ impl Clone for LCDControl { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for LCDControl { | ||||
|     fn default() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for LCDControl { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         Self(byte) | ||||
| @@ -142,12 +124,6 @@ impl From<TileMapAddress> for u8 { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for TileMapAddress { | ||||
|     fn default() -> Self { | ||||
|         Self::X9800 | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub enum TileDataAddress { | ||||
|     X8800 = 0, | ||||
| @@ -170,12 +146,6 @@ impl From<TileDataAddress> for u8 { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for TileDataAddress { | ||||
|     fn default() -> Self { | ||||
|         Self::X8800 | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub enum ObjectSize { | ||||
|     Eight = 0, | ||||
| @@ -183,7 +153,7 @@ pub enum ObjectSize { | ||||
| } | ||||
|  | ||||
| impl ObjectSize { | ||||
|     pub(crate) fn as_u8(&self) -> u8 { | ||||
|     pub(crate) fn size(&self) -> u8 { | ||||
|         use ObjectSize::*; | ||||
|  | ||||
|         match self { | ||||
| @@ -209,12 +179,6 @@ impl From<ObjectSize> for u8 { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for ObjectSize { | ||||
|     fn default() -> Self { | ||||
|         Self::Eight | ||||
|     } | ||||
| } | ||||
|  | ||||
| bitfield! { | ||||
|     pub struct BackgroundPalette(u8); | ||||
|     impl Debug; | ||||
| @@ -243,12 +207,6 @@ impl Clone for BackgroundPalette { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for BackgroundPalette { | ||||
|     fn default() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for BackgroundPalette { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         Self(byte) | ||||
| @@ -288,12 +246,6 @@ impl Clone for ObjectPalette { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for ObjectPalette { | ||||
|     fn default() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for ObjectPalette { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         Self(byte) | ||||
| @@ -332,7 +284,7 @@ bitfield! { | ||||
|     pub from into RenderPriority, priority, set_priority: 7, 7; | ||||
|     pub y_flip, set_y_flip: 6; | ||||
|     pub x_flip, set_x_flip: 5; | ||||
|     pub from into ObjectPaletteId, palette, set_palette: 4, 4; | ||||
|     pub from into ObjectPaletteKind, palette, set_palette: 4, 4; | ||||
| } | ||||
|  | ||||
| impl Eq for ObjectFlags {} | ||||
| @@ -361,35 +313,29 @@ impl From<ObjectFlags> for u8 { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for ObjectFlags { | ||||
|     fn default() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub enum ObjectPaletteId { | ||||
| pub enum ObjectPaletteKind { | ||||
|     Zero = 0, | ||||
|     One = 1, | ||||
| } | ||||
|  | ||||
| impl From<u8> for ObjectPaletteId { | ||||
| impl From<u8> for ObjectPaletteKind { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         match byte & 0b01 { | ||||
|             0b00 => ObjectPaletteId::Zero, | ||||
|             0b01 => ObjectPaletteId::One, | ||||
|             0b00 => ObjectPaletteKind::Zero, | ||||
|             0b01 => ObjectPaletteKind::One, | ||||
|             _ => unreachable!("{:#04X} is not a valid value for BgPaletteNumber", byte), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<ObjectPaletteId> for u8 { | ||||
|     fn from(palette_num: ObjectPaletteId) -> Self { | ||||
| impl From<ObjectPaletteKind> for u8 { | ||||
|     fn from(palette_num: ObjectPaletteKind) -> Self { | ||||
|         palette_num as u8 | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy, PartialEq)] | ||||
| #[derive(Debug, Clone, Copy, Eq, PartialEq)] | ||||
| pub enum RenderPriority { | ||||
|     Object = 0, | ||||
|     BackgroundAndWindow = 1, | ||||
| @@ -411,12 +357,6 @@ impl From<RenderPriority> for u8 { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for RenderPriority { | ||||
|     fn default() -> Self { | ||||
|         Self::Object | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub enum GrayShade { | ||||
|     White = 0, | ||||
| @@ -436,12 +376,6 @@ impl GrayShade { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for GrayShade { | ||||
|     fn default() -> Self { | ||||
|         Self::White | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for GrayShade { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         match byte & 0b11 { | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| use bitfield::bitfield; | ||||
|  | ||||
| #[derive(Debug, Clone, Copy, Default)] | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct Serial { | ||||
|     /// 0xFF01 | SB - Serial Transfer Data | ||||
|     pub(crate) next: u8, | ||||
| @@ -8,6 +8,15 @@ pub(crate) struct Serial { | ||||
|     pub(crate) ctrl: SerialControl, | ||||
| } | ||||
|  | ||||
| impl Default for Serial { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             next: Default::default(), | ||||
|             ctrl: SerialControl(0), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| bitfield! { | ||||
|     pub struct SerialControl(u8); | ||||
|     impl Debug; | ||||
| @@ -23,12 +32,6 @@ impl Clone for SerialControl { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for SerialControl { | ||||
|     fn default() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for SerialControl { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         Self(byte) | ||||
| @@ -47,12 +50,6 @@ enum ShiftClock { | ||||
|     Internal = 1, | ||||
| } | ||||
|  | ||||
| impl Default for ShiftClock { | ||||
|     fn default() -> Self { | ||||
|         Self::External | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for ShiftClock { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         match byte & 0b01 { | ||||
| @@ -69,12 +66,6 @@ enum ClockSpeed { | ||||
|     Fast = 1, | ||||
| } | ||||
|  | ||||
| impl Default for ClockSpeed { | ||||
|     fn default() -> Self { | ||||
|         Self::Normal | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for ClockSpeed { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         match byte & 0b01 { | ||||
|   | ||||
							
								
								
									
										1235
									
								
								src/sound.rs
									
									
									
									
									
								
							
							
						
						
									
										1235
									
								
								src/sound.rs
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										96
									
								
								src/timer.rs
									
									
									
									
									
								
							
							
						
						
									
										96
									
								
								src/timer.rs
									
									
									
									
									
								
							| @@ -1,21 +1,24 @@ | ||||
| use bitfield::bitfield; | ||||
|  | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct Timer { | ||||
|     /// 0xFF07 | TAC - Timer Control | ||||
|     pub(crate) ctrl: TimerControl, | ||||
|     /// 0xFF05 | TIMA - Timer Counter | ||||
|     pub(crate) counter: u8, | ||||
|     counter: u8, | ||||
|     /// 0xFF06 | TMA - Timer Modulo | ||||
|     pub(crate) modulo: u8, | ||||
|     /// 0xFF04 | DIV - Divider Register | ||||
|     pub(crate) divider: u16, | ||||
|     prev_and_result: Option<u8>, | ||||
|  | ||||
|     and_result: Option<u8>, | ||||
|     interrupt: bool, | ||||
|     state: State, | ||||
| } | ||||
|  | ||||
| impl Timer { | ||||
|     pub(crate) fn clock(&mut self) { | ||||
|     pub(crate) fn tick(&mut self) { | ||||
|         use State::*; | ||||
|         use TimerSpeed::*; | ||||
|  | ||||
|         self.divider = self.divider.wrapping_add(1); | ||||
| @@ -29,27 +32,45 @@ impl Timer { | ||||
|         }; | ||||
|  | ||||
|         let bit = (self.divider >> bit) as u8 & 0x01; | ||||
|         let timer_enable = self.ctrl.enabled() as u8; | ||||
|         let and_result = bit & timer_enable; | ||||
|         let new_result = bit & self.ctrl.enabled() as u8; | ||||
|  | ||||
|         if let Some(0x01) = self.prev_and_result { | ||||
|             if and_result == 0x00 { | ||||
|         if let Some(0x01) = self.and_result { | ||||
|             if new_result == 0x00 { | ||||
|                 // Falling Edge, increase TIMA Register | ||||
|                 self.increment_tima(); | ||||
|                 self.inc_counter(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         self.prev_and_result = Some(and_result); | ||||
|     } | ||||
|         self.and_result = Some(new_result); | ||||
|  | ||||
|     fn increment_tima(&mut self) { | ||||
|         let (result, did_overflow) = self.counter.overflowing_add(1); | ||||
|  | ||||
|         self.counter = if did_overflow { | ||||
|         match self.state { | ||||
|             TIMAOverflow(_) | AbortedTIMAOverflow(_) => self.next(), | ||||
|             LoadTIMA => { | ||||
|                 self.counter = self.modulo; | ||||
|                 self.interrupt = true; | ||||
|             self.modulo | ||||
|         } else { | ||||
|             result | ||||
|  | ||||
|                 self.next(); | ||||
|             } | ||||
|             Normal => {} | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// 0xFF05 | TIMA - Timer Counter | ||||
|     pub(crate) fn tima(&self) -> u8 { | ||||
|         self.counter | ||||
|     } | ||||
|  | ||||
|     /// 0xFF05 | TIMA - Timer Counter | ||||
|     pub(crate) fn set_tima(&mut self, byte: u8) { | ||||
|         use State::*; | ||||
|  | ||||
|         match self.state { | ||||
|             Normal | AbortedTIMAOverflow(_) => self.counter = byte, | ||||
|             TIMAOverflow(step) => { | ||||
|                 self.counter = byte; | ||||
|                 self.state = AbortedTIMAOverflow(step); | ||||
|             } | ||||
|             LoadTIMA => { /* Ignored */ } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -60,17 +81,38 @@ impl Timer { | ||||
|     pub(crate) fn set_interrupt(&mut self, value: bool) { | ||||
|         self.interrupt = value; | ||||
|     } | ||||
|  | ||||
|     fn inc_counter(&mut self) { | ||||
|         let (sum, did_overflow) = self.counter.overflowing_add(1); | ||||
|         self.counter = if did_overflow { 0 } else { sum }; | ||||
|  | ||||
|         if did_overflow { | ||||
|             self.state = State::TIMAOverflow(0); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn next(&mut self) { | ||||
|         use State::*; | ||||
|  | ||||
|         self.state = match self.state { | ||||
|             Normal | LoadTIMA | AbortedTIMAOverflow(3) => Normal, | ||||
|             TIMAOverflow(3) => LoadTIMA, | ||||
|             AbortedTIMAOverflow(step) => AbortedTIMAOverflow(step + 1), | ||||
|             TIMAOverflow(step) => TIMAOverflow(step + 1), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for Timer { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             ctrl: Default::default(), | ||||
|             ctrl: TimerControl(0), | ||||
|             counter: 0, | ||||
|             modulo: 0, | ||||
|             divider: 0, | ||||
|             interrupt: false, | ||||
|             prev_and_result: None, | ||||
|             and_result: None, | ||||
|             state: State::Normal, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -115,12 +157,6 @@ impl Clone for TimerControl { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for TimerControl { | ||||
|     fn default() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<u8> for TimerControl { | ||||
|     fn from(byte: u8) -> Self { | ||||
|         Self(byte) | ||||
| @@ -132,3 +168,11 @@ impl From<TimerControl> for u8 { | ||||
|         ctrl.0 | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||||
| enum State { | ||||
|     TIMAOverflow(u8), | ||||
|     AbortedTIMAOverflow(u8), | ||||
|     Normal, | ||||
|     LoadTIMA, | ||||
| } | ||||
|   | ||||
| @@ -5,19 +5,19 @@ const VARIABLE_WORK_RAM_SIZE: usize = WORK_RAM_SIZE; | ||||
| const WORK_RAM_START_ADDRESS: usize = 0xC000; | ||||
| const VARIABLE_WORK_RAM_START_ADDRESS: usize = 0xD000; | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct WorkRam { | ||||
|     bank: Box<[u8; WORK_RAM_SIZE]>, | ||||
| } | ||||
|  | ||||
| impl BusIo for WorkRam { | ||||
|     fn write_byte(&mut self, addr: u16, byte: u8) { | ||||
|         self.bank[addr as usize - WORK_RAM_START_ADDRESS] = byte; | ||||
|     } | ||||
|  | ||||
|     fn read_byte(&self, addr: u16) -> u8 { | ||||
|         self.bank[addr as usize - WORK_RAM_START_ADDRESS] | ||||
|     } | ||||
|  | ||||
|     fn write_byte(&mut self, addr: u16, byte: u8) { | ||||
|         self.bank[addr as usize - WORK_RAM_START_ADDRESS] = byte; | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for WorkRam { | ||||
| @@ -28,48 +28,25 @@ impl Default for WorkRam { | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| enum BankNumber { | ||||
|     One = 1, | ||||
|     Two = 2, | ||||
|     Three = 3, | ||||
|     Four = 4, | ||||
|     Five = 5, | ||||
|     Six = 6, | ||||
|     Seven = 7, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct VariableWorkRam { | ||||
|     current: BankNumber, | ||||
|     bank_n: Box<[[u8; VARIABLE_WORK_RAM_SIZE]; 7]>, // 4K for Variable amount of Banks (Banks 1 -> 7) in Game Boy Colour | ||||
|     buf: Box<[u8; VARIABLE_WORK_RAM_SIZE]>, // 4K for Variable amount of Banks (Banks 1 -> 7) in Game Boy Colour | ||||
| } | ||||
|  | ||||
| impl Default for VariableWorkRam { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             current: BankNumber::One, | ||||
|             bank_n: Box::new([[0u8; VARIABLE_WORK_RAM_SIZE]; 7]), | ||||
|             buf: Box::new([0u8; VARIABLE_WORK_RAM_SIZE]), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl VariableWorkRam { | ||||
|     fn set_current_bank(&mut self, bank: BankNumber) { | ||||
|         self.current = bank; | ||||
|     } | ||||
|  | ||||
|     fn get_current_bank(&self) -> BankNumber { | ||||
|         self.current | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl BusIo for VariableWorkRam { | ||||
|     fn write_byte(&mut self, addr: u16, byte: u8) { | ||||
|         self.bank_n[self.current as usize][addr as usize - VARIABLE_WORK_RAM_START_ADDRESS] = byte; | ||||
|     fn read_byte(&self, addr: u16) -> u8 { | ||||
|         self.buf[addr as usize - VARIABLE_WORK_RAM_START_ADDRESS] | ||||
|     } | ||||
|  | ||||
|     fn read_byte(&self, addr: u16) -> u8 { | ||||
|         self.bank_n[self.current as usize][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