Merge remote-tracking branch 'refs/remotes/origin/main'

This commit is contained in:
2026-03-13 16:41:28 +00:00
42 changed files with 1448 additions and 1266 deletions
+2 -21
View File
@@ -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<PathBuf>,
#[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,
}
@@ -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,
@@ -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;
+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_MASK: u32 = (TLB_SIZE - 1) as u32;
+2
View File
@@ -0,0 +1,2 @@
pub mod memory;
pub mod processor;
@@ -13,14 +13,20 @@ use std::{
use common::isa::instructions::{Instruction, Opcode};
use common::isa::register::Register;
use crate::{
<<<<<<< HEAD:dsa/emulator/src/processor/processor.rs
config::{IoMapping, MemoryMap, RegionType},
io::{IoDevice, MappedDevice},
memory::{mmu::MMU, ram::RandomAccessMemory},
processor::{interrupts::Interrupt, state::SharedState},
Page,
=======
MemoryBank, Page, SharedState,
backend::{memory::mmu::MMU, processor::interrupts::Interrupt},
config::{IoMapping, MemoryMap, RegionType},
>>>>>>> refs/remotes/origin/main:dsa/emulator/src/backend/processor/processor.rs
};
pub struct Emulator<Mem: RandomAccessMemory> {
pub struct Emulator {
internal_state: ProcessorSnapshot,
shared_state: Arc<SharedState>,
@@ -30,10 +36,9 @@ pub struct Emulator<Mem: RandomAccessMemory> {
// memory
mmu: MMU,
mainstore: Mem,
mainstore: MemoryBank,
// IO
mmio: Vec<MappedDevice>,
mmio_region: Option<(u32, u32)>, // start end end of segment
// optionals - not necessarily set on boot.
@@ -41,12 +46,13 @@ pub struct Emulator<Mem: RandomAccessMemory> {
// Config params
__mmap_configured: bool,
__io_configured: bool,
time: Instant,
}
unsafe impl<Mem: RandomAccessMemory> Send for Emulator<Mem> {}
impl<Mem: RandomAccessMemory> Emulator<Mem> {
pub fn new(mem: Mem) -> Self {
unsafe impl Send for Emulator {}
impl Emulator {
pub fn new() -> Self {
let (sender, receiver) = mpsc::channel::<u8>();
Self {
@@ -58,24 +64,16 @@ impl<Mem: RandomAccessMemory> Emulator<Mem> {
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<SharedState> {
@@ -88,25 +86,21 @@ impl<Mem: RandomAccessMemory> Emulator<Mem> {
}
#[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
@@ -128,23 +122,6 @@ impl<Mem: RandomAccessMemory> Emulator<Mem> {
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]
fn update(&mut self) {
self.shared_state
@@ -154,7 +131,7 @@ impl<Mem: RandomAccessMemory> Emulator<Mem> {
#[cold]
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());
}
@@ -162,48 +139,89 @@ impl<Mem: RandomAccessMemory> Emulator<Mem> {
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
@@ -215,8 +233,8 @@ impl<Mem: RandomAccessMemory> Emulator<Mem> {
}
}
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() {
@@ -226,24 +244,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 instruction = self.mem_read_word(pc);
@@ -258,23 +258,21 @@ impl<Mem: RandomAccessMemory> Emulator<Mem> {
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(())
}
@@ -486,7 +484,7 @@ impl<Mem: RandomAccessMemory> Emulator<Mem> {
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 => {}
}
@@ -507,147 +505,57 @@ impl<Mem: RandomAccessMemory> Emulator<Mem> {
#[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<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)]
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;
}
@@ -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<ProcessorSnapshot>,
pub running: AtomicBool,
pub reseting: AtomicBool,
pub paused: AtomicBool,
pub update_req: AtomicBool,
sender: mpsc::Sender<u8>,
interrupt_queue: mpsc::Sender<u8>,
}
impl SharedState {
pub fn new(sender: mpsc::Sender<u8>) -> 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),
}
}
+7 -5
View File
@@ -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<impl RandomAccessMemory>) {
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);
+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::{
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<Box<[AtomicU8]>>,
dirty: Arc<AtomicBool>,
mem: MemoryBank,
buffer: Vec<u8>,
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::<Vec<_>>()
.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<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()
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
fn internal_read(&self) -> Vec<u8> {
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::<Vec<u8>>();
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<u8> {
self.internal_read()
}
}
+35 -113
View File
@@ -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<dyn IoDevice>,
}
// 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, 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)
}
}
// /// Check if a flag is present.
// #[inline]
// pub fn contains(&self, flag: Self) -> bool {
// self.0 & flag.0 != 0
// }
// }
+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)]
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};
+15 -43
View File
@@ -1,4 +1,5 @@
use clap::Parser;
<<<<<<< HEAD
use common::isa::instructions::Instruction;
use dsa::{
args::DsaArgs, io::display::{DisplayDevice, DisplayHandle}, BulkAllocStore, Emulator, Page,
@@ -10,20 +11,22 @@ use std::{
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;
>>>>>>> refs/remotes/origin/main
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() {
@@ -53,41 +56,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<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!();
}
}
}
run_app(state, mem_handle).unwrap()
}
-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) }
}
}