mega refactor:

essentially completely reorganised the project.
previous contents of the dsa folder have now been split into emulator/ tooling/ and libraries/
libraries is for common functionality across crates, tooling is for core binaries needed for building and debugging DSA projects, including dsx client/server/build
emulator contains the dsa_emulator crate that implements the core DSA emulator, the dsa_gui crate that implements the graphical user interface for the emulator & all future tooling under a unified application, and finally the dsa_cli crate which will eventually provide lightweight wrapper for the core emulator, simply providing a tool for running DSA binaries without the overhead of a heavy GUI.
This commit is contained in:
2026-08-01 01:28:11 +01:00
parent 8c19497bc7
commit 27d38eaf89
120 changed files with 84 additions and 90 deletions
+7
View File
@@ -0,0 +1,7 @@
[package]
name = "dsa_cli"
edition.workspace = true
version.workspace = true
authors.workspace = true
[dependencies]
+3
View File
@@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "dsa_emulator"
edition.workspace = true
version.workspace = true
authors.workspace = true
[lib]
name = "dsa_emulator"
path = "src/lib.rs"
[dependencies]
arc-swap = "1.9.2"
common = { version = "0.1.0", path = "../../libraries/common" }
serde = { version = "1.0.228", features = ["derive"] }
ron = "0.12.2"
@@ -0,0 +1,10 @@
/* @[crate::config::IoMap] */
IoMap(
items: [
IoMapping(
device: "Display",
base: 0x0002_0000,
size: 0x0000_07D0, // 2000 bytes (80x25)
),
]
)
@@ -0,0 +1,42 @@
/* @[crate::config::MemoryMap] */
MemoryMap(
table_addr: 0x1000,
regions: [
// 0000-1000 = reserved
// Region(
// base: 0x0,
// size: 0x1000,
// region_type: Reserved
// ),
// // 1000-4000 = bootloader
// Region(
// base: 0x1000,
// size: 0x3000,
// region_type: Bootloader
// ),
// 5000-0FFFFFFF = MMIO
Region(
base: 0x20000,
size: 0x0FFF_FFFF,
region_type: MMIO
),
// 10000000-BFFFFFFF = userspace
Region(
base: 0x1000_0000,
size: 0xBFFF_FFFF,
region_type: Usable
),
// C0000000-C0FFFFFF = kernel page tables
Region(
base: 0xC000_0000,
size: 0x100_0000,
region_type: KernelTables
),
// C1000000-FFFFFFFF = kernel
Region(
base: 0xC100_0000,
size: 0x3EFF_FFFF,
region_type: Kernel
)
]
)
@@ -0,0 +1,2 @@
pub const DEFAULT_MEMORY_MAP: &'static str = include_str!("dsa.mmap.ron");
pub const DEFAULT_IO_MAP: &'static str = include_str!("dsa.iomap.ron");
+97
View File
@@ -0,0 +1,97 @@
use crate::config::default::{DEFAULT_IO_MAP, DEFAULT_MEMORY_MAP};
use crate::memory::PhysAddr;
use crate::memory::mmu::MMU;
use serde::{Deserialize, Serialize};
use crate::processor::DsaProcessor;
mod default;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[repr(u32)]
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
pub enum RegionType {
Reserved,
Bootloader,
Kernel,
KernelTables,
Usable,
MMIO,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub struct Region {
pub base: PhysAddr,
pub size: u32,
pub region_type: RegionType,
// flags for the ui
#[serde(default)]
pub identity_map_on_boot: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MemoryMap {
pub table_addr: PhysAddr,
pub regions: Vec<Region>,
}
#[derive(Serialize, Deserialize)]
pub struct IoMapping {
pub device: String,
pub base: u32,
pub size: u32,
}
#[derive(Serialize, Deserialize)]
pub struct IoMap { items: Vec<IoMapping> }
impl Default for IoMap {
fn default() -> Self {
ron::from_str(DEFAULT_IO_MAP).unwrap()
}
}
impl Default for MemoryMap {
fn default() -> Self {
ron::from_str(DEFAULT_MEMORY_MAP).unwrap()
}
}
impl MemoryMap {
pub const DEFAULT_MEMORY_MAP_ADDR: PhysAddr = 0x1000;
pub fn new() -> Self {
MemoryMap {
table_addr: Self::DEFAULT_MEMORY_MAP_ADDR,
regions: Vec::new(),
}
}
pub fn apply(&self, cpu: &mut DsaProcessor) {
for reg in &self.regions {
if reg.identity_map_on_boot {
Self::identity_map(cpu.mmu_mut(), reg);
}
}
let mem = cpu.memory_mut();
let mut offset = self.table_addr;
for region in &self.regions {
println!("Wrote entry: {region:?} to page table");
mem.write_word(offset, region.base);
mem.write_word(offset + 4, region.size);
mem.write_word(offset + 8, region.region_type as u32);
offset += 12;
}
}
pub fn identity_map(mmu: &mut MMU, region: &Region) {
let first_page = region.base >> 12;
let page_count = region.size >> 12;
for page in (first_page..first_page + page_count).map(|p| p << 12) {
mmu.create_mapping(page, page);
}
}
}
@@ -0,0 +1,45 @@
use crate::EmulatorHandle;
use crate::memory::PhysAddr;
pub struct Display {
width: u32,
height: u32,
addr: PhysAddr,
emulator: EmulatorHandle,
buffer: Vec<u8>,
}
impl Display {
pub fn new(width: u32, height: u32, addr: PhysAddr, emulator: EmulatorHandle) -> Self {
Self {
width,
height,
addr,
emulator,
buffer: Vec::new(),
}
}
pub fn width(&self) -> usize {
self.width as usize
}
pub fn height(&self) -> usize {
self.height as usize
}
pub fn changed(&mut self) -> bool {
let temp = self.read();
if temp != self.buffer {
self.buffer = temp;
return true;
}
false
}
pub fn read(&mut self) -> Vec<u8> {
self.emulator.mem_read(self.addr, self.width * self.height)
}
}
@@ -0,0 +1,35 @@
use crate::EmulatorHandle;
use crate::memory::PhysAddr;
pub struct FrameBuffer {
width: usize,
height: usize,
addr: PhysAddr,
emulator: EmulatorHandle,
}
impl FrameBuffer {
pub fn new(width: u32, height: u32, addr: PhysAddr, emulator: EmulatorHandle) -> Self {
let width = width as usize;
let height = height as usize;
Self {
width,
height,
addr,
emulator,
}
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
pub fn read(&self) -> Vec<u8> {
self.emulator.mem_read(self.addr, ( self.width * self.height * 4 ) as u32)
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod display;
pub mod framebuffer;
pub mod serial;
@@ -0,0 +1,90 @@
use crate::EmulatorHandle;
use crate::memory::PhysAddr;
use crate::processor::Interrupt;
use std::collections::VecDeque;
use std::sync::mpsc::{Receiver, Sender};
/// ## SERIAL LAYOUT: (from the perspective of code inside the ui)
/// - [base+0x00]: ser_out valid flag - set by kernel after writing a byte to serial
/// - [base+0x01]: ser_out byte - data payload for ui to read
/// - [base+0x02]: ser_in valid flag - set by ui after writing a byte to serial
/// - [base+0x03]: ser_in byte - data payload for kernel to read
pub struct Serial {
pub read_rx: Receiver<u8>,
pub write_tx: Sender<u8>,
pub read_buff: Vec<u8>,
pub write_buff: String,
}
impl Serial {
fn uart_io_thread(
mut handle: EmulatorHandle,
uart_base: PhysAddr,
tx: Sender<u8>,
rx: Receiver<u8>,
) {
let mut write_buffer: VecDeque<u8> = VecDeque::new();
loop {
while let Ok(byte) = rx.try_recv() {
write_buffer.push_back(byte);
}
// Kernel => ui: valid flag in byte 0, data in byte 1
let ser_out_valid = handle.read_byte(uart_base + 0);
if ser_out_valid == 0x01 {
let ser_out = handle.read_byte(uart_base + 1);
let _ = tx.send(ser_out);
unsafe {
handle.write_byte(uart_base, 0x00); // clear the whole word
}
}
// Emulator => kernel: valid flag in byte 2, data in byte 3
if !write_buffer.is_empty() {
let ser_in_valid = handle.read_byte(uart_base + 2);
if ser_in_valid == 0x00 {
if let Some(byte) = write_buffer.pop_front() {
unsafe {
handle.write_byte(uart_base + 3, byte);
handle.write_byte(uart_base + 2, 0x01);
}
// Presumably dsa_emulator thread has shut down by this point.
if handle.interrupt(Interrupt::SerialIn).is_err() {
return;
}
}
}
std::thread::sleep(std::time::Duration::from_millis(2));
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
}
pub fn new(uart_base: PhysAddr, handle: EmulatorHandle) -> Self {
let (read_tx, read_rx) = std::sync::mpsc::channel();
let (write_tx, write_rx) = std::sync::mpsc::channel();
let _ = std::thread::spawn(move || {
Self::uart_io_thread(handle, uart_base, read_tx, write_rx)
});
Self {
write_tx,
read_rx,
read_buff: Vec::new(),
write_buff: String::new(),
}
}
pub fn update(&mut self) {
while let Ok(byte) = self.read_rx.try_recv() {
self.read_buff.push(byte);
}
}
}
+235
View File
@@ -0,0 +1,235 @@
#![feature(likely_unlikely)]
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::thread;
use std::thread::{JoinHandle, Thread};
use std::time::Duration;
use common::prelude::{Instruction, Register};
use crate::config::{IoMap, MemoryMap};
use crate::InternalImplementation::{Handle, Owned};
use crate::memory::PhysAddr;
use crate::memory::ram::MemoryBank;
use crate::processor::{Interrupt, RunningState, SharedState};
pub mod processor;
pub mod memory;
pub mod config;
pub mod devices;
#[derive(Debug)]
pub enum DsaError {
BootFailure(String),
CommunicationError(String),
CriticalException(String),
}
/// This trait should be implemented for any type that can act as a fully functional DSA dsa_emulator with the ability to share state
pub trait DsaImplementation: Send {
/// may be modified in future with extra args like config
/// having direct control over the dsa_emulator's POST/boot state can be useful for
/// integration testing.
fn boot(&mut self) -> Result<(), DsaError>;
fn reset(&mut self);
/// main run method. the main-loop of the program execution
fn run(&mut self) -> Result<(), DsaError>;
/// memory mapping is required for dsa_emulator memory sections
fn apply_memory_map(&mut self, map: MemoryMap);
/// IO mapping also
fn apply_io_mapping(&mut self, map: IoMap);
/// List all memory io_devices
/// returns a copy of the IOmap (cannot be a reference since across-threads)
fn get_io_map(&self) -> IoMap;
/// returns a wrapper around the physical memory used by the dsa_emulator
fn get_memory_handle(&mut self) -> &mut MemoryBank;
/// returns a wrapper around the shared state of the dsa_emulator
fn get_state_handle(&mut self) -> Arc<SharedState>;
}
// pub trait DsaDebugImplementation: DsaImplementation {
// fn step()
// }
#[derive(Clone)]
pub struct EmulatorHandle {
memory: MemoryBank,
state: Arc<SharedState>,
}
impl EmulatorHandle {
pub fn read_byte(&self, addr: PhysAddr) -> u8 {
self.memory.read_byte(addr)
}
pub unsafe fn write_byte(&mut self, addr: PhysAddr, val: u8) {
self.memory.write_byte(addr, val);
}
pub fn mem_read(&self, addr: PhysAddr, size: u32) -> Vec<u8> {
self.memory.read_bytes(addr, size as usize).to_vec()
}
pub unsafe fn mem_write(&mut self, addr: PhysAddr, data: &[u8]) {
self.memory.write_bytes(addr, data);
}
pub fn interrupt(&mut self, code: Interrupt) -> Result<(), DsaError> {
self.state.interrupt_queue.send(code)
.map_err(|_| DsaError::CommunicationError("interrupt failed".into()) )
}
pub fn status(&self) -> RunningState {
self.state.proc.load().running.clone()
}
pub fn clock(&self) -> usize {
self.state.proc.load().clock
}
pub fn time(&self) -> Duration {
self.state.proc.load().time
}
/// Request that the dsa_emulator backend provide an updated copy of its current internal state to `state.proc`
pub fn request_update(&mut self) {
self.state.update_requested.store(true, Ordering::Relaxed);
}
/// Request that the dsa_emulator backend pauses it's current execution
/// no-op if in the `Paused` state
pub fn request_pause(&mut self) {
self.state.pause_requested.store(true, Ordering::Relaxed);
}
pub fn when_paused(&mut self, f: impl FnOnce(&mut Self)) {
self.request_pause();
while self.state.proc.load().running != RunningState::Paused {
continue;
}
f(self);
}
/// Request that the dsa_emulator backend resumes it's current execution
/// no-op if not in the `Paused` state
pub fn request_resume(&mut self) {
self.state.pause_requested.store(false, Ordering::Relaxed);
}
/// Request that the dsa_emulator backend performs a full reset, clearing all memory, registers and other internal state.
pub fn reset(&mut self) {
self.state.reset_requested.store(true, Ordering::Relaxed);
// wait until the reset is completed - this should be extremely quick.
while self.state.reset_requested.load(Ordering::Relaxed) {
continue;
}
}
pub fn get_register(&self, rg: Register) -> u32 {
self.state.proc.load().registers[rg as usize]
}
pub fn registers(&self) -> [u32; Register::COUNT as usize] {
self.state.proc.load().registers.clone()
}
}
enum InternalImplementation {
Owned(Box<dyn DsaImplementation + Send>),
Handle(JoinHandle<()>),
}
pub struct EmulatorController {
// the dsa_emulator itself
internal: Option<InternalImplementation>,
handle: EmulatorHandle,
}
impl AsMut<EmulatorHandle> for EmulatorController {
fn as_mut(&mut self) -> &mut EmulatorHandle {
&mut self.handle
}
}
impl EmulatorController {
pub fn new(mut emulator: Box<dyn DsaImplementation>) -> Self {
Self {
handle: EmulatorHandle {
memory: emulator.get_memory_handle().clone(),
state: emulator.get_state_handle().clone(),
},
internal: Some(Owned(emulator)),
}
}
pub fn run(&mut self) -> Result<(), DsaError> {
const STACK_SIZE: usize = 1024 * 1024 * 16;
let Some(Owned(mut handle)) = self.internal.take() else {
return Err(DsaError::CriticalException(String::from("owned dsa_emulator returned none!")))
};
self.internal = Some(Handle(
thread::Builder::new()
.stack_size(STACK_SIZE)
.spawn(move || {handle.run().unwrap() })
.unwrap()
));
Ok(())
}
pub fn get_handle(&mut self) -> &mut EmulatorHandle {
&mut self.handle
}
pub fn map_memory(&mut self, map: MemoryMap) -> Result<(), DsaError> {
let Some(Owned(t)) = &self.internal else {
return Err(DsaError::BootFailure("Emulator already initialised".into()));
};
Ok(())
}
pub fn shutdown(&mut self) {
// if let Some(handle) = &self.handle {
// handle.request_shutdown(); // sets an atomic the run loop checks each poll
// }
todo!();
if let Some(InternalImplementation::Handle(join_handle)) = self.internal.take() {
let _ = join_handle.join(); // wait for the thread to actually exit
}
self.internal = None;
}
}
+109
View File
@@ -0,0 +1,109 @@
use crate::memory::{FaultInfo, PhysAddr, VirtAddr, tlb::TLB};
pub(crate) struct MMU {
buffer: TLB,
enable_paging: bool,
// Memory Management Register (MMR) Tells MMU where to find page tables
mmr_value: u32,
page_table: Box<[Option<PageTable>; 1024]>,
}
impl MMU {
pub fn new() -> Self {
MMU {
buffer: TLB::new(),
enable_paging: false,
mmr_value: 0,
page_table: Box::new([const { None }; 1024]),
}
}
pub fn paging_enabled(&mut self, enable: bool) {
self.enable_paging = enable;
}
pub fn tlb_insert(&mut self, virtual_address: VirtAddr, physical_address: PhysAddr) {
self.buffer.insert(virtual_address, physical_address);
}
pub fn create_mapping(&mut self, virtual_address: VirtAddr, physical_address: PhysAddr) {
let table = (virtual_address >> 22) as usize;
let page = ((virtual_address >> 12) & 0x3FF) as usize;
self.page_table[table].get_or_insert_default().entries[page] =
Some(PageTableEntry::new(physical_address, true, true, false));
}
#[inline(always)]
pub fn lookup(&mut self, virtual_address: VirtAddr) -> Result<PhysAddr, FaultInfo> {
if !self.enable_paging {
return Ok(virtual_address);
}
if let Some(addr) = self.buffer.lookup(virtual_address) {
return Ok(addr);
}
self.walk(virtual_address)
}
#[cold]
#[inline(never)]
fn walk(&mut self, virtual_address: VirtAddr) -> Result<PhysAddr, FaultInfo> {
let phys_addr = self.walk_page_table(virtual_address)?;
self.buffer.insert(virtual_address, phys_addr);
Ok(phys_addr)
}
fn walk_page_table(&mut self, virtual_address: VirtAddr) -> Result<PhysAddr, FaultInfo> {
let table = (virtual_address >> 22) as usize;
let page = ((virtual_address >> 12) & 0x3FF) as usize;
if let Some(table) = &self.page_table[table] {
return table.entries[page]
.map(|entry| entry.physical_addr)
.ok_or(FaultInfo::PageFault);
} else {
Err(FaultInfo::PageFault)
}
}
}
#[derive(Debug, Clone)]
struct PageDir {
entries: Box<[PageTable; 1024]>,
}
#[derive(Debug, Clone)]
struct PageTable {
entries: Box<[Option<PageTableEntry>; 1024]>,
}
impl Default for PageTable {
fn default() -> Self {
Self {
entries: Box::new([None; 1024]),
}
}
}
#[derive(Debug, Clone, Copy)]
struct PageTableEntry {
physical_addr: PhysAddr,
writable: bool,
present: bool,
user: bool,
}
impl PageTableEntry {
fn new(physical_addr: PhysAddr, writable: bool, present: bool, user: bool) -> Self {
Self {
physical_addr,
writable,
present,
user,
}
}
}
+11
View File
@@ -0,0 +1,11 @@
mod cache;
pub mod mmu;
pub mod ram;
mod tlb;
pub type VirtAddr = u32;
pub type PhysAddr = u32;
pub enum FaultInfo {
PageFault,
}
+154
View File
@@ -0,0 +1,154 @@
use std::{
alloc::{Layout, alloc_zeroed, dealloc},
ops::{Deref, DerefMut},
};
use crate::memory::PhysAddr;
#[repr(transparent)]
#[derive(Clone)]
pub struct Page([u8; 4096]);
impl Page {
pub const SIZE: usize = 4096;
pub const COUNT: usize = u32::MAX as usize / Self::SIZE + 1;
pub const MASK: u32 = 0xFFF;
pub const ZERO: Self = Page::zeroed();
pub const fn zeroed() -> Self {
Self([0; 4096])
}
pub const fn from(data: [u8; 4096]) -> Self {
Self(data)
}
pub fn read_word(&self, offset: usize) -> u32 {
u32::from_le_bytes(self.0[offset..offset + 4].try_into().unwrap())
}
pub fn write_word(&mut self, offset: usize, value: u32) {
self.0[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
}
pub fn paginate(data: &[u8]) -> impl Iterator<Item = Page> + '_ {
data.chunks(4096).map(|chunk| {
let mut arr = [0u8; 4096];
arr[..chunk.len()].copy_from_slice(chunk);
Page(arr)
})
}
}
impl Deref for Page {
type Target = [u8];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Page {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl TryFrom<Vec<u8>> for Page {
type Error = String;
fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
let arr: [u8; 4096] = data
.try_into()
.map_err(|v: Vec<u8>| format!("Expected 4096 bytes, got {}", v.len()))?;
Ok(Page(arr))
}
}
pub(crate) struct MemoryBank {
heap: *mut u8,
}
unsafe impl Send for MemoryBank {}
unsafe impl Sync for MemoryBank {}
impl MemoryBank {
pub fn new() -> Self {
Self {
heap: unsafe {
// allocate the full 32-bit address range.
alloc_zeroed(Layout::from_size_align(u32::MAX as usize, 4096).unwrap())
},
}
}
pub unsafe fn dealloc(&self) {
unsafe {
dealloc(
self.heap,
Layout::from_size_align(u32::MAX as usize, 4096).unwrap(),
)
}
}
pub fn clear(&mut self) {
unsafe {
std::ptr::write_bytes(self.heap, 0, u32::MAX as usize);
}
}
#[inline(always)]
pub fn read_word(&self, addr: PhysAddr) -> u32 {
unsafe { (self.heap.add(addr as usize) as *const u32).read_volatile() }
}
#[inline(always)]
pub fn read_byte(&self, addr: PhysAddr) -> u8 {
unsafe { self.heap.add(addr as usize).read_volatile() }
}
#[inline]
pub fn read_bytes(&self, addr: PhysAddr, size: usize) -> &[u8] {
let start = addr as usize;
assert!(start + size <= u32::MAX as usize, "read out of bounds");
unsafe {
std::slice::from_raw_parts(self.heap.add(start), size)
}
}
#[inline(always)]
pub fn read_page(&self, addr: PhysAddr) -> &Page {
debug_assert_eq!(addr % 4096, 0);
unsafe { (self.heap.add(addr as usize) as *const Page).as_ref_unchecked() }
}
#[inline(always)]
pub fn write_byte(&self, addr: PhysAddr, value: u8) {
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) };
}
#[inline(always)]
pub fn write_page(&self, addr: PhysAddr, value: &Page) {
unsafe {
(self.heap.add(addr as usize) as *mut Page).copy_from(value, 1);
}
}
}
impl Clone for MemoryBank {
fn clone(&self) -> Self {
Self { heap: self.heap }
}
}
+54
View File
@@ -0,0 +1,54 @@
use crate::memory::{PhysAddr, VirtAddr};
const TLB_SIZE: usize = 256;
const TLB_MASK: u32 = (TLB_SIZE - 1) as u32;
pub struct TLB {
entries: [TlbEntry; TLB_SIZE],
}
#[derive(Debug, Clone, Copy)]
struct TlbEntry {
virtual_address: VirtAddr,
physical_address: PhysAddr,
valid: bool,
}
impl TLB {
pub fn new() -> Self {
TLB {
entries: [TlbEntry {
virtual_address: 0,
physical_address: 0,
valid: false,
}; TLB_SIZE],
}
}
#[inline(always)]
fn index(page: u32) -> usize {
let hash = page ^ (page >> 12);
(hash & TLB_MASK) as usize
}
#[inline(always)]
pub fn lookup(&self, virtual_address: VirtAddr) -> Option<PhysAddr> {
let page = virtual_address >> 12; // for 4KB pages
let entry = self.entries[Self::index(page)];
if entry.valid && entry.virtual_address == virtual_address {
Some(entry.physical_address)
} else {
None
}
}
#[inline(always)]
pub fn insert(&mut self, virt: VirtAddr, phys: PhysAddr) {
let idx = Self::index(virt) as usize;
self.entries[idx] = TlbEntry {
virtual_address: virt,
physical_address: phys,
valid: true,
};
}
}
@@ -0,0 +1,392 @@
use common::isa::instructions::Opcode;
use common::prelude::{Instruction, Register};
use crate::config::{IoMap, MemoryMap, RegionType};
use crate::DsaError;
use crate::DsaError::BootFailure;
use crate::memory::mmu::MMU;
use crate::memory::ram::{MemoryBank, Page};
use crate::processor::interrupts::Interrupt;
use crate::processor::state::{ProcessorSnapshot, SharedState};
use crate::processor::state::RunningState::Running;
#[derive(Copy, Clone, Debug)]
pub enum ExecutionResult {
Continue,
Halt,
Interrupt(Interrupt),
}
pub struct DsaProcessor {
pub internal_state: ProcessorSnapshot,
pub(crate) mmu: MMU,
pub(crate) mainstore: MemoryBank,
mmio_region: Option<(u32, u32)>,
memory_map: Option<MemoryMap>,
__mmap_configured: bool,
}
impl DsaProcessor {
pub fn reset(&mut self) {
self.internal_state = ProcessorSnapshot::default();
self.mmu = MMU::new();
// deallocates the old memory
self.mainstore.clear();
self.mmio_region = None;
self.memory_map = None;
self.__mmap_configured = false;
}
pub fn new() -> Self {
Self {
internal_state: ProcessorSnapshot::default(),
mmu: MMU::new(),
mainstore: MemoryBank::new(),
mmio_region: None,
memory_map: None,
__mmap_configured: false,
}
}
#[inline]
pub fn execute(&mut self, word: Instruction) -> ExecutionResult {
// This needs to be unsafe as we're using word.xxxx_uc() functions for decoding which are unsafe
// as they perform unchecked transmute operations (performance critical)
unsafe {
match Opcode::from_u8(word.opcode()) {
// Nothing
Some(Opcode::Nop) => {}
// move
Some(Opcode::Mov) => {
*self.mut_reg(word.dest_uc()) = self.reg(word.src1_uc());
}
Some(Opcode::CMov) => {
if self.reg(word.misc_uc()) != 0 {
*self.mut_reg(word.dest_uc()) = self.reg(word.src1_uc());
}
}
// load
Some(Opcode::Ldbs) => return ExecutionResult::Interrupt(Interrupt::InvalidOpcode),
Some(Opcode::Ldhs) => return ExecutionResult::Interrupt(Interrupt::InvalidOpcode),
Some(Opcode::Ldb) => {
*self.mut_reg(word.dest_uc()) = u32::from(
self.mem_read_byte(self.reg(word.src1_uc()) + word.imm16() as u32),
)
}
Some(Opcode::Ldh) => {
let addr = self.reg(word.src1_uc()) + word.imm16() as u32;
let hi = self.mem_read_byte(addr) as u32;
let lo = self.mem_read_byte(addr + 1) as u32;
*self.mut_reg(word.dest_uc()) = (hi << 8) | lo;
// *self.mut_reg(word.dest_uc()) = u32::from(
// self.mem_read_word(self.reg(word.src1_uc()) + word.imm16() as u32) >> 16,
// )
}
Some(Opcode::Ldw) => {
*self.mut_reg(word.dest_uc()) = u32::from(
self.mem_read_word(self.reg(word.src1_uc()) + word.imm16() as u32),
)
}
// store
Some(Opcode::Stb) => {
self.mem_write_byte(
self.reg(word.dest_uc()) + word.imm16() as u32,
self.reg(word.src1_uc()) as u8,
);
}
Some(Opcode::Sth) => {
self.mem_write_byte(
self.reg(word.dest_uc()) + word.imm16() as u32,
(self.reg(word.src1_uc()) as u16 >> 8) as u8,
);
self.mem_write_byte(
self.reg(word.dest_uc()) + word.imm16() as u32 + 1,
self.reg(word.src1_uc()) as u8,
);
}
Some(Opcode::Stw) => {
self.mem_write_word(
self.reg(word.dest_uc()) + word.imm16() as u32,
self.reg(word.src1_uc()),
);
}
// load immediate
Some(Opcode::Lli) => {
*self.mut_reg(word.dest_uc()) = word.imm16() as u32;
}
Some(Opcode::Lui) => {
*self.mut_reg(word.dest_uc()) =
(word.imm16() as u32) << 16 | self.reg(word.dest_uc());
}
// Comparison
Some(Opcode::Ieq) => {
*self.mut_reg(word.dest_uc()) =
(self.reg(word.src1_uc()) == self.reg(word.src2_uc())) as u32;
}
Some(Opcode::Ine) => {
*self.mut_reg(word.dest_uc()) =
(self.reg(word.src1_uc()) != self.reg(word.src2_uc())) as u32;
}
Some(Opcode::Ilt) => {
*self.mut_reg(word.dest_uc()) =
(self.reg(word.src1_uc()) < self.reg(word.src2_uc())) as u32;
}
Some(Opcode::Ile) => {
*self.mut_reg(word.dest_uc()) =
(self.reg(word.src1_uc()) <= self.reg(word.src2_uc())) as u32;
}
Some(Opcode::Igt) => {
*self.mut_reg(word.dest_uc()) =
(self.reg(word.src1_uc()) > self.reg(word.src2_uc())) as u32;
}
Some(Opcode::Ige) => {
*self.mut_reg(word.dest_uc()) =
(self.reg(word.src1_uc()) >= self.reg(word.src2_uc())) as u32;
}
// Jump
Some(Opcode::Jmp) => {
*self.mut_reg(Register::Pcx) = self.reg(word.dest_uc()) + word.imm16() as u32
}
Some(Opcode::Jez) => {
if self.reg(word.src1_uc()) == 0 {
*self.mut_reg(Register::Pcx) =
self.reg(word.dest_uc()) + word.imm16() as u32
}
}
Some(Opcode::Jnz) => {
if self.reg(word.src1_uc()) != 0 {
*self.mut_reg(Register::Pcx) =
self.reg(word.dest_uc()) + word.imm16() as u32
}
}
Some(Opcode::Jnc) => return ExecutionResult::Interrupt(Interrupt::InvalidOpcode),
Some(Opcode::Jic) => return ExecutionResult::Interrupt(Interrupt::InvalidOpcode),
// Bitwise
Some(Opcode::And) => {
*self.mut_reg(word.dest_uc()) =
self.reg(word.src1_uc()) & self.reg(word.src2_uc());
}
Some(Opcode::Nand) => {
*self.mut_reg(word.dest_uc()) =
!(self.reg(word.src1_uc()) & self.reg(word.src2_uc()));
}
Some(Opcode::Or) => {
*self.mut_reg(word.dest_uc()) =
self.reg(word.src1_uc()) | self.reg(word.src2_uc());
}
Some(Opcode::Nor) => {
*self.mut_reg(word.dest_uc()) =
!(self.reg(word.src1_uc()) | self.reg(word.src2_uc()));
}
Some(Opcode::Xor) => {
*self.mut_reg(word.dest_uc()) =
self.reg(word.src1_uc()) ^ self.reg(word.src2_uc());
}
Some(Opcode::Xnor) => {
*self.mut_reg(word.dest_uc()) =
!(self.reg(word.src1_uc()) ^ self.reg(word.src2_uc()));
}
Some(Opcode::Not) => {
*self.mut_reg(word.dest_uc()) = !self.reg(word.src1_uc());
}
// Arithmetic
Some(Opcode::Add) => {
*self.mut_reg(word.dest_uc()) =
self.reg(word.src1_uc()) + self.reg(word.src2_uc());
}
Some(Opcode::Sub) => {
*self.mut_reg(word.dest_uc()) =
self.reg(word.src1_uc()) - self.reg(word.src2_uc());
}
Some(Opcode::Shl) => {
*self.mut_reg(word.dest_uc()) = self.reg(word.src1_uc())
<< (self.reg(word.src2_uc()) + word.shamt() as u32);
}
Some(Opcode::Shr) => {
*self.mut_reg(word.dest_uc()) = self.reg(word.src1_uc())
>> (self.reg(word.src2_uc()) + word.shamt() as u32);
}
Some(Opcode::Addi) => {
*self.mut_reg(word.dest_uc()) = self.reg(word.src1_uc()) + word.imm16() as u32;
}
Some(Opcode::Subi) => {
*self.mut_reg(word.dest_uc()) = self.reg(word.src1_uc()) - word.imm16() as u32;
}
// Utility
Some(Opcode::Push) => {
self.push(word.src1_uc());
}
Some(Opcode::Pop) => {
self.pop(word.dest_uc());
}
// Function
Some(Opcode::Call) => {
self.push(Register::Pcx);
*self.mut_reg(Register::Pcx) = self.reg(word.dest_uc()) + word.imm16() as u32
}
Some(Opcode::Ret) => {
self.pop(Register::Ret);
*self.mut_reg(Register::Pcx) = self.reg(Register::Ret);
}
Some(Opcode::IRet) => {
self.pop(Register::Ret);
*self.mut_reg(Register::Pcx) = self.reg(Register::Ret);
println!(
"Returning from interrupt to address {:04X}",
self.reg(Register::Pcx)
);
}
Some(Opcode::Int) => return ExecutionResult::Interrupt(Interrupt::Software(word.imm16() as u8)),
Some(Opcode::Hlt) => return ExecutionResult::Halt,
None => return ExecutionResult::Interrupt(Interrupt::InvalidOpcode),
}
};
ExecutionResult::Continue
}
#[inline]
pub fn interrupt(&mut self, int: Interrupt) {
self.push(Register::Pcx);
*self.mut_reg(Register::Pcx) =
self.mem_read_word(self.reg(Register::Idr) + int.code() as u32 * 4);
}
#[must_use]
#[inline]
pub fn reg(&self, reg: Register) -> u32 {
debug_assert!((reg as u8) < Register::COUNT);
unsafe { *self.internal_state.registers.get_unchecked(reg as usize) }
}
#[inline]
pub fn mut_reg(&mut self, reg: Register) -> &mut u32 {
debug_assert!((reg as u8) < Register::COUNT);
unsafe {
self.internal_state
.registers
.get_unchecked_mut(reg as usize)
}
}
#[inline]
pub fn push(&mut self, reg: Register) {
*self.mut_reg(Register::Spr) -= 4;
self.mem_write_word(self.reg(Register::Spr), self.reg(reg));
}
#[inline]
pub fn pop(&mut self, reg: Register) {
*self.mut_reg(reg) = self.mem_read_word(self.reg(Register::Spr));
*self.mut_reg(Register::Spr) += 4;
}
#[inline]
pub fn mem_read_byte(&mut self, addr: u32) -> u8 {
self.mainstore.read_byte(addr)
}
#[inline]
pub fn mem_write_byte(&mut self, addr: u32, val: u8) {
// TODO: how can we extract this method for debugging?
// if addr == 0x40000 || addr == 0x40001 || addr == 0x40002 || addr == 0x40003 {
// if addr == 0x40001 || addr == 0x40003 {
// eprintln!("writing char {} to uart + {}", val as char, addr - 0x40000);
// }
// }
self.mainstore.write_byte(addr, val);
}
#[inline]
pub fn mem_read_word(&mut self, addr: u32) -> u32 {
self.mainstore.read_word(addr)
}
#[inline]
pub fn mem_write_word(&mut self, addr: u32, val: u32) {
self.mainstore.write_word(addr, val);
}
#[inline]
pub fn mem_read_page(&mut self, addr: u32) -> &Page {
self.mainstore.read_page(addr)
}
#[inline]
pub fn mem_write_page(&mut self, addr: u32, val: &Page) {
self.mainstore.write_page(addr, val);
}
#[inline]
pub fn mmu_mut(&mut self) -> &mut MMU {
&mut self.mmu
}
#[inline]
pub fn memory_mut(&mut self) -> &mut MemoryBank {
&mut self.mainstore
}
pub fn apply_memory_map(&mut self, map: MemoryMap) {
self.mmio_region = map
.regions
.iter()
.find(|r| matches!(r.region_type, RegionType::MMIO))
.map(|r| (r.base, r.base + r.size));
map.apply(self);
self.memory_map = Some(map);
self.__mmap_configured = true;
}
pub fn boot(&mut self) -> Result<(), DsaError> {
if !(self.__mmap_configured) {
return Err(BootFailure("Emulator<Mem> not configured".to_string()));
}
if let Some(map) = &self.memory_map {
MemoryMap::identity_map(&mut self.mmu, map.regions.get(0).unwrap());
}
self.internal_state.running = Running;
Ok(())
}
pub fn apply_io_mapping(&mut self, map: IoMap) {
todo!()
}
pub fn get_io_map(&self) -> IoMap {
todo!()
}
}
impl Drop for DsaProcessor {
fn drop(&mut self) {
// drop is only ever called once on an dsa_emulator instance, so we can be sure a double free will not happen.
unsafe { self.mainstore.dealloc() }
}
}
@@ -0,0 +1,243 @@
use crate::config::{IoMap, MemoryMap};
use crate::memory::ram::MemoryBank;
use crate::processor::dsa_core::{DsaProcessor, ExecutionResult};
use crate::processor::interrupts::Interrupt;
use crate::processor::state::RunningState::{Halted, Paused, Running, Shutdown};
use crate::processor::state::{RunningState, SharedState};
use crate::{DsaError, DsaImplementation};
use common::prelude::{Instruction, Register};
use std::hint::unlikely;
use std::ops::AddAssign;
use std::sync::atomic::Ordering;
use std::sync::{Arc, mpsc};
use std::sync::mpsc::TryRecvError;
use std::thread;
use std::time::{Duration, Instant};
pub struct Emulator {
core: DsaProcessor,
shared_state: Arc<SharedState>,
// Interrupts
interrupts: mpsc::Receiver<Interrupt>,
pending_fault: Option<Interrupt>,
time: Instant,
}
impl DsaImplementation for Emulator {
#[cold]
fn boot(&mut self) -> Result<(), DsaError> {
self.core.boot()
}
fn reset(&mut self) {
self.core.reset();
}
fn run(&mut self) -> Result<(), DsaError> {
// wait for UI to initialise.
if self.shared_state.pause_requested.load(Ordering::Relaxed) {
self.run_state_paused(Running)
}
self.boot()?;
'emu: loop {
// so that it always returns zero. this is more performance friendly than checking
// for a zero register each access as it's branchless
*self.core.mut_reg(Register::Zero) = 0;
// IMPORTANT
// do not change anything about this loop. it's fully optimised afaik.
// Check for commands or hardware Interrupts every 32k cycles
if unlikely(self.core.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.core.internal_state.clock & 0x7FFFF == 0) {
if self.shared_state.update_requested.load(Ordering::Relaxed) {
self.core.internal_state.time = self.time.elapsed();
self.share_state();
// Acknowledge update completed
self.shared_state.update_requested.store(false, Ordering::Relaxed);
}
}
// Transition: Paused
if unlikely(self.shared_state.pause_requested.load(Ordering::Relaxed)) {
self.run_state_paused(Running);
}
// Responsibility: Handle interrupts
match self.interrupts.try_recv() {
Ok(int) => self.core.interrupt(int),
Err(TryRecvError::Disconnected) => break 'emu,
Err(TryRecvError::Empty) => {}
}
}
// Responsibility: run fetch-decode-execute cycle.
let pc = self.core.reg(Register::Pcx);
let instruction = self.core.mem_read_word(pc);
//
// if cfg!(debug_assertions) {
// println!(
// "Clock: {} Executing {:?} PCX: {}, reg: {:?}",
// self.core.internal_state.clock,
// Instruction(instruction),
// pc,
// self.core.internal_state.registers
// );
// thread::sleep(Duration::from_micros(10));
// }
*self.core.mut_reg(Register::Pcx) = self.core.reg(Register::Pcx).wrapping_add(4);
match self.core.execute(Instruction(instruction)) {
ExecutionResult::Continue => {},
ExecutionResult::Halt => self.run_state_halted(Running),
ExecutionResult::Interrupt(interrupt) => {
if cfg!(debug_assertions) {
println!("Interrupt: {:?}", interrupt);
println!("Interrupt while at address {:04X}", self.core.reg(Register::Pcx));
}
self.core.interrupt(interrupt);
if cfg!(debug_assertions) {
println!(
"Jumping to interrupt at address {:04X}",
self.core.reg(Register::Pcx)
);
}
}
}
self.core.internal_state.clock += 1;
}
self.core.internal_state.running = Shutdown;
Ok(())
}
fn apply_memory_map(&mut self, map: MemoryMap) {
self.core.apply_memory_map(map)
}
fn apply_io_mapping(&mut self, map: IoMap) {
self.core.apply_io_mapping(map)
}
fn get_io_map(&self) -> IoMap {
self.core.get_io_map()
}
fn get_memory_handle(&mut self) -> &mut MemoryBank {
self.core.memory_mut()
}
fn get_state_handle(&mut self) -> Arc<SharedState> {
self.shared_state.clone()
}
}
impl Emulator {
pub fn new() -> Self {
let (sender, receiver) = mpsc::channel::<Interrupt>();
Self {
core: DsaProcessor::new(),
shared_state: Arc::new(SharedState::new(sender)),
interrupts: receiver,
pending_fault: None,
time: Instant::now(),
}
}
pub fn state_handle(&self) -> Arc<SharedState> {
self.shared_state.clone()
}
#[cold]
fn share_state(&mut self) {
self.shared_state
.proc
.store(Arc::new(self.core.internal_state.clone()));
}
#[cold]
pub fn run_state_halted(&mut self, prior: RunningState) {
self.core.internal_state.running = Halted;
// Responsibility: keep track of the time spent in the running state across multiple hlt/interrupt cycles
self.core.internal_state.time += self.time.elapsed();
loop {
// Transition: interrupt received
if let Ok(int) = self.interrupts.recv_timeout(Duration::from_millis(100)) {
self.core.internal_state.running = Running;
self.core.interrupt(int);
break;
}
// Transition: pause signal received
if self.shared_state.pause_requested.load(Ordering::Relaxed) {
self.run_state_paused(Halted);
}
// Responsibility: update UI when requested
if self.shared_state.update_requested.load(Ordering::Relaxed) == true {
self.share_state();
// Acknowledge update completed
self.shared_state.update_requested.store(false, Ordering::Relaxed);
}
}
// Responsibility: reset `private.time` on state exit
self.time = Instant::now();
self.core.internal_state.running = prior;
}
#[cold]
pub fn run_state_paused(&mut self, prior: RunningState) {
self.core.internal_state.running = Paused;
// Responsibility: save the running-time since last pause state.
self.core.internal_state.time = self.time.elapsed();
self.share_state();
loop {
// Responsibility: update UI when requested
if self.shared_state.update_requested.load(Ordering::Relaxed) == true {
self.share_state();
// Acknowledge update completed
self.shared_state.update_requested.store(false, Ordering::Relaxed);
}
// Responsibility: reset when requested by the UI
if self.shared_state.reset_requested.load(Ordering::Relaxed) == true {
self.reset();
self.shared_state.reset_requested.store(false, Ordering::Relaxed);
}
// Transition: run signal received
if self.shared_state.pause_requested.load(Ordering::Relaxed) == false {
break;
}
}
// Responsibility: reset private.time, shared.time, shared.cycles on state exit
self.time = Instant::now();
self.core.internal_state.time = Duration::new(0, 0);
self.core.internal_state.clock = 0;
self.core.internal_state.running = prior;
}
}
@@ -0,0 +1,54 @@
#[repr(u8)]
#[derive(Clone, Copy, Debug)]
pub enum Interrupt {
// CPU exceptions 0-31
InvalidInterrupt = 0,
PageFault = 1,
ProtectionFault = 2,
InvalidOpcode = 3,
DivideByZero = 4,
StackOverflow = 5,
WriteToReadOnly = 6,
ReadFromWriteOnly = 7,
UnmappedIo = 8,
// 8-31 reserved for future CPU exceptions
// IRQ's (32-63)
Hardware(u8),
SerialIn = 32,
// Syscalls (64-255)
// OS defined, program uses INT 64-127
Software(u8) = 128,
}
impl Interrupt {
pub fn code(&self) -> u8 {
match self {
Interrupt::InvalidInterrupt => 0,
Interrupt::PageFault => 1,
Interrupt::ProtectionFault => 2,
Interrupt::InvalidOpcode => 3,
Interrupt::DivideByZero => 4,
Interrupt::StackOverflow => 5,
Interrupt::WriteToReadOnly => 6,
Interrupt::ReadFromWriteOnly => 7,
Interrupt::UnmappedIo => 8,
Interrupt::SerialIn => 32,
Interrupt::Hardware(x) => {
if *x < 32 || *x > 63 {
0
} else {
*x
}
}
Interrupt::Software(x) => {
if *x < 64 {
0
} else {
*x
}
}
}
}
}
@@ -0,0 +1,13 @@
mod state;
mod dsa_core;
mod dsa_debug;
mod dsa_optimised;
mod interrupts;
pub use dsa_core::DsaProcessor;
pub use interrupts::Interrupt;
pub use dsa_optimised::Emulator;
pub use state::{
RunningState,
SharedState,
};
@@ -0,0 +1,56 @@
use std::sync::{Arc, atomic::AtomicBool, mpsc};
use std::sync::atomic::AtomicIsize;
use std::time::Duration;
use arc_swap::ArcSwap;
use common::prelude::Register;
use super::interrupts::Interrupt;
pub struct SharedState {
pub proc: ArcSwap<ProcessorSnapshot>,
pub reset_requested: AtomicBool,
pub pause_requested: AtomicBool,
pub update_requested: AtomicBool,
pub interrupt_queue: mpsc::Sender<Interrupt>,
}
impl SharedState {
pub fn new(sender: mpsc::Sender<Interrupt>) -> Self {
Self {
reset_requested: AtomicBool::new(false),
proc: ArcSwap::new(Arc::new(ProcessorSnapshot::default())),
pause_requested: AtomicBool::new(true),
interrupt_queue: sender,
update_requested: 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],
}
}
}
+32
View File
@@ -0,0 +1,32 @@
[package]
name = "dsa_gui"
version.workspace = true
edition.workspace = true
authors.workspace = true
[[bin]]
name = "dsa"
path = "src/main.rs"
[lib]
name = "dsa"
path = "src/lib.rs"
[dependencies]
common = { path = "../../libraries/common" }
dsa_formats = { path = "../../libraries/dsa_formats" }
assembler = { path = "../../tooling/assembler" }
dsa_emulator = { version = "0.3.0", path = "../dsa_emulator" }
disassembler = { version = "0.3.0", path = "../../tooling/disassembler" }
clap = { version = "4.5.60", features = ["derive"] }
ron = "0.12.0"
egui = "0.35.0"
eframe = "0.35.0"
egui_tiles = "0.16.0"
egui_code_editor = "0.3.7"
egui_file = "0.27.0"
egui_extras = "0.35.0"
egui-file-dialog = "0.14.1"
egui_commonmark = { version = "0.24.0", features = ["better_syntax_highlighting", "macros"] }
+61
View File
@@ -0,0 +1,61 @@
use std::{fs, path::PathBuf};
use clap::Parser;
use dsa_emulator::config::{IoMap, MemoryMap};
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct DsaArgs {
/// Memory map to use on boot.
/// Overrides default value.
#[arg(long = "mmap")]
memory_map: Option<PathBuf>,
#[arg(long = "bin")]
binary: Option<PathBuf>,
#[arg(long = "iomap")]
io_map: Option<PathBuf>,
#[arg(long = "headless")]
headless: bool,
}
impl DsaArgs {
pub fn headless(&self) -> bool {
self.headless
}
pub fn get_memory_map(&self) -> MemoryMap {
self.memory_map
.as_ref()
.and_then(|m| {
fs::read_to_string(m)
.ok()
.and_then(|map| ron::from_str(&map).ok())
})
.unwrap_or_else(|| {
MemoryMap::default()
})
}
pub fn get_binary(&self) -> Option<Vec<u8>> {
self.binary
.clone()
.and_then(|path| Some(fs::read(path).unwrap()))
}
pub fn get_io_map(&self) -> IoMap {
self.io_map
.as_ref()
.and_then(|m| {
fs::read_to_string(m)
.ok()
.and_then(|map| ron::from_str(&map).ok())
})
.unwrap_or_else(|| {
IoMap::default()
})
}
}
+18
View File
@@ -0,0 +1,18 @@
use crate::ui::DamnSimpleUI;
use eframe::NativeOptions;
use dsa_emulator::EmulatorController;
pub mod ui;
pub mod args;
pub mod state;
pub fn run_app(mut controller: EmulatorController) -> eframe::Result<()> {
// initialise dsa_emulator
controller.run().unwrap();
eframe::run_native(
"Damn Simple Architecture",
NativeOptions::default(),
Box::new(|cc| Ok(Box::new(DamnSimpleUI::new(cc, controller)))),
)
}
+15
View File
@@ -0,0 +1,15 @@
use clap::Parser;
use dsa::args::DsaArgs;
use dsa::run_app;
use dsa_emulator::{DsaImplementation, EmulatorController, processor::Emulator};
fn main() {
let args = DsaArgs::parse();
let mut emulator = Emulator::new();
emulator.apply_memory_map(args.get_memory_map());
let controller = EmulatorController::new(Box::new(emulator));
run_app(controller).unwrap();
}
+29
View File
@@ -0,0 +1,29 @@
use crate::ui::process_manager::ProcessSpawner;
use egui_tiles::TileId;
use std::fmt::Debug;
use std::path::PathBuf;
/// the action enum forms the core of the application's state.
/// - Actions are used as the main way that Tiles inside the application pass state updates back to the parent UI,
/// specifically when those state updates involve other files. for example, pressing "build" in the editor would emit
/// a SpawnProcess action, running some task which on completion emits a LoadBinary action on the result
pub enum Action {
None,
RequestCloseTab(TileId),
CloseTab(TileId),
Quit,
OpenFile(PathBuf),
LoadBinary(DataSource),
SaveFile(PathBuf, TileId),
RequestSaveAs(TileId),
ShowError(String),
SpawnProcess(ProcessSpawner)
}
#[derive(Debug, Clone)]
pub enum DataSource {
File(PathBuf),
Data(Vec<u8>),
}
+43
View File
@@ -0,0 +1,43 @@
pub fn configure_appearance(ctx: &egui::Context) {
// configure appearance of UI elements
let mut visuals = egui::Visuals::dark();
visuals.window_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.panel_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.widgets.inactive.fg_stroke =
egui::Stroke::from((1.0, egui::Color32::from_rgb(255, 255, 255)));
visuals.widgets.inactive.bg_stroke =
egui::Stroke::from((1.0, egui::Color32::from_rgb(60, 60, 60)));
visuals.widgets.inactive.corner_radius = egui::CornerRadius::from(4);
visuals.widgets.inactive.bg_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.widgets.inactive.weak_bg_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.widgets.inactive.expansion = 1.0;
// tile resize handle
visuals.window_stroke = egui::Stroke::new(1.0, egui::Color32::from_rgb(45, 45, 45));
visuals.extreme_bg_color = egui::Color32::from_rgb(20, 20, 20);
ctx.set_visuals(visuals);
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert(
"JetBrains Mono Nerd Font".to_string(),
std::sync::Arc::new(egui::FontData::from_static(include_bytes!(
"../../font/JetBrainsMonoNerdFontMono_Regular.ttf",
))),
);
fonts
.families
.entry(egui::FontFamily::Proportional)
.or_default()
.insert(0, "JetBrains Mono Nerd Font".to_string());
fonts
.families
.entry(egui::FontFamily::Monospace)
.or_default()
.insert(0, "JetBrains Mono Nerd Font".to_string());
ctx.set_fonts(fonts);
}
+92
View File
@@ -0,0 +1,92 @@
use eframe::epaint::Color32;
use common::prelude::Register;
use dsa_emulator::EmulatorHandle;
use dsa_emulator::processor::RunningState;
use crate::ui::ui_helpers::PaddedWidget;
pub struct Controller {
emulator: EmulatorHandle,
}
impl Controller {
pub fn new(state: EmulatorHandle) -> Self {
Self {
emulator: state,
}
}
pub fn ui(&mut self, ui: &mut egui::Ui) {
self.emulator.request_update();
egui::Frame::new()
.inner_margin(10.0)
.show(ui, |ui| {
ui.horizontal(|ui| {
use RunningState::*;
let status = self.emulator.status();
match status {
Paused | Breakpoint(_) =>
if PaddedWidget::button("▶ Run").color(Color32::GREEN).ui(ui).clicked() {
self.emulator.request_resume()
}
Running | Halted =>
if PaddedWidget::button("⏸ Pause").color(Color32::YELLOW).ui(ui).clicked() {
self.emulator.request_pause()
}
Shutdown => {
ui.add_enabled_ui(false, |ui| {
PaddedWidget::button("▶ Run").color(Color32::GREEN).ui(ui);
});
}
}
PaddedWidget::indicator(status).ui(ui);
if PaddedWidget::button("⟳ Reset All").color(Color32::ORANGE).ui(ui).clicked() {
self.emulator.when_paused(|emu| emu.reset());
}
// 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());
// });
// }
// 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.separator();
let clock = self.emulator.clock();
PaddedWidget::label(format!("Clock: {clock}")).ui(ui);
let pcx = self.emulator.get_register(Register::Pcx);
PaddedWidget::label( format!("Pcx: 0x{pcx:08X}")).ui(ui);
let time = self.emulator.time().as_micros();
PaddedWidget::label( format!("Elapsed: {} micros", time)).ui(ui);
let mips = (clock as u128).checked_div(time).unwrap_or(0);
PaddedWidget::label(format!("Freq: {}MHz", mips)).ui(ui);
});
});
}
}
@@ -0,0 +1,82 @@
use egui::{Modal, Id, Layout, Align, RichText};
use crate::ui::ui_helpers::PaddedWidget;
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 PaddedWidget::button("Confirm").ui(ui).clicked() {
result = Some(true);
}
ui.add_space(6.0);
if PaddedWidget::button("Cancel").ui(ui).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,39 @@
use std::path::PathBuf;
use egui_file_dialog::FileDialog;
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
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod file_dialog;
pub mod confirm_dialog;
pub mod error_dialog;
+281
View File
@@ -0,0 +1,281 @@
use crate::state::{Action, DataSource};
use crate::ui::controller::Controller;
use crate::ui::dialogs::error_dialog::ErrorDialog;
use crate::ui::tiles::about::AboutScreen;
use crate::ui::tiles::disassembler::BinaryViewer;
use crate::ui::tiles::memory::MemoryInspector;
use crate::ui::tiles::registers::Registers;
use crate::ui::tiles::{CentralTiles, TilePane};
use dialogs::confirm_dialog::ConfirmDialog;
use dialogs::file_dialog::DsaFileDialog;
use dsa_formats::binary_dse::DseExecutable;
use egui::{CentralPanel, Panel, Ui};
use egui_tiles::TileId;
use dsa_emulator::EmulatorController;
use dsa_emulator::devices::display::Display;
use dsa_emulator::devices::serial::Serial;
use tiles::PanelFactory;
use tiles::documentation::Documentation;
use tiles::editor::Editor;
use tiles::io_devices::framebuffer::FrameBufferUi;
use process_manager::ProcessManager;
use std::fs::File;
use std::io::BufReader;
use std::time::Duration;
mod dialogs;
mod tiles;
mod ui_helpers;
mod controller;
pub mod process_manager;
pub mod appearance;
pub struct DamnSimpleUI {
emulator: EmulatorController,
central_tiles: CentralTiles,
controller: Controller,
confirm_dialog: ConfirmDialog<Action>,
file_dialog: DsaFileDialog<Action>,
error_dialog: ErrorDialog,
tools: Vec<PanelFactory>,
show_process_manager: bool,
process_manager: ProcessManager
}
impl eframe::App for DamnSimpleUI {
fn ui(&mut self, ui: &mut 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);
}
while let Some(action) = self.process_manager.get_action() {
self.perform_action(action);
}
self.error_dialog.show(ui);
Panel::top("menu_bar").resizable(false).show(ui, |ui| {
self.toolbar(ui);
});
Panel::top("control_panel").resizable(false).show(ui, |ui| {
self.controller.ui(ui);
});
Panel::left("left_panel").resizable(false).show(ui, |ui| {
// TODO: File Explorer!
});
Panel::bottom("bottom_panel").resizable(true).show_collapsible(ui, &mut self.show_process_manager,|ui| {
self.process_manager.ui(ui);
});
CentralPanel::default().show(ui, |ui| {
if let Some(action) = self.central_tiles.ui(ui) {
self.perform_action(action);
}
});
}
}
impl DamnSimpleUI {
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,
mut emulator: EmulatorController,
) -> Self {
appearance::configure_appearance(&cc.egui_ctx);
let handle = emulator.get_handle().clone();
Self {
central_tiles: CentralTiles::new(),
controller: Controller::new(handle.clone()),
confirm_dialog: ConfirmDialog::new(),
file_dialog: DsaFileDialog::new(),
error_dialog: ErrorDialog::default(),
tools: vec![
PanelFactory::register("Memory Inspector", {
let handle = handle.clone();
move || MemoryInspector::new(handle.clone())
}),
PanelFactory::register("Documentation", Documentation::default),
PanelFactory::register("Display", {
let handle = handle.clone();
move || Display::new(80, 25, 0x20000, handle.clone())
}),
PanelFactory::register("Framebuffer", {
let handle = handle.clone();
move || FrameBufferUi::new(320, 240, 0x30000, handle.clone())
}),
PanelFactory::register("Serial", {
let handle = handle.clone();
move || Serial::new(0x40000, handle.clone())
}),
PanelFactory::register("Registers", {
let handle = handle.clone();
move || Registers::new(handle.clone())
})
],
emulator,
show_process_manager: true,
process_manager: ProcessManager::new()
}
}
pub fn perform_action(&mut self, action: Action) {
match action {
Action::None => {},
Action::SpawnProcess(spawner) => spawner(&mut self.process_manager),
Action::RequestCloseTab(tile_id) => {
self.confirm_dialog.open(
"Close Tab",
"Closing this tab may result in losing unsaved work",
Action::CloseTab(tile_id)
);
}
Action::CloseTab(tile_id) => {
self.central_tiles.close_tab(tile_id);
}
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" | "dsc") => { self.central_tiles.add_tab(Editor::new_open(&path)); },
Some("dsb") => { self.central_tiles.add_tab(BinaryViewer::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(DataSource::Data(exe)) => {
self.emulator.get_handle().when_paused(|emu| unsafe {
emu.mem_write(0, &exe);
});
}
Action::LoadBinary(DataSource::File(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.get_handle().when_paused(|emu| unsafe {
emu.reset();
emu.mem_write(0, &binary.to_memory_image());
});
}
}
Action::SaveFile(path, tile) => {
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));
}
};
}
fn toolbar(&mut self, ui: &mut 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(|path| Action::LoadBinary(DataSource::File(path)));
}
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);
}
});
});
}
}
+150
View File
@@ -0,0 +1,150 @@
use crate::state::Action;
use crate::ui::ui_helpers::text::strip_ansi;
use crate::ui::ui_helpers::painted_tab;
use common::process::{ProcessAgent, TraceableProcess};
use std::collections::VecDeque;
use std::sync::mpsc;
use std::thread::JoinHandle;
pub type ProcessSpawner = Box<dyn FnOnce(&mut ProcessManager) + Send>;
pub struct ProcessHandle {
id: usize,
name: String,
rx: mpsc::Receiver<String>,
join_handle: Option<JoinHandle<Action>>,
// current process state
logs: Vec<String>,
}
impl ProcessHandle {
pub fn spawn<P: TraceableProcess>(
id: usize,
name: impl Into<String>,
process: P,
on_complete: impl FnOnce(Result<P::Output, P::Error>) -> Action + Send + 'static
) -> Self {
let (tx, rx) = mpsc::channel::<String>();
let agent = ProcessAgent::new( tx );
let join_handle = std::thread::spawn(move || {
let result = TraceableProcess::run(Box::new(process), agent);
on_complete(result)
});
Self {
rx, id, name: name.into(), join_handle: Some(join_handle), logs: Vec::new()
}
}
}
#[derive(Default)]
pub struct ProcessManager {
_id_counter: usize,
handles: Vec<ProcessHandle>,
terminated_actions: VecDeque<Action>,
active_tab: Option<usize>,
}
impl ProcessManager {
pub fn new() -> Self {
Default::default()
}
fn new_id(&mut self) -> usize {
let id = self._id_counter;
self._id_counter += 1;
id
}
pub fn get_action(&mut self) -> Option<Action> {
self.terminated_actions.pop_front()
}
pub fn spawn<P: TraceableProcess>(&mut self, name: impl Into<String>, process: P, on_complete: impl FnOnce(Result<P::Output, P::Error>) -> Action + Send + 'static) {
let handle = ProcessHandle::spawn(self.new_id(), name, process, on_complete);
self.handles.push(handle);
}
fn drain_logs(&mut self) {
for handle in &mut self.handles {
while let Ok(line) = handle.rx.try_recv() {
handle.logs.push(strip_ansi(&line));
}
}
}
/// Checks for finished processes and queues their resulting Actions.
fn poll_finished(&mut self) {
for handle in &mut self.handles {
let is_finished = handle.join_handle.as_ref().is_some_and(|jh| jh.is_finished());
if is_finished {
if let Some(jh) = handle.join_handle.take() {
if let Ok(action) = jh.join() {
self.terminated_actions.push_back(action);
}
}
}
}
}
pub fn ui(&mut self, ui: &mut egui::Ui) {
self.drain_logs();
self.poll_finished();
if self.handles.is_empty() {
ui.centered_and_justified(|ui| {
ui.weak("No processes running.");
});
return;
}
// keep active_tab pointing at something valid
if !self.active_tab.map_or(false, |id| self.handles.iter().any(|h| h.id == id)) {
self.active_tab = self.handles.first().map(|h| h.id);
}
let mut to_close = None;
ui.horizontal(|ui| {
for handle in &self.handles {
let selected = self.active_tab == Some(handle.id);
let tab_id = ui.auto_id_with(("process_tab", handle.id));
let tab = painted_tab(ui, tab_id, handle.name.clone(), selected, true, false);
if tab.response.clicked() {
self.active_tab = Some(handle.id);
}
if tab.close_clicked {
to_close = Some(handle.id);
}
}
});
if let Some(id) = to_close {
self.handles.retain(|h| h.id != id);
if self.active_tab == Some(id) {
self.active_tab = self.handles.first().map(|h| h.id);
}
}
if let Some(active_id) = self.active_tab {
// --- log view for the active tab ---
if let Some(handle) = self.handles.iter().find(|h| h.id == active_id) {
egui::ScrollArea::vertical()
.id_salt(("process_log", active_id))
.stick_to_bottom(true)
.auto_shrink([false, false])
.show(ui, |ui| {
for line in &handle.logs {
ui.monospace(line);
}
});
}
}
}
}
+34
View File
@@ -0,0 +1,34 @@
use crate::state::Action;
use crate::ui::tiles::TilePane;
use eframe::epaint::Direction;
use egui::{Ui, WidgetText};
use egui_tiles::TileId;
use dsa_emulator::config::VERSION;
use std::any::Any;
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,282 @@
use std::any::Any;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use std::sync::Arc;
use std::sync::mpsc;
use std::thread;
use egui::{ScrollArea, Ui, WidgetText};
use egui_extras::{Size, StripBuilder};
use egui_file::FileDialog;
use egui_tiles::TileId;
use crate::state::{Action, DataSource};
use crate::ui::tiles::TilePane;
use crate::ui::tiles::memory::MemoryViewer;
use disassembler::{Disassembly, disassemble};
use dsa_formats::binary_dse::DseExecutable;
#[derive(PartialEq, Clone, Copy)]
enum ViewMode {
Memory,
Disassembly,
}
enum DisassemblyState {
Idle,
Running(mpsc::Receiver<Disassembly>),
Done(Disassembly),
}
pub struct BinaryViewer {
dialog: Option<FileDialog>,
error: Option<String>,
program: Option<Arc<DseExecutable>>,
memory_image: Option<Vec<u8>>,
view: ViewMode,
memory_viewer: MemoryViewer,
disassembly: DisassemblyState,
}
impl BinaryViewer {
pub fn new() -> Self {
Self {
dialog: None,
error: None,
program: None,
memory_image: None,
view: ViewMode::Memory,
memory_viewer: MemoryViewer::new(),
disassembly: DisassemblyState::Idle,
}
}
pub fn new_open(path: &Path) -> Self {
let mut viewer = Self::new();
match File::open(path).map(|f| DseExecutable::read(&mut BufReader::new(f))) {
Ok(Ok(bin)) => viewer.load_program(bin),
Ok(Err(e)) => viewer.error = Some(format!("Failed to load {}: {e}", path.display())),
Err(e) => viewer.error = Some(format!("Failed to load {}: {e}", path.display())),
}
viewer
}
fn load_program(&mut self, bin: DseExecutable) {
// TODO: replace with however DseExecutable actually exposes its raw memory image.
// Assuming a method like this exists — swap for the real accessor.
self.memory_image = Some(bin.to_memory_image());
self.program = Some(Arc::new(bin));
self.disassembly = DisassemblyState::Idle;
self.view = ViewMode::Memory;
self.error = 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 };
let file = file.to_path_buf();
self.dialog = None;
if !file.extension().is_some_and(|ext| ext == "dsb") {
self.error = Some("Selected file is not a .dsb binary".into());
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.load_program(bin),
Err(e) => self.error = Some(e.to_string()),
}
}
/// Kicks disassembly off on a background thread so a large binary
/// doesn't stall the UI. `DseExecutable` is wrapped in `Arc` so the
/// thread can borrow it without cloning the whole binary image.
fn start_disassembly(&mut self) {
let Some(program) = self.program.clone() else { return };
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let result = disassemble(&program);
let _ = tx.send(result);
});
self.disassembly = DisassemblyState::Running(rx);
self.view = ViewMode::Disassembly;
}
fn poll_disassembly(&mut self) {
if let DisassemblyState::Running(rx) = &self.disassembly {
if let Ok(result) = rx.try_recv() {
self.disassembly = DisassemblyState::Done(result);
}
}
}
fn toolbar(&mut self, ui: &mut Ui) -> Option<Action> {
let mut action = None;
ui.horizontal(|ui| {
ui.spacing_mut().button_padding = egui::vec2(8.0, 4.0);
ui.spacing_mut().item_spacing.x = 6.0;
if ui.button("Open").clicked() {
self.open();
}
let has_program = self.program.is_some();
if ui.add_enabled(has_program, egui::Button::new("Load")).clicked() {
if let Some(data) = &self.memory_image {
action = Some(Action::LoadBinary(DataSource::Data(data.clone())));
}
}
let disassembling = matches!(self.disassembly, DisassemblyState::Running(_));
if ui
.add_enabled(has_program && !disassembling, egui::Button::new("Disassemble"))
.clicked()
{
self.start_disassembly();
}
ui.separator();
ui.selectable_value(&mut self.view, ViewMode::Memory, "Memory");
let has_disassembly = !matches!(self.disassembly, DisassemblyState::Idle);
ui.add_enabled_ui(has_disassembly, |ui| {
ui.selectable_value(&mut self.view, ViewMode::Disassembly, "Disassembly");
});
if let Some(err) = &self.error {
ui.separator();
ui.colored_label(egui::Color32::from_rgb(220, 80, 80), err);
}
});
action
}
}
impl TilePane for BinaryViewer {
fn title(&self) -> WidgetText {
"Binary Viewer".into()
}
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
self.handle_file_dialog(ui);
self.poll_disassembly();
if matches!(self.disassembly, DisassemblyState::Running(_)) {
ui.ctx().request_repaint(); // keep polling promptly while the background thread works
}
let action = self.toolbar(ui);
ui.separator();
match self.view {
ViewMode::Memory => match &self.memory_image {
Some(memory) => self.memory_viewer.ui(ui, memory, 0),
None => {
ui.centered_and_justified(|ui| {
ui.weak("Open a binary to inspect its memory image.");
});
}
},
ViewMode::Disassembly => match &self.disassembly {
DisassemblyState::Idle => {
ui.centered_and_justified(|ui| {
ui.weak("Click Disassemble to generate a disassembly.");
});
}
DisassemblyState::Running(_) => {
ui.centered_and_justified(|ui| {
ui.horizontal(|ui| {
ui.spinner();
ui.label("Disassembling…");
});
});
}
DisassemblyState::Done(output) => render_disassembly(ui, output),
},
}
action
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
fn render_disassembly(ui: &mut Ui, output: &Disassembly) {
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])
.show(ui, |ui| {
ui.strong("Addr");
ui.strong("Name");
ui.strong("Section");
ui.strong("Bind");
ui.end_row();
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();
}
});
});
});
});
}
@@ -0,0 +1,60 @@
use crate::state::Action;
use crate::ui::tiles::TilePane;
use egui::{Ui, WidgetText};
use egui_commonmark::{CommonMarkCache, CommonMarkViewer};
use egui_tiles::TileId;
use std::any::Any;
use std::default::Default;
use std::path::Path;
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
}
}
+175
View File
@@ -0,0 +1,175 @@
use crate::state::Action;
use crate::state::DataSource::Data;
use crate::ui::tiles::TilePane;
use crate::ui::tiles::editor::syntax::SyntaxFrom;
use crate::ui::ui_helpers::PaddedWidget;
use assembler::prelude::Assembler;
use common::process::TraceableProcess;
use dsa_formats::binary_dse::DseExecutable;
use egui::{Color32, Key, Ui, Widget};
use egui_code_editor::{CodeEditor, Completer, Syntax};
use egui_tiles::TileId;
use std::any::Any;
use std::fs::File;
use std::path::{Path, PathBuf};
use theme::THEME;
mod syntax;
mod theme;
pub struct Editor {
buffer: String,
filename: Option<PathBuf>,
editor: CodeEditor,
syntax: Syntax,
completer: Completer,
}
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),
syntax: Syntax::default(),
completer: Completer::default().with_auto_indent().with_user_words(),
}
}
}
impl Editor {
pub fn new_open(path: &Path) -> Self {
let syntax = Syntax::from_file(path).unwrap_or(syntax::dsa());
Self {
buffer: std::fs::read_to_string(path).unwrap(),
filename: Some(path.to_path_buf()),
syntax: syntax.clone(),
completer: Completer::new_with_syntax(&syntax)
.with_auto_indent()
.with_user_words(),
..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
}
pub fn get_build_command(filename: PathBuf) -> impl TraceableProcess {
match filename.extension().unwrap().to_str().unwrap() {
"dsa" => Assembler::new(filename),
_ => todo!(),
}
}
pub fn create_build_action(&self, filename: PathBuf, on_complete: impl FnOnce(DseExecutable) -> Action + Send + 'static) -> Action {
Action::SpawnProcess(Box::new(move |pm| {
let builder = Assembler::new(filename);
pm.spawn(
builder.label(),
builder,
move |result| match result {
Ok(bin) => on_complete(bin),
Err(e) => Action::ShowError(e.to_string()),
}
);
}))
}
}
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> {
let mut action = Action::None;
egui::Panel::top("panel").show(ui, |ui| {
ui.horizontal(|ui| {
if PaddedWidget::button("Build & Load").ui(ui).clicked() {
let Some(filename) = self.filename.clone() else {
action = Action::ShowError("Error - no filename set".to_string());
return;
};
action = self.create_build_action(
filename, |res| {
Action::LoadBinary(Data(res.to_memory_image()))
}
);
}
if PaddedWidget::button("Build").ui(ui).clicked() {
let Some(filename) = self.filename.clone() else {
action = Action::ShowError("Error - no filename set".to_string());
return;
};
action = self.create_build_action(
filename.clone(), move |res| {
let filename = filename.with_extension("dsb");
let mut writer = File::create(&filename).unwrap();
res.write(&mut writer).unwrap();
Action::OpenFile(filename)
}
);
}
PaddedWidget::label(
self.filename
.as_ref()
.map(|f| f.display().to_string())
.unwrap_or_else(|| String::from("New File"))
).color(Color32::DARK_GRAY).ui(ui);
});
});
self.editor.show_with_completer(ui, &mut self.buffer, &self.syntax, &mut self.completer);
if ui.input(|i| i.key_pressed(Key::S) && i.modifiers.ctrl) {
action = if let Some(filename) = self.filename.clone() {
Action::SaveFile(filename, tile_id)
} else {
Action::RequestSaveAs(tile_id)
}
};
Some(action)
}
fn as_any_mut(&mut self) -> &mut dyn Any { self }
fn close_requires_confirmation(&mut self, _tile_id: TileId) -> bool {
true
}
}
@@ -0,0 +1,66 @@
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use egui_code_editor::{Patch, Syntax};
pub trait SyntaxFrom {
fn from_file(f: &Path) -> Option<Syntax>;
}
impl SyntaxFrom for Syntax {
fn from_file(f: &Path) -> Option<Syntax> {
match f.extension()?.to_str()? {
"dsa" => Some(dsa()),
"dsc" => Some(dsc()),
_ => None,
}
}
}
pub fn dsa() -> Syntax {
Syntax {
word_start: BTreeSet::new(),
quotes: BTreeSet::from(['"', '\'']),
language: "Assembly",
case_sensitive: false,
comment: "//",
comment_multiline: ["/*", "*/"],
hyperlinks: BTreeSet::from(["HTTP", "HTTPS"]),
keywords: BTreeSet::from([
"NOP", "MOV", "CMOV", "LDB", "LDBS", "LDH", "LDHS", "LDW", "STB", "STH", "STW", "LLI",
"LUI", "LWI", "JMP", "JNZ", "JEZ", "JNC", "JIC", "IEQ", "INE", "IGE", "IGT", "ILE",
"ILT", "SHL", "SHR", "ADD", "SUB", "AND", "OR", "NOT", "XOR", "NAND", "NOR", "XNOR",
"IRET", "RET", "CALL", "INT", "PUSH", "POP", "HLT",
]),
types: BTreeSet::from(["DB", "DW", "DH", "RESB", "RESH", "RESW", "INCLUDE"]),
special: BTreeSet::from([
"RG0", "RG1", "RG2", "RG3", "RG4", "RG5", "RG6", "RG7", "RG8", "RG9", "RGA", "RGB",
"RGC", "RGD", "RGE", "RGF", "ACC", "SPR", "BPR", "IDR", "MMR", "ZERO", "PCX", "STS",
"CIR",
]),
patch: Patch::default(),
}
}
pub fn dsc() -> Syntax {
Syntax {
word_start: BTreeSet::from(['_']),
quotes: BTreeSet::from(['"', '\'', '`']),
language: "Damn Simple Code",
case_sensitive: false,
comment: "//",
comment_multiline: ["/*", "*/"],
hyperlinks: BTreeSet::from(["HTTP", "HTTPS"]),
keywords: BTreeSet::from([
"INCLUDE", "FN", "LET", "CONST", "STATIC", "IF", "ELSE", "WHILE", "FOR", "BREAK",
"CONTINUE", "LOOP", "RETURN", "CLASS", "DEFER", "PUB", "CONST",
]),
types: BTreeSet::from([
"U32", "U16", "U8", "I32", "I16", "I8", "STR", "CHAR", "BOOL", "VOID",
]),
special: BTreeSet::from([
",", ";", ".", ":", "=", "+", "-", "*", "/", "%", "&", "|", "^", "~", "!", "?", "<",
">", "<<", ">>", "==", "!=", "<=", ">=", "&&", "||",
]),
patch: Patch::default(),
}
}
@@ -0,0 +1,18 @@
use egui_code_editor::ColorTheme;
pub const THEME: ColorTheme = ColorTheme {
name: "Theme",
dark: true,
bg: "1b1b1b",
cursor: "de5252", // fg4
selection: "28323B", // bg2
comments: "444444", // gray1
functions: "7CCCC7", // green1
keywords: "6C81E0", // red1
literals: "A3ABFF", // fg1
numerics: "8A46CF", // purple1
punctuation: "99C9C9", // orange1
strs: "618c84", // aqua1
types: "B8B9D4", // yellow1
special: "de5252", // blue1
};
@@ -0,0 +1,68 @@
use crate::state::Action;
use crate::ui::tiles::TilePane;
use egui::{Color32, FontId, Ui, Vec2, WidgetText};
use egui_tiles::TileId;
use dsa_emulator::devices::display::Display;
use std::any::Any;
impl TilePane for Display {
fn title(&self) -> WidgetText {
"Display".into()
}
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
let display_w = self.width();
let display_h = self.height();
let data = self.read();
let font_id = FontId::monospace(12.0);
let char_width = ui.fonts_mut(|f| f.glyph_width(&font_id, 'W'));
let line_height = ui.fonts_mut(|f| f.row_height(&font_id));
#[expect(clippy::cast_precision_loss)]
let display_size = Vec2::new(
char_width * display_w as f32,
line_height * display_h as f32,
);
let (rect, _response) = ui.allocate_exact_size(display_size, egui::Sense::all());
// ui.painter().rect_filled(rect, 0.0, Color32::BLACK);
// Draw text
for y in 0..display_h {
let mut row_text = String::with_capacity(display_w);
for x in 0..display_w {
let index = y * display_w + x;
if index < data.len() {
let byte = data[index];
let ch = if (32..=126).contains(&byte) {
byte as char
} else {
' '
};
row_text.push(ch);
} else {
row_text.push(' ');
}
}
#[expect(clippy::cast_precision_loss)]
let text_pos = rect.min + Vec2::new(0.0, y as f32 * line_height);
ui.painter().text(
text_pos,
egui::Align2::LEFT_TOP,
row_text,
font_id.clone(),
Color32::WHITE,
);
}
None
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
@@ -0,0 +1,79 @@
use crate::state::Action;
use crate::ui::tiles::TilePane;
use egui::{Ui, Vec2, WidgetText};
use egui_tiles::TileId;
use dsa_emulator::EmulatorHandle;
use dsa_emulator::devices::framebuffer::FrameBuffer;
use dsa_emulator::memory::PhysAddr;
use std::any::Any;
pub struct FrameBufferUi {
device: FrameBuffer,
texture: Option<egui::TextureHandle>,
}
impl FrameBufferUi {
pub fn new(width: u32, height: u32, addr: PhysAddr, emulator: EmulatorHandle) -> Self {
Self {
device: FrameBuffer::new(width, height, addr, emulator),
texture: None,
}
}
}
impl TilePane for FrameBufferUi {
fn title(&self) -> WidgetText {
"Framebuffer".into()
}
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
let frame = self.device.read();
let width = self.device.width();
let height = self.device.height();
// get or create the texture
let texture = self.texture.get_or_insert_with(|| {
ui.load_texture(
"framebuffer",
egui::ColorImage {
size: [width, height],
source_size: Vec2::from([width as f32, height as f32]),
pixels: vec![egui::Color32::BLACK; width * height],
},
egui::TextureOptions::NEAREST,
)
});
let pixels: Vec<egui::Color32> = frame
.as_chunks::<4>().0
.iter()
.map(|&[red, green, blue, _alpha]| {
egui::Color32::from_rgba_premultiplied(red, green, blue, 255)
})
.collect();
texture.set(
egui::ColorImage {
size: [width, height],
source_size: Vec2::from([width as f32, height as f32]),
pixels,
},
egui::TextureOptions::NEAREST,
);
// scale to fit available space while preserving aspect ratio
let available = ui.available_size();
let aspect = width as f32 / height as f32;
let size = if available.x / aspect <= available.y {
egui::vec2(available.x, available.x / aspect)
} else {
egui::vec2(available.y * aspect, available.y)
};
ui.image(egui::load::SizedTexture::new(texture.id(), size));
None
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
@@ -0,0 +1,3 @@
pub mod display;
pub mod framebuffer;
pub mod serial;
@@ -0,0 +1,37 @@
use crate::state::Action;
use crate::ui::tiles::TilePane;
use egui::{Ui, WidgetText};
use egui_tiles::TileId;
use dsa_emulator::devices::serial::Serial;
use std::any::Any;
impl TilePane for Serial {
fn title(&self) -> WidgetText {
"Serial".into()
}
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
self.update();
// Text field for input and a send button
ui.text_edit_multiline(&mut self.write_buff);
if ui.button("Send").clicked() {
self.write_buff.push(0 as char);
for byte in self.write_buff.bytes() {
let _ = self.write_tx.send(byte);
}
self.write_buff.clear();
}
// Display received data
ui.label(String::from_utf8(self.read_buff.clone()).unwrap());
if ui.button("Clear").clicked() {
self.read_buff.clear();
}
None
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
+271
View File
@@ -0,0 +1,271 @@
use crate::state::Action;
use crate::ui::tiles::TilePane;
use common::prelude::Instruction;
use egui::{Ui, WidgetText};
use egui_extras::{Column, TableBuilder};
use egui_tiles::TileId;
use dsa_emulator::EmulatorHandle;
use std::any::Any;
use std::num::ParseIntError;
pub struct MemoryInspector {
emulator: EmulatorHandle,
view_size: u32,
view_addr: u32,
addr_input: String,
// settings
memory_viewer: MemoryViewer
}
impl MemoryInspector {
#[must_use]
pub fn new(emulator: EmulatorHandle) -> Self {
Self {
emulator,
view_size: 1024,
view_addr: 0,
addr_input: String::from("0x00"),
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> {
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 window = self.emulator.mem_read(self.view_addr, self.view_size);
self.memory_viewer.ui(ui, &window, self.view_addr as usize);
None
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
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.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(75.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 memory.as_chunks::<4>().0.iter().enumerate() {
body.row(18.0, |mut row| {
let address = line_num * 4 + offset;
let value = u32::from_le_bytes(*chunk);
// Address column
row.col(|ui| {
ui.label(
egui::RichText::new(format!("0x{address:08X}"))
.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 {
row.col(|ui| {
ui.label(
egui::RichText::new(String::from_utf8_lossy(chunk))
.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)),
);
});
}
})
}
});
});
}
}
fn parse_address(address: &str) -> Result<u32, ParseIntError> {
if let Some(hex_part) = address.strip_prefix("0x") {
return u32::from_str_radix(hex_part, 16);
}
if let Some(bin_part) = address.strip_prefix("0b") {
return u32::from_str_radix(bin_part, 2);
}
if let Some(oct_part) = address.strip_prefix("0o") {
return u32::from_str_radix(oct_part, 8);
}
address.parse::<u32>()
}
+224
View File
@@ -0,0 +1,224 @@
pub mod documentation;
pub mod editor;
pub mod about;
pub mod disassembler;
pub mod memory;
pub mod registers;
pub mod io_devices;
use crate::state::Action;
use crate::state::Action::RequestCloseTab;
use crate::ui::ui_helpers::painted_tab;
use eframe::epaint::{Color32, CornerRadius};
use egui::{Id, Response, Stroke, Ui};
use egui_tiles::{Behavior, Container, SimplificationOptions, TabState, Tile, TileId, Tiles, Tree, UiResponse};
use std::any::Any;
/// 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 {
/// the title that's shown on the tab in the UI.
fn title(&self) -> egui::WidgetText;
/// main UI function. returns an action in order to communicate with the parent UI
fn ui(&mut self, ui: &mut egui::Ui, _tile_id: TileId) -> Option<Action>;
/// required in order to be used with a pane
fn as_any_mut(&mut self) -> &mut dyn Any;
/// the menu options exposed by the tile. these inform the main application menu based on the current focused tile
fn menu(&mut self, _ui: &mut egui::Ui, _tile_id: TileId) -> Option<Action> {
None
}
fn close_requires_confirmation(&mut self, _tile_id: TileId) -> bool {
false
}
}
type Pane = Box<dyn TilePane>;
#[derive(Default)]
struct TileBehavior {
pending_action: Option<Action>,
}
impl Behavior<Pane> for TileBehavior {
fn pane_ui(
&mut self,
ui: &mut egui::Ui,
tile_id: TileId,
pane: &mut Box<dyn TilePane>,
) -> UiResponse {
egui::Frame::new()
.corner_radius(CornerRadius::same(4))
.inner_margin(egui::Margin::same(10))
.stroke(Stroke::new(2.0, Color32::DARK_GRAY))
.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 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 {
let Some(Tile::Pane(pane)) = tiles.get_mut(tile_id) else {
return false
};
if pane.close_requires_confirmation(tile_id) {
self.pending_action = Some(RequestCloseTab(tile_id));
return false;
}
tiles.remove(tile_id);
true
}
fn tab_ui(
&mut self,
tiles: &mut Tiles<Pane>,
ui: &mut Ui,
id: Id,
tile_id: TileId,
state: &TabState,
) -> Response {
let title = self.tab_title_for_tile(tiles, tile_id);
let draggable = self.is_tile_draggable(tiles, tile_id);
let tab = painted_tab(ui, id, title, state.active, state.closable, draggable);
if tab.close_clicked && self.on_tab_close(tiles, tile_id) {
tiles.remove(tile_id);
}
self.on_tab_button(tiles, tile_id, tab.response)
}
fn tab_bar_height(&self, _style: &egui::Style) -> f32 {
36.0 // taller tab row = more vertical padding around the label
}
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()
}
}
fn tab_bg_color(&self, visuals: &egui::Visuals, _tiles: &Tiles<Box<dyn TilePane>>, _tile_id: TileId, tab_state: &TabState) -> egui::Color32 {
visuals.widgets.inactive.bg_fill
}
}
/// 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))
}
pub fn close_tab(&mut self, tile_id: TileId) {
self.tree.tiles.remove(tile_id);
}
/// 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>))
}
}
+129
View File
@@ -0,0 +1,129 @@
use crate::state::Action;
use crate::ui::tiles::TilePane;
use common::prelude::Register;
use egui::{Ui, WidgetText};
use egui_tiles::TileId;
use dsa_emulator::EmulatorHandle;
use std::any::Any;
use std::f32;
pub struct Registers {
state: EmulatorHandle,
}
impl Registers {
fn section(
&mut self,
ui: &mut Ui,
registers: Vec<(Register, u32)>,
col_pairs: usize,
name: &str,
) {
ui.collapsing(name, |ui| {
egui::Grid::new(name)
.num_columns(col_pairs * 2)
.spacing([40.0, 4.0])
.striped(true)
.show(ui, |ui| {
// only show header if there are multiple rows
if registers.len() > col_pairs {
for _ in 0..col_pairs {
ui.label("Register");
ui.label("Value");
}
ui.end_row();
}
for chunk in registers.chunks(col_pairs) {
for (reg, val) in chunk {
ui.label(reg.to_string());
ui.label(format!("0x{:08X} ({:012})", val, val));
}
for _ in chunk.len()..col_pairs {
ui.label("");
ui.label("");
}
ui.end_row();
}
});
});
}
}
impl Registers {
pub fn new(state: EmulatorHandle) -> Self {
Self {
state,
}
}
}
impl TilePane for Registers {
fn title(&self) -> WidgetText {
"Registers".into()
}
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
ui.set_min_width(200.0);
let available = ui.available_width();
let col_pairs = match available {
0.0..350.0 => 1,
350.0..600.0 => 2,
600.0..900.0 => 3,
f32::MIN..0.0 => unreachable!(),
_ => 4,
};
ui.vertical(|ui| {
// ── General Purpose ──────────────────────────────────────
let gp_registers: Vec<(Register, u32)> = (0..=15u8)
.map(|i| (Register::from_u8(i).unwrap(), self.state.registers()[i as usize]))
.collect();
self.section(
ui,
gp_registers,
col_pairs,
"General Purpose Registers",
);
ui.separator();
// ── Stack ─────────────────────────────────────────────────
let stack_registers = vec![
(Register::Spr, self.state.get_register(Register::Spr)),
(Register::Bpr, self.state.get_register(Register::Bpr)),
];
self.section(ui, stack_registers, col_pairs, "Stack Registers");
ui.separator();
// ── Special Purpose ───────────────────────────────────────
let special_registers = vec![
(Register::Acc, self.state.get_register(Register::Acc)),
(Register::Ret, self.state.get_register(Register::Ret)),
(Register::Idr, self.state.get_register(Register::Idr)),
(Register::Mmr, self.state.get_register(Register::Mmr)),
];
self.section(
ui,
special_registers,
col_pairs,
"Special Purpose Registers",
);
ui.separator();
// ── System ────────────────────────────────────────────────
let system_registers = vec![
(Register::Pcx, self.state.get_register(Register::Pcx)),
(Register::Sts, self.state.get_register(Register::Sts)),
(Register::Cir, self.state.get_register(Register::Cir)),
];
self.section(ui, system_registers, col_pairs, "System Registers");
});
None
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
+247
View File
@@ -0,0 +1,247 @@
pub mod text;
use eframe::emath::Vec2;
use egui::{Color32};
use dsa_emulator::processor::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 Into<String>) -> Self {
Self {
text: text.into(),
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: impl Into<String>) -> Self {
Self {
text: text.into(),
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 = if let WidgetType::Indicator = self.widget_type {
galley.size() + egui::vec2(12.0, 0.0) + self.padding.unwrap_or(Vec2::new(14.0, 8.0)) * 2.0
} else {
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,
);
if let WidgetType::Indicator = self.widget_type {
let size = egui::vec2(galley.size().clone().x + 6.0, 0.0);
let text_pos = rect.center() - galley.size() / 2.0 + egui::vec2(8.0, 0.0);
ui.painter().galley(text_pos, galley, self.color.unwrap_or(visuals.fg_stroke.color));
ui.painter().circle_filled(rect.center() - size / 2.0, 8.0 / 2.0, self.color.unwrap());
}
else {
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 trait StatusIndicator {
fn color(&self) -> egui::Color32;
fn label(&self) -> String;
}
impl StatusIndicator for RunningState {
fn color(&self) -> egui::Color32 {
match self {
RunningState::Running => egui::Color32::GREEN,
RunningState::Paused => egui::Color32::YELLOW,
RunningState::Halted => egui::Color32::ORANGE,
RunningState::Breakpoint(_) => egui::Color32::RED,
RunningState::Shutdown => egui::Color32::BLUE,
}
}
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(),
}
}
}
/// Shared paint logic — draws the crossed lines, using whatever
/// interact-state visuals (hover/press color, expansion) egui's style
/// would normally pick for this response.
fn paint_close_x(ui: &egui::Ui, rect: egui::Rect, response: &egui::Response) {
if !ui.is_rect_visible(rect) {
return;
}
let visuals = ui.style().interact(response);
let inner = rect.shrink(4.0).expand(visuals.expansion);
let stroke = visuals.fg_stroke;
ui.painter().line_segment([inner.left_top(), inner.right_bottom()], stroke);
ui.painter().line_segment([inner.right_top(), inner.left_bottom()], stroke);
}
/// Standalone close button — allocates its own space in the current
/// layout, like a normal widget. Use this in a `ui.horizontal(...)` row,
/// e.g. next to a label in a flat list (process manager tabs, a file
/// list with per-row close, etc).
pub fn close_button(ui: &mut egui::Ui, size: f32) -> egui::Response {
let (rect, response) = ui.allocate_exact_size(egui::vec2(size, size), egui::Sense::click());
let response = response.on_hover_cursor(egui::CursorIcon::Default);
response.widget_info(|| {
egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), "Close")
});
paint_close_x(ui, rect, &response);
response
}
/// Close button at an explicit rect you've already computed — for
/// cases like tab_ui, where the button needs to sit inside a larger
/// region you're laying out manually rather than in normal flow.
pub fn close_button_at(ui: &mut egui::Ui, rect: egui::Rect, id: egui::Id) -> egui::Response {
let response = ui
.interact(rect, id, egui::Sense::click_and_drag())
.on_hover_cursor(egui::CursorIcon::Default);
response.widget_info(|| {
egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), "Close")
});
paint_close_x(ui, rect, &response);
response
}
pub struct TabResponse {
pub response: egui::Response,
pub close_clicked: bool,
}
pub fn painted_tab(
ui: &mut egui::Ui,
id: egui::Id,
title: impl Into<egui::WidgetText>,
selected: bool,
closable: bool,
draggable: bool,
) -> TabResponse {
let padding = egui::vec2(14.0, 8.0);
let close_size = 14.0;
let close_gap = 6.0;
let font_id = egui::TextStyle::Button.resolve(ui.style());
let galley = title.into().into_galley(ui, Some(egui::TextWrapMode::Extend), f32::INFINITY, font_id);
let extra_width = if closable { close_size + close_gap } else { 0.0 };
let desired = egui::vec2(
galley.size().x + padding.x * 2.0 + extra_width,
ui.available_height().max(galley.size().y + padding.y * 2.0),
);
let (_, rect) = ui.allocate_space(desired);
let sense = if draggable { egui::Sense::click_and_drag() } else { egui::Sense::click() };
let response = ui.interact(rect, id, sense);
let response = if draggable {
response.on_hover_cursor(egui::CursorIcon::Grab)
} else {
response
};
response.widget_info(|| {
egui::WidgetInfo::selected(egui::WidgetType::Button, ui.is_enabled(), selected, galley.text())
});
let mut close_clicked = false;
if ui.is_rect_visible(rect) {
let visuals = ui.style().interact(&response);
ui.painter().rect(rect, 4.0, visuals.bg_fill, visuals.bg_stroke, egui::StrokeKind::Inside);
let text_color = if selected {
egui::Color32::WHITE
} else {
egui::Color32::DARK_GRAY
};
let text_pos = egui::pos2(rect.min.x + padding.x, rect.center().y - galley.size().y / 2.0);
ui.painter().galley(text_pos, galley, text_color);
if closable {
let close_rect = egui::Rect::from_min_size(
egui::pos2(rect.max.x - padding.x - close_size, rect.center().y - close_size / 2.0),
egui::vec2(close_size, close_size),
);
let close_id = id.with("close");
let close_response = close_button_at(ui, close_rect, close_id);
close_clicked = close_response.clicked() || response.clicked_by(egui::PointerButton::Middle);
}
}
TabResponse { response, close_clicked }
}
@@ -0,0 +1,22 @@
pub fn strip_ansi(input: &str) -> String {
let mut output = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1B' {
if chars.peek() == Some(&'[') {
chars.next();
while let Some(&next) = chars.peek() {
chars.next();
if next.is_ascii_alphabetic() {
break;
}
}
}
continue;
}
output.push(c);
}
output
}