From 76197fac8ff7c4bcb5673c03b21f7f5508ce04dd Mon Sep 17 00:00:00 2001 From: zxq5 Date: Mon, 23 Jun 2025 23:45:47 +0100 Subject: [PATCH] finished refactor of emulator - started on loader (needs significant changes before functional in the way that I would like) --- assembler/brainf.dsb | Bin 3000 -> 3000 bytes common/src/instructions.rs | 16 +- emulator/src/emulator/system/emulator.rs | 182 ++++++------ emulator/src/emulator/system/memory.rs | 8 +- emulator/src/emulator/system/model.rs | 155 +++++++--- emulator/src/emulator/ui/control_unit.rs | 60 ++-- emulator/src/emulator/ui/display.rs | 4 +- emulator/src/emulator/ui/editor.rs | 70 +---- emulator/src/emulator/ui/history.rs | 11 +- emulator/src/emulator/ui/interface.rs | 24 +- emulator/src/emulator/ui/loader.rs | 294 +++++++++++++++++++ emulator/src/emulator/ui/memory_inspector.rs | 44 +-- emulator/src/emulator/ui/mod.rs | 1 + emulator/src/emulator/ui/stack_inspector.rs | 7 +- emulator/src/lib.rs | 17 +- 15 files changed, 604 insertions(+), 289 deletions(-) create mode 100644 emulator/src/emulator/ui/loader.rs diff --git a/assembler/brainf.dsb b/assembler/brainf.dsb index c748cf895f8113e0d3484e6193d250944b48b3de..885a8c58464e52c7204808f3ecd083063309f49a 100644 GIT binary patch delta 22 dcmdlXzC(P&6mBj(aRv?}aRvqrgUR!_r2s})1v~%% delta 22 ecmdlXzC(P&6mBlv?+hG<-x(M*^e4~bmI44-dj|Oc diff --git a/common/src/instructions.rs b/common/src/instructions.rs index 952033c..381e739 100644 --- a/common/src/instructions.rs +++ b/common/src/instructions.rs @@ -3,6 +3,8 @@ use crate::{instructions::encode::Encode, prelude::*}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Interrupt { Software(u8), + Breakpoint, + HardFault, } pub type Address = u32; @@ -10,6 +12,8 @@ pub type Address = u32; impl Interrupt { const fn as_u8(self) -> u8 { match self { + Self::Breakpoint => 0, + Self::HardFault => 1, Self::Software(code) => code, } } @@ -19,10 +23,11 @@ impl Interrupt { impl From for Interrupt { #[allow(unreachable_code)] fn from(code: u8) -> Self { - return Self::Software(code); - todo!("Implement this once a hardware interrupt convention is established."); - - // Self::Software(_code) + match code { + 0 => Self::Breakpoint, + 1 => Self::HardFault, + _ => Self::Software(code), + } } } @@ -73,7 +78,8 @@ pub enum Register { } impl Register { - #[must_use] + // this is here so clippy shuts up about the must_use tag. + #[allow(clippy::must_use_candidate)] pub fn general() -> Vec { vec![ Self::Rg0, diff --git a/emulator/src/emulator/system/emulator.rs b/emulator/src/emulator/system/emulator.rs index 04c1118..40e4b57 100644 --- a/emulator/src/emulator/system/emulator.rs +++ b/emulator/src/emulator/system/emulator.rs @@ -8,8 +8,9 @@ use std::{ #[allow(unused_imports)] use crate::emulator::misc::rpc::{Activity, RpcClient}; +use crate::emulator::system::model::StateUpdate; use crate::emulator::system::{ - model::{Command, PersistentState, Running, State}, + model::{Command, Running}, processor::Processor, }; @@ -19,32 +20,32 @@ use common::prelude::*; #[allow(unused_variables)] pub fn run_emulator( cmd_rx: &Receiver, - state_tx: &Sender, + state_tx: &Sender, mut processor: Processor, rpc_client: Option<&Arc>, ) { println!("INFO: Starting emulator."); let mut running = Running::Paused; - let mut addr = 0u32; + let mut addr; let mut history = Vec::<(u32, Instruction)>::new(); let size = 256; - let memory_view = processor - .memory - .read_range(addr, size) - .expect("Failed to read initial memory state!"); - - let initial_state = state(&mut processor, running, 0, memory_view, &mut history); - let _ = state_tx.send(initial_state); + state_tx + .send(StateUpdate::Running(Running::Paused)) + .expect("Failed to send initial state!"); let mut instruction_count = 0; + let mut update = false; loop { let cmd = if running == Running::Running { match cmd_rx.try_recv() { Ok(cmd) => Some(cmd), - Err(mpsc::TryRecvError::Empty) => None, + Err(mpsc::TryRecvError::Empty) => { + update = false; + None + } Err(mpsc::TryRecvError::Disconnected) => break, } } else { @@ -96,6 +97,8 @@ pub fn run_emulator( processor.reset(); } Command::Step => { + update = true; + running = Running::Paused; // Execute one cycle. @@ -114,42 +117,91 @@ pub fn run_emulator( instruction_count += 1; } - Command::Read(new, _size) => { - addr = new; - } Command::Write(offset, data) => { + update = true; + processor .memory .write_range(offset, data) .unwrap_or_else(|_| { - eprintln!("Failed to write memory range!"); - processor.begin_interrupt(Interrupt::HardFault); + report_err( + state_tx, + "Failed to write memory range!", + &mut processor, + ); }); } Command::Interrupt(_interrupt) => { + update = true; + todo!("implement interrupts") } + Command::MemRequest(new, size) if update => { + addr = new; + let _ = state_tx.send(StateUpdate::MemoryView( + processor.memory.read_range(addr, size).unwrap_or_else(|_| { + report_err( + state_tx, + "Failed to read memory range!", + &mut processor, + ); + Vec::new() + }), + )); + } + Command::DisplayRequest if update => { + let _ = state_tx.send(StateUpdate::DisplayView( + processor.display().unwrap_or_else(|_| { + report_err( + state_tx, + "Failed to read display!", + &mut processor, + ); + Vec::new() + }), + )); + } + Command::StackRequest if update => { + let _ = state_tx.send(StateUpdate::StackView( + processor.get_stack(32).unwrap_or_else(|_| { + report_err(state_tx, "Failed to read stack!", &mut processor); + Vec::new() + }), + )); + } + Command::RegisterRequest if update => { + let _ = state_tx.send(StateUpdate::Registers(processor.registers)); + } + Command::RunningRequest if update => { + let _ = state_tx.send(StateUpdate::Running(running)); + } + Command::HistoryRequest if update => { + let hsc = history.clone(); + history.clear(); + let _ = state_tx.send(StateUpdate::InstructionHistory(hsc)); + } + Command::InstructionCountRequest if update => { + let _ = state_tx.send(StateUpdate::Instructions(instruction_count)); + } + Command::WriteBlock(addr, block) => { + processor + .memory + .write_range(addr, block.to_vec()) + .unwrap_or_else(|_| { + report_err( + state_tx, + "Failed to write memory block!", + &mut processor, + ); + }); + } + + _ => {} } - - let memory_view = - processor.memory.read_range(addr, size).unwrap_or_else(|_| { - eprintln!("Failed to read memory range!"); - processor.begin_interrupt(Interrupt::HardFault); - Vec::new() - }); - let state = state( - &mut processor, - running, - instruction_count, - memory_view, - &mut history, - ); - - let _ = state_tx.send(state); } if running == Running::Running { - let mut update = false; + update = true; // Execute one cycle. let instruction = match processor.cycle() { @@ -162,74 +214,16 @@ pub fn run_emulator( }; history.push(instruction); - - // let instruction = match Instruction::decode(cpu_lock.get(Register::Cir)) - // {}; - if matches!(instruction.1, Instruction::Halt) { running = Running::Halted; - update = true; } instruction_count += 1; - - // Send state updates every 100 instructions - if instruction_count % 100 == 0 { - update = true; - } - - if update { - let memory_view = - processor - .memory - .read_range(addr, size) - .unwrap_or_else(|why| { - eprintln!("Failed to read memory range! Reason: {why}"); - processor.begin_interrupt(Interrupt::HardFault); - Vec::new() - }); - let state = state( - &mut processor, - running, - instruction_count, - memory_view, - &mut history, - ); - println!("running state"); - // println!("state!!! {:?}", state.history); - let _ = state_tx.send(state); - } - } else { - thread::sleep(Duration::from_millis(1)); } } } -fn state( - processor: &mut Processor, - running: Running, - instruction_count: usize, - memory_view: Vec, - history: &mut Vec<(u32, Instruction)>, -) -> State { - let hsclone = history.clone(); - history.clear(); - - State { - // TODO: Replace with actual register access from your CPU. - reg_file: processor.registers, - running, - instructions: instruction_count, - stack_view: processor.get_stack(32).unwrap_or_else(|_| { - processor.begin_interrupt(Interrupt::HardFault); - Vec::new() - }), - memory_view, - display_view: processor.display().unwrap_or_else(|_| { - processor.begin_interrupt(Interrupt::HardFault); - Vec::new() - }), - error: None, - persistent: PersistentState { history: hsclone }, - } +fn report_err(state_tx: &Sender, why: &str, processor: &mut Processor) { + processor.begin_interrupt(Interrupt::HardFault); + let _ = state_tx.send(StateUpdate::Error(why.to_string())); } diff --git a/emulator/src/emulator/system/memory.rs b/emulator/src/emulator/system/memory.rs index ccc9719..b6a1425 100644 --- a/emulator/src/emulator/system/memory.rs +++ b/emulator/src/emulator/system/memory.rs @@ -26,15 +26,15 @@ pub trait MemoryUnit: Send + Sync { fn read_block(&mut self, addr: u32) -> Result<[u8; 256], ProcessorError> { let mut data = [0; 256]; - for i in 0..256 { - data[i] = self.read_byte(addr + i as u32)?; + for (i, byte) in data.iter_mut().enumerate() { + *byte = self.read_byte(addr + i as u32)?; } Ok(data) } fn write_block(&mut self, addr: u32, data: [u8; 256]) -> Result<(), ProcessorError> { - for i in 0..256 { - self.write_byte(addr + i as u32, data[i])?; + for (i, byte) in data.iter().enumerate() { + self.write_byte(addr + i as u32, *byte)?; } Ok(()) } diff --git a/emulator/src/emulator/system/model.rs b/emulator/src/emulator/system/model.rs index 5b39a8e..f31ad12 100644 --- a/emulator/src/emulator/system/model.rs +++ b/emulator/src/emulator/system/model.rs @@ -1,3 +1,5 @@ +use std::sync::mpsc::{self, Receiver, Sender}; + use common::prelude::*; #[derive(PartialEq, Eq, Debug, Clone, Copy)] @@ -16,15 +18,23 @@ pub trait IODevice: Send + Sync { #[derive(PartialEq, Eq, Debug, Clone)] pub enum Command { + // set emulator state. Start, Stop, Step, Reset(usize), Interrupt(Interrupt), - - // Performs direct read/write operations on the emulator's memory. - Read(Address, u32), Write(Address, Vec), + WriteBlock(Address, Box<[u8; 256]>), + + // request emulator state. + MemRequest(Address, u32), + DisplayRequest, + StackRequest, + RegisterRequest, + RunningRequest, + HistoryRequest, + InstructionCountRequest, } #[derive(Debug)] @@ -52,6 +62,101 @@ impl std::fmt::Display for ProcessorError { } } +pub struct State { + pub state_receiver: Receiver, + pub cmd_sender: Sender, + + // Processor state + pub reg_file: RegFile, + pub running: Running, + pub instructions: usize, + + // Memory access views + pub stack_view: Vec, + pub memory_view: Vec, + pub display_view: Vec, + + pub error_log: Vec, + + pub instruction_history: Vec<(u32, Instruction)>, +} + +impl State { + #[must_use] + pub fn new(sender: Sender, receiver: Receiver) -> Self { + Self { + state_receiver: receiver, + cmd_sender: sender, + reg_file: RegFile::default(), + running: Running::Paused, + instructions: 0, + stack_view: vec![], + memory_view: vec![], + display_view: vec![], + error_log: vec![], + instruction_history: vec![], + } + } + + pub fn send(&mut self, cmd: Command) { + if let Err(e) = self.cmd_sender.send(cmd) { + self.error_log.push(e.to_string()); + } + } + + pub fn update(&mut self) -> Result<(), mpsc::TryRecvError> { + while let Ok(update) = self.state_receiver.try_recv() { + match update { + StateUpdate::Registers(reg_file) => self.reg_file = reg_file, + StateUpdate::Running(running) => self.running = running, + StateUpdate::Instructions(instructions) => { + self.instructions = instructions; + } + StateUpdate::StackView(stack_view) => self.stack_view = stack_view, + StateUpdate::MemoryView(memory_view) => self.memory_view = memory_view, + StateUpdate::DisplayView(display_view) => { + self.display_view = display_view; + } + StateUpdate::Error(err_state) => self.error_log.push(err_state), + StateUpdate::InstructionHistory(history) => { + self.instruction_history.extend(history); + } + } + + if self.error_log.len() > 256 { + self.error_log.drain(0..self.error_log.len() - 256); + } + + if self.instruction_history.len() > 1024 { + self.instruction_history + .drain(0..self.instruction_history.len() - 1024); + } + } + + if let Err(e) = self.state_receiver.try_recv() { + match e { + mpsc::TryRecvError::Empty => {} + mpsc::TryRecvError::Disconnected => { + return Err(e); + } + } + } + + Ok(()) + } +} + +pub enum StateUpdate { + Registers(RegFile), + Running(Running), + Instructions(usize), + StackView(Vec), + MemoryView(Vec), + DisplayView(Vec), + Error(String), + InstructionHistory(Vec<(u32, Instruction)>), +} + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub struct RegFile { // General Purpose Registers @@ -220,47 +325,3 @@ impl RegFile { } } } - -pub struct State { - pub reg_file: RegFile, - pub running: Running, - pub instructions: usize, - - // Memory access views - pub stack_view: Vec, - pub memory_view: Vec, - pub display_view: Vec, - pub error: Option, - - pub persistent: PersistentState, -} - -impl Default for State { - fn default() -> Self { - Self { - reg_file: RegFile::default(), - running: Running::Paused, - instructions: 0, - stack_view: vec![], - memory_view: vec![], - display_view: vec![], - persistent: PersistentState::default(), - error: None, - } - } -} - -#[derive(Clone, Debug, Default)] -pub struct PersistentState { - pub history: Vec<(u32, Instruction)>, -} - -impl PersistentState { - pub fn update(&mut self, new_state: &Self) { - self.history.extend(new_state.history.clone()); - if self.history.len() > 1024 { - let len = self.history.len() - 1024; - self.history.drain(..len); - } - } -} diff --git a/emulator/src/emulator/ui/control_unit.rs b/emulator/src/emulator/ui/control_unit.rs index 3d8046a..75c2c3b 100644 --- a/emulator/src/emulator/ui/control_unit.rs +++ b/emulator/src/emulator/ui/control_unit.rs @@ -1,5 +1,3 @@ -use std::sync::mpsc::Sender; - use crate::emulator::{ system::model::{Command, Running, State}, ui::interface::Component, @@ -9,16 +7,18 @@ use common::{instructions::Register, prelude::Instruction}; pub struct ControlPanel { visible: bool, - sender: Sender, } impl ControlPanel { - #[must_use] - pub const fn new(sender: Sender) -> Self { - Self { - visible: false, - sender, - } + #[allow(clippy::must_use_candidate)] + pub const fn new() -> Self { + Self { visible: false } + } +} + +impl Default for ControlPanel { + fn default() -> Self { + Self::new() } } @@ -47,46 +47,58 @@ impl Component for ControlPanel { .clicked() { if state.running == Running::Running { - self.sender.send(Command::Stop).unwrap_or_else(|_| { - state.error = Some("Failed to send command".to_string()); + state.cmd_sender.send(Command::Stop).unwrap_or_else(|_| { + state.error_log.push("Failed to send command".to_string()); }); } else { - self.sender.send(Command::Start).unwrap_or_else(|_| { - state.error = Some("Failed to send command".to_string()); + state.cmd_sender.send(Command::Start).unwrap_or_else(|_| { + state.error_log.push("Failed to send command".to_string()); }); } } // Step if ui.button("Step").clicked() { - self.sender.send(Command::Step).unwrap_or_else(|_| { - state.error = Some("Failed to send command".to_string()); + state.cmd_sender.send(Command::Step).unwrap_or_else(|_| { + state.error_log.push("Failed to send command".to_string()); }); } // Resets the emulator and all attached devices if ui.button("Reset All").clicked() { - self.sender.send(Command::Reset(0)).unwrap_or_else(|_| { - state.error = Some("Failed to send command".to_string()); - }); + state + .cmd_sender + .send(Command::Reset(0)) + .unwrap_or_else(|_| { + state.error_log.push("Failed to send command".to_string()); + }); } // Resets the emulator and all attached devices if ui.button("Clear Registers").clicked() { - self.sender.send(Command::Reset(1)).unwrap_or_else(|_| { - state.error = Some("Failed to send command".to_string()); - }); + state + .cmd_sender + .send(Command::Reset(1)) + .unwrap_or_else(|_| { + state.error_log.push("Failed to send command".to_string()); + }); } // Resets the emulator and all attached devices if ui.button("Clear RAM").clicked() { - self.sender.send(Command::Reset(2)).unwrap_or_else(|_| { - state.error = Some("Failed to send command".to_string()); - }); + state + .cmd_sender + .send(Command::Reset(2)) + .unwrap_or_else(|_| { + state.error_log.push("Failed to send command".to_string()); + }); } ui.separator(); + state.send(Command::RegisterRequest); + state.send(Command::RunningRequest); + // Status info ui.label(format!( "Status: {}", diff --git a/emulator/src/emulator/ui/display.rs b/emulator/src/emulator/ui/display.rs index a4bde47..90ee4a9 100644 --- a/emulator/src/emulator/ui/display.rs +++ b/emulator/src/emulator/ui/display.rs @@ -1,5 +1,5 @@ use crate::emulator::{ - system::model::State, + system::model::{Command, State}, ui::interface::{Category, Component}, }; @@ -40,6 +40,8 @@ impl Component for Display { } fn render(&mut self, state: &mut State, ui: &mut egui::Ui, _ctx: &egui::Context) { + state.send(Command::DisplayRequest); + let display: Vec = state.display_view.clone(); let font_id = FontId::monospace(12.0); diff --git a/emulator/src/emulator/ui/editor.rs b/emulator/src/emulator/ui/editor.rs index 433ef83..bbf6840 100644 --- a/emulator/src/emulator/ui/editor.rs +++ b/emulator/src/emulator/ui/editor.rs @@ -3,7 +3,6 @@ use std::{ ffi::OsStr, fs, path::{Path, PathBuf}, - sync::mpsc::Sender, }; use common::prelude::Instruction; @@ -19,6 +18,7 @@ use crate::emulator::{ use assembler::prelude::*; +#[derive(Default)] pub struct Editor { // editor state path: Option, @@ -41,7 +41,6 @@ pub struct Editor { // other visible: bool, - sender: Sender, error: Option, } @@ -94,14 +93,13 @@ impl Component for Editor { impl Editor { #[must_use] - pub const fn new(sender: Sender) -> Self { + pub const fn new() -> Self { Self { path: None, text: String::new(), buffer: String::new(), output: Vec::new(), unsaved: true, - sender, cursor_col: 1, cursor_line: 1, visible: false, @@ -199,38 +197,6 @@ impl Editor { ) }); - // if let Some(path) = FileDialog::new() - // .add_filter("Assembly Files or Binaries", &["dsa", "dsb"]) - // .add_filter("all", &["*"]) - // .set_directory(&work_dir) - // .pick_file() - // { - // match path.extension().and_then(|ext| ext.to_str()) { - // Some("dsb") => { - // let contents = match std::fs::read(&path) { - // Ok(contents) => contents, - // Err(why) => { - // self.error = Some(format!("Failed to read file: {why}")); - // return; - // } - // }; - - // self.path = Some(path.clone()); - // self.output = contents; - // self.unsaved = false; - // self.text = String::from("Loaded Binary File!"); - // self.buffer = self.text.clone(); - // self.unsaved = false; - // } - // _ => { - // if let Ok(contents) = std::fs::read_to_string(&path) { - // self.path = Some(path.clone()); - // self.text.clone_from(&contents); - // self.buffer = contents; - // self.unsaved = false; - // } - // } - // } if self.save_file_dialog.is_some() { // TODO: Flash an error stating you can only have one menu open at once. self.save_file_dialog = None; @@ -338,33 +304,6 @@ impl Editor { } } - // fn open(&mut self) { - // let work_dir = std::env::current_dir().unwrap_or_else(|_| { - // dirs::home_dir().expect( - // "Couldn't get your current working directory or your home directory.", - // ) - // }); - - // if let Some(path) = FileDialog::new() - // .add_filter("Assembly Files or Binaries", &["dsa", "dsb"]) - // .add_filter("all", &["*"]) - // .set_directory(&work_dir) - // .pick_file() - // { - // if let Ok(contents) = std::fs::read_to_string(&path) { - // self.path = Some(path.clone()); - // self.text.clone_from(&contents); - // self.buffer = contents; - // self.unsaved = false; - // } - - // std::env::set_current_dir( - // path.parent().expect("A file should be in a directory!"), - // ) - // .expect("ERROR: Failed to set current working directory."); - // } - // } - fn render_output(&self, _state: &mut State, ui: &mut Ui, _ctx: &Context) { // Output area with synchronized scrolling egui::ScrollArea::vertical() @@ -526,7 +465,7 @@ impl Editor { } } - fn render_toolbar(&mut self, _state: &mut State, ui: &mut Ui, ctx: &Context) { + fn render_toolbar(&mut self, state: &State, ui: &mut Ui, ctx: &Context) { self.handle_file_dialogs(ctx); ui.horizontal(|ui| { @@ -567,7 +506,8 @@ impl Editor { Some("Can't load program at invalid offset!".to_string()); } - self.sender + state + .cmd_sender .send(Command::Write(self.load_offset, self.output.clone())) .unwrap_or_else(|_| { self.error = Some("Failed to send command".to_string()); diff --git a/emulator/src/emulator/ui/history.rs b/emulator/src/emulator/ui/history.rs index 342282f..64be793 100644 --- a/emulator/src/emulator/ui/history.rs +++ b/emulator/src/emulator/ui/history.rs @@ -1,6 +1,9 @@ use egui::{Context, Ui}; -use crate::emulator::{system::model::State, ui::interface::Component}; +use crate::emulator::{ + system::model::{Command, State}, + ui::interface::Component, +}; pub struct History { visible: bool, @@ -20,11 +23,13 @@ impl Component for History { } fn render(&mut self, state: &mut State, ui: &mut Ui, _ctx: &Context) { + state.send(Command::HistoryRequest); + egui::ScrollArea::vertical() .id_salt("output_scroll") .max_width(400.0) .show(ui, |ui| { - if state.persistent.history.is_empty() { + if state.instruction_history.is_empty() { ui.label( egui::RichText::new("No output data") .font(egui::FontId::monospace(12.0)) @@ -40,7 +45,7 @@ impl Component for History { .show(ui, |ui| { // Process bytes in chunks of 4 for (idx, instruction) in - state.persistent.history.iter().enumerate() + state.instruction_history.iter().enumerate() { ui.label(format!("{idx}: ")); diff --git a/emulator/src/emulator/ui/interface.rs b/emulator/src/emulator/ui/interface.rs index db10a0c..f5b85c9 100644 --- a/emulator/src/emulator/ui/interface.rs +++ b/emulator/src/emulator/ui/interface.rs @@ -1,4 +1,4 @@ -use crate::emulator::system::model::{Command, PersistentState, Running, State}; +use crate::emulator::system::model::{Command, Running, State, StateUpdate}; use std::sync::mpsc::{Receiver, Sender}; pub trait Component { @@ -34,21 +34,15 @@ impl Category { } pub struct EmulatorUI { - pub sender: Sender, - pub receiver: Receiver, pub state: State, - pub persistent: PersistentState, pub components: Vec>, } impl EmulatorUI { #[must_use] - pub fn new(sender: Sender, receiver: Receiver) -> Self { + pub fn new(sender: Sender, receiver: Receiver) -> Self { Self { - sender, - receiver, - state: State::default(), - persistent: PersistentState::default(), + state: State::new(sender, receiver), components: vec![], } } @@ -56,19 +50,13 @@ impl EmulatorUI { pub fn add_component(&mut self, component: Box) { self.components.push(component); } - - fn update_state(&mut self) { - while let Ok(state) = self.receiver.try_recv() { - self.state = state; - self.persistent.update(&self.state.persistent); - self.state.persistent = self.persistent.clone(); - } - } } impl eframe::App for EmulatorUI { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - self.update_state(); + if let Err(e) = self.state.update() { + self.state.error_log.push(e.to_string()); + } if self.state.running == Running::Running { ctx.request_repaint(); diff --git a/emulator/src/emulator/ui/loader.rs b/emulator/src/emulator/ui/loader.rs new file mode 100644 index 0000000..191b8f1 --- /dev/null +++ b/emulator/src/emulator/ui/loader.rs @@ -0,0 +1,294 @@ +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, +}; + +use common::prelude::Instruction; +use egui::{Context, Ui}; +use egui_file::FileDialog; + +use crate::emulator::{ + system::model::{Command, State}, + ui::interface::Component, +}; + +#[derive(Default)] +pub struct Loader { + path: Option, + output: Vec, + load_offset: u32, + offset_str: String, + + // file dialogs + open_file_dialog: Option, + + // other + visible: bool, + error: Option, +} + +impl Component for Loader { + fn name(&self) -> &'static str { + "Loader" + } + + fn visible(&mut self) -> &mut bool { + &mut self.visible + } + + fn category(&self) -> super::interface::Category { + super::interface::Category::Programming + } + + fn render(&mut self, state: &mut State, ui: &mut Ui, ctx: &Context) { + ui.vertical(|ui| { + self.render_toolbar(state, ui, ctx); + + ui.add_space(4.0); // Add some spacing instead of just a separator + ui.separator(); + + egui::ScrollArea::vertical() + .auto_shrink([false; 2]) + .max_height(ui.available_height() - 100.0) + .show(ui, |ui| { + self.render_output(state, ui, ctx); + }); + + self.render_bottom_bar(state, ui, ctx); + }); + } +} + +impl Loader { + #[must_use] + pub const fn new() -> Self { + Self { + path: None, + output: Vec::new(), + visible: false, + load_offset: 0, + offset_str: String::new(), + error: None, + open_file_dialog: None, + } + } + + fn filename(&self) -> &str { + if let Some(path) = &self.path { + return path + .file_name() + .unwrap_or_else(|| OsStr::new("Unnamed!")) + .to_str() + .map_or_else( + || unreachable!("File name should be valid UTF-8."), + |ext| ext, + ); + } + "Unnamed!" + } + + fn open(&mut self) { + let work_dir = std::env::current_dir().unwrap_or_else(|_| { + dirs::home_dir().expect( + "Couldn't get your current working directory or your home directory.", + ) + }); + + if self.open_file_dialog.is_some() { + // TODO: Flash an error stating you can only have one menu open at once. + self.open_file_dialog = None; + } + + if self.open_file_dialog.is_none() { + if let Some(p) = &self.path { + let path = p.parent().map(Path::to_path_buf); + let mut dialog = FileDialog::open_file(path); + dialog.open(); + self.open_file_dialog = Some(dialog); + } else { + let mut dialog = FileDialog::open_file(Some(work_dir)); + dialog.open(); + self.open_file_dialog = Some(dialog); + } + } + } + + fn handle_file_dialogs(&mut self, ctx: &egui::Context) { + // Handle open dialog + if let Some(dialog) = &mut self.open_file_dialog { + if dialog.show(ctx).selected() { + if let Some(file) = dialog.path() { + // check if the file is a binary file + if file.extension().is_some_and(|ext| ext == "dsb") { + match std::fs::read(file) { + Ok(content) => { + self.output = content; + self.error = None; + } + Err(e) => { + self.error = Some(format!("Failed to read file: {e}")); + } + } + } + } + self.open_file_dialog = None; + } + } + } + + fn render_output(&self, _state: &mut State, ui: &mut Ui, _ctx: &Context) { + // Output area with synchronized scrolling + egui::ScrollArea::vertical() + .id_salt("output_scroll") + .max_width(400.0) + .show(ui, |ui| { + if self.output.is_empty() { + ui.label( + egui::RichText::new("No output data") + .font(egui::FontId::monospace(12.0)) + .color(egui::Color32::GRAY), + ); + return; + } + + egui::Grid::new("output_grid") + .spacing([5.0, 2.0]) // Horizontal and vertical spacing + .num_columns(4) + .striped(false) + .show(ui, |ui| { + // Process bytes in chunks of 4 + for (line_num, chunk) in self.output.chunks(4).enumerate() { + let address = line_num * 4; + + // Convert chunk to u32 (little-endian) + let mut bytes = [0u8; 4]; + for (i, &byte) in chunk.iter().enumerate() { + if i < 4 { + bytes[i] = byte; + } + } + let value = u32::from_be_bytes(bytes); + + // Address column + ui.with_layout( + egui::Layout::left_to_right(egui::Align::Center), + |ui| { + ui.set_min_width(80.0); + let style = ui.style_mut(); + style.visuals.widgets.inactive.bg_fill = + egui::Color32::from_gray(30); + ui.label( + egui::RichText::new(format!("0x{address:04X}")) + .font(egui::FontId::monospace(12.0)), + ); + }, + ); + + // Individual bytes column + let byte_str = chunk + .iter() + .map(|b| format!("{b:02X}")) + .collect::>() + .join(" "); + + ui.label( + egui::RichText::new(format!("{byte_str:<11}")) + .font(egui::FontId::monospace(12.0)) + .color(egui::Color32::from_rgb(200, 200, 255)), + ); + + // Hex column + ui.label( + egui::RichText::new(format!("0x{value:08X}")) + .font(egui::FontId::monospace(12.0)) + .color(egui::Color32::from_rgb(255, 200, 200)), + ); + + // Instruction column + let instruction = Instruction::decode(value).map_or_else( + |_| format!("{value:10}"), + |instruction| instruction.to_string(), + ); + + ui.label( + egui::RichText::new(instruction) + .font(egui::FontId::monospace(12.0)) + .color(egui::Color32::from_rgb(200, 255, 200)), + ); + + ui.end_row(); + } + }); + }); + } + + fn render_bottom_bar(&self, _state: &mut State, ui: &mut Ui, _ctx: &Context) { + ui.horizontal(|ui| { + // error display + ui.label( + egui::RichText::new(self.error.clone().unwrap_or_default()) + .color(egui::Color32::RED), + ); + }); + } + + fn render_toolbar(&mut self, state: &State, ui: &mut Ui, ctx: &Context) { + self.handle_file_dialogs(ctx); + + ui.horizontal(|ui| { + ui.label(format!("Filename: {}", self.filename())); + }); + + 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 + if ui.button("Open").clicked() { + self.open(); + } + + // Loads the generated binary into the assembler at the provided offset + if ui.button("Load").clicked() { + if self.error.is_some() { + self.error = + Some("Can't load program at invalid offset!".to_string()); + } + + state + .cmd_sender + .send(Command::Write(self.load_offset, self.output.clone())) + .unwrap_or_else(|_| { + self.error = Some("Failed to send command".to_string()); + }); + } + + // Entry widget to enter a load offset + if ui.text_edit_singleline(&mut self.offset_str).changed() { + if let Some(offset) = parse_address(&self.offset_str) { + self.load_offset = offset; + self.error = None; + } else { + self.error = Some("Invalid offset".to_string()); + } + } + }); + } +} + +fn parse_address(address: &str) -> Option { + address.strip_prefix("0x").map_or_else( + || { + address.strip_prefix("0b").map_or_else( + || { + address.strip_prefix("0o").map_or_else( + || address.parse::().ok(), + |oct| u32::from_str_radix(oct, 8).ok(), + ) + }, + |bin| u32::from_str_radix(bin, 2).ok(), + ) + }, + |hex| u32::from_str_radix(hex, 16).ok(), + ) +} diff --git a/emulator/src/emulator/ui/memory_inspector.rs b/emulator/src/emulator/ui/memory_inspector.rs index cfd9263..d6206d1 100644 --- a/emulator/src/emulator/ui/memory_inspector.rs +++ b/emulator/src/emulator/ui/memory_inspector.rs @@ -1,4 +1,4 @@ -use std::{num::ParseIntError, sync::mpsc::Sender}; +use std::num::ParseIntError; use common::prelude::Instruction; @@ -7,23 +7,22 @@ use crate::emulator::{ ui::interface::Component, }; +#[derive(Default)] pub struct MemoryInspector { view_size: u32, view_addr: u32, visible: bool, addr_input: String, - sender: Sender, } impl MemoryInspector { #[must_use] - pub const fn new(sender: Sender) -> Self { + pub const fn new() -> Self { Self { view_size: 256, view_addr: 0, visible: false, addr_input: String::new(), - sender, } } } @@ -63,28 +62,26 @@ impl Component for MemoryInspector { let search_clicked = ui.button("🔍 Search").clicked(); // Handle Enter key in text field - let enter_pressed = - address_response.lost_focus() && ctx.input(|i| i.key_pressed(egui::Key::Enter)); + let enter_pressed = address_response.lost_focus() + && ctx.input(|i| i.key_pressed(egui::Key::Enter)); if search_clicked || enter_pressed { if let Ok(new) = parse_address(&self.addr_input) { self.view_addr = new; - - if let Err(why) = self.sender.send(Command::Read(new, self.view_size)) { - panic!( - "Error sending message across threads -- cannot be recovered: {why}" - ) - } } else { - state.error = Some("Invalid address".to_string()); + state.error_log.push("Invalid address".to_string()); } } + let _ = state + .cmd_sender + .send(Command::MemRequest(self.view_addr, self.view_size)); + ui.label("(hex or decimal)"); }); // Show input error if any - if let Some(error) = &state.error { + if let Some(error) = state.error_log.last() { ui.colored_label(egui::Color32::RED, format!("Error: {error}")); } @@ -113,9 +110,12 @@ impl Component for MemoryInspector { ui.end_row(); // Memory data (8 bytes per row) - for (row, chunk) in (0u32..).zip(state.memory_view.chunks(4)) { + for (row, chunk) in (0u32..).zip(state.memory_view.chunks(4)) + { let row_address = self.view_addr + (row * 4); - ui.monospace(format!("0x{row_address:08X} ({row_address})")); + ui.monospace(format!( + "0x{row_address:08X} ({row_address})" + )); for &byte in chunk { ui.monospace(format!("{byte:02X}")); } @@ -126,12 +126,16 @@ impl Component for MemoryInspector { } // combine all 4 bytes in the chunk into a u32 - let combined = chunk - .iter() - .fold(0u32, |acc, &byte| (acc << 8) | u32::from(byte)); + let combined = chunk.iter().fold(0u32, |acc, &byte| { + (acc << 8) | u32::from(byte) + }); ui.monospace(format!("{combined}")); - ui.monospace(format!("{}", Instruction::decode(combined).unwrap_or(Instruction::Nop))); + ui.monospace(format!( + "{}", + Instruction::decode(combined) + .unwrap_or(Instruction::Nop) + )); ui.end_row(); } diff --git a/emulator/src/emulator/ui/mod.rs b/emulator/src/emulator/ui/mod.rs index a4790e1..9e0cf7e 100644 --- a/emulator/src/emulator/ui/mod.rs +++ b/emulator/src/emulator/ui/mod.rs @@ -3,6 +3,7 @@ pub mod display; pub mod editor; pub mod history; pub mod interface; +pub mod loader; pub mod memory_inspector; pub mod menu; pub mod stack_inspector; diff --git a/emulator/src/emulator/ui/stack_inspector.rs b/emulator/src/emulator/ui/stack_inspector.rs index bd5ccfb..e15ac5a 100644 --- a/emulator/src/emulator/ui/stack_inspector.rs +++ b/emulator/src/emulator/ui/stack_inspector.rs @@ -1,4 +1,7 @@ -use crate::emulator::{system::model::State, ui::interface::Component}; +use crate::emulator::{ + system::model::{Command, State}, + ui::interface::Component, +}; use common::instructions::Register; @@ -33,6 +36,8 @@ impl Component for StackInspector { } fn render(&mut self, state: &mut State, ui: &mut egui::Ui, _ctx: &egui::Context) { + state.send(Command::StackRequest); + ui.vertical(|ui| { ui.heading("Stack Inspector"); egui::ScrollArea::vertical() diff --git a/emulator/src/lib.rs b/emulator/src/lib.rs index 19558c4..32ce805 100644 --- a/emulator/src/lib.rs +++ b/emulator/src/lib.rs @@ -30,7 +30,7 @@ use crate::emulator::{ system::{ emulator::run_emulator, memory::MainStore, - model::{Command, State}, + model::{Command, StateUpdate}, processor::Processor, }, ui::{ @@ -86,7 +86,7 @@ pub fn android_main(app: AndroidApp) -> Result<(), Box> { pub fn setup_emulator( cmd_receiver: Receiver, - state_sender: Sender, + state_sender: Sender, rpc_client: Option>, ) { let main_store = MainStore::new(); @@ -101,22 +101,22 @@ pub fn setup_emulator( #[must_use] pub fn setup_ui( cmd_sender: Sender, - state_reciever: Receiver, + state_reciever: Receiver, ) -> EmulatorUI { - let mut ui = EmulatorUI::new(cmd_sender.clone(), state_reciever); + let mut ui = EmulatorUI::new(cmd_sender, state_reciever); // Create UI modules. - let control_unit = ControlPanel::new(cmd_sender.clone()); + let control_unit = ControlPanel::new(); ui.add_component(Box::new(control_unit)); - let mem_inspector = MemoryInspector::new(cmd_sender.clone()); + let mem_inspector = MemoryInspector::new(); ui.add_component(Box::new(mem_inspector)); let stack_inspector = StackInspector::new(); ui.add_component(Box::new(stack_inspector)); - let editor = Editor::new(cmd_sender); + let editor = Editor::new(); ui.add_component(Box::new(editor)); let display = Display::new(); @@ -125,5 +125,8 @@ pub fn setup_ui( let history = emulator::ui::history::History::new(); ui.add_component(Box::new(history)); + let loader = emulator::ui::loader::Loader::new(); + ui.add_component(Box::new(loader)); + ui }