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)
This commit is contained in:
2026-07-19 01:18:00 +01:00
parent a306575cdd
commit adfba876d2
32 changed files with 1890 additions and 427 deletions
+1 -1
View File
@@ -5,4 +5,4 @@ resolver = "3"
[workspace.package]
edition = "2024"
version = "0.3.0"
authors = ["zxq5"]
authors = ["zxq5", "zxq4"]
+80
View File
@@ -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
+1 -1
View File
@@ -5,7 +5,7 @@ version.workspace = true
authors.workspace = true
[[bin]]
name = "dsa-a"
name = "dsa-d"
path = "src/main.rs"
[lib]
+8 -1
View File
@@ -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) };
@@ -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],
}
}
}
@@ -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<ProcessorSnapshot>,
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],
}
}
}
+3
View File
@@ -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"]
-1
View File
@@ -4,4 +4,3 @@
pub mod ui;
pub mod args;
@@ -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<SharedState>,
@@ -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));
});
}
}
@@ -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<FileDialog>,
error: Option<String>,
program: Option<DseExecutable>,
disassembled: Option<Disassembly>,
}
impl TilePane for Disassembler {
fn title(&self) -> WidgetText {
"Disassembler".into()
}
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
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))
}
}
}
}
@@ -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<Action> {
Component::ui(self, ui);
None
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Component for Display {
fn title(&self) -> &str {
"Display"
@@ -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<SharedState>,
@@ -39,7 +40,8 @@ pub struct Editor {
syntax: Syntax,
// output / loading
output: Output,
output: Vec<u8>,
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<u8>,
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<u8> {
&mut self.data
}
fn data(&self) -> &Vec<u8> {
&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::<Vec<_>>()
.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<u8>,
//
// 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<u8> {
// &mut self.data
// }
//
// fn data(&self) -> &Vec<u8> {
// &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::<Vec<_>>()
// .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)),
// );
// });
// }
// })
// }
// });
// });
// }
// }
@@ -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<Action> {
Component::ui(self, ui);
None
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Component for FrameBuffer {
fn title(&self) -> &str {
"Framebuffer"
@@ -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<Action> {
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 {
}
});
});
}
}
@@ -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.
@@ -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<Action> {
Component::ui(self, ui);
None
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Registers {
fn section(
&mut self,
@@ -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<Action> {
Component::ui(self, ui);
None
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Serial {
fn uart_io_thread(
mem: MemoryBank,
@@ -0,0 +1,79 @@
use egui::{Modal, Id, Layout, Align, RichText};
pub struct ConfirmDialog<T> {
open: bool,
title: String,
message: String,
action: Option<T>,
}
impl<T> ConfirmDialog<T> {
pub fn new() -> Self {
Self {
open: false,
title: String::new(),
message: String::new(),
action: None,
}
}
pub fn open(&mut self, title: impl Into<String>, message: impl Into<String>, 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<T> {
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<T> Default for ConfirmDialog<T> {
fn default() -> Self { Self::new() }
}
@@ -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<String>) {
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() }
}
@@ -0,0 +1,42 @@
use std::path::PathBuf;
use egui::Ui;
use egui_file_dialog::FileDialog;
use crate::ui::debugger::Action;
pub struct DsaFileDialog<T> {
inner: FileDialog, // or whatever backend you're using
constructor: Option<Box<dyn FnOnce(PathBuf) -> T>>,
}
impl<T> DsaFileDialog<T> {
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<T> {
self.inner.update(ctx);
if let Some(path) = self.inner.take_picked() {
return self.constructor.take().map(|f| f(path));
}
None
}
}
@@ -0,0 +1,3 @@
pub mod file_dialog;
pub mod confirm_dialog;
pub mod error_dialog;
@@ -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<SharedState>,
memory: MemoryBank,
}
impl EmulatorHandle {
pub fn new(state: Arc<SharedState>, 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<Action>,
file_dialog: DsaFileDialog<Action>,
error_dialog: ErrorDialog,
tools: Vec<PanelFactory>
}
impl DebuggerUi {
fn open_or_focus<T: TilePane + 'static>(
central_tiles: &mut CentralTiles,
slot: &mut Option<TileId>,
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<SharedState>,
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::<Editor>(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::<Editor>(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::<Editor>(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::<Editor>(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);
}
});
}
}
@@ -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<Action> {
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
}
}
@@ -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<String>,
cache: CommonMarkCache,
}
impl TilePane for Documentation {
fn title(&self) -> WidgetText {
"Documentation".into()
}
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
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<String> {
&mut self.buffer
}
}
@@ -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<PathBuf>,
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<PathBuf> {
&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<Action> {
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 }
}
@@ -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<Action>;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn menu(&mut self, _ui: &mut egui::Ui, _tile_id: TileId) -> Option<Action> {
None
}
}
type Pane = Box<dyn TilePane>;
#[derive(Default)]
struct TileBehavior {
pending_action: Option<Action>,
}
impl Behavior<Pane> for TileBehavior {
fn is_tab_closable(&self, _tiles: &Tiles<Pane>, _tile_id: TileId) -> bool {
true
}
fn on_tab_close(&mut self, tiles: &mut Tiles<Pane>, 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<Box<dyn TilePane>>, _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<dyn TilePane>,
) -> 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<Pane>,
behavior: TileBehavior,
}
impl CentralTiles {
pub fn new() -> Self {
let mut tiles = Tiles::<Pane>::default();
let root = tiles.insert_tab_tile(vec![]);
Self {
tree: Tree::empty("central_tiles"),
behavior: TileBehavior::default(),
}
}
pub fn active_tile(&self) -> Option<TileId> {
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<dyn TilePane>) -> 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<T: 'static>(&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::<T>(),
_ => None,
}
}
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<Action> {
self.behavior.pending_action = None;
self.tree.ui(&mut self.behavior, ui);
self.behavior.pending_action.take()
}
}
pub struct PanelFactory(pub String, pub Box<dyn Fn() -> Box<dyn TilePane>>);
impl PanelFactory {
pub fn register<T, F>(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<dyn TilePane>))
}
}
@@ -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<Color32>,
padding: Option<Vec2>,
widget_type: WidgetType
}
enum WidgetType {
Button,
Indicator,
Label
}
impl PaddedWidget {
pub fn button(text: impl AsRef<str>) -> 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<egui::WidgetText>, 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<egui::WidgetText>, 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<R>(
ui: &mut egui::Ui,
padding: egui::Vec2,
add_contents: impl FnOnce(&mut egui::Ui) -> R,
) -> egui::InnerResponse<R> {
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(),
}
}
}
+18 -2
View File
@@ -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<SharedState>, 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();
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
Binary file not shown.
+2 -1
View File
@@ -121,10 +121,11 @@ print_byte: func
stb rg0, rg1
addi rg1, 1
stw rg1, current
pop rg1
pop rg0
jmp _end
return
// ------------------------------------------
// prints the value of arg[0] to the screen in hex.