Compare commits
	
		
			20 Commits
		
	
	
		
			61156ca8a8
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8a352ca807 | |||
| 068e4124a6 | |||
| fedae65cc8 | |||
| 41586d8cc4 | |||
| 79740ac245 | |||
| d6bfde081e | |||
| d2f57ee66b | |||
| 3841d3a98c | |||
| 85940c8744 | |||
| c7adf54976 | |||
| f0f9003247 | |||
| 5eba097ad4 | |||
| 756c806c2a | |||
| 77af5453f0 | |||
| 41bc4f7afe | |||
| 36e46d3780 | |||
| 926c66746d | |||
| cf2357f917 | |||
| 3fb7edd333 | |||
| 3ab512d663 | 
							
								
								
									
										2
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "recommendations": [ | ||||
|         "matklad.rust-analyzer", | ||||
|         "rust-lang.rust-analyzer", | ||||
|         "tamasfe.even-better-toml", | ||||
|         "serayuzgur.crates", | ||||
|         "vadimcn.vscode-lldb", | ||||
|   | ||||
							
								
								
									
										2513
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2513
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										22
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								Cargo.toml
									
									
									
									
									
								
							| @@ -8,21 +8,21 @@ edition = "2021" | ||||
|  | ||||
| [dependencies] | ||||
| anyhow = "1.0" | ||||
| bitfield = "0.13" | ||||
| clap = "2.33" | ||||
| gilrs = "0.8" | ||||
| winit = "0.26" | ||||
| egui = "0.15" | ||||
| wgpu = "0.11" | ||||
| egui_wgpu_backend = "0.14" | ||||
| egui_winit_platform = "0.12" | ||||
| pollster = "0.2" | ||||
| rodio = "0.14" | ||||
| rtrb = "0.2" | ||||
| bitfield = "0.17" | ||||
| clap = { version = "4.0", features = ["cargo"] } | ||||
| gilrs = "0.11" | ||||
| winit = "0.28" | ||||
| egui = "0.21" | ||||
| egui_wgpu_backend = "0.22" | ||||
| egui_winit_platform = "0.18" | ||||
| pollster = "0.3" | ||||
| rodio = "0.19" | ||||
| rtrb = "0.3" | ||||
| directories-next = "2.0" | ||||
| tracing = "0.1" | ||||
| tracing-subscriber = { version = "0.3", features = ["std", "env-filter"] } | ||||
| thiserror = "1.0" | ||||
| once_cell = "1.10" | ||||
|  | ||||
| [profile.release] | ||||
| debug = true | ||||
|   | ||||
							
								
								
									
										11
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,10 +1,9 @@ | ||||
| # Rekai's Gameboy Emulator | ||||
| [](https://ci.paoda.moe/paoda/gb) | ||||
|  | ||||
| ### Status | ||||
| * From [Blargg Test ROMs](https://github.com/L-P/blargg-test-roms/) | ||||
|     * [x] cpu_instrs | ||||
|     * [ ] instr_timing (kind of) | ||||
|     * [x] instr_timing | ||||
|     * [x] mem_timing | ||||
|     * [x] mem_timing-2 | ||||
|     * [ ] dmg_sound (partial)  | ||||
| @@ -21,9 +20,15 @@ | ||||
| Supports: ROM-only, MBC1, MBC2, MBC3 and MBC5 games. | ||||
|  | ||||
|  | ||||
| ### Compiling | ||||
| This project was last successfully built on [Rust 1.64.0](https://github.com/rust-lang/rust/tree/1.64.0) | ||||
|  | ||||
| 1. `git clone https://github.com/paoda/gb` | ||||
| 2. `cd gb` | ||||
| 3. `cargo run --release` | ||||
|  | ||||
| ### Controls | ||||
| Controls are defined [here](https://git.musuka.dev/paoda/gb/src/branch/main/src/joypad.rs#L114) | ||||
| Controls are defined [here](https://github.com/paoda/gb/blob/85940c874460b9cb31bf9b8211bf7dda596d114a/src/joypad.rs#L114) | ||||
|  | ||||
| Key | Button | ||||
| --- | --- | ||||
|   | ||||
| @@ -169,7 +169,7 @@ pub(super) mod ch4 { | ||||
|             match byte & 0x01 { | ||||
|                 0b00 => Self::Short, | ||||
|                 0b01 => Self::Long, | ||||
|                 _ => unreachable!("{:#04X} is not a valid value for CounterWidth"), | ||||
|                 _ => unreachable!("{:#04X} is not a valid value for CounterWidth", byte), | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -44,7 +44,8 @@ pub fn save_and_exit(cpu: &Cpu, control_flow: &mut ControlFlow) { | ||||
|  | ||||
| #[inline] | ||||
| pub fn pixel_buf(cpu: &Cpu) -> &[u8; GB_HEIGHT * GB_WIDTH * 4] { | ||||
|     cpu.bus.ppu.frame_buf.as_ref() | ||||
|     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> { | ||||
| @@ -64,8 +65,7 @@ pub fn rom_title(cpu: &Cpu) -> &str { | ||||
|     cpu.bus | ||||
|         .cart | ||||
|         .as_ref() | ||||
|         .map(|c| c.title.as_deref()) | ||||
|         .flatten() | ||||
|         .and_then(|c| c.title.as_deref()) | ||||
|         .unwrap_or(DEFAULT_TITLE) | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										475
									
								
								src/gui.rs
									
									
									
									
									
								
							
							
						
						
									
										475
									
								
								src/gui.rs
									
									
									
									
									
								
							| @@ -1,18 +1,20 @@ | ||||
| use egui::{ClippedMesh, CtxRef, TextureId}; | ||||
| use egui_wgpu_backend::{BackendError, RenderPass, ScreenDescriptor}; | ||||
| 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, SurfaceConfiguration, SurfaceTexture, Texture, TextureFormat, TextureUsages, | ||||
|     TextureView, | ||||
|     Surface, SurfaceCapabilities, SurfaceConfiguration, Texture, TextureView, | ||||
|     TextureViewDescriptor, | ||||
| }; | ||||
|  | ||||
| use winit::dpi::PhysicalSize; | ||||
| use winit::error::OsError; | ||||
| use winit::event::{ElementState, KeyboardInput}; | ||||
| use winit::event_loop::EventLoop; | ||||
| use winit::event::{ElementState, Event, KeyboardInput}; | ||||
| use winit::event_loop::{ControlFlow, EventLoop}; | ||||
| use winit::window::Window; | ||||
|  | ||||
| use crate::cpu::Cpu; | ||||
| use crate::{GB_HEIGHT, GB_WIDTH}; | ||||
| use crate::{emu, GB_HEIGHT, GB_WIDTH}; | ||||
|  | ||||
| const EGUI_DIMENSIONS: (usize, usize) = (1280, 720); | ||||
| const FILTER_MODE: FilterMode = FilterMode::Nearest; | ||||
| @@ -20,23 +22,279 @@ const WINDOW_TITLE: &str = "DMG-01 Emulator"; | ||||
|  | ||||
| const SCALE: f32 = 3.0; | ||||
|  | ||||
| /// Holds GUI State | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct GuiState { | ||||
|     /// When true, egui winit should exit the application | ||||
|     pub quit: bool, | ||||
| 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 GuiState { | ||||
|     pub fn new(title: String) -> Self { | ||||
| impl Gui { | ||||
|     pub fn new<T>(title: String, event_loop: &EventLoop<T>, cpu: &Cpu) -> Self { | ||||
|         use wgpu::InstanceDescriptor; | ||||
|  | ||||
|         let window = build_window(event_loop).expect("build window"); | ||||
|  | ||||
|         let instance = Instance::new(InstanceDescriptor::default()); | ||||
|         let surface = unsafe { | ||||
|             instance | ||||
|                 .create_surface(&window) | ||||
|                 .expect("create wgpu instance surface") | ||||
|         }; | ||||
|  | ||||
|         let adapter = request_adapter(&instance, &surface).expect("request adaptor"); | ||||
|         let (device, queue) = request_device(&adapter).expect("request device"); | ||||
|  | ||||
|         let capabilities = surface.get_capabilities(&adapter); | ||||
|  | ||||
|         let surface_config = surface_config(&window, capabilities); | ||||
|         surface.configure(&device, &surface_config); | ||||
|         let platform = platform(&window); | ||||
|         let mut render_pass = RenderPass::new(&device, surface_config.format, 1); | ||||
|  | ||||
|         let texture_size = texture_size(); | ||||
|         let texture = create_texture(&device, texture_size); | ||||
|         write_to_texture(&queue, &texture, emu::pixel_buf(cpu), texture_size); | ||||
|  | ||||
|         let view = texture.create_view(&TextureViewDescriptor::default()); | ||||
|         let texture_id = expose_texture_to_egui(&mut render_pass, &device, &view); | ||||
|  | ||||
|         Self { | ||||
|             should_quit: Default::default(), | ||||
|             title, | ||||
|             quit: Default::default(), | ||||
|             mode: EmuMode::Running, | ||||
|  | ||||
|             window, | ||||
|             platform, | ||||
|  | ||||
|             surface, | ||||
|             device, | ||||
|             queue, | ||||
|             surface_config, | ||||
|             render_pass, | ||||
|  | ||||
|             texture, | ||||
|             texture_size, | ||||
|             texture_id, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn maybe_quit(&self, cpu: &Cpu, control_flow: &mut ControlFlow) { | ||||
|         if self.should_quit { | ||||
|             emu::save_and_exit(cpu, control_flow); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn request_redraw(&self) { | ||||
|         self.window.request_redraw(); | ||||
|     } | ||||
|  | ||||
|     pub fn handle_event<T>(&mut self, event: &Event<T>) { | ||||
|         self.platform.handle_event(event); | ||||
|     } | ||||
|  | ||||
|     pub fn update_time(&mut self, elapsed_seconds: f64) { | ||||
|         self.platform.update_time(elapsed_seconds); | ||||
|     } | ||||
|  | ||||
|     pub fn resize(&mut self, size: PhysicalSize<u32>) { | ||||
|         // See: https://github.com/rust-windowing/winit/issues/208 | ||||
|         if size.width > 0 && size.height > 0 { | ||||
|             self.surface_config.width = size.width; | ||||
|             self.surface_config.height = size.height; | ||||
|             self.surface.configure(&self.device, &self.surface_config); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn paint(&mut self, cpu: &Cpu) { | ||||
|         use wgpu::{Color, SurfaceError}; | ||||
|  | ||||
|         let data = emu::pixel_buf(cpu); | ||||
|         write_to_texture(&self.queue, &self.texture, data, self.texture_size); | ||||
|  | ||||
|         let output_frame = match self.surface.get_current_texture() { | ||||
|             Ok(frame) => frame, | ||||
|             Err(SurfaceError::Outdated) => return, // Occurs on minimization on Windows | ||||
|             Err(e) => { | ||||
|                 eprintln!("Dropped frame with error: {}", e); | ||||
|                 return; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         let output_view = output_frame | ||||
|             .texture | ||||
|             .create_view(&TextureViewDescriptor::default()); | ||||
|  | ||||
|         // Begin to draw Egui components | ||||
|         self.platform.begin_frame(); | ||||
|         self.draw_egui(cpu, &self.platform.context(), self.texture_id); | ||||
|  | ||||
|         // End the UI frame. We could now handle the output and draw the UI with the backend. | ||||
|         let full_output = self.platform.end_frame(Some(&self.window)); | ||||
|         let paint_jobs = self.platform.context().tessellate(full_output.shapes); | ||||
|  | ||||
|         let mut encoder = create_command_encoder(&self.device); | ||||
|         let screen_descriptor = create_screen_descriptor(&self.window, &self.surface_config); | ||||
|         let tdelta = full_output.textures_delta; | ||||
|         // Upload all resources for the GPU. | ||||
|         self.render_pass | ||||
|             .add_textures(&self.device, &self.queue, &tdelta) | ||||
|             .expect("add texture ok"); | ||||
|         self.render_pass | ||||
|             .update_buffers(&self.device, &self.queue, &paint_jobs, &screen_descriptor); | ||||
|  | ||||
|         // Record all render passes. | ||||
|         self.render_pass | ||||
|             .execute( | ||||
|                 &mut encoder, | ||||
|                 &output_view, | ||||
|                 &paint_jobs, | ||||
|                 &screen_descriptor, | ||||
|                 Some(Color::BLACK), | ||||
|             ) | ||||
|             .expect("execute render pass"); | ||||
|  | ||||
|         // Submit the commands. | ||||
|         self.queue.submit(std::iter::once(encoder.finish())); | ||||
|  | ||||
|         // Redraw egui | ||||
|         output_frame.present(); | ||||
|  | ||||
|         self.render_pass | ||||
|             .remove_textures(tdelta) | ||||
|             .expect("remove texture delta"); | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     pub fn draw_egui(&mut self, cpu: &Cpu, ctx: &Context, texture_id: TextureId) { | ||||
|         use crate::{cpu, instruction, ppu}; | ||||
|  | ||||
|         fn selectable_text(ui: &mut egui::Ui, mut text: &str) -> egui::Response { | ||||
|             ui.add(egui::TextEdit::multiline(&mut text).code_editor()) | ||||
|         } | ||||
|  | ||||
|         egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { | ||||
|             ui.menu_button("File", |ui| { | ||||
|                 if ui.button("Quit").clicked() { | ||||
|                     self.should_quit = true; | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             egui::Window::new(&self.title).show(ctx, |ui| { | ||||
|                 ui.image( | ||||
|                     texture_id, | ||||
|                     [GB_WIDTH as f32 * SCALE, GB_HEIGHT as f32 * SCALE], | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             egui::Window::new("Disassembly").show(ctx, |ui| { | ||||
|                 selectable_text(ui, &instruction::dbg::tmp_disasm(cpu, 20)); | ||||
|             }); | ||||
|  | ||||
|             egui::Window::new("Settings").show(ctx, |ui| { | ||||
|                 egui::ComboBox::from_label("Emulation Mode") | ||||
|                     .selected_text(format!("{:?}", self.mode)) | ||||
|                     .show_ui(ui, |ui| { | ||||
|                         ui.selectable_value(&mut self.mode, EmuMode::Running, "Running"); | ||||
|                         ui.selectable_value(&mut self.mode, EmuMode::Stopped, "Stopped"); | ||||
|                         ui.selectable_value(&mut self.mode, EmuMode::StepFrame, "Step Frame"); | ||||
|                         ui.selectable_value(&mut self.mode, EmuMode::Step, "Step"); | ||||
|                     }) | ||||
|             }); | ||||
|  | ||||
|             egui::Window::new("GB Info").show(ctx, |ui| { | ||||
|                 ui.horizontal(|ui| { | ||||
|                     ui.vertical(|ui| { | ||||
|                         ui.heading("CPU"); | ||||
|  | ||||
|                         ui.monospace(format!("AF: {:#06X}", cpu::dbg::af(cpu))); | ||||
|                         ui.monospace(format!("BC: {:#06X}", cpu::dbg::bc(cpu))); | ||||
|                         ui.monospace(format!("DE: {:#06X}", cpu::dbg::de(cpu))); | ||||
|                         ui.monospace(format!("HL: {:#06X}", cpu::dbg::hl(cpu))); | ||||
|                         ui.add_space(10.0); | ||||
|                         ui.monospace(format!("SP: {:#06X}", cpu::dbg::sp(cpu))); | ||||
|                         ui.monospace(format!("PC: {:#06X}", cpu::dbg::pc(cpu))); | ||||
|                     }); | ||||
|  | ||||
|                     ui.vertical(|ui| { | ||||
|                         let ppu = &cpu.bus.ppu; | ||||
|  | ||||
|                         ui.heading("PPU"); | ||||
|  | ||||
|                         ui.monospace(format!("LY: {}", ppu::dbg::ly(ppu))); | ||||
|                         ui.horizontal(|ui| { | ||||
|                             ui.monospace(format!("SCX: {}", ppu::dbg::scx(ppu))); | ||||
|                             ui.monospace(format!("SCY: {}", ppu::dbg::scy(ppu))); | ||||
|                         }); | ||||
|                         ui.horizontal(|ui| { | ||||
|                             ui.monospace(format!("WX: {}", ppu::dbg::wx(ppu))); | ||||
|                             ui.monospace(format!("WY: {}", ppu::dbg::wy(ppu))); | ||||
|                         }); | ||||
|  | ||||
|                         ui.monospace(format!( | ||||
|                             "Mode: {:?} {}", | ||||
|                             ppu::dbg::mode(ppu), | ||||
|                             ppu::dbg::dot(ppu) | ||||
|                         )) | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 ui.add_space(10.0); | ||||
|  | ||||
|                 let _ = ui.selectable_label(cpu::dbg::ime(cpu), "IME"); | ||||
|                 ui.horizontal(|ui| { | ||||
|                     let irq = cpu.int_request(); | ||||
|  | ||||
|                     ui.label("IRQ:"); | ||||
|                     let _ = ui.selectable_label(irq & 0b01 == 0x01, "VBlank"); | ||||
|                     let _ = ui.selectable_label(irq >> 1 & 0x01 == 0x01, "LCD STAT"); | ||||
|                     let _ = ui.selectable_label(irq >> 2 & 0x01 == 0x01, "Timer"); | ||||
|                     let _ = ui.selectable_label(irq >> 3 & 0x01 == 0x01, "Serial"); | ||||
|                     let _ = ui.selectable_label(irq >> 4 & 0x01 == 0x01, "Joypad"); | ||||
|                 }); | ||||
|  | ||||
|                 ui.horizontal(|ui| { | ||||
|                     let ie = cpu.int_enable(); | ||||
|  | ||||
|                     // TODO: Reimplement this | ||||
|                     // let r_len = ctx.fonts().glyph_width(egui::TextStyle::Body, 'R'); | ||||
|                     // let e_len = ctx.fonts().glyph_width(egui::TextStyle::Body, 'E'); | ||||
|                     // let q_len = ctx.fonts().glyph_width(egui::TextStyle::Body, 'Q'); | ||||
|  | ||||
|                     ui.label("IE:"); | ||||
|                     // ui.add_space(q_len - (e_len - r_len)); | ||||
|                     let _ = ui.selectable_label(ie & 0b01 == 0x01, "VBlank"); | ||||
|                     let _ = ui.selectable_label(ie >> 1 & 0x01 == 0x01, "LCD STAT"); | ||||
|                     let _ = ui.selectable_label(ie >> 2 & 0x01 == 0x01, "Timer"); | ||||
|                     let _ = ui.selectable_label(ie >> 3 & 0x01 == 0x01, "Serial"); | ||||
|                     let _ = ui.selectable_label(ie >> 4 & 0x01 == 0x01, "Joypad"); | ||||
|                 }); | ||||
|  | ||||
|                 ui.add_space(10.0); | ||||
|  | ||||
|                 ui.horizontal(|ui| { | ||||
|                     let flags = cpu::dbg::flags(cpu); | ||||
|  | ||||
|                     let _ = ui.selectable_label((flags >> 7 & 0x01) == 0x01, "Zero"); | ||||
|                     let _ = ui.selectable_label((flags >> 6 & 0x01) == 0x01, "Negative"); | ||||
|                     let _ = ui.selectable_label((flags >> 5 & 0x01) == 0x01, "Half-Carry"); | ||||
|                     let _ = ui.selectable_label((flags >> 4 & 0x01) == 0x01, "Carry"); | ||||
|                 }) | ||||
|             }) | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||||
| @@ -61,8 +319,7 @@ pub fn unused_key() -> KeyboardInput { | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn build_window<T>(event_loop: &EventLoop<T>) -> Result<Window, OsError> { | ||||
|     use winit::dpi::PhysicalSize; | ||||
| fn build_window<T>(event_loop: &EventLoop<T>) -> Result<Window, OsError> { | ||||
|     use winit::window::WindowBuilder; | ||||
|  | ||||
|     WindowBuilder::new() | ||||
| @@ -77,51 +334,45 @@ pub fn build_window<T>(event_loop: &EventLoop<T>) -> Result<Window, OsError> { | ||||
|         .build(event_loop) | ||||
| } | ||||
|  | ||||
| pub fn create_surface(window: &Window) -> (Instance, Surface) { | ||||
|     use wgpu::Backends; | ||||
|  | ||||
|     let instance = Instance::new(Backends::PRIMARY); | ||||
|     let surface = unsafe { instance.create_surface(window) }; | ||||
|     (instance, surface) | ||||
| } | ||||
|  | ||||
| pub fn request_adapter(instance: &Instance, surface: &Surface) -> Option<Adapter> { | ||||
| fn request_adapter(instance: &Instance, surface: &Surface) -> Option<Adapter> { | ||||
|     use wgpu::{PowerPreference, RequestAdapterOptions}; | ||||
|  | ||||
|     pollster::block_on(instance.request_adapter(&RequestAdapterOptions { | ||||
|         power_preference: PowerPreference::HighPerformance, | ||||
|         force_fallback_adapter: false, // TODO: What do I want to do with this? | ||||
|         compatible_surface: Some(surface), | ||||
|         force_fallback_adapter: false, // TODO: What do I want to do with this? | ||||
|     })) | ||||
| } | ||||
|  | ||||
| pub fn request_device(adapter: &Adapter) -> Result<(Device, Queue), RequestDeviceError> { | ||||
| fn request_device(adapter: &Adapter) -> Result<(Device, Queue), RequestDeviceError> { | ||||
|     use wgpu::{DeviceDescriptor, Features, Limits}; | ||||
|  | ||||
|     pollster::block_on(adapter.request_device( | ||||
|         &DeviceDescriptor { | ||||
|             label: None, | ||||
|             features: Features::default(), | ||||
|             limits: Limits::default(), | ||||
|             label: None, | ||||
|         }, | ||||
|         None, | ||||
|     )) | ||||
| } | ||||
|  | ||||
| pub fn surface_config(window: &Window, format: TextureFormat) -> SurfaceConfiguration { | ||||
|     use wgpu::PresentMode; | ||||
| 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, | ||||
|         format: capabilities.formats[0], | ||||
|         width: size.width as u32, | ||||
|         height: size.height as u32, | ||||
|         present_mode: PresentMode::Mailbox, | ||||
|         present_mode: PresentMode::Fifo, | ||||
|         alpha_mode: capabilities.alpha_modes[0], | ||||
|         view_formats: vec![TextureFormat::Rgba8UnormSrgb], | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn platform_desc(window: &Window) -> Platform { | ||||
| fn platform(window: &Window) -> Platform { | ||||
|     use egui::FontDefinitions; | ||||
|     use egui_winit_platform::PlatformDescriptor; | ||||
|  | ||||
| @@ -135,7 +386,7 @@ pub fn platform_desc(window: &Window) -> Platform { | ||||
|     }) | ||||
| } | ||||
|  | ||||
| pub fn texture_size() -> Extent3d { | ||||
| fn texture_size() -> Extent3d { | ||||
|     Extent3d { | ||||
|         width: GB_WIDTH as u32, | ||||
|         height: GB_HEIGHT as u32, | ||||
| @@ -143,22 +394,23 @@ pub fn texture_size() -> Extent3d { | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn create_texture(device: &Device, size: Extent3d) -> Texture { | ||||
|     use wgpu::{TextureDescriptor, TextureDimension}; | ||||
| 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::Rgba8UnormSrgb, | ||||
|         format: TextureFormat::Bgra8Unorm, | ||||
|         usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING, | ||||
|         label: Some("gb_pixel_buffer"), | ||||
|         view_formats: &[TextureFormat::Bgra8Unorm], | ||||
|     }) | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| pub fn write_to_texture( | ||||
| fn write_to_texture( | ||||
|     queue: &Queue, | ||||
|     texture: &Texture, | ||||
|     data: &[u8; GB_WIDTH * 4 * GB_HEIGHT], | ||||
| @@ -184,23 +436,16 @@ pub fn write_to_texture( | ||||
|     ); | ||||
| } | ||||
|  | ||||
| pub fn expose_texture_to_egui( | ||||
| fn expose_texture_to_egui( | ||||
|     render_pass: &mut RenderPass, | ||||
|     device: &Device, | ||||
|     texture: &Texture, | ||||
|     view: &TextureView, | ||||
| ) -> TextureId { | ||||
|     render_pass.egui_texture_from_wgpu_texture(device, texture, FILTER_MODE) | ||||
|     render_pass.egui_texture_from_wgpu_texture(device, view, FILTER_MODE) | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| pub fn create_view(frame: &SurfaceTexture) -> TextureView { | ||||
|     use wgpu::TextureViewDescriptor; | ||||
|  | ||||
|     frame.texture.create_view(&TextureViewDescriptor::default()) | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| pub fn create_command_encoder(device: &Device) -> CommandEncoder { | ||||
| fn create_command_encoder(device: &Device) -> CommandEncoder { | ||||
|     use wgpu::CommandEncoderDescriptor; | ||||
|  | ||||
|     device.create_command_encoder(&CommandEncoderDescriptor { | ||||
| @@ -209,10 +454,7 @@ pub fn create_command_encoder(device: &Device) -> CommandEncoder { | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| pub fn create_screen_descriptor( | ||||
|     window: &Window, | ||||
|     config: &SurfaceConfiguration, | ||||
| ) -> ScreenDescriptor { | ||||
| fn create_screen_descriptor(window: &Window, config: &SurfaceConfiguration) -> ScreenDescriptor { | ||||
|     ScreenDescriptor { | ||||
|         physical_width: config.width, | ||||
|         physical_height: config.height, | ||||
| @@ -220,131 +462,6 @@ pub fn create_screen_descriptor( | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| pub fn execute_render_pass( | ||||
|     render_pass: &mut RenderPass, | ||||
|     encoder: &mut CommandEncoder, | ||||
|     view: &TextureView, | ||||
|     jobs: Vec<ClippedMesh>, | ||||
|     descriptor: &ScreenDescriptor, | ||||
| ) -> Result<(), BackendError> { | ||||
|     render_pass.execute(encoder, view, &jobs, descriptor, Some(wgpu::Color::BLACK)) | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| pub fn draw_egui(cpu: &Cpu, app: &mut GuiState, ctx: &CtxRef, 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| { | ||||
|         egui::menu::menu(ui, "File", |ui| { | ||||
|             if ui.button("Quit").clicked() { | ||||
|                 app.quit = true; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         egui::Window::new(&app.title).show(ctx, |ui| { | ||||
|             ui.image( | ||||
|                 texture_id, | ||||
|                 [GB_WIDTH as f32 * SCALE, GB_HEIGHT as f32 * SCALE], | ||||
|             ); | ||||
|         }); | ||||
|  | ||||
|         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!("{:?}", app.mode)) | ||||
|                 .show_ui(ui, |ui| { | ||||
|                     ui.selectable_value(&mut app.mode, EmuMode::Running, "Running"); | ||||
|                     ui.selectable_value(&mut app.mode, EmuMode::Stopped, "Stopped"); | ||||
|                     ui.selectable_value(&mut app.mode, EmuMode::StepFrame, "Step Frame"); | ||||
|                     ui.selectable_value(&mut app.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))) | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             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(); | ||||
|  | ||||
|                 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"); | ||||
|             }) | ||||
|         }) | ||||
|     }); | ||||
| } | ||||
|  | ||||
| pub mod kbd { | ||||
|     use winit::event::{ElementState, KeyboardInput, VirtualKeyCode}; | ||||
|  | ||||
|   | ||||
| @@ -1654,7 +1654,9 @@ impl Instruction { | ||||
|             0o304 | 0o314 | 0o324 | 0o334 => CALL(jump_cond((byte >> 3) & 0x03)), | ||||
|             // PUSH r16 | ||||
|             0o305 | 0o325 | 0o345 | 0o365 => PUSH(group3((byte >> 4) & 0x03)), | ||||
|             // CALL u16 | ||||
|             0o315 => CALL(JpCond::Always), | ||||
|             // ADD, ADC, SUB, SBC, AND, XOR, OR, and CP | ||||
|             0o306 | 0o316 | 0o326 | 0o336 | 0o346 | 0o356 | 0o366 | 0o376 => { | ||||
|                 alu_imm_instr((byte >> 3) & 0x07) | ||||
|             } | ||||
| @@ -2071,7 +2073,7 @@ mod table { | ||||
|             0b101 => L, | ||||
|             0b110 => IndirectHL, | ||||
|             0b111 => A, | ||||
|             _ => unreachable!("{:#04X} is not a valid Register"), | ||||
|             _ => unreachable!("{:#04X} is not a valid Register", code), | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -2153,8 +2155,11 @@ mod table { | ||||
| } | ||||
|  | ||||
| pub(crate) mod dbg { | ||||
|     use std::borrow::Cow; | ||||
|  | ||||
|     use super::add::{Source as AddSource, Target as AddTarget}; | ||||
|     use super::jump::JpCond; | ||||
|     use super::alu::Source as AluSource; | ||||
|     use super::jump::{JpCond, JpLoc}; | ||||
|     use super::load::{Source as LDSource, Target as LDTarget}; | ||||
|     use super::{AllRegisters, BusIo, Cpu, Instruction, RegisterPair}; | ||||
|  | ||||
| @@ -2188,28 +2193,140 @@ pub(crate) mod dbg { | ||||
|         sm83_asm | ||||
|     } | ||||
|  | ||||
|     fn disasm(cpu: &Cpu, pc: u16, instr: Instruction) -> String { | ||||
|     pub(crate) fn new_disasm(cpu: &Cpu, limit: u8) -> String { | ||||
|         let mut assembly = String::new(); | ||||
|         let mut pc = cpu.register_pair(RegisterPair::PC); | ||||
|  | ||||
|         for _ in 0..limit { | ||||
|             let opcode = cpu.read_byte(pc); | ||||
|             pc += 1; | ||||
|  | ||||
|             let maybe_instr = if opcode == 0xCB { | ||||
|                 let opcode = cpu.read_byte(pc); | ||||
|                 pc += 1; | ||||
|  | ||||
|                 Instruction::prefixed(opcode) | ||||
|             } else { | ||||
|                 Instruction::unprefixed(opcode) | ||||
|             }; | ||||
|  | ||||
|             match maybe_instr { | ||||
|                 Instruction::Invalid => {} | ||||
|                 instr => { | ||||
|                     let output = format!("${:04X} {}\n", pc - 1, disasm(cpu, pc, instr)); | ||||
|                     assembly.push_str(&output); | ||||
|                     pc += delta::pc_inc_count(instr); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         assembly | ||||
|     } | ||||
|  | ||||
|     // TODO: It might be better if I pass in a mutable writer instead of usnig a String | ||||
|     fn disasm(cpu: &Cpu, pc: u16, instr: Instruction) -> Cow<str> { | ||||
|         use Instruction::*; | ||||
|  | ||||
|         let imm_byte = cpu.read_byte(pc + 1); | ||||
|         let imm_word = (cpu.read_byte(pc + 2) as u16) << 8 | imm_byte as u16; | ||||
|  | ||||
|         match instr { | ||||
|             NOP => "NOP".to_string(), | ||||
|             // Unprefixed Instructions | ||||
|             NOP => "NOP".into(), | ||||
|             LD(LDTarget::IndirectImmediateWord, LDSource::SP) => { | ||||
|                 format!("LD ({:#06X}), SP", imm_word) | ||||
|                 format!("LD ({:#06X}), SP", imm_word).into() | ||||
|             } | ||||
|             STOP => "STOP".to_string(), | ||||
|             JR(JpCond::Always) => format!("JR {}", imm_byte as i8), | ||||
|             JR(cond) => format!("JR {:?} {}", cond, imm_byte as i8), | ||||
|             STOP => "STOP".into(), | ||||
|             JR(JpCond::Always) => format!("JR {}", imm_byte as i8).into(), | ||||
|             JR(cond) => format!("JR {:?} {}", cond, imm_byte as i8).into(), | ||||
|             LD(LDTarget::Group1(rp), LDSource::ImmediateWord) => { | ||||
|                 format!("LD {:?} {:#06X}", rp, imm_word) | ||||
|                 format!("LD {:?} {:#06X}", rp, imm_word).into() | ||||
|             } | ||||
|             ADD(AddTarget::HL, AddSource::Group1(rp)) => format!("ADD HL, {:?}", rp), | ||||
|             LD(LDTarget::IndirectGroup2(rp), LDSource::A) => format!("LD ({:?}), A", rp), | ||||
|             LD(LDTarget::A, LDSource::IndirectGroup2(rp)) => format!("LD A, ({:?})", rp), | ||||
|             ADD(AddTarget::HL, AddSource::Group1(rp)) => format!("ADD HL, {:?}", rp).into(), | ||||
|             LD(LDTarget::IndirectGroup2(rp), LDSource::A) => format!("LD ({:?}), A", rp).into(), | ||||
|             LD(LDTarget::A, LDSource::IndirectGroup2(rp)) => format!("LD A, ({:?})", rp).into(), | ||||
|             INC(AllRegisters::Group1(rp)) => format!("INC {:?}", rp).into(), | ||||
|             DEC(AllRegisters::Group1(rp)) => format!("DEC {:?}", rp).into(), | ||||
|             INC(AllRegisters::Register(reg)) => format!("INC {:?}", reg).into(), | ||||
|             DEC(AllRegisters::Register(reg)) => format!("DEC {:?}", reg).into(), | ||||
|             LD(LDTarget::Register(reg), LDSource::ImmediateByte) => { | ||||
|                 format!("LD {:?}, {:#04X}", reg, imm_byte).into() | ||||
|             } | ||||
|             RLCA => "RLCA".into(), | ||||
|             RRCA => "RRCA".into(), | ||||
|             RLA => "RLA".into(), | ||||
|             RRA => "RRA".into(), | ||||
|             DAA => "DAA".into(), | ||||
|             CPL => "CPL".into(), | ||||
|             SCF => "SCF".into(), | ||||
|             CCF => "CCF".into(), | ||||
|             HALT => "HALT".into(), | ||||
|             LD(LDTarget::Register(left), LDSource::Register(right)) => { | ||||
|                 format!("LD {:?}, {:?}", left, right).into() | ||||
|             } | ||||
|             ADD(AddTarget::A, AddSource::Register(reg)) => format!("ADD A, {:?}", reg).into(), | ||||
|             ADC(AluSource::Register(reg)) => format!("ADC {:?}", reg).into(), | ||||
|             SUB(AluSource::Register(reg)) => format!("SUB {:?}", reg).into(), | ||||
|             SBC(AluSource::Register(reg)) => format!("SBC {:?}", reg).into(), | ||||
|             AND(AluSource::Register(reg)) => format!("AND {:?}", reg).into(), | ||||
|             XOR(AluSource::Register(reg)) => format!("XOR {:?}", reg).into(), | ||||
|             OR(AluSource::Register(reg)) => format!("OR {:?}", reg).into(), | ||||
|             CP(AluSource::Register(reg)) => format!("CP {:?}", reg).into(), | ||||
|             RET(JpCond::Always) => "RET".into(), | ||||
|             RET(cond) => format!("RET {:?}", cond).into(), | ||||
|             LD(LDTarget::IoWithImmediateOffset, LDSource::A) => { | ||||
|                 format!("LD ({:#06X}) , A", 0xFF00 + imm_byte as u16).into() | ||||
|             } | ||||
|             ADD(AddTarget::SP, AddSource::ImmediateSignedByte) => { | ||||
|                 format!("ADD SP, {}", imm_byte as i8).into() | ||||
|             } | ||||
|             LD(LDTarget::A, LDSource::IoWithImmediateOffset) => { | ||||
|                 format!("LD A, ({:#06X})", 0xFF00 + imm_byte as u16).into() | ||||
|             } | ||||
|             LDHL => format!("LD HL, SP + {}", imm_byte as i8).into(), | ||||
|             POP(rp) => format!("POP {:?}", rp).into(), | ||||
|             RETI => "RETI".into(), | ||||
|             JP(JpCond::Always, JpLoc::HL) => "JP HL".into(), | ||||
|             LD(LDTarget::SP, LDSource::HL) => "LD SP, HL".into(), | ||||
|             JP(JpCond::Always, JpLoc::ImmediateWord) => format!("JP {:#06X}", imm_word).into(), | ||||
|             JP(cond, JpLoc::ImmediateWord) => format!("JP {:?} {:#06X}", cond, imm_word).into(), | ||||
|             LD(LDTarget::IoWithC, LDSource::A) => "LD (0xFF00 + C), A".into(), | ||||
|             LD(LDTarget::IndirectImmediateWord, LDSource::A) => { | ||||
|                 format!("LD ({:#06X}), A", imm_byte).into() | ||||
|             } | ||||
|             LD(LDTarget::A, LDSource::IoWithC) => "LD A, (0xFF00 + C)".into(), | ||||
|             LD(LDTarget::A, LDSource::IndirectImmediateWord) => { | ||||
|                 format!("LD A, ({:#06X})", imm_word).into() | ||||
|             } | ||||
|             DI => "DI".into(), | ||||
|             EI => "EI".into(), | ||||
|             CALL(cond) => format!("CALL {:?} {:#06X}", cond, imm_word).into(), | ||||
|             PUSH(rp) => format!("PUSH {:?}", rp).into(), | ||||
|             ADD(AddTarget::A, AddSource::ImmediateByte) => { | ||||
|                 format!("ADD A, {:#04X}", imm_byte).into() | ||||
|             } | ||||
|             ADC(AluSource::ImmediateByte) => format!("ADC {:#04X}", imm_byte).into(), | ||||
|             SUB(AluSource::ImmediateByte) => format!("SUB {:#04X}", imm_byte).into(), | ||||
|             SBC(AluSource::ImmediateByte) => format!("SBC {:#04X}", imm_byte).into(), | ||||
|             AND(AluSource::ImmediateByte) => format!("AND {:#04X}", imm_byte).into(), | ||||
|             XOR(AluSource::ImmediateByte) => format!("XOR {:#04X}", imm_byte).into(), | ||||
|             OR(AluSource::ImmediateByte) => format!("OR {:#04X}", imm_byte).into(), | ||||
|             CP(AluSource::ImmediateByte) => format!("CP {:#04X}", imm_byte).into(), | ||||
|             RST(v) => format!("RST {:#04X}", v).into(), | ||||
|  | ||||
|             _ => todo!(), | ||||
|             // Prefixed Instructions | ||||
|             RLC(reg) => format!("RLC {:?}", reg).into(), | ||||
|             RRC(reg) => format!("RRC {:?}", reg).into(), | ||||
|             RL(reg) => format!("RL {:?}", reg).into(), | ||||
|             RR(reg) => format!("RR {:?}", reg).into(), | ||||
|             SLA(reg) => format!("SLA {:?}", reg).into(), | ||||
|             SRA(reg) => format!("SRA {:?}", reg).into(), | ||||
|             SWAP(reg) => format!("SWAP {:?}", reg).into(), | ||||
|             SRL(reg) => format!("SRL {:?}", reg).into(), | ||||
|             BIT(bit, reg) => format!("BIT {}, {:?}", bit, reg).into(), | ||||
|             RES(bit, reg) => format!("RES {}, {:?}", bit, reg).into(), | ||||
|             SET(bit, reg) => format!("SET {}, {:?}", bit, reg).into(), | ||||
|             _ => unreachable!("{:?} is an illegal instruction", instr), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
							
								
								
									
										134
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										134
									
								
								src/main.rs
									
									
									
									
									
								
							| @@ -1,39 +1,28 @@ | ||||
| use std::path::PathBuf; | ||||
| use std::time::Instant; | ||||
|  | ||||
| use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg}; | ||||
| use egui_wgpu_backend::RenderPass; | ||||
| use gb::gui::EmuMode; | ||||
| use clap::{arg, command, value_parser}; | ||||
| use gb::gui::{EmuMode, Gui}; | ||||
| use gb::{emu, gui}; | ||||
| use gilrs::Gilrs; | ||||
| use gui::GuiState; | ||||
| use rodio::{OutputStream, Sink}; | ||||
| use tracing_subscriber::EnvFilter; | ||||
| use winit::event::{Event, WindowEvent}; | ||||
| use winit::event_loop::EventLoop; | ||||
| use winit::event_loop::{EventLoop, EventLoopBuilder}; | ||||
|  | ||||
| const AUDIO_ENABLED: bool = true; | ||||
|  | ||||
| fn main() { | ||||
|     let app = App::new(crate_name!()) | ||||
|         .version(crate_version!()) | ||||
|         .author(crate_authors!()) | ||||
|         .about(crate_description!()); | ||||
|  | ||||
|     let m = app | ||||
|     let m = command!() | ||||
|         .arg( | ||||
|             Arg::with_name("rom") | ||||
|                 .value_name("ROM_FILE") | ||||
|                 .takes_value(true) | ||||
|                 .index(1) | ||||
|                 .help("Path to the Game ROM"), | ||||
|             arg!(-b --boot <FILE> "path to boot ROM") | ||||
|                 .required(false) | ||||
|                 .value_parser(value_parser!(PathBuf)), | ||||
|         ) | ||||
|         .arg( | ||||
|             Arg::with_name("boot") | ||||
|                 .short("b") | ||||
|                 .long("boot") | ||||
|                 .value_name("FILE") | ||||
|                 .takes_value(true) | ||||
|                 .help("Path to Boot ROM"), | ||||
|             arg!([ROM_FILE] "path to game ROM") | ||||
|                 .required(true) | ||||
|                 .value_parser(value_parser!(PathBuf)), | ||||
|         ) | ||||
|         .get_matches(); | ||||
|  | ||||
| @@ -46,25 +35,8 @@ fn main() { | ||||
|         .with_env_filter(EnvFilter::from_default_env()) | ||||
|         .init(); | ||||
|  | ||||
|     // --Here lies a lot of winit + wgpu Boilerplate-- | ||||
|     let event_loop: EventLoop<Event<()>> = EventLoop::with_user_event(); | ||||
|     let window = gui::build_window(&event_loop).expect("build window"); | ||||
|  | ||||
|     let (instance, surface) = gui::create_surface(&window); | ||||
|     let adapter = gui::request_adapter(&instance, &surface).expect("request adaptor"); | ||||
|     let (device, queue) = gui::request_device(&adapter).expect("request device"); | ||||
|     let format = surface | ||||
|         .get_preferred_format(&adapter) | ||||
|         .expect("get surface format"); | ||||
|  | ||||
|     let mut config = gui::surface_config(&window, format); | ||||
|     surface.configure(&device, &config); | ||||
|     let mut platform = gui::platform_desc(&window); | ||||
|     let mut render_pass = RenderPass::new(&device, format, 1); | ||||
|  | ||||
|     // We interrupt your boiler plate to initialize the emulator so that | ||||
|     // we can copy it's empty pixel buffer to the GPU | ||||
|     let mut cpu = match m.value_of("boot") { | ||||
|     // 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") | ||||
| @@ -75,20 +47,12 @@ fn main() { | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     // Set up the wgpu (and then EGUI) texture we'll be working with. | ||||
|     let texture_size = gui::texture_size(); | ||||
|     let texture = gui::create_texture(&device, texture_size); | ||||
|     gui::write_to_texture(&queue, &texture, emu::pixel_buf(&cpu), texture_size); | ||||
|     let texture_id = gui::expose_texture_to_egui(&mut render_pass, &device, &texture); | ||||
|  | ||||
|     // Load ROM if filepath was provided | ||||
|     if let Some(path) = m.value_of("rom") { | ||||
|     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"); | ||||
| @@ -115,20 +79,20 @@ fn main() { | ||||
|     } | ||||
|  | ||||
|     // Set up state for the Immediate-mode GUI | ||||
|     let mut app = GuiState::new(rom_title); | ||||
|     let mut last_key = gui::unused_key(); | ||||
|     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| { | ||||
|         platform.handle_event(&event); | ||||
|         app.handle_event(&event); | ||||
|  | ||||
|         match event { | ||||
|             Event::MainEventsCleared => { | ||||
|                 if app.quit { | ||||
|                     emu::save_and_exit(&cpu, control_flow); | ||||
|                 } | ||||
|                 app.maybe_quit(&cpu, control_flow); | ||||
|  | ||||
|                 match app.mode { | ||||
|                     EmuMode::Running => emu::run_frame(&mut cpu, &mut gamepad, last_key), | ||||
| @@ -144,63 +108,15 @@ fn main() { | ||||
|                 // Input has been consumed, reset it | ||||
|                 last_key = gui::unused_key(); | ||||
|  | ||||
|                 window.request_redraw(); | ||||
|                 app.request_redraw(); | ||||
|             } | ||||
|             Event::RedrawRequested(..) => { | ||||
|                 platform.update_time(start_time.elapsed().as_secs_f64()); | ||||
|  | ||||
|                 let data = emu::pixel_buf(&cpu); | ||||
|                 gui::write_to_texture(&queue, &texture, data, texture_size); | ||||
|  | ||||
|                 let output_frame = match surface.get_current_texture() { | ||||
|                     Ok(frame) => frame, | ||||
|                     Err(e) => { | ||||
|                         eprintln!("Dropped frame with error: {}", e); | ||||
|                         return; | ||||
|                     } | ||||
|                 }; | ||||
|                 let output_view = gui::create_view(&output_frame); | ||||
|  | ||||
|                 // Begin to draw Egui components | ||||
|                 platform.begin_frame(); | ||||
|                 gui::draw_egui(&cpu, &mut app, &platform.context(), texture_id); | ||||
|                 // End the UI frame. We could now handle the output and draw the UI with the backend. | ||||
|                 let (_, paint_commands) = platform.end_frame(Some(&window)); | ||||
|                 let paint_jobs = platform.context().tessellate(paint_commands); | ||||
|  | ||||
|                 let mut encoder = gui::create_command_encoder(&device); | ||||
|                 let screen_descriptor = gui::create_screen_descriptor(&window, &config); | ||||
|  | ||||
|                 // Upload all resources for the GPU. | ||||
|                 render_pass.update_texture(&device, &queue, &platform.context().texture()); | ||||
|                 render_pass.update_user_textures(&device, &queue); | ||||
|                 render_pass.update_buffers(&device, &queue, &paint_jobs, &screen_descriptor); | ||||
|  | ||||
|                 // Record all render passes. | ||||
|                 gui::execute_render_pass( | ||||
|                     &mut render_pass, | ||||
|                     &mut encoder, | ||||
|                     &output_view, | ||||
|                     paint_jobs, | ||||
|                     &screen_descriptor, | ||||
|                 ) | ||||
|                 .expect("record render passes"); | ||||
|  | ||||
|                 // Submit the commands. | ||||
|                 queue.submit(std::iter::once(encoder.finish())); | ||||
|  | ||||
|                 // Redraw egui | ||||
|                 output_frame.present(); | ||||
|                 app.update_time(start_time.elapsed().as_secs_f64()); | ||||
|                 app.paint(&cpu); | ||||
|             } | ||||
|             Event::WindowEvent { event, .. } => match event { | ||||
|                 WindowEvent::Resized(size) => { | ||||
|                     config.width = size.width; | ||||
|                     config.height = size.height; | ||||
|                     surface.configure(&device, &config); | ||||
|                 } | ||||
|                 WindowEvent::CloseRequested => { | ||||
|                     emu::save_and_exit(&cpu, control_flow); | ||||
|                 } | ||||
|                 WindowEvent::Resized(size) => app.resize(size), | ||||
|                 WindowEvent::CloseRequested => emu::save_and_exit(&cpu, control_flow), | ||||
|                 WindowEvent::KeyboardInput { input, .. } => last_key = input, | ||||
|                 _ => {} | ||||
|             }, | ||||
|   | ||||
							
								
								
									
										114
									
								
								src/ppu.rs
									
									
									
									
									
								
							
							
						
						
									
										114
									
								
								src/ppu.rs
									
									
									
									
									
								
							| @@ -10,6 +10,8 @@ use types::{ | ||||
|     ObjectPaletteKind, ObjectSize, Pixels, RenderPriority, TileDataAddress, | ||||
| }; | ||||
|  | ||||
| use once_cell::sync::Lazy; | ||||
|  | ||||
| mod dma; | ||||
| mod types; | ||||
|  | ||||
| @@ -32,6 +34,14 @@ const LIGHT_GRAY: [u8; 4] = 0xAEBA89FFu32.to_be_bytes(); | ||||
| const DARK_GRAY: [u8; 4] = 0x5E6745FFu32.to_be_bytes(); | ||||
| const BLACK: [u8; 4] = 0x202020FFu32.to_be_bytes(); | ||||
|  | ||||
| static BLANK_SCREEN: Lazy<Box<[u8; (GB_WIDTH * 4) * GB_HEIGHT]>> = Lazy::new(|| { | ||||
|     WHITE | ||||
|         .repeat(GB_WIDTH * GB_HEIGHT) | ||||
|         .into_boxed_slice() | ||||
|         .try_into() | ||||
|         .unwrap() | ||||
| }); | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct Ppu { | ||||
|     pub(crate) int: Interrupt, | ||||
| @@ -48,7 +58,7 @@ pub struct Ppu { | ||||
|     fetch: PixelFetcher, | ||||
|     fifo: PixelFifo, | ||||
|     obj_buffer: ObjectBuffer, | ||||
|     pub(crate) frame_buf: Box<[u8; GB_WIDTH * GB_HEIGHT * 4]>, | ||||
|     pub(crate) frame_buf: FrameBuffer, | ||||
|     win_stat: WindowStatus, | ||||
|  | ||||
|     scanline_start: bool, | ||||
| @@ -70,15 +80,26 @@ impl BusIo for Ppu { | ||||
|  | ||||
| impl Ppu { | ||||
|     pub(crate) fn tick(&mut self) { | ||||
|         self.dot += 1; | ||||
|  | ||||
|         if !self.ctrl.lcd_enabled() { | ||||
|             if self.dot > 0 { | ||||
|                 // Check ensures this expensive operation only happens once | ||||
|                 self.frame_buf | ||||
|                     .get_mut(Device::Guest) | ||||
|                     .copy_from_slice(BLANK_SCREEN.as_ref()); | ||||
|             } | ||||
|  | ||||
|             self.stat.set_mode(PpuMode::HBlank); | ||||
|             self.pos.line_y = 0; | ||||
|             self.dot = 0; | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         self.dot += 1; | ||||
|  | ||||
|         match self.stat.mode() { | ||||
|             PpuMode::OamScan => { | ||||
|                 // Cycles 1 -> 80 | ||||
|  | ||||
|                 if self.dot >= 80 { | ||||
|                     self.x_pos = 0; | ||||
|                     self.scanline_start = true; | ||||
| @@ -104,12 +125,7 @@ impl Ppu { | ||||
|                 self.scan_oam(); | ||||
|             } | ||||
|             PpuMode::Drawing => { | ||||
|                 if self.ctrl.lcd_enabled() { | ||||
|                     // Only Draw when the LCD Is Enabled | ||||
|                     self.draw(); | ||||
|                 } else { | ||||
|                     self.reset(); | ||||
|                 } | ||||
|                 self.draw(); | ||||
|  | ||||
|                 if self.x_pos == 160 { | ||||
|                     if self.stat.hblank_int() { | ||||
| @@ -164,6 +180,9 @@ impl Ppu { | ||||
|                             self.int.set_lcd_stat(true); | ||||
|                         } | ||||
|  | ||||
|                         // Screen is done drawing | ||||
|                         self.frame_buf.swap(); | ||||
|  | ||||
|                         PpuMode::VBlank | ||||
|                     } else { | ||||
|                         if self.stat.oam_int() { | ||||
| @@ -397,8 +416,8 @@ impl Ppu { | ||||
|                 let x = self.x_pos as usize; | ||||
|  | ||||
|                 let i = (GB_WIDTH * 4) * y + (x * 4); | ||||
|                 self.frame_buf[i..(i + rgba.len())].copy_from_slice(&rgba); | ||||
|  | ||||
|                 self.frame_buf.get_mut(Device::Guest)[i..(i + rgba.len())].copy_from_slice(&rgba); | ||||
|                 self.x_pos += 1; | ||||
|             } | ||||
|  | ||||
| @@ -415,14 +434,6 @@ impl Ppu { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn reset(&mut self) { | ||||
|         self.pos.line_y = 0; | ||||
|         self.stat.set_mode(PpuMode::HBlank); | ||||
|  | ||||
|         let mut blank = WHITE.repeat(self.frame_buf.len() / 4); | ||||
|         self.frame_buf.swap_with_slice(&mut blank); | ||||
|     } | ||||
|  | ||||
|     fn clock_fifo(&mut self) -> Option<GrayShade> { | ||||
|         use RenderPriority::*; | ||||
|  | ||||
| @@ -468,7 +479,7 @@ impl Default for Ppu { | ||||
|         Self { | ||||
|             vram: Box::new([0u8; VRAM_SIZE]), | ||||
|             dot: Default::default(), | ||||
|             frame_buf: Box::new([0; GB_WIDTH * GB_HEIGHT * 4]), | ||||
|             frame_buf: FrameBuffer::new().expect("create frame buffers"), | ||||
|             int: Default::default(), | ||||
|             ctrl: LCDControl(0), | ||||
|             monochrome: Default::default(), | ||||
| @@ -913,6 +924,7 @@ struct WindowStatus { | ||||
|  | ||||
| pub(crate) mod dbg { | ||||
|     use super::{Ppu, PpuMode}; | ||||
|     use crate::Cycle; | ||||
|  | ||||
|     pub(crate) fn ly(ppu: &Ppu) -> u8 { | ||||
|         ppu.pos.line_y | ||||
| @@ -937,4 +949,68 @@ pub(crate) mod dbg { | ||||
|     pub(crate) fn wy(ppu: &Ppu) -> i16 { | ||||
|         ppu.pos.window_y as i16 | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn dot(ppu: &Ppu) -> Cycle { | ||||
|         ppu.dot | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct FrameBuffer { | ||||
|     buf: [Box<[u8; Self::FRAME_LEN]>; 2], | ||||
|     current: bool, | ||||
| } | ||||
|  | ||||
| #[derive(PartialEq)] | ||||
| pub enum Device { | ||||
|     Guest, | ||||
|     Host, | ||||
| } | ||||
|  | ||||
| impl FrameBuffer { | ||||
|     const FRAME_LEN: usize = GB_WIDTH * std::mem::size_of::<u32>() * GB_HEIGHT; | ||||
|  | ||||
|     pub fn new() -> Result<Self, FrameBufferError> { | ||||
|         Ok(Self { | ||||
|             buf: [ | ||||
|                 vec![0; Self::FRAME_LEN] | ||||
|                     .into_boxed_slice() | ||||
|                     .try_into() | ||||
|                     .map_err(|_| FrameBufferError::TryFrom)?, | ||||
|                 vec![0; Self::FRAME_LEN] | ||||
|                     .into_boxed_slice() | ||||
|                     .try_into() | ||||
|                     .map_err(|_| FrameBufferError::TryFrom)?, | ||||
|             ], | ||||
|             current: false, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub fn swap(&mut self) { | ||||
|         self.current = !self.current; | ||||
|     } | ||||
|  | ||||
|     pub fn get_mut(&mut self, device: Device) -> &mut [u8; Self::FRAME_LEN] { | ||||
|         let idx = match device { | ||||
|             Device::Guest => self.current, | ||||
|             Device::Host => !self.current, | ||||
|         }; | ||||
|  | ||||
|         &mut *self.buf[idx as usize] | ||||
|     } | ||||
|  | ||||
|     pub fn get(&self, device: Device) -> &[u8; Self::FRAME_LEN] { | ||||
|         let idx = match device { | ||||
|             Device::Guest => self.current, | ||||
|             Device::Host => !self.current, | ||||
|         }; | ||||
|  | ||||
|         &*self.buf[idx as usize] | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, thiserror::Error)] | ||||
| pub enum FrameBufferError { | ||||
|     #[error("Failed to coerce boxed slice to boxed array")] | ||||
|     TryFrom, | ||||
| } | ||||
|   | ||||
| @@ -335,7 +335,7 @@ impl From<ObjectPaletteKind> for u8 { | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy, PartialEq)] | ||||
| #[derive(Debug, Clone, Copy, Eq, PartialEq)] | ||||
| pub enum RenderPriority { | ||||
|     Object = 0, | ||||
|     BackgroundAndWindow = 1, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user