From adfba876d2c49457fbd488b65700ebd7d180d54d Mon Sep 17 00:00:00 2001 From: zxq5 Date: Sun, 19 Jul 2026 01:18:00 +0100 Subject: [PATCH] megacommit: - setup new UI layout using tiling and an action/message based system for updating global state based on interactions with individual tiles and vice versa - added custom dialogs and UI helpers to create a more cohesive UI - added a disassembler widget (WIP) - added a documentation widget (WIP) - refactored backend halt/pause logic to fix bugs. - started emulator implementation documentation (to complement the ISA spec) --- Cargo.toml | 2 +- doc/DSA State Machine Semantics.md | 80 ++++ dsa/disassembler/Cargo.toml | 2 +- dsa/emulator/emulator_core/src/memory/ram.rs | 9 +- .../emulator_core/src/processor/processor.rs | 156 ++++--- .../emulator_core/src/processor/state.rs | 35 +- dsa/emulator/emulator_ui/Cargo.toml | 3 + dsa/emulator/emulator_ui/src/lib.rs | 1 - .../src/ui/components/controller.rs | 144 ++++--- .../src/ui/components/disassembler.rs | 219 ++++++++++ .../emulator_ui/src/ui/components/display.rs | 22 +- .../src/ui/components/editor/mod.rs | 398 +++++++++--------- .../src/ui/components/framebuffer.rs | 21 +- .../emulator_ui/src/ui/components/memory.rs | 152 ++++--- .../emulator_ui/src/ui/components/mod.rs | 1 + .../src/ui/components/registers.rs | 23 +- .../emulator_ui/src/ui/components/serial.rs | 21 + .../src/ui/debugger/dialog/confirm_dialog.rs | 79 ++++ .../src/ui/debugger/dialog/error_dialog.rs | 52 +++ .../src/ui/debugger/dialog/file_dialog.rs | 42 ++ .../emulator_ui/src/ui/debugger/dialog/mod.rs | 3 + .../emulator_ui/src/ui/debugger/mod.rs | 262 ++++++++++++ .../src/ui/debugger/panels/about.rs | 37 ++ .../src/ui/debugger/panels/documentation.rs | 60 +++ .../src/ui/debugger/panels/editor.rs | 89 ++++ .../emulator_ui/src/ui/debugger/panels/mod.rs | 174 ++++++++ .../src/ui/debugger/ui_helpers/mod.rs | 203 +++++++++ dsa/emulator/emulator_ui/src/ui/mod.rs | 20 +- dsa_resources/animation.dsa | 2 +- dsa_resources/bench.dsa | 2 +- dsa_resources/out.dsb | Bin 2500 -> 3709 bytes dsa_resources/print.dsa | 3 +- 32 files changed, 1890 insertions(+), 427 deletions(-) create mode 100644 doc/DSA State Machine Semantics.md create mode 100644 dsa/emulator/emulator_ui/src/ui/components/disassembler.rs create mode 100644 dsa/emulator/emulator_ui/src/ui/debugger/dialog/confirm_dialog.rs create mode 100644 dsa/emulator/emulator_ui/src/ui/debugger/dialog/error_dialog.rs create mode 100644 dsa/emulator/emulator_ui/src/ui/debugger/dialog/file_dialog.rs create mode 100644 dsa/emulator/emulator_ui/src/ui/debugger/dialog/mod.rs create mode 100644 dsa/emulator/emulator_ui/src/ui/debugger/mod.rs create mode 100644 dsa/emulator/emulator_ui/src/ui/debugger/panels/about.rs create mode 100644 dsa/emulator/emulator_ui/src/ui/debugger/panels/documentation.rs create mode 100644 dsa/emulator/emulator_ui/src/ui/debugger/panels/editor.rs create mode 100644 dsa/emulator/emulator_ui/src/ui/debugger/panels/mod.rs create mode 100644 dsa/emulator/emulator_ui/src/ui/debugger/ui_helpers/mod.rs diff --git a/Cargo.toml b/Cargo.toml index a5bd01b..9aa0ce5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,4 +5,4 @@ resolver = "3" [workspace.package] edition = "2024" version = "0.3.0" -authors = ["zxq5"] +authors = ["zxq5", "zxq4"] diff --git a/doc/DSA State Machine Semantics.md b/doc/DSA State Machine Semantics.md new file mode 100644 index 0000000..e159688 --- /dev/null +++ b/doc/DSA State Machine Semantics.md @@ -0,0 +1,80 @@ +# States + +### Definitions +- Model + - Private - internal state of the processor, always up to date. + - time: Duration, + - state: **Running | Paused | Halted | Shutdown | Breakpoint** + - Shared - shared across threads. updated periodically + - time: Duration + - cycles: Integer + + +## Running +the running state is the main loop of the emulator. + +### Responsibilities: +- run fetch-decode-execute cycle +- handle interrupts +- go to paused state if requested by UI. +- go to the halted state if it hits that instruction +- keep the UI updated occasionally, but prioritise extremely high throughput + +### Transitions: +- to `Paused` + - when the `pause` signal is received +- to `Halted` + - when hitting the `Hlt` instruction. +- to `Shutdown` + - when the `shutdown` signal is received + +***DEBUG IMPLEMENTATION ONLY*** +- to `Breakpoint` + - when hitting a breakpoint + +## Paused +the paused state is started and ended by the UI thread. + +### Responsibilities: +- update the UI when requested +- reset when requested by the UI +- save the running-time and executed instructions since the last pause state. + - add `private.time` to `shared.time` + - reset `private.time, shared.time, shared.cycles` on state exit + +### Transitions +- to `Halted` + - when the `run` signal is received and the previous state was `Halted` +- to `Running` + - when the `run` signal is received +- to `Shutdown` + - when the `shutdown` signal is received + +## Halted +the state reached when the emulator has hit a `Hlt` instruction and is waiting to continue + +### Responsibilities: +- update the UI when requested +- unpause when an interrupt is received +- keep track of the time spent in the running state across multiple hlt/interrupt cycles + - add `private.time` to `shared.time` + - reset `private.time` on state exit + - preserve `shared` on state exit + +### Transitions +- to `Running` + - when the `interrupt` signal is received +- to `Paused` + - when the `pause` signal is received +- to `Shutdown` + - when the `shutdown` signal is received + +## Shutdown +the shutdown state is a terminal state. the emulator cleans up its memory and exits. + +## Breakpoint +debug state used only in the debugger implementation of the emulator + +### Transitions +- to `Running` + - when the `run` signal is received \ No newline at end of file diff --git a/dsa/disassembler/Cargo.toml b/dsa/disassembler/Cargo.toml index af7dce3..d24f872 100644 --- a/dsa/disassembler/Cargo.toml +++ b/dsa/disassembler/Cargo.toml @@ -5,7 +5,7 @@ version.workspace = true authors.workspace = true [[bin]] -name = "dsa-a" +name = "dsa-d" path = "src/main.rs" [lib] diff --git a/dsa/emulator/emulator_core/src/memory/ram.rs b/dsa/emulator/emulator_core/src/memory/ram.rs index 96890d8..e74259c 100644 --- a/dsa/emulator/emulator_core/src/memory/ram.rs +++ b/dsa/emulator/emulator_core/src/memory/ram.rs @@ -136,7 +136,7 @@ impl MemoryBank { pub fn read_byte(&self, addr: PhysAddr) -> u8 { unsafe { self.heap.add(addr as usize).read_volatile() } } - + #[inline(always)] pub fn read_page(&self, addr: PhysAddr) -> &Page { debug_assert_eq!(addr % 4096, 0); @@ -148,6 +148,13 @@ impl MemoryBank { unsafe { self.heap.add(addr as usize).write_volatile(value) } } + #[inline] + pub fn write_bytes(&self, addr: PhysAddr, bytes: &[u8]) { + unsafe { + self.heap.add(addr as usize).copy_from(bytes.as_ptr(), bytes.len()); + } + } + #[inline(always)] pub fn write_word(&self, addr: PhysAddr, value: u32) { unsafe { (self.heap.add(addr as usize) as *mut u32).write_volatile(value) }; diff --git a/dsa/emulator/emulator_core/src/processor/processor.rs b/dsa/emulator/emulator_core/src/processor/processor.rs index 5ae070e..4618dd7 100644 --- a/dsa/emulator/emulator_core/src/processor/processor.rs +++ b/dsa/emulator/emulator_core/src/processor/processor.rs @@ -26,6 +26,8 @@ use crate::{ }; use crate::config::{MemoryMap, RegionType}; use crate::memory::ram::Page; +use crate::processor::state::{ProcessorSnapshot, RunningState}; +use crate::processor::state::RunningState::{Halted, Paused, Running, Shutdown}; pub struct Emulator { internal_state: ProcessorSnapshot, @@ -124,7 +126,7 @@ impl Emulator { } #[cold] - fn update(&mut self) { + fn share_state(&mut self) { self.shared_state .proc .store(Arc::new(self.internal_state.clone())); @@ -140,90 +142,87 @@ impl Emulator { MemoryMap::identity_map(&mut self.mmu, map.regions.get(0).unwrap()); } - self.internal_state.halted = false; + self.internal_state.running = Running; Ok(()) } - #[cold] - fn shutdown(&mut self) { - self.internal_state.halted = true; - } - - pub fn halt(&mut self) { - self.internal_state.halted = true; - - // wait until we're un-halted. - while self.internal_state.halted { - self.wait_for_instructions(); - } - } - - pub fn check_update(&mut self) { - // UI requested a state update. - if self.shared_state.update_req.load(Ordering::Relaxed) { - self.update(); - } - - if let x = self.shared_state.goto.load(Ordering::Relaxed) && x != -1 { - *self.mut_reg(Register::Pcx) = x as u32; - self.shared_state.goto.store(-1, Ordering::Relaxed); - } - - if self.shared_state.reseting.load(Ordering::Relaxed) { - self.internal_state.registers = [0; Register::COUNT as usize]; - self.internal_state.clock = 0; - - // sit in a wait loop while resetting. - while self.shared_state.reseting.load(Ordering::Relaxed) { - thread::sleep(Duration::from_millis(100)); - } - - self.internal_state.halted = false; - } - } #[cold] - pub fn wait_for_instructions(&mut self) { - { - // record time for previous execution. - self.internal_state.time = self.time.elapsed(); - self.update(); - self.time = Instant::now(); - } + pub fn run_state_halted(&mut self, prior: RunningState) { + self.internal_state.running = Halted; - // if paused from the UI thread, just wait. - while self.shared_state.paused.load(Ordering::Relaxed) { - thread::sleep(Duration::from_millis(10)); - // UI is making changes / requesting updates. - self.check_update(); - } + // Responsibility: keep track of the time spent in the running state across multiple hlt/interrupt cycles + self.internal_state.time += self.time.elapsed(); loop { - // Check for interrupts. + // Transition: interrupt received if let Ok(int) = self.interrupts.recv_timeout(Duration::from_millis(100)) { - self.internal_state.halted = false; + self.internal_state.running = Running; self.interrupt(int); break; } - // if the cpu didn't halt on it's own then it's ready to run again. - if !self.internal_state.halted { - return; + // Transition: pause signal received + if self.shared_state.paused.load(Ordering::Relaxed) { + self.run_state_paused(Halted); } - // UI is resetting the ui - self.check_update(); + // Responsibility: update UI when requested + if self.shared_state.update_req.load(Ordering::Relaxed) == true { + self.share_state(); + } } + + // Responsibility: reset `private.time` on state exit + self.time = Instant::now(); + self.internal_state.running = prior; } + #[cold] + pub fn run_state_paused(&mut self, prior: RunningState) { + self.internal_state.running = Paused; + + // Responsibility: save the running-time since last pause state. + self.internal_state.time = self.time.elapsed(); + self.share_state(); + + loop { + // Responsibility: update UI when requested + if self.shared_state.update_req.load(Ordering::Relaxed) == true { + self.share_state(); + } + + // Responsibility: reset when requested by the UI + if self.shared_state.reseting.load(Ordering::Relaxed) == true { + self.internal_state.registers = [0; Register::COUNT as usize]; + self.internal_state.clock = 0; + + while self.shared_state.reseting.load(Ordering::Relaxed) == true { + thread::sleep(Duration::from_millis(100)); + } + } + + // Transition: run signal received + if self.shared_state.paused.load(Ordering::Relaxed) == false { + break; + } + } + + // Responsibility: reset private.time, shared.time, shared.cycles on state exit + self.time = Instant::now(); + self.internal_state.time = Duration::new(0, 0); + self.internal_state.clock = 0; + + self.internal_state.running = prior; + } + pub fn run(&mut self) -> Result<(), String> { - // wait for UI to initialise ui. + // wait for UI to initialise. if self.shared_state.paused.load(Ordering::Relaxed) { - self.wait_for_instructions(); + self.run_state_paused(Running) } self.boot()?; - self.time = Instant::now(); 'emu: loop { // so that it always returns zero. this is more performance friendly than checking @@ -234,17 +233,21 @@ impl Emulator { // do not change anything about this loop. it's fully optimised afaik. // Check for commands or hardware Interrupts every 32k cycles if unlikely(self.internal_state.clock & 0x7FFF == 0) { + + // Responsibility: keep the UI thread up to date periodically // Update UI thread (roughly every 512k cycles targeting 60 UPS) if unlikely(self.internal_state.clock & 0x7FFFF == 0) { if self.shared_state.update_req.load(Ordering::Relaxed) { - self.update(); + self.share_state(); } } + // Transition: Paused if unlikely(self.shared_state.paused.load(Ordering::Relaxed)) { - self.wait_for_instructions(); + self.run_state_paused(Running) } + // Responsibility: Handle interrupts match self.interrupts.try_recv() { Ok(int) => self.interrupt(int), Err(TryRecvError::Disconnected) => break 'emu, @@ -252,6 +255,7 @@ impl Emulator { } } + // Responsibility: run fetch-decode-execute cycle. let pc = self.reg(Register::Pcx); let instruction = self.mem_read_word(pc); @@ -280,7 +284,7 @@ impl Emulator { self.internal_state.clock += 1; } - self.shutdown(); + self.internal_state.running = Shutdown; Ok(()) } @@ -512,7 +516,9 @@ impl Emulator { Some(Opcode::Int) => { self.interrupt(Interrupt::Software(word.imm16() as u8)); } - Some(Opcode::Hlt) => self.halt(), + Some(Opcode::Hlt) => { + self.run_state_halted(Running); + }, Some(Opcode::IRet) => { self.pop(Register::Ret); *self.mut_reg(Register::Pcx) = self.reg(Register::Ret); @@ -569,21 +575,3 @@ impl Emulator { } } -#[derive(Debug, Clone)] -pub struct ProcessorSnapshot { - pub halted: bool, - pub clock: usize, - pub time: Duration, - pub registers: [u32; Register::COUNT as usize], -} - -impl Default for ProcessorSnapshot { - fn default() -> Self { - Self { - halted: false, - clock: 0, - time: Duration::ZERO, - registers: [0; Register::COUNT as usize], - } - } -} diff --git a/dsa/emulator/emulator_core/src/processor/state.rs b/dsa/emulator/emulator_core/src/processor/state.rs index 0cb4165..4acce97 100644 --- a/dsa/emulator/emulator_core/src/processor/state.rs +++ b/dsa/emulator/emulator_core/src/processor/state.rs @@ -1,15 +1,13 @@ use std::sync::{Arc, atomic::AtomicBool, mpsc}; use std::sync::atomic::AtomicIsize; +use std::time::Duration; use arc_swap::ArcSwap; - -use crate::processor::processor::ProcessorSnapshot; +use common::prelude::Register; use super::interrupts::Interrupt; - pub struct SharedState { pub proc: ArcSwap, - pub goto: AtomicIsize, pub reseting: AtomicBool, pub paused: AtomicBool, pub update_req: AtomicBool, @@ -22,10 +20,37 @@ impl SharedState { reseting: AtomicBool::new(false), proc: ArcSwap::new(Arc::new(ProcessorSnapshot::default())), - goto: AtomicIsize::new(-1), paused: AtomicBool::new(true), interrupt_queue: sender, update_req: AtomicBool::new(false), } } } + +#[derive(Debug, Clone, PartialEq)] +pub enum RunningState { + Paused, + Halted, + Running, + Shutdown, + Breakpoint(u32), +} + +#[derive(Debug, Clone)] +pub struct ProcessorSnapshot { + pub running: RunningState, + pub clock: usize, + pub time: Duration, + pub registers: [u32; Register::COUNT as usize], +} + +impl Default for ProcessorSnapshot { + fn default() -> Self { + Self { + running: RunningState::Paused, + clock: 0, + time: Duration::ZERO, + registers: [0; Register::COUNT as usize], + } + } +} diff --git a/dsa/emulator/emulator_ui/Cargo.toml b/dsa/emulator/emulator_ui/Cargo.toml index 1c5c75d..2665186 100644 --- a/dsa/emulator/emulator_ui/Cargo.toml +++ b/dsa/emulator/emulator_ui/Cargo.toml @@ -32,6 +32,9 @@ dirs = "6.0.0" assembler = { path = "../../assembler" } egui_extras = "0.35.0" emulator_core = { version = "0.3.0", path = "../emulator_core" } +disassembler = { version = "0.3.0", path = "../../disassembler" } +egui-file-dialog = "0.14.1" +egui_commonmark = { version = "0.24.0", features = ["better_syntax_highlighting", "macros"] } [features] default = ["mainstore-prealloc"] diff --git a/dsa/emulator/emulator_ui/src/lib.rs b/dsa/emulator/emulator_ui/src/lib.rs index b0cb793..fbaf689 100644 --- a/dsa/emulator/emulator_ui/src/lib.rs +++ b/dsa/emulator/emulator_ui/src/lib.rs @@ -4,4 +4,3 @@ pub mod ui; pub mod args; - diff --git a/dsa/emulator/emulator_ui/src/ui/components/controller.rs b/dsa/emulator/emulator_ui/src/ui/components/controller.rs index 0a9ba63..63095ba 100644 --- a/dsa/emulator/emulator_ui/src/ui/components/controller.rs +++ b/dsa/emulator/emulator_ui/src/ui/components/controller.rs @@ -1,11 +1,17 @@ +use crate::ui::debugger::ui_helpers::{ PaddedWidget, StatusIndicator}; use std::sync::{Arc, atomic::Ordering}; - +use std::thread; +use std::time::Duration; +use eframe::emath::Vec2; +use eframe::epaint::Color32; +use egui::Widget; use super::Component; use common::prelude::Register; use emulator_core::{ memory::ram::MemoryBank, processor::state::SharedState }; +use emulator_core::processor::state::RunningState; pub struct Controller { state: Arc, @@ -33,81 +39,89 @@ impl Component for Controller { } fn ui(&mut self, ui: &mut egui::Ui) { - let state = self.state.proc.load(); + self.state.update_req.store(true, Ordering::Relaxed); + + thread::sleep(Duration::from_millis(5)); + + + let state = self.state.proc.load(); let paused = self.state.paused.load(Ordering::Relaxed); - ui.horizontal(|ui| { - // Pause / Run - if ui - .button(match paused { - false => "Pause", - true => "Resume", - }) - .clicked() - { - self.state.paused.store(!paused, Ordering::Relaxed); - } + egui::Frame::new() + .inner_margin(10.0) + .show(ui, |ui| { + ui.horizontal(|ui| { - // Step - // if ui.button("Step").clicked() { - // state - // .cmd_sender - // .send(Command::Step(self.step_amount)) - // .unwrap_or_else(|_| { - // state.error_log.push("Failed to send command".to_string()); - // }); - // } + let padding = Vec2::new(14.0, 8.0); - // Resets the ui and all attached devices - if ui.button("Reset All").clicked() { - self.state.reseting.store(true, Ordering::Relaxed); - self.mem.clear(); - self.state.reseting.store(false, Ordering::Relaxed); - } + if match state.running { + RunningState::Paused | RunningState::Halted | RunningState::Breakpoint(_) + => PaddedWidget::button("▶ Run").color(Color32::GREEN).ui(ui), + RunningState::Running + => PaddedWidget::button("⏸ Pause").color(Color32::YELLOW).ui(ui), + RunningState::Shutdown => { + ui.add_enabled_ui(false, |ui| { + PaddedWidget::button("▶ Run").color(Color32::GREEN).ui(ui) + }).response + }, + }.clicked() + { + self.state.paused.store(!paused, Ordering::Relaxed); + } - ui.separator(); + if PaddedWidget::button("⟳ Reset All").color(Color32::YELLOW).ui(ui).clicked() { + self.state.reseting.store(true, Ordering::Relaxed); + self.mem.clear(); + self.state.reseting.store(false, Ordering::Relaxed); + } - // if ui - // .text_edit_singleline(&mut self.step_amount_input) - // .changed() - // { - // self.step_amount = if let Ok(amount) = self.step_amount_input.parse() { - // amount - // } else { - // state - // .error_log - // .push("Unable to parse step amount".to_string()); - // 1 - // } - // } + // Step + // if ui.button("Step").clicked() { + // state + // .cmd_sender + // .send(Command::Step(self.step_amount)) + // .unwrap_or_else(|_| { + // state.error_log.push("Failed to send command".to_string()); + // }); + // } - // Status info - ui.label(format!( - "Status: {}", - match paused { - true => "Paused", - false => "Running", - } - )); + // if ui + // .text_edit_singleline(&mut self.step_amount_input) + // .changed() + // { + // self.step_amount = if let Ok(amount) = self.step_amount_input.parse() { + // amount + // } else { + // state + // .error_log + // .push("Unable to parse step amount".to_string()); + // 1 + // } + // } - ui.label(format!("Halted: {}", state.halted)); - ui.separator(); + state.running.clone().ui(ui, padding); + + ui.separator(); + + let pcx = state.registers[Register::Pcx as usize]; + let clock = state.clock; + let time = state.time.as_micros(); + + let mips = if time != 0 { + state.clock as u128 / time + } else { + 0 + }; + + PaddedWidget::label(format!("Clock: {clock}")).ui(ui); + PaddedWidget::label( format!("Pcx: 0x{pcx:08X}")).ui(ui); + PaddedWidget::label( format!("Elapsed: {} micros", time)).ui(ui); + PaddedWidget::label(format!("Freq: {}MHz", mips)).ui(ui); + }); + }); - let pcx = state.registers[Register::Pcx as usize]; - let clock = state.clock; - let time = state.time.as_micros(); - let mips = if time != 0 { - state.clock as u128 / time - } else { - 0 - }; - ui.label(format!("Clock: {clock}")); - ui.label(format!("Pcx: 0x{pcx:08X}")); - ui.label(format!("Elapsed: {} micros", time)); - ui.label(format!("Freq: {}MHz", mips)); - }); } } diff --git a/dsa/emulator/emulator_ui/src/ui/components/disassembler.rs b/dsa/emulator/emulator_ui/src/ui/components/disassembler.rs new file mode 100644 index 0000000..91e7ef7 --- /dev/null +++ b/dsa/emulator/emulator_ui/src/ui/components/disassembler.rs @@ -0,0 +1,219 @@ +use std::any::Any; +use std::fs::File; +use std::io::BufReader; +use std::path::Path; +use egui::{ScrollArea, Ui, WidgetText}; +use egui_extras::{Size, StripBuilder}; +use egui_file::FileDialog; +use egui_tiles::TileId; +use common::formats::binary_dse::DseExecutable; +use disassembler::{disassemble, Disassembly}; +use crate::ui::components::Component; +use crate::ui::debugger::Action; +use crate::ui::debugger::panels::TilePane; + +pub struct Disassembler { + visible: bool, + dialog: Option, + error: Option, + + program: Option, + disassembled: Option, +} + +impl TilePane for Disassembler { + fn title(&self) -> WidgetText { + "Disassembler".into() + } + + fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option { + Component::ui(self, ui); + None + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + +impl Disassembler { + pub(crate) fn new() -> Self { + Self { + visible: true, + dialog: None, + error: None, + + program: None, + disassembled: None, + } + } + + pub fn new_open(path: &Path) -> Self { + let reader = File::open(path).unwrap(); + let bin = DseExecutable::read(&mut BufReader::new(reader)).unwrap(); + + Self { + visible: true, + dialog: None, + error: None, + program: Some(bin), + disassembled: None, + } + } + + fn open(&mut self) { + let Ok(cwd) = std::env::current_dir() else { + self.error = Some("Couldn't get CWD to open file!".to_string()); + return; + }; + + if self.dialog.is_none() { + let mut dialog = FileDialog::open_file(); + dialog.set_path(cwd); + dialog.open(); + self.dialog = Some(dialog); + } + } + + fn handle_file_dialog(&mut self, ui: &mut Ui) { + + let Some(dialog) = &mut self.dialog else { + return; + }; + + if !dialog.show(ui).selected() { return }; + + let Some(file) = dialog.path() else { + return + }; + + if !file.extension().is_some_and(|ext| ext == "dsb") { + return + } + + let reader = match File::open(file) { + Ok(f) => f, + Err(e) => { + self.error = Some(e.to_string()); + return; + } + }; + + match DseExecutable::read(&mut BufReader::new(reader)) { + Ok(bin) => self.program = Some(bin), + Err(e) => { + self.error = Some(e.to_string()); + return; + } + }; + + self.dialog = None; + } +} + +impl Component for Disassembler { + fn title(&self) -> &'static str { + "Disassembler" + } + + fn visible(&mut self) -> &mut bool { + &mut self.visible + } + + fn ui(&mut self, ui: &mut egui::Ui) { + self.handle_file_dialog(ui); + + ui.horizontal(|ui| { + ui.spacing_mut().button_padding = egui::vec2(8.0, 4.0); + ui.spacing_mut().item_spacing.x = 6.0; + + // Opens a file + // ui.add_enabled(false, egui::Button::new("Open")); + if ui.button("Open").clicked() { + self.open(); + } + }); + + + if let Some(binary) = &self.program { + if let Some(output) = &self.disassembled { + StripBuilder::new(ui) + .size(Size::exact(350.0)) + .size(Size::remainder().at_most(500.0)) + .horizontal(|mut strip| { + + strip.cell(|ui| { + ScrollArea::vertical() + .id_salt("symbols") + .show(ui, |ui| { + egui::Grid::new("symbols_grid") + .striped(true) + .spacing([12.0, 2.0]) // horizontal, vertical spacing + .show(ui, |ui| { + // Header + ui.strong("Addr"); + ui.strong("Name"); + ui.strong("Section"); + ui.strong("Bind"); + ui.end_row(); + + // Rows + for symbol in &output.symbols { + ui.monospace(format!("{:#06x}", symbol.address)); + ui.monospace(&symbol.name); + ui.label(format!("{:?}", symbol.section)); + ui.label(format!("{:?}", symbol.binding)); + + ui.end_row(); + } + }); + }); + }); + + strip.cell(|ui| { + ScrollArea::vertical() + .id_salt("asm") + .show(ui, |ui| { + egui::Grid::new("asm_grid") + .striped(true) + .spacing([12.0, 2.0]) + .show(ui, |ui| { + for (addr, line) in output.text_lines.iter().enumerate() { + ui.label( + egui::RichText::new(format!("{:04X}", addr)) + .monospace() + .weak(), + ); + + ui.monospace(line); + + ui.end_row(); + } + }); + }); + }); + }); + + + // ui.horizontal(|ui| { + // + // egui::ScrollArea::vertical() + // .id_salt("symbols_scroll") + // .show(ui, |ui| { + // + // }); + // + // ui.separator(); + // + // egui::ScrollArea::vertical() + // .id_salt("asm_scroll") + // .show(ui, |ui| { + // + // }); + // }); + } else { + self.disassembled = Some(disassemble(&binary)) + } + } + } +} \ No newline at end of file diff --git a/dsa/emulator/emulator_ui/src/ui/components/display.rs b/dsa/emulator/emulator_ui/src/ui/components/display.rs index e9360eb..017b084 100644 --- a/dsa/emulator/emulator_ui/src/ui/components/display.rs +++ b/dsa/emulator/emulator_ui/src/ui/components/display.rs @@ -1,8 +1,12 @@ -use egui::{Color32, FontId, Vec2}; +use std::any::Any; +use egui::{Color32, FontId, Ui, Vec2, WidgetText}; +use egui_tiles::TileId; use emulator_core::{ memory::PhysAddr, memory::ram::{MemoryBank, Page} }; +use crate::ui::debugger::Action; +use crate::ui::debugger::panels::TilePane; use super::Component; pub struct Display { @@ -65,6 +69,22 @@ impl Display { } } + +impl TilePane for Display { + fn title(&self) -> WidgetText { + "Display".into() + } + + fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option { + Component::ui(self, ui); + None + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + impl Component for Display { fn title(&self) -> &str { "Display" diff --git a/dsa/emulator/emulator_ui/src/ui/components/editor/mod.rs b/dsa/emulator/emulator_ui/src/ui/components/editor/mod.rs index e5da6cd..466d161 100644 --- a/dsa/emulator/emulator_ui/src/ui/components/editor/mod.rs +++ b/dsa/emulator/emulator_ui/src/ui/components/editor/mod.rs @@ -22,6 +22,7 @@ use emulator_core::{ memory::ram::{MemoryBank, Page}, processor::state::SharedState }; +use crate::ui::components::memory::MemoryViewer; pub struct Editor { state: Arc, @@ -39,7 +40,8 @@ pub struct Editor { syntax: Syntax, // output / loading - output: Output, + output: Vec, + viewer: MemoryViewer, load_offset: u32, offset_str: String, @@ -87,7 +89,7 @@ impl Component for Editor { |ui| { if self.show_output { egui::Panel::right("Editor Output").resizable(false).show_inside(ui, |ui| { - self.output.ui(ui); + self.viewer.ui(ui, self.output.as_slice(), 0); }); } self.render_editor(ui); @@ -116,7 +118,8 @@ impl Editor { .with_rows(0) .with_theme(THEME) .with_numlines(true), - output: Output::new(), + output: Vec::new(), + viewer: MemoryViewer::new(), unsaved: true, visible: false, load_offset: 0, @@ -346,7 +349,7 @@ impl Editor { assembler.start(()); // Or block until done - *self.output.mut_data() = match assembler.output() { + self.output = match assembler.output() { Ok(instructions) => instructions, Err(e) => { self.error = Some(e.to_string()); @@ -380,7 +383,7 @@ impl Editor { // } Some("dsb") => { if let Ok(bytes) = fs::read(path) { - *self.output.mut_data() = bytes; + self.output = bytes; } else { self.error = Some("Failed to read file".to_string()); } @@ -436,7 +439,7 @@ impl Editor { self.error = Some("Can't load program at invalid offset!".to_string()); } - let mut reader = Cursor::new(self.output.data().clone()); + let mut reader = Cursor::new(self.output.clone()); let Ok(executable) = DseExecutable::read(&mut reader) else { self.error = Some("Failed to read program!".to_string()); return @@ -450,7 +453,6 @@ impl Editor { for (i, page) in image.iter().enumerate() { self.memory.write_page(i as u32 * 4096, &page); } - self.state.goto.store(executable.entry_point(0) as isize, Ordering::Relaxed); self.state.reseting.store(false, Ordering::Relaxed); } @@ -502,194 +504,194 @@ pub const THEME: ColorTheme = ColorTheme { types: "B8B9D4", // yellow1 special: "de5252", // blue1 }; - -struct Output { - pub data: Vec, - - show_bytes: bool, - show_words: bool, - show_ascii: bool, - show_instruction: bool, - show_decimal: bool, -} - -impl Output { - fn new() -> Self { - Self { - data: vec![], - show_bytes: true, - show_words: true, - show_ascii: true, - show_instruction: true, - show_decimal: true, - } - } - - fn mut_data(&mut self) -> &mut Vec { - &mut self.data - } - - fn data(&self) -> &Vec { - &self.data - } - - fn ui(&mut self, ui: &mut Ui) { - // Output area with synchronized scrolling - egui::ScrollArea::vertical() - .id_salt("output_scroll") - .max_width(400.0) - .show(ui, |ui| { - if self.data.is_empty() { - ui.label( - egui::RichText::new("No output data") - .font(egui::FontId::monospace(12.0)) - .color(egui::Color32::GRAY), - ); - return; - } - - ui.horizontal(|ui| { - ui.toggle_value(&mut self.show_bytes, "Bytes"); - ui.toggle_value(&mut self.show_words, "Words"); - ui.toggle_value(&mut self.show_decimal, "Decimal"); - ui.toggle_value(&mut self.show_ascii, "Ascii"); - ui.toggle_value(&mut self.show_instruction, "Instructions"); - }); - - ui.separator(); - - let mut table = TableBuilder::new(ui) - .resizable(false) // makes all columns resizable at once - .column(Column::initial(55.0)); // addr - - if self.show_bytes { - table = table.column(Column::initial(90.0)); - } - if self.show_words { - table = table.column(Column::initial(75.0)); - } - if self.show_decimal { - table = table.column(Column::initial(80.0)); - } - if self.show_ascii { - table = table.column(Column::initial(50.0)); - } - if self.show_instruction { - table = table.column(Column::initial(200.0).clip(true)); - } - - table - .header(20.0, |mut header| { - header.col(|ui| { - ui.label("Addr"); - }); - if self.show_bytes { - header.col(|ui| { - ui.label("Bytes"); - }); - } - if self.show_words { - header.col(|ui| { - ui.label("Word"); - }); - } - if self.show_decimal { - header.col(|ui| { - ui.label("Decimal"); - }); - } - if self.show_ascii { - header.col(|ui| { - ui.label("Ascii"); - }); - } - if self.show_instruction { - header.col(|ui| { - ui.label("Instruction"); - }); - } - }) - .body(|mut body| { - // Process bytes in chunks of 4 - for (line_num, chunk) in self.data.chunks(4).enumerate() { - body.row(18.0, |mut row| { - let address = line_num * 4; - let mut buf = [0u8; 4]; - buf[..chunk.len()].copy_from_slice(chunk); - let value = u32::from_le_bytes(buf); - - // Address column - row.col(|ui| { - ui.label( - egui::RichText::new(format!("0x{address:04X}")) - .font(egui::FontId::monospace(12.0)), - ); - }); - - if self.show_bytes { - row.col(|ui| { - // Individual bytes column - ui.label( - egui::RichText::new( - chunk - .iter() - .map(|b| format!("{b:02X}")) - .collect::>() - .join(" "), - ) - .font(egui::FontId::monospace(12.0)) - .color(egui::Color32::from_rgb(200, 200, 255)), - ); - }); - } - - if self.show_words { - row.col(|ui| { - // Hex column - ui.label( - egui::RichText::new(format!("0x{value:08X}")) - .font(egui::FontId::monospace(12.0)) - .color(egui::Color32::from_rgb(255, 100, 100)), - ); - }); - } - - if self.show_decimal { - row.col(|ui| { - ui.label( - egui::RichText::new(format!("{value}")) - .font(egui::FontId::monospace(12.0)) - .color(egui::Color32::from_rgb(100, 255, 100)), - ); - }); - } - - if self.show_ascii { - let ascii = value.to_le_bytes(); - row.col(|ui| { - ui.label( - egui::RichText::new(String::from_utf8_lossy(&ascii)) - .font(egui::FontId::monospace(12.0)) - .color(egui::Color32::from_rgb(100, 255, 255)), - ); - }); - } - - if self.show_instruction { - row.col(|ui| { - ui.label( - egui::RichText::new(format!( - "{:?}", - Instruction(value) - )) - .font(egui::FontId::monospace(12.0)) - .color(egui::Color32::from_rgb(255, 100, 255)), - ); - }); - } - }) - } - }); - }); - } -} +// +// struct Output { +// pub data: Vec, +// +// show_bytes: bool, +// show_words: bool, +// show_ascii: bool, +// show_instruction: bool, +// show_decimal: bool, +// } +// +// impl Output { +// fn new() -> Self { +// Self { +// data: vec![], +// show_bytes: true, +// show_words: true, +// show_ascii: true, +// show_instruction: true, +// show_decimal: true, +// } +// } +// +// fn mut_data(&mut self) -> &mut Vec { +// &mut self.data +// } +// +// fn data(&self) -> &Vec { +// &self.data +// } +// +// fn ui(&mut self, ui: &mut Ui) { +// // Output area with synchronized scrolling +// egui::ScrollArea::vertical() +// .id_salt("output_scroll") +// .max_width(400.0) +// .show(ui, |ui| { +// if self.data.is_empty() { +// ui.label( +// egui::RichText::new("No output data") +// .font(egui::FontId::monospace(12.0)) +// .color(egui::Color32::GRAY), +// ); +// return; +// } +// +// ui.horizontal(|ui| { +// ui.toggle_value(&mut self.show_bytes, "Bytes"); +// ui.toggle_value(&mut self.show_words, "Words"); +// ui.toggle_value(&mut self.show_decimal, "Decimal"); +// ui.toggle_value(&mut self.show_ascii, "Ascii"); +// ui.toggle_value(&mut self.show_instruction, "Instructions"); +// }); +// +// ui.separator(); +// +// let mut table = TableBuilder::new(ui) +// .resizable(false) // makes all columns resizable at once +// .column(Column::initial(55.0)); // addr +// +// if self.show_bytes { +// table = table.column(Column::initial(90.0)); +// } +// if self.show_words { +// table = table.column(Column::initial(75.0)); +// } +// if self.show_decimal { +// table = table.column(Column::initial(80.0)); +// } +// if self.show_ascii { +// table = table.column(Column::initial(50.0)); +// } +// if self.show_instruction { +// table = table.column(Column::initial(200.0).clip(true)); +// } +// +// table +// .header(20.0, |mut header| { +// header.col(|ui| { +// ui.label("Addr"); +// }); +// if self.show_bytes { +// header.col(|ui| { +// ui.label("Bytes"); +// }); +// } +// if self.show_words { +// header.col(|ui| { +// ui.label("Word"); +// }); +// } +// if self.show_decimal { +// header.col(|ui| { +// ui.label("Decimal"); +// }); +// } +// if self.show_ascii { +// header.col(|ui| { +// ui.label("Ascii"); +// }); +// } +// if self.show_instruction { +// header.col(|ui| { +// ui.label("Instruction"); +// }); +// } +// }) +// .body(|mut body| { +// // Process bytes in chunks of 4 +// for (line_num, chunk) in self.data.chunks(4).enumerate() { +// body.row(18.0, |mut row| { +// let address = line_num * 4; +// let mut buf = [0u8; 4]; +// buf[..chunk.len()].copy_from_slice(chunk); +// let value = u32::from_le_bytes(buf); +// +// // Address column +// row.col(|ui| { +// ui.label( +// egui::RichText::new(format!("0x{address:04X}")) +// .font(egui::FontId::monospace(12.0)), +// ); +// }); +// +// if self.show_bytes { +// row.col(|ui| { +// // Individual bytes column +// ui.label( +// egui::RichText::new( +// chunk +// .iter() +// .map(|b| format!("{b:02X}")) +// .collect::>() +// .join(" "), +// ) +// .font(egui::FontId::monospace(12.0)) +// .color(egui::Color32::from_rgb(200, 200, 255)), +// ); +// }); +// } +// +// if self.show_words { +// row.col(|ui| { +// // Hex column +// ui.label( +// egui::RichText::new(format!("0x{value:08X}")) +// .font(egui::FontId::monospace(12.0)) +// .color(egui::Color32::from_rgb(255, 100, 100)), +// ); +// }); +// } +// +// if self.show_decimal { +// row.col(|ui| { +// ui.label( +// egui::RichText::new(format!("{value}")) +// .font(egui::FontId::monospace(12.0)) +// .color(egui::Color32::from_rgb(100, 255, 100)), +// ); +// }); +// } +// +// if self.show_ascii { +// let ascii = value.to_le_bytes(); +// row.col(|ui| { +// ui.label( +// egui::RichText::new(String::from_utf8_lossy(&ascii)) +// .font(egui::FontId::monospace(12.0)) +// .color(egui::Color32::from_rgb(100, 255, 255)), +// ); +// }); +// } +// +// if self.show_instruction { +// row.col(|ui| { +// ui.label( +// egui::RichText::new(format!( +// "{:?}", +// Instruction(value) +// )) +// .font(egui::FontId::monospace(12.0)) +// .color(egui::Color32::from_rgb(255, 100, 255)), +// ); +// }); +// } +// }) +// } +// }); +// }); +// } +// } diff --git a/dsa/emulator/emulator_ui/src/ui/components/framebuffer.rs b/dsa/emulator/emulator_ui/src/ui/components/framebuffer.rs index 2536a02..7fc784b 100644 --- a/dsa/emulator/emulator_ui/src/ui/components/framebuffer.rs +++ b/dsa/emulator/emulator_ui/src/ui/components/framebuffer.rs @@ -1,8 +1,12 @@ -use egui::Vec2; +use std::any::Any; +use egui::{Ui, Vec2, WidgetText}; +use egui_tiles::TileId; use emulator_core::{ memory::PhysAddr, memory::ram::{MemoryBank, Page} }; +use crate::ui::debugger::Action; +use crate::ui::debugger::panels::TilePane; use super::Component; pub struct FrameBuffer { @@ -79,6 +83,21 @@ impl FrameBuffer { } } +impl TilePane for FrameBuffer { + fn title(&self) -> WidgetText { + "Framebuffer".into() + } + + fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option { + Component::ui(self, ui); + None + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + impl Component for FrameBuffer { fn title(&self) -> &str { "Framebuffer" diff --git a/dsa/emulator/emulator_ui/src/ui/components/memory.rs b/dsa/emulator/emulator_ui/src/ui/components/memory.rs index 612cc29..05ace73 100644 --- a/dsa/emulator/emulator_ui/src/ui/components/memory.rs +++ b/dsa/emulator/emulator_ui/src/ui/components/memory.rs @@ -1,7 +1,12 @@ +use std::any::Any; use std::num::ParseIntError; +use egui::{Ui, WidgetText}; use egui_extras::{Column, TableBuilder}; +use egui_tiles::{Tile, TileId}; use common::prelude::Instruction; use emulator_core::memory::ram::MemoryBank; +use crate::ui::debugger::Action; +use crate::ui::debugger::panels::TilePane; use super::Component; pub struct MemoryInspector { @@ -13,11 +18,7 @@ pub struct MemoryInspector { addr_input: String, // settings - show_bytes: bool, - show_words: bool, - show_ascii: bool, - show_instruction: bool, - show_decimal: bool, + memory_viewer: MemoryViewer } impl MemoryInspector { @@ -30,15 +31,27 @@ impl MemoryInspector { visible: false, addr_input: String::from("0x00"), - show_bytes: true, - show_words: true, - show_ascii: true, - show_instruction: true, - show_decimal: true, + memory_viewer: MemoryViewer::new() } } } + +impl TilePane for MemoryInspector { + fn title(&self) -> WidgetText { + "Memory Inspector".into() + } + + fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option { + Component::ui(self, ui); + None + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + impl Component for MemoryInspector { fn title(&self) -> &'static str { "Memory Inspector" @@ -49,56 +62,74 @@ impl Component for MemoryInspector { } fn ui(&mut self, ui: &mut egui::Ui) { - // Right column - Memory - // ui.vertical(|ui| { - // ui.add_space(10.0); - // }); - // Memory table + ui.horizontal(|ui| { + ui.label("Address:"); + let address_response = ui.add( + egui::TextEdit::singleline(&mut self.addr_input) + .hint_text("0x1000 or 4096") + .desired_width(150.0), + ); + + ui.add_space(10.0); + + // Search button + let search_clicked = ui.button("🔍 Search").clicked(); + + // Handle Enter key in text field + let enter_pressed = + address_response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)); + + if search_clicked || enter_pressed { + if let Ok(new) = parse_address(&self.addr_input) { + if new % 4 == 0 { + self.view_addr = new; + } else { + println!("Address must be 4-byte aligned"); + } + } else { + println!("Invalid address"); + // state.error_log.push("Invalid address".to_string()); + } + } + + ui.label("(hex or decimal)"); + }); + + let mut window = vec![0u8; self.view_size as usize]; + for i in 0..self.view_size { + window[i as usize] = self.memory.read_byte(self.view_addr + i as u32); + } + + self.memory_viewer.ui(ui, &window, self.view_addr as usize) + } +} + + +pub struct MemoryViewer { + show_bytes: bool, + show_words: bool, + show_ascii: bool, + show_instruction: bool, + show_decimal: bool, +} + +impl MemoryViewer { + pub fn new() -> Self { + Self { + show_bytes: true, + show_words: true, + show_ascii: true, + show_instruction: true, + show_decimal: true, + } + } + + pub fn ui(&mut self, ui: &mut egui::Ui, memory: &[u8], offset: usize) { egui::ScrollArea::vertical() .auto_shrink([false; 2]) .id_salt("memory_inspector_scroll") .show(ui, |ui| { - - // ui.heading("Memory Inspector"); - // ui.add_space(10.0); - // - // // Address input section - ui.horizontal(|ui| { - ui.label("Address:"); - - let address_response = ui.add( - egui::TextEdit::singleline(&mut self.addr_input) - .hint_text("0x1000 or 4096") - .desired_width(150.0), - ); - - ui.add_space(10.0); - - // Search button - let search_clicked = ui.button("🔍 Search").clicked(); - - // Handle Enter key in text field - let enter_pressed = - address_response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)); - - if search_clicked || enter_pressed { - if let Ok(new) = parse_address(&self.addr_input) { - if new % 4 == 0 { - self.view_addr = new; - } else { - println!("Address must be 4-byte aligned"); - } - } else { - println!("Invalid address"); - // state.error_log.push("Invalid address".to_string()); - } - } - - ui.label("(hex or decimal)"); - }); - - ui.horizontal(|ui| { ui.toggle_value(&mut self.show_bytes, "Bytes"); ui.toggle_value(&mut self.show_words, "Words"); @@ -162,12 +193,11 @@ impl Component for MemoryInspector { }) .body(|mut body| { // Process bytes in chunks of 4 - for (line_num, value) in (0..self.view_size / 4) - .map(|i| (i, self.memory.read_word(self.view_addr + i * 4))) { + for (line_num, chunk) in memory.as_chunks::<4>().0.iter().enumerate() { body.row(18.0, |mut row| { - let address = line_num * 4 * self.view_addr; + let address = line_num * 4 + offset; - let chunk = value.to_le_bytes(); + let value = u32::from_le_bytes(*chunk); // Address column row.col(|ui| { @@ -216,10 +246,9 @@ impl Component for MemoryInspector { } if self.show_ascii { - let ascii = value.to_le_bytes(); row.col(|ui| { ui.label( - egui::RichText::new(String::from_utf8_lossy(&ascii)) + egui::RichText::new(String::from_utf8_lossy(chunk)) .font(egui::FontId::monospace(12.0)) .color(egui::Color32::from_rgb(100, 255, 255)), ); @@ -242,7 +271,6 @@ impl Component for MemoryInspector { } }); }); - } } diff --git a/dsa/emulator/emulator_ui/src/ui/components/mod.rs b/dsa/emulator/emulator_ui/src/ui/components/mod.rs index a5b6c95..f777fca 100644 --- a/dsa/emulator/emulator_ui/src/ui/components/mod.rs +++ b/dsa/emulator/emulator_ui/src/ui/components/mod.rs @@ -5,6 +5,7 @@ pub mod framebuffer; pub mod memory; pub mod registers; pub mod serial; +pub mod disassembler; pub trait Component { /// The tab label shown in the tile header. diff --git a/dsa/emulator/emulator_ui/src/ui/components/registers.rs b/dsa/emulator/emulator_ui/src/ui/components/registers.rs index 067606e..33cfa40 100644 --- a/dsa/emulator/emulator_ui/src/ui/components/registers.rs +++ b/dsa/emulator/emulator_ui/src/ui/components/registers.rs @@ -1,8 +1,12 @@ use std::{f32, sync::Arc}; - +use std::any::Any; +use egui::{Ui, WidgetText}; +use egui_tiles::TileId; use common::prelude::Register; use emulator_core::processor::state::SharedState; - +use crate::ui::components::memory::MemoryInspector; +use crate::ui::debugger::Action; +use crate::ui::debugger::panels::TilePane; use super::Component; pub struct Registers { @@ -10,6 +14,21 @@ pub struct Registers { visible: bool, } +impl TilePane for Registers { + fn title(&self) -> WidgetText { + "Registers".into() + } + + fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option { + Component::ui(self, ui); + None + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + impl Registers { fn section( &mut self, diff --git a/dsa/emulator/emulator_ui/src/ui/components/serial.rs b/dsa/emulator/emulator_ui/src/ui/components/serial.rs index f383b6b..f833c8e 100644 --- a/dsa/emulator/emulator_ui/src/ui/components/serial.rs +++ b/dsa/emulator/emulator_ui/src/ui/components/serial.rs @@ -6,6 +6,9 @@ use std::{ mpsc::{Receiver, Sender}, }, }; +use std::any::Any; +use egui::{Ui, WidgetText}; +use egui_tiles::TileId; use emulator_core::{ memory::{ PhysAddr, @@ -16,6 +19,9 @@ use emulator_core::{ state::SharedState } }; +use crate::ui::components::registers::Registers; +use crate::ui::debugger::Action; +use crate::ui::debugger::panels::TilePane; use super::Component; /// ## SERIAL LAYOUT: (from the perspective of code inside the ui) @@ -32,6 +38,21 @@ pub struct Serial { pub visible: bool, } +impl TilePane for Serial { + fn title(&self) -> WidgetText { + "Serial".into() + } + + fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option { + Component::ui(self, ui); + None + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + impl Serial { fn uart_io_thread( mem: MemoryBank, diff --git a/dsa/emulator/emulator_ui/src/ui/debugger/dialog/confirm_dialog.rs b/dsa/emulator/emulator_ui/src/ui/debugger/dialog/confirm_dialog.rs new file mode 100644 index 0000000..bb3cb05 --- /dev/null +++ b/dsa/emulator/emulator_ui/src/ui/debugger/dialog/confirm_dialog.rs @@ -0,0 +1,79 @@ +use egui::{Modal, Id, Layout, Align, RichText}; + +pub struct ConfirmDialog { + open: bool, + title: String, + message: String, + action: Option, +} + +impl ConfirmDialog { + pub fn new() -> Self { + Self { + open: false, + title: String::new(), + message: String::new(), + action: None, + } + } + + pub fn open(&mut self, title: impl Into, message: impl Into, action: T) { + self.open = true; + self.title = title.into(); + self.message = message.into(); + self.action = Some(action); + } + + pub fn show(&mut self, ctx: &egui::Context) -> Option { + if !self.open { + return None; + } + + let mut result = None; + + let modal = Modal::new(Id::new("confirm_dialog")).show(ctx, |ui| { + ui.set_width(300.0); + + ui.vertical_centered(|ui| { + ui.label(RichText::new(&self.title).strong().size(16.0)); + ui.add_space(6.0); + ui.separator(); + ui.add_space(8.0); + ui.label(&self.message); + }); + + ui.add_space(8.0); + ui.separator(); + + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if ui.button(RichText::new("Confirm").strong()).clicked() { + result = Some(true); + } + ui.add_space(6.0); + if ui.button("Cancel").clicked() { + result = Some(false); + } + }); + }); + + if modal.should_close() { + self.open = false; + self.action = None; + return None; + } + + if let Some(confirmed) = result { + self.open = false; + if confirmed { + return self.action.take(); + } + self.action = None; + } + + None + } +} + +impl Default for ConfirmDialog { + fn default() -> Self { Self::new() } +} \ No newline at end of file diff --git a/dsa/emulator/emulator_ui/src/ui/debugger/dialog/error_dialog.rs b/dsa/emulator/emulator_ui/src/ui/debugger/dialog/error_dialog.rs new file mode 100644 index 0000000..f0e7c0c --- /dev/null +++ b/dsa/emulator/emulator_ui/src/ui/debugger/dialog/error_dialog.rs @@ -0,0 +1,52 @@ +use egui::{Modal, Id, Layout, Align, RichText}; + +pub struct ErrorDialog { + open: bool, + message: String, +} + +impl ErrorDialog { + pub fn new() -> Self { + Self { + open: false, + message: String::new(), + } + } + + pub fn open(&mut self, message: impl Into) { + self.open = true; + self.message = message.into(); + } + + pub fn show(&mut self, ctx: &egui::Context) { + if !self.open { + return + } + + let modal = Modal::new(Id::new("error_dialog")).show(ctx, |ui| { + ui.set_width(300.0); + + ui.vertical_centered(|ui| { + ui.label(RichText::new("Error").strong().size(16.0)); + ui.add_space(6.0); + ui.separator(); + ui.add_space(8.0); + ui.label(&self.message); + }); + + ui.add_space(8.0); + ui.separator(); + + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if ui.button(RichText::new("Ok").strong()).clicked() { + self.open = false; + return; + } + }); + }); + } +} + +impl Default for ErrorDialog { + fn default() -> Self { Self::new() } +} \ No newline at end of file diff --git a/dsa/emulator/emulator_ui/src/ui/debugger/dialog/file_dialog.rs b/dsa/emulator/emulator_ui/src/ui/debugger/dialog/file_dialog.rs new file mode 100644 index 0000000..3fca216 --- /dev/null +++ b/dsa/emulator/emulator_ui/src/ui/debugger/dialog/file_dialog.rs @@ -0,0 +1,42 @@ +use std::path::PathBuf; +use egui::Ui; +use egui_file_dialog::FileDialog; +use crate::ui::debugger::Action; + + +pub struct DsaFileDialog { + inner: FileDialog, // or whatever backend you're using + constructor: Option T>>, +} + +impl DsaFileDialog { + pub fn new() -> Self { + Self { + inner: egui_file_dialog::FileDialog::new() + .add_save_extension("DSA source", "dsa") + .add_save_extension("DSA binary", "dsb") + .add_save_extension("DSC source", "dsc"), + constructor: None, + } + } + + pub fn open_pick(&mut self, constructor: impl FnOnce(PathBuf) -> T + 'static) { + self.constructor = Some(Box::new(constructor)); + self.inner.pick_file(); + } + + pub fn open_save(&mut self, constructor: impl FnOnce(PathBuf) -> T + 'static) { + self.constructor = Some(Box::new(constructor)); + self.inner.save_file(); + } + + /// Call every frame. Returns Some(action) the frame a file is picked. + pub fn show(&mut self, ctx: &egui::Context) -> Option { + self.inner.update(ctx); + + if let Some(path) = self.inner.take_picked() { + return self.constructor.take().map(|f| f(path)); + } + None + } +} \ No newline at end of file diff --git a/dsa/emulator/emulator_ui/src/ui/debugger/dialog/mod.rs b/dsa/emulator/emulator_ui/src/ui/debugger/dialog/mod.rs new file mode 100644 index 0000000..44670fd --- /dev/null +++ b/dsa/emulator/emulator_ui/src/ui/debugger/dialog/mod.rs @@ -0,0 +1,3 @@ +pub mod file_dialog; +pub mod confirm_dialog; +pub mod error_dialog; \ No newline at end of file diff --git a/dsa/emulator/emulator_ui/src/ui/debugger/mod.rs b/dsa/emulator/emulator_ui/src/ui/debugger/mod.rs new file mode 100644 index 0000000..9e59b00 --- /dev/null +++ b/dsa/emulator/emulator_ui/src/ui/debugger/mod.rs @@ -0,0 +1,262 @@ +use std::fs::File; +use std::io::BufReader; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; +use egui_tiles::TileId; +use common::formats::binary_dse::DseExecutable; +use emulator_core::memory::ram::MemoryBank; +use emulator_core::processor::state::SharedState; +use crate::ui::components::controller::Controller; +use crate::ui::components::disassembler::Disassembler; +use crate::ui::components::memory::MemoryInspector; +use crate::ui::DsaUi; +use crate::ui::components::Component; +use dialog::confirm_dialog::ConfirmDialog; +use panels::documentation::Documentation; +use panels::editor::Editor; +use dialog::file_dialog::DsaFileDialog; +use emulator_core::memory::PhysAddr; +use panels::PanelFactory; +use crate::ui::components::display::Display; +use crate::ui::components::framebuffer::FrameBuffer; +use crate::ui::components::registers::Registers; +use crate::ui::components::serial::Serial; +use crate::ui::debugger::dialog::error_dialog::ErrorDialog; +use crate::ui::debugger::panels::{CentralTiles, TilePane}; +use crate::ui::debugger::panels::about::AboutScreen; + +pub mod dialog; +pub mod panels; +pub mod ui_helpers; + +#[derive(Debug, Clone)] +pub enum Action { + Quit, + OpenFile(PathBuf), + LoadBinary(PathBuf), + SaveFile(PathBuf, TileId), + RequestSaveAs(TileId), + ShowError(String), +} + +struct EmulatorHandle { + state: Arc, + memory: MemoryBank, +} + +impl EmulatorHandle { + pub fn new(state: Arc, memory: MemoryBank) -> Self { + Self { state, memory } + } + + pub fn write_bytes(&mut self, offset: PhysAddr, data: &[u8]) { + self.memory.write_bytes(offset, data); + } +} + + +pub struct DebuggerUi { + emulator: EmulatorHandle, + + central_tiles: CentralTiles, + + controller: Controller, + confirm_dialog: ConfirmDialog, + file_dialog: DsaFileDialog, + error_dialog: ErrorDialog, + + tools: Vec +} + +impl DebuggerUi { + + fn open_or_focus( + central_tiles: &mut CentralTiles, + slot: &mut Option, + make_pane: impl FnOnce() -> T, + ) { + match slot { + Some(tile_id) => central_tiles.activate_tab(*tile_id), + None => *slot = Some(central_tiles.add_tab(make_pane())), + } + } + + pub fn new( + cc: &eframe::CreationContext, + state: Arc, + mem_handle: MemoryBank, + ) -> Self { + DsaUi::configure_appearance(&cc.egui_ctx); + Self { + emulator: EmulatorHandle::new(state.clone(), mem_handle.clone()), + central_tiles: CentralTiles::new(), + controller: Controller::new(state.clone(), mem_handle.clone()), + confirm_dialog: ConfirmDialog::new(), + file_dialog: DsaFileDialog::new(), + error_dialog: ErrorDialog::default(), + + tools: vec![ + PanelFactory::register("Memory Inspector", { + let mem_handle = mem_handle.clone(); + move || MemoryInspector::new(mem_handle.clone()) + }), + PanelFactory::register("Documentation", Documentation::default), + PanelFactory::register("Display", { + let mem_handle = mem_handle.clone(); + move || Display::new(80, 25, 0x20000, mem_handle.clone()) + }), + PanelFactory::register("Framebuffer", { + let mem_handle = mem_handle.clone(); + move || FrameBuffer::new(320, 240, 0x30000, mem_handle.clone()) + }), + PanelFactory::register("Serial", { + let mem_handle = mem_handle.clone(); + let state = state.clone(); + move || Serial::new(0x40000, state.clone(), mem_handle.clone()) + }), + PanelFactory::register("Registers", { + let state = state.clone(); + move || Registers::new(state.clone()) + }) + ] + } + } + + pub fn perform_action(&mut self, action: Action) { + match action { + Action::Quit => std::process::exit(0), + + // Opens a file (usually constructed by the file dialogue) + Action::OpenFile(path) => { + match path.extension().map(|s| s.to_str()).flatten() { + Some("dsa") => { self.central_tiles.add_tab(Editor::new_open(&path)); }, + Some("dsb") => { self.central_tiles.add_tab(Disassembler::new_open(&path)); }, + Some("md") => { self.central_tiles.add_tab(Documentation::open(&path)); }, + Some(_) => todo!(), + None => {}, + } + } + + Action::ShowError(err) => { + self.error_dialog.open(err); + } + + Action::LoadBinary(path) => { + if let Some(ext) = path.extension().map(|s| s.to_str()).flatten() && ext == "dsb" { + + let Ok(file) = File::open(path) else { + self.perform_action(Action::ShowError("Failed to open file".into())); + return + }; + + let Ok(binary) = DseExecutable::read(&mut BufReader::new(file)) else { + self.perform_action(Action::ShowError("Failed to read binary".into())); + return; + }; + + self.emulator.write_bytes(0, &binary.to_memory_image()) + } + } + + Action::SaveFile(path, tile) => { + println!("ACTION: saving file {} from tile {tile:?}", path.display()); + + if let Some(editor) = self.central_tiles.pane_mut::(tile) { + if let Err(e) = std::fs::write(path, editor.get_buffer().as_bytes()) { + self.perform_action(Action::ShowError(e.to_string())) + } + } + } + + Action::RequestSaveAs(tile) => { + self.file_dialog.open_save(move |path| Action::SaveFile(path, tile)); + } + }; + } +} + +impl eframe::App for DebuggerUi { + fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { + ui.request_repaint_after(Duration::from_millis(16)); + + if let Some(action) = self.confirm_dialog.show(ui) { + self.perform_action(action); + } + + if let Some(action) = self.file_dialog.show(ui) { + self.perform_action(action); + } + + self.error_dialog.show(ui); + + egui::Panel::top("toolbar").resizable(false).show(ui, |ui| { + egui::MenuBar::new().ui(ui, |ui| { + + let save_enabled = self.central_tiles.active_tile() + .and_then(|id| self.central_tiles.pane_mut::(id)) + .is_some(); + + ui.set_height(30.0); + ui.menu_button("File", |ui| { + + if ui.button("Open").clicked() { + self.file_dialog.open_pick(Action::OpenFile); + } + + if ui.button("Load Binary").clicked() { + self.file_dialog.open_pick(Action::LoadBinary); + } + + if ui.add_enabled(save_enabled, egui::Button::new("Save")).clicked() { + if let Some(active) = self.central_tiles.active_tile() { + if let Some(editor) = self.central_tiles.pane_mut::(active) { + match editor.filename().cloned() { + Some(path) => std::fs::write(path, editor.get_buffer().as_bytes()).unwrap(), + None => self.file_dialog.open_save(move |path| Action::SaveFile(path, active)), + } + } + } + } + + if ui.add_enabled(save_enabled, egui::Button::new("Save As")).clicked() { + if let Some(active) = self.central_tiles.active_tile() { + if let Some(editor) = self.central_tiles.pane_mut::(active) { + self.file_dialog.open_save(move |path| Action::SaveFile(path, active)) + } + } + } + + if ui.button("Exit").clicked() { + self.confirm_dialog.open("Exit","Exit Application\n(you will lose unsaved work)", Action::Quit) + } + }); + + ui.menu_button("Tools", |ui| { + for PanelFactory(name, constructor) in &self.tools { + if ui.button(name).clicked() { + self.central_tiles.add_dyn_tab(constructor()); + } + } + }); + + ui.menu_button("Help", |ui| { + if ui.button("About").clicked() { + self.central_tiles.add_tab(AboutScreen); + } + }); + }); + }); + egui::Panel::top("info_panel").resizable(false).show(ui, |ui| { + self.controller.ui(ui); + }); + egui::Panel::left("left_panel").resizable(false).show(ui, |ui| {}); + + egui::Panel::right("right_panel").resizable(false).show(ui, |ui| {}); + egui::CentralPanel::default().show(ui, |ui| { + if let Some(action) = self.central_tiles.ui(ui) { + self.perform_action(action); + } + }); + } +} diff --git a/dsa/emulator/emulator_ui/src/ui/debugger/panels/about.rs b/dsa/emulator/emulator_ui/src/ui/debugger/panels/about.rs new file mode 100644 index 0000000..400bd23 --- /dev/null +++ b/dsa/emulator/emulator_ui/src/ui/debugger/panels/about.rs @@ -0,0 +1,37 @@ +use std::default::Default; +use std::any::Any; +use std::path::Path; +use eframe::epaint::Direction; +use egui::{Ui, Widget, WidgetText}; +use egui_commonmark::{CommonMarkCache, CommonMarkViewer}; +use egui_tiles::TileId; +use emulator_core::config::{VERSION}; +use crate::ui::debugger::Action; +use crate::ui::debugger::panels::TilePane; + +pub struct AboutScreen; + +impl TilePane for AboutScreen { + fn title(&self) -> WidgetText { + "About DSA".into() + } + + fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option { + + ui.with_layout( + egui::Layout::centered_and_justified(Direction::TopDown) + .with_main_align(egui::Align::Center), + |ui| { + + // title + ui.heading(format!("Damn Simple Architecture v{} 🚀", VERSION)); + }, + ); + + None + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} \ No newline at end of file diff --git a/dsa/emulator/emulator_ui/src/ui/debugger/panels/documentation.rs b/dsa/emulator/emulator_ui/src/ui/debugger/panels/documentation.rs new file mode 100644 index 0000000..13b7bbc --- /dev/null +++ b/dsa/emulator/emulator_ui/src/ui/debugger/panels/documentation.rs @@ -0,0 +1,60 @@ +use std::default::Default; +use std::any::Any; +use std::path::Path; +use egui::{Ui, Widget, WidgetText}; +use egui_commonmark::{CommonMarkCache, CommonMarkViewer}; +use egui_tiles::TileId; +use crate::ui::debugger::Action; +use crate::ui::debugger::panels::TilePane; + +pub struct Documentation { + buffer: Option, + cache: CommonMarkCache, +} + +impl TilePane for Documentation { + fn title(&self) -> WidgetText { + "Documentation".into() + } + + fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option { + egui::ScrollArea::vertical() + .id_salt("documentation") + .max_width(1000.0) + .show(ui, |ui| { + if let Some(buffer) = &self.buffer { + CommonMarkViewer::new().show(ui, &mut self.cache, buffer); + } else { + CommonMarkViewer::new().show(ui, &mut self.cache, "# No content loaded\nOpen a markdown file here!"); + } + }); + + None + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + +impl Default for Documentation { + fn default() -> Self { + Self { + buffer: None, + cache: CommonMarkCache::default(), + } + } +} + +impl Documentation { + pub fn open(path: &Path) -> Self { + Self { + buffer: Some(std::fs::read_to_string(path).unwrap()), + cache: CommonMarkCache::default(), + } + } + + pub fn get_mut(&mut self) -> &mut Option { + &mut self.buffer + } +} \ No newline at end of file diff --git a/dsa/emulator/emulator_ui/src/ui/debugger/panels/editor.rs b/dsa/emulator/emulator_ui/src/ui/debugger/panels/editor.rs new file mode 100644 index 0000000..e682d0c --- /dev/null +++ b/dsa/emulator/emulator_ui/src/ui/debugger/panels/editor.rs @@ -0,0 +1,89 @@ +use std::any::Any; +use std::path::{Path, PathBuf}; +use egui::accesskit::Role::Code; +use egui::{Key, Ui}; +use egui_code_editor::{CodeEditor, Syntax}; +use egui_file_dialog::FileDialog; +use egui_tiles::TileId; +use crate::ui::components::Component; +use crate::ui::components::editor::{syntax, THEME}; +use crate::ui::debugger::Action; +use crate::ui::debugger::panels::TilePane; + +pub struct Editor { + buffer: String, + filename: Option, + + editor: CodeEditor, + dialog: FileDialog, + visible: bool, +} + +impl Default for Editor { + fn default() -> Self { + Self { + buffer: String::new(), + filename: None, + editor: CodeEditor::default().with_fontsize(12.0).with_theme(THEME).with_numlines(true), + dialog: FileDialog::new(), + visible: false, + } + } +} + +impl Editor { + pub fn new_open(path: &Path) -> Self { + Self { + buffer: std::fs::read_to_string(path).unwrap(), + filename: Some(path.to_path_buf()), + ..Default::default() + } + } + + pub fn filename_mut(&mut self) -> &mut Option { + &mut self.filename + } + + pub fn filename(&self) -> Option<&PathBuf> { + self.filename.as_ref() + } + + pub fn get_buffer(&mut self) -> &mut String { + &mut self.buffer + } +} + +impl TilePane for Editor { + fn title(&self) -> egui::WidgetText { + self.filename.clone() + .map(|f| f.file_name().unwrap().to_str().unwrap().into()) + .unwrap_or("New File*".into()) + } + + fn ui(&mut self, ui: &mut Ui, tile_id: TileId) -> Option { + + egui::Panel::top("panel").show(ui, |ui| { + if let Some(filename) = self.filename.as_ref() { + ui.label(filename.display().to_string()); + } else { + ui.label("New File*"); + } + }); + + let mut action = None; + + self.editor.show(ui, &mut self.buffer, &syntax::dsa()); + + if ui.input(|i| i.key_pressed(Key::S) && i.modifiers.ctrl) { + if let Some(filename) = self.filename.clone() { + action = Some(Action::SaveFile(filename, tile_id)); + } else { + action = Some(Action::RequestSaveAs(tile_id)); + } + }; + + action + } + + fn as_any_mut(&mut self) -> &mut dyn Any { self } +} \ No newline at end of file diff --git a/dsa/emulator/emulator_ui/src/ui/debugger/panels/mod.rs b/dsa/emulator/emulator_ui/src/ui/debugger/panels/mod.rs new file mode 100644 index 0000000..24f87ea --- /dev/null +++ b/dsa/emulator/emulator_ui/src/ui/debugger/panels/mod.rs @@ -0,0 +1,174 @@ +pub mod documentation; +pub mod editor; +pub mod about; + +use std::any::Any; +use egui::{Id, Response, Stroke, StrokeKind, Ui, Widget}; +use egui_tiles::{Behavior, Container, SimplificationOptions, TabState, Tile, TileId, Tiles, Tree, UiResponse}; +use crate::ui::debugger::Action; + +/// Anything that can live inside a tab in the central tiled area. +/// `as_any_mut` lets callers reach back into a specific pane's concrete +/// type after it's been handed off to the tree (e.g. to write into the +/// editor's buffer from an `Action` handler). +pub trait TilePane: Any { + fn title(&self) -> egui::WidgetText; + fn ui(&mut self, ui: &mut egui::Ui, _tile_id: TileId) -> Option; + fn as_any_mut(&mut self) -> &mut dyn Any; + + fn menu(&mut self, _ui: &mut egui::Ui, _tile_id: TileId) -> Option { + None + } +} + +type Pane = Box; + +#[derive(Default)] +struct TileBehavior { + pending_action: Option, +} + +impl Behavior for TileBehavior { + fn is_tab_closable(&self, _tiles: &Tiles, _tile_id: TileId) -> bool { + true + } + + fn on_tab_close(&mut self, tiles: &mut Tiles, tile_id: TileId) -> bool { + tiles.remove(tile_id); + true + } + + fn tab_bar_height(&self, _style: &egui::Style) -> f32 { + 36.0 // taller tab row = more vertical padding around the label + } + + fn tab_bg_color(&self, visuals: &egui::Visuals, _tiles: &egui_tiles::Tiles>, _tile_id: egui_tiles::TileId, tab_state: &egui_tiles::TabState) -> egui::Color32 { + visuals.widgets.inactive.bg_fill + } + + + fn pane_ui( + &mut self, + ui: &mut egui::Ui, + tile_id: TileId, + pane: &mut Box, + ) -> UiResponse { + egui::Frame::new() + .inner_margin(egui::Margin::same(10)) + .stroke(ui.visuals().widgets.active.bg_stroke) + .show(ui, |ui| { + if let Some(action) = pane.ui(ui, tile_id) { + self.pending_action = Some(action); + } + }); + + UiResponse::None + } + + + fn tab_title_for_pane(&mut self, pane: &Pane) -> egui::WidgetText { + pane.title() + } + + fn simplification_options(&self) -> SimplificationOptions { + SimplificationOptions { + // Keep the tab bar around even with only one tab, and don't let + // panes get dragged out into their own floating windows. + all_panes_must_have_tabs: true, + ..Default::default() + } + } +} + +/// A single top-level tab strip that tools can add tabs to at runtime. +pub struct CentralTiles { + tree: Tree, + behavior: TileBehavior, +} + +impl CentralTiles { + pub fn new() -> Self { + let mut tiles = Tiles::::default(); + let root = tiles.insert_tab_tile(vec![]); + Self { + tree: Tree::empty("central_tiles"), + behavior: TileBehavior::default(), + } + } + + pub fn active_tile(&self) -> Option { + match self.tree.root.and_then(|root| self.tree.tiles.get(root)) { + Some(Tile::Container(Container::Tabs(tabs))) => tabs.active, + _ => None, + } + } + + pub fn contains(&self, tile_id: TileId) -> bool { + self.tree.tiles.get(tile_id).is_some() + } + + pub fn add_tab(&mut self, pane: impl TilePane) -> TileId { + self.add_dyn_tab(Box::new(pane)) + } + + + /// Add a new tab and make it active. Returns the `TileId` so the + /// caller can hold onto it for later access via `pane_mut`. + pub fn add_dyn_tab(&mut self, pane: Box) -> TileId { + let tile_id = self.tree.tiles.insert_pane(pane); + + match self.tree.root { + Some(root) => { + if let Some(Tile::Container(Container::Tabs(tabs))) = self.tree.tiles.get_mut(root) { + tabs.children.push(tile_id); + tabs.active = Some(tile_id); + } else { + // Root exists but isn't a Tabs container for some reason — + // wrap both under a fresh tab container rather than losing it. + let new_root = self.tree.tiles.insert_tab_tile(vec![root, tile_id]); + self.tree.root = Some(new_root); + } + } + None => { + // Tree was empty (or got simplified down to nothing) — + // this is the first tab, so create the root container now. + let root = self.tree.tiles.insert_tab_tile(vec![tile_id]); + self.tree.root = Some(root); + } + } + + tile_id + } + + /// Bring an already-open tab to the front (e.g. re-focus "Docs" + /// instead of opening a duplicate tab). + pub fn activate_tab(&mut self, tile_id: TileId) { + self.tree.make_active(|id, _tile| id == tile_id); + } + + /// Reach into a specific pane's concrete type by id. + pub fn pane_mut(&mut self, tile_id: TileId) -> Option<&mut T> { + match self.tree.tiles.get_mut(tile_id) { + Some(Tile::Pane(pane)) => pane.as_any_mut().downcast_mut::(), + _ => None, + } + } + + pub fn ui(&mut self, ui: &mut egui::Ui) -> Option { + self.behavior.pending_action = None; + self.tree.ui(&mut self.behavior, ui); + self.behavior.pending_action.take() + } +} + +pub struct PanelFactory(pub String, pub Box Box>); + +impl PanelFactory { + pub fn register(name: &str, constructor: F) -> Self + where + T: TilePane + 'static, + F: Fn() -> T + 'static, + { + Self(name.to_string(), Box::new(move || Box::new(constructor()) as Box)) + } +} \ No newline at end of file diff --git a/dsa/emulator/emulator_ui/src/ui/debugger/ui_helpers/mod.rs b/dsa/emulator/emulator_ui/src/ui/debugger/ui_helpers/mod.rs new file mode 100644 index 0000000..0120819 --- /dev/null +++ b/dsa/emulator/emulator_ui/src/ui/debugger/ui_helpers/mod.rs @@ -0,0 +1,203 @@ +use eframe::emath::Vec2; +use egui::{Color32, Frame, Stroke}; +use egui::widget_style::{Classes, WidgetState}; +use emulator_core::processor::state::RunningState; + + +pub struct PaddedWidget { + text: String, + color: Option, + padding: Option, + widget_type: WidgetType +} + +enum WidgetType { + Button, + Indicator, + Label +} + +impl PaddedWidget { + pub fn button(text: impl AsRef) -> Self { + Self { + text: text.as_ref().to_string(), + color: None, + padding: None, + widget_type: WidgetType::Button + } + } + + pub fn indicator(i: impl StatusIndicator) -> Self { + Self { + text: i.label().to_string(), + color: Some(i.color()), + padding: None, + widget_type: WidgetType::Indicator + } + } + + pub fn label(text: String) -> Self { + Self { + text, + color: None, + padding: None, + widget_type: WidgetType::Label + } + } + + pub fn color(&mut self, color: Color32) -> &mut Self { + self.color = Some(color); + self + } + + pub fn padding(&mut self, padding: Vec2) -> &mut Self { + self.padding = Some(padding); + self + } + + pub fn ui(&mut self, ui: &mut egui::Ui) -> egui::Response { + let text: egui::WidgetText = self.text.clone().into(); + let galley = text.into_galley(ui, None, f32::INFINITY, egui::TextStyle::Button); + let desired_size = galley.size() + self.padding.unwrap_or(Vec2::new(14.0, 8.0)) * 2.0; + + let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click()); + + if ui.is_rect_visible(rect) { + // picks noninteractive/inactive/hovered/active automatically based on `response` + + let visuals = match self.widget_type { + WidgetType::Button => ui.style().interact(&response), + _ => &ui.visuals().widgets.inactive, + }; + + ui.painter().rect( + rect, + visuals.corner_radius, + visuals.bg_fill, + visuals.bg_stroke, + egui::StrokeKind::Inside, + ); + + let text_pos = rect.center() - galley.size() / 2.0; + ui.painter().galley(text_pos, galley, self.color.unwrap_or(visuals.fg_stroke.color)); + } + + response + } +} + +// pub fn frame_button(ui: &mut egui::Ui, text: impl Into, color: egui::Color32, padding: Vec2) -> egui::Response { +// let text: egui::WidgetText = text.into(); +// let galley = text.into_galley(ui, None, f32::INFINITY, egui::TextStyle::Button); +// let desired_size = galley.size() + padding * 2.0; +// +// let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click()); +// +// if ui.is_rect_visible(rect) { +// // picks noninteractive/inactive/hovered/active automatically based on `response` +// let visuals = ui.style().interact(&response); +// +// ui.painter().rect( +// rect, +// visuals.corner_radius, +// visuals.bg_fill, +// visuals.bg_stroke, +// egui::StrokeKind::Inside, +// ); +// +// let text_pos = rect.center() - galley.size() / 2.0; +// ui.painter().galley(text_pos, galley, color); +// } +// +// response +// } +// +// pub fn info_field(ui: &mut egui::Ui, text: impl Into, padding: egui::Vec2) -> egui::Response { +// let text: egui::WidgetText = text.into(); +// let galley = text.into_galley(ui, None, f32::INFINITY, egui::TextStyle::Button); +// let desired_size = galley.size() + padding * 2.0; +// +// let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::click()); +// +// if ui.is_rect_visible(rect) { +// // Always use `inactive` — no hover highlight, no active/pressed feedback either. +// // Response is still fully clickable; this only affects what gets drawn. +// let visuals = &ui.visuals().widgets.inactive; +// +// ui.painter().rect( +// rect, +// visuals.corner_radius, +// visuals.bg_fill, +// visuals.bg_stroke, +// egui::StrokeKind::Inside, +// ); +// +// let text_pos = rect.center() - galley.size() / 2.0; +// ui.painter().galley(text_pos, galley, visuals.fg_stroke.color); +// } +// +// response +// } + + +pub fn bordered_frame( + ui: &mut egui::Ui, + padding: egui::Vec2, + add_contents: impl FnOnce(&mut egui::Ui) -> R, +) -> egui::InnerResponse { + let visuals = ui.visuals().widgets.inactive; // same visuals a normal button uses + + egui::Frame::new() + .inner_margin(egui::Margin::symmetric(padding.x as i8, padding.y as i8)) + .stroke(visuals.bg_stroke) + .corner_radius(visuals.corner_radius) + .fill(visuals.bg_fill) + .show(ui, add_contents) +} + +pub trait StatusIndicator { + fn ui(&mut self, ui: &mut egui::Ui, padding: egui::Vec2) -> egui::Response { + bordered_frame(ui, padding, |ui| { + ui.horizontal(|ui| { + let dot_size = 8.0; + let (rect, _) = ui.allocate_exact_size( + egui::vec2(dot_size, dot_size), + egui::Sense::hover(), + ); + ui.painter().circle_filled(rect.center(), dot_size / 2.0, self.color()); + ui.label( self.label() ); + }) + .response + }) + .response + } + + + fn color(&self) -> egui::Color32; + + fn label(&self) -> String; +} + + + +impl StatusIndicator for RunningState { + fn color(&self) -> egui::Color32 { + match self { + RunningState::Running => egui::Color32::from_rgb(0x4c, 0xaf, 0x50), + RunningState::Paused => egui::Color32::from_rgb(0xff, 0xc1, 0x07), + RunningState::Halted => egui::Color32::from_rgb(0xff, 0xc1, 0x07), + RunningState::Breakpoint(_) => egui::Color32::from_rgb(0xe5, 0x39, 0x35), + RunningState::Shutdown => egui::Color32::from_rgb(0xff, 0xc1, 0x07), + } + } + + fn label(&self) -> String { + match self { + RunningState::Running => "Running".into(), + RunningState::Paused => "Paused".into(), + RunningState::Halted => "Halted".into(), + RunningState::Breakpoint(addr) => format!("Breakpoint @ {addr:#06x}"), + RunningState::Shutdown => "Shutdown".into(), + } + } +} \ No newline at end of file diff --git a/dsa/emulator/emulator_ui/src/ui/mod.rs b/dsa/emulator/emulator_ui/src/ui/mod.rs index aae8e8d..6e78b44 100644 --- a/dsa/emulator/emulator_ui/src/ui/mod.rs +++ b/dsa/emulator/emulator_ui/src/ui/mod.rs @@ -2,8 +2,11 @@ use eframe::NativeOptions; use eframe::egui; use std::sync::Arc; use std::sync::atomic::Ordering; +use egui::Order::Debug; mod components; +pub mod debugger; + use components::{ controller::Controller, display::Display, editor::Editor, framebuffer::FrameBuffer, memory::MemoryInspector, registers::Registers, serial::Serial, Component, @@ -13,15 +16,27 @@ use emulator_core::{ memory::ram::MemoryBank, processor::state::SharedState }; +use crate::ui::components::disassembler::Disassembler; +use crate::ui::debugger::DebuggerUi; pub fn run_app(state: Arc, mem_handle: MemoryBank) -> eframe::Result<()> { + + let e = e(); + eframe::run_native( "Damn Simple Architecture", NativeOptions::default(), - Box::new(|cc| Ok(Box::new(DsaUi::new(cc, state, mem_handle)))), + + // Box::new(|cc| Ok(Box::new(DsaUi::new(cc, state, mem_handle)))), + Box::new(|cc| Ok(Box::new(DebuggerUi::new(cc, state, mem_handle)))), ) } + +fn e() -> i32 { + return 5 +} + impl eframe::App for DsaUi { fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { @@ -96,6 +111,7 @@ impl DsaUi { Box::new(Serial::new(0x40000, state.clone(), mem_handle.clone())), Box::new(Editor::new(state.clone(), mem_handle.clone())), Box::new(MemoryInspector::new(mem_handle.clone())), + Box::new(Disassembler::new()) ]; components.iter_mut().for_each(|c| *c.visible() = false); @@ -108,7 +124,7 @@ impl DsaUi { app } - fn configure_appearance(ctx: &egui::Context) { + pub fn configure_appearance(ctx: &egui::Context) { // configure appearance of UI elements let mut visuals = egui::Visuals::dark(); diff --git a/dsa_resources/animation.dsa b/dsa_resources/animation.dsa index 61eb01c..c2fc1c0 100644 --- a/dsa_resources/animation.dsa +++ b/dsa_resources/animation.dsa @@ -8,7 +8,7 @@ dw COL2: 0xFF00FF00 // green dw COL3: 0xFFFF0000 // blue dw COL4: 0xFF00FFFF // yellow dw COL_IDX: 0 - + // registers: // rg0 = x position // rg1 = y position diff --git a/dsa_resources/bench.dsa b/dsa_resources/bench.dsa index 20d8dcd..326e222 100644 --- a/dsa_resources/bench.dsa +++ b/dsa_resources/bench.dsa @@ -1,6 +1,6 @@ include print: "./print.dsa" db msg: "hello world" -dw stack: 0x1000 +dw stack: 0x10000 _init: ldw stack, bpr mov bpr, spr diff --git a/dsa_resources/out.dsb b/dsa_resources/out.dsb index 50db19cf16a7e7d7ee3d00051db10c166874de5c..facbb272f30a9f238b6543c30697898827d9b125 100644 GIT binary patch literal 3709 zcmeHJUx-vy82_F5W3DYEfj*70D+Ej9Tx=TZ<#tP(2*H*$ClRLMIy?8OL$h~=J9pL< zERJC4!#?c8Hpvp(J_tciOK^igVs>59%F0A;F9tp=Xkc$k_#hPDIp4UmBPjKn2Zrf`808GVzff#Tm0o+akt|8F#$Xe;2w0GqL}j| z06Lr}zDu0X0yrPj_}2ieG3IRmSB1_W1aO+;_W@ks*dLLWjS`*{0k9sSh zQ%V1eVLb`^2*9$^rdhNst7pYDiy13zWr|kXvPxDlUCfwitE8SxHA+^|Ok0_fRm_w& zZY^dWvybt`C#mcoMln+=ng4N{Y4+1SX;3-s(=;r@vWlun9g^-`gYKpEY6>ir{n2(I{uI2kp3X5_W}@8zuUR4;RYdctM8^O+*}KUl16azYan;D+d&9yHhKU~y1J{h*_{k{Z zXQPB)3?M!z-*R&I&@RY#oZK4C?>V^#G?$#*plBDQ>*P{2PdGV~=9ZH?K=X%A?gGuB zlk1>eSV=A$z;afG+h=9?%B&0z&*o#%ybPl`87|GquroImkLK?VXpMyHt_$3mN#U=8 z3?uq~G(CoBRAArqs-9%5%_;K!t_?rGFDy_^_V6K@i*Hz}a)x&jBacPc= z8#to2UOU{DVRlp3&&_x=e<>c#Lv)Ppxe8E?r5C9mGEC$*$EddMasM3vmJPZqGQ2Sd zU_tir(>cj-a&|M`*m-|I-y2{!1;hqr_-5{WY_b6C9U`v+!_&1`Ghu#dNr-_-35^9#mnR_Ii_mn&Bb`TAb0GO m3-M@FZX1?k81Ca(@9_q1(J|I~*80!*Uc6l({Knp|pX)zY&CYTF literal 2500 zcmeHH&ubGw6#ll&kBt;OwWqWsJq6btN?5GmFxV2of|jal5tap|TWCS6*hVi3bEx1U zhaBP_atQtbK|DBm@RC%aRjYY>LX-jtb5V4I>vulB+2x5Inyd+&R1lHGVP zISRn|3uA}H9tdE}pwwZEMLd)`CNr4nFoQ9dIfi3WgSqNNmATBJ1~XmenC|{m$NWQH zJKj00%Wfl%>ALhE(73~QWGc&V`i|T&?=uywBudie4+6DnO^1$UG zE-oe3v{eGE=7F7bD;d;uGziehZ)!~fH1h`5h>3TE;XR@FKnT{!Nqi&@J`oq63DDce z&w19Dgm3eUp0y?Ms%L$Z=z7)2arn66lQP~#$~a?z}E(WqupL2WZ0_9){zehoWiJ@MV9 zA(kZqW4RczC15xmIhQ-NvfBA@8!d5{Wf zUMi?TYmm-fHOLyx6LKD0#0qv&q3w8hSAc99 z=;@PM>M7p_#xhc$hHUAAF7=0f9n8@(GNzvTeN0^sYSMpP^)gUd7I@72alNJX)Tnm8 zWq-%;hVa1{-PL`2RP80?m%fp%bEl`MhK%|R9PDYUeeVFIqwzdG%%@b%0=<^