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:
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "dsa_cli"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
@@ -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");
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"] }
|
||||
Binary file not shown.
@@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)))),
|
||||
)
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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>),
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod file_dialog;
|
||||
pub mod confirm_dialog;
|
||||
pub mod error_dialog;
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>()
|
||||
}
|
||||
|
||||
@@ -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>))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user