diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..ab1f416
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,10 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Ignored default folder with query files
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/.idea/dsa2.iml b/.idea/dsa2.iml
new file mode 100644
index 0000000..40aec87
--- /dev/null
+++ b/.idea/dsa2.iml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..2190bf5
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dsa/common/src/register.rs b/dsa/common/src/register.rs
index c438b3d..a54238b 100644
--- a/dsa/common/src/register.rs
+++ b/dsa/common/src/register.rs
@@ -23,38 +23,46 @@ pub enum Register {
Rgf = 15,
// special purpose
- Zero = 16,
- Acc = 17,
- Spr = 18,
- Bpr = 19,
- Ret = 20,
- Idr = 21,
- Mmr = 22,
+ Acc = 16,
+ Spr = 17,
+ Bpr = 18,
+ Ret = 19,
+ Idr = 20,
+ Mmr = 21,
// system - read only
- Mar = 23,
- Mdr = 24,
- Sts = 25,
- Cir = 26,
- Pcx = 27,
+ Zero = 22,
+ Sts = 23,
+ Cir = 24,
+ Pcx = 25,
#[default]
- Null = 28,
+ Null = 26,
}
impl Register {
+ /// Total number of registers, not counting Null as it's not a real register.
+ /// reading or writing to Null is undefined behaviour.
+ pub const COUNT: u8 = Self::Null as u8;
+
+ /// # Safety
+ /// the caller must ensure that the index is always within the range 0..COUNT, failing to do so will result in undefined behaviour.
#[must_use]
#[inline]
pub unsafe fn from_u8_unchecked(idx: u8) -> Self {
- debug_assert!(idx <= Self::Null as u8);
+ debug_assert!(
+ idx < Self::COUNT,
+ "Tried to access a null register using the unchecked method. undefined behaviour alert!"
+ );
unsafe { std::mem::transmute(idx) }
}
#[inline]
pub fn from_u8(idx: u8) -> Result {
- if idx > 28 {
+ if idx >= Self::COUNT {
return Err(());
}
+
Ok(unsafe { Self::from_u8_unchecked(idx) })
}
}
@@ -84,7 +92,6 @@ impl fmt::Display for Register {
Self::Rgf => "Rgf",
// special purpose
- Self::Zero => "Zero",
Self::Acc => "Acc",
Self::Spr => "Spr",
Self::Bpr => "Bpr",
@@ -93,8 +100,7 @@ impl fmt::Display for Register {
Self::Mmr => "Mmr",
// system - read only
- Self::Mar => "Mar",
- Self::Mdr => "Mdr",
+ Self::Zero => "Zero",
Self::Sts => "Sts",
Self::Cir => "Cir",
Self::Pcx => "Pcx",
@@ -129,7 +135,6 @@ impl std::str::FromStr for Register {
"rgf" => Ok(Self::Rgf),
// special purpose
- "zero" => Ok(Self::Zero),
"acc" => Ok(Self::Acc),
"spr" => Ok(Self::Spr),
"bpr" => Ok(Self::Bpr),
@@ -138,8 +143,7 @@ impl std::str::FromStr for Register {
"mmr" => Ok(Self::Mmr),
// system - read only
- "mar" => Ok(Self::Mar),
- "mdr" => Ok(Self::Mdr),
+ "zero" => Ok(Self::Zero),
"sts" => Ok(Self::Sts),
"cir" => Ok(Self::Cir),
"pcx" => Ok(Self::Pcx),
diff --git a/dsa/emulator/Cargo.toml b/dsa/emulator/Cargo.toml
index 22c947b..0add207 100644
--- a/dsa/emulator/Cargo.toml
+++ b/dsa/emulator/Cargo.toml
@@ -1,7 +1,8 @@
[package]
name = "emulator"
-version = "0.1.0"
-edition = "2024"
+version = "0.3.0"
+edition.workspace = true
+authors.workspace = true
[[bin]]
name = "dsa"
@@ -22,15 +23,14 @@ ron = "0.12.0"
serde = { version = "1.0.228", features = ["derive"] }
common = { path = "../common" }
+egui = "0.33.3"
+eframe = "0.33.3"
+egui_tiles = "0.14.1"
#assembler = { path = "../assembler" }
-[[bench]]
-name = "bench_mainstore"
-harness = false
-
[features]
-default = ["mainstore-bulkalloc"]
+default = ["mainstore-prealloc"]
# Memory Bank Features
mainstore-bulkalloc = [] # Fastest for Writes
diff --git a/dsa/emulator/benches/bench_mainstore.rs b/dsa/emulator/benches/bench_mainstore.rs
deleted file mode 100644
index 28df431..0000000
--- a/dsa/emulator/benches/bench_mainstore.rs
+++ /dev/null
@@ -1,208 +0,0 @@
-use criterion::{
- BenchmarkGroup, BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main,
- measurement::WallTime,
-};
-use std::time::Duration;
-
-use dsa::{Page, RandomAccessMemory};
-
-type PhysAddr = u32;
-
-// ── address generators ────────────────────────────────────────────────────────
-
-fn sequential_addrs(n: usize, max_addr: u32) -> Vec {
- (0..n).map(|i| ((i as u32 * 4) & (max_addr - 1))).collect()
-}
-
-fn random_addrs(n: usize, max_addr: u32) -> Vec {
- let mut addrs = Vec::with_capacity(n);
- let mut x: u32 = 0xdeadbeef;
- for _ in 0..n {
- x = x.wrapping_mul(1664525).wrapping_add(1013904223);
- addrs.push((x & (max_addr - 1)) & !3);
- }
- addrs
-}
-
-fn page_stride_addrs(n: usize) -> Vec {
- (0..n).map(|i| (i as u32 * 4096) & 0x00FF_FFFF).collect()
-}
-
-fn random_page_addrs(n: usize, max_addr: u32) -> Vec {
- let mut addrs = Vec::with_capacity(n);
- let mut x: u32 = 0xc0ffee42;
- for _ in 0..n {
- x = x.wrapping_mul(1664525).wrapping_add(1013904223);
- addrs.push((x & (max_addr - 1)) & !0xFFF);
- }
- addrs
-}
-
-// ── runners ───────────────────────────────────────────────────────────────────
-
-fn run_read_byte(
- group: &mut BenchmarkGroup,
- addrs: &[PhysAddr],
- implementations: &mut [(&str, Box)],
-) {
- group.throughput(Throughput::Elements(addrs.len() as u64));
- for (name, mem) in implementations.iter() {
- group.bench_with_input(BenchmarkId::new(*name, ""), addrs, |b, addrs| {
- b.iter(|| {
- let mut sum: u32 = 0;
- for &addr in addrs {
- sum = sum.wrapping_add(mem.read_byte(black_box(addr)) as u32);
- }
- black_box(sum)
- })
- });
- }
-}
-
-fn run_write_byte(
- group: &mut BenchmarkGroup,
- addrs: &[PhysAddr],
- implementations: &mut [(&str, Box)],
-) {
- group.throughput(Throughput::Elements(addrs.len() as u64));
- for (name, mem) in implementations.iter_mut() {
- group.bench_with_input(BenchmarkId::new(*name, ""), addrs, |b, addrs| {
- b.iter(|| {
- for (i, &addr) in addrs.iter().enumerate() {
- mem.write_byte(black_box(addr), black_box(i as u8));
- }
- })
- });
- }
-}
-
-fn run_read_word(
- group: &mut BenchmarkGroup,
- addrs: &[PhysAddr],
- implementations: &mut [(&str, Box)],
-) {
- group.throughput(Throughput::Elements(addrs.len() as u64));
- for (name, mem) in implementations.iter() {
- group.bench_with_input(BenchmarkId::new(*name, ""), addrs, |b, addrs| {
- b.iter(|| {
- let mut sum: u32 = 0;
- for &addr in addrs {
- sum = sum.wrapping_add(mem.read_word(black_box(addr)));
- }
- black_box(sum)
- })
- });
- }
-}
-
-fn run_write_word(
- group: &mut BenchmarkGroup,
- addrs: &[PhysAddr],
- implementations: &mut [(&str, Box)],
-) {
- group.throughput(Throughput::Elements(addrs.len() as u64));
- for (name, mem) in implementations.iter_mut() {
- group.bench_with_input(BenchmarkId::new(*name, ""), addrs, |b, addrs| {
- b.iter(|| {
- for (i, &addr) in addrs.iter().enumerate() {
- mem.write_word(black_box(addr), black_box(i as u32));
- }
- })
- });
- }
-}
-
-fn run_read_page(
- group: &mut BenchmarkGroup,
- addrs: &[PhysAddr],
- implementations: &mut [(&str, Box)],
-) {
- group.throughput(Throughput::Bytes(addrs.len() as u64 * 4096));
- for (name, mem) in implementations.iter() {
- group.bench_with_input(BenchmarkId::new(*name, ""), addrs, |b, addrs| {
- b.iter(|| {
- let mut sum: u64 = 0;
- for &addr in addrs {
- let page = mem.read_page(black_box(addr));
- for chunk in page.chunks_exact(8) {
- sum = sum.wrapping_add(u64::from_le_bytes(chunk.try_into().unwrap()));
- }
- }
- black_box(sum)
- })
- });
- }
-}
-
-fn run_write_page(
- group: &mut BenchmarkGroup,
- addrs: &[PhysAddr],
- implementations: &mut [(&str, Box)],
-) {
- let page_data = Page::from([0xABu8; 4096]);
- group.throughput(Throughput::Bytes(addrs.len() as u64 * 4096));
- for (name, mem) in implementations.iter_mut() {
- group.bench_with_input(BenchmarkId::new(*name, ""), addrs, |b, addrs| {
- b.iter(|| {
- for &addr in addrs {
- mem.write_page(black_box(addr), black_box(&page_data));
- }
- })
- });
- }
-}
-
-// ── entry point ───────────────────────────────────────────────────────────────
-
-fn benchmarks(c: &mut Criterion) {
- const N: usize = 4096;
- const MAX_ADDR: u32 = 0x00FF_FFFF;
-
- let seq_addrs = sequential_addrs(N, MAX_ADDR);
- let rand_addrs = random_addrs(N, MAX_ADDR);
- let page_addrs_seq = page_stride_addrs(N);
- let page_addrs_rand = random_page_addrs(N, MAX_ADDR);
-
- let mut implementations: Vec<(&str, Box)> = vec![
- #[cfg(feature = "mainstore-hashmap")]
- ("HashStore", Box::new(dsa::HashStore::new())),
- #[cfg(feature = "mainstore-arraymap")]
- ("ArrayStore", Box::new(dsa::ArrayStore::new())),
- #[cfg(feature = "mainstore-stackarray")]
- ("StackArrayStore", Box::new(dsa::StackArrayStore::new())),
- #[cfg(feature = "mainstore-prealloc")]
- ("PreAllocStore", Box::new(dsa::PreAllocStore::new())),
- #[cfg(feature = "mainstore-bulkalloc")]
- ("BulkAllocStore", Box::new(dsa::BulkAllocStore::new())),
- ];
-
- macro_rules! group {
- ($name:expr, $secs:expr, $runner:ident, $addrs:expr) => {{
- let mut g = c.benchmark_group($name);
- g.measurement_time(Duration::from_secs($secs));
- $runner(&mut g, $addrs, &mut implementations);
- g.finish();
- }};
- }
-
- group!("write_word/random", 30, run_write_word, &rand_addrs);
- group!("write_byte/random", 30, run_write_byte, &rand_addrs);
-
- group!("read_byte/sequential", 30, run_read_byte, &seq_addrs);
- group!("read_byte/random", 30, run_read_byte, &rand_addrs);
-
- group!("read_word/sequential", 30, run_read_word, &seq_addrs);
- group!("read_word/random", 30, run_read_word, &rand_addrs);
-
- group!("write_byte/sequential", 30, run_write_byte, &seq_addrs);
- group!("write_word/sequential", 30, run_write_word, &seq_addrs);
-
- group!("read_page/sequential", 30, run_read_page, &page_addrs_seq);
- group!("read_page/random", 30, run_read_page, &page_addrs_rand);
-
- group!("write_page/sequential", 30, run_write_page, &page_addrs_seq);
- group!("write_page/random", 30, run_write_page, &page_addrs_rand);
-}
-
-criterion_group!(benches, benchmarks);
-criterion_main!(benches);
diff --git a/dsa/emulator/font/JetBrainsMonoNerdFontMono_Regular.ttf b/dsa/emulator/font/JetBrainsMonoNerdFontMono_Regular.ttf
new file mode 100644
index 0000000..99f2e9b
Binary files /dev/null and b/dsa/emulator/font/JetBrainsMonoNerdFontMono_Regular.ttf differ
diff --git a/dsa/emulator/src/args.rs b/dsa/emulator/src/args.rs
index b59e863..4bec2ab 100644
--- a/dsa/emulator/src/args.rs
+++ b/dsa/emulator/src/args.rs
@@ -1,9 +1,8 @@
use std::{fs, path::PathBuf};
-use clap::{Parser, ValueEnum};
-use serde::{Deserialize, Serialize};
+use clap::Parser;
-use crate::{MemoryMap, RandomAccessMemory, config::IoMapping, io::DeviceId};
+use crate::config::{IoMapping, MemoryMap};
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
@@ -18,9 +17,6 @@ pub struct DsaArgs {
#[arg(long = "iomap")]
io_map: Option,
-
- #[arg(value_enum, long = "mem", default_value = "bulk-alloc")]
- pub memory_bank: MemoryBank,
}
impl DsaArgs {
@@ -56,18 +52,3 @@ impl DsaArgs {
})
}
}
-
-#[derive(Debug, Clone, ValueEnum, Default)]
-pub enum MemoryBank {
- #[cfg(feature = "mainstore-bulkalloc")]
- #[default]
- BulkAlloc,
- #[cfg(feature = "mainstore-arraymap")]
- ArrayMap,
- #[cfg(feature = "mainstore-hashmap")]
- HashMap,
- #[cfg(feature = "mainstore-prealloc")]
- PreAlloc,
- #[cfg(feature = "mainstore-stackarray")]
- StackArray,
-}
diff --git a/dsa/emulator/src/memory/cache.rs b/dsa/emulator/src/backend/memory/cache.rs
similarity index 100%
rename from dsa/emulator/src/memory/cache.rs
rename to dsa/emulator/src/backend/memory/cache.rs
diff --git a/dsa/emulator/src/memory/mmu.rs b/dsa/emulator/src/backend/memory/mmu.rs
similarity index 97%
rename from dsa/emulator/src/memory/mmu.rs
rename to dsa/emulator/src/backend/memory/mmu.rs
index 761634d..6afe4af 100644
--- a/dsa/emulator/src/memory/mmu.rs
+++ b/dsa/emulator/src/backend/memory/mmu.rs
@@ -1,4 +1,4 @@
-use crate::memory::{FaultInfo, PhysAddr, VirtAddr, tlb::TLB};
+use crate::backend::memory::{FaultInfo, PhysAddr, VirtAddr, tlb::TLB};
pub struct MMU {
buffer: TLB,
diff --git a/dsa/emulator/src/memory/mod.rs b/dsa/emulator/src/backend/memory/mod.rs
similarity index 52%
rename from dsa/emulator/src/memory/mod.rs
rename to dsa/emulator/src/backend/memory/mod.rs
index 2bcfbb2..fbc528c 100644
--- a/dsa/emulator/src/memory/mod.rs
+++ b/dsa/emulator/src/backend/memory/mod.rs
@@ -1,7 +1,3 @@
-use serde::{Deserialize, Serialize};
-
-use crate::{RandomAccessMemory, memory::mmu::MMU, processor::processor::Emulator};
-
mod cache;
pub mod mmu;
pub mod ram;
diff --git a/dsa/emulator/src/backend/memory/ram.rs b/dsa/emulator/src/backend/memory/ram.rs
new file mode 100644
index 0000000..3a7dea9
--- /dev/null
+++ b/dsa/emulator/src/backend/memory/ram.rs
@@ -0,0 +1,168 @@
+use std::{
+ alloc::{Layout, alloc_zeroed, dealloc},
+ ops::{Deref, DerefMut},
+};
+
+#[cfg(feature = "mainstore-arraymap")]
+pub mod arraymap;
+#[cfg(feature = "mainstore-arraymap")]
+pub use arraymap::ArrayStore;
+
+#[cfg(feature = "mainstore-hashmap")]
+pub mod hashmap;
+#[cfg(feature = "mainstore-hashmap")]
+pub use hashmap::HashStore;
+
+#[cfg(feature = "mainstore-stackarray")]
+pub mod stackarray;
+#[cfg(feature = "mainstore-stackarray")]
+pub use stackarray::StackArrayStore;
+
+#[cfg(feature = "mainstore-bulkalloc")]
+pub mod bulkalloc;
+#[cfg(feature = "mainstore-bulkalloc")]
+pub use bulkalloc::BulkAllocStore;
+
+// #[cfg(feature = "mainstore-prealloc")]
+// pub mod prealloc;
+// #[cfg(feature = "mainstore-prealloc")]
+// pub use prealloc::PreAllocStore;
+
+use crate::backend::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- + '_ {
+ 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> for Page {
+ type Error = String;
+
+ fn try_from(data: Vec) -> Result {
+ let arr: [u8; 4096] = data
+ .try_into()
+ .map_err(|v: Vec| format!("Expected 4096 bytes, got {}", v.len()))?;
+ Ok(Page(arr))
+ }
+}
+
+#[inline(always)]
+const fn idx(addr: PhysAddr) -> (usize, usize) {
+ ((addr >> 12) as usize, (addr & 0xFFF) as usize)
+}
+
+pub 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(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(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 }
+ }
+}
diff --git a/dsa/emulator/src/memory/tlb.rs b/dsa/emulator/src/backend/memory/tlb.rs
similarity index 96%
rename from dsa/emulator/src/memory/tlb.rs
rename to dsa/emulator/src/backend/memory/tlb.rs
index 2a04745..f6d6da4 100644
--- a/dsa/emulator/src/memory/tlb.rs
+++ b/dsa/emulator/src/backend/memory/tlb.rs
@@ -1,4 +1,4 @@
-use crate::memory::{PhysAddr, VirtAddr};
+use crate::backend::memory::{PhysAddr, VirtAddr};
const TLB_SIZE: usize = 256;
const TLB_MASK: u32 = (TLB_SIZE - 1) as u32;
diff --git a/dsa/emulator/src/backend/mod.rs b/dsa/emulator/src/backend/mod.rs
new file mode 100644
index 0000000..af5b873
--- /dev/null
+++ b/dsa/emulator/src/backend/mod.rs
@@ -0,0 +1,2 @@
+pub mod memory;
+pub mod processor;
diff --git a/dsa/emulator/src/processor/interrupts.rs b/dsa/emulator/src/backend/processor/interrupts.rs
similarity index 100%
rename from dsa/emulator/src/processor/interrupts.rs
rename to dsa/emulator/src/backend/processor/interrupts.rs
diff --git a/dsa/emulator/src/processor/mod.rs b/dsa/emulator/src/backend/processor/mod.rs
similarity index 100%
rename from dsa/emulator/src/processor/mod.rs
rename to dsa/emulator/src/backend/processor/mod.rs
diff --git a/dsa/emulator/src/processor/processor.rs b/dsa/emulator/src/backend/processor/processor.rs
similarity index 71%
rename from dsa/emulator/src/processor/processor.rs
rename to dsa/emulator/src/backend/processor/processor.rs
index d3c278c..36d225f 100644
--- a/dsa/emulator/src/processor/processor.rs
+++ b/dsa/emulator/src/backend/processor/processor.rs
@@ -16,14 +16,12 @@ use common::{
};
use crate::{
- Page,
+ MemoryBank, Page, SharedState,
+ backend::{memory::mmu::MMU, processor::interrupts::Interrupt},
config::{IoMapping, MemoryMap, RegionType},
- io::{IoAccess, IoDevice, MappedDevice},
- memory::{mmu::MMU, ram::RandomAccessMemory},
- processor::{interrupts::Interrupt, state::SharedState},
};
-pub struct Emulator {
+pub struct Emulator {
internal_state: ProcessorSnapshot,
shared_state: Arc,
@@ -33,10 +31,9 @@ pub struct Emulator {
// memory
mmu: MMU,
- mainstore: Mem,
+ mainstore: MemoryBank,
// IO
- mmio: Vec,
mmio_region: Option<(u32, u32)>, // start end end of segment
// optionals - not necessarily set on boot.
@@ -44,12 +41,13 @@ pub struct Emulator {
// Config params
__mmap_configured: bool,
- __io_configured: bool,
+
+ time: Instant,
}
-unsafe impl Send for Emulator {}
-impl Emulator {
- pub fn new(mem: Mem) -> Self {
+unsafe impl Send for Emulator {}
+impl Emulator {
+ pub fn new() -> Self {
let (sender, receiver) = mpsc::channel::();
Self {
@@ -61,24 +59,16 @@ impl Emulator {
memory_map: None,
mmu: MMU::new(),
- mainstore: mem,
+ mainstore: MemoryBank::new(),
// mmio
- mmio: Vec::new(),
mmio_region: None,
// Config params
- __io_configured: false,
__mmap_configured: false,
- }
- }
- pub fn with_device(mut self, device: impl IoDevice + 'static) -> Self {
- self.mmio.push(MappedDevice {
- base: 0,
- device: Box::new(device),
- });
- self
+ time: Instant::now(),
+ }
}
pub fn state_handle(&self) -> Arc {
@@ -91,25 +81,21 @@ impl Emulator {
}
#[inline]
- pub fn memory_mut(&mut self) -> &mut impl RandomAccessMemory {
+ pub fn memory_mut(&mut self) -> &mut MemoryBank {
&mut self.mainstore
}
#[must_use]
#[inline]
pub fn reg(&self, reg: Register) -> u32 {
- if reg as u8 == Register::Zero as u8 {
- return 0;
- }
- debug_assert!((reg as usize) < ProcessorSnapshot::REG_COUNT);
+ 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 usize) < ProcessorSnapshot::REG_COUNT);
-
+ debug_assert!((reg as u8) < Register::COUNT);
unsafe {
self.internal_state
.registers
@@ -131,23 +117,6 @@ impl Emulator {
self
}
- pub fn apply_io_map(mut self, map: Vec) -> Result {
- if !self.__mmap_configured {
- return Err("You must map memory before applying I/O mappings".to_string());
- }
-
- for entry in map {
- if let Some(idx) = self.mmio.iter().position(|d| d.device.id() == entry.device) {
- self.mmio[idx].base = entry.base;
- } else {
- eprintln!("WARN: no device registgered for {:?}", entry.device);
- }
- }
-
- self.__io_configured = true;
- Ok(self)
- }
-
#[cold]
fn update(&mut self) {
self.shared_state
@@ -157,7 +126,7 @@ impl Emulator {
#[cold]
fn boot(&mut self) -> Result<(), String> {
- if !(self.__io_configured && self.__mmap_configured) {
+ if !(self.__mmap_configured) {
return Err("Emulator not configured".to_string());
}
@@ -165,48 +134,89 @@ impl Emulator {
MemoryMap::identity_map(&mut self.mmu, map.regions.get(0).unwrap());
}
- self.internal_state.running = true;
+ self.internal_state.halted = false;
Ok(())
}
#[cold]
fn shutdown(&mut self) {
- self.internal_state.running = false;
+ self.internal_state.halted = true;
+ }
+
+ pub fn halt(&mut self) {
+ self.internal_state.halted = true;
+
+ // wait until we're un-halted.
+ while self.internal_state.halted {
+ self.wait_for_instructions();
+ }
+ }
+
+ pub fn check_update(&mut self) {
+ // UI requested a state update.
+ if self.shared_state.update_req.load(Ordering::Relaxed) {
+ self.update();
+ }
+
+ if self.shared_state.reseting.load(Ordering::Relaxed) {
+ self.internal_state.registers = [0; Register::COUNT as usize];
+ self.internal_state.clock = 0;
+
+ // sit in a wait loop while resetting.
+ while self.shared_state.reseting.load(Ordering::Relaxed) {
+ thread::sleep(Duration::from_millis(100));
+ }
+ }
}
#[cold]
- pub fn idle_wait(&mut self) {
- self.internal_state.running = false;
+ pub fn wait_for_instructions(&mut self) {
+ {
+ // record time for previous execution.
+ self.internal_state.time = self.time.elapsed();
+ self.update();
+ self.time = Instant::now();
+ }
+
+ // if paused from the UI thread, just wait.
+ while self.shared_state.paused.load(Ordering::Relaxed) {
+ thread::sleep(Duration::from_millis(10));
+ // UI is making changes / requesting updates.
+ self.check_update();
+ }
+
+ // if the cpu didn't halt on it's own then it's ready to run again.
+ if !self.internal_state.halted {
+ return;
+ }
- // Wait for an interrupt or state update to continue.
loop {
// Check for interrupts.
if let Ok(code) = self.interrupts.recv_timeout(Duration::from_millis(100)) {
+ self.internal_state.halted = false;
self.interrupt(Interrupt::Software(code));
break;
}
- // // If we've received a request to continue running. DEPRECATED (we can run through interrupts)
- // if self.shared_state.running.load(Ordering::Relaxed) {
- // panic!("3");
- // break;
- // }
-
- // UI requested a state update.
- if self.shared_state.update_req.load(Ordering::Relaxed) {
- self.update();
- }
+ // UI is resetting the emulator
+ self.check_update();
}
-
- self.internal_state.running = true;
}
pub fn run(&mut self) -> Result<(), String> {
- self.boot()?;
+ // wait for UI to initialise emulator.
+ if self.shared_state.paused.load(Ordering::Relaxed) {
+ self.wait_for_instructions();
+ }
- let mut time = Instant::now();
+ self.boot()?;
+ self.time = Instant::now();
'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.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
@@ -218,8 +228,8 @@ impl Emulator {
}
}
- if unlikely(!self.shared_state.running.load(Ordering::Relaxed)) {
- self.idle_wait();
+ if unlikely(self.shared_state.paused.load(Ordering::Relaxed)) {
+ self.wait_for_instructions();
}
match self.interrupts.try_recv() {
@@ -229,24 +239,6 @@ impl Emulator {
}
}
- // if we got a halt instruction, wait
- if unlikely(!self.internal_state.running) {
- let mips = (self.internal_state.clock as u128 / time.elapsed().as_micros());
- println!(
- "TIME TAKEN: {:?}, clock: {}, {}MIPS",
- time.elapsed(),
- self.internal_state.clock,
- mips
- );
- time = Instant::now();
-
- // temporary while i figure out a better solution
- // exit when we hit halt (not useful for a real os ofc)
- break 'emu;
-
- self.idle_wait();
- }
-
let pc = self.reg(Register::Pcx);
let instruction = self.mem_read_word(pc);
@@ -261,23 +253,21 @@ impl Emulator {
thread::sleep(Duration::from_micros(10));
}
- self.mut_reg(Register::Pcx).add_assign(4);
-
- self.execute(Instruction(instruction));
-
- // Check if executing the interrupt caused a fault.
+ // Check if the previous instruction caused a fault.
if let Some(fault) = self.pending_fault {
- self.pending_fault = None;
println!("WARN fault: {:?}", fault);
+ self.pending_fault = None;
self.interrupt(fault);
}
+ self.mut_reg(Register::Pcx).add_assign(4);
+ self.execute(Instruction(instruction));
+
// always increment clock.
self.internal_state.clock += 1;
}
self.shutdown();
-
Ok(())
}
@@ -489,7 +479,7 @@ impl Emulator {
Some(Opcode::Int) => {
self.interrupt(Interrupt::Software(word.imm16() as u8));
}
- Some(Opcode::Hlt) => self.internal_state.running = false,
+ Some(Opcode::Hlt) => self.halt(),
Some(Opcode::IRet) => todo!(),
None => {}
}
@@ -510,147 +500,57 @@ impl Emulator {
#[inline]
fn mem_read_byte(&mut self, addr: u32) -> u8 {
- if unlikely(self.is_mmio(addr)) {
- return self.io_read_byte(addr);
- }
self.mainstore.read_byte(addr)
}
#[inline]
fn mem_write_byte(&mut self, addr: u32, val: u8) {
- if unlikely(self.is_mmio(addr)) {
- self.io_write_byte(addr, val);
- return;
+ if addr == 0x40000 {
+ eprintln!("emulator wrote 0x{:08x} to uart", val);
}
self.mainstore.write_byte(addr, val);
}
#[inline]
fn mem_read_word(&mut self, addr: u32) -> u32 {
- if unlikely(self.is_mmio(addr)) {
- return self.io_read_word(addr);
- }
self.mainstore.read_word(addr)
}
#[inline]
fn mem_write_word(&mut self, addr: u32, val: u32) {
- if unlikely(self.is_mmio(addr)) {
- self.io_write_word(addr, val);
- return;
- }
self.mainstore.write_word(addr, val);
}
#[inline]
fn mem_read_page(&mut self, addr: u32) -> &Page {
- if unlikely(self.is_mmio(addr)) {
- // pages spanning MMIO don't really make sense
- // treat as fault
- self.pending_fault = Some(Interrupt::ProtectionFault);
- return &Page::ZERO;
- }
self.mainstore.read_page(addr)
}
#[inline]
fn mem_write_page(&mut self, addr: u32, val: &Page) {
- if unlikely(self.is_mmio(addr)) {
- self.pending_fault = Some(Interrupt::ProtectionFault);
- return;
- }
self.mainstore.write_page(addr, val);
}
- // single MMIO check reused by all of the above
- #[inline]
- fn is_mmio(&self, addr: u32) -> bool {
- self.mmio_region
- .map(|(base, end)| addr >= base && addr < end)
- .unwrap_or(false)
- }
-
fn fault(&mut self, interrupt: Interrupt) {
self.pending_fault = Some(interrupt);
}
-
- #[inline]
- fn get_device(&mut self, addr: u32) -> Option<(&mut Box, u32)> {
- // Find the device that covers the address.
- self.mmio
- .iter_mut()
- .find(|d| d.base <= addr && addr < d.base + d.device.size())
- .map(|d| (&mut d.device, addr - d.base))
- }
- // --- cold IO paths ---
-
- #[cold]
- fn io_read_byte(&mut self, addr: u32) -> u8 {
- if let Some((device, offset)) = self.get_device(addr) {
- device.read_byte(offset).unwrap_or_else(|fault| {
- self.fault(fault);
- 0
- })
- } else {
- self.fault(Interrupt::UnmappedIo);
- 0
- }
- }
-
- #[cold]
- fn io_read_word(&mut self, addr: u32) -> u32 {
- if let Some((device, offset)) = self.get_device(addr) {
- device.read_word(offset).unwrap_or_else(|fault| {
- self.fault(fault);
- 0
- })
- } else {
- self.fault(Interrupt::UnmappedIo);
-
- 0
- }
- }
-
- #[cold]
- fn io_write_byte(&mut self, addr: u32, val: u8) {
- if let Some((dev, offset)) = self.get_device(addr) {
- dev.write_byte(offset, val).unwrap_or_else(|fault| {
- self.fault(fault);
- });
- } else {
- self.fault(Interrupt::UnmappedIo);
- }
- }
-
- #[cold]
- fn io_write_word(&mut self, addr: u32, val: u32) {
- if let Some((dev, offset)) = self.get_device(addr) {
- dev.write_word(offset, val).unwrap_or_else(|fault| {
- self.fault(fault);
- });
- } else {
- self.fault(Interrupt::UnmappedIo);
- }
- }
}
#[derive(Debug, Clone)]
pub struct ProcessorSnapshot {
- pub running: bool,
+ pub halted: bool,
pub clock: usize,
- pub registers: [u32; Self::REG_COUNT],
+ pub time: Duration,
+ pub registers: [u32; Register::COUNT as usize],
}
impl Default for ProcessorSnapshot {
fn default() -> Self {
Self {
- running: false,
+ halted: false,
clock: 0,
- registers: [0; Self::REG_COUNT],
+ time: Duration::ZERO,
+ registers: [0; Register::COUNT as usize],
}
}
}
-
-impl ProcessorSnapshot {
- const REG_COUNT: usize = 28;
-}
diff --git a/dsa/emulator/src/processor/state.rs b/dsa/emulator/src/backend/processor/state.rs
similarity index 58%
rename from dsa/emulator/src/processor/state.rs
rename to dsa/emulator/src/backend/processor/state.rs
index 506c7e1..f0ce121 100644
--- a/dsa/emulator/src/processor/state.rs
+++ b/dsa/emulator/src/backend/processor/state.rs
@@ -1,24 +1,27 @@
use std::sync::{Arc, atomic::AtomicBool, mpsc};
-use crate::processor::processor::ProcessorSnapshot;
use arc_swap::ArcSwap;
+use crate::backend::processor::processor::ProcessorSnapshot;
+
pub struct SharedState {
pub proc: ArcSwap,
- pub running: AtomicBool,
+ pub reseting: AtomicBool,
+ pub paused: AtomicBool,
pub update_req: AtomicBool,
- sender: mpsc::Sender,
+ interrupt_queue: mpsc::Sender,
}
impl SharedState {
pub fn new(sender: mpsc::Sender) -> Self {
Self {
+ reseting: AtomicBool::new(false),
proc: ArcSwap::new(Arc::new(ProcessorSnapshot::default())),
- running: AtomicBool::new(true),
- sender: sender,
+ paused: AtomicBool::new(true),
+ interrupt_queue: sender,
update_req: AtomicBool::new(false),
}
}
diff --git a/dsa/emulator/src/bin/bench_emu.rs b/dsa/emulator/src/bin/bench_emu.rs
deleted file mode 100644
index fd2e29e..0000000
--- a/dsa/emulator/src/bin/bench_emu.rs
+++ /dev/null
@@ -1,330 +0,0 @@
-use common::instructions::Instruction;
-use dsa::{BulkAllocStore, Emulator, Page, RandomAccessMemory};
-/// DSA Emulator Benchmark
-///
-/// Place this in `dsa/src/bin/bench.rs` and run with:
-/// cargo run --bin bench --release
-///
-/// Three workloads are tested:
-/// 1. ALU-heavy — tight arithmetic loop, no memory, no branches
-/// 2. Memory-heavy — repeated load/store to a working set of addresses
-/// 3. Branch-heavy — the bf.dsa dispatch pattern (ieq + jnz chains)
-///
-/// Each workload is a hand-assembled flat binary loaded directly into
-/// the emulator, bypassing all the args/display/IO plumbing.
-use std::{
- thread,
- time::{Duration, Instant},
- u16,
-};
-
-// ---------------------------------------------------------------------------
-// Minimal no-op memory map + IO so the emulator boots without a config file.
-// ---------------------------------------------------------------------------
-
-fn make_emulator() -> Emulator {
- use dsa::{
- config::{MemoryMap, Region, RegionType},
- io::display::DisplayDevice,
- };
-
- let mmap = MemoryMap {
- table_addr: 0x40000,
- regions: vec![
- Region {
- base: 0x0000_0000,
- size: 0x0010_0000, // 1 MiB RAM
- region_type: RegionType::Usable,
- identity_map_on_boot: true,
- },
- Region {
- base: 0x0020_0000,
- size: 0x0002_0000, // 128 KiB MMIO (display)
- region_type: RegionType::MMIO,
- identity_map_on_boot: false,
- },
- ],
- };
-
- let iomap = vec![dsa::config::IoMapping {
- device: dsa::io::DeviceId::Display,
- base: 0x0020_0000,
- size: 0x0000_07D0,
- }];
-
- let (display, _handle) = DisplayDevice::new(80, 25);
- Emulator::new(BulkAllocStore::new())
- .with_device(display)
- .apply_memory_map(mmap)
- .apply_io_map(iomap)
- .unwrap()
-}
-
-// ---------------------------------------------------------------------------
-// Instruction encoding helpers (little-endian words)
-// ---------------------------------------------------------------------------
-
-/// Encode an I-type instruction: [op:6][src:5][dst:5][imm:16]
-fn enc_i(op: u8, src: u8, dst: u8, imm: u16) -> u32 {
- ((op as u32 & 0x3F) << 26)
- | ((src as u32 & 0x1F) << 21)
- | ((dst as u32 & 0x1F) << 16)
- | imm as u32
-}
-
-/// Encode an R-type instruction: [op:6][r1:5][r2:5][r3:5][r4:5][u6:6]
-fn enc_r(op: u8, r1: u8, r2: u8, r3: u8, r4: u8, u6: u8) -> u32 {
- ((op as u32 & 0x3F) << 26)
- | ((r1 as u32 & 0x1F) << 21)
- | ((r2 as u32 & 0x1F) << 16)
- | ((r3 as u32 & 0x1F) << 11)
- | ((r4 as u32 & 0x1F) << 6)
- | (u6 as u32 & 0x3F)
-}
-
-// Register indices — must match your Register enum discriminants
-const RG0: u8 = 0;
-const RG1: u8 = 1;
-const RG2: u8 = 2;
-const RG3: u8 = 3;
-const RG4: u8 = 4;
-const RG5: u8 = 5;
-const ZERO: u8 = 16;
-
-// Opcodes — must match your Opcode enum discriminants.
-// Fill these in from your ISA.md / encode.rs.
-const OP_NOP: u8 = 0x0D; // adjust to match your actual opcode values
-const OP_HLT: u8 = 0x2B;
-const OP_ADD: u8 = 0x22;
-const OP_ADDI: u8 = 0x23;
-const OP_SUBI: u8 = 0x24;
-const OP_LDW: u8 = 0x07;
-const OP_STW: u8 = 0x0A;
-const OP_LLI: u8 = 0x0B;
-const OP_LUI: u8 = 0x0C;
-const OP_IEQ: u8 = 0x0E;
-const OP_JNZ: u8 = 0x15;
-const OP_JMP: u8 = 0x12;
-
-// ---------------------------------------------------------------------------
-// Workload 1: ALU-heavy
-//
-// Counts down from 10_000_000 using subi, jumps back to top.
-// Pure register arithmetic, no memory accesses after init.
-// Instruction mix: lli, subi, ieq, jnz, hlt
-//
-// Layout (each word at address = index * 4):
-// 0: lli rg0, 0 (upper half of count) [lui follows]
-// 4: lui rg0,
-// 8: loop: subi rg0, 1, rg0
-// 12: ieq rg0, zero, rg1
-// 16: jnz rg1, zero, 8 [jump to loop]
-// 20: hlt
-// ---------------------------------------------------------------------------
-fn alu_workload() -> Vec {
- let count: u32 = 10_000_000;
- let lo = (count & 0xFFFF) as u16;
- let hi = ((count >> 16) & 0xFFFF) as u16;
-
- let words: &[u32] = &[
- enc_i(OP_LLI, 0, RG0, lo), // 0x00: lli rg0, lo(count)
- enc_i(OP_LUI, 0, RG0, hi), // 0x04: lui rg0, hi(count)
- // loop @ 0x08:
- enc_i(OP_SUBI, RG0, RG0, 1), // 0x08: subi rg0, rg0, 1
- enc_r(OP_IEQ, RG0, ZERO, RG1, 0, 0), // 0x0C: ieq rg0, zero, rg1
- enc_i(OP_JNZ, RG1, ZERO, 0x0008), // 0x10: jnz rg1, zero, 0x08 <- BUG: jnz jumps when rg1!=0
- // rg1=1 when rg0==0, so we want jez here to keep looping
- // fix: use jez (jump when rg1==0, i.e. rg0!=0)
- enc_i(OP_HLT, 0, 0, 0), // 0x14: hlt
- ];
- // Note: replace OP_JNZ above with your JEZ opcode so the loop continues
- // while rg0 != 0. JNZ exits when rg1 != 0, i.e. when rg0 == 0 (done).
- // The ieq sets rg1=1 only when rg0==0, so jnz rg1 exits the loop correctly.
- // Actually this IS correct: loop while ieq returns 0 (rg0 != 0),
- // exit when ieq returns 1 (rg0 == 0). jnz rg1 = jump when rg1 != 0 = jump when done.
- // We want to jump BACK while not done, so we need jez:
- // jez rg1, zero, 0x08 = jump back when rg1==0 (rg0 != 0, not done yet)
- // Swap OP_JNZ for your JEZ opcode.
- words_to_bytes(words)
-}
-
-// ---------------------------------------------------------------------------
-// Workload 2: Memory-heavy
-//
-// Repeatedly writes and reads a 64-word working set.
-// Tests page table lookup performance under repeated memory access.
-//
-// rg0 = base address (0x1000, well within RAM, page-aligned)
-// rg1 = loop counter (1000 outer iterations)
-// rg2 = inner counter (64 words per pass)
-// rg3 = scratch for store value
-// ---------------------------------------------------------------------------
-fn memory_workload() -> Vec {
- let base: u32 = 0x1000;
- let outer: u16 = 1000;
- let inner: u16 = 64;
-
- let words: &[u32] = &[
- // init
- enc_i(OP_LLI, 0, RG0, (base & 0xFFFF) as u16), // rg0 = base
- enc_i(OP_LLI, 0, RG1, outer), // rg1 = outer count
- // outer_loop @ 0x08:
- enc_i(OP_LLI, 0, RG2, inner), // rg2 = inner count
- enc_i(OP_LLI, 0, RG3, 0xABCD), // rg3 = value to write
- // inner_loop @ 0x10:
- enc_i(OP_STW, RG3, RG0, 0), // stw rg3, rg0, 0
- enc_i(OP_LDW, RG0, RG3, 0), // ldw rg0, rg3, 0 (read back)
- enc_i(OP_ADDI, RG0, RG0, 4), // rg0 += 4
- enc_i(OP_SUBI, RG2, RG2, 1), // rg2 -= 1
- enc_r(OP_IEQ, RG2, ZERO, RG4, 0, 0), // ieq rg2, zero, rg4
- enc_i(OP_JNZ, RG4, ZERO, 0x0010), // jez rg4 -> inner_loop (swap opcode)
- // restore base, dec outer
- enc_i(OP_LLI, 0, RG0, (base & 0xFFFF) as u16),
- enc_i(OP_SUBI, RG1, RG1, 1),
- enc_r(OP_IEQ, RG1, ZERO, RG4, 0, 0),
- enc_i(OP_JNZ, RG4, ZERO, 0x0008), // jez rg4 -> outer_loop (swap opcode)
- enc_i(OP_HLT, 0, 0, 0),
- ];
- words_to_bytes(words)
-}
-
-// ---------------------------------------------------------------------------
-// Workload 3: Branch-heavy
-//
-// Mimics the bf.dsa dispatch pattern: compare a value against 8 constants,
-// branch on each. Worst case for branch predictor.
-// rg0 = current "instruction" (cycles through 8 values)
-// rg8-rgf = the 8 bf opcode constants
-// ---------------------------------------------------------------------------
-fn branch_workload() -> Vec {
- // 8 bf opcodes: + - > < . , [ ]
- let opcodes: [u16; 8] = [43, 45, 62, 60, 46, 44, 91, 93];
- // We'll cycle rg0 through these values and do the full dispatch chain.
- // To keep it self-contained: load opcodes, set outer loop counter,
- // inner loop cycles rg0 through all 8 values doing ieq+jnz for each.
- let outer: u16 = u16::MAX;
-
- let mut w: Vec = Vec::new();
-
- // Load 8 opcode constants into rg8-rgf (regs 8-15)
- for (i, &op) in opcodes.iter().enumerate() {
- w.push(enc_i(OP_LLI, 0, 8 + i as u8, op));
- }
- // rg1 = outer counter
- w.push(enc_i(OP_LLI, 0, RG1, outer));
-
- // outer_loop:
- let outer_loop_addr = (w.len() * 4) as u16;
-
- // rg5 = inner counter (8 opcodes)
- w.push(enc_i(OP_LLI, 0, RG5, 8));
- // rg2 = pointer into opcode table (we'll use addi to step rg0 through values)
- // For simplicity: just set rg0 to each opcode in sequence via lli each iteration
- // This is branch-heavy, not arithmetic-heavy, so the lli overhead is fine.
- for (i, &op) in opcodes.iter().enumerate() {
- let next_offset = ((w.len() + 4) * 4) as u16; // addr after the jnz
- w.push(enc_i(OP_LLI, 0, RG0, op)); // rg0 = opcode
- w.push(enc_r(OP_IEQ, RG0, 8 + i as u8, RG4, 0, 0)); // ieq rg0, rgX, rg4
- w.push(enc_i(OP_JNZ, RG4, ZERO, next_offset)); // jnz rg4 (jez to skip)
- w.push(enc_i(OP_NOP, 0, 0, 0)); // "handler" nop
- }
- // dec outer, loop
- let after_dispatch = (w.len() * 4) as u16;
- w.push(enc_i(OP_SUBI, RG1, RG1, 1));
- w.push(enc_r(OP_IEQ, RG1, ZERO, RG4, 0, 0));
- w.push(enc_i(OP_JNZ, RG4, ZERO, outer_loop_addr)); // jez -> outer_loop
- w.push(enc_i(OP_HLT, 0, 0, 0));
-
- words_to_bytes(&w)
-}
-
-fn words_to_bytes(words: &[u32]) -> Vec {
- words.iter().flat_map(|w| w.to_le_bytes()).collect()
-}
-
-// ---------------------------------------------------------------------------
-// Runner
-// ---------------------------------------------------------------------------
-
-struct BenchResult {
- name: &'static str,
- instructions: u64,
- elapsed: Duration,
- mips: f64,
-}
-
-fn run_bench(name: &'static str, binary: Vec) -> BenchResult {
- let mut emu = make_emulator();
- let handle = emu.state_handle();
-
- let pages: Vec = Page::paginate(&binary).collect();
- for (i, page) in pages.iter().enumerate() {
- emu.memory_mut().write_page(i as u32 * 4096, page);
- }
-
- let start = Instant::now();
- // run() blocks until HLT
- emu.run().expect("emulator run failed");
- let elapsed = start.elapsed();
-
- // read clock from emulator — we need to expose it.
- // For now, estimate from elapsed + target MIPS.
- // TODO: expose emulator.clock() -> u64 and use that directly.
- let instructions = handle.proc.load().clock as u64;
- let mips = instructions as f64 / elapsed.as_micros() as f64;
-
- BenchResult {
- name,
- instructions,
- elapsed,
- mips,
- }
-}
-
-fn main() {
- println!("DSA Emulator Benchmark");
- println!("======================");
- println!();
-
- // NOTE: you need to expose `pub fn clock(&self) -> usize` on Emulator
- // and ensure `run()` returns normally on HLT without blocking in idle_wait.
- // The idle_wait loop currently spins forever after HLT — for benchmarking,
- // you want run() to return when the program halts. Add a flag or check:
- // if !self.internal_state.running { break 'emu; }
- // after the HLT handler sets running=false.
-
- let workloads: &[(&'static str, fn() -> Vec)] = &[
- ("ALU-heavy (10M countdown)", alu_workload),
- ("Memory-heavy (64K rw ops)", memory_workload),
- ("Branch-heavy (bf dispatch)", branch_workload),
- ];
-
- let mut results = Vec::new();
- for &(name, build) in workloads {
- print!("Running {}... ", name);
- let binary = build();
- let result = run_bench(name, binary);
- println!("done ({:.1}ms)", result.elapsed.as_secs_f64() * 1000.0);
- results.push(result);
- }
-
- println!();
- println!(
- "{:<35} {:>12} {:>12} {:>10}",
- "Workload", "Instructions", "Time", "MIPS"
- );
- println!("{}", "-".repeat(72));
- for r in &results {
- println!(
- "{:<35} {:>12} {:>10.1}ms {:>10.1}",
- r.name,
- r.instructions,
- r.elapsed.as_secs_f64() * 1000.0,
- r.mips,
- );
- }
-
- let avg_mips: f64 = results.iter().map(|r| r.mips).sum::() / results.len() as f64;
- println!("{}", "-".repeat(72));
- println!("{:<35} {:>35.1} MIPS avg", "Overall", avg_mips);
-}
diff --git a/dsa/emulator/src/config/mod.rs b/dsa/emulator/src/config/mod.rs
index 56fabb5..76f440f 100644
--- a/dsa/emulator/src/config/mod.rs
+++ b/dsa/emulator/src/config/mod.rs
@@ -1,11 +1,13 @@
use serde::{Deserialize, Serialize};
use crate::{
- Emulator, RandomAccessMemory,
- io::DeviceId,
- memory::{PhysAddr, mmu::MMU},
+ Emulator,
+ backend::memory::{PhysAddr, mmu::MMU},
+ // io::DeviceId,
};
+pub const VERSION: &str = env!("CARGO_PKG_VERSION");
+
#[repr(u32)]
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
pub enum RegionType {
@@ -36,7 +38,7 @@ pub struct MemoryMap {
#[derive(Serialize, Deserialize)]
pub struct IoMapping {
- pub device: DeviceId,
+ // pub device: DeviceId,
pub base: u32,
pub size: u32,
}
@@ -51,7 +53,7 @@ impl MemoryMap {
}
}
- pub fn apply(&self, cpu: &mut Emulator) {
+ pub fn apply(&self, cpu: &mut Emulator) {
for reg in &self.regions {
if reg.identity_map_on_boot {
Self::identity_map(cpu.mmu_mut(), reg);
diff --git a/dsa/emulator/src/frontend/components.rs b/dsa/emulator/src/frontend/components.rs
new file mode 100644
index 0000000..4476c75
--- /dev/null
+++ b/dsa/emulator/src/frontend/components.rs
@@ -0,0 +1,15 @@
+pub mod controller;
+pub mod display;
+pub mod framebuffer;
+pub mod registers;
+pub mod serial;
+
+pub trait Component {
+ /// The tab label shown in the tile header.
+ fn title(&self) -> &str;
+
+ fn visible(&mut self) -> &mut bool;
+
+ /// Draw the component's UI inside the provided `Ui`.
+ fn ui(&mut self, ui: &mut egui::Ui, ctx: &egui::Context);
+}
diff --git a/dsa/emulator/src/frontend/components/controller.rs b/dsa/emulator/src/frontend/components/controller.rs
new file mode 100644
index 0000000..ec8ef3c
--- /dev/null
+++ b/dsa/emulator/src/frontend/components/controller.rs
@@ -0,0 +1,104 @@
+use std::sync::{Arc, atomic::Ordering};
+
+use super::Component;
+use crate::{MemoryBank, SharedState};
+
+use common::prelude::Register;
+
+pub struct Controller {
+ state: Arc,
+ mem: MemoryBank,
+ visible: bool,
+}
+
+impl Controller {
+ pub fn new(state: Arc, mem: MemoryBank) -> Self {
+ Self {
+ state,
+ mem,
+ visible: false,
+ }
+ }
+}
+
+impl Component for Controller {
+ fn title(&self) -> &str {
+ "Control Panel"
+ }
+
+ fn visible(&mut self) -> &mut bool {
+ &mut self.visible
+ }
+
+ fn ui(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) {
+ let state = self.state.proc.load();
+
+ let paused = self.state.paused.load(Ordering::Relaxed);
+
+ ui.horizontal(|ui| {
+ // Pause / Run
+ if ui
+ .button(match paused {
+ false => "Pause",
+ true => "Resume",
+ })
+ .clicked()
+ {
+ self.state.paused.store(!paused, Ordering::Relaxed);
+ }
+
+ // 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());
+ // });
+ // }
+
+ // Resets the emulator and all attached devices
+ if ui.button("Reset All").clicked() {
+ self.state.reseting.store(true, Ordering::Relaxed);
+ self.mem.clear();
+ self.state.reseting.store(false, Ordering::Relaxed);
+ }
+
+ ui.separator();
+
+ // 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
+ // }
+ // }
+
+ // Status info
+ ui.label(format!(
+ "Status: {}",
+ match (state.halted, paused) {
+ (true, false) => "Halted",
+ (_, false) => "Running",
+ (_, true) => "Paused",
+ }
+ ));
+
+ let pcx = state.registers[Register::Pcx as usize];
+ let clock = state.clock;
+ let time = state.time.as_micros();
+ let mips = state.clock as u128 / time;
+
+ ui.label(format!("Clock: {clock}"));
+ ui.label(format!("Pcx: 0x{pcx:08X}"));
+ ui.label(format!("Elapsed: {}micros", time));
+ ui.label(format!("Freq: {}MHz", mips));
+ });
+ }
+}
diff --git a/dsa/emulator/src/frontend/components/display.rs b/dsa/emulator/src/frontend/components/display.rs
new file mode 100644
index 0000000..5d131d5
--- /dev/null
+++ b/dsa/emulator/src/frontend/components/display.rs
@@ -0,0 +1,65 @@
+use egui::{Color32, FontId, Vec2};
+
+use super::Component;
+pub use crate::io::display::Display;
+
+impl Component for Display {
+ fn title(&self) -> &str {
+ "Display"
+ }
+
+ fn visible(&mut self) -> &mut bool {
+ self.visible()
+ }
+
+ fn ui(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) {
+ 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,
+ );
+ }
+ }
+}
diff --git a/dsa/emulator/src/frontend/components/framebuffer.rs b/dsa/emulator/src/frontend/components/framebuffer.rs
new file mode 100644
index 0000000..5d9a91a
--- /dev/null
+++ b/dsa/emulator/src/frontend/components/framebuffer.rs
@@ -0,0 +1,137 @@
+use egui::Vec2;
+
+use crate::{MemoryBank, Page, backend::memory::PhysAddr};
+
+use super::Component;
+
+pub struct FrameBuffer {
+ width: usize,
+ height: usize,
+ addr: PhysAddr,
+ mem: MemoryBank,
+ pub buffer: Vec, // RGBA pixels
+ visible: bool,
+
+ texture: Option,
+}
+
+impl FrameBuffer {
+ pub fn new(width: u32, height: u32, addr: PhysAddr, mem: MemoryBank) -> Self {
+ Self {
+ width: width as usize,
+ height: height as usize,
+ addr,
+ mem,
+ buffer: vec![0u32; width as usize * height as usize],
+ visible: true,
+
+ texture: None,
+ }
+ }
+
+ pub fn width(&self) -> usize {
+ self.width
+ }
+ pub fn height(&self) -> usize {
+ self.height
+ }
+ pub fn visible(&mut self) -> &mut bool {
+ &mut self.visible
+ }
+
+ fn internal_read(&self) -> Vec {
+ let byte_size = self.width * self.height * 4;
+ let page_count = byte_size.div_ceil(Page::SIZE);
+
+ (0..page_count)
+ .map(|i| {
+ let page = self.mem.read_page(self.addr + i as u32 * 4096);
+ page.as_slice().to_owned()
+ })
+ .flatten()
+ .take(byte_size)
+ .collect::>()
+ .chunks_exact(4)
+ .map(|chunk| u32::from_le_bytes(chunk.try_into().unwrap()))
+ .collect()
+ }
+
+ pub fn changed(&mut self) -> bool {
+ let temp = self.internal_read();
+ if temp != self.buffer {
+ self.buffer = temp;
+ return true;
+ }
+ false
+ }
+
+ pub fn read(&self) -> &[u32] {
+ &self.buffer
+ }
+
+ /// Convert buffer to RGBA bytes for use with egui/wgpu textures
+ pub fn as_rgba_bytes(&self) -> Vec {
+ self.buffer
+ .iter()
+ .flat_map(|&pixel| pixel.to_le_bytes())
+ .collect()
+ }
+}
+
+impl Component for FrameBuffer {
+ fn title(&self) -> &str {
+ "Framebuffer"
+ }
+
+ fn visible(&mut self) -> &mut bool {
+ &mut self.visible
+ }
+
+ fn ui(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) {
+ let changed = self.changed();
+
+ // get or create the texture
+ let texture = self.texture.get_or_insert_with(|| {
+ ctx.load_texture(
+ "framebuffer",
+ egui::ColorImage {
+ size: [self.width, self.height],
+ source_size: Vec2::from([self.width as f32, self.height as f32]),
+ pixels: vec![egui::Color32::BLACK; self.width * self.height],
+ },
+ egui::TextureOptions::NEAREST,
+ )
+ });
+
+ if changed {
+ let pixels: Vec = self
+ .buffer
+ .iter()
+ .map(|&p| {
+ let bytes = p.to_le_bytes();
+ egui::Color32::from_rgba_premultiplied(bytes[0], bytes[1], bytes[2], bytes[3])
+ })
+ .collect();
+
+ texture.set(
+ egui::ColorImage {
+ size: [self.width, self.height],
+ source_size: Vec2::from([self.width as f32, self.height as f32]),
+ pixels,
+ },
+ egui::TextureOptions::NEAREST,
+ );
+ }
+
+ // scale to fit available space while preserving aspect ratio
+ let available = ui.available_size();
+ let aspect = self.width as f32 / self.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));
+ }
+}
diff --git a/dsa/emulator/src/frontend/components/registers.rs b/dsa/emulator/src/frontend/components/registers.rs
new file mode 100644
index 0000000..59c3a9d
--- /dev/null
+++ b/dsa/emulator/src/frontend/components/registers.rs
@@ -0,0 +1,131 @@
+use std::{f32, sync::Arc};
+
+use common::prelude::Register;
+
+use crate::SharedState;
+
+use super::Component;
+
+pub struct Registers {
+ state: Arc,
+ visible: bool,
+}
+
+impl Registers {
+ fn section(
+ &mut self,
+ ui: &mut egui::Ui,
+ ctx: &egui::Context,
+ 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} ({})", val, val));
+ }
+ for _ in chunk.len()..col_pairs {
+ ui.label("");
+ ui.label("");
+ }
+ ui.end_row();
+ }
+ });
+ });
+ }
+}
+
+impl Registers {
+ pub fn new(state: Arc) -> Self {
+ Self {
+ state,
+ visible: false,
+ }
+ }
+}
+
+impl Component for Registers {
+ fn title(&self) -> &str {
+ "Registers"
+ }
+
+ fn visible(&mut self) -> &mut bool {
+ &mut self.visible
+ }
+
+ fn ui(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) {
+ ui.set_min_width(200.0);
+
+ let state = self.state.proc.load();
+ 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(), state.registers[i as usize]))
+ .collect();
+ self.section(
+ ui,
+ ctx,
+ gp_registers,
+ col_pairs,
+ "General Purpose Registers",
+ );
+ ui.separator();
+
+ // ── Stack ─────────────────────────────────────────────────
+ let stack_registers = vec![
+ (Register::Spr, state.registers[Register::Spr as usize]),
+ (Register::Bpr, state.registers[Register::Bpr as usize]),
+ ];
+ self.section(ui, ctx, stack_registers, col_pairs, "Stack Registers");
+ ui.separator();
+
+ // ── Special Purpose ───────────────────────────────────────
+ let special_registers = vec![
+ (Register::Acc, state.registers[Register::Acc as usize]),
+ (Register::Ret, state.registers[Register::Ret as usize]),
+ (Register::Idr, state.registers[Register::Idr as usize]),
+ (Register::Mmr, state.registers[Register::Mmr as usize]),
+ ];
+ self.section(
+ ui,
+ ctx,
+ special_registers,
+ col_pairs,
+ "Special Purpose Registers",
+ );
+ ui.separator();
+
+ // ── System ────────────────────────────────────────────────
+ let system_registers = vec![
+ (Register::Pcx, state.registers[Register::Pcx as usize]),
+ (Register::Sts, state.registers[Register::Sts as usize]),
+ (Register::Cir, state.registers[Register::Cir as usize]),
+ ];
+ self.section(ui, ctx, system_registers, col_pairs, "System Registers");
+ });
+ }
+}
diff --git a/dsa/emulator/src/frontend/components/serial.rs b/dsa/emulator/src/frontend/components/serial.rs
new file mode 100644
index 0000000..4a6ba7d
--- /dev/null
+++ b/dsa/emulator/src/frontend/components/serial.rs
@@ -0,0 +1,17 @@
+use crate::{frontend::components::Component, io::serial::Serial};
+
+impl Component for Serial {
+ fn title(&self) -> &str {
+ "Serial"
+ }
+
+ fn visible(&mut self) -> &mut bool {
+ &mut self.visible
+ }
+
+ fn ui(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) {
+ self.update();
+
+ ui.label(String::from_utf8(self.buffer.clone()).unwrap());
+ }
+}
diff --git a/dsa/emulator/src/frontend/mod.rs b/dsa/emulator/src/frontend/mod.rs
new file mode 100644
index 0000000..dc152cb
--- /dev/null
+++ b/dsa/emulator/src/frontend/mod.rs
@@ -0,0 +1,149 @@
+use eframe::NativeOptions;
+use eframe::egui;
+use std::sync::Arc;
+use std::sync::atomic::Ordering;
+
+mod components;
+
+use crate::frontend::components::Component;
+use crate::frontend::components::framebuffer::FrameBuffer;
+use crate::io::serial::Serial;
+use crate::{MemoryBank, SharedState, config::VERSION};
+use components::{controller::Controller, display::Display, registers::Registers};
+
+pub fn run_app(state: Arc, mem_handle: MemoryBank) -> eframe::Result<()> {
+ eframe::run_native(
+ "Damn Simple Architecture",
+ NativeOptions::default(),
+ Box::new(|cc| Ok(Box::new(DsaUi::new(cc, state, mem_handle)))),
+ )
+}
+
+impl eframe::App for DsaUi {
+ fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
+ // request an update from emulator every cycle
+ self.state.update_req.store(true, Ordering::Relaxed);
+
+ // title panel with dsa title
+ egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
+ ui.with_layout(
+ egui::Layout::top_down_justified(egui::Align::Center)
+ .with_main_align(egui::Align::Min),
+ |ui| {
+ ui.allocate_space(egui::vec2(0.0, 5.0));
+ ui.heading(format!("Damn Simple Architecture v{} 🚀", VERSION));
+ ui.allocate_space(egui::vec2(0.0, 5.0));
+ },
+ );
+ });
+
+ // menu bar.
+ egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
+ egui::MenuBar::new().ui(ui, |ui| {
+ ui.menu_button("Panels", |ui| {
+ ui.set_max_width(300.0);
+ ui.set_min_width(300.0);
+ ui.spacing_mut().button_padding = egui::vec2(10.0, 5.0);
+
+ for comp in &mut self.components {
+ let name = comp.title().to_string();
+ ui.toggle_value(comp.visible(), name);
+ }
+ });
+ });
+ });
+
+ egui::CentralPanel::default().show(ctx, |ui| {
+ for c in &mut self.components {
+ let mut visible = *c.visible();
+ if visible {
+ egui::Window::new(c.title())
+ .open(&mut visible)
+ .show(ctx, |ui| {
+ c.ui(ui, ctx);
+ });
+ }
+ *c.visible() = visible;
+ }
+ });
+
+ ctx.request_repaint_after(std::time::Duration::from_millis(16)); // ~60fps
+ }
+}
+
+pub struct DsaUi {
+ state: Arc,
+ mem_handle: MemoryBank,
+ components: Vec>,
+}
+
+impl DsaUi {
+ pub fn new(
+ cc: &eframe::CreationContext,
+ state: Arc,
+ mem_handle: MemoryBank,
+ ) -> Self {
+ // components
+ let mut components: Vec> = vec![
+ Box::new(Controller::new(state.clone(), mem_handle.clone())),
+ Box::new(Display::new(80, 25, 0x20000, mem_handle.clone())),
+ Box::new(FrameBuffer::new(320, 240, 0x30000, mem_handle.clone())),
+ Box::new(Registers::new(state.clone())),
+ Box::new(Serial::new(0x40000, state.clone(), mem_handle.clone())),
+ ];
+ components.iter_mut().for_each(|c| *c.visible() = false);
+
+ Self::configure_appearance(&cc.egui_ctx);
+ let app = Self {
+ state,
+ mem_handle,
+ components,
+ };
+ app
+ }
+
+ fn configure_appearance(ctx: &egui::Context) {
+ // configure appearance of UI elements
+ let mut visuals = egui::Visuals::dark();
+
+ // --- your existing settings ---
+ 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);
+ }
+}
diff --git a/dsa/emulator/src/io/display.rs b/dsa/emulator/src/io/display.rs
index 25906d0..68f980b 100644
--- a/dsa/emulator/src/io/display.rs
+++ b/dsa/emulator/src/io/display.rs
@@ -1,80 +1,62 @@
-use std::sync::{
- Arc,
- atomic::{AtomicBool, AtomicU8, Ordering},
-};
+// use super::{IoAccess, IoHandle};
+use crate::{MemoryBank, Page, backend::memory::PhysAddr};
-use crate::{
- io::{IoAccess, IoDevice},
- processor::interrupts::Interrupt,
-};
+pub struct Display {
+ width: usize,
+ height: usize,
+ addr: PhysAddr,
-pub struct DisplayDevice {
- buffer: Arc>,
- dirty: Arc,
+ mem: MemoryBank,
+ buffer: Vec,
+ visible: bool,
}
-impl DisplayDevice {
- pub fn new(width: usize, height: usize) -> (Self, DisplayHandle) {
- let buffer = Arc::new(
- (0..width * height)
- .map(|_| AtomicU8::new(0))
- .collect::>()
- .into_boxed_slice(),
- );
- let dirty = Arc::new(AtomicBool::new(false));
+impl Display {
+ pub fn new(width: u32, height: u32, addr: PhysAddr, mem: MemoryBank) -> Self {
+ Self {
+ width: width as usize,
+ height: height as usize,
+ addr,
+ mem,
+ buffer: Vec::new(),
+ visible: true,
+ }
+ }
- let handle = DisplayHandle {
- buffer: Arc::clone(&buffer),
- dirty: Arc::clone(&dirty),
- };
+ pub fn visible(&mut self) -> &mut bool {
+ &mut self.visible
+ }
- (Self { buffer, dirty }, handle)
- }
-}
-
-impl IoDevice for DisplayDevice {
- fn size(&self) -> u32 {
- (self.buffer.len()) as u32
- }
- fn access(&self) -> IoAccess {
- IoAccess::WRITE
- }
-
- fn id(&self) -> super::DeviceId {
- super::DeviceId::Display
- }
-
- fn write_word(&mut self, offset: u32, val: u32) -> Result<(), Interrupt> {
- let bytes = val.to_le_bytes();
- self.buffer[offset as usize].store(bytes[0], Ordering::Relaxed);
- self.buffer[offset as usize + 1].store(bytes[1], Ordering::Relaxed);
- self.buffer[offset as usize + 2].store(bytes[2], Ordering::Relaxed);
- self.buffer[offset as usize + 3].store(bytes[3], Ordering::Relaxed);
- self.dirty.store(true, Ordering::Relaxed);
- Ok(())
- }
-
- fn write_byte(&mut self, offset: u32, val: u8) -> Result<(), Interrupt> {
- self.buffer[offset as usize].store(val, Ordering::Relaxed);
- self.dirty.store(true, Ordering::Relaxed);
- Ok(())
- }
-}
-
-pub struct DisplayHandle {
- pub buffer: Arc>,
- pub dirty: Arc,
-}
-
-impl DisplayHandle {
- pub fn is_dirty(&self) -> bool {
- self.dirty.swap(false, Ordering::Relaxed)
- }
-
- pub fn read_all(&self) -> Vec {
- self.buffer
- .iter()
- .map(|b| b.load(Ordering::Relaxed))
- .collect()
+ pub fn width(&self) -> usize {
+ self.width
+ }
+
+ pub fn height(&self) -> usize {
+ self.height
+ }
+
+ fn internal_read(&self) -> Vec {
+ let pages = (0..((self.width * self.height).div_ceil(Page::SIZE)))
+ .map(|i| {
+ let page = self.mem.read_page(self.addr + i as u32 * 4096);
+ page.as_slice().to_owned()
+ })
+ .flatten()
+ .take((self.width * self.height) as usize)
+ .collect::>();
+ pages
+ }
+
+ pub fn changed(&mut self) -> bool {
+ let temp = self.internal_read();
+ if temp != self.buffer {
+ self.buffer = temp;
+ return true;
+ }
+ false
+ }
+
+ pub fn read(&mut self) -> Vec {
+ self.internal_read()
}
}
diff --git a/dsa/emulator/src/io/mod.rs b/dsa/emulator/src/io/mod.rs
index adaf491..204ffb4 100644
--- a/dsa/emulator/src/io/mod.rs
+++ b/dsa/emulator/src/io/mod.rs
@@ -1,124 +1,46 @@
use serde::{Deserialize, Serialize};
-use crate::{Emulator, RandomAccessMemory, processor::interrupts::Interrupt};
-
pub mod display;
+pub mod serial;
+// #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+// pub enum DeviceId {
+// Display,
+// Serial,
+// Random,
+// Timer,
+// }
-pub struct MappedDevice {
- pub base: u32,
- pub device: Box,
-}
+// pub trait IoHandle: Send {
+// /// Return the size (in bytes) of the device's addressable region.
+// fn size(&self) -> usize;
-#[derive(Clone, Copy, Debug, PartialEq)]
-pub struct IoAccess(u8);
+// /// Return the access permissions for the device.
+// fn access(&self) -> IoAccess;
-impl IoAccess {
- pub const READ: Self = Self(0b01);
- pub const WRITE: Self = Self(0b10);
+// /// Return the DeviceId
+// fn id(&self) -> DeviceId;
+// }
- /// Convenience alias for Read + Write.
- pub const READWRITE: Self = Self(Self::READ.0 | Self::WRITE.0);
- pub const NONE: Self = Self(0);
+// #[derive(Clone, Copy, Debug, PartialEq)]
+// pub struct IoAccess(u8);
- /// Bitwise OR – returns a new `IoAccess` that contains all flags from both operands.
- #[inline]
- pub fn or(self, other: Self) -> Self {
- Self(self.0 | other.0)
- }
+// impl IoAccess {
+// pub const READ: Self = Self(0b01);
+// pub const WRITE: Self = Self(0b10);
- /// Check if a flag is present.
- #[inline]
- pub fn contains(&self, flag: Self) -> bool {
- self.0 & flag.0 != 0
- }
-}
+// /// Convenience alias for Read + Write.
+// pub const READWRITE: Self = Self(Self::READ.0 | Self::WRITE.0);
+// pub const NONE: Self = Self(0);
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-pub enum DeviceId {
- Display,
- Serial,
- Random,
- Timer,
-}
+// /// Bitwise OR – returns a new `IoAccess` that contains all flags from both operands.
+// #[inline]
+// pub fn or(self, other: Self) -> Self {
+// Self(self.0 | other.0)
+// }
-/// Trait representing an input/output device that can be mapped into a memory space.
-///
-/// - Default methods return dummy data.
-pub trait IoDevice: Send {
- /// Return the size (in bytes) of the device's addressable region.
- fn size(&self) -> u32;
-
- /// Return the access permissions for the device.
- fn access(&self) -> IoAccess;
-
- /// Return the DeviceId
- fn id(&self) -> DeviceId;
-
- #[inline]
- /// Read a single byte from the specified address.
- ///
- /// By default, write‑only devices return `None`. All other devices
- /// return `Some(0)` as placeholder data. Implementations should override
- /// this method to provide actual read logic.
- fn read_byte(&self, offset: u32) -> Result {
- if self.access().contains(IoAccess::READ) {
- Ok(0)
- } else {
- Err(Interrupt::ReadFromWriteOnly)
- }
- }
-
- #[inline]
- /// Write a single byte to the specified address.
- ///
- /// By default, read‑only devices return `false` indicating failure.
- /// All other devices return `true`. Implementations should override
- /// this method to perform real write operations and return whether
- /// the operation succeeded.
- fn write_byte(&mut self, offset: u32, val: u8) -> Result<(), Interrupt> {
- if self.access().contains(IoAccess::WRITE) {
- Ok(())
- } else {
- Err(Interrupt::WriteToReadOnly)
- }
- }
-
- /// Read a 32‑bit word from the specified address in little‑endian order.
- ///
- /// The default implementation composes four consecutive byte reads using
- /// `read_byte`. If any byte read fails, the whole operation returns `None`.
- /// Write‑only devices return `None` immediately. Override this method for
- /// more efficient word reads.
- #[inline]
- fn read_word(&self, offset: u32) -> Result {
- if self.access().contains(IoAccess::READ) {
- // default: compose from bytes
- let b0 = self.read_byte(offset)? as u32;
- let b1 = self.read_byte(offset + 1)? as u32;
- let b2 = self.read_byte(offset + 2)? as u32;
- let b3 = self.read_byte(offset + 3)? as u32;
- return Ok(b0 | b1 << 8 | b2 << 16 | b3 << 24);
- }
-
- Err(Interrupt::ReadFromWriteOnly)
- }
-
- /// Write a 32‑bit word to the specified address in little‑endian order.
- ///
- /// The default implementation splits the value into bytes and writes each
- /// using `write_byte`. Read‑only devices return `false`; otherwise returns
- /// `true` after attempting all byte writes. Override for more efficient
- /// word writes or to handle partial failures.
- fn write_word(&mut self, offset: u32, val: u32) -> Result<(), Interrupt> {
- if self.access().contains(IoAccess::WRITE) {
- let bytes = val.to_le_bytes();
- self.write_byte(offset, bytes[0]);
- self.write_byte(offset + 1, bytes[1]);
- self.write_byte(offset + 2, bytes[2]);
- self.write_byte(offset + 3, bytes[3]);
- return Ok(());
- }
-
- Err(Interrupt::WriteToReadOnly)
- }
-}
+// /// Check if a flag is present.
+// #[inline]
+// pub fn contains(&self, flag: Self) -> bool {
+// self.0 & flag.0 != 0
+// }
+// }
diff --git a/dsa/emulator/src/io/serial.rs b/dsa/emulator/src/io/serial.rs
new file mode 100644
index 0000000..c431c3f
--- /dev/null
+++ b/dsa/emulator/src/io/serial.rs
@@ -0,0 +1,48 @@
+use std::{
+ path::Component,
+ sync::{
+ Arc,
+ mpsc::{Receiver, Sender},
+ },
+};
+
+use crate::{MemoryBank, SharedState, backend::memory::PhysAddr};
+
+pub struct Serial {
+ pub receiver: Receiver,
+ pub buffer: Vec,
+
+ pub visible: bool,
+}
+
+impl Serial {
+ fn uart_io_thread(mem: MemoryBank, uart_base: PhysAddr, tx: Sender) {
+ loop {
+ let word = mem.read_word(uart_base);
+ // println!("word {:#010X}", word);
+ if word >> 24 == 0x01 {
+ let byte = ((word >> 16) & 0xFF) as u8;
+ let _ = tx.send(byte);
+ mem.write_word(uart_base, 0x00_00_00_00); // clear valid flag
+ }
+ std::hint::spin_loop();
+ }
+ }
+
+ pub fn new(uart_base: PhysAddr, state: Arc, mem_handle: MemoryBank) -> Self {
+ let (sender, receiver) = std::sync::mpsc::channel();
+ let _ = std::thread::spawn(move || Self::uart_io_thread(mem_handle, uart_base, sender));
+
+ Self {
+ visible: false,
+ receiver,
+ buffer: Vec::new(),
+ }
+ }
+
+ pub fn update(&mut self) {
+ while let Ok(byte) = self.receiver.try_recv() {
+ self.buffer.push(byte);
+ }
+ }
+}
diff --git a/dsa/emulator/src/lib.rs b/dsa/emulator/src/lib.rs
index 9920c43..da07b95 100644
--- a/dsa/emulator/src/lib.rs
+++ b/dsa/emulator/src/lib.rs
@@ -2,11 +2,10 @@
#![feature(inherent_associated_types)]
pub mod args;
+pub mod backend;
pub mod config;
+pub mod frontend;
pub mod io;
-mod memory;
-mod processor;
-pub use {config::MemoryMap, processor::processor::Emulator, processor::state::SharedState};
-
-pub use memory::ram::*;
+pub use backend::memory::ram::*;
+pub use backend::{processor::processor::Emulator, processor::state::SharedState};
diff --git a/dsa/emulator/src/main.rs b/dsa/emulator/src/main.rs
index 2842702..c2a6400 100644
--- a/dsa/emulator/src/main.rs
+++ b/dsa/emulator/src/main.rs
@@ -1,32 +1,18 @@
use clap::Parser;
-use common::{instructions::Instruction, register::Register};
-use dsa::{
- BulkAllocStore, Emulator, Page, RandomAccessMemory, SharedState,
- args::DsaArgs,
- io::display::{DisplayDevice, DisplayHandle},
-};
-use std::{
- os::unix::thread::JoinHandleExt,
- process::exit,
- sync::{Arc, atomic::Ordering},
- thread,
- time::Duration,
-};
+
+use common::prelude::Instruction;
+use dsa::{Emulator, Page, args::DsaArgs, frontend::run_app};
+use std::thread;
+
+const STACK_SIZE: usize = 1024 * 1024 * 16;
fn main() {
let args = DsaArgs::parse();
- let mmap = args.get_memory_map();
- let iomap = args.get_io_map();
+ let mut emulator = Emulator::new().apply_memory_map(args.get_memory_map());
- let store = BulkAllocStore::new();
- let (display, handle) = DisplayDevice::new(80, 25);
-
- let mut emulator = Emulator::new(store)
- .with_device(display)
- .apply_memory_map(mmap)
- .apply_io_map(iomap)
- .unwrap();
+ let mem_handle = emulator.memory_mut().clone();
+ let state = emulator.state_handle();
if let Some(bin) = args.get_binary() {
for (i, ch) in bin.chunks(4).enumerate() {
@@ -56,41 +42,10 @@ fn main() {
}
}
- let state = emulator.state_handle();
-
- const STACK_SIZE: usize = 1024 * 1024 * 16;
- let runner = thread::Builder::new()
+ let _runner = thread::Builder::new()
.stack_size(STACK_SIZE)
.spawn(move || emulator.run().unwrap())
.unwrap();
- let observer = thread::spawn(|| observe(state, handle));
-
- runner.join().unwrap();
- thread::sleep(Duration::from_millis(100));
-
- // yeah we don't need this tbh.
- // observer.join().unwrap();
-}
-
-/// todo: remove this!
-fn observe(state: Arc, handle: DisplayHandle) {
- loop {
- state.update_req.store(true, Ordering::Relaxed);
- thread::sleep(std::time::Duration::from_millis(20));
- let state = state.proc.load();
- // println!(
- // "clock: {}, GP Registers: {:?}",
- // state.clock, state.registers
- // );
-
- if handle.is_dirty() {
- for line in handle.read_all().chunks(80) {
- for (i, byte) in line.iter().enumerate() {
- print!("{}", *byte as char);
- }
- println!();
- }
- }
- }
+ run_app(state, mem_handle).unwrap()
}
diff --git a/dsa/emulator/src/memory/ram/arraymap.rs b/dsa/emulator/src/memory/ram/arraymap.rs
deleted file mode 100644
index 8b3016b..0000000
--- a/dsa/emulator/src/memory/ram/arraymap.rs
+++ /dev/null
@@ -1,107 +0,0 @@
-use std::{
- alloc::{Layout, alloc_zeroed},
- hint::{likely, unlikely},
-};
-
-use crate::memory::{
- PhysAddr, RandomAccessMemory,
- ram::{NUM_PAGES, Page, idx},
-};
-
-pub struct ArrayStore {
- pages: Box<[*mut Page; NUM_PAGES]>,
-}
-
-unsafe impl Send for ArrayStore {}
-impl ArrayStore {
- pub fn new() -> Self {
- let pages = vec![0 as *mut Page; 2 << 20]
- .into_boxed_slice()
- .try_into()
- .unwrap();
- Self { pages }
- }
-
- fn alloc(&mut self) -> *mut Page {
- let layout = Layout::from_size_align(4096, 4096).unwrap();
- unsafe { alloc_zeroed(layout) as *mut Page }
- }
-}
-
-impl RandomAccessMemory for ArrayStore {
- #[inline(always)]
- fn read_word(&self, addr: PhysAddr) -> u32 {
- debug_assert_eq!(addr % 4, 0);
- let (idx, offset) = idx(addr);
-
- if likely(!self.pages[idx].is_null()) {
- return unsafe { (self.pages[idx] as *const u32).byte_add(offset).read() };
- }
-
- // Slow path: MMIO, fault, etc — never inlined
- // Since we're only reading, we can safely return 0
- return 0;
- }
-
- #[inline(always)]
- fn read_byte(&self, addr: PhysAddr) -> u8 {
- let (idx, offset) = idx(addr);
-
- if likely(!self.pages[idx].is_null()) {
- return unsafe { (self.pages[idx] as *const u8).byte_add(offset).read() };
- }
-
- return 0;
- }
-
- #[inline(always)]
- fn read_page(&self, addr: PhysAddr) -> &Page {
- debug_assert_eq!(addr % 4096, 0);
- let (idx, _) = idx(addr);
-
- if likely(!self.pages[idx].is_null()) {
- return unsafe { &*(self.pages[idx] as *const Page) };
- }
-
- return &Page([0; 4096]);
- }
-
- #[inline(always)]
- fn write_byte(&mut self, addr: PhysAddr, value: u8) {
- let (idx, offset) = idx(addr);
-
- if unlikely(self.pages[idx].is_null()) {
- self.pages[idx] = self.alloc();
- }
-
- unsafe {
- (self.pages[idx] as *mut u8).byte_add(offset).write(value);
- }
- }
-
- #[inline(always)]
- fn write_word(&mut self, addr: PhysAddr, value: u32) {
- debug_assert_eq!(addr % 4, 0);
- let (idx, offset) = idx(addr);
-
- if unlikely(self.pages[idx].is_null()) {
- self.pages[idx] = self.alloc();
- }
-
- unsafe { (self.pages[idx] as *mut u32).byte_add(offset).write(value) }
- }
-
- #[inline(always)]
- fn write_page(&mut self, addr: PhysAddr, value: &Page) {
- debug_assert_eq!(addr % 4096, 0);
- let (idx, _) = idx(addr);
-
- if unlikely(self.pages[idx].is_null()) {
- self.pages[idx] = self.alloc();
- }
-
- unsafe {
- (self.pages[idx] as *mut Page).copy_from(value, 1);
- }
- }
-}
diff --git a/dsa/emulator/src/memory/ram/bulkalloc.rs b/dsa/emulator/src/memory/ram/bulkalloc.rs
deleted file mode 100644
index 278b457..0000000
--- a/dsa/emulator/src/memory/ram/bulkalloc.rs
+++ /dev/null
@@ -1,134 +0,0 @@
-use std::{
- alloc::{Layout, alloc_zeroed},
- hint::{likely, unlikely},
-};
-
-use crate::memory::{
- PhysAddr, RandomAccessMemory,
- ram::{Page, idx},
-};
-
-pub struct BulkAllocStore {
- pages: Box<[*mut Page; Page::COUNT]>,
- pre_allocated: [*mut Page; Self::ALLOC_AT_ONCE],
- idx: u8,
-}
-
-unsafe impl Send for BulkAllocStore {}
-impl BulkAllocStore {
- const ALLOC_AT_ONCE: usize = 256 as usize;
-
- pub fn new() -> Self {
- let pages = vec![0 as *mut Page; Page::COUNT]
- .into_boxed_slice()
- .try_into()
- .unwrap();
-
- Self {
- pages,
- pre_allocated: Self::alloc_set(),
- idx: 0,
- }
- }
-
- fn alloc_set() -> [*mut Page; Self::ALLOC_AT_ONCE] {
- let layout = Layout::from_size_align(Page::SIZE * Self::ALLOC_AT_ONCE, Page::SIZE).unwrap();
- let mut region = unsafe { alloc_zeroed(layout) as *mut Page };
- let mut set = [0 as *mut Page; Self::ALLOC_AT_ONCE];
- for i in 0..Self::ALLOC_AT_ONCE {
- set[i] = region;
- region = unsafe { region.byte_add(4096) };
- }
- set
- }
-
- fn alloc(&mut self) -> *mut Page {
- if self.idx < Self::ALLOC_AT_ONCE as u8 {
- let page = self.pre_allocated[self.idx as usize];
- self.idx += 1;
- page
- } else {
- self.idx = 0;
- self.pre_allocated = Self::alloc_set();
- self.pre_allocated[0]
- }
- }
-}
-
-impl RandomAccessMemory for BulkAllocStore {
- #[inline(always)]
- fn read_word(&self, addr: PhysAddr) -> u32 {
- debug_assert_eq!(addr % 4, 0);
- let (idx, offset) = idx(addr);
-
- if likely(!self.pages[idx].is_null()) {
- return unsafe { (self.pages[idx] as *const u32).byte_add(offset).read() };
- }
-
- // Slow path: MMIO, fault, etc — never inlined
- // Since we're only reading, we can safely return 0
- return 0;
- }
-
- #[inline(always)]
- fn read_byte(&self, addr: PhysAddr) -> u8 {
- let (idx, offset) = idx(addr);
-
- if likely(!self.pages[idx].is_null()) {
- return unsafe { (self.pages[idx] as *const u8).byte_add(offset).read() };
- }
-
- return 0;
- }
-
- #[inline(always)]
- fn read_page(&self, addr: PhysAddr) -> &Page {
- debug_assert_eq!(addr % Page::SIZE as u32, 0);
- let (idx, _) = idx(addr);
-
- if likely(!self.pages[idx].is_null()) {
- return unsafe { &*(self.pages[idx] as *const Page) };
- }
-
- return &Page::ZERO;
- }
-
- #[inline(always)]
- fn write_byte(&mut self, addr: PhysAddr, value: u8) {
- let (idx, offset) = idx(addr);
-
- if unlikely(self.pages[idx].is_null()) {
- self.pages[idx] = self.alloc();
- }
-
- unsafe {
- (self.pages[idx] as *mut u8).byte_add(offset).write(value);
- }
- }
-
- #[inline(always)]
- fn write_word(&mut self, addr: PhysAddr, value: u32) {
- debug_assert_eq!(addr % 4, 0);
- let (idx, offset) = idx(addr);
-
- if unlikely(self.pages[idx].is_null()) {
- self.pages[idx] = self.alloc();
- }
-
- unsafe { (self.pages[idx] as *mut u32).byte_add(offset).write(value) }
- }
-
- #[inline(always)]
- fn write_page(&mut self, addr: PhysAddr, value: &Page) {
- debug_assert_eq!(addr % Page::SIZE as u32, 0);
- let (idx, _) = idx(addr);
-
- if unlikely(self.pages[idx].is_null()) {
- self.pages[idx] = self.alloc();
- }
-
- unsafe {
- (self.pages[idx] as *mut Page).copy_from(value, 1);
- }
- }
-}
diff --git a/dsa/emulator/src/memory/ram/hashmap.rs b/dsa/emulator/src/memory/ram/hashmap.rs
deleted file mode 100644
index c73cde6..0000000
--- a/dsa/emulator/src/memory/ram/hashmap.rs
+++ /dev/null
@@ -1,66 +0,0 @@
-use fxhash::FxHashMap;
-
-use crate::memory::{PhysAddr, RandomAccessMemory, ram::Page};
-
-pub struct HashStore {
- pages: FxHashMap,
-}
-
-impl HashStore {
- pub fn new() -> Self {
- Self {
- pages: FxHashMap::default(),
- }
- }
-
- #[inline(always)]
- const fn segment_addr(addr: PhysAddr) -> (PhysAddr, usize) {
- (addr & !(0xFFF), (addr & 0xFFF) as usize)
- }
-}
-
-impl RandomAccessMemory for HashStore {
- #[inline(always)]
- fn read_byte(&self, addr: PhysAddr) -> u8 {
- let (page, offset) = Self::segment_addr(addr);
- self.pages
- .get(&page)
- .map(|p| p.as_ref()[offset])
- .unwrap_or(0)
- }
-
- #[inline(always)]
- fn read_word(&self, addr: PhysAddr) -> u32 {
- let (page, offset) = Self::segment_addr(addr);
- debug_assert_eq!(offset % 4, 0);
- let page = self.pages.get(&page).unwrap_or(&Page::ZERO);
- u32::from_be_bytes(page[offset..=offset + 3].try_into().unwrap())
- }
-
- #[inline(always)]
- fn read_page(&self, addr: PhysAddr) -> &Page {
- debug_assert_eq!(addr % 0x1000, 0);
- self.pages.get(&addr).unwrap_or(&Page::ZERO)
- }
-
- #[inline(always)]
- fn write_byte(&mut self, addr: PhysAddr, value: u8) {
- let (page, offset) = Self::segment_addr(addr);
- self.pages.entry(page).or_insert_with(|| Page::zeroed())[offset] = value;
- }
-
- #[inline(always)]
- fn write_word(&mut self, addr: PhysAddr, value: u32) {
- let (page, offset) = Self::segment_addr(addr);
- debug_assert_eq!(offset % 4, 0);
- let page = self.pages.entry(page).or_insert_with(|| Page::zeroed());
- page[offset..=offset + 3].copy_from_slice(&value.to_be_bytes());
- }
-
- #[inline(always)]
- fn write_page(&mut self, addr: PhysAddr, value: &Page) {
- debug_assert_eq!(addr % 0x1000, 0);
- let page = self.pages.entry(addr).or_insert_with(|| Page::zeroed());
- page.0 = value.0;
- }
-}
diff --git a/dsa/emulator/src/memory/ram/mod.rs b/dsa/emulator/src/memory/ram/mod.rs
deleted file mode 100644
index 97f4578..0000000
--- a/dsa/emulator/src/memory/ram/mod.rs
+++ /dev/null
@@ -1,108 +0,0 @@
-use std::ops::{Deref, DerefMut};
-
-use crate::memory::PhysAddr;
-
-#[cfg(feature = "mainstore-arraymap")]
-pub mod arraymap;
-#[cfg(feature = "mainstore-arraymap")]
-pub use arraymap::ArrayStore;
-
-#[cfg(feature = "mainstore-hashmap")]
-pub mod hashmap;
-#[cfg(feature = "mainstore-hashmap")]
-pub use hashmap::HashStore;
-
-#[cfg(feature = "mainstore-stackarray")]
-pub mod stackarray;
-#[cfg(feature = "mainstore-stackarray")]
-pub use stackarray::StackArrayStore;
-
-#[cfg(feature = "mainstore-bulkalloc")]
-pub mod bulkalloc;
-#[cfg(feature = "mainstore-bulkalloc")]
-pub use bulkalloc::BulkAllocStore;
-
-#[cfg(feature = "mainstore-prealloc")]
-pub mod prealloc;
-#[cfg(feature = "mainstore-prealloc")]
-pub use prealloc::PreAllocStore;
-
-#[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
- + '_ {
- 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> for Page {
- type Error = String;
-
- fn try_from(data: Vec) -> Result {
- let arr: [u8; 4096] = data
- .try_into()
- .map_err(|v: Vec| format!("Expected 4096 bytes, got {}", v.len()))?;
- Ok(Page(arr))
- }
-}
-
-#[inline(always)]
-const fn idx(addr: PhysAddr) -> (usize, usize) {
- ((addr >> 12) as usize, (addr & 0xFFF) as usize)
-}
-
-pub trait RandomAccessMemory {
- fn read_byte(&self, addr: PhysAddr) -> u8;
-
- fn write_byte(&mut self, addr: PhysAddr, value: u8);
-
- fn read_word(&self, addr: PhysAddr) -> u32;
-
- fn write_word(&mut self, addr: PhysAddr, value: u32);
-
- fn read_page(&self, addr: PhysAddr) -> &Page;
-
- fn write_page(&mut self, addr: PhysAddr, value: &Page);
-}
diff --git a/dsa/emulator/src/memory/ram/prealloc.rs b/dsa/emulator/src/memory/ram/prealloc.rs
deleted file mode 100644
index 76f0f55..0000000
--- a/dsa/emulator/src/memory/ram/prealloc.rs
+++ /dev/null
@@ -1,50 +0,0 @@
-use crate::memory::{
- PhysAddr, RandomAccessMemory,
- ram::{NUM_PAGES, Page},
-};
-
-pub struct PreAllocStore {
- heap: Box<[u8; NUM_PAGES * 4096]>,
-}
-
-unsafe impl Send for PreAllocStore {}
-impl PreAllocStore {
- pub fn new() -> Self {
- Self {
- heap: unsafe { Box::new_zeroed().assume_init() },
- }
- }
-}
-
-impl RandomAccessMemory for PreAllocStore {
- #[inline(always)]
- fn read_word(&self, addr: PhysAddr) -> u32 {
- unsafe { (self.heap.as_ptr().add(addr as usize) as *const u32).read() }
- }
-
- #[inline(always)]
- fn read_byte(&self, addr: PhysAddr) -> u8 {
- self.heap[addr as usize]
- }
-
- #[inline(always)]
- fn read_page(&self, addr: PhysAddr) -> &Page {
- debug_assert_eq!(addr % 4096, 0);
- unsafe { (self.heap.as_ptr().add(addr as usize) as *const Page).as_ref_unchecked() }
- }
-
- #[inline(always)]
- fn write_byte(&mut self, addr: PhysAddr, value: u8) {
- self.heap[addr as usize] = value;
- }
-
- #[inline(always)]
- fn write_word(&mut self, addr: PhysAddr, value: u32) {
- unsafe { (self.heap.as_ptr().add(addr as usize) as *mut u32).write(value) };
- }
-
- #[inline(always)]
- fn write_page(&mut self, addr: PhysAddr, value: &Page) {
- self.heap[addr as usize..addr as usize + 4096].copy_from_slice(value);
- }
-}
diff --git a/dsa/emulator/src/memory/ram/stackarray.rs b/dsa/emulator/src/memory/ram/stackarray.rs
deleted file mode 100644
index 61ac1aa..0000000
--- a/dsa/emulator/src/memory/ram/stackarray.rs
+++ /dev/null
@@ -1,103 +0,0 @@
-use std::{
- alloc::{Layout, alloc_zeroed},
- hint::{likely, unlikely},
-};
-
-use crate::memory::{
- PhysAddr, RandomAccessMemory,
- ram::{NUM_PAGES, Page, idx},
-};
-
-pub struct StackArrayStore {
- pages: [*mut Page; NUM_PAGES],
-}
-
-unsafe impl Send for StackArrayStore {}
-impl StackArrayStore {
- pub fn new() -> Self {
- Self {
- pages: [0 as *mut Page; 2 << 20],
- }
- }
-
- fn alloc(&mut self) -> *mut Page {
- let layout = Layout::from_size_align(4096, 4096).unwrap();
- unsafe { alloc_zeroed(layout) as *mut Page }
- }
-}
-
-impl RandomAccessMemory for StackArrayStore {
- #[inline(always)]
- fn read_word(&self, addr: PhysAddr) -> u32 {
- debug_assert_eq!(addr % 4, 0);
- let (idx, offset) = idx(addr);
-
- if likely(!self.pages[idx].is_null()) {
- return unsafe { (self.pages[idx] as *const u32).byte_add(offset).read() };
- }
-
- // Slow path: MMIO, fault, etc — never inlined
- // Since we're only reading, we can safely return 0
- return 0;
- }
-
- #[inline(always)]
- fn read_byte(&self, addr: PhysAddr) -> u8 {
- let (idx, offset) = idx(addr);
-
- if likely(!self.pages[idx].is_null()) {
- return unsafe { (self.pages[idx] as *const u8).byte_add(offset).read() };
- }
-
- return 0;
- }
-
- #[inline(always)]
- fn read_page(&self, addr: PhysAddr) -> &Page {
- debug_assert_eq!(addr % 4096, 0);
- let (idx, _) = idx(addr);
-
- if likely(!self.pages[idx].is_null()) {
- return unsafe { &*(self.pages[idx] as *const Page) };
- }
-
- return &Page::ZERO;
- }
-
- #[inline(always)]
- fn write_byte(&mut self, addr: PhysAddr, value: u8) {
- let (idx, offset) = idx(addr);
-
- if unlikely(self.pages[idx].is_null()) {
- self.pages[idx] = self.alloc();
- }
-
- unsafe {
- (self.pages[idx] as *mut u8).byte_add(offset).write(value);
- }
- }
-
- #[inline(always)]
- fn write_word(&mut self, addr: PhysAddr, value: u32) {
- debug_assert_eq!(addr % 4, 0);
- let (idx, offset) = idx(addr);
-
- if unlikely(self.pages[idx].is_null()) {
- self.pages[idx] = self.alloc();
- }
-
- unsafe { (self.pages[idx] as *mut u32).byte_add(offset).write(value) }
- }
-
- #[inline(always)]
- fn write_page(&mut self, addr: PhysAddr, value: &Page) {
- debug_assert_eq!(addr % 4096, 0);
- let (idx, _) = idx(addr);
-
- if unlikely(self.pages[idx].is_null()) {
- self.pages[idx] = self.alloc();
- }
-
- unsafe { (self.pages[idx] as *mut Page).copy_from(value, 1) }
- }
-}
diff --git a/dsa_resources/animation.dsa b/dsa_resources/animation.dsa
new file mode 100644
index 0000000..43ecc92
--- /dev/null
+++ b/dsa_resources/animation.dsa
@@ -0,0 +1,133 @@
+dw DISPLAY: 0x30000
+dw stack: 0x10000
+
+// registers:
+// rg0 = x position
+// rg1 = y position
+// rg2 = x direction (1 = right, 0 = left)
+// rg3 = y direction (1 = down, 0 = up)
+
+_init:
+ ldw stack, bpr
+ mov bpr, spr
+
+start:
+ lli 0, rg0
+ lli 0, rg1
+ lli 1, rg2
+ lli 1, rg3
+
+frame_loop:
+ // --- clear display ---
+ ldw DISPLAY, rg4
+ lwi 76800, rg5
+
+clear_loop:
+ stw zero, rg4
+ addi rg4, 4, rg4
+ subi rg5, 1, rg5
+ jnz rg5, clear_loop
+
+ // --- draw square ---
+ mov rg1, rg6
+
+draw_y_loop:
+ lli 20, rg8
+ add rg1, rg8, rg8
+ ilt rg6, rg8, rg9
+ jez rg9, draw_done
+
+ mov rg0, rg7
+
+draw_x_loop:
+ lli 20, rg8
+ add rg0, rg8, rg8
+ ilt rg7, rg8, rg9
+ jez rg9, draw_x_done
+
+ shl rg6, 8, rg8
+ shl rg6, 6, rg9
+ add rg8, rg9, rg8
+ add rg8, rg7, rg8
+ shl rg8, 2, rg8
+ ldw DISPLAY, rg9
+ add rg9, rg8, rg8
+
+ lli 0xFFFF, rg9
+ lui 0xFFFF, rg9
+ stw rg9, rg8
+
+ addi rg7, 1, rg7
+ jmp draw_x_loop
+
+draw_x_done:
+ addi rg6, 1, rg6
+ jmp draw_y_loop
+
+draw_done:
+ // --- check x walls before moving ---
+ lli 1, rg8
+ ieq rg2, rg8, rg9
+ jez rg9, check_x_left_wall
+
+check_x_right_wall:
+ // moving right: if x >= 300 flip direction
+ lli 300, rg8
+ ige rg0, rg8, rg9
+ jez rg9, move_x
+ lli 0, rg2
+ jmp move_x
+
+check_x_left_wall:
+ // moving left: if x == 0 flip direction
+ ieq rg0, zero, rg9
+ jez rg9, move_x
+ lli 1, rg2
+
+move_x:
+ lli 1, rg8
+ ieq rg2, rg8, rg9
+ jez rg9, do_x_left
+ addi rg0, 1, rg0
+ jmp check_y_walls
+do_x_left:
+ subi rg0, 1, rg0
+
+check_y_walls:
+ // --- check y walls before moving ---
+ lli 1, rg8
+ ieq rg3, rg8, rg9
+ jez rg9, check_y_up_wall
+
+check_y_down_wall:
+ // moving down: if y >= 220 flip direction
+ lli 220, rg8
+ ige rg1, rg8, rg9
+ jez rg9, move_y
+ lli 0, rg3
+ jmp move_y
+
+check_y_up_wall:
+ // moving up: if y == 0 flip direction
+ ieq rg1, zero, rg9
+ jez rg9, move_y
+ lli 1, rg3
+
+move_y:
+ lli 1, rg8
+ ieq rg3, rg8, rg9
+ jez rg9, do_y_up
+ addi rg1, 1, rg1
+ jmp delay
+do_y_up:
+ subi rg1, 1, rg1
+
+delay:
+ lwi 2000000, rg8
+delay_loop:
+ subi rg8, 1, rg8
+ jnz rg8, delay_loop
+
+ jmp frame_loop
+
+ hlt
diff --git a/dsa_resources/bf.dsa b/dsa_resources/bf.dsa
index 598c6d2..2d0272a 100644
--- a/dsa_resources/bf.dsa
+++ b/dsa_resources/bf.dsa
@@ -3,7 +3,7 @@
include print "./print.dsa"
-// "print hello world"
+// "prints first 16 fibonacci numbers"
db program: "++++++++++++++++++++++++++++++++++++++++++++
>++++++++++++++++++++++++++++++++
>++++++++++++++++
@@ -35,6 +35,7 @@ db program: "++++++++++++++++++++++++++++++++++++++++++++
<<++..."
db error: "Invalid Instruction!"
+dh counter: 0xFF
dw stack: 0x10000
dw input: 0x30000
resb data: 1024
@@ -44,11 +45,15 @@ _init_stack:
ldw stack, bpr
mov bpr, spr
+init:
+ ldw counter, rg7
+
start:
+ call bf_reset
+
// load the start of the program into rg0
lwi program, rg0
lwi data, rg1
-
// rg0 is our instruction pointer
// rg1 is our data pointer
// rg2 is the value at the data pointer
@@ -98,7 +103,16 @@ loop_start:
pop rg1
pop rg0
+db success: "Success!"
end:
+ call print::newline
+ lwi success, rg0
+ push rg0
+ call print::print
+ pop zero
+
+ subi rg7, 1
+ jnz rg7, start
hlt
loop_end:
@@ -225,3 +239,34 @@ close_left:
// push zero to the stack
push zero
jmp _traverse_left
+
+
+// ------------------------------------------
+// reset interpreter
+// clears screen, resets cursor and clears data buffer.
+bf_reset: func
+ push rg0
+ push rg1
+
+ // clear screen and reset cursor
+ call print::clear
+ call print::reset
+
+ // clear data buffer (resb data: 1024 = 256 words)
+ lli 256, rg0
+ lwi data, rg1
+_bf_reset_clear_data:
+ subi rg0, 1, rg0
+ stw zero, rg1
+ addi rg1, 4, rg1
+ igt rg0, zero, rg4
+ jnz rg4, _bf_reset_clear_data
+
+ pop rg1
+ pop rg0
+
+ // reload interpreter state
+ lwi program, rg0
+ lwi data, rg1
+ lli 0, rg2
+ return
diff --git a/dsa_resources/framebuffer.dsa b/dsa_resources/framebuffer.dsa
new file mode 100644
index 0000000..baad080
--- /dev/null
+++ b/dsa_resources/framebuffer.dsa
@@ -0,0 +1,65 @@
+
+include print: "./print.dsa"
+
+db success: "Success!"
+dw framebuffer: 0x30000
+dw stack: 0x10000
+
+_init:
+ ldw stack, bpr
+ mov bpr, spr
+
+start:
+ ldw framebuffer, rg0 // rg0 = display base address
+ lli 0, rg1 // rg1 = y (0..240)
+
+y_loop:
+ lli 0, rg2 // rg2 = x (0..320)
+
+x_loop:
+ // red = x >> 1 (0..159, left to right gradient)
+ shr rg2, zero, 1, rg4
+
+ // green = y (0..239, top to bottom gradient)
+ mov rg1, rg5
+
+ // blue = constant 128
+ lli 0x80, rg6
+
+ // pack as 0xAABBGGRR (LE reads as RGBA)
+ // start with red in bits 0-7
+ mov rg4, rg7
+
+ // green into bits 8-15
+ shl rg5, zero, 8, rg5
+ or rg7, rg5, rg7
+
+ // blue into bits 16-23
+ shl rg6, zero, 16, rg6
+ or rg7, rg6, rg7
+
+ // alpha into bits 24-31
+ lli 0xFF, rg6
+ shl rg6, zero, 24, rg6
+ or rg7, rg6, rg7
+
+ stw rg7, rg0 // write pixel
+ addi rg0, 4, rg0 // next pixel
+
+ addi rg2, 1, rg2
+ lli 320, rg6
+ ilt rg2, rg6, rg6
+ jnz rg6, x_loop
+
+ addi rg1, 1, rg1
+ lli 240, rg6
+ ilt rg1, rg6, rg6
+ jnz rg6, y_loop
+
+
+_end:
+ lwi success: rg0
+ push rg0
+ call print::print
+ pop zero
+ hlt
diff --git a/dsa_resources/out.dsb b/dsa_resources/out.dsb
index 5128996..7b3af3e 100644
Binary files a/dsa_resources/out.dsb and b/dsa_resources/out.dsb differ
diff --git a/dsa_resources/print.dsa b/dsa_resources/print.dsa
index 5e6816a..f31af1e 100644
--- a/dsa_resources/print.dsa
+++ b/dsa_resources/print.dsa
@@ -185,8 +185,8 @@ print_whitespace: func
addi rg1, 1
jmp _end
-// // ------------------------------------------
-// // print newline
+// ------------------------------------------
+// print newline
// print_newline:
// push bpr
// mov spr, bpr
@@ -214,6 +214,29 @@ print_whitespace: func
// jmp _end
+newline:
+ push bpr
+ mov spr, bpr
+
+ ldw display, rg0
+ ldw current, rg1
+
+ sub rg1, rg0, rg2 // offset from base
+ lli 80, rg3
+
+_newline_mod:
+ ilt rg2, rg3, rg4
+ jnz rg4, _newline_done
+ sub rg2, rg3, rg2
+ jmp _newline_mod
+
+_newline_done:
+ // rg2 = offset % 80 (position within current line)
+ sub rg1, rg2, rg1 // go back to line start
+ addi rg1, 80, rg1 // advance to next line
+
+ jmp _end
+
// ------------------------------------------
// prints arg[0] as a decimal number to the screen.
// print_num:
diff --git a/dsa_resources/serial.dsa b/dsa_resources/serial.dsa
new file mode 100644
index 0000000..66b756d
--- /dev/null
+++ b/dsa_resources/serial.dsa
@@ -0,0 +1,63 @@
+// lib:
+// serial.dsa
+//
+// usage:
+// include serial ""
+//
+// push (register containing address of string)
+// call serial::print
+//
+// push (register containing byte)
+// call serial::print_byte
+
+dw uart: 0x40000
+
+// ------------------------------------------
+// transmit a single byte via sentinel protocol
+// expects: byte in rg0, trashes rg2, rg3, rg4, rg5
+_serial_send:
+ shl rg0, 16, rg2 // shift byte to bits [23:16]
+ lli 0x01, rg0
+ shl rg0, 24, rg0 // valid flag in bits [31:24]
+ or rg0, rg2, rg2 // rg2 = 0x01_XX_0000
+
+ ldw uart, rg5 // rg5 = 0x40000 (uart address, held for store)
+
+_serial_wait:
+ ldw rg5, rg0 // poll mem[0x40000]
+ shr rg0, 24, rg0
+ lli 0x01, rg3
+ ieq rg0, rg3, rg4
+ jnz rg4, _serial_wait // loop while valid == 1
+
+ stw rg2, rg5 // mem[0x40000] = sentinel word
+ ret
+
+// ------------------------------------------
+// prints the null-terminated string at addr(arg[0])
+print: func
+ ldw bpr, rg0, 8 // load string address from arg
+
+_serial_print_loop:
+ ldb rg0, rg2 // load byte
+ ieq rg2, zero, rg4 // null terminator?
+ jnz rg4, _serial_end
+
+ push rg0 // save string pointer
+ mov rg2, rg0
+ call _serial_send
+ pop rg0
+
+ addi rg0, 1
+ jmp _serial_print_loop
+
+// ------------------------------------------
+// prints the last byte of arg[0]
+print_byte: func
+ ldw bpr, rg0, 8
+ call _serial_send
+ jmp _serial_end
+
+// ------------------------------------------
+_serial_end:
+ return
diff --git a/dsa_resources/serialtest.dsa b/dsa_resources/serialtest.dsa
new file mode 100644
index 0000000..83a5036
--- /dev/null
+++ b/dsa_resources/serialtest.dsa
@@ -0,0 +1,14 @@
+include serial: "./serial.dsa"
+
+dw stack: 0x1000
+_init:
+ ldw stack, bpr
+ mov bpr, spr
+
+db hello: "Serial works lol. this took ages and some clanker forgetting to dereference a pointer."
+main:
+ lwi hello, rg0
+ push rg0
+ call serial::print
+ pop zero
+ hlt