mirror of
https://github.com/FranLMSP/snes.git
synced 2026-01-01 07:21:35 -05:00
Migrate to egui (#3)
This commit is contained in:
@@ -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<dyn ROM>,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
138
snes-frontend/src/emu_state/debug_options.rs
Normal file
138
snes-frontend/src/emu_state/debug_options.rs
Normal file
@@ -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<u8>,
|
||||
pub char_framebuffer: Vec<u8>,
|
||||
pub bg_texture: Option<TextureHandle>,
|
||||
pub char_texture: Option<TextureHandle>,
|
||||
}
|
||||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
11
snes-frontend/src/emu_state/emulation.rs
Normal file
11
snes-frontend/src/emu_state/emulation.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
pub struct EmulationState {
|
||||
pub is_paused: bool,
|
||||
}
|
||||
|
||||
impl EmulationState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_paused: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
4
snes-frontend/src/emu_state/mod.rs
Normal file
4
snes-frontend/src/emu_state/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod debug_options;
|
||||
pub mod state;
|
||||
pub mod emulation;
|
||||
pub use state::AppState;
|
||||
24
snes-frontend/src/emu_state/state.rs
Normal file
24
snes-frontend/src/emu_state/state.rs
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
13
snes-frontend/src/emu_ui/debug/common.rs
Normal file
13
snes-frontend/src/emu_ui/debug/common.rs
Normal file
@@ -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()),
|
||||
}
|
||||
}
|
||||
92
snes-frontend/src/emu_ui/debug/cpu.rs
Normal file
92
snes-frontend/src/emu_ui/debug/cpu.rs
Normal file
@@ -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)
|
||||
);
|
||||
});
|
||||
}
|
||||
55
snes-frontend/src/emu_ui/debug/debug.rs
Normal file
55
snes-frontend/src/emu_ui/debug/debug.rs
Normal file
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
86
snes-frontend/src/emu_ui/debug/memory_map.rs
Normal file
86
snes-frontend/src/emu_ui/debug/memory_map.rs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
8
snes-frontend/src/emu_ui/debug/mod.rs
Normal file
8
snes-frontend/src/emu_ui/debug/mod.rs
Normal file
@@ -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;
|
||||
195
snes-frontend/src/emu_ui/debug/ppu.rs
Normal file
195
snes-frontend/src/emu_ui/debug/ppu.rs
Normal file
@@ -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::<Vec<u16>>();
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
165
snes-frontend/src/emu_ui/debug/ppu_graphics.rs
Normal file
165
snes-frontend/src/emu_ui/debug/ppu_graphics.rs
Normal file
@@ -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<TextureHandle>, 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(),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
18
snes-frontend/src/emu_ui/game.rs
Normal file
18
snes-frontend/src/emu_ui/game.rs
Normal file
@@ -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,
|
||||
)
|
||||
}
|
||||
26
snes-frontend/src/emu_ui/menu.rs
Normal file
26
snes-frontend/src/emu_ui/menu.rs
Normal file
@@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
3
snes-frontend/src/emu_ui/mod.rs
Normal file
3
snes-frontend/src/emu_ui/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod game;
|
||||
pub mod menu;
|
||||
pub mod debug;
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod ppu;
|
||||
pub mod cpu;
|
||||
pub mod state;
|
||||
@@ -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))),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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::<Vec<u16>>();
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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<imgui::TextureId>,
|
||||
pub char_texture_id: Option<imgui::TextureId>,
|
||||
pub framebuffer: Vec<u8>,
|
||||
pub char_framebuffer: Vec<u8>,
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user