initial commit
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
[package]
|
||||
name = "emulator"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
default-run = "emulator"
|
||||
|
||||
[lib]
|
||||
name = "dsa_rs"
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "emulator"
|
||||
required-features = ["config"]
|
||||
|
||||
[dependencies]
|
||||
common = { path = "../common" }
|
||||
assembler = { path = "../assembler" }
|
||||
dsa_editor = { path = "../dsa_editor" }
|
||||
egui = "0.31.1"
|
||||
dirs = "6.0.0"
|
||||
discord-presence = { version = "1.6.0", optional = true }
|
||||
toml = { version = "0.8.23", optional = true }
|
||||
serde = { version = "1.0.219", features = ["derive"], optional = true }
|
||||
egui_file = "0.22.1"
|
||||
|
||||
[features]
|
||||
default = ["config"]
|
||||
discord-rpc = ["dep:discord-presence"]
|
||||
config = ["dep:toml", "dep:serde"]
|
||||
|
||||
# Add support for Android for the fun of it. Currently crashes lol.
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
winit = { version = "0.30.11", features = ["android-native-activity"] }
|
||||
# jni = "0.21.1"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies.eframe]
|
||||
version = "0.31.1"
|
||||
features = ["android-native-activity"]
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies.eframe]
|
||||
version = "0.31.1"
|
||||
@@ -0,0 +1,38 @@
|
||||
//! Loads configuration information from a TOML file in the current working directory.
|
||||
//! Currently doesn't do much but this may be expanded.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct Config {
|
||||
pub misc: MiscTable,
|
||||
}
|
||||
|
||||
/// For config options where you aren't sure what table it should go under.
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct MiscTable {
|
||||
/// Whether or not we can enable Discord RPC for fun.
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
pub use_discord_rpc: bool,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load(path: &Path) -> Result<Self, toml::de::Error> {
|
||||
let file_contents = match std::fs::read_to_string(path) {
|
||||
Ok(file_contents) => file_contents,
|
||||
Err(why) => {
|
||||
eprintln!(
|
||||
"WARN: Expected to read config file from '{}' with error '{}'. Using default settings.",
|
||||
path.display(),
|
||||
why
|
||||
);
|
||||
|
||||
return Ok(Self::default());
|
||||
}
|
||||
};
|
||||
|
||||
Self::deserialize(toml::Deserializer::new(&file_contents))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod rpc;
|
||||
@@ -0,0 +1,221 @@
|
||||
//! Just for fun I thought I would add a Discord RPC client to the emulator.
|
||||
//!
|
||||
//! This will display information like the current value of PCX, architecture name and
|
||||
//! GitHub repo links to show off the ISA. Perhaps in the future if we cross-compile to
|
||||
//! WASM we could include a link to run this software in the browser.
|
||||
//!
|
||||
//!
|
||||
//! # Configuration
|
||||
//!
|
||||
//! This may be disabled like so in your `.dsa.emulator.toml` file:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [misc]
|
||||
//! use_discord_rpc = false
|
||||
//! ```
|
||||
//!
|
||||
//! Alternatively, you can hide this in your Discord settings.
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
use discord_presence::{Client, DiscordError, models::ActivityTimestamps};
|
||||
|
||||
use crate::emulator::config::Config;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
pub enum RpcClientError {
|
||||
Client(DiscordError),
|
||||
}
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
impl std::fmt::Display for RpcClientError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Client(why) => write!(f, "discord RPC error: {why}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
impl std::error::Error for RpcClientError {}
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
impl From<DiscordError> for RpcClientError {
|
||||
fn from(err: DiscordError) -> Self {
|
||||
Self::Client(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of activity the user is currently doing.
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
pub enum Activity {
|
||||
Idle,
|
||||
EditingFile(PathBuf),
|
||||
}
|
||||
|
||||
/// Messages to send over the wire.
|
||||
#[derive(Debug)]
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
pub enum Message {
|
||||
/// Sent when we want to update the [`Context`].
|
||||
Update(Activity),
|
||||
/// Sent when the main program wants to exit.
|
||||
Stop,
|
||||
}
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
unsafe impl Send for Message {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
pub struct RpcClient {
|
||||
/// Sends updates to [`Context`] (our state).
|
||||
sender: Sender<Message>,
|
||||
/// Stored for later cleanup on Drop.
|
||||
thread_handle: Option<Arc<std::thread::JoinHandle<()>>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
impl RpcClient {
|
||||
#[expect(clippy::unreadable_literal)]
|
||||
/// Sets up the [`RpcClient`].
|
||||
pub fn new(
|
||||
sender: Sender<Message>,
|
||||
reciever: Receiver<Message>,
|
||||
) -> Result<Self, RpcClientError> {
|
||||
// TODO: Put client id into a .env file.
|
||||
let mut client = discord_presence::Client::new(1384303074088190042);
|
||||
|
||||
let thread_handle = std::thread::spawn(move || {
|
||||
client.start();
|
||||
|
||||
eprintln!("INFO: Started Discord RPC client.");
|
||||
|
||||
std::thread::sleep(Duration::from_millis(1000));
|
||||
|
||||
// Recieve updates and do shit.
|
||||
for message in &reciever {
|
||||
match message {
|
||||
Message::Update(activity) => {
|
||||
Self::handle_activity(&mut client, &activity);
|
||||
}
|
||||
Message::Stop => {
|
||||
eprintln!("INFO: Stopping discord RPC client.");
|
||||
if let Err(why) = client.shutdown() {
|
||||
eprintln!("ERROR: Stopping discord RPC client failed: {why}");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
sender,
|
||||
thread_handle: Some(Arc::new(thread_handle)),
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_activity(client: &mut Client, activity: &Activity) {
|
||||
let current_time = std::time::SystemTime::now();
|
||||
let timestamps = ActivityTimestamps::new().start(
|
||||
current_time
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("Failed to get UNIX timestamp for activity.")
|
||||
.as_secs(),
|
||||
);
|
||||
|
||||
match activity {
|
||||
Activity::Idle => {
|
||||
client
|
||||
.set_activity(|act| act.details("Idle").timestamps(|_| timestamps))
|
||||
.expect("TODO: Exponential backoff.");
|
||||
}
|
||||
Activity::EditingFile(file_path) => {
|
||||
client
|
||||
.set_activity(|act| {
|
||||
act.details(format!("Editing file: {}", file_path.display()))
|
||||
.timestamps(|_| timestamps)
|
||||
})
|
||||
.expect("TODO: Exponential backoff.");
|
||||
}
|
||||
}
|
||||
|
||||
eprintln!("INFO: RPC sent: {activity:?}");
|
||||
}
|
||||
|
||||
/// Stops the [`RpcClient`].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// May panic if the reciever was deallocated. This should not happen.
|
||||
fn stop(&self) {
|
||||
self.sender
|
||||
.send(Message::Stop)
|
||||
.expect("Failed to send stop message to RPC client.");
|
||||
}
|
||||
|
||||
/// Send an update with a given [`Activity`] to the [`RpcClient`].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// May panic if the reciever was deallocated. This should not happen.
|
||||
pub fn update(&self, activity: Activity) {
|
||||
self.sender
|
||||
.send(Message::Update(activity))
|
||||
.expect("Failed to send update to RPC client. This should not happen.");
|
||||
}
|
||||
}
|
||||
|
||||
// Possibly unneeded but good practice.
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
impl Drop for RpcClient {
|
||||
fn drop(&mut self) {
|
||||
self.stop();
|
||||
|
||||
if let Some(handle) = self.thread_handle.take() {
|
||||
if let Some(handle) = Arc::into_inner(handle) {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stub for when the feature is disabled.
|
||||
#[cfg(not(feature = "discord-rpc"))]
|
||||
pub struct RpcClient {}
|
||||
|
||||
/// Stub for when the feature is disabled.
|
||||
#[cfg(not(feature = "discord-rpc"))]
|
||||
pub enum Message {}
|
||||
|
||||
/// Stub for when the feature is disabled.
|
||||
#[cfg(not(feature = "discord-rpc"))]
|
||||
pub enum Activity {}
|
||||
|
||||
/// Gets the discord [`RpcClient`] or returns None if this has been disabled in the config
|
||||
/// options.
|
||||
#[cfg(feature = "config")]
|
||||
#[allow(clippy::needless_pass_by_value, unused_variables)]
|
||||
pub fn get_rpc_client_or_none(
|
||||
config: &Config,
|
||||
rpc_sender: Sender<Message>,
|
||||
rpc_reciever: Receiver<Message>,
|
||||
) -> Result<Option<RpcClient>, Box<dyn std::error::Error + 'static>> {
|
||||
#[cfg(not(feature = "discord-rpc"))]
|
||||
return Ok(None);
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
if config.misc.use_discord_rpc {
|
||||
Ok(Some(RpcClient::new(rpc_sender, rpc_reciever)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#[cfg(feature = "config")]
|
||||
pub mod config;
|
||||
pub mod misc;
|
||||
pub mod system;
|
||||
pub mod ui;
|
||||
@@ -0,0 +1,245 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::{self, Receiver, Sender};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crate::emulator::misc::rpc::{Activity, RpcClient};
|
||||
|
||||
use crate::emulator::system::model::StateUpdate;
|
||||
use crate::emulator::system::{
|
||||
model::{Command, Running},
|
||||
processor::Processor,
|
||||
};
|
||||
|
||||
use common::prelude::*;
|
||||
|
||||
#[expect(clippy::too_many_lines)]
|
||||
#[allow(unused_variables)]
|
||||
pub fn run_emulator(
|
||||
cmd_rx: &Receiver<Command>,
|
||||
state_tx: &Sender<StateUpdate>,
|
||||
mut processor: Processor,
|
||||
rpc_client: Option<&Arc<RpcClient>>,
|
||||
) {
|
||||
println!("INFO: Starting emulator.");
|
||||
|
||||
let mut running = Running::Paused;
|
||||
let mut step = 0;
|
||||
let mut addr;
|
||||
let mut history = Vec::<(u32, Instruction)>::new();
|
||||
let size = 256;
|
||||
|
||||
state_tx
|
||||
.send(StateUpdate::Running(Running::Paused))
|
||||
.expect("Failed to send initial state!");
|
||||
|
||||
let mut instruction_count = 0;
|
||||
let mut update = false;
|
||||
|
||||
loop {
|
||||
let cmd = if running == Running::Running || step > 0 {
|
||||
match cmd_rx.try_recv() {
|
||||
Ok(cmd) => Some(cmd),
|
||||
Err(mpsc::TryRecvError::Empty) => {
|
||||
update = false;
|
||||
None
|
||||
}
|
||||
Err(mpsc::TryRecvError::Disconnected) => break,
|
||||
}
|
||||
} else {
|
||||
match cmd_rx.recv() {
|
||||
Ok(cmd) => Some(cmd),
|
||||
Err(_) => break,
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(cmd) = cmd {
|
||||
match cmd {
|
||||
Command::Start => {
|
||||
running = Running::Running;
|
||||
|
||||
// Update RPC with current state. TODO: Make this only occur on state
|
||||
// changes.
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
if let Some(rpc_client) = rpc_client {
|
||||
use std::{path::PathBuf, str::FromStr};
|
||||
|
||||
rpc_client.update(Activity::EditingFile(
|
||||
PathBuf::from_str("test")
|
||||
.expect("This is a valid path, WTF."),
|
||||
));
|
||||
}
|
||||
}
|
||||
Command::Stop => {
|
||||
running = Running::Paused;
|
||||
}
|
||||
Command::Reset(x) => {
|
||||
running = Running::Paused;
|
||||
|
||||
match x {
|
||||
0 => {
|
||||
processor.clear();
|
||||
processor.reset();
|
||||
instruction_count = 0;
|
||||
}
|
||||
1 => {
|
||||
processor.reset();
|
||||
instruction_count = 0;
|
||||
}
|
||||
2 => {
|
||||
processor.clear();
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
processor.reset();
|
||||
}
|
||||
Command::Step(x) => {
|
||||
step = x;
|
||||
}
|
||||
Command::Write(offset, data) => {
|
||||
update = true;
|
||||
|
||||
processor
|
||||
.memory
|
||||
.write_range(offset, data)
|
||||
.unwrap_or_else(|_| {
|
||||
report_err(
|
||||
state_tx,
|
||||
"Failed to write memory range!",
|
||||
&mut processor,
|
||||
);
|
||||
});
|
||||
}
|
||||
Command::Interrupt(_interrupt) => {
|
||||
update = true;
|
||||
|
||||
todo!("implement interrupts")
|
||||
}
|
||||
Command::MemRequest(new, size) if update => {
|
||||
addr = new;
|
||||
let _ = state_tx.send(StateUpdate::MemoryView(
|
||||
processor.memory.read_range(addr, size).unwrap_or_else(|_| {
|
||||
report_err(
|
||||
state_tx,
|
||||
"Failed to read memory range!",
|
||||
&mut processor,
|
||||
);
|
||||
Vec::new()
|
||||
}),
|
||||
));
|
||||
}
|
||||
Command::DisplayRequest if update => {
|
||||
let _ = state_tx.send(StateUpdate::DisplayView(
|
||||
processor.display().unwrap_or_else(|_| {
|
||||
report_err(
|
||||
state_tx,
|
||||
"Failed to read display!",
|
||||
&mut processor,
|
||||
);
|
||||
Vec::new()
|
||||
}),
|
||||
));
|
||||
}
|
||||
Command::StackRequest if update => {
|
||||
let _ = state_tx.send(StateUpdate::StackView(
|
||||
processor.get_stack(32).unwrap_or_else(|_| {
|
||||
report_err(state_tx, "Failed to read stack!", &mut processor);
|
||||
Vec::new()
|
||||
}),
|
||||
));
|
||||
}
|
||||
Command::RegisterRequest if update => {
|
||||
let _ = state_tx.send(StateUpdate::Registers(processor.registers));
|
||||
}
|
||||
Command::RunningRequest if update => {
|
||||
let _ = state_tx.send(StateUpdate::Running(running));
|
||||
}
|
||||
Command::HistoryRequest if update => {
|
||||
let hsc = history.clone();
|
||||
history.clear();
|
||||
let _ = state_tx.send(StateUpdate::InstructionHistory(hsc));
|
||||
}
|
||||
Command::InstructionCountRequest if update => {
|
||||
let _ = state_tx.send(StateUpdate::Instructions(instruction_count));
|
||||
}
|
||||
Command::WriteBlock(addr, block) => {
|
||||
processor
|
||||
.memory
|
||||
.write_range(addr, block.to_vec())
|
||||
.unwrap_or_else(|_| {
|
||||
report_err(
|
||||
state_tx,
|
||||
"Failed to write memory block!",
|
||||
&mut processor,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if step > 0 {
|
||||
step -= 1;
|
||||
update = true;
|
||||
running = Running::Paused;
|
||||
|
||||
// Execute one cycle.
|
||||
match processor.cycle() {
|
||||
Ok((addr, instruction)) => {
|
||||
history.push((addr, instruction));
|
||||
}
|
||||
Err(why) => {
|
||||
let pcx = processor
|
||||
.get(Register::Pcx)
|
||||
.expect("SPR should never be invalid");
|
||||
report_err(
|
||||
state_tx,
|
||||
&format!(
|
||||
"Could not decode instruction at {pcx:x}. Reason: {why}"
|
||||
),
|
||||
&mut processor,
|
||||
);
|
||||
}
|
||||
}
|
||||
instruction_count += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if running == Running::Running {
|
||||
update = true;
|
||||
|
||||
// Execute one cycle.
|
||||
let instruction = match processor.cycle() {
|
||||
Ok(instruction) => instruction,
|
||||
Err(why) => {
|
||||
let pcx = processor
|
||||
.get(Register::Pcx)
|
||||
.expect("PCX should never be invalid");
|
||||
report_err(
|
||||
state_tx,
|
||||
&format!(
|
||||
"Could not decode instruction at {pcx:x}. Reason: {why}"
|
||||
),
|
||||
&mut processor,
|
||||
);
|
||||
(pcx, Instruction::Nop)
|
||||
}
|
||||
};
|
||||
|
||||
history.push(instruction);
|
||||
if matches!(instruction.1, Instruction::Halt) {
|
||||
running = Running::Halted;
|
||||
}
|
||||
|
||||
instruction_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn report_err(state_tx: &Sender<StateUpdate>, why: &str, processor: &mut Processor) {
|
||||
processor
|
||||
.begin_interrupt(Interrupt::HardFault)
|
||||
.expect("What kind of goofy ahh shenanigans did you do with your fault handler? At this point, the emulator can just crash. this is on you.");
|
||||
let _ = state_tx.send(StateUpdate::Error(why.to_string()));
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::emulator::system::model::ProcessorError;
|
||||
|
||||
pub trait MemoryUnit: Send + Sync {
|
||||
fn reset(&mut self);
|
||||
fn read_byte(&mut self, addr: u32) -> Result<u8, ProcessorError>;
|
||||
fn write_byte(&mut self, addr: u32, value: u8) -> Result<(), ProcessorError>;
|
||||
fn read_word(&mut self, addr: u32) -> Result<u32, ProcessorError>;
|
||||
fn write_word(&mut self, addr: u32, value: u32) -> Result<(), ProcessorError>;
|
||||
|
||||
fn read_range(&mut self, addr: u32, size: u32) -> Result<Vec<u8>, ProcessorError> {
|
||||
let mut data = Vec::with_capacity(size as usize);
|
||||
for i in 0..size {
|
||||
data.push(self.read_byte(addr + i)?);
|
||||
}
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn write_range(&mut self, addr: u32, value: Vec<u8>) -> Result<(), ProcessorError> {
|
||||
for (i, byte) in value.into_iter().enumerate() {
|
||||
self.write_byte(addr + i as u32, byte)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_block(&mut self, addr: u32) -> Result<[u8; 256], ProcessorError> {
|
||||
let mut data = [0; 256];
|
||||
for (i, byte) in data.iter_mut().enumerate() {
|
||||
*byte = self.read_byte(addr + i as u32)?;
|
||||
}
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn write_block(&mut self, addr: u32, data: [u8; 256]) -> Result<(), ProcessorError> {
|
||||
for (i, byte) in data.iter().enumerate() {
|
||||
self.write_byte(addr + i as u32, *byte)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MainStore {
|
||||
pub data: HashMap<u32, Block>,
|
||||
}
|
||||
|
||||
pub struct Block {
|
||||
data: [u8; 256],
|
||||
}
|
||||
|
||||
impl Default for MainStore {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MainStore {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
const fn segment_addr(addr: u32) -> (u32, u8) {
|
||||
(addr / 256, (addr % 256) as u8)
|
||||
}
|
||||
|
||||
fn mut_block(&mut self, addr: u32) -> &mut Block {
|
||||
self.data
|
||||
.entry(addr)
|
||||
.or_insert_with(|| Block { data: [0; 256] });
|
||||
|
||||
self.data.get_mut(&addr).map_or_else(
|
||||
|| panic!("Could not fetch block with address {addr:x?}"),
|
||||
|block| block,
|
||||
)
|
||||
}
|
||||
|
||||
fn block(&mut self, addr: u32) -> &Block {
|
||||
self.data
|
||||
.entry(addr)
|
||||
.or_insert_with(|| Block { data: [0; 256] });
|
||||
|
||||
self.data.get(&addr).map_or_else(
|
||||
|| panic!("Could not fetch block with address {addr:x?}"),
|
||||
|block| block,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryUnit for MainStore {
|
||||
fn reset(&mut self) {
|
||||
self.data.clear();
|
||||
}
|
||||
|
||||
fn read_byte(&mut self, addr: u32) -> Result<u8, ProcessorError> {
|
||||
let (block_addr, offset) = Self::segment_addr(addr);
|
||||
let block = self.block(block_addr);
|
||||
Ok(block.data[offset as usize])
|
||||
}
|
||||
|
||||
fn read_word(&mut self, addr: u32) -> Result<u32, ProcessorError> {
|
||||
if addr % 4 != 0 {
|
||||
return Err(ProcessorError::BadMemoryAccess(addr));
|
||||
}
|
||||
|
||||
let (block_addr, offset) = Self::segment_addr(addr);
|
||||
let block = self.mut_block(block_addr);
|
||||
let mut bytes = [0; 4];
|
||||
bytes[0] = block.data[offset as usize];
|
||||
bytes[1] = block.data[(offset + 1) as usize];
|
||||
bytes[2] = block.data[(offset + 2) as usize];
|
||||
bytes[3] = block.data[(offset + 3) as usize];
|
||||
Ok(u32::from_be_bytes(bytes))
|
||||
}
|
||||
|
||||
fn read_range(&mut self, addr: u32, size: u32) -> Result<Vec<u8>, ProcessorError> {
|
||||
let mut data = Vec::with_capacity(size as usize);
|
||||
for i in 0..size {
|
||||
data.push(self.read_byte(addr + i)?);
|
||||
}
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn write_byte(&mut self, addr: u32, value: u8) -> Result<(), ProcessorError> {
|
||||
let (block_addr, offset) = Self::segment_addr(addr);
|
||||
let block = self.mut_block(block_addr);
|
||||
block.data[offset as usize] = value;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_word(&mut self, addr: u32, value: u32) -> Result<(), ProcessorError> {
|
||||
if addr % 4 != 0 {
|
||||
return Err(ProcessorError::BadMemoryAccess(addr));
|
||||
}
|
||||
|
||||
let (block_addr, offset) = Self::segment_addr(addr);
|
||||
let block = self.mut_block(block_addr);
|
||||
block.data[offset as usize] = (value >> 24) as u8;
|
||||
block.data[(offset + 1) as usize] = (value >> 16) as u8;
|
||||
block.data[(offset + 2) as usize] = (value >> 8) as u8;
|
||||
block.data[(offset + 3) as usize] = value as u8;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_range(&mut self, addr: u32, value: Vec<u8>) -> Result<(), ProcessorError> {
|
||||
for (i, byte) in value.into_iter().enumerate() {
|
||||
let (block_addr, offset) = Self::segment_addr(addr + i as u32);
|
||||
let block = self.mut_block(block_addr);
|
||||
block.data[offset as usize] = byte;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_block(&mut self, addr: u32) -> Result<[u8; 256], ProcessorError> {
|
||||
let (block_addr, _) = Self::segment_addr(addr);
|
||||
let block = self.block(block_addr);
|
||||
Ok(block.data)
|
||||
}
|
||||
|
||||
fn write_block(&mut self, addr: u32, data: [u8; 256]) -> Result<(), ProcessorError> {
|
||||
let (block_addr, _) = Self::segment_addr(addr);
|
||||
let block = self.mut_block(block_addr);
|
||||
block.data = data;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod emulator;
|
||||
pub mod memory;
|
||||
pub mod model;
|
||||
pub mod processor;
|
||||
@@ -0,0 +1,327 @@
|
||||
use std::sync::mpsc::{self, Receiver, Sender};
|
||||
|
||||
use common::prelude::*;
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
|
||||
pub enum Running {
|
||||
Running,
|
||||
Paused,
|
||||
Halted,
|
||||
}
|
||||
|
||||
pub trait IODevice: Send + Sync {
|
||||
fn read_byte(&mut self, addr: u32) -> u8;
|
||||
fn write_byte(&mut self, addr: u32, value: u8);
|
||||
fn read_range(&mut self, addr: u32, size: u32) -> Vec<u8>;
|
||||
fn write_range(&mut self, addr: u32, value: Vec<u8>);
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub enum Command {
|
||||
// set emulator state.
|
||||
Start,
|
||||
Stop,
|
||||
Step(usize),
|
||||
Reset(usize),
|
||||
Interrupt(Interrupt),
|
||||
Write(Address, Vec<u8>),
|
||||
WriteBlock(Address, Box<[u8; 256]>),
|
||||
|
||||
// request emulator state.
|
||||
MemRequest(Address, u32),
|
||||
DisplayRequest,
|
||||
StackRequest,
|
||||
RegisterRequest,
|
||||
RunningRequest,
|
||||
HistoryRequest,
|
||||
InstructionCountRequest,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ProcessorError {
|
||||
InvalidInstruction(u32),
|
||||
InvalidRegister(u8),
|
||||
BadMemoryAccess(u32),
|
||||
}
|
||||
|
||||
impl std::error::Error for ProcessorError {}
|
||||
|
||||
impl std::fmt::Display for ProcessorError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::InvalidInstruction(instruction) => {
|
||||
write!(f, "Invalid instruction: {instruction}")
|
||||
}
|
||||
Self::InvalidRegister(register) => {
|
||||
write!(f, "Invalid register: {register}")
|
||||
}
|
||||
Self::BadMemoryAccess(address) => {
|
||||
write!(f, "Bad memory access: {address}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
pub state_receiver: Receiver<StateUpdate>,
|
||||
pub cmd_sender: Sender<Command>,
|
||||
|
||||
// Processor state
|
||||
pub reg_file: RegFile,
|
||||
pub running: Running,
|
||||
pub instructions: usize,
|
||||
|
||||
// Memory access views
|
||||
pub stack_view: Vec<u8>,
|
||||
pub memory_view: Vec<u8>,
|
||||
pub display_view: Vec<u8>,
|
||||
|
||||
pub error_log: Vec<String>,
|
||||
|
||||
pub instruction_history: Vec<(u32, Instruction)>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
#[must_use]
|
||||
pub fn new(sender: Sender<Command>, receiver: Receiver<StateUpdate>) -> Self {
|
||||
Self {
|
||||
state_receiver: receiver,
|
||||
cmd_sender: sender,
|
||||
reg_file: RegFile::default(),
|
||||
running: Running::Paused,
|
||||
instructions: 0,
|
||||
stack_view: vec![],
|
||||
memory_view: vec![],
|
||||
display_view: vec![],
|
||||
error_log: vec![],
|
||||
instruction_history: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send(&mut self, cmd: Command) {
|
||||
if let Err(e) = self.cmd_sender.send(cmd) {
|
||||
self.error_log.push(e.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self) -> Result<(), mpsc::TryRecvError> {
|
||||
while let Ok(update) = self.state_receiver.try_recv() {
|
||||
match update {
|
||||
StateUpdate::Registers(reg_file) => self.reg_file = reg_file,
|
||||
StateUpdate::Running(running) => self.running = running,
|
||||
StateUpdate::Instructions(instructions) => {
|
||||
self.instructions = instructions;
|
||||
}
|
||||
StateUpdate::StackView(stack_view) => self.stack_view = stack_view,
|
||||
StateUpdate::MemoryView(memory_view) => self.memory_view = memory_view,
|
||||
StateUpdate::DisplayView(display_view) => {
|
||||
self.display_view = display_view;
|
||||
}
|
||||
StateUpdate::Error(err_state) => self.error_log.push(err_state),
|
||||
StateUpdate::InstructionHistory(history) => {
|
||||
self.instruction_history.extend(history);
|
||||
}
|
||||
}
|
||||
|
||||
if self.error_log.len() > 256 {
|
||||
self.error_log.drain(0..self.error_log.len() - 256);
|
||||
}
|
||||
|
||||
if self.instruction_history.len() > 1024 {
|
||||
self.instruction_history
|
||||
.drain(0..self.instruction_history.len() - 1024);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = self.state_receiver.try_recv() {
|
||||
match e {
|
||||
mpsc::TryRecvError::Empty => {}
|
||||
mpsc::TryRecvError::Disconnected => {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub enum StateUpdate {
|
||||
Registers(RegFile),
|
||||
Running(Running),
|
||||
Instructions(usize),
|
||||
StackView(Vec<u8>),
|
||||
MemoryView(Vec<u8>),
|
||||
DisplayView(Vec<u8>),
|
||||
Error(String),
|
||||
InstructionHistory(Vec<(u32, Instruction)>),
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct RegFile {
|
||||
// General Purpose Registers
|
||||
rg0: u32,
|
||||
rg1: u32,
|
||||
rg2: u32,
|
||||
rg3: u32,
|
||||
rg4: u32,
|
||||
rg5: u32,
|
||||
rg6: u32,
|
||||
rg7: u32,
|
||||
rg8: u32,
|
||||
rg9: u32,
|
||||
rga: u32,
|
||||
rgb: u32,
|
||||
rgc: u32,
|
||||
rgd: u32,
|
||||
rge: u32,
|
||||
rgf: u32,
|
||||
|
||||
// Special Purpose Registers
|
||||
acc: u32,
|
||||
spr: u32,
|
||||
bpr: u32,
|
||||
ret: u32,
|
||||
idr: u32,
|
||||
mmr: u32,
|
||||
|
||||
// System Registers
|
||||
mar: u32,
|
||||
mdr: u32,
|
||||
sts: u32,
|
||||
cir: u32,
|
||||
pcx: u32,
|
||||
}
|
||||
|
||||
impl RegFile {
|
||||
#[must_use]
|
||||
pub fn all(&self) -> Vec<(&str, u32)> {
|
||||
vec![
|
||||
("Rg0", self.rg0),
|
||||
("Rg1", self.rg1),
|
||||
("Rg2", self.rg2),
|
||||
("Rg3", self.rg3),
|
||||
("Rg4", self.rg4),
|
||||
("Rg5", self.rg5),
|
||||
("Rg6", self.rg6),
|
||||
("Rg7", self.rg7),
|
||||
("Rg8", self.rg8),
|
||||
("Rg9", self.rg9),
|
||||
("Rga", self.rga),
|
||||
("Rgb", self.rgb),
|
||||
("Rgc", self.rgc),
|
||||
("Rgd", self.rgd),
|
||||
("Rge", self.rge),
|
||||
("Rgf", self.rgf),
|
||||
("Acc", self.acc),
|
||||
("Spr", self.spr),
|
||||
("Bpr", self.bpr),
|
||||
("Ret", self.ret),
|
||||
("Idr", self.idr),
|
||||
("Mmr", self.mmr),
|
||||
("Mar", self.mar),
|
||||
("Mdr", self.mdr),
|
||||
("Sts", self.sts),
|
||||
("Cir", self.cir),
|
||||
("Pcx", self.pcx),
|
||||
]
|
||||
}
|
||||
|
||||
pub const fn reset(&mut self) {
|
||||
self.rg0 = 0;
|
||||
self.rg1 = 0;
|
||||
self.rg2 = 0;
|
||||
self.rg3 = 0;
|
||||
self.rg4 = 0;
|
||||
self.rg5 = 0;
|
||||
self.rg6 = 0;
|
||||
self.rg7 = 0;
|
||||
self.rg8 = 0;
|
||||
self.rg9 = 0;
|
||||
self.rga = 0;
|
||||
self.rgb = 0;
|
||||
self.rgc = 0;
|
||||
self.rgd = 0;
|
||||
self.rge = 0;
|
||||
self.rgf = 0;
|
||||
self.acc = 0;
|
||||
self.spr = 0;
|
||||
self.bpr = 0;
|
||||
self.ret = 0;
|
||||
self.idr = 0;
|
||||
self.mmr = 0;
|
||||
self.mar = 0;
|
||||
self.mdr = 0;
|
||||
self.sts = 0;
|
||||
self.cir = 0;
|
||||
self.pcx = 0;
|
||||
}
|
||||
|
||||
pub const fn reg(&mut self, reg: Register) -> Result<&mut u32, ProcessorError> {
|
||||
Ok(match reg {
|
||||
Register::Rg0 => &mut self.rg0,
|
||||
Register::Rg1 => &mut self.rg1,
|
||||
Register::Rg2 => &mut self.rg2,
|
||||
Register::Rg3 => &mut self.rg3,
|
||||
Register::Rg4 => &mut self.rg4,
|
||||
Register::Rg5 => &mut self.rg5,
|
||||
Register::Rg6 => &mut self.rg6,
|
||||
Register::Rg7 => &mut self.rg7,
|
||||
Register::Rg8 => &mut self.rg8,
|
||||
Register::Rg9 => &mut self.rg9,
|
||||
Register::Rga => &mut self.rga,
|
||||
Register::Rgb => &mut self.rgb,
|
||||
Register::Rgc => &mut self.rgc,
|
||||
Register::Rgd => &mut self.rgd,
|
||||
Register::Rge => &mut self.rge,
|
||||
Register::Rgf => &mut self.rgf,
|
||||
Register::Acc => &mut self.acc,
|
||||
Register::Spr => &mut self.spr,
|
||||
Register::Bpr => &mut self.bpr,
|
||||
Register::Ret => &mut self.ret,
|
||||
Register::Idr => &mut self.idr,
|
||||
Register::Mmr => &mut self.mmr,
|
||||
Register::Mar => &mut self.mar,
|
||||
Register::Mdr => &mut self.mdr,
|
||||
Register::Sts => &mut self.sts,
|
||||
Register::Cir => &mut self.cir,
|
||||
Register::Pcx => &mut self.pcx,
|
||||
_ => return Err(ProcessorError::InvalidRegister(Register::NoReg as u8)),
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn get(&self, reg: Register) -> Result<u32, ProcessorError> {
|
||||
Ok(match reg {
|
||||
Register::Rg0 => self.rg0,
|
||||
Register::Rg1 => self.rg1,
|
||||
Register::Rg2 => self.rg2,
|
||||
Register::Rg3 => self.rg3,
|
||||
Register::Rg4 => self.rg4,
|
||||
Register::Rg5 => self.rg5,
|
||||
Register::Rg6 => self.rg6,
|
||||
Register::Rg7 => self.rg7,
|
||||
Register::Rg8 => self.rg8,
|
||||
Register::Rg9 => self.rg9,
|
||||
Register::Rga => self.rga,
|
||||
Register::Rgb => self.rgb,
|
||||
Register::Rgc => self.rgc,
|
||||
Register::Rgd => self.rgd,
|
||||
Register::Rge => self.rge,
|
||||
Register::Rgf => self.rgf,
|
||||
Register::Acc => self.acc,
|
||||
Register::Spr => self.spr,
|
||||
Register::Bpr => self.bpr,
|
||||
Register::Ret => self.ret,
|
||||
Register::Idr => self.idr,
|
||||
Register::Mmr => self.mmr,
|
||||
Register::Mar => self.mar,
|
||||
Register::Mdr => self.mdr,
|
||||
Register::Sts => self.sts,
|
||||
Register::Cir => self.cir,
|
||||
Register::Pcx => self.pcx,
|
||||
Register::Zero => 0,
|
||||
_ => return Err(ProcessorError::InvalidRegister(Register::NoReg as u8)),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,507 @@
|
||||
use std::{
|
||||
cmp::{max, min},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use crate::emulator::system::{
|
||||
memory::MemoryUnit,
|
||||
model::{IODevice, ProcessorError, RegFile},
|
||||
};
|
||||
|
||||
use common::instructions::{Instruction, Interrupt, Register};
|
||||
|
||||
pub struct Processor {
|
||||
pub memory: Box<dyn MemoryUnit>,
|
||||
pub registers: RegFile,
|
||||
pub halted: bool,
|
||||
pub io_devices: Vec<Arc<dyn IODevice>>,
|
||||
|
||||
pub void: u32,
|
||||
}
|
||||
|
||||
fn log(message: &str) {
|
||||
println!("\x1b[32mINFO:\x1b[0m {message}");
|
||||
}
|
||||
|
||||
impl Processor {
|
||||
#[must_use]
|
||||
pub fn new(memory: Box<dyn MemoryUnit>, io_devices: Vec<Arc<dyn IODevice>>) -> Self {
|
||||
Self {
|
||||
memory,
|
||||
registers: RegFile::default(),
|
||||
halted: false,
|
||||
io_devices,
|
||||
void: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn reset(&mut self) {
|
||||
// set all registers to zero
|
||||
// run memory.reset()
|
||||
self.registers.reset();
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.memory.reset();
|
||||
}
|
||||
|
||||
pub fn cycle(&mut self) -> Result<(u32, Instruction), ProcessorError> {
|
||||
self.halted = false;
|
||||
|
||||
// Get value from PCX.
|
||||
let addr = self.fetch()?;
|
||||
// Increment PCX.
|
||||
self.advance();
|
||||
|
||||
// Set MAR to the previous value of PCX.
|
||||
*self.reg(Register::Mar)? = addr;
|
||||
let val = self.memory.read_word(addr)?;
|
||||
|
||||
// Set CIR to the value of RAM[MAR].
|
||||
*self.reg(Register::Mar)? = val;
|
||||
|
||||
// Decode and execute the instruction.
|
||||
let instruction = Instruction::decode(val)
|
||||
.map_err(|_| ProcessorError::InvalidInstruction(val))?;
|
||||
|
||||
instruction.execute(self)?;
|
||||
Ok((addr, instruction))
|
||||
}
|
||||
|
||||
const fn fetch(&self) -> Result<u32, ProcessorError> {
|
||||
self.get(Register::Pcx)
|
||||
}
|
||||
|
||||
pub const fn get(&self, reg: Register) -> Result<u32, ProcessorError> {
|
||||
self.registers.get(reg)
|
||||
}
|
||||
|
||||
pub const fn reg(&mut self, reg: Register) -> Result<&mut u32, ProcessorError> {
|
||||
match reg {
|
||||
Register::Zero => Ok(&mut self.void),
|
||||
_ => self.registers.reg(reg),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display(&mut self) -> Result<Vec<u8>, ProcessorError> {
|
||||
self.memory.read_range(0x20000, 2000)
|
||||
}
|
||||
|
||||
pub fn cmp(&mut self, a: u32, b: u32) {
|
||||
self.set_flag(Flag::Equal, a == b);
|
||||
self.set_flag(Flag::GreaterThan, a > b);
|
||||
self.set_flag(Flag::LessThan, a < b);
|
||||
}
|
||||
|
||||
// functions to set new state
|
||||
fn set_flag(&mut self, flag: Flag, value: bool) {
|
||||
if value {
|
||||
*self
|
||||
.reg(Register::Sts)
|
||||
.expect("STS should never be invalid") |= flag as u32;
|
||||
} else {
|
||||
*self
|
||||
.reg(Register::Sts)
|
||||
.expect("STS should never be invalid") &= !(flag as u32);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_flag(&self, flag: Flag) -> Result<bool, ProcessorError> {
|
||||
Ok(self.get(Register::Sts)? & (flag as u32) != 0)
|
||||
}
|
||||
|
||||
fn advance(&mut self) -> Result<(), ProcessorError> {
|
||||
// increment PCX
|
||||
*self.reg(Register::Pcx)? += 4;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn jump(&mut self, reg: Register, offset: u16) -> Result<(), ProcessorError> {
|
||||
*self.reg(Register::Pcx)? = self.get(reg)? + u32::from(offset);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn begin_interrupt(
|
||||
&mut self,
|
||||
interrupt: Interrupt,
|
||||
) -> Result<(), ProcessorError> {
|
||||
let idt = self.get(Register::Idr)?;
|
||||
|
||||
let addr = self
|
||||
.memory
|
||||
.read_word(idt + u32::from(interrupt.as_u8()) * 4)?;
|
||||
println!("INFO: Interrupt {interrupt:?} addr: {addr}");
|
||||
|
||||
self.push(self.get(Register::Pcx)?)?;
|
||||
*self.reg(Register::Pcx)? = addr;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn push(&mut self, val: u32) -> Result<(), ProcessorError> {
|
||||
*self.reg(Register::Spr)? -= 4;
|
||||
let reg = *self.reg(Register::Spr)?;
|
||||
self.memory.write_word(reg, val)
|
||||
}
|
||||
|
||||
fn pop(&mut self) -> Result<u32, ProcessorError> {
|
||||
let reg = *self.reg(Register::Spr)?;
|
||||
let val = self.memory.read_word(reg)?;
|
||||
*self.reg(Register::Spr)? += 4;
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
// TODO: remove this once implemented
|
||||
#[allow(clippy::needless_pass_by_ref_mut)]
|
||||
fn end_interrupt(&mut self) -> Result<(), ProcessorError> {
|
||||
let ret = self.pop()?;
|
||||
*self.reg(Register::Ret)? = ret;
|
||||
*self.reg(Register::Pcx)? = ret;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_stack(&mut self, n: u32) -> Result<Vec<u8>, ProcessorError> {
|
||||
let addr = self.get(Register::Spr)?;
|
||||
let size = n * 4;
|
||||
// returns the stack
|
||||
self.memory.read_range(
|
||||
max(addr, 0), // ensures that we cannot read from a negative address
|
||||
min(size, addr), // ensures we don't read above the top of the stack
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[expect(dead_code)]
|
||||
enum Flag {
|
||||
Equal = 1,
|
||||
GreaterThan = 2,
|
||||
LessThan = 4,
|
||||
Zero = 8,
|
||||
Positive = 16,
|
||||
Negative = 32,
|
||||
Carry = 64,
|
||||
UserMode = 128,
|
||||
InterruptsEnabled = 256,
|
||||
}
|
||||
|
||||
trait Executable {
|
||||
fn execute(self, cpu: &mut Processor) -> Result<(), ProcessorError>;
|
||||
}
|
||||
|
||||
impl Executable for Instruction {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn execute(self, cpu: &mut Processor) -> Result<(), ProcessorError> {
|
||||
match self {
|
||||
// No operation - a blank line.
|
||||
// Copies from SrcReg to a.drReg.
|
||||
Self::Mov(a) => {
|
||||
*cpu.reg(a.dr)? = cpu.get(a.sr1)?;
|
||||
}
|
||||
|
||||
// Copies from SrcReg to a.drReg, sign extending the value to take up a full
|
||||
// word.
|
||||
Self::MovSigned(a) => {
|
||||
*cpu.reg(a.dr)? = sign_extend(cpu.get(a.sr1)?);
|
||||
}
|
||||
|
||||
// Loads a byte from memory address (base + offset) into a.drReg. The
|
||||
// effective address must be byte-aligned.
|
||||
Self::LoadByte(a) => {
|
||||
*cpu.reg(a.r2)? = u32::from(
|
||||
cpu.memory
|
||||
.read_byte(cpu.get(a.r1)? + u32::from(a.immediate))?,
|
||||
);
|
||||
}
|
||||
|
||||
// Loads a sign-extended byte from memory address (base + offset) into
|
||||
// a.drReg. The effective address must be byte-aligned.
|
||||
Self::LoadByteSigned(a) => {
|
||||
*cpu.reg(a.r2)? = sign_extend(u32::from(
|
||||
cpu.memory
|
||||
.read_byte(cpu.get(a.r1)? + u32::from(a.immediate))?,
|
||||
));
|
||||
}
|
||||
|
||||
// Loads a half-word from memory address (base + offset) into a.drReg. The
|
||||
// effective address must be 2-byte-aligned.
|
||||
Self::LoadHalfword(a) => {
|
||||
// we read an entire word, then right shift so we only get the first half
|
||||
// of the word
|
||||
*cpu.reg(a.r2)? = cpu
|
||||
.memory
|
||||
.read_word(cpu.get(a.r1)? + u32::from(a.immediate))?
|
||||
>> 16;
|
||||
}
|
||||
|
||||
// Loads a sign-extended half-word from memory address (base + offset) into
|
||||
// a.drReg. The effective address must be 2-byte-aligned.
|
||||
Self::LoadHalfwordSigned(a) => {
|
||||
*cpu.reg(a.r2)? = sign_extend(
|
||||
cpu.memory
|
||||
.read_word(cpu.get(a.r1)? + u32::from(a.immediate))?
|
||||
>> 16,
|
||||
);
|
||||
}
|
||||
|
||||
// Loads a word from memory address (base + offset) into a.drReg. The
|
||||
// effective address must be 4-byte-aligned.
|
||||
Self::LoadWord(a) => {
|
||||
*cpu.reg(a.r2)? = cpu
|
||||
.memory
|
||||
.read_word(cpu.get(a.r1)? + u32::from(a.immediate))?;
|
||||
}
|
||||
|
||||
// Stores a byte from SrcReg in memory address (base + offset) The effective
|
||||
// address must be byte-aligned.
|
||||
Self::StoreByte(a) => {
|
||||
cpu.memory.write_byte(
|
||||
cpu.get(a.r2)? + u32::from(a.immediate),
|
||||
cpu.get(a.r1)? as u8,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Stores a half-word from SrcReg in memory address (base + offset) The
|
||||
// effective address must be 2-byte-aligned.
|
||||
Self::StoreHalfword(a) => {
|
||||
// split the value into bytes and then write two bytes
|
||||
let bytes = (cpu.get(a.r1)? as u16).to_le_bytes();
|
||||
cpu.memory
|
||||
.write_byte(cpu.get(a.r2)? + u32::from(a.immediate), bytes[0])?;
|
||||
cpu.memory
|
||||
.write_byte(cpu.get(a.r2)? + u32::from(a.immediate) + 1, bytes[1])?;
|
||||
}
|
||||
|
||||
// Stores a word from SrcReg in memory address (base + offset) The effective
|
||||
// address must be 4-byte-aligned.
|
||||
Self::StoreWord(a) => {
|
||||
cpu.memory.write_word(
|
||||
cpu.get(a.r2)? + u32::from(a.immediate),
|
||||
cpu.get(a.r1)?,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Loads a 16-bit literal value into reg, setting the bottom 16 bits of the
|
||||
// word. To populate the upper 16 bits, see LUI.
|
||||
Self::LoadLowerImmediate(a) => {
|
||||
*cpu.reg(a.r1)? = u32::from(a.immediate);
|
||||
}
|
||||
|
||||
// Loads a 16-bit literal value into reg, setting the top 16 bits of the word.
|
||||
// To populate the lower 16 bits, see LLI.
|
||||
Self::LoadUpperImmediate(a) => {
|
||||
*cpu.reg(a.r1)? =
|
||||
(cpu.get(a.r1)? & 0x0000_FFFF) | (u32::from(a.immediate) << 16);
|
||||
}
|
||||
|
||||
// Unconditionally jumps to the calculated address or direct address
|
||||
Self::Jump(a) => cpu.jump(a.r1, a.immediate)?,
|
||||
|
||||
// Jumps to the calculated address or direct address if equal flag set.
|
||||
Self::JumpEq(a) => {
|
||||
if cpu.get_flag(Flag::Equal)? {
|
||||
cpu.jump(a.r1, a.immediate)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Jumps to the calculated address or direct address if equal flag not set.
|
||||
Self::JumpNeq(a) => {
|
||||
if !cpu.get_flag(Flag::Equal)? {
|
||||
cpu.jump(a.r1, a.immediate)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Jumps to the calculated address or direct address if greater than flag set.
|
||||
Self::JumpGt(a) => {
|
||||
if cpu.get_flag(Flag::GreaterThan)? {
|
||||
cpu.jump(a.r1, a.immediate)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Jumps to the calculated address or direct address if greater than flag or
|
||||
// equal flag set.
|
||||
Self::JumpGe(a) => {
|
||||
if cpu.get_flag(Flag::GreaterThan)? || cpu.get_flag(Flag::Equal)? {
|
||||
cpu.jump(a.r1, a.immediate)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Jumps to the calculated address or direct address if less than flag set.
|
||||
Self::JumpLt(a) => {
|
||||
if cpu.get_flag(Flag::LessThan)? {
|
||||
cpu.jump(a.r1, a.immediate)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Jumps to the calculated address or direct address if less than flag or
|
||||
// equal flag set.
|
||||
Self::JumpLe(a) => {
|
||||
if cpu.get_flag(Flag::LessThan)? || cpu.get_flag(Flag::Equal)? {
|
||||
cpu.jump(a.r1, a.immediate)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Increments the value in the given register
|
||||
Self::Increment(a) => *cpu.reg(a.sr1)? = inc(cpu.get(a.sr1)?),
|
||||
|
||||
// Decrements the value in the given register
|
||||
Self::Decrement(a) => *cpu.reg(a.sr1)? = dec(cpu.get(a.sr1)?),
|
||||
|
||||
// Left shifts the value in Reg by the given amount (either a register, or a
|
||||
// literal value)
|
||||
Self::ShiftLeft(a) => {
|
||||
let reg = cpu.get(a.sr1)?;
|
||||
let val = a.shamt;
|
||||
*cpu.reg(a.sr1)? = shl(reg, val);
|
||||
}
|
||||
|
||||
// Right shifts the value in Reg by the given amount (either a register, or a
|
||||
// literal value).
|
||||
Self::ShiftRight(a) => {
|
||||
let regval = cpu.get(a.sr1)?;
|
||||
let val = a.shamt;
|
||||
*cpu.reg(a.sr1)? = shr(regval, val);
|
||||
}
|
||||
|
||||
// Adds the value of Src2 to Src1 and writes the result to a.dr
|
||||
Self::Add(a) => {
|
||||
*cpu.reg(a.dr)? = add(cpu.get(a.sr1)?, cpu.get(a.sr2)?);
|
||||
}
|
||||
|
||||
// Subtracts the value of Src2 from Src1 and writes the result to a.dr
|
||||
Self::Sub(a) => {
|
||||
*cpu.reg(a.dr)? = sub(cpu.get(a.sr1)?, cpu.get(a.sr2)?);
|
||||
}
|
||||
|
||||
Self::AddImmediate(a) => {
|
||||
*cpu.reg(a.r2)? = add(cpu.get(a.r1)?, u32::from(a.immediate));
|
||||
}
|
||||
|
||||
Self::SubImmediate(a) => {
|
||||
*cpu.reg(a.r2)? = sub(cpu.get(a.r1)?, u32::from(a.immediate));
|
||||
}
|
||||
|
||||
// Performs bitwise AND on Src1 and Src2 storing the result in a.dr
|
||||
Self::And(a) => *cpu.reg(a.dr)? = and(cpu.get(a.sr1)?, cpu.get(a.sr2)?),
|
||||
|
||||
// Performs bitwise OR on Src1 and Src2 storing the result in a.dr
|
||||
Self::Or(a) => *cpu.reg(a.dr)? = or(cpu.get(a.sr1)?, cpu.get(a.sr2)?),
|
||||
|
||||
// Performs bitwise NOT on Src storing the result in a.dr
|
||||
Self::Not(a) => *cpu.reg(a.dr)? = not(cpu.get(a.sr1)?),
|
||||
|
||||
// Performs bitwise XOR on Src1 and Src2 storing the result in a.dr
|
||||
Self::Xor(a) => *cpu.reg(a.dr)? = xor(cpu.get(a.sr1)?, cpu.get(a.sr2)?),
|
||||
|
||||
// Performs bitwise NAND on Src1 and Src2 storing the result in a.dr
|
||||
Self::Nand(a) => *cpu.reg(a.dr)? = nand(cpu.get(a.sr1)?, cpu.get(a.sr2)?),
|
||||
|
||||
// Performs bitwise NOR on Src1 and Src2 storing the result in a.dr
|
||||
Self::Nor(a) => *cpu.reg(a.dr)? = nor(cpu.get(a.sr1)?, cpu.get(a.sr2)?),
|
||||
|
||||
// Performs bitwise XNOR on Src1 and Src2 storing the result in a.dr
|
||||
Self::Xnor(a) => *cpu.reg(a.dr)? = xnor(cpu.get(a.sr1)?, cpu.get(a.sr2)?),
|
||||
|
||||
// Compares the value of Reg1 to the value in Reg2. The results of the
|
||||
// comparisons are set in the Status register.
|
||||
Self::Compare(a) => {
|
||||
cpu.cmp(cpu.get(a.sr1)?, cpu.get(a.sr2)?);
|
||||
}
|
||||
|
||||
// Initiates an interrupt with the given 8 bit interrupt code.
|
||||
// Triggering an interrupt invokes the following behaviour:
|
||||
// - The return address is saved to the RET register.
|
||||
// - The stack base ptr is set to the kernel stack.
|
||||
Self::Interrupt(interrupt_code) => {
|
||||
cpu.begin_interrupt(interrupt_code)?;
|
||||
}
|
||||
|
||||
// Returns from an interrupt,
|
||||
Self::IntReturn => {
|
||||
cpu.end_interrupt()?;
|
||||
}
|
||||
|
||||
// Halts the processor.
|
||||
Self::Halt => {
|
||||
cpu.halted = true;
|
||||
}
|
||||
|
||||
Self::Segment(_) | Self::Nop | Self::Data(_) => {}
|
||||
_ => {
|
||||
eprintln!("WARN: unimplemented instruction: {self}");
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// mathematical and logical functions & other operations
|
||||
const fn add(a: u32, b: u32) -> u32 {
|
||||
a.wrapping_add(b)
|
||||
}
|
||||
|
||||
const fn sub(a: u32, b: u32) -> u32 {
|
||||
a.wrapping_sub(b)
|
||||
}
|
||||
|
||||
const fn and(a: u32, b: u32) -> u32 {
|
||||
a & b
|
||||
}
|
||||
|
||||
const fn inc(a: u32) -> u32 {
|
||||
a.wrapping_add(1)
|
||||
}
|
||||
|
||||
const fn dec(a: u32) -> u32 {
|
||||
a.wrapping_sub(1)
|
||||
}
|
||||
|
||||
const fn shl(a: u32, amount: u8) -> u32 {
|
||||
a << amount
|
||||
}
|
||||
|
||||
const fn shr(a: u32, amount: u8) -> u32 {
|
||||
a >> amount
|
||||
}
|
||||
|
||||
const fn or(a: u32, b: u32) -> u32 {
|
||||
a | b
|
||||
}
|
||||
|
||||
const fn not(a: u32) -> u32 {
|
||||
!a
|
||||
}
|
||||
|
||||
const fn xor(a: u32, b: u32) -> u32 {
|
||||
a ^ b
|
||||
}
|
||||
|
||||
const fn nand(a: u32, b: u32) -> u32 {
|
||||
!(a & b)
|
||||
}
|
||||
|
||||
const fn nor(a: u32, b: u32) -> u32 {
|
||||
!(a | b)
|
||||
}
|
||||
|
||||
const fn xnor(a: u32, b: u32) -> u32 {
|
||||
!(a ^ b)
|
||||
}
|
||||
|
||||
const fn sign_extend(val: u32) -> u32 {
|
||||
let (mask, sign_bit): (u32, u8) = match val {
|
||||
0..=0xFF => (0xFFFF_FF00, 7),
|
||||
// I presume this was the intended behaviour?
|
||||
0x100..=0xFFFF => (0xFFFF_0000, 15),
|
||||
_ => (0x0000_0000, 31),
|
||||
};
|
||||
|
||||
if val & (1 << sign_bit) != 0 {
|
||||
val | mask
|
||||
} else {
|
||||
val
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,695 @@
|
||||
use super::*;
|
||||
use crate::emulator::system::memory::*;
|
||||
|
||||
use common::prelude::*;
|
||||
|
||||
fn create_test_processor() -> Processor {
|
||||
let memory = Box::new(MainStore::new());
|
||||
Processor::new(memory, Vec::new())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nop_instruction() {
|
||||
let mut cpu = create_test_processor();
|
||||
let initial_state = cpu.registers;
|
||||
|
||||
Instruction::Nop.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
cpu.registers
|
||||
.get(Register::Rg0)
|
||||
.expect("Failed to get register Rg0"),
|
||||
initial_state
|
||||
.get(Register::Rg0)
|
||||
.expect("Failed to get register Rg0")
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.registers
|
||||
.get(Register::Acc)
|
||||
.expect("Failed to get register Acc"),
|
||||
initial_state
|
||||
.get(Register::Acc)
|
||||
.expect("Failed to get register Acc")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mov_instruction() {
|
||||
let mut cpu = create_test_processor();
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 0x1234_5678;
|
||||
|
||||
let mov_instr = Instruction::Mov(RTypeArgs::new(
|
||||
Some(Register::Rg1),
|
||||
None,
|
||||
Some(Register::Rg2),
|
||||
None,
|
||||
));
|
||||
|
||||
mov_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Rg2).expect("Failed to get register Rg2"),
|
||||
0x1234_5678
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mov_signed_instruction() {
|
||||
let mut cpu = create_test_processor();
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 0x0000_00FF;
|
||||
|
||||
let mov_signed_instr = Instruction::MovSigned(RTypeArgs::new(
|
||||
Some(Register::Rg1),
|
||||
None,
|
||||
Some(Register::Rg2),
|
||||
None,
|
||||
));
|
||||
|
||||
mov_signed_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Rg2).expect("Failed to get register Rg2"),
|
||||
0xFFFF_FFFF
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_byte_instruction() {
|
||||
let mut cpu = create_test_processor();
|
||||
let addr = 0x100;
|
||||
cpu.memory
|
||||
.write_byte(addr, 0xAB)
|
||||
.expect("Failed to write byte to memory");
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = addr - 4;
|
||||
|
||||
let load_byte_instr = Instruction::LoadByte(ITypeArgs::new(
|
||||
4,
|
||||
Some(Register::Rg1),
|
||||
Some(Register::Rg2),
|
||||
));
|
||||
|
||||
load_byte_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Rg2).expect("Failed to get register Rg2"),
|
||||
0x0000_00AB
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_byte_signed_instruction() {
|
||||
let mut cpu = create_test_processor();
|
||||
let addr = 0x100;
|
||||
cpu.memory
|
||||
.write_byte(addr, 0xFF)
|
||||
.expect("Failed to write byte to memory");
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = addr;
|
||||
|
||||
let load_byte_signed_instr = Instruction::LoadByteSigned(ITypeArgs::new(
|
||||
0,
|
||||
Some(Register::Rg1),
|
||||
Some(Register::Rg2),
|
||||
));
|
||||
|
||||
load_byte_signed_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Rg2).expect("Failed to get register Rg2"),
|
||||
0xFFFF_FFFF
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_halfword_instruction() {
|
||||
let mut cpu = create_test_processor();
|
||||
let addr = 0x100;
|
||||
cpu.memory
|
||||
.write_word(addr, 0x1234_5678)
|
||||
.expect("Failed to write word to memory");
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = addr;
|
||||
|
||||
let load_halfword_instr = Instruction::LoadHalfword(ITypeArgs::new(
|
||||
0,
|
||||
Some(Register::Rg1),
|
||||
Some(Register::Rg2),
|
||||
));
|
||||
|
||||
load_halfword_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Rg2).expect("Failed to get register Rg2"),
|
||||
0x0000_1234
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_word_instruction() {
|
||||
let mut cpu = create_test_processor();
|
||||
let addr = 0x100;
|
||||
cpu.memory
|
||||
.write_word(addr, 0x1234_5678)
|
||||
.expect("Failed to write word to memory");
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = addr;
|
||||
|
||||
let load_word_instr = Instruction::LoadWord(ITypeArgs::new(
|
||||
0,
|
||||
Some(Register::Rg1),
|
||||
Some(Register::Rg2),
|
||||
));
|
||||
|
||||
load_word_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Rg2).expect("Failed to get register Rg2"),
|
||||
0x1234_5678
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_store_byte_instruction() {
|
||||
let mut cpu = create_test_processor();
|
||||
let addr = 0x100;
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = addr;
|
||||
*cpu.reg(Register::Rg2).expect("Failed to get register Rg2") = 0xAB;
|
||||
|
||||
let store_byte_instr = Instruction::StoreByte(ITypeArgs::new(
|
||||
0,
|
||||
Some(Register::Rg2),
|
||||
Some(Register::Rg1),
|
||||
));
|
||||
|
||||
store_byte_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(cpu.memory.read_byte(addr).expect("Emulator was slain by losing the game while attempting to execute instruction"), 0xAB);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_store_word_instruction() {
|
||||
let mut cpu = create_test_processor();
|
||||
let addr = 0x100;
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = addr;
|
||||
*cpu.reg(Register::Rg2).expect("Failed to get register Rg2") = 0x1234_5678;
|
||||
|
||||
let store_word_instr = Instruction::StoreWord(ITypeArgs::new(
|
||||
0,
|
||||
Some(Register::Rg2),
|
||||
Some(Register::Rg1),
|
||||
));
|
||||
|
||||
store_word_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(cpu.memory.read_word(addr).expect("Emulator was slain by losing the game while attempting to execute instruction"), 0x1234_5678);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_instruction() {
|
||||
let mut cpu = create_test_processor();
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 15;
|
||||
*cpu.reg(Register::Rg2).expect("Failed to get register Rg2") = 25;
|
||||
|
||||
let add_instr = Instruction::Add(RTypeArgs::new(
|
||||
Some(Register::Rg1),
|
||||
Some(Register::Rg2),
|
||||
Some(Register::Rg3),
|
||||
None,
|
||||
));
|
||||
|
||||
add_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Rg3).expect("Failed to get register Rg3"),
|
||||
40
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sub_instruction() {
|
||||
let mut cpu = create_test_processor();
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 50;
|
||||
*cpu.reg(Register::Rg2).expect("Failed to get register Rg2") = 20;
|
||||
|
||||
let sub_instr = Instruction::Sub(RTypeArgs::new(
|
||||
Some(Register::Rg1),
|
||||
Some(Register::Rg2),
|
||||
Some(Register::Rg3),
|
||||
None,
|
||||
));
|
||||
|
||||
sub_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Rg3).expect("Failed to get register Rg3"),
|
||||
30
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_and_instruction() {
|
||||
let mut cpu = create_test_processor();
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 0b1100;
|
||||
*cpu.reg(Register::Rg2).expect("Failed to get register Rg2") = 0b1010;
|
||||
|
||||
let and_instr = Instruction::And(RTypeArgs::new(
|
||||
Some(Register::Rg1),
|
||||
Some(Register::Rg2),
|
||||
Some(Register::Rg3),
|
||||
None,
|
||||
));
|
||||
|
||||
and_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Rg3).expect("Failed to get register Rg3"),
|
||||
0b1000
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_or_instruction() {
|
||||
let mut cpu = create_test_processor();
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 0b1100;
|
||||
*cpu.reg(Register::Rg2).expect("Failed to get register Rg2") = 0b1010;
|
||||
|
||||
let or_instr = Instruction::Or(RTypeArgs::new(
|
||||
Some(Register::Rg1),
|
||||
Some(Register::Rg2),
|
||||
Some(Register::Rg3),
|
||||
None,
|
||||
));
|
||||
|
||||
or_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Rg3).expect("Failed to get register Rg3"),
|
||||
0b1110
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xor_instruction() {
|
||||
let mut cpu = create_test_processor();
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 0b1100;
|
||||
*cpu.reg(Register::Rg2).expect("Failed to get register Rg2") = 0b1010;
|
||||
|
||||
let xor_instr = Instruction::Xor(RTypeArgs::new(
|
||||
Some(Register::Rg1),
|
||||
Some(Register::Rg2),
|
||||
Some(Register::Rg3),
|
||||
None,
|
||||
));
|
||||
|
||||
xor_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Rg3).expect("Failed to get register Rg3"),
|
||||
0b0110
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_instruction() {
|
||||
let mut cpu = create_test_processor();
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 0x0F0F_0F0F;
|
||||
|
||||
let not_instr = Instruction::Not(RTypeArgs::new(
|
||||
Some(Register::Rg1),
|
||||
None,
|
||||
Some(Register::Rg2),
|
||||
None,
|
||||
));
|
||||
|
||||
not_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Rg2).expect("Failed to get register Rg2"),
|
||||
0xF0F0_F0F0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compare_equal() {
|
||||
let mut cpu = create_test_processor();
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 42;
|
||||
*cpu.reg(Register::Rg2).expect("Failed to get register Rg2") = 42;
|
||||
|
||||
let cmp_instr = Instruction::Compare(RTypeArgs::new(
|
||||
Some(Register::Rg1),
|
||||
Some(Register::Rg2),
|
||||
None,
|
||||
None,
|
||||
));
|
||||
|
||||
cmp_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
|
||||
assert!(cpu.get_flag(Flag::Equal).expect("Failed to get flag Equal"));
|
||||
assert!(
|
||||
!cpu.get_flag(Flag::GreaterThan)
|
||||
.expect("Failed to get flag GreaterThan")
|
||||
);
|
||||
assert!(
|
||||
!cpu.get_flag(Flag::LessThan)
|
||||
.expect("Failed to get flag LessThan")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compare_greater_than() {
|
||||
let mut cpu = create_test_processor();
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 50;
|
||||
*cpu.reg(Register::Rg2).expect("Failed to get register Rg2") = 30;
|
||||
|
||||
let cmp_instr = Instruction::Compare(RTypeArgs::new(
|
||||
Some(Register::Rg1),
|
||||
Some(Register::Rg2),
|
||||
None,
|
||||
None,
|
||||
));
|
||||
|
||||
cmp_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
|
||||
assert!(!cpu.get_flag(Flag::Equal).expect("Failed to get flag Equal"));
|
||||
assert!(
|
||||
cpu.get_flag(Flag::GreaterThan)
|
||||
.expect("Failed to get flag GreaterThan")
|
||||
);
|
||||
assert!(
|
||||
!cpu.get_flag(Flag::LessThan)
|
||||
.expect("Failed to get flag LessThan")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compare_less_than() {
|
||||
let mut cpu = create_test_processor();
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 20;
|
||||
*cpu.reg(Register::Rg2).expect("Failed to get register Rg2") = 30;
|
||||
|
||||
let cmp_instr = Instruction::Compare(RTypeArgs::new(
|
||||
Some(Register::Rg1),
|
||||
Some(Register::Rg2),
|
||||
None,
|
||||
None,
|
||||
));
|
||||
|
||||
cmp_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
|
||||
assert!(!cpu.get_flag(Flag::Equal).expect("Failed to get flag Equal"));
|
||||
assert!(
|
||||
!cpu.get_flag(Flag::GreaterThan)
|
||||
.expect("Failed to get flag GreaterThan")
|
||||
);
|
||||
assert!(
|
||||
cpu.get_flag(Flag::LessThan)
|
||||
.expect("Failed to get flag LessThan")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_increment_instruction() {
|
||||
let mut cpu = create_test_processor();
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 42;
|
||||
|
||||
let inc_instr =
|
||||
Instruction::Increment(RTypeArgs::new(Some(Register::Rg1), None, None, None));
|
||||
|
||||
inc_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Rg1).expect("Failed to get register Rg1"),
|
||||
43
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrement_instruction() {
|
||||
let mut cpu = create_test_processor();
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 42;
|
||||
|
||||
let dec_instr =
|
||||
Instruction::Decrement(RTypeArgs::new(Some(Register::Rg1), None, None, None));
|
||||
|
||||
dec_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Rg1).expect("Failed to get register Rg1"),
|
||||
41
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shift_left_with_shamt() {
|
||||
let mut cpu = create_test_processor();
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 0b1010;
|
||||
|
||||
let shl_instr = Instruction::ShiftLeft(RTypeArgs::new(
|
||||
Some(Register::Rg1),
|
||||
Some(Register::Zero),
|
||||
None,
|
||||
Some(2),
|
||||
));
|
||||
|
||||
shl_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Rg1).expect("Failed to get register Rg1"),
|
||||
0b10_1000
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shift_right_with_shamt() {
|
||||
let mut cpu = create_test_processor();
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 0b10_1000;
|
||||
|
||||
let shr_instr = Instruction::ShiftRight(RTypeArgs::new(
|
||||
Some(Register::Rg1),
|
||||
Some(Register::Zero),
|
||||
None,
|
||||
Some(2),
|
||||
));
|
||||
|
||||
shr_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Rg1).expect("Failed to get register Rg1"),
|
||||
0b1010
|
||||
);
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn test_shift_left_with_register() {
|
||||
// let mut cpu = create_test_processor();
|
||||
// *cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 0b1010;
|
||||
|
||||
// let shl_instr =
|
||||
// Instruction::ShiftLeft(RTypeArgs::new(Some(Register::Rg1), None, None,
|
||||
// Some(3)));
|
||||
|
||||
// shl_instr.execute(&mut cpu).expect(
|
||||
// "Emulator was slain by losing the game while attempting to execute
|
||||
// instruction", );
|
||||
// assert_eq!(
|
||||
// cpu.get(Register::Rg1).expect("Failed to get register Rg1"),
|
||||
// 0b101_0000
|
||||
// );
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn test_load_lower_immediate() {
|
||||
let mut cpu = create_test_processor();
|
||||
|
||||
let lli_instr = Instruction::LoadLowerImmediate(ITypeArgs::new(
|
||||
0x1234,
|
||||
Some(Register::Rg1),
|
||||
None,
|
||||
));
|
||||
|
||||
lli_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Rg1).expect("Failed to get register Rg1"),
|
||||
0x0000_1234
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_upper_immediate() {
|
||||
let mut cpu = create_test_processor();
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 0x0000_5678;
|
||||
|
||||
let lui_instr = Instruction::LoadUpperImmediate(ITypeArgs::new(
|
||||
0x1234,
|
||||
Some(Register::Rg1),
|
||||
None,
|
||||
));
|
||||
|
||||
lui_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Rg1).expect("Failed to get register Rg1"),
|
||||
0x1234_5678
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_jump_unconditional() {
|
||||
let mut cpu = create_test_processor();
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 0x1000;
|
||||
let initial_pc = cpu.get(Register::Pcx).expect("Failed to get register Pcx");
|
||||
|
||||
let jump_instr = Instruction::Jump(ITypeArgs::new(0x100, Some(Register::Rg1), None));
|
||||
|
||||
jump_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Pcx).expect("Failed to get register Pcx"),
|
||||
0x1100
|
||||
);
|
||||
assert_ne!(
|
||||
cpu.get(Register::Pcx).expect("Failed to get register Pcx"),
|
||||
initial_pc
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_jump_equal_when_flag_set() {
|
||||
let mut cpu = create_test_processor();
|
||||
cpu.set_flag(Flag::Equal, true);
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 0x1000;
|
||||
|
||||
let jump_eq_instr =
|
||||
Instruction::JumpEq(ITypeArgs::new(0x100, Some(Register::Rg1), None));
|
||||
|
||||
jump_eq_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Pcx).expect("Failed to get register Pcx"),
|
||||
0x1100
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_jump_equal_when_flag_not_set() {
|
||||
let mut cpu = create_test_processor();
|
||||
cpu.set_flag(Flag::Equal, false);
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 0x1000;
|
||||
let initial_pc = cpu.get(Register::Pcx).expect("Failed to get register Pcx");
|
||||
|
||||
let jump_eq_instr =
|
||||
Instruction::JumpEq(ITypeArgs::new(0x100, Some(Register::Rg1), None));
|
||||
|
||||
jump_eq_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Pcx).expect("Failed to get register Pcx"),
|
||||
initial_pc
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_halt_instruction() {
|
||||
let mut cpu = create_test_processor();
|
||||
assert!(!cpu.halted);
|
||||
|
||||
Instruction::Halt.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert!(cpu.halted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nand_instruction() {
|
||||
let mut cpu = create_test_processor();
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 0b1100;
|
||||
*cpu.reg(Register::Rg2).expect("Failed to get register Rg2") = 0b1010;
|
||||
|
||||
let nand_instr = Instruction::Nand(RTypeArgs::new(
|
||||
Some(Register::Rg1),
|
||||
Some(Register::Rg2),
|
||||
Some(Register::Rg3),
|
||||
None,
|
||||
));
|
||||
|
||||
nand_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Rg3).expect("Failed to get register Rg3"),
|
||||
!0b1000
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nor_instruction() {
|
||||
let mut cpu = create_test_processor();
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 0b1100;
|
||||
*cpu.reg(Register::Rg2).expect("Failed to get register Rg2") = 0b1010;
|
||||
|
||||
let nor_instr = Instruction::Nor(RTypeArgs::new(
|
||||
Some(Register::Rg1),
|
||||
Some(Register::Rg2),
|
||||
Some(Register::Rg3),
|
||||
None,
|
||||
));
|
||||
|
||||
nor_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Rg3).expect("Failed to get register Rg3"),
|
||||
!0b1110
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xnor_instruction() {
|
||||
let mut cpu = create_test_processor();
|
||||
*cpu.reg(Register::Rg1).expect("Failed to get register Rg1") = 0b1100;
|
||||
*cpu.reg(Register::Rg2).expect("Failed to get register Rg2") = 0b1010;
|
||||
|
||||
let xnor_instr = Instruction::Xnor(RTypeArgs::new(
|
||||
Some(Register::Rg1),
|
||||
Some(Register::Rg2),
|
||||
Some(Register::Rg3),
|
||||
None,
|
||||
));
|
||||
|
||||
xnor_instr.execute(&mut cpu).expect(
|
||||
"Emulator was slain by losing the game while attempting to execute instruction",
|
||||
);
|
||||
assert_eq!(
|
||||
cpu.get(Register::Rg3).expect("Failed to get register Rg3"),
|
||||
!0b0110
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
use crate::emulator::{
|
||||
system::model::{Command, Running, State},
|
||||
ui::interface::Component,
|
||||
};
|
||||
|
||||
use common::{instructions::Register, prelude::Instruction};
|
||||
|
||||
pub struct ControlPanel {
|
||||
visible: bool,
|
||||
step_amount_input: String,
|
||||
step_amount: usize,
|
||||
}
|
||||
|
||||
impl ControlPanel {
|
||||
#[allow(clippy::must_use_candidate)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
visible: false,
|
||||
step_amount_input: String::from("1"),
|
||||
step_amount: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ControlPanel {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ControlPanel {
|
||||
fn category(&self) -> super::interface::Category {
|
||||
super::interface::Category::Control
|
||||
}
|
||||
|
||||
fn visible(&mut self) -> &mut bool {
|
||||
&mut self.visible
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Control Panel"
|
||||
}
|
||||
|
||||
fn render(&mut self, state: &mut State, ui: &mut egui::Ui, ctx: &egui::Context) {
|
||||
ui.horizontal(|ui| {
|
||||
// Pause / Run
|
||||
if ui
|
||||
.button(if state.running == Running::Running {
|
||||
"Pause"
|
||||
} else {
|
||||
"Run"
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
if state.running == Running::Running {
|
||||
state.cmd_sender.send(Command::Stop).unwrap_or_else(|_| {
|
||||
state.error_log.push("Failed to send command".to_string());
|
||||
});
|
||||
} else {
|
||||
state.cmd_sender.send(Command::Start).unwrap_or_else(|_| {
|
||||
state.error_log.push("Failed to send command".to_string());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
state
|
||||
.cmd_sender
|
||||
.send(Command::Reset(0))
|
||||
.unwrap_or_else(|_| {
|
||||
state.error_log.push("Failed to send command".to_string());
|
||||
});
|
||||
}
|
||||
|
||||
// Resets the emulator and all attached devices
|
||||
if ui.button("Clear Registers").clicked() {
|
||||
state
|
||||
.cmd_sender
|
||||
.send(Command::Reset(1))
|
||||
.unwrap_or_else(|_| {
|
||||
state.error_log.push("Failed to send command".to_string());
|
||||
});
|
||||
}
|
||||
|
||||
// Resets the emulator and all attached devices
|
||||
if ui.button("Clear RAM").clicked() {
|
||||
state
|
||||
.cmd_sender
|
||||
.send(Command::Reset(2))
|
||||
.unwrap_or_else(|_| {
|
||||
state.error_log.push("Failed to send command".to_string());
|
||||
});
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
state.send(Command::RegisterRequest);
|
||||
state.send(Command::RunningRequest);
|
||||
state.send(Command::InstructionCountRequest);
|
||||
|
||||
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.running {
|
||||
Running::Running => "Running",
|
||||
Running::Paused => "Paused",
|
||||
Running::Halted => "Halted",
|
||||
}
|
||||
));
|
||||
|
||||
let pcx = state
|
||||
.reg_file
|
||||
.get(Register::Pcx)
|
||||
.expect("PCX should never be invalid");
|
||||
let instructions = state.instructions;
|
||||
|
||||
ui.label(format!("Instructions: {instructions}"));
|
||||
ui.label(format!("PC: 0x{pcx:08X}"));
|
||||
|
||||
let instruction = Instruction::decode(
|
||||
state
|
||||
.reg_file
|
||||
.get(Register::Cir)
|
||||
.expect("CIR should never be invalid"),
|
||||
)
|
||||
.map_or_else(
|
||||
|_| "Invalid Instruction".to_string(),
|
||||
|instruction| instruction.to_string(),
|
||||
);
|
||||
|
||||
ui.label(format!("Instruction: {instruction}"));
|
||||
});
|
||||
|
||||
render_register_table(state, ui, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_register_table(state: &State, ui: &mut egui::Ui, _ctx: &egui::Context) {
|
||||
// Left column - Registers
|
||||
ui.vertical(|ui| {
|
||||
ui.heading("Registers");
|
||||
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt("register_inspector_scroll")
|
||||
.show(ui, |ui| {
|
||||
egui::Grid::new("registers_grid")
|
||||
.num_columns(8)
|
||||
.spacing([40.0, 4.0])
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
ui.label("Register");
|
||||
ui.label("Value");
|
||||
ui.label("Register");
|
||||
ui.label("Value");
|
||||
ui.label("Register");
|
||||
ui.label("Value");
|
||||
ui.label("Register");
|
||||
ui.label("Value");
|
||||
ui.end_row();
|
||||
|
||||
// iterate over state.reg_file.iter() in chunks of 4 registers
|
||||
for chunk in state.reg_file.all().chunks(4) {
|
||||
for reg in chunk {
|
||||
ui.label(reg.0.to_string());
|
||||
ui.label(format!("0x{:08X} ({})", reg.1, reg.1,));
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
use crate::emulator::{
|
||||
system::model::{Command, State},
|
||||
ui::interface::{Category, Component},
|
||||
};
|
||||
|
||||
use eframe::egui;
|
||||
use egui::{Color32, FontId, Vec2};
|
||||
|
||||
const VGA_WIDTH: usize = 80;
|
||||
const VGA_HEIGHT: usize = 25;
|
||||
|
||||
pub struct Display {
|
||||
visible: bool,
|
||||
}
|
||||
|
||||
impl Display {
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self { visible: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Display {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Display {
|
||||
fn name(&self) -> &'static str {
|
||||
"Display"
|
||||
}
|
||||
|
||||
fn category(&self) -> Category {
|
||||
Category::IO
|
||||
}
|
||||
|
||||
fn visible(&mut self) -> &mut bool {
|
||||
&mut self.visible
|
||||
}
|
||||
|
||||
fn render(&mut self, state: &mut State, ui: &mut egui::Ui, _ctx: &egui::Context) {
|
||||
state.send(Command::DisplayRequest);
|
||||
|
||||
let display: Vec<u8> = state.display_view.clone();
|
||||
let font_id = FontId::monospace(12.0);
|
||||
|
||||
let char_width = ui.fonts(|f| f.glyph_width(&font_id, 'W'));
|
||||
let line_height = ui.fonts(|f| f.row_height(&font_id));
|
||||
|
||||
#[expect(clippy::cast_precision_loss)]
|
||||
let display_size = Vec2::new(
|
||||
char_width * VGA_WIDTH as f32,
|
||||
line_height * VGA_HEIGHT as f32,
|
||||
);
|
||||
|
||||
let (rect, _response) = ui.allocate_exact_size(display_size, egui::Sense::all());
|
||||
|
||||
// Fill background
|
||||
// ui.painter().rect_filled(rect, 0.0, Color32::BLACK);
|
||||
|
||||
// Draw text
|
||||
for y in 0..VGA_HEIGHT {
|
||||
let mut row_text = String::with_capacity(VGA_WIDTH);
|
||||
for x in 0..VGA_WIDTH {
|
||||
let index = y * VGA_WIDTH + x;
|
||||
if index < display.len() {
|
||||
let byte = display[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,545 @@
|
||||
use std::fmt::Write;
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use common::prelude::Instruction;
|
||||
use egui::{Align, Context, Key, Layout, Ui};
|
||||
|
||||
use dsa_editor::{CodeEditor, ColorTheme, Syntax};
|
||||
use egui_file::FileDialog;
|
||||
|
||||
use crate::emulator::{
|
||||
system::model::{Command, State},
|
||||
ui::interface::Component,
|
||||
};
|
||||
|
||||
use assembler::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Editor {
|
||||
// editor state
|
||||
path: Option<PathBuf>,
|
||||
unsaved: bool,
|
||||
text: String,
|
||||
buffer: String,
|
||||
|
||||
// output / loading
|
||||
output: Vec<u8>,
|
||||
load_offset: u32,
|
||||
offset_str: String,
|
||||
|
||||
// cursor - currently unused
|
||||
cursor_col: usize,
|
||||
cursor_line: usize,
|
||||
|
||||
// file dialogs
|
||||
open_file_dialog: Option<FileDialog>,
|
||||
save_file_dialog: Option<FileDialog>,
|
||||
|
||||
// other
|
||||
visible: bool,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
impl Component for Editor {
|
||||
fn name(&self) -> &'static str {
|
||||
"Editor"
|
||||
}
|
||||
|
||||
fn visible(&mut self) -> &mut bool {
|
||||
&mut self.visible
|
||||
}
|
||||
|
||||
fn category(&self) -> super::interface::Category {
|
||||
super::interface::Category::Programming
|
||||
}
|
||||
|
||||
fn render(&mut self, state: &mut State, ui: &mut Ui, ctx: &Context) {
|
||||
if self.buffer != self.text {
|
||||
self.unsaved = true;
|
||||
}
|
||||
|
||||
ui.vertical(|ui| {
|
||||
// Top bar
|
||||
|
||||
if ui.input(|i| i.key_pressed(Key::S) && i.modifiers.ctrl) {
|
||||
self.save();
|
||||
}
|
||||
|
||||
self.render_toolbar(state, ui, ctx);
|
||||
|
||||
ui.add_space(4.0); // Add some spacing instead of just a separator
|
||||
ui.separator();
|
||||
|
||||
let remaining_height = f32::max(ui.available_height() - 100.0, 100.0);
|
||||
|
||||
ui.allocate_ui_with_layout(
|
||||
egui::Vec2::new(ui.available_width(), remaining_height),
|
||||
Layout::left_to_right(Align::Min),
|
||||
|ui| {
|
||||
self.render_editor(state, ui, ctx);
|
||||
ui.separator();
|
||||
self.render_output(state, ui, ctx);
|
||||
},
|
||||
);
|
||||
|
||||
self.render_bottom_bar(state, ui, ctx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
path: None,
|
||||
text: String::new(),
|
||||
buffer: String::new(),
|
||||
output: Vec::new(),
|
||||
unsaved: true,
|
||||
cursor_col: 1,
|
||||
cursor_line: 1,
|
||||
visible: false,
|
||||
load_offset: 0,
|
||||
offset_str: String::new(),
|
||||
error: None,
|
||||
open_file_dialog: None,
|
||||
save_file_dialog: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn filename(&self) -> &str {
|
||||
if let Some(path) = &self.path {
|
||||
return path
|
||||
.file_name()
|
||||
.unwrap_or_else(|| OsStr::new("Unnamed!"))
|
||||
.to_str()
|
||||
.map_or_else(
|
||||
|| unreachable!("File name should be valid UTF-8."),
|
||||
|ext| ext,
|
||||
);
|
||||
}
|
||||
"Unnamed!"
|
||||
}
|
||||
|
||||
fn extension(&self) -> &str {
|
||||
if let Some(path) = &self.path {
|
||||
return path
|
||||
.extension()
|
||||
.map_or_else(|| OsStr::new("Unknown!"), |ext| ext)
|
||||
.to_str()
|
||||
.map_or_else(
|
||||
|| unreachable!("File name should be valid UTF-8."),
|
||||
|ext| ext,
|
||||
);
|
||||
}
|
||||
"Unknown!"
|
||||
}
|
||||
|
||||
fn save(&mut self) {
|
||||
if self.open_file_dialog.is_some() {
|
||||
// TODO: Flash an error stating you can only have one menu open at once.
|
||||
self.open_file_dialog = None;
|
||||
}
|
||||
|
||||
if let Some(path) = &self.path {
|
||||
// Save to existing path
|
||||
self.buffer = self.text.clone();
|
||||
|
||||
let text = if path.extension().is_some_and(|ext| ext == "dsb") {
|
||||
let mut res = Vec::new();
|
||||
for line in self.text.lines() {
|
||||
for line in line.split_whitespace() {
|
||||
match u32::from_str_radix(line, 16) {
|
||||
Ok(num) => res.push(num),
|
||||
Err(e) => {
|
||||
self.error = Some(format!("Failed to parse file: {e}"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
res.into_iter()
|
||||
.flat_map(u32::to_be_bytes)
|
||||
.collect::<Vec<u8>>()
|
||||
} else {
|
||||
self.text.as_bytes().to_vec()
|
||||
};
|
||||
|
||||
if let Err(why) = std::fs::write(path, text) {
|
||||
self.error = Some(format!("Failed to save file: {why}"));
|
||||
} else {
|
||||
self.unsaved = false;
|
||||
}
|
||||
} else {
|
||||
// Open the save dialog.
|
||||
let work_dir = std::env::current_dir().unwrap_or_else(|_| {
|
||||
dirs::home_dir().expect(
|
||||
"Couldn't get your current working directory or your home directory.",
|
||||
)
|
||||
});
|
||||
|
||||
if self.save_file_dialog.is_none() {
|
||||
let mut dialog = FileDialog::save_file(Some(work_dir));
|
||||
dialog.open();
|
||||
self.save_file_dialog = Some(dialog);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open(&mut self) {
|
||||
let work_dir = std::env::current_dir().unwrap_or_else(|_| {
|
||||
dirs::home_dir().expect(
|
||||
"Couldn't get your current working directory or your home directory.",
|
||||
)
|
||||
});
|
||||
|
||||
if self.save_file_dialog.is_some() {
|
||||
// TODO: Flash an error stating you can only have one menu open at once.
|
||||
self.save_file_dialog = None;
|
||||
}
|
||||
|
||||
if self.open_file_dialog.is_none() {
|
||||
if let Some(p) = &self.path {
|
||||
let path = p.parent().map(Path::to_path_buf);
|
||||
let mut dialog = FileDialog::open_file(path);
|
||||
dialog.open();
|
||||
self.open_file_dialog = Some(dialog);
|
||||
} else {
|
||||
let mut dialog = FileDialog::open_file(Some(work_dir));
|
||||
dialog.open();
|
||||
self.open_file_dialog = Some(dialog);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_file_dialogs(&mut self, ctx: &egui::Context) {
|
||||
// Handle open dialog
|
||||
if let Some(dialog) = &mut self.open_file_dialog
|
||||
&& dialog.show(ctx).selected()
|
||||
{
|
||||
if let Some(file) = dialog.path() {
|
||||
// check if the file is a binary file
|
||||
if file.extension().is_some_and(|ext| ext == "dsb") {
|
||||
match std::fs::read(file) {
|
||||
Ok(content) => {
|
||||
let mut res = String::new();
|
||||
for (i, b) in content.iter().enumerate() {
|
||||
_ = write!(res, "{b:02x}");
|
||||
if i % 4 == 3 {
|
||||
res.push('\n');
|
||||
}
|
||||
}
|
||||
self.text = res.clone();
|
||||
self.buffer = res;
|
||||
self.path = Some(file.to_path_buf());
|
||||
self.unsaved = false;
|
||||
self.error = None;
|
||||
}
|
||||
Err(e) => {
|
||||
self.error = Some(format!("Failed to read file: {e}"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match std::fs::read_to_string(file) {
|
||||
Ok(content) => {
|
||||
self.text = content.clone();
|
||||
self.buffer = content;
|
||||
self.path = Some(file.to_path_buf());
|
||||
self.unsaved = false;
|
||||
self.error = None;
|
||||
}
|
||||
Err(e) => {
|
||||
self.error = Some(format!("Failed to read file: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.open_file_dialog = None;
|
||||
}
|
||||
|
||||
// Handle save dialog
|
||||
if let Some(dialog) = &mut self.save_file_dialog
|
||||
&& dialog.show(ctx).selected()
|
||||
{
|
||||
if let Some(file) = dialog.path() {
|
||||
self.buffer = self.text.clone();
|
||||
|
||||
let content = if file.extension().is_some_and(|ext| ext == "dsb") {
|
||||
let mut res = Vec::new();
|
||||
for line in self.text.lines() {
|
||||
for line in line.split_whitespace() {
|
||||
match u32::from_str_radix(line, 16) {
|
||||
Ok(num) => res.push(num),
|
||||
Err(e) => {
|
||||
self.error =
|
||||
Some(format!("Failed to parse file: {e}"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
res.into_iter()
|
||||
.flat_map(u32::to_be_bytes)
|
||||
.collect::<Vec<u8>>()
|
||||
} else {
|
||||
self.text.clone().as_bytes().to_vec()
|
||||
};
|
||||
|
||||
match std::fs::write(file, content) {
|
||||
Ok(()) => {
|
||||
self.path = Some(file.to_path_buf());
|
||||
self.unsaved = false;
|
||||
self.error = None;
|
||||
}
|
||||
Err(e) => {
|
||||
self.error = Some(format!("Failed to save file: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.save_file_dialog = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn render_output(&self, _state: &mut State, ui: &mut Ui, _ctx: &Context) {
|
||||
// Output area with synchronized scrolling
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt("output_scroll")
|
||||
.max_width(400.0)
|
||||
.show(ui, |ui| {
|
||||
if self.output.is_empty() {
|
||||
ui.label(
|
||||
egui::RichText::new("No output data")
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::GRAY),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
egui::Grid::new("output_grid")
|
||||
.spacing([5.0, 2.0]) // Horizontal and vertical spacing
|
||||
.num_columns(4)
|
||||
.striped(false)
|
||||
.show(ui, |ui| {
|
||||
// Process bytes in chunks of 4
|
||||
for (line_num, chunk) in self.output.chunks(4).enumerate() {
|
||||
let address = line_num * 4;
|
||||
|
||||
// Convert chunk to u32 (little-endian)
|
||||
let mut bytes = [0u8; 4];
|
||||
for (i, &byte) in chunk.iter().enumerate() {
|
||||
if i < 4 {
|
||||
bytes[i] = byte;
|
||||
}
|
||||
}
|
||||
let value = u32::from_be_bytes(bytes);
|
||||
|
||||
// Address column
|
||||
ui.with_layout(
|
||||
egui::Layout::left_to_right(egui::Align::Center),
|
||||
|ui| {
|
||||
ui.set_min_width(80.0);
|
||||
let style = ui.style_mut();
|
||||
style.visuals.widgets.inactive.bg_fill =
|
||||
egui::Color32::from_gray(30);
|
||||
ui.label(
|
||||
egui::RichText::new(format!("0x{address:04X}"))
|
||||
.font(egui::FontId::monospace(12.0)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// Individual bytes column
|
||||
let byte_str = chunk
|
||||
.iter()
|
||||
.map(|b| format!("{b:02X}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
ui.label(
|
||||
egui::RichText::new(format!("{byte_str:<11}"))
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::from_rgb(200, 200, 255)),
|
||||
);
|
||||
|
||||
// Hex column
|
||||
ui.label(
|
||||
egui::RichText::new(format!("0x{value:08X}"))
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::from_rgb(255, 200, 200)),
|
||||
);
|
||||
|
||||
// Instruction column
|
||||
let instruction = Instruction::decode(value).map_or_else(
|
||||
|_| format!("{value:10}"),
|
||||
|instruction| instruction.to_string(),
|
||||
);
|
||||
|
||||
ui.label(
|
||||
egui::RichText::new(instruction)
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::from_rgb(200, 255, 200)),
|
||||
);
|
||||
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn render_editor(&mut self, _state: &mut State, ui: &mut Ui, _ctx: &Context) {
|
||||
let available_width = ui.available_width();
|
||||
let syntax = match self.extension() {
|
||||
"dsa" => Some(Syntax::new("dsa")),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let ed = CodeEditor::default()
|
||||
.id_source("editor")
|
||||
.with_fontsize(12.0)
|
||||
.with_rows(0)
|
||||
.with_theme(ColorTheme::default())
|
||||
.with_syntax(Syntax::dsa())
|
||||
.with_numlines(true)
|
||||
.desired_width(available_width - 500.0);
|
||||
|
||||
let mut editor = ed.clone();
|
||||
|
||||
if let Some(syntax) = syntax {
|
||||
editor = ed.with_syntax(syntax);
|
||||
}
|
||||
|
||||
editor.show(ui, &mut self.text);
|
||||
}
|
||||
|
||||
fn render_bottom_bar(&self, _state: &mut State, ui: &mut Ui, _ctx: &Context) {
|
||||
ui.horizontal(|ui| {
|
||||
// error display
|
||||
ui.label(
|
||||
egui::RichText::new(self.error.clone().unwrap_or_default())
|
||||
.color(egui::Color32::RED),
|
||||
);
|
||||
|
||||
// line and col
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.label(format!("Ln {}, Col {}", self.cursor_line, self.cursor_col));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn build(&mut self) {
|
||||
if let Some(path) = &self.path {
|
||||
match path.extension().and_then(|ext| ext.to_str()) {
|
||||
Some("dsa") => {
|
||||
let mut compiler = CompilerEngine::new();
|
||||
compiler.start_compilation(path);
|
||||
|
||||
// Or block until done
|
||||
let instructions = match compiler.wait_for_result() {
|
||||
Ok(instructions) => instructions,
|
||||
Err(e) => {
|
||||
self.error = Some(e.to_string());
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.output = instructions
|
||||
.iter()
|
||||
.flat_map(|i| i.encode().to_be_bytes().to_vec())
|
||||
.collect();
|
||||
}
|
||||
Some("dsb") => {
|
||||
if let Ok(bytes) = fs::read(path) {
|
||||
self.output = bytes;
|
||||
} else {
|
||||
self.error = Some("Failed to read file".to_string());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.error = Some(format!("Invalid file type: {}", self.filename()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_toolbar(&mut self, state: &State, ui: &mut Ui, ctx: &Context) {
|
||||
self.handle_file_dialogs(ctx);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("File type: {}", self.extension()));
|
||||
ui.label(format!("Filename: {}", self.filename()));
|
||||
ui.label(format!("Unsaved: {}", self.unsaved));
|
||||
|
||||
// number of lines in the file
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let line_count = self.text.lines().count();
|
||||
ui.label(format!("Lines: {line_count}"));
|
||||
});
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().button_padding = egui::vec2(8.0, 4.0);
|
||||
ui.spacing_mut().item_spacing.x = 6.0;
|
||||
|
||||
// Opens a file
|
||||
if ui.button("Open").clicked() {
|
||||
self.open();
|
||||
}
|
||||
|
||||
// Saves the current file
|
||||
if ui.button("Save").clicked() {
|
||||
self.save();
|
||||
}
|
||||
|
||||
// builds the current file
|
||||
if ui.button("Build").clicked() && !self.unsaved {
|
||||
self.build();
|
||||
}
|
||||
|
||||
// Loads the generated binary into the assembler at the provided offset
|
||||
if ui.button("Load").clicked() {
|
||||
if self.error.is_some() {
|
||||
self.error =
|
||||
Some("Can't load program at invalid offset!".to_string());
|
||||
}
|
||||
|
||||
state
|
||||
.cmd_sender
|
||||
.send(Command::Write(self.load_offset, self.output.clone()))
|
||||
.unwrap_or_else(|_| {
|
||||
self.error = Some("Failed to send command".to_string());
|
||||
});
|
||||
}
|
||||
|
||||
// Entry widget to enter a load offset
|
||||
if ui.text_edit_singleline(&mut self.offset_str).changed() {
|
||||
if let Some(offset) = parse_address(&self.offset_str) {
|
||||
self.load_offset = offset;
|
||||
self.error = None;
|
||||
} else {
|
||||
self.error = Some("Invalid offset".to_string());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_address(address: &str) -> Option<u32> {
|
||||
address.strip_prefix("0x").map_or_else(
|
||||
|| {
|
||||
address.strip_prefix("0b").map_or_else(
|
||||
|| {
|
||||
address.strip_prefix("0o").map_or_else(
|
||||
|| address.parse::<u32>().ok(),
|
||||
|oct| u32::from_str_radix(oct, 8).ok(),
|
||||
)
|
||||
},
|
||||
|bin| u32::from_str_radix(bin, 2).ok(),
|
||||
)
|
||||
},
|
||||
|hex| u32::from_str_radix(hex, 16).ok(),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
use egui::{Context, Ui};
|
||||
|
||||
use crate::emulator::{
|
||||
system::model::{Command, State},
|
||||
ui::interface::Component,
|
||||
};
|
||||
|
||||
pub struct History {
|
||||
visible: bool,
|
||||
}
|
||||
|
||||
impl Component for History {
|
||||
fn name(&self) -> &'static str {
|
||||
"Instruction History"
|
||||
}
|
||||
|
||||
fn visible(&mut self) -> &mut bool {
|
||||
&mut self.visible
|
||||
}
|
||||
|
||||
fn category(&self) -> super::interface::Category {
|
||||
super::interface::Category::Control
|
||||
}
|
||||
|
||||
fn render(&mut self, state: &mut State, ui: &mut Ui, _ctx: &Context) {
|
||||
state.send(Command::HistoryRequest);
|
||||
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt("output_scroll")
|
||||
.max_width(400.0)
|
||||
.show(ui, |ui| {
|
||||
if state.instruction_history.is_empty() {
|
||||
ui.label(
|
||||
egui::RichText::new("No output data")
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::GRAY),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
egui::Grid::new("output_grid")
|
||||
.spacing([5.0, 2.0]) // Horizontal and vertical spacing
|
||||
.num_columns(4)
|
||||
.striped(false)
|
||||
.show(ui, |ui| {
|
||||
// Process bytes in chunks of 4
|
||||
for (idx, instruction) in
|
||||
state.instruction_history.iter().enumerate()
|
||||
{
|
||||
ui.label(format!("{idx}: "));
|
||||
|
||||
// Hex column
|
||||
let addr = instruction.0;
|
||||
ui.label(
|
||||
egui::RichText::new(format!("0x{addr:08X}"))
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::from_rgb(255, 200, 200)),
|
||||
);
|
||||
|
||||
ui.label(
|
||||
egui::RichText::new(instruction.1.to_string())
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::from_rgb(200, 255, 200)),
|
||||
);
|
||||
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for History {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl History {
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self { visible: false }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
use crate::emulator::system::model::{Command, Running, State, StateUpdate};
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
|
||||
pub trait Component {
|
||||
fn render(&mut self, state: &mut State, ui: &mut egui::Ui, ctx: &egui::Context);
|
||||
fn visible(&mut self) -> &mut bool;
|
||||
fn name(&self) -> &'static str;
|
||||
fn category(&self) -> Category;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Category {
|
||||
Control,
|
||||
Memory,
|
||||
IO,
|
||||
Programming,
|
||||
}
|
||||
|
||||
impl Category {
|
||||
#[must_use]
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Control => "Control Systems",
|
||||
Self::Memory => "Memory Systems",
|
||||
Self::IO => "I/O Systems",
|
||||
Self::Programming => "Programming",
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn list() -> Vec<Self> {
|
||||
vec![Self::Control, Self::Memory, Self::IO, Self::Programming]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EmulatorUI {
|
||||
pub state: State,
|
||||
pub components: Vec<Box<dyn Component>>,
|
||||
}
|
||||
|
||||
impl EmulatorUI {
|
||||
#[must_use]
|
||||
pub fn new(sender: Sender<Command>, receiver: Receiver<StateUpdate>) -> Self {
|
||||
Self {
|
||||
state: State::new(sender, receiver),
|
||||
components: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_component(&mut self, component: Box<dyn Component>) {
|
||||
self.components.push(component);
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for EmulatorUI {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
if let Err(e) = self.state.update() {
|
||||
self.state.error_log.push(e.to_string());
|
||||
}
|
||||
|
||||
if self.state.running == Running::Running {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
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, 15.0));
|
||||
ui.heading("DSA Simulator (Damn Simple Architecture 🔥)");
|
||||
ui.allocate_space(egui::vec2(0.0, 15.0));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |_ui| {
|
||||
egui::Window::new("Main Menu")
|
||||
.resizable(false)
|
||||
.default_width(300.0)
|
||||
.show(ctx, |ui| {
|
||||
super::menu::render_menu(self, ui, ctx);
|
||||
});
|
||||
|
||||
for c in &mut self.components {
|
||||
let mut visible = *c.visible();
|
||||
if visible {
|
||||
egui::Window::new(c.name())
|
||||
.open(&mut visible)
|
||||
.show(ctx, |ui| {
|
||||
c.render(&mut self.state, ui, ctx);
|
||||
});
|
||||
}
|
||||
*c.visible() = visible;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
egui::TopBottomPanel::bottom("bottom_panel").show(ctx, |ui| {
|
||||
ui.horizontal_centered(|ui| {
|
||||
ui.group(|ui| {
|
||||
ui.add_space(10.0);
|
||||
ui.strong("Authors:");
|
||||
ui.add_space(5.0);
|
||||
ui.label("zxq5");
|
||||
ui.label("nullndvoid");
|
||||
ui.add_space(10.0);
|
||||
ui.separator();
|
||||
ui.add_space(10.0);
|
||||
ui.strong("Version");
|
||||
ui.add_space(5.0);
|
||||
ui.label("1.0.0");
|
||||
ui.add_space(10.0);
|
||||
ui.separator();
|
||||
ui.add_space(10.0);
|
||||
ui.strong("Source:");
|
||||
ui.add_space(5.0);
|
||||
ui.hyperlink_to(
|
||||
"https://git.zxq5.dev/LowLevelDevs/damn_simple_architecture",
|
||||
"https://git.zxq5.dev/LowLevelDevs/damn_simple_architecture",
|
||||
);
|
||||
ui.add_space(10.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use common::prelude::Instruction;
|
||||
use egui::{Context, Ui};
|
||||
use egui_file::FileDialog;
|
||||
|
||||
use crate::emulator::{
|
||||
system::model::{Command, State},
|
||||
ui::interface::Component,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Loader {
|
||||
path: Option<PathBuf>,
|
||||
output: Vec<u8>,
|
||||
load_offset: u32,
|
||||
offset_str: String,
|
||||
|
||||
// file dialogs
|
||||
open_file_dialog: Option<FileDialog>,
|
||||
|
||||
// other
|
||||
visible: bool,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
impl Component for Loader {
|
||||
fn name(&self) -> &'static str {
|
||||
"Loader"
|
||||
}
|
||||
|
||||
fn visible(&mut self) -> &mut bool {
|
||||
&mut self.visible
|
||||
}
|
||||
|
||||
fn category(&self) -> super::interface::Category {
|
||||
super::interface::Category::Programming
|
||||
}
|
||||
|
||||
fn render(&mut self, state: &mut State, ui: &mut Ui, ctx: &Context) {
|
||||
ui.vertical(|ui| {
|
||||
self.render_toolbar(state, ui, ctx);
|
||||
|
||||
ui.add_space(4.0); // Add some spacing instead of just a separator
|
||||
ui.separator();
|
||||
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false; 2])
|
||||
.max_height(ui.available_height() - 100.0)
|
||||
.show(ui, |ui| {
|
||||
self.render_output(state, ui, ctx);
|
||||
});
|
||||
|
||||
self.render_bottom_bar(state, ui, ctx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Loader {
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
path: None,
|
||||
output: Vec::new(),
|
||||
visible: false,
|
||||
load_offset: 0,
|
||||
offset_str: String::new(),
|
||||
error: None,
|
||||
open_file_dialog: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn filename(&self) -> &str {
|
||||
if let Some(path) = &self.path {
|
||||
return path
|
||||
.file_name()
|
||||
.unwrap_or_else(|| OsStr::new("Unnamed!"))
|
||||
.to_str()
|
||||
.map_or_else(
|
||||
|| unreachable!("File name should be valid UTF-8."),
|
||||
|ext| ext,
|
||||
);
|
||||
}
|
||||
"Unnamed!"
|
||||
}
|
||||
|
||||
fn open(&mut self) {
|
||||
let work_dir = std::env::current_dir().unwrap_or_else(|_| {
|
||||
dirs::home_dir().expect(
|
||||
"Couldn't get your current working directory or your home directory.",
|
||||
)
|
||||
});
|
||||
|
||||
if self.open_file_dialog.is_some() {
|
||||
// TODO: Flash an error stating you can only have one menu open at once.
|
||||
self.open_file_dialog = None;
|
||||
}
|
||||
|
||||
if self.open_file_dialog.is_none() {
|
||||
if let Some(p) = &self.path {
|
||||
let path = p.parent().map(Path::to_path_buf);
|
||||
let mut dialog = FileDialog::open_file(path);
|
||||
dialog.open();
|
||||
self.open_file_dialog = Some(dialog);
|
||||
} else {
|
||||
let mut dialog = FileDialog::open_file(Some(work_dir));
|
||||
dialog.open();
|
||||
self.open_file_dialog = Some(dialog);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_file_dialogs(&mut self, ctx: &egui::Context) {
|
||||
// Handle open dialog
|
||||
if let Some(dialog) = &mut self.open_file_dialog
|
||||
&& dialog.show(ctx).selected()
|
||||
{
|
||||
if let Some(file) = dialog.path() {
|
||||
// check if the file is a binary file
|
||||
if file.extension().is_some_and(|ext| ext == "dsb") {
|
||||
match std::fs::read(file) {
|
||||
Ok(content) => {
|
||||
self.output = content;
|
||||
self.error = None;
|
||||
}
|
||||
Err(e) => {
|
||||
self.error = Some(format!("Failed to read file: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.open_file_dialog = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn render_output(&self, _state: &mut State, ui: &mut Ui, _ctx: &Context) {
|
||||
// Output area with synchronized scrolling
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt("output_scroll")
|
||||
.max_width(400.0)
|
||||
.show(ui, |ui| {
|
||||
if self.output.is_empty() {
|
||||
ui.label(
|
||||
egui::RichText::new("No output data")
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::GRAY),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
egui::Grid::new("output_grid")
|
||||
.spacing([5.0, 2.0]) // Horizontal and vertical spacing
|
||||
.num_columns(4)
|
||||
.striped(false)
|
||||
.show(ui, |ui| {
|
||||
// Process bytes in chunks of 4
|
||||
for (line_num, chunk) in self.output.chunks(4).enumerate() {
|
||||
let address = line_num * 4;
|
||||
|
||||
// Convert chunk to u32 (little-endian)
|
||||
let mut bytes = [0u8; 4];
|
||||
for (i, &byte) in chunk.iter().enumerate() {
|
||||
if i < 4 {
|
||||
bytes[i] = byte;
|
||||
}
|
||||
}
|
||||
let value = u32::from_be_bytes(bytes);
|
||||
|
||||
// Address column
|
||||
ui.with_layout(
|
||||
egui::Layout::left_to_right(egui::Align::Center),
|
||||
|ui| {
|
||||
ui.set_min_width(80.0);
|
||||
let style = ui.style_mut();
|
||||
style.visuals.widgets.inactive.bg_fill =
|
||||
egui::Color32::from_gray(30);
|
||||
ui.label(
|
||||
egui::RichText::new(format!("0x{address:04X}"))
|
||||
.font(egui::FontId::monospace(12.0)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// Individual bytes column
|
||||
let byte_str = chunk
|
||||
.iter()
|
||||
.map(|b| format!("{b:02X}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
ui.label(
|
||||
egui::RichText::new(format!("{byte_str:<11}"))
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::from_rgb(200, 200, 255)),
|
||||
);
|
||||
|
||||
// Hex column
|
||||
ui.label(
|
||||
egui::RichText::new(format!("0x{value:08X}"))
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::from_rgb(255, 200, 200)),
|
||||
);
|
||||
|
||||
// Instruction column
|
||||
let instruction = Instruction::decode(value).map_or_else(
|
||||
|_| format!("{value:10}"),
|
||||
|instruction| instruction.to_string(),
|
||||
);
|
||||
|
||||
ui.label(
|
||||
egui::RichText::new(instruction)
|
||||
.font(egui::FontId::monospace(12.0))
|
||||
.color(egui::Color32::from_rgb(200, 255, 200)),
|
||||
);
|
||||
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn render_bottom_bar(&self, _state: &mut State, ui: &mut Ui, _ctx: &Context) {
|
||||
ui.horizontal(|ui| {
|
||||
// error display
|
||||
ui.label(
|
||||
egui::RichText::new(self.error.clone().unwrap_or_default())
|
||||
.color(egui::Color32::RED),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_toolbar(&mut self, state: &State, ui: &mut Ui, ctx: &Context) {
|
||||
self.handle_file_dialogs(ctx);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("Filename: {}", self.filename()));
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().button_padding = egui::vec2(8.0, 4.0);
|
||||
ui.spacing_mut().item_spacing.x = 6.0;
|
||||
|
||||
// Opens a file
|
||||
if ui.button("Open").clicked() {
|
||||
self.open();
|
||||
}
|
||||
|
||||
// Loads the generated binary into the assembler at the provided offset
|
||||
if ui.button("Load").clicked() {
|
||||
if self.error.is_some() {
|
||||
self.error =
|
||||
Some("Can't load program at invalid offset!".to_string());
|
||||
}
|
||||
|
||||
state
|
||||
.cmd_sender
|
||||
.send(Command::Write(self.load_offset, self.output.clone()))
|
||||
.unwrap_or_else(|_| {
|
||||
self.error = Some("Failed to send command".to_string());
|
||||
});
|
||||
}
|
||||
|
||||
// Entry widget to enter a load offset
|
||||
if ui.text_edit_singleline(&mut self.offset_str).changed() {
|
||||
if let Some(offset) = parse_address(&self.offset_str) {
|
||||
self.load_offset = offset;
|
||||
self.error = None;
|
||||
} else {
|
||||
self.error = Some("Invalid offset".to_string());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_address(address: &str) -> Option<u32> {
|
||||
address.strip_prefix("0x").map_or_else(
|
||||
|| {
|
||||
address.strip_prefix("0b").map_or_else(
|
||||
|| {
|
||||
address.strip_prefix("0o").map_or_else(
|
||||
|| address.parse::<u32>().ok(),
|
||||
|oct| u32::from_str_radix(oct, 8).ok(),
|
||||
)
|
||||
},
|
||||
|bin| u32::from_str_radix(bin, 2).ok(),
|
||||
)
|
||||
},
|
||||
|hex| u32::from_str_radix(hex, 16).ok(),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
use std::num::ParseIntError;
|
||||
|
||||
use common::prelude::Instruction;
|
||||
|
||||
use crate::emulator::{
|
||||
system::model::{Command, State},
|
||||
ui::interface::Component,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MemoryInspector {
|
||||
view_size: u32,
|
||||
view_addr: u32,
|
||||
visible: bool,
|
||||
addr_input: String,
|
||||
}
|
||||
|
||||
impl MemoryInspector {
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
view_size: 256,
|
||||
view_addr: 0,
|
||||
visible: false,
|
||||
addr_input: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for MemoryInspector {
|
||||
fn category(&self) -> super::interface::Category {
|
||||
super::interface::Category::Memory
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Memory Inspector"
|
||||
}
|
||||
|
||||
fn visible(&mut self) -> &mut bool {
|
||||
&mut self.visible
|
||||
}
|
||||
|
||||
fn render(&mut self, state: &mut State, ui: &mut egui::Ui, ctx: &egui::Context) {
|
||||
// Right column - Memory
|
||||
ui.vertical(|ui| {
|
||||
ui.heading("Memory Inspector");
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Address input section
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Address:");
|
||||
|
||||
let address_response = ui.add(
|
||||
egui::TextEdit::singleline(&mut self.addr_input)
|
||||
.hint_text("0x1000 or 4096")
|
||||
.desired_width(150.0),
|
||||
);
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Search button
|
||||
let search_clicked = ui.button("🔍 Search").clicked();
|
||||
|
||||
// Handle Enter key in text field
|
||||
let enter_pressed = address_response.lost_focus()
|
||||
&& ctx.input(|i| i.key_pressed(egui::Key::Enter));
|
||||
|
||||
if search_clicked || enter_pressed {
|
||||
if let Ok(new) = parse_address(&self.addr_input) {
|
||||
self.view_addr = new;
|
||||
} else {
|
||||
state.error_log.push("Invalid address".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let _ = state
|
||||
.cmd_sender
|
||||
.send(Command::MemRequest(self.view_addr, self.view_size));
|
||||
|
||||
ui.label("(hex or decimal)");
|
||||
});
|
||||
|
||||
// Show input error if any
|
||||
if let Some(error) = state.error_log.last() {
|
||||
ui.colored_label(egui::Color32::RED, format!("Error: {error}"));
|
||||
}
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Memory table
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink(true)
|
||||
.id_salt("memory_inspector_scroll")
|
||||
.show(ui, |ui| {
|
||||
egui::Grid::new("memory_grid")
|
||||
.spacing([12.0, 2.0])
|
||||
.min_col_width(5.0)
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
// Header
|
||||
ui.strong("Address");
|
||||
|
||||
for i in 0..4 {
|
||||
ui.strong(format!("{i:X}"));
|
||||
}
|
||||
|
||||
ui.strong("Decimal");
|
||||
ui.strong("Instruction");
|
||||
|
||||
ui.end_row();
|
||||
|
||||
// Memory data (8 bytes per row)
|
||||
for (row, chunk) in (0u32..).zip(state.memory_view.chunks(4))
|
||||
{
|
||||
let row_address = self.view_addr + (row * 4);
|
||||
ui.monospace(format!(
|
||||
"0x{row_address:08X} ({row_address})"
|
||||
));
|
||||
for &byte in chunk {
|
||||
ui.monospace(format!("{byte:02X}"));
|
||||
}
|
||||
|
||||
// Fill remaining columns if last row is incomplete
|
||||
for _ in chunk.len()..4 {
|
||||
ui.label("");
|
||||
}
|
||||
|
||||
// combine all 4 bytes in the chunk into a u32
|
||||
let combined = chunk.iter().fold(0u32, |acc, &byte| {
|
||||
(acc << 8) | u32::from(byte)
|
||||
});
|
||||
|
||||
ui.monospace(format!("{combined}"));
|
||||
ui.monospace(format!(
|
||||
"{}",
|
||||
Instruction::decode(combined)
|
||||
.unwrap_or(Instruction::Nop)
|
||||
));
|
||||
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_address(address: &str) -> Result<u32, ParseIntError> {
|
||||
if let Some(hex_part) = address.strip_prefix("0x") {
|
||||
return u32::from_str_radix(hex_part, 16);
|
||||
}
|
||||
|
||||
if let Some(bin_part) = address.strip_prefix("0b") {
|
||||
return u32::from_str_radix(bin_part, 2);
|
||||
}
|
||||
|
||||
if let Some(oct_part) = address.strip_prefix("0o") {
|
||||
return u32::from_str_radix(oct_part, 8);
|
||||
}
|
||||
|
||||
address.parse::<u32>()
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
use crate::emulator::ui::interface::{Category, EmulatorUI};
|
||||
|
||||
pub fn render_menu(state: &mut EmulatorUI, ui: &mut egui::Ui, _ctx: &egui::Context) {
|
||||
ui.with_layout(
|
||||
egui::Layout::top_down_justified(egui::Align::Center),
|
||||
|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 cat in Category::list() {
|
||||
ui.add_space(10.0);
|
||||
ui.heading(cat.as_str());
|
||||
ui.add_space(10.0);
|
||||
|
||||
for comp in &mut state.components {
|
||||
let name = comp.name();
|
||||
if comp.category() == cat {
|
||||
ui.toggle_value(comp.visible(), name);
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(10.0);
|
||||
ui.separator();
|
||||
}
|
||||
|
||||
ui.add_space(10.0);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
pub mod control_unit;
|
||||
pub mod display;
|
||||
pub mod editor;
|
||||
pub mod history;
|
||||
pub mod interface;
|
||||
pub mod loader;
|
||||
pub mod memory_inspector;
|
||||
pub mod menu;
|
||||
pub mod stack_inspector;
|
||||
@@ -0,0 +1,79 @@
|
||||
use crate::emulator::{
|
||||
system::model::{Command, State},
|
||||
ui::interface::Component,
|
||||
};
|
||||
|
||||
use common::instructions::Register;
|
||||
|
||||
pub struct StackInspector {
|
||||
visible: bool,
|
||||
}
|
||||
|
||||
impl Default for StackInspector {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl StackInspector {
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self { visible: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for StackInspector {
|
||||
fn visible(&mut self) -> &mut bool {
|
||||
&mut self.visible
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Stack Inspector"
|
||||
}
|
||||
|
||||
fn category(&self) -> super::interface::Category {
|
||||
super::interface::Category::Memory
|
||||
}
|
||||
|
||||
fn render(&mut self, state: &mut State, ui: &mut egui::Ui, _ctx: &egui::Context) {
|
||||
state.send(Command::StackRequest);
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.heading("Stack Inspector");
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt("stack_inspector_scroll")
|
||||
.show(ui, |ui| {
|
||||
egui::Grid::new("stack_grid")
|
||||
.num_columns(2)
|
||||
.spacing([40.0, 4.0])
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
ui.label("Address");
|
||||
ui.label("Value");
|
||||
ui.end_row();
|
||||
|
||||
for (i, value) in
|
||||
state.stack_view.chunks(4).take(32).enumerate()
|
||||
{
|
||||
let value = u32::from_be_bytes(value.try_into().expect(
|
||||
"Could not read 4 byte instruction or data! Something is wrong.",
|
||||
));
|
||||
ui.label(format!(
|
||||
"{} [{}]",
|
||||
i,
|
||||
state.reg_file.get(Register::Spr).expect("SPR should never be invalid") - i as u32 * 4
|
||||
));
|
||||
ui.label(format!("0x{value:08X} ({value})"));
|
||||
ui.end_row();
|
||||
}
|
||||
|
||||
if state.stack_view.is_empty() {
|
||||
ui.label("(empty)");
|
||||
ui.label("-");
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
#![deny(
|
||||
clippy::unwrap_used,
|
||||
clippy::nursery,
|
||||
clippy::perf,
|
||||
clippy::pedantic,
|
||||
clippy::complexity
|
||||
)]
|
||||
#![allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::missing_panics_doc,
|
||||
clippy::missing_errors_doc,
|
||||
clippy::match_wildcard_for_single_variants
|
||||
)]
|
||||
|
||||
pub mod emulator;
|
||||
|
||||
use std::{
|
||||
sync::{
|
||||
Arc,
|
||||
mpsc::{Receiver, Sender},
|
||||
},
|
||||
thread,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
use winit::platform::android::{EventLoopBuilderExtAndroid, activity::AndroidApp};
|
||||
|
||||
use crate::emulator::{
|
||||
misc::rpc::RpcClient,
|
||||
system::{
|
||||
emulator::run_emulator,
|
||||
memory::MainStore,
|
||||
model::{Command, StateUpdate},
|
||||
processor::Processor,
|
||||
},
|
||||
ui::{
|
||||
control_unit::ControlPanel, display::Display, editor::Editor,
|
||||
interface::EmulatorUI, memory_inspector::MemoryInspector,
|
||||
stack_inspector::StackInspector,
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
#[unsafe(no_mangle)]
|
||||
pub fn android_main(app: AndroidApp) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use crate::emulator::{config::Config, misc::rpc::get_rpc_client_or_none};
|
||||
use std::path::Path;
|
||||
|
||||
// Initialize channels and read in configuration.
|
||||
let (cmd_sender, cmd_receiver) = std::sync::mpsc::channel();
|
||||
let (state_sender, state_reciever) = std::sync::mpsc::channel();
|
||||
let config = Config::load(Path::new(".dsa.emulator.toml"))?;
|
||||
|
||||
// Setup RPC if enabled.
|
||||
let (rpc_sender, rpc_reciever) = std::sync::mpsc::channel();
|
||||
|
||||
let rpc_client =
|
||||
get_rpc_client_or_none(&config, rpc_sender, rpc_reciever)?.map(Arc::new);
|
||||
|
||||
setup_emulator(cmd_receiver, state_sender, rpc_client);
|
||||
|
||||
let ui = setup_ui(cmd_sender, state_reciever);
|
||||
|
||||
// Run UI.
|
||||
#[allow(unused_variables)]
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default().with_inner_size([800.0, 600.0]),
|
||||
event_loop_builder: Some(Box::new(move |builder| {
|
||||
#[cfg(target_os = "android")]
|
||||
builder.with_android_app(app);
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eframe::run_native(
|
||||
"DSA Simulator (Damn Simple Architecture 🔥)",
|
||||
options,
|
||||
Box::new(move |cc| {
|
||||
cc.egui_ctx.set_visuals(egui::Visuals::default());
|
||||
Ok(Box::new(ui))
|
||||
}),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn setup_emulator(
|
||||
cmd_receiver: Receiver<Command>,
|
||||
state_sender: Sender<StateUpdate>,
|
||||
rpc_client: Option<Arc<RpcClient>>,
|
||||
) {
|
||||
let main_store = MainStore::new();
|
||||
let processor = Processor::new(Box::new(main_store), vec![]);
|
||||
|
||||
thread::spawn(move || {
|
||||
run_emulator(&cmd_receiver, &state_sender, processor, rpc_client.as_ref());
|
||||
});
|
||||
}
|
||||
|
||||
/// Creates the [`EmulatorUI`].
|
||||
#[must_use]
|
||||
pub fn setup_ui(
|
||||
cmd_sender: Sender<Command>,
|
||||
state_reciever: Receiver<StateUpdate>,
|
||||
) -> EmulatorUI {
|
||||
let mut ui = EmulatorUI::new(cmd_sender, state_reciever);
|
||||
|
||||
// Create UI modules.
|
||||
let control_unit = ControlPanel::new();
|
||||
|
||||
ui.add_component(Box::new(control_unit));
|
||||
|
||||
let mem_inspector = MemoryInspector::new();
|
||||
ui.add_component(Box::new(mem_inspector));
|
||||
|
||||
let stack_inspector = StackInspector::new();
|
||||
ui.add_component(Box::new(stack_inspector));
|
||||
|
||||
let editor = Editor::new();
|
||||
ui.add_component(Box::new(editor));
|
||||
|
||||
let display = Display::new();
|
||||
ui.add_component(Box::new(display));
|
||||
|
||||
let history = emulator::ui::history::History::new();
|
||||
ui.add_component(Box::new(history));
|
||||
|
||||
let loader = emulator::ui::loader::Loader::new();
|
||||
ui.add_component(Box::new(loader));
|
||||
|
||||
ui
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use dsa_rs::emulator::{config::Config, misc::rpc::get_rpc_client_or_none};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize channels and read in configuration.
|
||||
let (cmd_sender, cmd_receiver) = std::sync::mpsc::channel();
|
||||
let (state_sender, state_reciever) = std::sync::mpsc::channel();
|
||||
let config = Config::load(Path::new(".dsa.emulator.toml"))?;
|
||||
|
||||
// Setup RPC if enabled.
|
||||
let (rpc_sender, rpc_reciever) = std::sync::mpsc::channel();
|
||||
|
||||
let rpc_client =
|
||||
get_rpc_client_or_none(&config, rpc_sender, rpc_reciever)?.map(Arc::new);
|
||||
|
||||
dsa_rs::setup_emulator(cmd_receiver, state_sender, rpc_client);
|
||||
|
||||
let ui = dsa_rs::setup_ui(cmd_sender, state_reciever);
|
||||
|
||||
// Run UI.
|
||||
#[allow(unused_variables)]
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default().with_inner_size([800.0, 600.0]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eframe::run_native(
|
||||
"DSA Simulator (Damn Simple Architecture 🔥)",
|
||||
options,
|
||||
Box::new(move |cc| {
|
||||
cc.egui_ctx.set_visuals(egui::Visuals::default());
|
||||
Ok(Box::new(ui))
|
||||
}),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user