progress: new assembler works, instruction set fully implemented with call/ret and push/pop, bf.dsa works which means the dsa assembly language works reliably. still a few bugs to fix. might be able to squeeze out a tiny bit more performance
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
[package]
|
||||
name = "emulator"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "dsa"
|
||||
path = "src/main.rs"
|
||||
|
||||
[lib]
|
||||
name = "dsa"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5.0", features = ["html_reports"] }
|
||||
|
||||
[dependencies]
|
||||
arc-swap = "1.8.2"
|
||||
clap = { version = "4.5.60", features = ["derive"] }
|
||||
fxhash = "0.2.1"
|
||||
ron = "0.12.0"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
|
||||
common = { path = "../common" }
|
||||
#assembler = { path = "../assembler" }
|
||||
|
||||
|
||||
[[bench]]
|
||||
name = "bench_mainstore"
|
||||
harness = false
|
||||
|
||||
[features]
|
||||
default = ["mainstore-bulkalloc"]
|
||||
|
||||
# Memory Bank Features
|
||||
mainstore-bulkalloc = [] # Fastest for Writes
|
||||
mainstore-prealloc = [] # Fastest for Reads
|
||||
mainstore-stackarray = [] # Slightly outperforms ArrayMap but requires a large stack
|
||||
mainstore-arraymap = [] # Simple implementation
|
||||
mainstore-hashmap = [] # Old implementation, no raw pointers. Uses a hashmap
|
||||
@@ -0,0 +1,208 @@
|
||||
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);
|
||||
@@ -0,0 +1,73 @@
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use clap::{Parser, ValueEnum};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{MemoryMap, RandomAccessMemory, config::IoMapping, io::DeviceId};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct DsaArgs {
|
||||
/// Memory map to use on boot.
|
||||
/// Overrides default value.
|
||||
#[arg(long = "mmap")]
|
||||
memory_map: Option<PathBuf>,
|
||||
|
||||
#[arg(long = "bin")]
|
||||
binary: Option<PathBuf>,
|
||||
|
||||
#[arg(long = "iomap")]
|
||||
io_map: Option<PathBuf>,
|
||||
|
||||
#[arg(value_enum, long = "mem", default_value = "bulk-alloc")]
|
||||
pub memory_bank: MemoryBank,
|
||||
}
|
||||
|
||||
impl DsaArgs {
|
||||
pub fn get_memory_map(&self) -> MemoryMap {
|
||||
self.memory_map
|
||||
.as_ref()
|
||||
.and_then(|m| {
|
||||
fs::read_to_string(m)
|
||||
.ok()
|
||||
.and_then(|map| ron::from_str(&map).ok())
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
ron::from_str(include_str!("./config/default/dsa.mmap.ron")).unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_binary(&self) -> Option<Vec<u8>> {
|
||||
self.binary
|
||||
.clone()
|
||||
.and_then(|path| Some(fs::read(path).unwrap()))
|
||||
}
|
||||
|
||||
pub fn get_io_map(&self) -> Vec<IoMapping> {
|
||||
self.io_map
|
||||
.as_ref()
|
||||
.and_then(|m| {
|
||||
fs::read_to_string(m)
|
||||
.ok()
|
||||
.and_then(|map| ron::from_str(&map).ok())
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
ron::from_str(include_str!("./config/default/dsa.iomap.ron")).unwrap()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
IoMapping(
|
||||
device: Display,
|
||||
base: 0x0002_0000,
|
||||
size: 0x0000_07D0, // 2000 bytes (80x25)
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,41 @@
|
||||
MemoryMap(
|
||||
table_addr: 0x1000,
|
||||
regions: [
|
||||
// 0000-1000 = reserved
|
||||
// Region(
|
||||
// base: 0x0,
|
||||
// size: 0x1000,
|
||||
// region_type: Reserved
|
||||
// ),
|
||||
// // 1000-4000 = bootloader
|
||||
// Region(
|
||||
// base: 0x1000,
|
||||
// size: 0x3000,
|
||||
// region_type: Bootloader
|
||||
// ),
|
||||
// 5000-0FFFFFFF = MMIO
|
||||
Region(
|
||||
base: 0x20000,
|
||||
size: 0x0FFF_FFFF,
|
||||
region_type: MMIO
|
||||
),
|
||||
// 10000000-BFFFFFFF = userspace
|
||||
Region(
|
||||
base: 0x1000_0000,
|
||||
size: 0xBFFF_FFFF,
|
||||
region_type: Usable
|
||||
),
|
||||
// C0000000-C0FFFFFF = kernel page tables
|
||||
Region(
|
||||
base: 0xC000_0000,
|
||||
size: 0x100_0000,
|
||||
region_type: KernelTables
|
||||
),
|
||||
// C1000000-FFFFFFFF = kernel
|
||||
Region(
|
||||
base: 0xC100_0000,
|
||||
size: 0x3EFF_FFFF,
|
||||
region_type: Kernel
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,80 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
Emulator, RandomAccessMemory,
|
||||
io::DeviceId,
|
||||
memory::{PhysAddr, mmu::MMU},
|
||||
};
|
||||
|
||||
#[repr(u32)]
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||
pub enum RegionType {
|
||||
Reserved,
|
||||
Bootloader,
|
||||
Kernel,
|
||||
KernelTables,
|
||||
Usable,
|
||||
MMIO,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
pub struct Region {
|
||||
pub base: PhysAddr,
|
||||
pub size: u32,
|
||||
pub region_type: RegionType,
|
||||
|
||||
// flags for the emulator
|
||||
#[serde(default)]
|
||||
pub identity_map_on_boot: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct MemoryMap {
|
||||
pub table_addr: PhysAddr,
|
||||
pub regions: Vec<Region>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct IoMapping {
|
||||
pub device: DeviceId,
|
||||
pub base: u32,
|
||||
pub size: u32,
|
||||
}
|
||||
|
||||
impl MemoryMap {
|
||||
pub const DEFAULT_MEMORY_MAP_ADDR: PhysAddr = 0x1000;
|
||||
|
||||
pub fn new() -> Self {
|
||||
MemoryMap {
|
||||
table_addr: Self::DEFAULT_MEMORY_MAP_ADDR,
|
||||
regions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply(&self, cpu: &mut Emulator<impl RandomAccessMemory>) {
|
||||
for reg in &self.regions {
|
||||
if reg.identity_map_on_boot {
|
||||
Self::identity_map(cpu.mmu_mut(), reg);
|
||||
}
|
||||
}
|
||||
|
||||
let mem = cpu.memory_mut();
|
||||
let mut offset = self.table_addr;
|
||||
for region in &self.regions {
|
||||
println!("Wrote entry: {region:?} to page table");
|
||||
mem.write_word(offset, region.base);
|
||||
mem.write_word(offset + 4, region.size);
|
||||
mem.write_word(offset + 8, region.region_type as u32);
|
||||
offset += 12;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn identity_map(mmu: &mut MMU, region: &Region) {
|
||||
let first_page = region.base >> 12;
|
||||
let page_count = region.size >> 12;
|
||||
|
||||
for page in (first_page..first_page + page_count).map(|p| p << 12) {
|
||||
mmu.create_mapping(page, page);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, AtomicU8, Ordering},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
io::{IoAccess, IoDevice},
|
||||
processor::interrupts::Interrupt,
|
||||
};
|
||||
|
||||
pub struct DisplayDevice {
|
||||
buffer: Arc<Box<[AtomicU8]>>,
|
||||
dirty: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
let handle = DisplayHandle {
|
||||
buffer: Arc::clone(&buffer),
|
||||
dirty: Arc::clone(&dirty),
|
||||
};
|
||||
|
||||
(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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Emulator, RandomAccessMemory, processor::interrupts::Interrupt};
|
||||
|
||||
pub mod display;
|
||||
|
||||
pub struct MappedDevice {
|
||||
pub base: u32,
|
||||
pub device: Box<dyn IoDevice>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct IoAccess(u8);
|
||||
|
||||
impl IoAccess {
|
||||
pub const READ: Self = Self(0b01);
|
||||
pub const WRITE: Self = Self(0b10);
|
||||
|
||||
/// Convenience alias for Read + Write.
|
||||
pub const READWRITE: Self = Self(Self::READ.0 | Self::WRITE.0);
|
||||
pub const NONE: Self = Self(0);
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// Check if a flag is present.
|
||||
#[inline]
|
||||
pub fn contains(&self, flag: Self) -> bool {
|
||||
self.0 & flag.0 != 0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DeviceId {
|
||||
Display,
|
||||
Serial,
|
||||
Random,
|
||||
Timer,
|
||||
}
|
||||
|
||||
/// Trait representing an input/output device that can be mapped into a memory space.
|
||||
///
|
||||
/// - Default methods return dummy data.
|
||||
pub trait IoDevice: Send {
|
||||
/// Return the size (in bytes) of the device's addressable region.
|
||||
fn size(&self) -> u32;
|
||||
|
||||
/// Return the access permissions for the device.
|
||||
fn access(&self) -> IoAccess;
|
||||
|
||||
/// Return the DeviceId
|
||||
fn id(&self) -> DeviceId;
|
||||
|
||||
#[inline]
|
||||
/// Read a single byte from the specified address.
|
||||
///
|
||||
/// By default, write‑only devices return `None`. All other devices
|
||||
/// return `Some(0)` as placeholder data. Implementations should override
|
||||
/// this method to provide actual read logic.
|
||||
fn read_byte(&self, offset: u32) -> Result<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, read‑only devices return `false` indicating failure.
|
||||
/// All other devices return `true`. Implementations should override
|
||||
/// this method to perform real write operations and return whether
|
||||
/// the operation succeeded.
|
||||
fn write_byte(&mut self, offset: u32, val: u8) -> Result<(), Interrupt> {
|
||||
if self.access().contains(IoAccess::WRITE) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Interrupt::WriteToReadOnly)
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a 32‑bit word from the specified address in little‑endian order.
|
||||
///
|
||||
/// The default implementation composes four consecutive byte reads using
|
||||
/// `read_byte`. If any byte read fails, the whole operation returns `None`.
|
||||
/// Write‑only devices return `None` immediately. Override this method for
|
||||
/// more efficient word reads.
|
||||
#[inline]
|
||||
fn read_word(&self, offset: u32) -> Result<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 32‑bit word to the specified address in little‑endian order.
|
||||
///
|
||||
/// The default implementation splits the value into bytes and writes each
|
||||
/// using `write_byte`. Read‑only devices return `false`; otherwise returns
|
||||
/// `true` after attempting all byte writes. Override for more efficient
|
||||
/// word writes or to handle partial failures.
|
||||
fn write_word(&mut self, offset: u32, val: u32) -> Result<(), Interrupt> {
|
||||
if self.access().contains(IoAccess::WRITE) {
|
||||
let bytes = val.to_le_bytes();
|
||||
self.write_byte(offset, bytes[0]);
|
||||
self.write_byte(offset + 1, bytes[1]);
|
||||
self.write_byte(offset + 2, bytes[2]);
|
||||
self.write_byte(offset + 3, bytes[3]);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(Interrupt::WriteToReadOnly)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#![feature(likely_unlikely)]
|
||||
|
||||
pub mod args;
|
||||
pub mod config;
|
||||
pub mod io;
|
||||
mod memory;
|
||||
mod processor;
|
||||
|
||||
pub use {config::MemoryMap, processor::processor::Emulator, processor::state::SharedState};
|
||||
|
||||
pub use memory::ram::*;
|
||||
@@ -0,0 +1,96 @@
|
||||
use clap::Parser;
|
||||
use common::{instructions::Instruction, register::Register};
|
||||
use dsa::{
|
||||
BulkAllocStore, Emulator, Page, RandomAccessMemory, SharedState,
|
||||
args::DsaArgs,
|
||||
io::display::{DisplayDevice, DisplayHandle},
|
||||
};
|
||||
use std::{
|
||||
os::unix::thread::JoinHandleExt,
|
||||
process::exit,
|
||||
sync::{Arc, atomic::Ordering},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
let args = DsaArgs::parse();
|
||||
|
||||
let mmap = args.get_memory_map();
|
||||
let iomap = args.get_io_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();
|
||||
|
||||
if let Some(bin) = args.get_binary() {
|
||||
for (i, ch) in bin.chunks(4).enumerate() {
|
||||
let instruction = Instruction(u32::from_le_bytes(ch.try_into().unwrap()));
|
||||
let hex = ch
|
||||
.iter()
|
||||
.map(|b| format!("{:02X}", b))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let chars = ch
|
||||
.iter()
|
||||
.map(|b| {
|
||||
if b.is_ascii_graphic() {
|
||||
*b as char
|
||||
} else {
|
||||
'.'
|
||||
}
|
||||
})
|
||||
.collect::<String>();
|
||||
println!("{:04X}: {:<12} {:4} {:?}", i * 4, hex, chars, instruction);
|
||||
}
|
||||
|
||||
let program: Vec<Page> = Page::paginate(&bin).collect();
|
||||
|
||||
for (i, page) in program.iter().enumerate() {
|
||||
emulator.memory_mut().write_page(i as u32 * 4096, &page);
|
||||
}
|
||||
}
|
||||
|
||||
let state = emulator.state_handle();
|
||||
|
||||
const STACK_SIZE: usize = 1024 * 1024 * 16;
|
||||
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!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
use crate::memory::{FaultInfo, PhysAddr, VirtAddr, tlb::TLB};
|
||||
|
||||
pub struct MMU {
|
||||
buffer: TLB,
|
||||
enable_paging: bool,
|
||||
|
||||
// Memory Management Register (MMR) Tells MMU where to find page tables
|
||||
mmr_value: u32,
|
||||
|
||||
page_table: Box<[Option<PageTable>; 1024]>,
|
||||
}
|
||||
|
||||
impl MMU {
|
||||
pub fn new() -> Self {
|
||||
MMU {
|
||||
buffer: TLB::new(),
|
||||
enable_paging: false,
|
||||
mmr_value: 0,
|
||||
page_table: Box::new([const { None }; 1024]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paging_enabled(&mut self, enable: bool) {
|
||||
self.enable_paging = enable;
|
||||
}
|
||||
|
||||
pub fn tlb_insert(&mut self, virtual_address: VirtAddr, physical_address: PhysAddr) {
|
||||
self.buffer.insert(virtual_address, physical_address);
|
||||
}
|
||||
|
||||
pub fn create_mapping(&mut self, virtual_address: VirtAddr, physical_address: PhysAddr) {
|
||||
let table = (virtual_address >> 22) as usize;
|
||||
let page = ((virtual_address >> 12) & 0x3FF) as usize;
|
||||
|
||||
self.page_table[table].get_or_insert_default().entries[page] =
|
||||
Some(PageTableEntry::new(physical_address, true, true, false));
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn lookup(&mut self, virtual_address: VirtAddr) -> Result<PhysAddr, FaultInfo> {
|
||||
if !self.enable_paging {
|
||||
return Ok(virtual_address);
|
||||
}
|
||||
|
||||
if let Some(addr) = self.buffer.lookup(virtual_address) {
|
||||
return Ok(addr);
|
||||
}
|
||||
|
||||
self.walk(virtual_address)
|
||||
}
|
||||
|
||||
#[cold]
|
||||
#[inline(never)]
|
||||
fn walk(&mut self, virtual_address: VirtAddr) -> Result<PhysAddr, FaultInfo> {
|
||||
let phys_addr = self.walk_page_table(virtual_address)?;
|
||||
self.buffer.insert(virtual_address, phys_addr);
|
||||
Ok(phys_addr)
|
||||
}
|
||||
|
||||
fn walk_page_table(&mut self, virtual_address: VirtAddr) -> Result<PhysAddr, FaultInfo> {
|
||||
let table = (virtual_address >> 22) as usize;
|
||||
let page = ((virtual_address >> 12) & 0x3FF) as usize;
|
||||
|
||||
if let Some(table) = &self.page_table[table] {
|
||||
return table.entries[page]
|
||||
.map(|entry| entry.physical_addr)
|
||||
.ok_or(FaultInfo::PageFault);
|
||||
} else {
|
||||
Err(FaultInfo::PageFault)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PageDir {
|
||||
entries: Box<[PageTable; 1024]>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PageTable {
|
||||
entries: Box<[Option<PageTableEntry>; 1024]>,
|
||||
}
|
||||
|
||||
impl Default for PageTable {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
entries: Box::new([None; 1024]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct PageTableEntry {
|
||||
physical_addr: PhysAddr,
|
||||
writable: bool,
|
||||
present: bool,
|
||||
user: bool,
|
||||
}
|
||||
|
||||
impl PageTableEntry {
|
||||
fn new(physical_addr: PhysAddr, writable: bool, present: bool, user: bool) -> Self {
|
||||
Self {
|
||||
physical_addr,
|
||||
writable,
|
||||
present,
|
||||
user,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{RandomAccessMemory, memory::mmu::MMU, processor::processor::Emulator};
|
||||
|
||||
mod cache;
|
||||
pub mod mmu;
|
||||
pub mod ram;
|
||||
mod tlb;
|
||||
|
||||
pub type VirtAddr = u32;
|
||||
pub type PhysAddr = u32;
|
||||
|
||||
pub enum FaultInfo {
|
||||
PageFault,
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
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) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use crate::memory::{PhysAddr, VirtAddr};
|
||||
|
||||
const TLB_SIZE: usize = 256;
|
||||
const TLB_MASK: u32 = (TLB_SIZE - 1) as u32;
|
||||
|
||||
pub struct TLB {
|
||||
entries: [TlbEntry; TLB_SIZE],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct TlbEntry {
|
||||
virtual_address: VirtAddr,
|
||||
physical_address: PhysAddr,
|
||||
valid: bool,
|
||||
}
|
||||
|
||||
impl TLB {
|
||||
pub fn new() -> Self {
|
||||
TLB {
|
||||
entries: [TlbEntry {
|
||||
virtual_address: 0,
|
||||
physical_address: 0,
|
||||
valid: false,
|
||||
}; TLB_SIZE],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn index(page: u32) -> usize {
|
||||
let hash = page ^ (page >> 12);
|
||||
(hash & TLB_MASK) as usize
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn lookup(&self, virtual_address: VirtAddr) -> Option<PhysAddr> {
|
||||
let page = virtual_address >> 12; // for 4KB pages
|
||||
let entry = self.entries[Self::index(page)];
|
||||
if entry.valid && entry.virtual_address == virtual_address {
|
||||
Some(entry.physical_address)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn insert(&mut self, virt: VirtAddr, phys: PhysAddr) {
|
||||
let idx = Self::index(virt) as usize;
|
||||
self.entries[idx] = TlbEntry {
|
||||
virtual_address: virt,
|
||||
physical_address: phys,
|
||||
valid: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Interrupt {
|
||||
// CPU exceptions 0-31
|
||||
InvalidInterrupt = 0,
|
||||
PageFault = 1,
|
||||
ProtectionFault = 2,
|
||||
InvalidOpcode = 3,
|
||||
DivideByZero = 4,
|
||||
StackOverflow = 5,
|
||||
WriteToReadOnly = 6,
|
||||
ReadFromWriteOnly = 7,
|
||||
UnmappedIo = 8,
|
||||
// 8-31 reserved for future CPU exceptions
|
||||
|
||||
// IRQ's (32-63)
|
||||
Hardware(u8),
|
||||
|
||||
// Syscalls (64-255)
|
||||
// OS defined, program uses INT 64-127
|
||||
Software(u8) = 128,
|
||||
}
|
||||
|
||||
impl Interrupt {
|
||||
pub fn code(&self) -> u8 {
|
||||
match self {
|
||||
Interrupt::InvalidInterrupt => 0,
|
||||
Interrupt::PageFault => 1,
|
||||
Interrupt::ProtectionFault => 2,
|
||||
Interrupt::InvalidOpcode => 3,
|
||||
Interrupt::DivideByZero => 4,
|
||||
Interrupt::StackOverflow => 5,
|
||||
Interrupt::WriteToReadOnly => 6,
|
||||
Interrupt::ReadFromWriteOnly => 7,
|
||||
Interrupt::UnmappedIo => 8,
|
||||
Interrupt::Hardware(x) => {
|
||||
if *x < 32 || *x > 63 {
|
||||
0
|
||||
} else {
|
||||
*x
|
||||
}
|
||||
}
|
||||
Interrupt::Software(x) => {
|
||||
if *x < 64 {
|
||||
0
|
||||
} else {
|
||||
*x
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod interrupts;
|
||||
pub mod processor;
|
||||
pub mod state;
|
||||
@@ -0,0 +1,653 @@
|
||||
use std::{
|
||||
hint::unlikely,
|
||||
ops::{Add, AddAssign},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::Ordering,
|
||||
mpsc::{self, TryRecvError},
|
||||
},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use common::{
|
||||
instructions::{Instruction, Opcode},
|
||||
register::Register,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Page,
|
||||
config::{IoMapping, MemoryMap, RegionType},
|
||||
io::{IoAccess, IoDevice, MappedDevice},
|
||||
memory::{mmu::MMU, ram::RandomAccessMemory},
|
||||
processor::{interrupts::Interrupt, state::SharedState},
|
||||
};
|
||||
|
||||
pub struct Emulator<Mem: RandomAccessMemory> {
|
||||
internal_state: ProcessorSnapshot,
|
||||
shared_state: Arc<SharedState>,
|
||||
|
||||
// Interrupts
|
||||
interrupts: mpsc::Receiver<u8>,
|
||||
pending_fault: Option<Interrupt>,
|
||||
|
||||
// memory
|
||||
mmu: MMU,
|
||||
mainstore: Mem,
|
||||
|
||||
// IO
|
||||
mmio: Vec<MappedDevice>,
|
||||
mmio_region: Option<(u32, u32)>, // start end end of segment
|
||||
|
||||
// optionals - not necessarily set on boot.
|
||||
memory_map: Option<MemoryMap>,
|
||||
|
||||
// Config params
|
||||
__mmap_configured: bool,
|
||||
__io_configured: bool,
|
||||
}
|
||||
|
||||
unsafe impl<Mem: RandomAccessMemory> Send for Emulator<Mem> {}
|
||||
impl<Mem: RandomAccessMemory> Emulator<Mem> {
|
||||
pub fn new(mem: Mem) -> Self {
|
||||
let (sender, receiver) = mpsc::channel::<u8>();
|
||||
|
||||
Self {
|
||||
interrupts: receiver,
|
||||
pending_fault: None,
|
||||
|
||||
internal_state: ProcessorSnapshot::default(),
|
||||
shared_state: Arc::new(SharedState::new(sender)),
|
||||
|
||||
memory_map: None,
|
||||
mmu: MMU::new(),
|
||||
mainstore: mem,
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
pub fn state_handle(&self) -> Arc<SharedState> {
|
||||
self.shared_state.clone()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn mmu_mut(&mut self) -> &mut MMU {
|
||||
&mut self.mmu
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn memory_mut(&mut self) -> &mut impl RandomAccessMemory {
|
||||
&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);
|
||||
|
||||
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);
|
||||
|
||||
unsafe {
|
||||
self.internal_state
|
||||
.registers
|
||||
.get_unchecked_mut(reg as usize)
|
||||
}
|
||||
}
|
||||
|
||||
#[cold]
|
||||
pub fn apply_memory_map(mut self, map: MemoryMap) -> Self {
|
||||
self.mmio_region = map
|
||||
.regions
|
||||
.iter()
|
||||
.find(|r| matches!(r.region_type, RegionType::MMIO))
|
||||
.map(|r| (r.base, r.base + r.size));
|
||||
|
||||
map.apply(&mut self);
|
||||
self.memory_map = Some(map);
|
||||
self.__mmap_configured = true;
|
||||
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
|
||||
.proc
|
||||
.store(Arc::new(self.internal_state.clone()));
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn boot(&mut self) -> Result<(), String> {
|
||||
if !(self.__io_configured && self.__mmap_configured) {
|
||||
return Err("Processor not configured".to_string());
|
||||
}
|
||||
|
||||
if let Some(map) = &self.memory_map {
|
||||
MemoryMap::identity_map(&mut self.mmu, map.regions.get(0).unwrap());
|
||||
}
|
||||
|
||||
self.internal_state.running = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn shutdown(&mut self) {
|
||||
self.internal_state.running = false;
|
||||
}
|
||||
|
||||
#[cold]
|
||||
pub fn idle_wait(&mut self) {
|
||||
self.internal_state.running = false;
|
||||
|
||||
// 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.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();
|
||||
}
|
||||
}
|
||||
|
||||
self.internal_state.running = true;
|
||||
}
|
||||
|
||||
pub fn run(&mut self) -> Result<(), String> {
|
||||
self.boot()?;
|
||||
|
||||
let mut time = Instant::now();
|
||||
|
||||
'emu: loop {
|
||||
// Update UI thread (roughly every 512k cycles targeting 60 UPS)
|
||||
if unlikely(self.internal_state.clock & 0x7FFFF == 0) {
|
||||
if self.shared_state.update_req.load(Ordering::Relaxed) {
|
||||
self.update();
|
||||
}
|
||||
}
|
||||
|
||||
// Check for commands or hardware Interrupts every 32k cycles
|
||||
if self.internal_state.clock % 0x7FFF == 0 {
|
||||
if self.shared_state.running.load(Ordering::Relaxed) == false {
|
||||
self.idle_wait();
|
||||
}
|
||||
|
||||
match self.interrupts.try_recv() {
|
||||
Ok(code) => self.interrupt(Interrupt::Software(code)),
|
||||
Err(TryRecvError::Disconnected) => break 'emu,
|
||||
Err(TryRecvError::Empty) => {}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
println!(
|
||||
"Clock: {} Executing {:?} PCX: {}, reg: {:?}",
|
||||
self.internal_state.clock,
|
||||
Instruction(instruction),
|
||||
pc,
|
||||
self.internal_state.registers
|
||||
);
|
||||
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.
|
||||
if let Some(fault) = self.pending_fault {
|
||||
self.pending_fault = None;
|
||||
println!("WARN fault: {:?}", fault);
|
||||
self.interrupt(fault);
|
||||
}
|
||||
|
||||
// always increment clock.
|
||||
self.internal_state.clock += 1;
|
||||
}
|
||||
|
||||
self.shutdown();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn interrupt(&mut self, int: Interrupt) {
|
||||
let idt = self.reg(Register::Idr);
|
||||
|
||||
*self.mut_reg(Register::Spr) -= 4;
|
||||
let spr = self.reg(Register::Spr);
|
||||
let pcx = self.reg(Register::Pcx);
|
||||
self.mem_write_word(spr, pcx);
|
||||
|
||||
*self.mut_reg(Register::Pcx) = self.mem_read_word(idt + int.code() as u32 * 4)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn execute(&mut self, word: Instruction) {
|
||||
// This needs to be unsafe as we're using word.xxxx_uc() functions for decoding which are unsafe
|
||||
// as they perform unchecked transmute operations (performance critical)
|
||||
unsafe {
|
||||
match Opcode::from_u8(word.opcode()) {
|
||||
// Nothing
|
||||
Some(Opcode::Nop) => {}
|
||||
|
||||
// move
|
||||
Some(Opcode::Mov) => {
|
||||
*self.mut_reg(word.dest_uc()) = self.reg(word.src1_uc());
|
||||
}
|
||||
Some(Opcode::CMov) => {
|
||||
if self.reg(word.misc_uc()) != 0 {
|
||||
*self.mut_reg(word.dest_uc()) = self.reg(word.src1_uc());
|
||||
}
|
||||
}
|
||||
|
||||
// load
|
||||
Some(Opcode::Ldbs) => todo!(),
|
||||
Some(Opcode::Ldhs) => todo!(),
|
||||
Some(Opcode::Ldb) => {
|
||||
*self.mut_reg(word.dest_uc()) = u32::from(
|
||||
self.mem_read_byte(self.reg(word.src1_uc()) + word.imm16() as u32),
|
||||
)
|
||||
}
|
||||
Some(Opcode::Ldh) => {
|
||||
*self.mut_reg(word.dest_uc()) = u32::from(
|
||||
self.mem_read_word(self.reg(word.src1_uc()) + word.imm16() as u32) >> 16,
|
||||
)
|
||||
}
|
||||
Some(Opcode::Ldw) => {
|
||||
*self.mut_reg(word.dest_uc()) = u32::from(
|
||||
self.mem_read_word(self.reg(word.src1_uc()) + word.imm16() as u32),
|
||||
)
|
||||
}
|
||||
|
||||
// store
|
||||
Some(Opcode::Stb) => {
|
||||
self.mem_write_byte(
|
||||
self.reg(word.dest_uc()) + word.imm16() as u32,
|
||||
self.reg(word.src1_uc()) as u8,
|
||||
);
|
||||
}
|
||||
Some(Opcode::Sth) => {
|
||||
self.mem_write_byte(
|
||||
self.reg(word.dest_uc()) + word.imm16() as u32,
|
||||
(self.reg(word.src1_uc()) as u16 >> 8) as u8,
|
||||
);
|
||||
self.mem_write_byte(
|
||||
self.reg(word.dest_uc()) + word.imm16() as u32 + 1,
|
||||
self.reg(word.src1_uc()) as u8,
|
||||
);
|
||||
}
|
||||
Some(Opcode::Stw) => {
|
||||
self.mem_write_word(
|
||||
self.reg(word.dest_uc()) + word.imm16() as u32,
|
||||
self.reg(word.src1_uc()),
|
||||
);
|
||||
}
|
||||
|
||||
// load immediate
|
||||
Some(Opcode::Lli) => {
|
||||
*self.mut_reg(word.dest_uc()) = word.imm16() as u32;
|
||||
}
|
||||
Some(Opcode::Lui) => {
|
||||
*self.mut_reg(word.dest_uc()) =
|
||||
(word.imm16() as u32) << 16 | self.reg(word.dest_uc());
|
||||
}
|
||||
|
||||
// Comparison
|
||||
Some(Opcode::Ieq) => {
|
||||
*self.mut_reg(word.dest_uc()) =
|
||||
(self.reg(word.src1_uc()) == self.reg(word.src2_uc())) as u32;
|
||||
}
|
||||
Some(Opcode::Ine) => {
|
||||
*self.mut_reg(word.dest_uc()) =
|
||||
(self.reg(word.src1_uc()) != self.reg(word.src2_uc())) as u32;
|
||||
}
|
||||
Some(Opcode::Ilt) => {
|
||||
*self.mut_reg(word.dest_uc()) =
|
||||
(self.reg(word.src1_uc()) < self.reg(word.src2_uc())) as u32;
|
||||
}
|
||||
Some(Opcode::Ile) => {
|
||||
*self.mut_reg(word.dest_uc()) =
|
||||
(self.reg(word.src1_uc()) <= self.reg(word.src2_uc())) as u32;
|
||||
}
|
||||
Some(Opcode::Igt) => {
|
||||
*self.mut_reg(word.dest_uc()) =
|
||||
(self.reg(word.src1_uc()) > self.reg(word.src2_uc())) as u32;
|
||||
}
|
||||
Some(Opcode::Ige) => {
|
||||
*self.mut_reg(word.dest_uc()) =
|
||||
(self.reg(word.src1_uc()) >= self.reg(word.src2_uc())) as u32;
|
||||
}
|
||||
|
||||
// Jump
|
||||
Some(Opcode::Jmp) => {
|
||||
*self.mut_reg(Register::Pcx) = self.reg(word.dest_uc()) + word.imm16() as u32
|
||||
}
|
||||
|
||||
Some(Opcode::Jez) => {
|
||||
if self.reg(word.src1_uc()) == 0 {
|
||||
*self.mut_reg(Register::Pcx) =
|
||||
self.reg(word.dest_uc()) + word.imm16() as u32
|
||||
}
|
||||
}
|
||||
Some(Opcode::Jnz) => {
|
||||
if self.reg(word.src1_uc()) != 0 {
|
||||
*self.mut_reg(Register::Pcx) =
|
||||
self.reg(word.dest_uc()) + word.imm16() as u32
|
||||
}
|
||||
}
|
||||
Some(Opcode::Jnc) => todo!(),
|
||||
Some(Opcode::Jic) => todo!(),
|
||||
|
||||
// Bitwise
|
||||
Some(Opcode::And) => {
|
||||
*self.mut_reg(word.dest_uc()) =
|
||||
self.reg(word.src1_uc()) & self.reg(word.src2_uc());
|
||||
}
|
||||
Some(Opcode::Nand) => {
|
||||
*self.mut_reg(word.dest_uc()) =
|
||||
!(self.reg(word.src1_uc()) & self.reg(word.src2_uc()));
|
||||
}
|
||||
Some(Opcode::Or) => {
|
||||
*self.mut_reg(word.dest_uc()) =
|
||||
self.reg(word.src1_uc()) | self.reg(word.src2_uc());
|
||||
}
|
||||
Some(Opcode::Nor) => {
|
||||
*self.mut_reg(word.dest_uc()) =
|
||||
!(self.reg(word.src1_uc()) | self.reg(word.src2_uc()));
|
||||
}
|
||||
Some(Opcode::Xor) => {
|
||||
*self.mut_reg(word.dest_uc()) =
|
||||
self.reg(word.src1_uc()) ^ self.reg(word.src2_uc());
|
||||
}
|
||||
Some(Opcode::Xnor) => {
|
||||
*self.mut_reg(word.dest_uc()) =
|
||||
!(self.reg(word.src1_uc()) ^ self.reg(word.src2_uc()));
|
||||
}
|
||||
Some(Opcode::Not) => {
|
||||
*self.mut_reg(word.dest_uc()) = !self.reg(word.src1_uc());
|
||||
}
|
||||
|
||||
// Arithmetic
|
||||
Some(Opcode::Add) => {
|
||||
*self.mut_reg(word.dest_uc()) =
|
||||
self.reg(word.src1_uc()) + self.reg(word.src2_uc());
|
||||
}
|
||||
|
||||
Some(Opcode::Sub) => {
|
||||
*self.mut_reg(word.dest_uc()) =
|
||||
self.reg(word.src1_uc()) - self.reg(word.src2_uc());
|
||||
}
|
||||
|
||||
Some(Opcode::Shl) => {
|
||||
*self.mut_reg(word.dest_uc()) = self.reg(word.src1_uc())
|
||||
<< (self.reg(word.src2_uc()) + word.shamt() as u32);
|
||||
}
|
||||
|
||||
Some(Opcode::Shr) => {
|
||||
*self.mut_reg(word.dest_uc()) = self.reg(word.src1_uc())
|
||||
>> (self.reg(word.src2_uc()) + word.shamt() as u32);
|
||||
}
|
||||
|
||||
Some(Opcode::Addi) => {
|
||||
*self.mut_reg(word.dest_uc()) = self.reg(word.src1_uc()) + word.imm16() as u32;
|
||||
}
|
||||
|
||||
Some(Opcode::Subi) => {
|
||||
*self.mut_reg(word.dest_uc()) = self.reg(word.src1_uc()) - word.imm16() as u32;
|
||||
}
|
||||
|
||||
// Utility
|
||||
Some(Opcode::Push) => {
|
||||
self.push(word.src1_uc());
|
||||
}
|
||||
Some(Opcode::Pop) => {
|
||||
self.pop(word.dest_uc());
|
||||
}
|
||||
|
||||
// Function
|
||||
Some(Opcode::Call) => {
|
||||
self.push(Register::Pcx);
|
||||
*self.mut_reg(Register::Pcx) = self.reg(word.dest_uc()) + word.imm16() as u32
|
||||
}
|
||||
Some(Opcode::Ret) => {
|
||||
self.pop(Register::Ret);
|
||||
*self.mut_reg(Register::Pcx) = self.reg(Register::Ret);
|
||||
}
|
||||
|
||||
Some(Opcode::Int) => {
|
||||
self.interrupt(Interrupt::Software(word.imm16() as u8));
|
||||
}
|
||||
Some(Opcode::Hlt) => self.internal_state.running = false,
|
||||
Some(Opcode::IRet) => todo!(),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn push(&mut self, reg: Register) {
|
||||
*self.mut_reg(Register::Spr) -= 4;
|
||||
self.mem_write_word(self.reg(Register::Spr), self.reg(reg));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn pop(&mut self, reg: Register) {
|
||||
*self.mut_reg(reg) = self.mem_read_word(self.reg(Register::Spr));
|
||||
*self.mut_reg(Register::Spr) += 4;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
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;
|
||||
}
|
||||
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 clock: usize,
|
||||
pub registers: [u32; Self::REG_COUNT],
|
||||
}
|
||||
|
||||
impl Default for ProcessorSnapshot {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
running: false,
|
||||
clock: 0,
|
||||
registers: [0; Self::REG_COUNT],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProcessorSnapshot {
|
||||
const REG_COUNT: usize = 28;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use std::sync::{Arc, atomic::AtomicBool, mpsc};
|
||||
|
||||
use crate::processor::processor::ProcessorSnapshot;
|
||||
use arc_swap::ArcSwap;
|
||||
|
||||
pub struct SharedState {
|
||||
pub proc: ArcSwap<ProcessorSnapshot>,
|
||||
|
||||
pub running: AtomicBool,
|
||||
pub update_req: AtomicBool,
|
||||
|
||||
sender: mpsc::Sender<u8>,
|
||||
}
|
||||
|
||||
impl SharedState {
|
||||
pub fn new(sender: mpsc::Sender<u8>) -> Self {
|
||||
Self {
|
||||
proc: ArcSwap::new(Arc::new(ProcessorSnapshot::default())),
|
||||
|
||||
running: AtomicBool::new(true),
|
||||
sender: sender,
|
||||
update_req: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user