finished refactor of emulator - started on loader (needs significant changes before functional in the way that I would like)

This commit is contained in:
2025-06-23 23:45:47 +01:00
parent bc5ddef311
commit 76197fac8f
15 changed files with 604 additions and 289 deletions
Binary file not shown.
+11 -5
View File
@@ -3,6 +3,8 @@ use crate::{instructions::encode::Encode, prelude::*};
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Interrupt { pub enum Interrupt {
Software(u8), Software(u8),
Breakpoint,
HardFault,
} }
pub type Address = u32; pub type Address = u32;
@@ -10,6 +12,8 @@ pub type Address = u32;
impl Interrupt { impl Interrupt {
const fn as_u8(self) -> u8 { const fn as_u8(self) -> u8 {
match self { match self {
Self::Breakpoint => 0,
Self::HardFault => 1,
Self::Software(code) => code, Self::Software(code) => code,
} }
} }
@@ -19,10 +23,11 @@ impl Interrupt {
impl From<u8> for Interrupt { impl From<u8> for Interrupt {
#[allow(unreachable_code)] #[allow(unreachable_code)]
fn from(code: u8) -> Self { fn from(code: u8) -> Self {
return Self::Software(code); match code {
todo!("Implement this once a hardware interrupt convention is established."); 0 => Self::Breakpoint,
1 => Self::HardFault,
// Self::Software(_code) _ => Self::Software(code),
}
} }
} }
@@ -73,7 +78,8 @@ pub enum Register {
} }
impl 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<Self> { pub fn general() -> Vec<Self> {
vec![ vec![
Self::Rg0, Self::Rg0,
+88 -94
View File
@@ -8,8 +8,9 @@ use std::{
#[allow(unused_imports)] #[allow(unused_imports)]
use crate::emulator::misc::rpc::{Activity, RpcClient}; use crate::emulator::misc::rpc::{Activity, RpcClient};
use crate::emulator::system::model::StateUpdate;
use crate::emulator::system::{ use crate::emulator::system::{
model::{Command, PersistentState, Running, State}, model::{Command, Running},
processor::Processor, processor::Processor,
}; };
@@ -19,32 +20,32 @@ use common::prelude::*;
#[allow(unused_variables)] #[allow(unused_variables)]
pub fn run_emulator( pub fn run_emulator(
cmd_rx: &Receiver<Command>, cmd_rx: &Receiver<Command>,
state_tx: &Sender<State>, state_tx: &Sender<StateUpdate>,
mut processor: Processor, mut processor: Processor,
rpc_client: Option<&Arc<RpcClient>>, rpc_client: Option<&Arc<RpcClient>>,
) { ) {
println!("INFO: Starting emulator."); println!("INFO: Starting emulator.");
let mut running = Running::Paused; let mut running = Running::Paused;
let mut addr = 0u32; let mut addr;
let mut history = Vec::<(u32, Instruction)>::new(); let mut history = Vec::<(u32, Instruction)>::new();
let size = 256; let size = 256;
let memory_view = processor state_tx
.memory .send(StateUpdate::Running(Running::Paused))
.read_range(addr, size) .expect("Failed to send initial state!");
.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);
let mut instruction_count = 0; let mut instruction_count = 0;
let mut update = false;
loop { loop {
let cmd = if running == Running::Running { let cmd = if running == Running::Running {
match cmd_rx.try_recv() { match cmd_rx.try_recv() {
Ok(cmd) => Some(cmd), Ok(cmd) => Some(cmd),
Err(mpsc::TryRecvError::Empty) => None, Err(mpsc::TryRecvError::Empty) => {
update = false;
None
}
Err(mpsc::TryRecvError::Disconnected) => break, Err(mpsc::TryRecvError::Disconnected) => break,
} }
} else { } else {
@@ -96,6 +97,8 @@ pub fn run_emulator(
processor.reset(); processor.reset();
} }
Command::Step => { Command::Step => {
update = true;
running = Running::Paused; running = Running::Paused;
// Execute one cycle. // Execute one cycle.
@@ -114,42 +117,91 @@ pub fn run_emulator(
instruction_count += 1; instruction_count += 1;
} }
Command::Read(new, _size) => {
addr = new;
}
Command::Write(offset, data) => { Command::Write(offset, data) => {
update = true;
processor processor
.memory .memory
.write_range(offset, data) .write_range(offset, data)
.unwrap_or_else(|_| { .unwrap_or_else(|_| {
eprintln!("Failed to write memory range!"); report_err(
processor.begin_interrupt(Interrupt::HardFault); state_tx,
"Failed to write memory range!",
&mut processor,
);
}); });
} }
Command::Interrupt(_interrupt) => { Command::Interrupt(_interrupt) => {
update = true;
todo!("implement interrupts") 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 { if running == Running::Running {
let mut update = false; update = true;
// Execute one cycle. // Execute one cycle.
let instruction = match processor.cycle() { let instruction = match processor.cycle() {
@@ -162,74 +214,16 @@ pub fn run_emulator(
}; };
history.push(instruction); history.push(instruction);
// let instruction = match Instruction::decode(cpu_lock.get(Register::Cir))
// {};
if matches!(instruction.1, Instruction::Halt) { if matches!(instruction.1, Instruction::Halt) {
running = Running::Halted; running = Running::Halted;
update = true;
} }
instruction_count += 1; 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( fn report_err(state_tx: &Sender<StateUpdate>, why: &str, processor: &mut Processor) {
processor: &mut Processor, processor.begin_interrupt(Interrupt::HardFault);
running: Running, let _ = state_tx.send(StateUpdate::Error(why.to_string()));
instruction_count: usize,
memory_view: Vec<u8>,
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 },
}
} }
+4 -4
View File
@@ -26,15 +26,15 @@ pub trait MemoryUnit: Send + Sync {
fn read_block(&mut self, addr: u32) -> Result<[u8; 256], ProcessorError> { fn read_block(&mut self, addr: u32) -> Result<[u8; 256], ProcessorError> {
let mut data = [0; 256]; let mut data = [0; 256];
for i in 0..256 { for (i, byte) in data.iter_mut().enumerate() {
data[i] = self.read_byte(addr + i as u32)?; *byte = self.read_byte(addr + i as u32)?;
} }
Ok(data) Ok(data)
} }
fn write_block(&mut self, addr: u32, data: [u8; 256]) -> Result<(), ProcessorError> { fn write_block(&mut self, addr: u32, data: [u8; 256]) -> Result<(), ProcessorError> {
for i in 0..256 { for (i, byte) in data.iter().enumerate() {
self.write_byte(addr + i as u32, data[i])?; self.write_byte(addr + i as u32, *byte)?;
} }
Ok(()) Ok(())
} }
+108 -47
View File
@@ -1,3 +1,5 @@
use std::sync::mpsc::{self, Receiver, Sender};
use common::prelude::*; use common::prelude::*;
#[derive(PartialEq, Eq, Debug, Clone, Copy)] #[derive(PartialEq, Eq, Debug, Clone, Copy)]
@@ -16,15 +18,23 @@ pub trait IODevice: Send + Sync {
#[derive(PartialEq, Eq, Debug, Clone)] #[derive(PartialEq, Eq, Debug, Clone)]
pub enum Command { pub enum Command {
// set emulator state.
Start, Start,
Stop, Stop,
Step, Step,
Reset(usize), Reset(usize),
Interrupt(Interrupt), Interrupt(Interrupt),
// Performs direct read/write operations on the emulator's memory.
Read(Address, u32),
Write(Address, Vec<u8>), Write(Address, Vec<u8>),
WriteBlock(Address, Box<[u8; 256]>),
// request emulator state.
MemRequest(Address, u32),
DisplayRequest,
StackRequest,
RegisterRequest,
RunningRequest,
HistoryRequest,
InstructionCountRequest,
} }
#[derive(Debug)] #[derive(Debug)]
@@ -52,6 +62,101 @@ impl std::fmt::Display for ProcessorError {
} }
} }
pub struct State {
pub state_receiver: Receiver<StateUpdate>,
pub cmd_sender: Sender<Command>,
// Processor state
pub reg_file: RegFile,
pub running: Running,
pub instructions: usize,
// Memory access views
pub stack_view: Vec<u8>,
pub memory_view: Vec<u8>,
pub display_view: Vec<u8>,
pub error_log: Vec<String>,
pub instruction_history: Vec<(u32, Instruction)>,
}
impl State {
#[must_use]
pub fn new(sender: Sender<Command>, receiver: Receiver<StateUpdate>) -> 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<u8>),
MemoryView(Vec<u8>),
DisplayView(Vec<u8>),
Error(String),
InstructionHistory(Vec<(u32, Instruction)>),
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub struct RegFile { pub struct RegFile {
// General Purpose Registers // 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<u8>,
pub memory_view: Vec<u8>,
pub display_view: Vec<u8>,
pub error: Option<String>,
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);
}
}
}
+36 -24
View File
@@ -1,5 +1,3 @@
use std::sync::mpsc::Sender;
use crate::emulator::{ use crate::emulator::{
system::model::{Command, Running, State}, system::model::{Command, Running, State},
ui::interface::Component, ui::interface::Component,
@@ -9,16 +7,18 @@ use common::{instructions::Register, prelude::Instruction};
pub struct ControlPanel { pub struct ControlPanel {
visible: bool, visible: bool,
sender: Sender<Command>,
} }
impl ControlPanel { impl ControlPanel {
#[must_use] #[allow(clippy::must_use_candidate)]
pub const fn new(sender: Sender<Command>) -> Self { pub const fn new() -> Self {
Self { Self { visible: false }
visible: false, }
sender, }
}
impl Default for ControlPanel {
fn default() -> Self {
Self::new()
} }
} }
@@ -47,46 +47,58 @@ impl Component for ControlPanel {
.clicked() .clicked()
{ {
if state.running == Running::Running { if state.running == Running::Running {
self.sender.send(Command::Stop).unwrap_or_else(|_| { state.cmd_sender.send(Command::Stop).unwrap_or_else(|_| {
state.error = Some("Failed to send command".to_string()); state.error_log.push("Failed to send command".to_string());
}); });
} else { } else {
self.sender.send(Command::Start).unwrap_or_else(|_| { state.cmd_sender.send(Command::Start).unwrap_or_else(|_| {
state.error = Some("Failed to send command".to_string()); state.error_log.push("Failed to send command".to_string());
}); });
} }
} }
// Step // Step
if ui.button("Step").clicked() { if ui.button("Step").clicked() {
self.sender.send(Command::Step).unwrap_or_else(|_| { state.cmd_sender.send(Command::Step).unwrap_or_else(|_| {
state.error = Some("Failed to send command".to_string()); state.error_log.push("Failed to send command".to_string());
}); });
} }
// Resets the emulator and all attached devices // Resets the emulator and all attached devices
if ui.button("Reset All").clicked() { if ui.button("Reset All").clicked() {
self.sender.send(Command::Reset(0)).unwrap_or_else(|_| { state
state.error = Some("Failed to send command".to_string()); .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 // Resets the emulator and all attached devices
if ui.button("Clear Registers").clicked() { if ui.button("Clear Registers").clicked() {
self.sender.send(Command::Reset(1)).unwrap_or_else(|_| { state
state.error = Some("Failed to send command".to_string()); .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 // Resets the emulator and all attached devices
if ui.button("Clear RAM").clicked() { if ui.button("Clear RAM").clicked() {
self.sender.send(Command::Reset(2)).unwrap_or_else(|_| { state
state.error = Some("Failed to send command".to_string()); .cmd_sender
}); .send(Command::Reset(2))
.unwrap_or_else(|_| {
state.error_log.push("Failed to send command".to_string());
});
} }
ui.separator(); ui.separator();
state.send(Command::RegisterRequest);
state.send(Command::RunningRequest);
// Status info // Status info
ui.label(format!( ui.label(format!(
"Status: {}", "Status: {}",
+3 -1
View File
@@ -1,5 +1,5 @@
use crate::emulator::{ use crate::emulator::{
system::model::State, system::model::{Command, State},
ui::interface::{Category, Component}, 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) { fn render(&mut self, state: &mut State, ui: &mut egui::Ui, _ctx: &egui::Context) {
state.send(Command::DisplayRequest);
let display: Vec<u8> = state.display_view.clone(); let display: Vec<u8> = state.display_view.clone();
let font_id = FontId::monospace(12.0); let font_id = FontId::monospace(12.0);
+5 -65
View File
@@ -3,7 +3,6 @@ use std::{
ffi::OsStr, ffi::OsStr,
fs, fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::mpsc::Sender,
}; };
use common::prelude::Instruction; use common::prelude::Instruction;
@@ -19,6 +18,7 @@ use crate::emulator::{
use assembler::prelude::*; use assembler::prelude::*;
#[derive(Default)]
pub struct Editor { pub struct Editor {
// editor state // editor state
path: Option<PathBuf>, path: Option<PathBuf>,
@@ -41,7 +41,6 @@ pub struct Editor {
// other // other
visible: bool, visible: bool,
sender: Sender<Command>,
error: Option<String>, error: Option<String>,
} }
@@ -94,14 +93,13 @@ impl Component for Editor {
impl Editor { impl Editor {
#[must_use] #[must_use]
pub const fn new(sender: Sender<Command>) -> Self { pub const fn new() -> Self {
Self { Self {
path: None, path: None,
text: String::new(), text: String::new(),
buffer: String::new(), buffer: String::new(),
output: Vec::new(), output: Vec::new(),
unsaved: true, unsaved: true,
sender,
cursor_col: 1, cursor_col: 1,
cursor_line: 1, cursor_line: 1,
visible: false, 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() { if self.save_file_dialog.is_some() {
// TODO: Flash an error stating you can only have one menu open at once. // TODO: Flash an error stating you can only have one menu open at once.
self.save_file_dialog = None; 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) { fn render_output(&self, _state: &mut State, ui: &mut Ui, _ctx: &Context) {
// Output area with synchronized scrolling // Output area with synchronized scrolling
egui::ScrollArea::vertical() 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); self.handle_file_dialogs(ctx);
ui.horizontal(|ui| { ui.horizontal(|ui| {
@@ -567,7 +506,8 @@ impl Editor {
Some("Can't load program at invalid offset!".to_string()); Some("Can't load program at invalid offset!".to_string());
} }
self.sender state
.cmd_sender
.send(Command::Write(self.load_offset, self.output.clone())) .send(Command::Write(self.load_offset, self.output.clone()))
.unwrap_or_else(|_| { .unwrap_or_else(|_| {
self.error = Some("Failed to send command".to_string()); self.error = Some("Failed to send command".to_string());
+8 -3
View File
@@ -1,6 +1,9 @@
use egui::{Context, Ui}; 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 { pub struct History {
visible: bool, visible: bool,
@@ -20,11 +23,13 @@ impl Component for History {
} }
fn render(&mut self, state: &mut State, ui: &mut Ui, _ctx: &Context) { fn render(&mut self, state: &mut State, ui: &mut Ui, _ctx: &Context) {
state.send(Command::HistoryRequest);
egui::ScrollArea::vertical() egui::ScrollArea::vertical()
.id_salt("output_scroll") .id_salt("output_scroll")
.max_width(400.0) .max_width(400.0)
.show(ui, |ui| { .show(ui, |ui| {
if state.persistent.history.is_empty() { if state.instruction_history.is_empty() {
ui.label( ui.label(
egui::RichText::new("No output data") egui::RichText::new("No output data")
.font(egui::FontId::monospace(12.0)) .font(egui::FontId::monospace(12.0))
@@ -40,7 +45,7 @@ impl Component for History {
.show(ui, |ui| { .show(ui, |ui| {
// Process bytes in chunks of 4 // Process bytes in chunks of 4
for (idx, instruction) in for (idx, instruction) in
state.persistent.history.iter().enumerate() state.instruction_history.iter().enumerate()
{ {
ui.label(format!("{idx}: ")); ui.label(format!("{idx}: "));
+6 -18
View File
@@ -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}; use std::sync::mpsc::{Receiver, Sender};
pub trait Component { pub trait Component {
@@ -34,21 +34,15 @@ impl Category {
} }
pub struct EmulatorUI { pub struct EmulatorUI {
pub sender: Sender<Command>,
pub receiver: Receiver<State>,
pub state: State, pub state: State,
pub persistent: PersistentState,
pub components: Vec<Box<dyn Component>>, pub components: Vec<Box<dyn Component>>,
} }
impl EmulatorUI { impl EmulatorUI {
#[must_use] #[must_use]
pub fn new(sender: Sender<Command>, receiver: Receiver<State>) -> Self { pub fn new(sender: Sender<Command>, receiver: Receiver<StateUpdate>) -> Self {
Self { Self {
sender, state: State::new(sender, receiver),
receiver,
state: State::default(),
persistent: PersistentState::default(),
components: vec![], components: vec![],
} }
} }
@@ -56,19 +50,13 @@ impl EmulatorUI {
pub fn add_component(&mut self, component: Box<dyn Component>) { pub fn add_component(&mut self, component: Box<dyn Component>) {
self.components.push(component); 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 { impl eframe::App for EmulatorUI {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 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 { if self.state.running == Running::Running {
ctx.request_repaint(); ctx.request_repaint();
+294
View File
@@ -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<PathBuf>,
output: Vec<u8>,
load_offset: u32,
offset_str: String,
// file dialogs
open_file_dialog: Option<FileDialog>,
// other
visible: bool,
error: Option<String>,
}
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::<Vec<_>>()
.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<u32> {
address.strip_prefix("0x").map_or_else(
|| {
address.strip_prefix("0b").map_or_else(
|| {
address.strip_prefix("0o").map_or_else(
|| address.parse::<u32>().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(),
)
}
+24 -20
View File
@@ -1,4 +1,4 @@
use std::{num::ParseIntError, sync::mpsc::Sender}; use std::num::ParseIntError;
use common::prelude::Instruction; use common::prelude::Instruction;
@@ -7,23 +7,22 @@ use crate::emulator::{
ui::interface::Component, ui::interface::Component,
}; };
#[derive(Default)]
pub struct MemoryInspector { pub struct MemoryInspector {
view_size: u32, view_size: u32,
view_addr: u32, view_addr: u32,
visible: bool, visible: bool,
addr_input: String, addr_input: String,
sender: Sender<Command>,
} }
impl MemoryInspector { impl MemoryInspector {
#[must_use] #[must_use]
pub const fn new(sender: Sender<Command>) -> Self { pub const fn new() -> Self {
Self { Self {
view_size: 256, view_size: 256,
view_addr: 0, view_addr: 0,
visible: false, visible: false,
addr_input: String::new(), addr_input: String::new(),
sender,
} }
} }
} }
@@ -63,28 +62,26 @@ impl Component for MemoryInspector {
let search_clicked = ui.button("🔍 Search").clicked(); let search_clicked = ui.button("🔍 Search").clicked();
// Handle Enter key in text field // Handle Enter key in text field
let enter_pressed = let enter_pressed = address_response.lost_focus()
address_response.lost_focus() && ctx.input(|i| i.key_pressed(egui::Key::Enter)); && ctx.input(|i| i.key_pressed(egui::Key::Enter));
if search_clicked || enter_pressed { if search_clicked || enter_pressed {
if let Ok(new) = parse_address(&self.addr_input) { if let Ok(new) = parse_address(&self.addr_input) {
self.view_addr = new; 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 { } 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)"); ui.label("(hex or decimal)");
}); });
// Show input error if any // 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}")); ui.colored_label(egui::Color32::RED, format!("Error: {error}"));
} }
@@ -113,9 +110,12 @@ impl Component for MemoryInspector {
ui.end_row(); ui.end_row();
// Memory data (8 bytes per 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); 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 { for &byte in chunk {
ui.monospace(format!("{byte:02X}")); ui.monospace(format!("{byte:02X}"));
} }
@@ -126,12 +126,16 @@ impl Component for MemoryInspector {
} }
// combine all 4 bytes in the chunk into a u32 // combine all 4 bytes in the chunk into a u32
let combined = chunk let combined = chunk.iter().fold(0u32, |acc, &byte| {
.iter() (acc << 8) | u32::from(byte)
.fold(0u32, |acc, &byte| (acc << 8) | u32::from(byte)); });
ui.monospace(format!("{combined}")); 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(); ui.end_row();
} }
+1
View File
@@ -3,6 +3,7 @@ pub mod display;
pub mod editor; pub mod editor;
pub mod history; pub mod history;
pub mod interface; pub mod interface;
pub mod loader;
pub mod memory_inspector; pub mod memory_inspector;
pub mod menu; pub mod menu;
pub mod stack_inspector; pub mod stack_inspector;
+6 -1
View File
@@ -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; 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) { fn render(&mut self, state: &mut State, ui: &mut egui::Ui, _ctx: &egui::Context) {
state.send(Command::StackRequest);
ui.vertical(|ui| { ui.vertical(|ui| {
ui.heading("Stack Inspector"); ui.heading("Stack Inspector");
egui::ScrollArea::vertical() egui::ScrollArea::vertical()
+10 -7
View File
@@ -30,7 +30,7 @@ use crate::emulator::{
system::{ system::{
emulator::run_emulator, emulator::run_emulator,
memory::MainStore, memory::MainStore,
model::{Command, State}, model::{Command, StateUpdate},
processor::Processor, processor::Processor,
}, },
ui::{ ui::{
@@ -86,7 +86,7 @@ pub fn android_main(app: AndroidApp) -> Result<(), Box<dyn std::error::Error>> {
pub fn setup_emulator( pub fn setup_emulator(
cmd_receiver: Receiver<Command>, cmd_receiver: Receiver<Command>,
state_sender: Sender<State>, state_sender: Sender<StateUpdate>,
rpc_client: Option<Arc<RpcClient>>, rpc_client: Option<Arc<RpcClient>>,
) { ) {
let main_store = MainStore::new(); let main_store = MainStore::new();
@@ -101,22 +101,22 @@ pub fn setup_emulator(
#[must_use] #[must_use]
pub fn setup_ui( pub fn setup_ui(
cmd_sender: Sender<Command>, cmd_sender: Sender<Command>,
state_reciever: Receiver<State>, state_reciever: Receiver<StateUpdate>,
) -> EmulatorUI { ) -> EmulatorUI {
let mut ui = EmulatorUI::new(cmd_sender.clone(), state_reciever); let mut ui = EmulatorUI::new(cmd_sender, state_reciever);
// Create UI modules. // Create UI modules.
let control_unit = ControlPanel::new(cmd_sender.clone()); let control_unit = ControlPanel::new();
ui.add_component(Box::new(control_unit)); 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)); ui.add_component(Box::new(mem_inspector));
let stack_inspector = StackInspector::new(); let stack_inspector = StackInspector::new();
ui.add_component(Box::new(stack_inspector)); ui.add_component(Box::new(stack_inspector));
let editor = Editor::new(cmd_sender); let editor = Editor::new();
ui.add_component(Box::new(editor)); ui.add_component(Box::new(editor));
let display = Display::new(); let display = Display::new();
@@ -125,5 +125,8 @@ pub fn setup_ui(
let history = emulator::ui::history::History::new(); let history = emulator::ui::history::History::new();
ui.add_component(Box::new(history)); ui.add_component(Box::new(history));
let loader = emulator::ui::loader::Loader::new();
ui.add_component(Box::new(loader));
ui ui
} }