diff --git a/snes-core/src/emulator.rs b/snes-core/src/emulator.rs index f305bd8..edf9040 100644 --- a/snes-core/src/emulator.rs +++ b/snes-core/src/emulator.rs @@ -1,12 +1,9 @@ use crate::cpu::CPU; use crate::cpu::bus::Bus; -use crate::rom::ROM; -use crate::rom::lo_rom::LoROM; pub struct Emulator { pub cpu: CPU, pub bus: Bus, - pub rom: Box, } impl Emulator { @@ -14,7 +11,6 @@ impl Emulator { Self { cpu: CPU::new(), bus: Bus::new(), - rom: Box::new(LoROM::new()), } } @@ -41,4 +37,10 @@ impl Emulator { pub fn reset(&mut self) { self.cpu.reset_vector(&mut self.bus); } +} + +impl Default for Emulator { + fn default() -> Self { + Self::new() + } } \ No newline at end of file diff --git a/snes-frontend/Cargo.toml b/snes-frontend/Cargo.toml index 2cc42f5..5b56b10 100644 --- a/snes-frontend/Cargo.toml +++ b/snes-frontend/Cargo.toml @@ -7,14 +7,9 @@ edition = "2021" [dependencies] snes-core = { path = "../snes-core" } -regex = "1.8.1" -winit = "0.26.1" -imgui = "0.8.2" -wgpu = "0.12.0" -imgui-wgpu = "0.19.0" -pollster = "0.2.5" -rfd = "0.10.0" -[dependencies.imgui-winit-support] -version = "0.8.2" -features = ["winit-26"] -default-features = false + +# Frontend stuff +eframe = "0.24.1" +env_logger = "0.10.1" +rfd = "0.12.1" +regex = "1.10.2" diff --git a/snes-frontend/src/cpu.rs b/snes-frontend/src/cpu.rs deleted file mode 100644 index f19950f..0000000 --- a/snes-frontend/src/cpu.rs +++ /dev/null @@ -1,15 +0,0 @@ -extern crate snes_core; -use snes_core::{emulator::Emulator, cpu::instructions::mapper::map_opcode_to_instruction}; - -pub struct CPUDisassembler { - _history_limit: usize, - _instruction_history: Vec, -} - -impl CPUDisassembler { - pub fn get_next_instruction(emulator: &Emulator) -> String { - let opcode = emulator.bus.read_external(emulator.cpu.registers.get_pc_address()); - let instruction = map_opcode_to_instruction(opcode); - instruction.mnemonic(&emulator.cpu.registers, &emulator.bus, opcode) - } -} diff --git a/snes-frontend/src/emu_state/debug_options.rs b/snes-frontend/src/emu_state/debug_options.rs new file mode 100644 index 0000000..9b9af1f --- /dev/null +++ b/snes-frontend/src/emu_state/debug_options.rs @@ -0,0 +1,138 @@ +use eframe::epaint::TextureHandle; +use snes_core::ppu::registers::{ + Background as PPUBg, + MAX_BG_WIDTH, + MAX_BG_HEIGHT, +}; + +pub struct DebugOptions { + pub enable_debugging: bool, + pub show_debug_options_window: bool, + pub memory_map_conrtrol_options: MemoryMapControlOptions, + pub cpu_debug_control_options: CPUDebugControlOptions, + pub ppu_debug_control_options: PPUDebugControlOptions, +} + +impl DebugOptions { + pub fn new() -> Self { + Self { + enable_debugging: true, + show_debug_options_window: true, + memory_map_conrtrol_options: MemoryMapControlOptions::new(), + cpu_debug_control_options: CPUDebugControlOptions::new(), + ppu_debug_control_options: PPUDebugControlOptions::new(), + } + } +} + +pub struct MemoryMapControlOptions { + pub is_enabled: bool, + pub inputs: MemoryMapInputs, + pub inputs_result: MemoryMapInputs, +} + +impl MemoryMapControlOptions { + pub fn new() -> Self { + Self { + is_enabled: true, + inputs: MemoryMapInputs::new(), + inputs_result: MemoryMapInputs::new(), + } + } +} + +pub struct MemoryMapInputs { + pub page_start: String, + pub page_end: String, + pub address_start: String, + pub address_end: String, +} + +impl MemoryMapInputs { + pub fn new() -> Self { + Self { + page_start: String::from("00"), + page_end: String::from("0F"), + address_start: String::from("0000"), + address_end: String::from("01FF"), + } + } +} + +pub struct CPUDebugControlOptions { + pub is_enabled: bool, + pub show_registers: bool, + pub show_upcoming_instruction: bool, +} + +impl CPUDebugControlOptions { + pub fn new() -> Self { + Self { + is_enabled: true, + show_registers: true, + show_upcoming_instruction: true, + } + } +} + +pub struct BgDebug { + pub is_enabled: bool, + pub background: PPUBg, + pub bg_framebuffer: Vec, + pub char_framebuffer: Vec, + pub bg_texture: Option, + pub char_texture: Option, +} + +impl BgDebug { + pub fn new(background: PPUBg) -> Self { + Self { + is_enabled: false, + background, + bg_framebuffer: vec![0x00; MAX_BG_WIDTH * MAX_BG_HEIGHT * 4], + // 8x8 pixels, 16x8 characters + char_framebuffer: vec![0x00; 8 * 8 * 16 * 8 * 4], + bg_texture: None, + char_texture: None, + } + } +} + +pub struct PPUDebugControlOptions { + pub is_enabled: bool, + pub show_registers: bool, + pub show_vram: bool, + pub vram_inputs: VramInputs, + pub backgrounds: [BgDebug; 4], +} + +impl PPUDebugControlOptions { + pub fn new() -> Self { + Self { + is_enabled: true, + show_registers: true, + show_vram: true, + vram_inputs: VramInputs::new(), + backgrounds: [ + BgDebug::new(PPUBg::Bg1), + BgDebug::new(PPUBg::Bg2), + BgDebug::new(PPUBg::Bg3), + BgDebug::new(PPUBg::Bg4), + ], + } + } +} + +pub struct VramInputs { + pub address_start: String, + pub address_end: String, +} + +impl VramInputs { + pub fn new() -> Self { + Self { + address_start: String::from("0000"), + address_end: String::from("01FF"), + } + } +} diff --git a/snes-frontend/src/emu_state/emulation.rs b/snes-frontend/src/emu_state/emulation.rs new file mode 100644 index 0000000..69faad6 --- /dev/null +++ b/snes-frontend/src/emu_state/emulation.rs @@ -0,0 +1,11 @@ +pub struct EmulationState { + pub is_paused: bool, +} + +impl EmulationState { + pub fn new() -> Self { + Self { + is_paused: true, + } + } +} \ No newline at end of file diff --git a/snes-frontend/src/emu_state/mod.rs b/snes-frontend/src/emu_state/mod.rs new file mode 100644 index 0000000..25070e6 --- /dev/null +++ b/snes-frontend/src/emu_state/mod.rs @@ -0,0 +1,4 @@ +pub mod debug_options; +pub mod state; +pub mod emulation; +pub use state::AppState; \ No newline at end of file diff --git a/snes-frontend/src/emu_state/state.rs b/snes-frontend/src/emu_state/state.rs new file mode 100644 index 0000000..f70d232 --- /dev/null +++ b/snes-frontend/src/emu_state/state.rs @@ -0,0 +1,24 @@ +use crate::emu_state::debug_options::DebugOptions; +use crate::emu_state::emulation::EmulationState; + + +pub struct AppState { + pub debug_options: DebugOptions, + pub emulation_state: EmulationState, +} + +impl AppState { + pub fn new() -> Self { + Self { + debug_options: DebugOptions::new(), + emulation_state: EmulationState::new(), + } + } +} + + +impl Default for AppState { + fn default() -> Self { + Self::new() + } +} \ No newline at end of file diff --git a/snes-frontend/src/emu_ui/debug/common.rs b/snes-frontend/src/emu_ui/debug/common.rs new file mode 100644 index 0000000..90b93d4 --- /dev/null +++ b/snes-frontend/src/emu_ui/debug/common.rs @@ -0,0 +1,13 @@ +use regex::Regex; + +pub fn sanitize_input(input: &mut String, is_page: bool) { + let re = Regex::new(r"^(0x)?[0-9a-fA-F]{2,4}$").unwrap(); + if !re.is_match(input) { + *input = String::from("0x00"); + } + let trimmed_input = input.trim_start_matches("0x"); + *input = match is_page { + true => format!("{:02X}", u8::from_str_radix(trimmed_input, 16).unwrap()), + false => format!("{:04X}", u16::from_str_radix(trimmed_input, 16).unwrap()), + } +} diff --git a/snes-frontend/src/emu_ui/debug/cpu.rs b/snes-frontend/src/emu_ui/debug/cpu.rs new file mode 100644 index 0000000..c3f42fd --- /dev/null +++ b/snes-frontend/src/emu_ui/debug/cpu.rs @@ -0,0 +1,92 @@ +use eframe::egui; +use snes_core::{emulator::Emulator, cpu::instructions::mapper::map_opcode_to_instruction}; + +use crate::emu_state::{debug_options::CPUDebugControlOptions, emulation::EmulationState}; + + +pub fn build_cpu_debug_controls(ctx: &egui::Context, cpu_debug_options: &mut CPUDebugControlOptions, emulation_state: &mut EmulationState, emulator: &mut Emulator) { + if !cpu_debug_options.is_enabled { + return + } + + egui::Window::new("CPU Debug Controls") + .auto_sized() + .max_width(125.0) + .open(&mut cpu_debug_options.is_enabled) + .show(ctx, |ui| { + ui.monospace("Controls:"); + ui.horizontal(|ui| { + let pause_text = if emulation_state.is_paused {"Resume"} else {"Pause"}; + if ui.button(pause_text).clicked() { + emulation_state.is_paused = !emulation_state.is_paused; + } + let tick_button = ui.add_enabled(emulation_state.is_paused, egui::Button::new("Tick")); + if tick_button.clicked() { + emulator.tick(); + } + }); + ui.monospace("Vectors:"); + ui.horizontal(|ui| { + if ui.button("Reset").clicked() { + emulator.reset(); + } + }); + ui.separator(); + ui.horizontal(|ui| { + if ui.selectable_label( + cpu_debug_options.show_registers, + "Show registers" + ).clicked() { + cpu_debug_options.show_registers = !cpu_debug_options.show_registers; + } + if ui.selectable_label( + cpu_debug_options.show_upcoming_instruction, + "Show upcoming instruction" + ).clicked() { + cpu_debug_options.show_upcoming_instruction = !cpu_debug_options.show_upcoming_instruction; + } + }); + }); + + build_cpu_registers_window(ctx, cpu_debug_options, emulator); + build_upcoming_instruction_window(ctx, cpu_debug_options, emulator); +} + +fn build_cpu_registers_window(ctx: &egui::Context, cpu_debug_options: &mut CPUDebugControlOptions, emulator: &Emulator) { + egui::Window::new("CPU Registers") + .auto_sized() + .max_width(125.0) + .open(&mut cpu_debug_options.show_registers) + .show(ctx, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { + ui.monospace(format!("SP: | {:#06X}", emulator.cpu.registers.sp)); + ui.monospace(format!("X: | {:#06X}", emulator.cpu.registers.x)); + ui.monospace(format!("Y: | {:#06X}", emulator.cpu.registers.y)); + ui.monospace(format!("A: | {:#06X}", emulator.cpu.registers.a)); + ui.monospace(format!("P: | {:#04X}", emulator.cpu.registers.p)); + ui.monospace(format!("D: | {:#06X}", emulator.cpu.registers.d)); + ui.monospace(format!("PBR: | {:#04X}", emulator.cpu.registers.pbr)); + ui.monospace(format!("DBR: | {:#04X}", emulator.cpu.registers.dbr)); + ui.monospace(format!("PC: | {:#06X}", emulator.cpu.registers.pc)); + ui.monospace(format!("EMU MODE: | {}", emulator.cpu.registers.emulation_mode)); + ui.separator(); + ui.monospace("Status flags:"); + ui.monospace("NVMXDIZC"); + ui.monospace(format!("{:08b}", emulator.cpu.registers.p)); + }); + }); +} + +fn build_upcoming_instruction_window(ctx: &egui::Context, cpu_debug_options: &mut CPUDebugControlOptions, emulator: &Emulator) { + egui::Window::new("Upcoming CPU Instruction") + .auto_sized() + .min_width(150.0) + .open(&mut cpu_debug_options.show_upcoming_instruction) + .show(ctx, |ui| { + let opcode = emulator.bus.read_external(emulator.cpu.registers.get_pc_address()); + let instruction = map_opcode_to_instruction(opcode); + ui.monospace( + instruction.mnemonic(&emulator.cpu.registers, &emulator.bus, opcode) + ); + }); +} diff --git a/snes-frontend/src/emu_ui/debug/debug.rs b/snes-frontend/src/emu_ui/debug/debug.rs new file mode 100644 index 0000000..83f63b3 --- /dev/null +++ b/snes-frontend/src/emu_ui/debug/debug.rs @@ -0,0 +1,55 @@ +use eframe::egui; +use snes_core::emulator::Emulator; + +use crate::emu_state::debug_options::DebugOptions; +use crate::emu_state::emulation::EmulationState; + +use super::memory_map::build_memory_map_window; +use super::cpu::build_cpu_debug_controls; +use super::ppu::build_ppu_debug_controls; +use super::ppu_graphics::build_bg_preview_windows; + + +pub fn build_all_debug_options(ctx: &egui::Context, debug_options: &mut DebugOptions, emulation_state: &mut EmulationState, emulator: &mut Emulator) { + build_debug_options_window(ctx, debug_options); + + if !debug_options.enable_debugging { + return; + } + + build_memory_map_window(ctx, &mut debug_options.memory_map_conrtrol_options, emulator); + build_cpu_debug_controls(ctx, &mut debug_options.cpu_debug_control_options, emulation_state, emulator); + build_ppu_debug_controls(ctx, &mut debug_options.ppu_debug_control_options, &emulator.bus.ppu.registers); + build_bg_preview_windows(ctx, &mut debug_options.ppu_debug_control_options.backgrounds, &emulator.bus.ppu.registers); +} + + +fn build_debug_options_window(ctx: &egui::Context, debug_options: &mut DebugOptions) { + egui::Window::new("Debug") + .open(&mut debug_options.show_debug_options_window) + .show(ctx, |ui| { + ui.checkbox( + &mut debug_options.enable_debugging, + "Enable debugging", + ); + ui.separator(); + if ui.selectable_label( + debug_options.memory_map_conrtrol_options.is_enabled, + "Show memory map" + ).clicked() { + debug_options.memory_map_conrtrol_options.is_enabled = !debug_options.memory_map_conrtrol_options.is_enabled; + } + if ui.selectable_label( + debug_options.cpu_debug_control_options.is_enabled, + "Show CPU Debug Controls" + ).clicked() { + debug_options.cpu_debug_control_options.is_enabled = !debug_options.cpu_debug_control_options.is_enabled; + } + if ui.selectable_label( + debug_options.ppu_debug_control_options.is_enabled, + "Show PPU Debug Controls" + ).clicked() { + debug_options.ppu_debug_control_options.is_enabled = !debug_options.ppu_debug_control_options.is_enabled; + } + }); +} diff --git a/snes-frontend/src/emu_ui/debug/memory_map.rs b/snes-frontend/src/emu_ui/debug/memory_map.rs new file mode 100644 index 0000000..e3b976d --- /dev/null +++ b/snes-frontend/src/emu_ui/debug/memory_map.rs @@ -0,0 +1,86 @@ +use eframe::egui; +use snes_core::emulator::Emulator; +use crate::emu_ui::debug::common::sanitize_input; + +use crate::emu_state::debug_options::{MemoryMapInputs, MemoryMapControlOptions}; + + +pub fn build_memory_map_window(ctx: &egui::Context, memory_map_control_options: &mut MemoryMapControlOptions, emulator: &Emulator) { + if !memory_map_control_options.is_enabled { + return + } + + egui::Window::new("Memory Map") + .min_size([400.0, 400.0]) + .open(&mut memory_map_control_options.is_enabled) + .show(ctx, |ui| { + build_inputs( + ui, + &mut memory_map_control_options.inputs, + &mut memory_map_control_options.inputs_result, + ); + ui.separator(); + egui::ScrollArea::both().show(ui, |ui| { + build_memory_map_text( + ui, + &memory_map_control_options.inputs_result, + emulator, + ); + }); + }); +} + +fn build_inputs(ui: &mut egui::Ui, input_values: &mut MemoryMapInputs, input_result: &mut MemoryMapInputs) { + ui.horizontal(|ui| { + ui.label("Page Start: "); + ui.text_edit_singleline(&mut input_values.page_start); + }); + ui.horizontal(|ui| { + ui.label("Page End: "); + ui.text_edit_singleline(&mut input_values.page_end); + }); + ui.horizontal(|ui| { + ui.label("Address Start: "); + ui.text_edit_singleline(&mut input_values.address_start); + }); + ui.horizontal(|ui| { + ui.label("Address End: "); + ui.text_edit_singleline(&mut input_values.address_end); + }); + if ui.button("Search").clicked() { + sanitize_input(&mut input_values.page_start, true); + sanitize_input(&mut input_values.page_end, true); + sanitize_input(&mut input_values.address_start, false); + sanitize_input(&mut input_values.address_end, false); + input_result.page_start = format!("{}", input_values.page_start); + input_result.page_end = format!("{}", input_values.page_end); + input_result.address_start = format!("{}", input_values.address_start); + input_result.address_end = format!("{}", input_values.address_end); + } +} + +fn build_memory_map_text(ui: &mut egui::Ui, input_values: &MemoryMapInputs, emulator: &Emulator) { + let page_start = u8::from_str_radix(&input_values.page_start, 16).unwrap(); + let page_end = u8::from_str_radix(&input_values.page_end, 16).unwrap(); + let address_start = u16::from_str_radix(&input_values.address_start, 16).unwrap(); + let address_end = u16::from_str_radix(&input_values.address_end, 16).unwrap(); + + let mut header = String::from(" | "); + for page in (page_start..=page_end).rev() { + header = format!("{}{:02X} ", header, page); + } + ui.monospace(header); + let mut divider = String::from("-----|-"); + for _page in (page_start..=page_end).rev() { + divider = format!("{}---", divider); + } + ui.monospace(divider); + for address in (address_start..=address_end).rev() { + let mut address_row = format!("{:04X} | ", address); + for page in (page_start..=page_end).rev() { + let bus_address = ((page as u32) << 16) | (address as u32); + address_row = format!("{}{:02X} ", address_row, emulator.bus.read_external(bus_address)); + } + ui.monospace(address_row); + } +} diff --git a/snes-frontend/src/emu_ui/debug/mod.rs b/snes-frontend/src/emu_ui/debug/mod.rs new file mode 100644 index 0000000..f6c99ce --- /dev/null +++ b/snes-frontend/src/emu_ui/debug/mod.rs @@ -0,0 +1,8 @@ +pub mod debug; +pub mod memory_map; +pub mod cpu; +pub mod ppu; +pub mod ppu_graphics; +pub mod common; + +pub use debug::build_all_debug_options; \ No newline at end of file diff --git a/snes-frontend/src/emu_ui/debug/ppu.rs b/snes-frontend/src/emu_ui/debug/ppu.rs new file mode 100644 index 0000000..afd2a72 --- /dev/null +++ b/snes-frontend/src/emu_ui/debug/ppu.rs @@ -0,0 +1,195 @@ +use eframe::egui; +use snes_core::ppu::registers::PPURegisters; + +use crate::emu_state::debug_options::PPUDebugControlOptions; +use crate::emu_ui::debug::common::sanitize_input; + + +pub fn build_ppu_debug_controls(ctx: &egui::Context, ppu_debug_options: &mut PPUDebugControlOptions, ppu_registers: &PPURegisters) { + if !ppu_debug_options.is_enabled { + return + } + + egui::Window::new("PPU Debug Controls") + .auto_sized() + .max_width(125.0) + .open(&mut ppu_debug_options.is_enabled) + .show(ctx, |ui| { + ui.monospace("Controls:"); + ui.horizontal(|ui| { + if ui.selectable_label( + ppu_debug_options.show_registers, + "Show PPU Registers" + ).clicked() { + ppu_debug_options.show_registers = !ppu_debug_options.show_registers; + } + if ui.selectable_label( + ppu_debug_options.show_vram, + "Show VRAM" + ).clicked() { + ppu_debug_options.show_vram = !ppu_debug_options.show_vram; + } + }); + ui.monospace("Backgrounds:"); + ui.horizontal(|ui| { + for bgdebug in ppu_debug_options.backgrounds.iter_mut() { + if ui.selectable_label( + bgdebug.is_enabled, + format!("Show {:?}", bgdebug.background), + ).clicked() { + bgdebug.is_enabled = !bgdebug.is_enabled; + } + } + }); + }); + + build_ppu_registers_window(ctx, ppu_debug_options, ppu_registers); + build_vram_window(ctx, ppu_debug_options, ppu_registers); +} + +fn build_ppu_registers_window(ctx: &egui::Context, ppu_debug_options: &mut PPUDebugControlOptions, ppu_registers: &PPURegisters) { + if !ppu_debug_options.show_registers { + return; + } + egui::Window::new("PPU Registers") + .max_width(180.0) + .open(&mut ppu_debug_options.show_registers) + .show(ctx, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { + ui.monospace(format!( + "H count: {} V count: {}", + ppu_registers.h_count, + ppu_registers.v_count, + )); + ui.monospace(format!("VBlanking: {}", ppu_registers.is_vblanking())); + ui.monospace(format!("NMI Flag: {}", ppu_registers.vblank_nmi)); + ui.separator(); + ui.monospace("Registers:"); + for (index, register_value) in ppu_registers.registers().iter().enumerate() { + let register = (index as u16) + 0x2100; + let register_name = get_register_name(register); + ui.monospace(format!("{:04X} {}: {:02X} {:08b}", register, register_name, register_value, register_value)); + } + }); + }); +} + +fn get_register_name(register: u16) -> &'static str { + match register { + snes_core::ppu::registers::INIDISP => "INIDISP", + snes_core::ppu::registers::OBSEL => "OBSEL ", + snes_core::ppu::registers::OAMADDL => "OAMADDL", + snes_core::ppu::registers::OAMADDH => "OAMADDH", + snes_core::ppu::registers::OAMDATA => "OAMDATA", + snes_core::ppu::registers::BGMODE => "BGMODE ", + snes_core::ppu::registers::MOSAIC => "MOSAIC ", + snes_core::ppu::registers::BG1SC => "BG1SC ", + snes_core::ppu::registers::BG2SC => "BG2SC ", + snes_core::ppu::registers::BG3SC => "BG3SC ", + snes_core::ppu::registers::BG4SC => "BG4SC ", + snes_core::ppu::registers::BG12NBA => "BG12NBA", + snes_core::ppu::registers::BG34NBA => "BG34NBA", + snes_core::ppu::registers::BG1HOFS => "BG1HOFS", + snes_core::ppu::registers::BG1VOFS => "BG1VOFS", + snes_core::ppu::registers::BG2HOFS => "BG2HOFS", + snes_core::ppu::registers::BG2VOFS => "BG2VOFS", + snes_core::ppu::registers::BG3HOFS => "BG3HOFS", + snes_core::ppu::registers::BG3VOFS => "BG3VOFS", + snes_core::ppu::registers::BG4HOFS => "BG4HOFS", + snes_core::ppu::registers::BG4VOFS => "BG4VOFS", + snes_core::ppu::registers::VMAIN => "VMAIN ", + snes_core::ppu::registers::VMADDL => "VMADDL ", + snes_core::ppu::registers::VMADDH => "VMADDH ", + snes_core::ppu::registers::VMDATAL => "VMDATAL", + snes_core::ppu::registers::VMDATAH => "VMDATAH", + snes_core::ppu::registers::M7SEL => "M7SEL ", + snes_core::ppu::registers::M7A => "M7A ", + snes_core::ppu::registers::M7B => "M7B ", + snes_core::ppu::registers::M7C => "M7C ", + snes_core::ppu::registers::M7D => "M7D ", + snes_core::ppu::registers::M7X => "M7X ", + snes_core::ppu::registers::M7Y => "M7Y ", + snes_core::ppu::registers::CGADD => "CGADD ", + snes_core::ppu::registers::CGDATA => "CGDATA ", + snes_core::ppu::registers::W12SEL => "W12SEL ", + snes_core::ppu::registers::W34SEL => "W34SEL ", + snes_core::ppu::registers::WOBJSEL => "WOBJSEL", + snes_core::ppu::registers::WH0 => "WH0 ", + snes_core::ppu::registers::WH1 => "WH1 ", + snes_core::ppu::registers::WH2 => "WH2 ", + snes_core::ppu::registers::WH3 => "WH3 ", + snes_core::ppu::registers::WBGLOG => "WBGLOG ", + snes_core::ppu::registers::WOBJLOG => "WOBJLOG", + snes_core::ppu::registers::TM => "TM ", + snes_core::ppu::registers::TS => "TS ", + snes_core::ppu::registers::TMW => "TMW ", + snes_core::ppu::registers::TSW => "TSW ", + snes_core::ppu::registers::CGWSEL => "CGWSEL ", + snes_core::ppu::registers::CGADSUB => "CGADSUB", + snes_core::ppu::registers::COLDATA => "COLDATA", + snes_core::ppu::registers::SETINI => "SETINI ", + snes_core::ppu::registers::MPYL => "MPYL ", + snes_core::ppu::registers::MPYM => "MPYM ", + snes_core::ppu::registers::MPYH => "MPYH ", + snes_core::ppu::registers::SLHV => "SLHV ", + snes_core::ppu::registers::RDOAM => "RDOAM ", + snes_core::ppu::registers::RDVRAML => "RDVRAML", + snes_core::ppu::registers::RDVRAMH => "RDVRAMH", + snes_core::ppu::registers::RDCGRAM => "RDCGRAM", + snes_core::ppu::registers::OPHCT => "OPHCT ", + snes_core::ppu::registers::OPVCT => "OPVCT ", + snes_core::ppu::registers::STAT77 => "STAT77 ", + snes_core::ppu::registers::STAT78 => "STAT78 ", + _ => "N/A ", + } +} + +fn build_vram_window(ctx: &egui::Context, ppu_debug_options: &mut PPUDebugControlOptions, ppu_registers: &PPURegisters) { + if !ppu_debug_options.show_vram { + return; + } + egui::Window::new("VRAM Viewer") + .open(&mut ppu_debug_options.show_vram) + .default_width(610.0) + .show(ctx, |ui| { + ui.horizontal(|ui| { + ui.label("Address Start: "); + ui.text_edit_singleline(&mut ppu_debug_options.vram_inputs.address_start); + }); + ui.horizontal(|ui| { + ui.label("Address End: "); + ui.text_edit_singleline(&mut ppu_debug_options.vram_inputs.address_end); + }); + + if ui.button("Search").clicked() { + sanitize_input(&mut ppu_debug_options.vram_inputs.address_start, false); + sanitize_input(&mut ppu_debug_options.vram_inputs.address_end, false); + } + + ui.separator(); + + egui::ScrollArea::both().show(ui, |ui| { + let address_start = u16::from_str_radix(&ppu_debug_options.vram_inputs.address_start, 16).unwrap(); + let address_end = u16::from_str_radix(&ppu_debug_options.vram_inputs.address_end, 16).unwrap(); + let mut header = String::from(" | "); + for page in 0x00..=0x0F { + header = format!("{} {:02X} ", header, page); + } + ui.monospace(header); + let mut divider = String::from("-----|-"); + for _ in 0x00..=0x0F { + divider = format!("{}-----", divider); + } + ui.monospace(divider); + let vector = (address_start..=address_end).collect::>(); + let chunks = vector.chunks(0x10); + for row in chunks { + let mut address_row = format!("{:04X} | ", row[0]); + for address in row { + address_row = format!("{}{:04X} ", address_row, ppu_registers.vram()[((*address) & 0x7FFF) as usize]); + } + ui.monospace(address_row); + } + }); + }); +} diff --git a/snes-frontend/src/emu_ui/debug/ppu_graphics.rs b/snes-frontend/src/emu_ui/debug/ppu_graphics.rs new file mode 100644 index 0000000..edf2a66 --- /dev/null +++ b/snes-frontend/src/emu_ui/debug/ppu_graphics.rs @@ -0,0 +1,165 @@ +use eframe::epaint::{Vec2, TextureHandle}; +use eframe::{egui::Context, epaint::ColorImage}; + +use eframe::egui::{self, TextureOptions, Ui}; +use snes_core::ppu::registers::{PPURegisters, Background}; +use crate::emu_state::debug_options::BgDebug; + + +pub fn build_bg_preview_windows(ctx: &Context, background_debug_list: &mut [BgDebug; 4], registers: &PPURegisters) { + for bgdebug in background_debug_list { + build_bg_preview_window(ctx, bgdebug, registers); + } +} + +fn build_bg_preview_window(ctx: &Context, bgdebug: &mut BgDebug, registers: &PPURegisters) { + if !bgdebug.is_enabled { + return; + } + initialize_bg_texture(ctx, bgdebug); + egui::Window::new("BG Background Preview") + .auto_sized() + .open(&mut bgdebug.is_enabled) + .show(ctx, |ui| { + egui::ScrollArea::both().show(ui, |ui| { + ui.label("Charset"); + compute_2bpp_bg_char_framebuffer(bgdebug.background, &mut bgdebug.char_framebuffer, registers); + paint_texture(ui, &mut bgdebug.char_texture, &mut bgdebug.char_framebuffer, 16 * 8, 8 * 8); + // ui.label("Background"); + // paint_texture(ui, &mut bgdebug.bg_texture, &mut bgdebug.bg_framebuffer, MAX_BG_WIDTH, MAX_BG_HEIGHT); + }); + }); +} + +fn compute_2bpp_bg_char_framebuffer(background: Background, framebuffer: &mut [u8], registers: &PPURegisters) { + let vram_base_address = registers.get_bg_char_base_address(background) as usize; + let vram = registers.vram(); + let width: usize = 8 * 16; + let height: usize = 8 * 8; + for y in 0..height { + for x in 0..width { + let current_char = (x / 8) + ((y / 8) * 16); + let current_char_column = x.rem_euclid(8); + let current_char_row = y.rem_euclid(8); + // 8x8 pixels, 2 bitplanes, each word (16bit) holds 8 pixels + // so 1 char is 8 bytes x 2 + let char_base_vram_address = current_char * 8; + let effective_vram_address = vram_base_address + ( + char_base_vram_address + (current_char_row) + ); + let current_pixel = x + (y * width); + + let vram_word = vram[effective_vram_address]; + let lsb_bitplane= vram_word as u8; + let msb_bitplane= (vram_word >> 8) as u8; + let pixels = [ + ( + (lsb_bitplane >> 7) | + ((msb_bitplane >> 7) << 1) + ), + ( + ((lsb_bitplane >> 6) & 1) | + (((msb_bitplane >> 6) & 1) << 1) + ), + ( + ((lsb_bitplane >> 5) & 1) | + (((msb_bitplane >> 5) & 1) << 1) + ), + ( + ((lsb_bitplane >> 4) & 1) | + (((msb_bitplane >> 4) & 1) << 1) + ), + ( + ((lsb_bitplane >> 3) & 1) | + (((msb_bitplane >> 3) & 1) << 1) + ), + ( + ((lsb_bitplane >> 2) & 1) | + (((msb_bitplane >> 2) & 1) << 1) + ), + ( + ((lsb_bitplane >> 1) & 1) | + (((msb_bitplane >> 1) & 1) << 1) + ), + ( + (lsb_bitplane & 1) | + ((msb_bitplane & 1) << 1) + ), + ]; + let effective_pixel = pixels[current_char_column]; + + let fb_index = current_pixel * 4; + let r = fb_index; + let g = fb_index + 1; + let b = fb_index + 2; + let a = fb_index + 3; + + framebuffer[a] = 0xFF; + match effective_pixel { + 0b00 => { + framebuffer[r] = 0xFF; + framebuffer[g] = 0xFF; + framebuffer[b] = 0xFF; + }, + 0b01 => { + framebuffer[r] = 0xAC; + framebuffer[g] = 0xAC; + framebuffer[b] = 0xAC; + }, + 0b10 => { + framebuffer[r] = 0x56; + framebuffer[g] = 0x56; + framebuffer[b] = 0x56; + }, + 0b11 => { + framebuffer[r] = 0x00; + framebuffer[g] = 0x00; + framebuffer[b] = 0x00; + }, + _ => unreachable!(), + }; + } + } +} + +fn paint_texture(ui: &mut Ui, texture: &mut Option, framebuffer: &[u8], width: usize, height: usize) { + if let Some(txt) = texture { + txt.set( + ColorImage::from_rgba_premultiplied([width, height], framebuffer), + TextureOptions::default(), + ); + let (whole_rect, _) = + ui.allocate_exact_size(Vec2::from([width as f32, height as f32]), egui::Sense::focusable_noninteractive()); + egui::Image::new(( + txt.id(), + txt.size_vec2(), + )) + .paint_at(ui, whole_rect); + } +} + +fn initialize_bg_texture(ctx: &Context, bgdebug: &mut BgDebug) { + if !bgdebug.is_enabled { + return; + } + if let None = bgdebug.bg_texture { + println!("Initializing BG texture {:?}", bgdebug.background); + bgdebug.bg_texture = Some( + ctx.load_texture( + format!("BG Texture {:?}", bgdebug.background), + ColorImage::default(), + Default::default(), + ) + ); + } + if let None = bgdebug.char_texture { + println!("Initializing Char texture {:?}", bgdebug.background); + bgdebug.char_texture = Some( + ctx.load_texture( + format!("Char Texture {:?}", bgdebug.background), + ColorImage::default(), + Default::default(), + ) + ); + } +} diff --git a/snes-frontend/src/emu_ui/game.rs b/snes-frontend/src/emu_ui/game.rs new file mode 100644 index 0000000..c1272ea --- /dev/null +++ b/snes-frontend/src/emu_ui/game.rs @@ -0,0 +1,18 @@ +use eframe::egui; + + +pub fn build_game_window(ctx: &egui::Context) { + egui::Window::new("Game").show(ctx, |ui| { + ui.label("Hello World!"); + render_framebuffer(ctx, ui, &[]); + }); +} + + +fn render_framebuffer(ctx: &egui::Context, ui: &mut egui::Ui, _framebuffer: &[u8]) { + ui.painter().rect_filled( + ctx.available_rect(), + egui::Rounding::ZERO, + egui::Color32::WHITE, + ) +} diff --git a/snes-frontend/src/emu_ui/menu.rs b/snes-frontend/src/emu_ui/menu.rs new file mode 100644 index 0000000..989e370 --- /dev/null +++ b/snes-frontend/src/emu_ui/menu.rs @@ -0,0 +1,26 @@ +use eframe::egui; +use snes_core::emulator::Emulator; + +use crate::emu_state::AppState; + + +pub fn build_menu_bar(emulator: &mut Emulator, ui: &mut egui::Ui, state: &mut AppState) { + egui::menu::bar(ui, |ui| { + ui.menu_button("Emulator", |ui| { + if ui.button("Load ROM file").clicked() { + if let Some(path) = rfd::FileDialog::new().pick_file() { + let picked_path = path.display().to_string(); + match emulator.bus.rom.load(&picked_path) { + Ok(_) => println!("Loaded ROM"), + Err(err) => println!("Error loading the ROM: {}", err), + } + } + } + }); + ui.menu_button("Debug", |ui| { + if ui.button("Show Debug Menu").clicked() { + state.debug_options.show_debug_options_window = true; + } + }); + }); +} diff --git a/snes-frontend/src/emu_ui/mod.rs b/snes-frontend/src/emu_ui/mod.rs new file mode 100644 index 0000000..de884d2 --- /dev/null +++ b/snes-frontend/src/emu_ui/mod.rs @@ -0,0 +1,3 @@ +pub mod game; +pub mod menu; +pub mod debug; diff --git a/snes-frontend/src/lib.rs b/snes-frontend/src/lib.rs deleted file mode 100644 index 67a8a1b..0000000 --- a/snes-frontend/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod ppu; -pub mod cpu; -pub mod state; \ No newline at end of file diff --git a/snes-frontend/src/main.rs b/snes-frontend/src/main.rs index c97799e..65341c2 100644 --- a/snes-frontend/src/main.rs +++ b/snes-frontend/src/main.rs @@ -1,526 +1,41 @@ -extern crate imgui_winit_support; +use eframe::egui; +use snes_core::emulator::Emulator; -use rfd::FileDialog; -use imgui::*; -use imgui_wgpu::{Renderer, RendererConfig}; -use pollster::block_on; - -use std::time::Instant; -use winit::{ - dpi::LogicalSize, - event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::Window, -}; - -use snes_frontend::state::State; -extern crate snes_core; -use snes_core::{emulator::Emulator, ppu::registers::{MAX_BG_WIDTH, MAX_BG_HEIGHT}}; -use snes_frontend::ppu as ppu_render; -use snes_frontend::cpu as cpu_debug; +mod emu_ui; +mod emu_state; -// TODO: refactor this please - -fn main() { - // Windowing state - let mut state = State::new(); - let mut emulator = Emulator::new(); - - - // Set up window and GPU - let event_loop = EventLoop::new(); - - let instance = wgpu::Instance::new(wgpu::Backends::PRIMARY); - - let (window, size, surface) = { - let version = env!("CARGO_PKG_VERSION"); - - let window = Window::new(&event_loop).unwrap(); - window.set_inner_size(LogicalSize { - width: 1280.0, - height: 720.0, - }); - window.set_title(&format!("SNES {}", version)); - let size = window.inner_size(); - - let surface = unsafe { instance.create_surface(&window) }; - - (window, size, surface) - }; - - let hidpi_factor = window.scale_factor(); - - let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::HighPerformance, - compatible_surface: Some(&surface), - force_fallback_adapter: false, - })) - .unwrap(); - - let (device, queue) = - block_on(adapter.request_device(&wgpu::DeviceDescriptor::default(), None)).unwrap(); - - // Set up swap chain - let surface_desc = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: wgpu::TextureFormat::Bgra8UnormSrgb, - width: size.width as u32, - height: size.height as u32, - present_mode: wgpu::PresentMode::Mailbox, - }; - - surface.configure(&device, &surface_desc); - - // Set up dear imgui - let mut imgui = imgui::Context::create(); - let mut platform = imgui_winit_support::WinitPlatform::init(&mut imgui); - platform.attach_window( - imgui.io_mut(), - &window, - imgui_winit_support::HiDpiMode::Default, - ); - imgui.set_ini_filename(None); - - let font_size = (14.0 * hidpi_factor) as f32; - imgui.io_mut().font_global_scale = (1.0 / hidpi_factor) as f32; - - imgui.fonts().add_font(&[FontSource::DefaultFontData { - config: Some(imgui::FontConfig { - oversample_h: 1, - pixel_snap_h: true, - size_pixels: font_size, - ..Default::default() - }), - }]); - - // - // Set up dear imgui wgpu renderer - // - let clear_color = wgpu::Color { - r: 0.1, - g: 0.2, - b: 0.3, - a: 1.0, - }; - - let renderer_config = RendererConfig { - texture_format: surface_desc.format, - ..Default::default() - }; - - let mut renderer = Renderer::new(&mut imgui, &device, &queue, renderer_config); - - let mut last_frame = Instant::now(); - - let mut last_cursor = None; - - // Generate dummy texture - const WIDTH: usize = 400; - const HEIGHT: usize = 400; - let mut data = Vec::with_capacity(WIDTH * HEIGHT); - for i in 0..WIDTH { - for j in 0..HEIGHT { - // Insert RGB values - data.push(i as u8); - data.push(j as u8); - data.push((i + j) as u8); - } - } - - let texture = imgui_wgpu::Texture::new( - &device, - &renderer, - imgui_wgpu::TextureConfig { - label: Some("framebuffer texture"), - size: wgpu::Extent3d { - width: WIDTH as u32, - height: HEIGHT as u32, - depth_or_array_layers: 1, - }, - format: Some(wgpu::TextureFormat::Rgba8Unorm), - ..imgui_wgpu::TextureConfig::default() - }, - ); - let texture_id = renderer.textures.insert(texture); - - for bgdebug in state.ppudebug.backgrounds.iter_mut() { - bgdebug.texture_id = Some( - ppu_render::background_texture(&device, &mut renderer, bgdebug.background, MAX_BG_WIDTH as u32, MAX_BG_HEIGHT as u32, "Background") - ); - bgdebug.char_texture_id = Some( - ppu_render::background_texture(&device, &mut renderer, bgdebug.background, 16 * 8, 8 * 8, "Charset") - ); - } - - - // Event loop - event_loop.run(move |event, _, control_flow| { - *control_flow = if cfg!(feature = "metal-auto-capture") { - ControlFlow::Exit - } else { - ControlFlow::Poll - }; - match event { - Event::WindowEvent { - event: WindowEvent::Resized(_), - .. - } => { - let size = window.inner_size(); - - let surface_desc = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: wgpu::TextureFormat::Bgra8UnormSrgb, - width: size.width as u32, - height: size.height as u32, - present_mode: wgpu::PresentMode::Mailbox, - }; - - surface.configure(&device, &surface_desc); - } - Event::WindowEvent { - event: - WindowEvent::KeyboardInput { - input: - KeyboardInput { - virtual_keycode: Some(VirtualKeyCode::Escape), - state: ElementState::Pressed, - .. - }, - .. - }, - .. - } - | Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => { - *control_flow = ControlFlow::Exit; - } - Event::MainEventsCleared => window.request_redraw(), - Event::RedrawEventsCleared => { - let now = Instant::now(); - imgui.io_mut().update_delta_time(now - last_frame); - last_frame = now; - - let frame = match surface.get_current_texture() { - Ok(frame) => frame, - Err(e) => { - eprintln!("dropped frame: {:?}", e); - return; - } - }; - platform - .prepare_frame(imgui.io_mut(), &window) - .expect("Failed to prepare frame"); - let ui = imgui.frame(); - - { - ui.main_menu_bar(|| { - ui.menu("Emulator", || { - if imgui::MenuItem::new("Load ROM") - .build(&ui) - { - if let Some(path) = FileDialog::new() - .pick_file() - { - match emulator.bus.rom.load(&String::from(path.to_str().unwrap())) { - Ok(_) => { - // state.emulation.is_paused = false; - }, - Err(err) => { - state.error_message.show = true; - state.error_message.message = format!( - "Could not load rom: {}", err, - ); - } - } - } - } - ui.separator(); - }); - ui.menu("Debug", || { - if imgui::MenuItem::new("Show Debug menu") - .build(&ui) - { - state.debug_options.show_debug_window = true; - } - }); - }); - - if state.debug_options.show_debug_window { - let window = imgui::Window::new("Debug window"); - window - .position([15.0, 885.0], imgui::Condition::FirstUseEver) - .size([300.0, 300.0], Condition::FirstUseEver) - .opened(&mut state.debug_options.show_debug_window) - .build(&ui, || { - ui.text("Controls:"); - let pause_text = if state.emulation.is_paused {"Resume"} else {"Pause"}; - if ui.button(pause_text) { - state.emulation.is_paused = !state.emulation.is_paused; - } - ui.disabled(!state.emulation.is_paused, || { - if ui.button("Tick") { - emulator.tick(); - } - }); - if ui.button("Reset Vector") { - emulator.reset(); - } - ui.separator(); - - ui.checkbox( - "Enable debugging", - &mut state.debug_options.is_enabled, - ); - ui.separator(); - ui.checkbox( - "Show Memory Map", - &mut state.debug_options.memory_map.is_enabled , - ); - ui.checkbox( - "Show PPU debugging options", - &mut state.ppudebug.is_enabled, - ); - ui.checkbox( - "Show CPU registers", - &mut state.debug_options.show_cpu_registers, - ); - ui.checkbox( - "Show CPU disassembler", - &mut state.debug_options.show_cpu_disassembler, - ); - }); - } - - // Render all debugging stuff - if state.debug_options.is_enabled { - if state.ppudebug.is_enabled { - let window = imgui::Window::new("PPU Debugging options"); - window - .position([330.0, 885.0], imgui::Condition::FirstUseEver) - .size([180.0, 280.0], Condition::FirstUseEver) - .build(&ui, || { - ui.text("Backgrounds:"); - for bgdebug in state.ppudebug.backgrounds.iter_mut() { - ui.checkbox( - format!("Show {:?}", bgdebug.background), - &mut bgdebug.is_enabled, - ); - } - - ui.separator(); - - ui.text("Registers:"); - ui.checkbox( - "Show PPU Registers", - &mut state.ppudebug.show_registers, - ); - - ui.separator(); - - ui.text("VRAM:"); - ui.checkbox( - "Show PPU VRAM", - &mut state.ppudebug.show_vram, - ); - }); - for bgdebug in state.ppudebug.backgrounds.iter_mut() { - ppu_render::background_window( - bgdebug, - &emulator.bus.ppu.registers, - &ui, - &mut renderer, - &queue, - ) - } - - if state.ppudebug.show_registers { - ppu_render::registers_window( - &emulator.bus.ppu.registers, - &mut state.ppudebug.show_registers, - &ui, - ) - } - - if state.ppudebug.show_vram { - ppu_render::vram_window( - &emulator.bus.ppu.registers, - &mut state.ppudebug.vram_map, - &mut state.ppudebug.show_vram, - &ui, - ) - } - } - - if state.debug_options.show_cpu_registers { - let window = imgui::Window::new("CPU Registers"); - window - .position([15.0, 480.0], Condition::FirstUseEver) - .size([220.0, 310.0], Condition::FirstUseEver) - .build(&ui, || { - ui.text(format!("SP: | {:#06X}", emulator.cpu.registers.sp)); - ui.text(format!("X: | {:#06X}", emulator.cpu.registers.x)); - ui.text(format!("Y: | {:#06X}", emulator.cpu.registers.y)); - ui.text(format!("A: | {:#06X}", emulator.cpu.registers.a)); - ui.text(format!("P: | {:#04X}", emulator.cpu.registers.p)); - ui.text(format!("D: | {:#06X}", emulator.cpu.registers.d)); - ui.text(format!("PBR: | {:#04X}", emulator.cpu.registers.pbr)); - ui.text(format!("DBR: | {:#04X}", emulator.cpu.registers.dbr)); - ui.text(format!("PC: | {:#06X}", emulator.cpu.registers.pc)); - ui.text(format!("EMU MODE: | {}", emulator.cpu.registers.emulation_mode)); - ui.separator(); - ui.text("Status flags:"); - ui.text("NVMXDIZC"); - ui.text(format!("{:08b}", emulator.cpu.registers.p)); - }); - } - - if state.debug_options.show_cpu_disassembler { - let window = imgui::Window::new("CPU Disassembler"); - window - .position([15.0, 800.0], imgui::Condition::FirstUseEver) - .size([300.0, 70.0], Condition::FirstUseEver) - .build(&ui, || { - ui.text("Upcoming instruction:"); - ui.text(cpu_debug::CPUDisassembler::get_next_instruction(&mut emulator)); - }); - } - - if state.debug_options.memory_map.is_enabled { - let window = imgui::Window::new("Memory Map"); - window - .position([500.0, 30.0], Condition::FirstUseEver) - .size([450.0, 480.0], Condition::FirstUseEver) - .build(&ui, || { - let page_start_input = ui.input_text( - "Page start", - &mut state.debug_options.memory_map.page_start_input - ); - page_start_input.build(); - - let page_end_input = ui.input_text( - "Page end", - &mut state.debug_options.memory_map.page_end_input - ); - page_end_input.build(); - - let address_start_input = ui.input_text( - "Address start", - &mut state.debug_options.memory_map.address_start_input - ); - address_start_input.build(); - - let address_end_input = ui.input_text( - "Address end", - &mut state.debug_options.memory_map.address_end_input, - ); - address_end_input.build(); - - if ui.button("Apply") { - state.debug_options.memory_map.set_values_from_inputs(); - } - - ui.separator(); - let page_start = state.debug_options.memory_map.page_start; - let page_end = state.debug_options.memory_map.page_end; - let address_start = state.debug_options.memory_map.address_start; - let address_end = state.debug_options.memory_map.address_end; - let mut header = String::from(" | "); - for page in (page_start..=page_end).rev() { - header = format!("{}{:02X} ", header, page); - } - ui.text(header); - let mut divider = String::from("-----|-"); - for _page in (page_start..=page_end).rev() { - divider = format!("{}---", divider); - } - ui.text(divider); - for address in (address_start..=address_end).rev() { - let mut address_row = format!("{:04X} | ", address); - for page in (page_start..=page_end).rev() { - let bus_address = ((page as u32) << 16) | (address as u32); - address_row = format!("{}{:02X} ", address_row, emulator.bus.read_external(bus_address)); - } - ui.text(address_row); - } - }); - } - } - - // Actually run the emulation - if !state.emulation.is_paused { - emulator.loop_frame(); - } - - // Render emulator framebuffer - { - let tex = renderer.textures.get_mut(texture_id).unwrap(); - tex.write(&queue, &vec![0xAA; 400 * 400 * 4], 400, 400); - let game_window = imgui::Window::new("Game"); - game_window - .position([15.0, 30.0], Condition::FirstUseEver) - .size([425.0, 440.0], Condition::FirstUseEver) - .collapsible(false) - .build(&ui, || { - let game_image = imgui::Image::new(texture_id, [400.0, 400.0]); - game_image.build(&ui); - }); - } - - if state.error_message.show { - let window = imgui::Window::new("Error"); - window - .size([300.0, 100.0], Condition::Always) - .collapsible(false) - .build(&ui, || { - ui.text(&state.error_message.message); - if ui.button("Ok") { - state.error_message.show = false - } - }); - } - - } - - let mut encoder: wgpu::CommandEncoder = - device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - - if last_cursor != Some(ui.mouse_cursor()) { - last_cursor = Some(ui.mouse_cursor()); - platform.prepare_render(&ui, &window); - } - - let view = frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: None, - color_attachments: &[wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(clear_color), - store: true, - }, - }], - depth_stencil_attachment: None, - }); - - renderer - .render(ui.render(), &queue, &device, &mut rpass) - .expect("Rendering failed"); - - drop(rpass); - - queue.submit(Some(encoder.finish())); - - frame.present(); - } - _ => (), - } - - platform.handle_event(imgui.io_mut(), &window, &event); - }); +#[derive(Default)] +struct SnesEmulatorApp { + emulator: Emulator, + state: emu_state::AppState, +} + +impl SnesEmulatorApp { + fn new(_cc: &eframe::CreationContext<'_>) -> Self { + Self::default() + } +} + +impl eframe::App for SnesEmulatorApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + emu_ui::menu::build_menu_bar(&mut self.emulator, ui, &mut self.state); + ui.separator(); + // ui::game::build_game_window(ctx); + }); + if !self.state.emulation_state.is_paused { + self.emulator.loop_frame(); + } + emu_ui::debug::build_all_debug_options(ctx, &mut self.state.debug_options, &mut self.state.emulation_state, &mut self.emulator); + } +} + +fn main() -> eframe::Result<()> { + let native_options = eframe::NativeOptions::default(); + eframe::run_native( + "SNES Emulator", + native_options, + Box::new(|cc| Box::new(SnesEmulatorApp::new(cc))), + ) } diff --git a/snes-frontend/src/ppu.rs b/snes-frontend/src/ppu.rs deleted file mode 100644 index a2f2743..0000000 --- a/snes-frontend/src/ppu.rs +++ /dev/null @@ -1,318 +0,0 @@ -extern crate snes_core; -use snes_core::ppu::registers::{ - MAX_BG_WIDTH, - MAX_BG_HEIGHT, - PPURegisters, - Background as PPUBg, -}; -use imgui_wgpu::Renderer; -use imgui::TextureId; -use wgpu::{Device, Queue}; - -use super::state::{BgDebug, VRAMMap}; - - -pub fn background_texture( - device: &Device, - renderer: &mut Renderer, - background: PPUBg, - width: u32, - height: u32, - name: &str, -) -> TextureId { - let texture = imgui_wgpu::Texture::new( - device, - renderer, - imgui_wgpu::TextureConfig { - label: Some(&format!("{} {:?} texture", name, background)), - size: wgpu::Extent3d { - width: width, - height: height, - depth_or_array_layers: 1, - }, - format: Some(wgpu::TextureFormat::Rgba8Unorm), - ..imgui_wgpu::TextureConfig::default() - }, - ); - renderer.textures.insert(texture) -} - -fn render_background(bgdebug: &mut BgDebug, registers: &PPURegisters) { - let (tile_width, tile_height) = registers.get_bg_tile_size(bgdebug.background).to_usize(); - let (bg_width, bg_height) = registers.get_bg_size(bgdebug.background).to_usize(); - let width = tile_width * bg_width; - let height = tile_height * bg_height; - for x in 0..width { - for y in 0..height { - let index = ((y * MAX_BG_WIDTH) + x) * 4; - let r = index; - let g = index + 1; - let b = index + 2; - let a = index + 3; - - bgdebug.framebuffer[r] = 0xFF; - bgdebug.framebuffer[g] = 0xFF; - bgdebug.framebuffer[b] = 0xFF; - bgdebug.framebuffer[a] = 0xFF; - } - } -} - -fn render_background_char_2bpp(bgdebug: &mut BgDebug, registers: &PPURegisters) { - let vram_base_address = registers.get_bg_char_base_address(bgdebug.background) as usize; - let vram = registers.vram(); - let width: usize = 8 * 16; - let height: usize = 8 * 8; - for y in 0..height { - for x in 0..width { - let current_char = (x / 8) + ((y / 8) * 16); - let current_char_column = x.rem_euclid(8); - let current_char_row = y.rem_euclid(8); - // 8x8 pixels, 2 bitplanes, each word (16bit) holds 8 pixels - // so 1 char is 8 bytes x 2 - let char_base_vram_address = current_char * 8; - let effective_vram_address = vram_base_address + ( - char_base_vram_address + (current_char_row) - ); - let current_pixel = x + (y * width); - - let vram_word = vram[effective_vram_address]; - let lsb_bitplane= vram_word as u8; - let msb_bitplane= (vram_word >> 8) as u8; - let pixels = [ - ( - (lsb_bitplane >> 7) | - ((msb_bitplane >> 7) << 1) - ), - ( - ((lsb_bitplane >> 6) & 1) | - (((msb_bitplane >> 6) & 1) << 1) - ), - ( - ((lsb_bitplane >> 5) & 1) | - (((msb_bitplane >> 5) & 1) << 1) - ), - ( - ((lsb_bitplane >> 4) & 1) | - (((msb_bitplane >> 4) & 1) << 1) - ), - ( - ((lsb_bitplane >> 3) & 1) | - (((msb_bitplane >> 3) & 1) << 1) - ), - ( - ((lsb_bitplane >> 2) & 1) | - (((msb_bitplane >> 2) & 1) << 1) - ), - ( - ((lsb_bitplane >> 1) & 1) | - (((msb_bitplane >> 1) & 1) << 1) - ), - ( - (lsb_bitplane & 1) | - ((msb_bitplane & 1) << 1) - ), - ]; - let effective_pixel = pixels[current_char_column]; - - let fb_index = current_pixel * 4; - let r = fb_index; - let g = fb_index + 1; - let b = fb_index + 2; - let a = fb_index + 3; - - bgdebug.char_framebuffer[a] = 0xFF; - match effective_pixel { - 0b00 => { - bgdebug.char_framebuffer[r] = 0xFF; - bgdebug.char_framebuffer[g] = 0xFF; - bgdebug.char_framebuffer[b] = 0xFF; - }, - 0b01 => { - bgdebug.char_framebuffer[r] = 0xAC; - bgdebug.char_framebuffer[g] = 0xAC; - bgdebug.char_framebuffer[b] = 0xAC; - }, - 0b10 => { - bgdebug.char_framebuffer[r] = 0x56; - bgdebug.char_framebuffer[g] = 0x56; - bgdebug.char_framebuffer[b] = 0x56; - }, - 0b11 => { - bgdebug.char_framebuffer[r] = 0x00; - bgdebug.char_framebuffer[g] = 0x00; - bgdebug.char_framebuffer[b] = 0x00; - }, - _ => unreachable!(), - }; - } - } -} - -pub fn background_window(bgdebug: &mut BgDebug, registers: &PPURegisters, ui: &imgui::Ui, renderer: &mut Renderer, queue: &Queue) { - if !bgdebug.is_enabled { - return; - } - let texture_id = bgdebug.texture_id.unwrap(); - let char_texture_id = bgdebug.char_texture_id.unwrap(); - render_background(bgdebug, registers); - render_background_char_2bpp(bgdebug, registers); - - let tex = renderer.textures.get_mut(texture_id).unwrap(); - tex.write(queue, &bgdebug.framebuffer, MAX_BG_WIDTH as u32, MAX_BG_HEIGHT as u32); - let char_tex = renderer.textures.get_mut(char_texture_id).unwrap(); - char_tex.write(queue, &bgdebug.char_framebuffer, 16 * 8, 8 * 8); - let bg_window = imgui::Window::new(format!("BG: {:?}", bgdebug.background)); - bg_window - .size([MAX_BG_WIDTH as f32, MAX_BG_HEIGHT as f32], imgui::Condition::FirstUseEver) - .opened(&mut bgdebug.is_enabled) - .build(ui, || { - ui.text("Charset:"); - let charset_image = imgui::Image::new(char_texture_id, [16.0 * 8.0 * 2.0, 8.0 * 8.0 * 2.0]); - charset_image.build(&ui); - ui.separator(); - ui.text("Background:"); - let bg_image = imgui::Image::new(texture_id, [MAX_BG_WIDTH as f32, MAX_BG_HEIGHT as f32]); - bg_image.build(&ui); - }); -} - -fn get_register_name(register: u16) -> &'static str { - match register { - snes_core::ppu::registers::INIDISP => "INIDISP", - snes_core::ppu::registers::OBSEL => "OBSEL ", - snes_core::ppu::registers::OAMADDL => "OAMADDL", - snes_core::ppu::registers::OAMADDH => "OAMADDH", - snes_core::ppu::registers::OAMDATA => "OAMDATA", - snes_core::ppu::registers::BGMODE => "BGMODE ", - snes_core::ppu::registers::MOSAIC => "MOSAIC ", - snes_core::ppu::registers::BG1SC => "BG1SC ", - snes_core::ppu::registers::BG2SC => "BG2SC ", - snes_core::ppu::registers::BG3SC => "BG3SC ", - snes_core::ppu::registers::BG4SC => "BG4SC ", - snes_core::ppu::registers::BG12NBA => "BG12NBA", - snes_core::ppu::registers::BG34NBA => "BG34NBA", - snes_core::ppu::registers::BG1HOFS => "BG1HOFS", - snes_core::ppu::registers::BG1VOFS => "BG1VOFS", - snes_core::ppu::registers::BG2HOFS => "BG2HOFS", - snes_core::ppu::registers::BG2VOFS => "BG2VOFS", - snes_core::ppu::registers::BG3HOFS => "BG3HOFS", - snes_core::ppu::registers::BG3VOFS => "BG3VOFS", - snes_core::ppu::registers::BG4HOFS => "BG4HOFS", - snes_core::ppu::registers::BG4VOFS => "BG4VOFS", - snes_core::ppu::registers::VMAIN => "VMAIN ", - snes_core::ppu::registers::VMADDL => "VMADDL ", - snes_core::ppu::registers::VMADDH => "VMADDH ", - snes_core::ppu::registers::VMDATAL => "VMDATAL", - snes_core::ppu::registers::VMDATAH => "VMDATAH", - snes_core::ppu::registers::M7SEL => "M7SEL ", - snes_core::ppu::registers::M7A => "M7A ", - snes_core::ppu::registers::M7B => "M7B ", - snes_core::ppu::registers::M7C => "M7C ", - snes_core::ppu::registers::M7D => "M7D ", - snes_core::ppu::registers::M7X => "M7X ", - snes_core::ppu::registers::M7Y => "M7Y ", - snes_core::ppu::registers::CGADD => "CGADD ", - snes_core::ppu::registers::CGDATA => "CGDATA ", - snes_core::ppu::registers::W12SEL => "W12SEL ", - snes_core::ppu::registers::W34SEL => "W34SEL ", - snes_core::ppu::registers::WOBJSEL => "WOBJSEL", - snes_core::ppu::registers::WH0 => "WH0 ", - snes_core::ppu::registers::WH1 => "WH1 ", - snes_core::ppu::registers::WH2 => "WH2 ", - snes_core::ppu::registers::WH3 => "WH3 ", - snes_core::ppu::registers::WBGLOG => "WBGLOG ", - snes_core::ppu::registers::WOBJLOG => "WOBJLOG", - snes_core::ppu::registers::TM => "TM ", - snes_core::ppu::registers::TS => "TS ", - snes_core::ppu::registers::TMW => "TMW ", - snes_core::ppu::registers::TSW => "TSW ", - snes_core::ppu::registers::CGWSEL => "CGWSEL ", - snes_core::ppu::registers::CGADSUB => "CGADSUB", - snes_core::ppu::registers::COLDATA => "COLDATA", - snes_core::ppu::registers::SETINI => "SETINI ", - snes_core::ppu::registers::MPYL => "MPYL ", - snes_core::ppu::registers::MPYM => "MPYM ", - snes_core::ppu::registers::MPYH => "MPYH ", - snes_core::ppu::registers::SLHV => "SLHV ", - snes_core::ppu::registers::RDOAM => "RDOAM ", - snes_core::ppu::registers::RDVRAML => "RDVRAML", - snes_core::ppu::registers::RDVRAMH => "RDVRAMH", - snes_core::ppu::registers::RDCGRAM => "RDCGRAM", - snes_core::ppu::registers::OPHCT => "OPHCT ", - snes_core::ppu::registers::OPVCT => "OPVCT ", - snes_core::ppu::registers::STAT77 => "STAT77 ", - snes_core::ppu::registers::STAT78 => "STAT78 ", - _ => "N/A ", - } -} - -pub fn registers_window(ppu_registers: &PPURegisters, show_registers: &mut bool, ui: &imgui::Ui) { - let window = imgui::Window::new("PPU Registers"); - window - .position([250.0, 480.0], imgui::Condition::FirstUseEver) - .size([230.0, 310.0], imgui::Condition::FirstUseEver) - .opened(show_registers) - .build(ui, || { - ui.text("H/V Counters:"); - ui.text(format!("V: {}", ppu_registers.v_count)); - ui.text(format!("H: {}", ppu_registers.h_count)); - ui.text(format!("VBlanking: {}", ppu_registers.is_vblanking())); - ui.text(format!("NMI Flag: {}", ppu_registers.vblank_nmi)); - ui.separator(); - ui.text("Registers:"); - for (index, register_value) in ppu_registers.registers().iter().enumerate() { - let register = (index as u16) + 0x2100; - let register_name = get_register_name(register); - ui.text(format!("{:04X} {}: {:02X} {:08b}", register, register_name, register_value, register_value)); - } - }); -} - -pub fn vram_window(ppu_registers: &PPURegisters, vram_debug: &mut VRAMMap, show_vram: &mut bool, ui: &imgui::Ui) { - let window = imgui::Window::new("VRAM"); - window - .size([450.0, 435.0], imgui::Condition::FirstUseEver) - .position([500.0, 530.0], imgui::Condition::FirstUseEver) - .opened(show_vram) - .build(ui, || { - let address_start_input = ui.input_text( - "Address start", - &mut vram_debug.address_start_input - ); - address_start_input.build(); - - let address_end_input = ui.input_text( - "Address end", - &mut vram_debug.address_end_input, - ); - address_end_input.build(); - - if ui.button("Apply") { - vram_debug.set_values_from_inputs(); - } - - ui.separator(); - let address_start = vram_debug.address_start; - let address_end = vram_debug.address_end; - let mut header = String::from(" | "); - for page in 0x00..=0x0F { - header = format!("{} {:02X} ", header, page); - } - ui.text(header); - let mut divider = String::from("-----|-"); - for _ in 0x00..=0x0F { - divider = format!("{}-----", divider); - } - ui.text(divider); - let vector = (address_start..=address_end).collect::>(); - let chunks = vector.chunks(0x10); - for row in chunks { - let mut address_row = format!("{:04X} | ", row[0]); - for address in row { - address_row = format!("{}{:04X} ", address_row, ppu_registers.vram()[((*address) & 0x7FFF) as usize]); - } - ui.text(address_row); - } - }); -} diff --git a/snes-frontend/src/state.rs b/snes-frontend/src/state.rs deleted file mode 100644 index 16cef61..0000000 --- a/snes-frontend/src/state.rs +++ /dev/null @@ -1,237 +0,0 @@ -extern crate snes_core; -use snes_core::ppu::registers::{ - Background as PPUBg, - MAX_BG_WIDTH, - MAX_BG_HEIGHT, -}; -use regex::Regex; - - -pub struct MemoryMap { - pub is_enabled: bool, - pub page_start: u8, - pub page_end: u8, - pub address_start: u16, - pub address_end: u16, - pub page_start_input: String, - pub page_end_input: String, - pub address_start_input: String, - pub address_end_input: String, -} - -impl MemoryMap { - pub fn new() -> Self { - Self { - is_enabled: true, - page_start: 0xF0, - page_end: 0xFF, - address_start: 0xFFF0, - address_end: 0xFFFF, - page_start_input: String::from("0xF0"), - page_end_input: String::from("0xFF"), - address_start_input: String::from("0xFFF0"), - address_end_input: String::from("0xFFFF"), - } - } - - fn validate_values(&mut self) { - if self.page_start > self.page_end { - self.page_start = self.page_end; - } - if self.page_end < self.page_start { - self.page_end = self.page_start; - } - if self.address_start > self.address_end { - self.address_start = self.address_end; - } - if self.address_end < self.address_start { - self.address_end = self.address_start; - } - } - - pub fn set_values_from_inputs(&mut self) { - let page_regex = Regex::new(r"^(0x)?[0-9a-fA-F]{2,2}$").unwrap(); - let address_regex = Regex::new(r"^(0x)?[0-9a-fA-F]{4,4}$").unwrap(); - - if !page_regex.is_match(&self.page_start_input) { - println!("Page start didn't match"); - self.page_start_input = String::from("0x00"); - } - if !page_regex.is_match(&self.page_end_input) { - println!("Page end didn't match"); - self.page_end_input = String::from("0x00"); - } - if !address_regex.is_match(&self.address_start_input) { - println!("Addr start didn't match"); - self.address_start_input = String::from("0x0000"); - } - if !address_regex.is_match(&self.address_end_input) { - println!("Addr end didn't match"); - self.address_end_input = String::from("0x0000"); - } - - self.page_start = u8::from_str_radix(&self.page_start_input.trim_start_matches("0x"), 16).unwrap(); - self.page_end = u8::from_str_radix(&self.page_end_input.trim_start_matches("0x"), 16).unwrap(); - self.address_start = u16::from_str_radix(&self.address_start_input.trim_start_matches("0x"), 16).unwrap(); - self.address_end = u16::from_str_radix(&self.address_end_input.trim_start_matches("0x"), 16).unwrap(); - self.validate_values() - } -} - -pub struct VRAMMap { - pub address_start: u16, - pub address_end: u16, - pub address_start_input: String, - pub address_end_input: String, -} - -impl VRAMMap { - pub fn new() -> Self { - Self { - address_start: 0x0000, - address_end: 0x00FF, - address_start_input: String::from("0000"), - address_end_input: String::from("0100"), - } - } - - fn validate_values(&mut self) { - if self.address_start > self.address_end { - self.address_start = self.address_end; - } - if self.address_end < self.address_start { - self.address_end = self.address_start; - } - } - - pub fn set_values_from_inputs(&mut self) { - let address_regex = Regex::new(r"^(0x)?[0-9a-fA-F]{4,4}$").unwrap(); - - if !address_regex.is_match(&self.address_start_input) { - println!("Addr start didn't match"); - self.address_start_input = String::from("0x0000"); - } - if !address_regex.is_match(&self.address_end_input) { - println!("Addr end didn't match"); - self.address_end_input = String::from("0x0000"); - } - - self.address_start = u16::from_str_radix(&self.address_start_input.trim_start_matches("0x"), 16).unwrap(); - self.address_end = u16::from_str_radix(&self.address_end_input.trim_start_matches("0x"), 16).unwrap(); - self.validate_values() - } -} - -pub struct DebugOptions { - pub is_enabled: bool, - pub show_debug_window: bool, - pub show_cpu_registers: bool, - pub show_cpu_disassembler: bool, - pub show_spc700_registers: bool, - pub memory_map: MemoryMap, -} - -impl DebugOptions { - pub fn new() -> Self { - Self { - is_enabled: true, - show_debug_window: true, - show_cpu_registers: true, - show_cpu_disassembler: true, - show_spc700_registers: true, - memory_map: MemoryMap::new(), - } - } -} - -pub struct ErrorMessage { - pub show: bool, - pub message: String, -} - -impl ErrorMessage { - pub fn new() -> Self { - Self { - show: false, - message: String::from(""), - } - } -} - -pub struct BgDebug { - pub background: PPUBg, - pub is_enabled: bool, - pub texture_id: Option, - pub char_texture_id: Option, - pub framebuffer: Vec, - pub char_framebuffer: Vec, -} - -impl BgDebug { - pub fn new(background: PPUBg) -> Self { - Self { - background: background, - is_enabled: false, - texture_id: None, - char_texture_id: None, - framebuffer: vec![0x00; MAX_BG_WIDTH * MAX_BG_HEIGHT * 4], - // 8x8 pixels, 16x8 characters - char_framebuffer: vec![0x00; 8 * 8 * 16 * 8 * 4], - } - } -} - -pub struct PPUDebug { - pub is_enabled: bool, - pub show_registers: bool, - pub show_vram: bool, - pub backgrounds: [BgDebug; 4], - pub vram_map: VRAMMap, -} - -impl PPUDebug { - pub fn new() -> Self { - Self { - is_enabled: true, - show_registers: true, - show_vram: true, - backgrounds: [ - BgDebug::new(PPUBg::Bg1), - BgDebug::new(PPUBg::Bg2), - BgDebug::new(PPUBg::Bg3), - BgDebug::new(PPUBg::Bg4), - ], - vram_map: VRAMMap::new(), - } - } -} - -pub struct Emulation { - pub is_paused: bool, -} - -impl Emulation { - pub fn new() -> Self { - Self { - is_paused: true, - } - } -} - -pub struct State { - pub debug_options: DebugOptions, - pub error_message: ErrorMessage, - pub ppudebug: PPUDebug, - pub emulation: Emulation, -} - -impl State { - pub fn new() -> Self { - Self { - debug_options: DebugOptions::new(), - error_message: ErrorMessage::new(), - ppudebug: PPUDebug::new(), - emulation: Emulation::new(), - } - } -}