new ui, control panel kinda works, display works, framebuffer works, serial write works. widget for registers works. next is serial input and the editor

This commit is contained in:
2026-03-13 04:30:32 +00:00
parent 6da473c272
commit e1705efe7c
46 changed files with 1470 additions and 1613 deletions
+10
View File
@@ -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/
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/dsa/assembler_old/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/dsa/common/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/dsa/compiler/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/dsa/emulator/benches" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/dsa/emulator/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/dsa/linker/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/dsa2.iml" filepath="$PROJECT_DIR$/.idea/dsa2.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+25 -21
View File
@@ -23,38 +23,46 @@ pub enum Register {
Rgf = 15, Rgf = 15,
// special purpose // special purpose
Zero = 16, Acc = 16,
Acc = 17, Spr = 17,
Spr = 18, Bpr = 18,
Bpr = 19, Ret = 19,
Ret = 20, Idr = 20,
Idr = 21, Mmr = 21,
Mmr = 22,
// system - read only // system - read only
Mar = 23, Zero = 22,
Mdr = 24, Sts = 23,
Sts = 25, Cir = 24,
Cir = 26, Pcx = 25,
Pcx = 27,
#[default] #[default]
Null = 28, Null = 26,
} }
impl Register { 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] #[must_use]
#[inline] #[inline]
pub unsafe fn from_u8_unchecked(idx: u8) -> Self { 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) } unsafe { std::mem::transmute(idx) }
} }
#[inline] #[inline]
pub fn from_u8(idx: u8) -> Result<Self, ()> { pub fn from_u8(idx: u8) -> Result<Self, ()> {
if idx > 28 { if idx >= Self::COUNT {
return Err(()); return Err(());
} }
Ok(unsafe { Self::from_u8_unchecked(idx) }) Ok(unsafe { Self::from_u8_unchecked(idx) })
} }
} }
@@ -84,7 +92,6 @@ impl fmt::Display for Register {
Self::Rgf => "Rgf", Self::Rgf => "Rgf",
// special purpose // special purpose
Self::Zero => "Zero",
Self::Acc => "Acc", Self::Acc => "Acc",
Self::Spr => "Spr", Self::Spr => "Spr",
Self::Bpr => "Bpr", Self::Bpr => "Bpr",
@@ -93,8 +100,7 @@ impl fmt::Display for Register {
Self::Mmr => "Mmr", Self::Mmr => "Mmr",
// system - read only // system - read only
Self::Mar => "Mar", Self::Zero => "Zero",
Self::Mdr => "Mdr",
Self::Sts => "Sts", Self::Sts => "Sts",
Self::Cir => "Cir", Self::Cir => "Cir",
Self::Pcx => "Pcx", Self::Pcx => "Pcx",
@@ -129,7 +135,6 @@ impl std::str::FromStr for Register {
"rgf" => Ok(Self::Rgf), "rgf" => Ok(Self::Rgf),
// special purpose // special purpose
"zero" => Ok(Self::Zero),
"acc" => Ok(Self::Acc), "acc" => Ok(Self::Acc),
"spr" => Ok(Self::Spr), "spr" => Ok(Self::Spr),
"bpr" => Ok(Self::Bpr), "bpr" => Ok(Self::Bpr),
@@ -138,8 +143,7 @@ impl std::str::FromStr for Register {
"mmr" => Ok(Self::Mmr), "mmr" => Ok(Self::Mmr),
// system - read only // system - read only
"mar" => Ok(Self::Mar), "zero" => Ok(Self::Zero),
"mdr" => Ok(Self::Mdr),
"sts" => Ok(Self::Sts), "sts" => Ok(Self::Sts),
"cir" => Ok(Self::Cir), "cir" => Ok(Self::Cir),
"pcx" => Ok(Self::Pcx), "pcx" => Ok(Self::Pcx),
+7 -7
View File
@@ -1,7 +1,8 @@
[package] [package]
name = "emulator" name = "emulator"
version = "0.1.0" version = "0.3.0"
edition = "2024" edition.workspace = true
authors.workspace = true
[[bin]] [[bin]]
name = "dsa" name = "dsa"
@@ -22,15 +23,14 @@ ron = "0.12.0"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
common = { path = "../common" } common = { path = "../common" }
egui = "0.33.3"
eframe = "0.33.3"
egui_tiles = "0.14.1"
#assembler = { path = "../assembler" } #assembler = { path = "../assembler" }
[[bench]]
name = "bench_mainstore"
harness = false
[features] [features]
default = ["mainstore-bulkalloc"] default = ["mainstore-prealloc"]
# Memory Bank Features # Memory Bank Features
mainstore-bulkalloc = [] # Fastest for Writes mainstore-bulkalloc = [] # Fastest for Writes
-208
View File
@@ -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<PhysAddr> {
(0..n).map(|i| ((i as u32 * 4) & (max_addr - 1))).collect()
}
fn random_addrs(n: usize, max_addr: u32) -> Vec<PhysAddr> {
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<PhysAddr> {
(0..n).map(|i| (i as u32 * 4096) & 0x00FF_FFFF).collect()
}
fn random_page_addrs(n: usize, max_addr: u32) -> Vec<PhysAddr> {
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<WallTime>,
addrs: &[PhysAddr],
implementations: &mut [(&str, Box<dyn RandomAccessMemory>)],
) {
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<WallTime>,
addrs: &[PhysAddr],
implementations: &mut [(&str, Box<dyn RandomAccessMemory>)],
) {
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<WallTime>,
addrs: &[PhysAddr],
implementations: &mut [(&str, Box<dyn RandomAccessMemory>)],
) {
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<WallTime>,
addrs: &[PhysAddr],
implementations: &mut [(&str, Box<dyn RandomAccessMemory>)],
) {
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<WallTime>,
addrs: &[PhysAddr],
implementations: &mut [(&str, Box<dyn RandomAccessMemory>)],
) {
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<WallTime>,
addrs: &[PhysAddr],
implementations: &mut [(&str, Box<dyn RandomAccessMemory>)],
) {
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<dyn RandomAccessMemory>)> = 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);
+2 -21
View File
@@ -1,9 +1,8 @@
use std::{fs, path::PathBuf}; use std::{fs, path::PathBuf};
use clap::{Parser, ValueEnum}; use clap::Parser;
use serde::{Deserialize, Serialize};
use crate::{MemoryMap, RandomAccessMemory, config::IoMapping, io::DeviceId}; use crate::config::{IoMapping, MemoryMap};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
@@ -18,9 +17,6 @@ pub struct DsaArgs {
#[arg(long = "iomap")] #[arg(long = "iomap")]
io_map: Option<PathBuf>, io_map: Option<PathBuf>,
#[arg(value_enum, long = "mem", default_value = "bulk-alloc")]
pub memory_bank: MemoryBank,
} }
impl DsaArgs { 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,
}
@@ -1,4 +1,4 @@
use crate::memory::{FaultInfo, PhysAddr, VirtAddr, tlb::TLB}; use crate::backend::memory::{FaultInfo, PhysAddr, VirtAddr, tlb::TLB};
pub struct MMU { pub struct MMU {
buffer: TLB, buffer: TLB,
@@ -1,7 +1,3 @@
use serde::{Deserialize, Serialize};
use crate::{RandomAccessMemory, memory::mmu::MMU, processor::processor::Emulator};
mod cache; mod cache;
pub mod mmu; pub mod mmu;
pub mod ram; pub mod ram;
+168
View File
@@ -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<Item = Page> + '_ {
data.chunks(4096).map(|chunk| {
let mut arr = [0u8; 4096];
arr[..chunk.len()].copy_from_slice(chunk);
Page(arr)
})
}
}
impl Deref for Page {
type Target = [u8];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Page {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl TryFrom<Vec<u8>> for Page {
type Error = String;
fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
let arr: [u8; 4096] = data
.try_into()
.map_err(|v: Vec<u8>| format!("Expected 4096 bytes, got {}", v.len()))?;
Ok(Page(arr))
}
}
#[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 }
}
}
@@ -1,4 +1,4 @@
use crate::memory::{PhysAddr, VirtAddr}; use crate::backend::memory::{PhysAddr, VirtAddr};
const TLB_SIZE: usize = 256; const TLB_SIZE: usize = 256;
const TLB_MASK: u32 = (TLB_SIZE - 1) as u32; const TLB_MASK: u32 = (TLB_SIZE - 1) as u32;
+2
View File
@@ -0,0 +1,2 @@
pub mod memory;
pub mod processor;
@@ -16,14 +16,12 @@ use common::{
}; };
use crate::{ use crate::{
Page, MemoryBank, Page, SharedState,
backend::{memory::mmu::MMU, processor::interrupts::Interrupt},
config::{IoMapping, MemoryMap, RegionType}, config::{IoMapping, MemoryMap, RegionType},
io::{IoAccess, IoDevice, MappedDevice},
memory::{mmu::MMU, ram::RandomAccessMemory},
processor::{interrupts::Interrupt, state::SharedState},
}; };
pub struct Emulator<Mem: RandomAccessMemory> { pub struct Emulator {
internal_state: ProcessorSnapshot, internal_state: ProcessorSnapshot,
shared_state: Arc<SharedState>, shared_state: Arc<SharedState>,
@@ -33,10 +31,9 @@ pub struct Emulator<Mem: RandomAccessMemory> {
// memory // memory
mmu: MMU, mmu: MMU,
mainstore: Mem, mainstore: MemoryBank,
// IO // IO
mmio: Vec<MappedDevice>,
mmio_region: Option<(u32, u32)>, // start end end of segment mmio_region: Option<(u32, u32)>, // start end end of segment
// optionals - not necessarily set on boot. // optionals - not necessarily set on boot.
@@ -44,12 +41,13 @@ pub struct Emulator<Mem: RandomAccessMemory> {
// Config params // Config params
__mmap_configured: bool, __mmap_configured: bool,
__io_configured: bool,
time: Instant,
} }
unsafe impl<Mem: RandomAccessMemory> Send for Emulator<Mem> {} unsafe impl Send for Emulator {}
impl<Mem: RandomAccessMemory> Emulator<Mem> { impl Emulator {
pub fn new(mem: Mem) -> Self { pub fn new() -> Self {
let (sender, receiver) = mpsc::channel::<u8>(); let (sender, receiver) = mpsc::channel::<u8>();
Self { Self {
@@ -61,24 +59,16 @@ impl<Mem: RandomAccessMemory> Emulator<Mem> {
memory_map: None, memory_map: None,
mmu: MMU::new(), mmu: MMU::new(),
mainstore: mem, mainstore: MemoryBank::new(),
// mmio // mmio
mmio: Vec::new(),
mmio_region: None, mmio_region: None,
// Config params // Config params
__io_configured: false,
__mmap_configured: false, __mmap_configured: false,
}
}
pub fn with_device(mut self, device: impl IoDevice + 'static) -> Self { time: Instant::now(),
self.mmio.push(MappedDevice { }
base: 0,
device: Box::new(device),
});
self
} }
pub fn state_handle(&self) -> Arc<SharedState> { pub fn state_handle(&self) -> Arc<SharedState> {
@@ -91,25 +81,21 @@ impl<Mem: RandomAccessMemory> Emulator<Mem> {
} }
#[inline] #[inline]
pub fn memory_mut(&mut self) -> &mut impl RandomAccessMemory { pub fn memory_mut(&mut self) -> &mut MemoryBank {
&mut self.mainstore &mut self.mainstore
} }
#[must_use] #[must_use]
#[inline] #[inline]
pub fn reg(&self, reg: Register) -> u32 { pub fn reg(&self, reg: Register) -> u32 {
if reg as u8 == Register::Zero as u8 { debug_assert!((reg as u8) < Register::COUNT);
return 0;
}
debug_assert!((reg as usize) < ProcessorSnapshot::REG_COUNT);
unsafe { *self.internal_state.registers.get_unchecked(reg as usize) } unsafe { *self.internal_state.registers.get_unchecked(reg as usize) }
} }
#[inline] #[inline]
pub fn mut_reg(&mut self, reg: Register) -> &mut u32 { 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 { unsafe {
self.internal_state self.internal_state
.registers .registers
@@ -131,23 +117,6 @@ impl<Mem: RandomAccessMemory> Emulator<Mem> {
self self
} }
pub fn apply_io_map(mut self, map: Vec<IoMapping>) -> Result<Self, String> {
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] #[cold]
fn update(&mut self) { fn update(&mut self) {
self.shared_state self.shared_state
@@ -157,7 +126,7 @@ impl<Mem: RandomAccessMemory> Emulator<Mem> {
#[cold] #[cold]
fn boot(&mut self) -> Result<(), String> { fn boot(&mut self) -> Result<(), String> {
if !(self.__io_configured && self.__mmap_configured) { if !(self.__mmap_configured) {
return Err("Emulator<Mem> not configured".to_string()); return Err("Emulator<Mem> not configured".to_string());
} }
@@ -165,48 +134,89 @@ impl<Mem: RandomAccessMemory> Emulator<Mem> {
MemoryMap::identity_map(&mut self.mmu, map.regions.get(0).unwrap()); MemoryMap::identity_map(&mut self.mmu, map.regions.get(0).unwrap());
} }
self.internal_state.running = true; self.internal_state.halted = false;
Ok(()) Ok(())
} }
#[cold] #[cold]
fn shutdown(&mut self) { fn shutdown(&mut self) {
self.internal_state.running = false; self.internal_state.halted = true;
} }
#[cold] pub fn halt(&mut self) {
pub fn idle_wait(&mut self) { self.internal_state.halted = true;
self.internal_state.running = false;
// Wait for an interrupt or state update to continue. // wait until we're un-halted.
loop { while self.internal_state.halted {
// Check for interrupts. self.wait_for_instructions();
if let Ok(code) = self.interrupts.recv_timeout(Duration::from_millis(100)) { }
self.interrupt(Interrupt::Software(code));
break;
} }
// // If we've received a request to continue running. DEPRECATED (we can run through interrupts) pub fn check_update(&mut self) {
// if self.shared_state.running.load(Ordering::Relaxed) {
// panic!("3");
// break;
// }
// UI requested a state update. // UI requested a state update.
if self.shared_state.update_req.load(Ordering::Relaxed) { if self.shared_state.update_req.load(Ordering::Relaxed) {
self.update(); 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));
}
}
} }
self.internal_state.running = true; #[cold]
pub fn wait_for_instructions(&mut self) {
{
// record time for previous execution.
self.internal_state.time = self.time.elapsed();
self.update();
self.time = Instant::now();
}
// 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;
}
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;
}
// UI is resetting the emulator
self.check_update();
}
} }
pub fn run(&mut self) -> Result<(), String> { 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 { '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 // IMPORTANT
// do not change anything about this loop. it's fully optimised afaik. // do not change anything about this loop. it's fully optimised afaik.
// Check for commands or hardware Interrupts every 32k cycles // Check for commands or hardware Interrupts every 32k cycles
@@ -218,8 +228,8 @@ impl<Mem: RandomAccessMemory> Emulator<Mem> {
} }
} }
if unlikely(!self.shared_state.running.load(Ordering::Relaxed)) { if unlikely(self.shared_state.paused.load(Ordering::Relaxed)) {
self.idle_wait(); self.wait_for_instructions();
} }
match self.interrupts.try_recv() { match self.interrupts.try_recv() {
@@ -229,24 +239,6 @@ impl<Mem: RandomAccessMemory> Emulator<Mem> {
} }
} }
// 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 pc = self.reg(Register::Pcx);
let instruction = self.mem_read_word(pc); let instruction = self.mem_read_word(pc);
@@ -261,23 +253,21 @@ impl<Mem: RandomAccessMemory> Emulator<Mem> {
thread::sleep(Duration::from_micros(10)); thread::sleep(Duration::from_micros(10));
} }
self.mut_reg(Register::Pcx).add_assign(4); // Check if the previous instruction caused a fault.
self.execute(Instruction(instruction));
// Check if executing the interrupt caused a fault.
if let Some(fault) = self.pending_fault { if let Some(fault) = self.pending_fault {
self.pending_fault = None;
println!("WARN fault: {:?}", fault); println!("WARN fault: {:?}", fault);
self.pending_fault = None;
self.interrupt(fault); self.interrupt(fault);
} }
self.mut_reg(Register::Pcx).add_assign(4);
self.execute(Instruction(instruction));
// always increment clock. // always increment clock.
self.internal_state.clock += 1; self.internal_state.clock += 1;
} }
self.shutdown(); self.shutdown();
Ok(()) Ok(())
} }
@@ -489,7 +479,7 @@ impl<Mem: RandomAccessMemory> Emulator<Mem> {
Some(Opcode::Int) => { Some(Opcode::Int) => {
self.interrupt(Interrupt::Software(word.imm16() as u8)); 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!(), Some(Opcode::IRet) => todo!(),
None => {} None => {}
} }
@@ -510,147 +500,57 @@ impl<Mem: RandomAccessMemory> Emulator<Mem> {
#[inline] #[inline]
fn mem_read_byte(&mut self, addr: u32) -> u8 { 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) self.mainstore.read_byte(addr)
} }
#[inline] #[inline]
fn mem_write_byte(&mut self, addr: u32, val: u8) { fn mem_write_byte(&mut self, addr: u32, val: u8) {
if unlikely(self.is_mmio(addr)) { if addr == 0x40000 {
self.io_write_byte(addr, val); eprintln!("emulator wrote 0x{:08x} to uart", val);
return;
} }
self.mainstore.write_byte(addr, val); self.mainstore.write_byte(addr, val);
} }
#[inline] #[inline]
fn mem_read_word(&mut self, addr: u32) -> u32 { 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) self.mainstore.read_word(addr)
} }
#[inline] #[inline]
fn mem_write_word(&mut self, addr: u32, val: u32) { 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); self.mainstore.write_word(addr, val);
} }
#[inline] #[inline]
fn mem_read_page(&mut self, addr: u32) -> &Page { 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) self.mainstore.read_page(addr)
} }
#[inline] #[inline]
fn mem_write_page(&mut self, addr: u32, val: &Page) { 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); 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) { fn fault(&mut self, interrupt: Interrupt) {
self.pending_fault = Some(interrupt); self.pending_fault = Some(interrupt);
} }
#[inline]
fn get_device(&mut self, addr: u32) -> Option<(&mut Box<dyn IoDevice>, 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)] #[derive(Debug, Clone)]
pub struct ProcessorSnapshot { pub struct ProcessorSnapshot {
pub running: bool, pub halted: bool,
pub clock: usize, pub clock: usize,
pub registers: [u32; Self::REG_COUNT], pub time: Duration,
pub registers: [u32; Register::COUNT as usize],
} }
impl Default for ProcessorSnapshot { impl Default for ProcessorSnapshot {
fn default() -> Self { fn default() -> Self {
Self { Self {
running: false, halted: false,
clock: 0, clock: 0,
registers: [0; Self::REG_COUNT], time: Duration::ZERO,
registers: [0; Register::COUNT as usize],
} }
} }
} }
impl ProcessorSnapshot {
const REG_COUNT: usize = 28;
}
@@ -1,24 +1,27 @@
use std::sync::{Arc, atomic::AtomicBool, mpsc}; use std::sync::{Arc, atomic::AtomicBool, mpsc};
use crate::processor::processor::ProcessorSnapshot;
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
use crate::backend::processor::processor::ProcessorSnapshot;
pub struct SharedState { pub struct SharedState {
pub proc: ArcSwap<ProcessorSnapshot>, pub proc: ArcSwap<ProcessorSnapshot>,
pub running: AtomicBool, pub reseting: AtomicBool,
pub paused: AtomicBool,
pub update_req: AtomicBool, pub update_req: AtomicBool,
sender: mpsc::Sender<u8>, interrupt_queue: mpsc::Sender<u8>,
} }
impl SharedState { impl SharedState {
pub fn new(sender: mpsc::Sender<u8>) -> Self { pub fn new(sender: mpsc::Sender<u8>) -> Self {
Self { Self {
reseting: AtomicBool::new(false),
proc: ArcSwap::new(Arc::new(ProcessorSnapshot::default())), proc: ArcSwap::new(Arc::new(ProcessorSnapshot::default())),
running: AtomicBool::new(true), paused: AtomicBool::new(true),
sender: sender, interrupt_queue: sender,
update_req: AtomicBool::new(false), update_req: AtomicBool::new(false),
} }
} }
-330
View File
@@ -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<BulkAllocStore> {
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, <high>
// 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<u8> {
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<u8> {
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<u8> {
// 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<u32> = 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<u8> {
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<u8>) -> BenchResult {
let mut emu = make_emulator();
let handle = emu.state_handle();
let pages: Vec<Page> = 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<u8>)] = &[
("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::<f64>() / results.len() as f64;
println!("{}", "-".repeat(72));
println!("{:<35} {:>35.1} MIPS avg", "Overall", avg_mips);
}
+7 -5
View File
@@ -1,11 +1,13 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
Emulator, RandomAccessMemory, Emulator,
io::DeviceId, backend::memory::{PhysAddr, mmu::MMU},
memory::{PhysAddr, mmu::MMU}, // io::DeviceId,
}; };
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[repr(u32)] #[repr(u32)]
#[derive(Serialize, Deserialize, Clone, Copy, Debug)] #[derive(Serialize, Deserialize, Clone, Copy, Debug)]
pub enum RegionType { pub enum RegionType {
@@ -36,7 +38,7 @@ pub struct MemoryMap {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct IoMapping { pub struct IoMapping {
pub device: DeviceId, // pub device: DeviceId,
pub base: u32, pub base: u32,
pub size: u32, pub size: u32,
} }
@@ -51,7 +53,7 @@ impl MemoryMap {
} }
} }
pub fn apply(&self, cpu: &mut Emulator<impl RandomAccessMemory>) { pub fn apply(&self, cpu: &mut Emulator) {
for reg in &self.regions { for reg in &self.regions {
if reg.identity_map_on_boot { if reg.identity_map_on_boot {
Self::identity_map(cpu.mmu_mut(), reg); Self::identity_map(cpu.mmu_mut(), reg);
+15
View File
@@ -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);
}
@@ -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<SharedState>,
mem: MemoryBank,
visible: bool,
}
impl Controller {
pub fn new(state: Arc<SharedState>, 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));
});
}
}
@@ -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,
);
}
}
}
@@ -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<u32>, // RGBA pixels
visible: bool,
texture: Option<egui::TextureHandle>,
}
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<u32> {
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::<Vec<u8>>()
.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<u8> {
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<egui::Color32> = 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));
}
}
@@ -0,0 +1,131 @@
use std::{f32, sync::Arc};
use common::prelude::Register;
use crate::SharedState;
use super::Component;
pub struct Registers {
state: Arc<SharedState>,
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<SharedState>) -> 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");
});
}
}
@@ -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());
}
}
+149
View File
@@ -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<SharedState>, 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<SharedState>,
mem_handle: MemoryBank,
components: Vec<Box<dyn Component>>,
}
impl DsaUi {
pub fn new(
cc: &eframe::CreationContext,
state: Arc<SharedState>,
mem_handle: MemoryBank,
) -> Self {
// components
let mut components: Vec<Box<dyn Component>> = 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);
}
}
+54 -72
View File
@@ -1,80 +1,62 @@
use std::sync::{ // use super::{IoAccess, IoHandle};
Arc, use crate::{MemoryBank, Page, backend::memory::PhysAddr};
atomic::{AtomicBool, AtomicU8, Ordering},
};
use crate::{ pub struct Display {
io::{IoAccess, IoDevice}, width: usize,
processor::interrupts::Interrupt, height: usize,
}; addr: PhysAddr,
pub struct DisplayDevice { mem: MemoryBank,
buffer: Arc<Box<[AtomicU8]>>, buffer: Vec<u8>,
dirty: Arc<AtomicBool>, visible: bool,
} }
impl DisplayDevice { impl Display {
pub fn new(width: usize, height: usize) -> (Self, DisplayHandle) { pub fn new(width: u32, height: u32, addr: PhysAddr, mem: MemoryBank) -> Self {
let buffer = Arc::new( Self {
(0..width * height) width: width as usize,
.map(|_| AtomicU8::new(0)) height: height as usize,
.collect::<Vec<_>>() addr,
.into_boxed_slice(), mem,
); buffer: Vec::new(),
let dirty = Arc::new(AtomicBool::new(false)); visible: true,
}
}
let handle = DisplayHandle { pub fn visible(&mut self) -> &mut bool {
buffer: Arc::clone(&buffer), &mut self.visible
dirty: Arc::clone(&dirty), }
};
(Self { buffer, dirty }, handle) pub fn width(&self) -> usize {
} self.width
} }
impl IoDevice for DisplayDevice { pub fn height(&self) -> usize {
fn size(&self) -> u32 { self.height
(self.buffer.len()) as u32 }
}
fn access(&self) -> IoAccess { fn internal_read(&self) -> Vec<u8> {
IoAccess::WRITE 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);
fn id(&self) -> super::DeviceId { page.as_slice().to_owned()
super::DeviceId::Display })
} .flatten()
.take((self.width * self.height) as usize)
fn write_word(&mut self, offset: u32, val: u32) -> Result<(), Interrupt> { .collect::<Vec<u8>>();
let bytes = val.to_le_bytes(); pages
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); pub fn changed(&mut self) -> bool {
self.buffer[offset as usize + 3].store(bytes[3], Ordering::Relaxed); let temp = self.internal_read();
self.dirty.store(true, Ordering::Relaxed); if temp != self.buffer {
Ok(()) self.buffer = temp;
} return true;
}
fn write_byte(&mut self, offset: u32, val: u8) -> Result<(), Interrupt> { false
self.buffer[offset as usize].store(val, Ordering::Relaxed); }
self.dirty.store(true, Ordering::Relaxed);
Ok(()) pub fn read(&mut self) -> Vec<u8> {
} self.internal_read()
}
pub struct DisplayHandle {
pub buffer: Arc<Box<[AtomicU8]>>,
pub dirty: Arc<AtomicBool>,
}
impl DisplayHandle {
pub fn is_dirty(&self) -> bool {
self.dirty.swap(false, Ordering::Relaxed)
}
pub fn read_all(&self) -> Vec<u8> {
self.buffer
.iter()
.map(|b| b.load(Ordering::Relaxed))
.collect()
} }
} }
+35 -113
View File
@@ -1,124 +1,46 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{Emulator, RandomAccessMemory, processor::interrupts::Interrupt};
pub mod display; 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 trait IoHandle: Send {
pub base: u32, // /// Return the size (in bytes) of the device's addressable region.
pub device: Box<dyn IoDevice>, // fn size(&self) -> usize;
}
#[derive(Clone, Copy, Debug, PartialEq)] // /// Return the access permissions for the device.
pub struct IoAccess(u8); // fn access(&self) -> IoAccess;
impl IoAccess { // /// Return the DeviceId
pub const READ: Self = Self(0b01); // fn id(&self) -> DeviceId;
pub const WRITE: Self = Self(0b10); // }
/// Convenience alias for Read + Write. // #[derive(Clone, Copy, Debug, PartialEq)]
pub const READWRITE: Self = Self(Self::READ.0 | Self::WRITE.0); // pub struct IoAccess(u8);
pub const NONE: Self = Self(0);
/// Bitwise OR returns a new `IoAccess` that contains all flags from both operands. // impl IoAccess {
#[inline] // pub const READ: Self = Self(0b01);
pub fn or(self, other: Self) -> Self { // pub const WRITE: Self = Self(0b10);
Self(self.0 | other.0)
}
/// Check if a flag is present. // /// Convenience alias for Read + Write.
#[inline] // pub const READWRITE: Self = Self(Self::READ.0 | Self::WRITE.0);
pub fn contains(&self, flag: Self) -> bool { // pub const NONE: Self = Self(0);
self.0 & flag.0 != 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] // /// Bitwise OR returns a new `IoAccess` that contains all flags from both operands.
pub enum DeviceId { // #[inline]
Display, // pub fn or(self, other: Self) -> Self {
Serial, // Self(self.0 | other.0)
Random, // }
Timer,
}
/// Trait representing an input/output device that can be mapped into a memory space. // /// Check if a flag is present.
/// // #[inline]
/// - Default methods return dummy data. // pub fn contains(&self, flag: Self) -> bool {
pub trait IoDevice: Send { // self.0 & flag.0 != 0
/// 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, writeonly 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<u8, Interrupt> {
if self.access().contains(IoAccess::READ) {
Ok(0)
} else {
Err(Interrupt::ReadFromWriteOnly)
}
}
#[inline]
/// Write a single byte to the specified address.
///
/// By default, readonly 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 32bit word from the specified address in littleendian order.
///
/// The default implementation composes four consecutive byte reads using
/// `read_byte`. If any byte read fails, the whole operation returns `None`.
/// Writeonly devices return `None` immediately. Override this method for
/// more efficient word reads.
#[inline]
fn read_word(&self, offset: u32) -> Result<u32, Interrupt> {
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 32bit word to the specified address in littleendian order.
///
/// The default implementation splits the value into bytes and writes each
/// using `write_byte`. Readonly 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)
}
}
+48
View File
@@ -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<u8>,
pub buffer: Vec<u8>,
pub visible: bool,
}
impl Serial {
fn uart_io_thread(mem: MemoryBank, uart_base: PhysAddr, tx: Sender<u8>) {
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<SharedState>, 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);
}
}
}
+4 -5
View File
@@ -2,11 +2,10 @@
#![feature(inherent_associated_types)] #![feature(inherent_associated_types)]
pub mod args; pub mod args;
pub mod backend;
pub mod config; pub mod config;
pub mod frontend;
pub mod io; pub mod io;
mod memory;
mod processor;
pub use {config::MemoryMap, processor::processor::Emulator, processor::state::SharedState}; pub use backend::memory::ram::*;
pub use backend::{processor::processor::Emulator, processor::state::SharedState};
pub use memory::ram::*;
+11 -56
View File
@@ -1,32 +1,18 @@
use clap::Parser; use clap::Parser;
use common::{instructions::Instruction, register::Register};
use dsa::{ use common::prelude::Instruction;
BulkAllocStore, Emulator, Page, RandomAccessMemory, SharedState, use dsa::{Emulator, Page, args::DsaArgs, frontend::run_app};
args::DsaArgs, use std::thread;
io::display::{DisplayDevice, DisplayHandle},
}; const STACK_SIZE: usize = 1024 * 1024 * 16;
use std::{
os::unix::thread::JoinHandleExt,
process::exit,
sync::{Arc, atomic::Ordering},
thread,
time::Duration,
};
fn main() { fn main() {
let args = DsaArgs::parse(); let args = DsaArgs::parse();
let mmap = args.get_memory_map(); let mut emulator = Emulator::new().apply_memory_map(args.get_memory_map());
let iomap = args.get_io_map();
let store = BulkAllocStore::new(); let mem_handle = emulator.memory_mut().clone();
let (display, handle) = DisplayDevice::new(80, 25); let state = emulator.state_handle();
let mut emulator = Emulator::new(store)
.with_device(display)
.apply_memory_map(mmap)
.apply_io_map(iomap)
.unwrap();
if let Some(bin) = args.get_binary() { if let Some(bin) = args.get_binary() {
for (i, ch) in bin.chunks(4).enumerate() { for (i, ch) in bin.chunks(4).enumerate() {
@@ -56,41 +42,10 @@ fn main() {
} }
} }
let state = emulator.state_handle(); let _runner = thread::Builder::new()
const STACK_SIZE: usize = 1024 * 1024 * 16;
let runner = thread::Builder::new()
.stack_size(STACK_SIZE) .stack_size(STACK_SIZE)
.spawn(move || emulator.run().unwrap()) .spawn(move || emulator.run().unwrap())
.unwrap(); .unwrap();
let observer = thread::spawn(|| observe(state, handle)); run_app(state, mem_handle).unwrap()
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<SharedState>, 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!();
}
}
}
} }
-107
View File
@@ -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);
}
}
}
-134
View File
@@ -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);
}
}
}
-66
View File
@@ -1,66 +0,0 @@
use fxhash::FxHashMap;
use crate::memory::{PhysAddr, RandomAccessMemory, ram::Page};
pub struct HashStore {
pages: FxHashMap<PhysAddr, Page>,
}
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;
}
}
-108
View File
@@ -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<Item = Page> + '_ {
data.chunks(4096).map(|chunk| {
let mut arr = [0u8; 4096];
arr[..chunk.len()].copy_from_slice(chunk);
Page(arr)
})
}
}
impl Deref for Page {
type Target = [u8];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Page {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl TryFrom<Vec<u8>> for Page {
type Error = String;
fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
let arr: [u8; 4096] = data
.try_into()
.map_err(|v: Vec<u8>| format!("Expected 4096 bytes, got {}", v.len()))?;
Ok(Page(arr))
}
}
#[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);
}
-50
View File
@@ -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);
}
}
-103
View File
@@ -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) }
}
}
+133
View File
@@ -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
+47 -2
View File
@@ -3,7 +3,7 @@
include print "./print.dsa" include print "./print.dsa"
// "print hello world" // "prints first 16 fibonacci numbers"
db program: "++++++++++++++++++++++++++++++++++++++++++++ db program: "++++++++++++++++++++++++++++++++++++++++++++
>++++++++++++++++++++++++++++++++ >++++++++++++++++++++++++++++++++
>++++++++++++++++ >++++++++++++++++
@@ -35,6 +35,7 @@ db program: "++++++++++++++++++++++++++++++++++++++++++++
<<++..." <<++..."
db error: "Invalid Instruction!" db error: "Invalid Instruction!"
dh counter: 0xFF
dw stack: 0x10000 dw stack: 0x10000
dw input: 0x30000 dw input: 0x30000
resb data: 1024 resb data: 1024
@@ -44,11 +45,15 @@ _init_stack:
ldw stack, bpr ldw stack, bpr
mov bpr, spr mov bpr, spr
init:
ldw counter, rg7
start: start:
call bf_reset
// load the start of the program into rg0 // load the start of the program into rg0
lwi program, rg0 lwi program, rg0
lwi data, rg1 lwi data, rg1
// rg0 is our instruction pointer // rg0 is our instruction pointer
// rg1 is our data pointer // rg1 is our data pointer
// rg2 is the value at the data pointer // rg2 is the value at the data pointer
@@ -98,7 +103,16 @@ loop_start:
pop rg1 pop rg1
pop rg0 pop rg0
db success: "Success!"
end: end:
call print::newline
lwi success, rg0
push rg0
call print::print
pop zero
subi rg7, 1
jnz rg7, start
hlt hlt
loop_end: loop_end:
@@ -225,3 +239,34 @@ close_left:
// push zero to the stack // push zero to the stack
push zero push zero
jmp _traverse_left 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
+65
View File
@@ -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
Binary file not shown.
+25 -2
View File
@@ -185,8 +185,8 @@ print_whitespace: func
addi rg1, 1 addi rg1, 1
jmp _end jmp _end
// // ------------------------------------------ // ------------------------------------------
// // print newline // print newline
// print_newline: // print_newline:
// push bpr // push bpr
// mov spr, bpr // mov spr, bpr
@@ -214,6 +214,29 @@ print_whitespace: func
// jmp _end // 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. // prints arg[0] as a decimal number to the screen.
// print_num: // print_num:
+63
View File
@@ -0,0 +1,63 @@
// lib:
// serial.dsa
//
// usage:
// include serial "<relative path>"
//
// 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
+14
View File
@@ -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