REFACTOR:

- created dsa_formats crate for DSA file formats such as DSE executables (.dsb, will become .dse once relocations/linking are introduced)
- moved IO devices from emulator_ui to emulator_core (keeping only the UI trait impls in emulator_ui)
- other refactors within emulator_ui to make the project structure a bit more sane
This commit is contained in:
2026-08-01 00:36:08 +01:00
parent a5190b69c6
commit 8c19497bc7
52 changed files with 816 additions and 1620 deletions
+1
View File
@@ -14,6 +14,7 @@
<sourceFolder url="file://$MODULE_DIR$/dsx/dsx_client/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/dsx/dsx_client/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/dsx/dsx_common/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/dsx/dsx_common/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/dsx/dsx_server/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/dsx/dsx_server/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/dsa/dsa_formats/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/.idea/caches" /> <excludeFolder url="file://$MODULE_DIR$/.idea/caches" />
<excludeFolder url="file://$MODULE_DIR$/doc/.obsidian" /> <excludeFolder url="file://$MODULE_DIR$/doc/.obsidian" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
+17 -1
View File
@@ -1,8 +1,24 @@
[workspace] [workspace]
members = ["dsa/common", "dsa/assembler", "dsa/emulator/emulator_ui", "dsa/linker", "dsa/compiler", "dsa/emulator/emulator_core", "dsx/dsx_common", "dsx/dsx_server", "dsx/dsx_client", "dsa/disassembler"] members = ["dsa/common", "dsa/assembler", "dsa/emulator/emulator_ui", "dsa/linker", "dsa/compiler", "dsa/emulator/emulator_core", "dsx/dsx_common", "dsx/dsx_server", "dsx/dsx_client", "dsa/disassembler", "dsa/dsa_formats"]
resolver = "3" resolver = "3"
[workspace.package] [workspace.package]
edition = "2024" edition = "2024"
version = "0.3.0" version = "0.3.0"
authors = ["zxq5", "zxq4"] authors = ["zxq5", "zxq4"]
#[workspace.lints.clippy]
#all = "warn"
#pedantic = "warn"
#cast_possible_truncation = "warn"
#cast_sign_loss = "warn"
#cast_possible_wrap = "warn"
#as_conversions = "warn"
#unwrap_used = "warn"
#expect_used = "warn"
#indexing_slicing = "warn"
#large_enum_variant = "warn"
#mem_replace_with_default = "warn"
#mutex_atomic = "warn"
#todo = "warn"
#dbg_macro = "warn"
+1 -1
View File
@@ -13,9 +13,9 @@ name = "assembler"
path = "src/lib.rs" path = "src/lib.rs"
[dependencies] [dependencies]
binrw = "0.15.1"
clap = { version = "4.6.0", features = ["derive"] } clap = { version = "4.6.0", features = ["derive"] }
common = { path = "../common" } common = { path = "../common" }
dsa_formats = { path = "../dsa_formats" }
num_cpus = "1.17.0" num_cpus = "1.17.0"
strum = { version = "0.28.0", features = ["derive"] } strum = { version = "0.28.0", features = ["derive"] }
+5 -11
View File
@@ -1,22 +1,16 @@
#![allow(dead_code, unused)] #![allow(dead_code, unused)]
use crate::assembler::resolver2::{SectionId, resolve_address, resolve_dependencies, resolve_symbols, split_sections};
use common::process::{ProcessAgent, TraceableProcess};
use dsa_formats::binary_dse::{DseExecutable, SymbolEntry};
use std::{ use std::{
collections::HashSet, collections::HashSet,
fmt, fs, fmt, fs,
hash::{DefaultHasher, Hash, Hasher}, hash::{DefaultHasher, Hash, Hasher},
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{ sync::
Arc, Mutex, mpsc::{self},
mpsc::{self, Receiver, Sender},
},
thread,
}; };
use std::collections::HashMap;
use std::io::Cursor;
use common::formats::binary_dse::{DseExecutable, SymbolEntry};
use common::prelude::Instruction;
use common::process::{ProcessAgent, TraceableProcess};
use crate::assembler::resolver2::{resolve_address, resolve_dependencies, resolve_symbols, split_sections, SectionId};
// Module declarations // Module declarations
#[macro_use] #[macro_use]
-2
View File
@@ -1,2 +0,0 @@
pub mod binary_dse;
pub mod binary_dso;
-1
View File
@@ -1,6 +1,5 @@
pub mod asm; pub mod asm;
pub mod isa; pub mod isa;
pub mod formats;
pub mod process; pub mod process;
pub mod prelude { pub mod prelude {
+2 -1
View File
@@ -14,4 +14,5 @@ path = "src/lib.rs"
[dependencies] [dependencies]
clap = { version = "4.6.0", features = ["derive"] } clap = { version = "4.6.0", features = ["derive"] }
common = { path = "../common" } common = { path = "../common" }
dsa_formats = { path = "../dsa_formats" }
-350
View File
@@ -1,350 +0,0 @@
use std::collections::HashMap;
use std::fmt;
use common::formats::binary_dse::DseExecutable;
use common::isa::instructions::Opcode;
use common::prelude::{Instruction, Register};
#[derive(Debug)]
pub struct Disassembly {
pub symbols: Vec<SymbolLine>,
pub text_lines: Vec<String>,
pub data_lines: Vec<String>,
}
#[derive(Debug)]
pub struct SymbolLine {
pub name: String,
pub address: u32,
pub section: SectionKind,
pub binding: BindingKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SectionKind { Data, Text, Abs, Undef }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BindingKind { Local, Global, Weak }
impl SectionKind {
fn from_raw(v: u8) -> Self {
match v { 1 => Self::Data, 2 => Self::Text, 0 => Self::Abs, _ => Self::Undef }
}
}
impl BindingKind {
fn from_raw(v: u8) -> Self {
match v { 1 => Self::Global, 2 => Self::Weak, _ => Self::Local }
}
}
pub fn disassemble(exe: &DseExecutable) -> Disassembly {
let symbols = build_symbol_lines(exe);
let mut text_labels: HashMap<u32, String> = HashMap::new();
let mut data_labels: HashMap<u32, String> = HashMap::new();
for sym in &symbols {
match sym.section {
SectionKind::Text => { text_labels.insert(sym.address, sym.name.clone()); }
SectionKind::Data => { data_labels.insert(sym.address, sym.name.clone()); }
_ => {}
}
}
// symbols can refer to either section by full resolved address (per your
// v1 loader convention: data lives right after text), so a single combined
// map is actually what address-resolution needs — keep both, but merge
// for lookups against full 32-bit addresses.
let mut all_labels = text_labels.clone();
all_labels.extend(data_labels.clone());
let text_lines = build_text_lines(&exe.text, &all_labels);
let data_lines = build_data_lines(&exe.data, &data_labels);
Disassembly { symbols, text_lines, data_lines }
}
fn build_symbol_lines(exe: &DseExecutable) -> Vec<SymbolLine> {
exe.symbols
.iter()
.map(|s| SymbolLine {
name: read_string(&exe.string_table, s.name_offset),
address: s.value,
section: SectionKind::from_raw(s.section),
binding: BindingKind::from_raw(s.binding),
})
.collect()
}
fn read_string(strtab: &[u8], offset: u32) -> String {
let start = offset as usize;
let end = strtab[start..].iter().position(|&b| b == 0).map(|p| start + p).unwrap_or(strtab.len());
String::from_utf8_lossy(&strtab[start..end]).into_owned()
}
/// Address a jump-like instruction actually targets, if statically known.
/// Returns None when the jump is register-relative (addr reg != Zero) since
/// that depends on runtime state and can't be resolved here.
fn static_jump_target(addr_reg: Register, imm: u16) -> Option<u32> {
(addr_reg == Register::Zero).then_some(imm as u32)
}
fn build_text_lines(words: &[u32], labels: &HashMap<u32, String>) -> Vec<String> {
let mut out = Vec::new();
let mut i = 0;
while i < words.len() {
let addr = 4 * i as u32;
if let Some(label) = labels.get(&addr) {
out.push(format!("{label}:"));
}
let instr = Instruction(words[i]);
let opcode = Opcode::from_u8(instr.opcode());
// Collapse Lli+Lui address-load pairs back into `lwi` (or a full
// ldx/stx pseudo-op if a matching mem instruction follows), mirroring
// what expand_ldx/expand_stx/expand_lwi originally expanded from.
if let Some(Opcode::Lli) = opcode {
if let Some(collapsed) = try_collapse_addr_load(words, i, labels) {
out.push(format!(" {}", collapsed.text));
i += collapsed.consumed;
continue;
}
}
out.push(format!(" {}", render_instruction(instr, opcode, labels)));
i += 1;
}
out
}
struct Collapsed {
text: String,
consumed: usize,
}
fn try_collapse_addr_load(
words: &[u32],
i: usize,
labels: &HashMap<u32, String>,
) -> Option<Collapsed> {
let lli = Instruction(words[i]);
let lui = Instruction(*words.get(i + 1)?);
if Opcode::from_u8(lui.opcode())? as u8 != Opcode::Lui as u8 {
return None;
}
if lui.dest_checked() != lli.dest_checked() {
return None;
}
let dest = lli.dest_checked();
let full_addr = (lli.imm16() as u32) | ((lui.imm16() as u32) << 16);
let addr_text = labels.get(&full_addr).cloned().unwrap_or_else(|| format!("{full_addr:#010x}"));
// Check whether a mem op immediately follows, referencing `dest` —
// that's the ldb/ldh/ldw/stb/sth/stw expansion pattern, not a bare lwi.
if let Some(&mem_word) = words.get(i + 2) {
let mem = Instruction(mem_word);
if let Some(mem_op) = Opcode::from_u8(mem.opcode()) {
let is_mem_op = matches!(
mem_op,
Opcode::Ldb | Opcode::Ldbs | Opcode::Ldh | Opcode::Ldhs | Opcode::Ldw
| Opcode::Stb | Opcode::Sth | Opcode::Stw
);
if is_mem_op && (mem.src1_checked() == dest || mem.dest_checked() == dest) {
let mnemonic = format!("{mem_op:?}").to_lowercase();
let other = if mem.src1_checked() == dest { mem.dest_checked() } else { mem.src1_checked() };
return Some(Collapsed {
text: format!("{mnemonic} {addr_text}, {other:?}, {:#06x}", mem.imm16()),
consumed: 3,
});
}
}
}
Some(Collapsed {
text: format!("lwi {addr_text}, {dest:?}"),
consumed: 2,
})
}
fn render_instruction(instr: Instruction, opcode: Option<Opcode>, labels: &HashMap<u32, String>) -> String {
let Some(op) = opcode else {
return format!("; INVALID OPCODE {:#04x} (word {:#010x})", instr.opcode(), instr.0);
};
let mnemonic = format!("{op:?}").to_lowercase();
match op {
Opcode::Nop | Opcode::Hlt | Opcode::Ret | Opcode::IRet => mnemonic,
Opcode::Push => format!("{mnemonic} {:?}", instr.src1_checked()),
Opcode::Pop => format!("{mnemonic} {:?}", instr.dest_checked()),
Opcode::Lli | Opcode::Lui => {
format!("{mnemonic} {:?}, {:#06x}", instr.dest_checked(), instr.imm16())
}
Opcode::Int => format!("{mnemonic} {:#06x}", instr.imm16()),
Opcode::Ldb | Opcode::Ldbs | Opcode::Ldh | Opcode::Ldhs | Opcode::Ldw
| Opcode::Stb | Opcode::Sth | Opcode::Stw | Opcode::Addi | Opcode::Subi => format!(
"{mnemonic} {:?}, {:?}, {:#06x}",
instr.src1_checked(), instr.dest_checked(), instr.imm16()
),
Opcode::Jez | Opcode::Jnz => {
let addr_reg = instr.dest_checked();
let target = static_jump_target(addr_reg, instr.imm16())
.and_then(|a| labels.get(&a).cloned());
match target {
Some(label) => format!("{mnemonic} {:?}, {label}", instr.src1_checked()),
None if addr_reg == Register::Zero => {
format!("{mnemonic} {:?}, {:#06x}", instr.src1_checked(), instr.imm16())
}
None => format!(
"{mnemonic} {:?}, {:#06x}, {addr_reg:?}",
instr.src1_checked(), instr.imm16()
),
}
}
Opcode::Jmp | Opcode::Call | Opcode::Jic | Opcode::Jnc => {
let addr_reg = instr.dest_checked();
let target = static_jump_target(addr_reg, instr.imm16())
.and_then(|a| labels.get(&a).cloned());
match target {
Some(label) => format!("{mnemonic} {label}"),
None if addr_reg == Register::Zero => format!("{mnemonic} {:#06x}", instr.imm16()),
None => format!("{mnemonic} {:#06x}, {addr_reg:?}", instr.imm16()),
}
}
Opcode::Mov | Opcode::Not => {
format!("{mnemonic} {:?}, {:?}", instr.src1_checked(), instr.dest_checked())
}
Opcode::CMov => format!(
"{mnemonic} {:?}, {:?}, {:?}",
instr.src1_checked(), instr.dest_checked(), instr.src2_checked()
),
Opcode::Add | Opcode::Sub | Opcode::And | Opcode::Nand | Opcode::Or | Opcode::Nor
| Opcode::Xor | Opcode::Xnor | Opcode::Ieq | Opcode::Ine | Opcode::Ilt | Opcode::Ile
| Opcode::Igt | Opcode::Ige => format!(
"{mnemonic} {:?}, {:?}, {:?}",
instr.src1_checked(), instr.src2_checked(), instr.dest_checked()
),
Opcode::Shl | Opcode::Shr => format!(
"{mnemonic} {:?}, {:?}, {:?}, {}",
instr.src1_checked(), instr.dest_checked(), instr.src2_checked(), instr.shamt()
),
}
}
fn build_data_lines(data: &[u8], data_labels: &HashMap<u32, String>) -> Vec<String> {
let mut out = Vec::new();
let mut i = 0;
while i < data.len() {
let addr = i as u32;
if let Some(label) = data_labels.get(&addr) {
out.push(format!("{label}:"));
}
// Try string detection only when this byte could actually start one.
if data[i] != 0 {
if let Some(end) = data[i..].iter().position(|&b| b == 0) {
let slice = &data[i..i + end];
if slice.iter().all(|&b| b.is_ascii_graphic() || b == b' ') {
out.push(format!(" db \"{}\"", String::from_utf8_lossy(slice).escape_default()));
i += end + 1; // skip the null terminator too
continue;
}
}
}
// Not a string start: consume only the run of zero bytes here,
// grouped into 4-byte words, stopping the instant a non-zero byte
// appears so we never swallow the start of the next string/value.
let run_end = data[i..]
.iter()
.position(|&b| b != 0)
.map(|p| i + p)
.unwrap_or(data.len());
if run_end == i {
// data[i] is non-zero but didn't form a printable/terminated
// string (e.g. binary constant) — dump one raw word.
let mut buf = [0u8; 4];
let take = (data.len() - i).min(4);
buf[..take].copy_from_slice(&data[i..i + take]);
out.push(format!(" dw {:#010x}", u32::from_le_bytes(buf)));
i += take;
} else {
let mut j = i;
while j < run_end {
let take = (run_end - j).min(4);
let mut buf = [0u8; 4];
buf[..take].copy_from_slice(&data[j..j + take]);
out.push(format!(" dw {:#010x}", u32::from_le_bytes(buf)));
j += take;
}
i = j;
}
}
out
}
impl fmt::Display for SectionKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Data => write!(f, "DATA"),
Self::Text => write!(f, "TEXT"),
Self::Abs => write!(f, "ABS"),
Self::Undef => write!(f, "UNDEF"),
}
}
}
impl fmt::Display for BindingKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Local => write!(f, "local"),
Self::Global => write!(f, "global"),
Self::Weak => write!(f, "weak"),
}
}
}
impl fmt::Display for SymbolLine {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"; {:<24} {:#010x} {:<5} {}",
self.name, self.address, self.section, self.binding
)
}
}
impl fmt::Display for Disassembly {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "; ==== symbols ====")?;
for sym in &self.symbols {
writeln!(f, "{sym}")?;
}
writeln!(f, "\n; ==== data ====")?;
for line in &self.data_lines {
writeln!(f, "{line}")?;
}
writeln!(f, "\n; ==== text ====")?;
for line in &self.text_lines {
writeln!(f, "{line}")?;
}
Ok(())
}
}
+348 -13
View File
@@ -1,15 +1,350 @@
mod internal; use common::isa::instructions::Opcode;
use common::prelude::{Instruction, Register};
use std::fs; use dsa_formats::binary_dse::DseExecutable;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::collections::HashMap; use std::collections::HashMap;
use common::formats::binary_dse::DseExecutable; use std::fmt;
use common::isa::instructions::{Instruction, Opcode};
use common::isa::register::Register;
pub use internal::{ #[derive(Debug)]
disassemble, Disassembly, BindingKind, SectionKind, SymbolLine pub struct Disassembly {
}; pub symbols: Vec<SymbolLine>,
pub text_lines: Vec<String>,
pub data_lines: Vec<String>,
}
#[derive(Debug)]
pub struct SymbolLine {
pub name: String,
pub address: u32,
pub section: SectionKind,
pub binding: BindingKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SectionKind { Data, Text, Abs, Undef }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BindingKind { Local, Global, Weak }
impl SectionKind {
fn from_raw(v: u8) -> Self {
match v { 1 => Self::Data, 2 => Self::Text, 0 => Self::Abs, _ => Self::Undef }
}
}
impl BindingKind {
fn from_raw(v: u8) -> Self {
match v { 1 => Self::Global, 2 => Self::Weak, _ => Self::Local }
}
}
pub fn disassemble(exe: &DseExecutable) -> Disassembly {
let symbols = build_symbol_lines(exe);
let mut text_labels: HashMap<u32, String> = HashMap::new();
let mut data_labels: HashMap<u32, String> = HashMap::new();
for sym in &symbols {
match sym.section {
SectionKind::Text => { text_labels.insert(sym.address, sym.name.clone()); }
SectionKind::Data => { data_labels.insert(sym.address, sym.name.clone()); }
_ => {}
}
}
// symbols can refer to either section by full resolved address (per your
// v1 loader convention: data lives right after text), so a single combined
// map is actually what address-resolution needs — keep both, but merge
// for lookups against full 32-bit addresses.
let mut all_labels = text_labels.clone();
all_labels.extend(data_labels.clone());
let text_lines = build_text_lines(&exe.text, &all_labels);
let data_lines = build_data_lines(&exe.data, &data_labels);
Disassembly { symbols, text_lines, data_lines }
}
fn build_symbol_lines(exe: &DseExecutable) -> Vec<SymbolLine> {
exe.symbols
.iter()
.map(|s| SymbolLine {
name: read_string(&exe.string_table, s.name_offset),
address: s.value,
section: SectionKind::from_raw(s.section),
binding: BindingKind::from_raw(s.binding),
})
.collect()
}
fn read_string(strtab: &[u8], offset: u32) -> String {
let start = offset as usize;
let end = strtab[start..].iter().position(|&b| b == 0).map(|p| start + p).unwrap_or(strtab.len());
String::from_utf8_lossy(&strtab[start..end]).into_owned()
}
/// Address a jump-like instruction actually targets, if statically known.
/// Returns None when the jump is register-relative (addr reg != Zero) since
/// that depends on runtime state and can't be resolved here.
fn static_jump_target(addr_reg: Register, imm: u16) -> Option<u32> {
(addr_reg == Register::Zero).then_some(imm as u32)
}
fn build_text_lines(words: &[u32], labels: &HashMap<u32, String>) -> Vec<String> {
let mut out = Vec::new();
let mut i = 0;
while i < words.len() {
let addr = 4 * i as u32;
if let Some(label) = labels.get(&addr) {
out.push(format!("{label}:"));
}
let instr = Instruction(words[i]);
let opcode = Opcode::from_u8(instr.opcode());
// Collapse Lli+Lui address-load pairs back into `lwi` (or a full
// ldx/stx pseudo-op if a matching mem instruction follows), mirroring
// what expand_ldx/expand_stx/expand_lwi originally expanded from.
if let Some(Opcode::Lli) = opcode {
if let Some(collapsed) = try_collapse_addr_load(words, i, labels) {
out.push(format!(" {}", collapsed.text));
i += collapsed.consumed;
continue;
}
}
out.push(format!(" {}", render_instruction(instr, opcode, labels)));
i += 1;
}
out
}
struct Collapsed {
text: String,
consumed: usize,
}
fn try_collapse_addr_load(
words: &[u32],
i: usize,
labels: &HashMap<u32, String>,
) -> Option<Collapsed> {
let lli = Instruction(words[i]);
let lui = Instruction(*words.get(i + 1)?);
if Opcode::from_u8(lui.opcode())? as u8 != Opcode::Lui as u8 {
return None;
}
if lui.dest_checked() != lli.dest_checked() {
return None;
}
let dest = lli.dest_checked();
let full_addr = (lli.imm16() as u32) | ((lui.imm16() as u32) << 16);
let addr_text = labels.get(&full_addr).cloned().unwrap_or_else(|| format!("{full_addr:#010x}"));
// Check whether a mem op immediately follows, referencing `dest` —
// that's the ldb/ldh/ldw/stb/sth/stw expansion pattern, not a bare lwi.
if let Some(&mem_word) = words.get(i + 2) {
let mem = Instruction(mem_word);
if let Some(mem_op) = Opcode::from_u8(mem.opcode()) {
let is_mem_op = matches!(
mem_op,
Opcode::Ldb | Opcode::Ldbs | Opcode::Ldh | Opcode::Ldhs | Opcode::Ldw
| Opcode::Stb | Opcode::Sth | Opcode::Stw
);
if is_mem_op && (mem.src1_checked() == dest || mem.dest_checked() == dest) {
let mnemonic = format!("{mem_op:?}").to_lowercase();
let other = if mem.src1_checked() == dest { mem.dest_checked() } else { mem.src1_checked() };
return Some(Collapsed {
text: format!("{mnemonic} {addr_text}, {other:?}, {:#06x}", mem.imm16()),
consumed: 3,
});
}
}
}
Some(Collapsed {
text: format!("lwi {addr_text}, {dest:?}"),
consumed: 2,
})
}
fn render_instruction(instr: Instruction, opcode: Option<Opcode>, labels: &HashMap<u32, String>) -> String {
let Some(op) = opcode else {
return format!("; INVALID OPCODE {:#04x} (word {:#010x})", instr.opcode(), instr.0);
};
let mnemonic = format!("{op:?}").to_lowercase();
match op {
Opcode::Nop | Opcode::Hlt | Opcode::Ret | Opcode::IRet => mnemonic,
Opcode::Push => format!("{mnemonic} {:?}", instr.src1_checked()),
Opcode::Pop => format!("{mnemonic} {:?}", instr.dest_checked()),
Opcode::Lli | Opcode::Lui => {
format!("{mnemonic} {:?}, {:#06x}", instr.dest_checked(), instr.imm16())
}
Opcode::Int => format!("{mnemonic} {:#06x}", instr.imm16()),
Opcode::Ldb | Opcode::Ldbs | Opcode::Ldh | Opcode::Ldhs | Opcode::Ldw
| Opcode::Stb | Opcode::Sth | Opcode::Stw | Opcode::Addi | Opcode::Subi => format!(
"{mnemonic} {:?}, {:?}, {:#06x}",
instr.src1_checked(), instr.dest_checked(), instr.imm16()
),
Opcode::Jez | Opcode::Jnz => {
let addr_reg = instr.dest_checked();
let target = static_jump_target(addr_reg, instr.imm16())
.and_then(|a| labels.get(&a).cloned());
match target {
Some(label) => format!("{mnemonic} {:?}, {label}", instr.src1_checked()),
None if addr_reg == Register::Zero => {
format!("{mnemonic} {:?}, {:#06x}", instr.src1_checked(), instr.imm16())
}
None => format!(
"{mnemonic} {:?}, {:#06x}, {addr_reg:?}",
instr.src1_checked(), instr.imm16()
),
}
}
Opcode::Jmp | Opcode::Call | Opcode::Jic | Opcode::Jnc => {
let addr_reg = instr.dest_checked();
let target = static_jump_target(addr_reg, instr.imm16())
.and_then(|a| labels.get(&a).cloned());
match target {
Some(label) => format!("{mnemonic} {label}"),
None if addr_reg == Register::Zero => format!("{mnemonic} {:#06x}", instr.imm16()),
None => format!("{mnemonic} {:#06x}, {addr_reg:?}", instr.imm16()),
}
}
Opcode::Mov | Opcode::Not => {
format!("{mnemonic} {:?}, {:?}", instr.src1_checked(), instr.dest_checked())
}
Opcode::CMov => format!(
"{mnemonic} {:?}, {:?}, {:?}",
instr.src1_checked(), instr.dest_checked(), instr.src2_checked()
),
Opcode::Add | Opcode::Sub | Opcode::And | Opcode::Nand | Opcode::Or | Opcode::Nor
| Opcode::Xor | Opcode::Xnor | Opcode::Ieq | Opcode::Ine | Opcode::Ilt | Opcode::Ile
| Opcode::Igt | Opcode::Ige => format!(
"{mnemonic} {:?}, {:?}, {:?}",
instr.src1_checked(), instr.src2_checked(), instr.dest_checked()
),
Opcode::Shl | Opcode::Shr => format!(
"{mnemonic} {:?}, {:?}, {:?}, {}",
instr.src1_checked(), instr.dest_checked(), instr.src2_checked(), instr.shamt()
),
}
}
fn build_data_lines(data: &[u8], data_labels: &HashMap<u32, String>) -> Vec<String> {
let mut out = Vec::new();
let mut i = 0;
while i < data.len() {
let addr = i as u32;
if let Some(label) = data_labels.get(&addr) {
out.push(format!("{label}:"));
}
// Try string detection only when this byte could actually start one.
if data[i] != 0 {
if let Some(end) = data[i..].iter().position(|&b| b == 0) {
let slice = &data[i..i + end];
if slice.iter().all(|&b| b.is_ascii_graphic() || b == b' ') {
out.push(format!(" db \"{}\"", String::from_utf8_lossy(slice).escape_default()));
i += end + 1; // skip the null terminator too
continue;
}
}
}
// Not a string start: consume only the run of zero bytes here,
// grouped into 4-byte words, stopping the instant a non-zero byte
// appears so we never swallow the start of the next string/value.
let run_end = data[i..]
.iter()
.position(|&b| b != 0)
.map(|p| i + p)
.unwrap_or(data.len());
if run_end == i {
// data[i] is non-zero but didn't form a printable/terminated
// string (e.g. binary constant) — dump one raw word.
let mut buf = [0u8; 4];
let take = (data.len() - i).min(4);
buf[..take].copy_from_slice(&data[i..i + take]);
out.push(format!(" dw {:#010x}", u32::from_le_bytes(buf)));
i += take;
} else {
let mut j = i;
while j < run_end {
let take = (run_end - j).min(4);
let mut buf = [0u8; 4];
buf[..take].copy_from_slice(&data[j..j + take]);
out.push(format!(" dw {:#010x}", u32::from_le_bytes(buf)));
j += take;
}
i = j;
}
}
out
}
impl fmt::Display for SectionKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Data => write!(f, "DATA"),
Self::Text => write!(f, "TEXT"),
Self::Abs => write!(f, "ABS"),
Self::Undef => write!(f, "UNDEF"),
}
}
}
impl fmt::Display for BindingKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Local => write!(f, "local"),
Self::Global => write!(f, "global"),
Self::Weak => write!(f, "weak"),
}
}
}
impl fmt::Display for SymbolLine {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"; {:<24} {:#010x} {:<5} {}",
self.name, self.address, self.section, self.binding
)
}
}
impl fmt::Display for Disassembly {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "; ==== symbols ====")?;
for sym in &self.symbols {
writeln!(f, "{sym}")?;
}
writeln!(f, "\n; ==== data ====")?;
for line in &self.data_lines {
writeln!(f, "{line}")?;
}
writeln!(f, "\n; ==== text ====")?;
for line in &self.text_lines {
writeln!(f, "{line}")?;
}
Ok(())
}
}
+12 -7
View File
@@ -1,9 +1,10 @@
use std::fs::File; use std::fs;
use std::io::Write;
use std::path::PathBuf;
use clap::Parser; use clap::Parser;
use disassembler::disassemble; use disassembler::disassemble;
use common::formats::binary_dse::DseExecutable; use dsa_formats::binary_dse::DseExecutable;
use std::fs::File;
use std::io::{Cursor, Write};
use std::path::PathBuf;
#[derive(Parser, Debug, Clone)] #[derive(Parser, Debug, Clone)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
@@ -17,11 +18,15 @@ struct Args {
fn main() { fn main() {
let args = Args::parse(); let args = Args::parse();
let Ok(exec) = DseExecutable::load(args.input_path) else { let Ok(contents) = fs::read(args.input_path) else {
panic!("Failed to read the input file")
};
let Ok(exec) = DseExecutable::read(&mut Cursor::new(contents)) else {
panic!("Failed to load the input file") panic!("Failed to load the input file")
}; };
let result = disassemble(&exec); let result = disassemble(&exec);
File::create(args.output_path).unwrap() let _bytes_written = File::create(args.output_path).expect("Failed to create the output file")
.write(result.to_string().as_bytes()).unwrap(); .write(result.to_string().as_bytes()).expect("Failed to write to output file");
} }
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "dsa_formats"
edition.workspace = true
version.workspace = true
authors.workspace = true
[dependencies]
binrw = "0.15.2"
common = { path = "../common" }
@@ -1,10 +1,9 @@
use std::fs::File; use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom, Write}; use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use std::path::PathBuf; use std::path::PathBuf;
use binrw::{binrw, BinRead, binwrite, NullString, BinReaderExt, BinWriterExt, BinWrite}; use binrw::{binrw, BinRead, BinReaderExt, BinWriterExt, BinWrite};
use std::fmt; use std::fmt;
use crate::prelude::Instruction; use common::isa::instructions::Instruction;
#[binrw] #[binrw]
#[brw(magic = b"DSAX", little)] #[brw(magic = b"DSAX", little)]
@@ -48,11 +47,6 @@ pub struct DseExecutable {
} }
impl DseExecutable { impl DseExecutable {
pub fn load(path: PathBuf) -> binrw::BinResult<Self> {
let mut file = File::open(path).map_err(|e| binrw::Error::Io(e))?;
Self::read(&mut file)
}
pub fn read<R: Read + Seek>(reader: &mut R) -> binrw::BinResult<Self> { pub fn read<R: Read + Seek>(reader: &mut R) -> binrw::BinResult<Self> {
let header = DseHeader::read(reader)?; let header = DseHeader::read(reader)?;
@@ -76,13 +70,7 @@ impl DseExecutable {
Ok(DseExecutable { header, symbols, string_table, data, text }) Ok(DseExecutable { header, symbols, string_table, data, text })
} }
pub fn save(&self, path: PathBuf) -> binrw::BinResult<()> {
let mut file = File::create(path)
.map_err(|e| binrw::Error::Io(e))?;
self.write(&mut file)
}
pub fn write<W: Write + Seek>(&self, writer: &mut W) -> binrw::BinResult<()> { pub fn write<W: Write + Seek>(&self, writer: &mut W) -> binrw::BinResult<()> {
self.header.write(writer)?; self.header.write(writer)?;
+12
View File
@@ -0,0 +1,12 @@
pub mod binary_dse;
pub mod binary_dso;
#[cfg(test)]
mod tests {
// #[test]
// fn it_works() {
// let result = add(2, 2);
// assert_eq!(result, 4);
// }
}
@@ -0,0 +1,45 @@
use crate::EmulatorHandle;
use crate::memory::PhysAddr;
pub struct Display {
width: u32,
height: u32,
addr: PhysAddr,
emulator: EmulatorHandle,
buffer: Vec<u8>,
}
impl Display {
pub fn new(width: u32, height: u32, addr: PhysAddr, emulator: EmulatorHandle) -> Self {
Self {
width,
height,
addr,
emulator,
buffer: Vec::new(),
}
}
pub fn width(&self) -> usize {
self.width as usize
}
pub fn height(&self) -> usize {
self.height as usize
}
pub fn changed(&mut self) -> bool {
let temp = self.read();
if temp != self.buffer {
self.buffer = temp;
return true;
}
false
}
pub fn read(&mut self) -> Vec<u8> {
self.emulator.mem_read(self.addr, self.width * self.height)
}
}
@@ -0,0 +1,35 @@
use crate::EmulatorHandle;
use crate::memory::PhysAddr;
pub struct FrameBuffer {
width: usize,
height: usize,
addr: PhysAddr,
emulator: EmulatorHandle,
}
impl FrameBuffer {
pub fn new(width: u32, height: u32, addr: PhysAddr, emulator: EmulatorHandle) -> Self {
let width = width as usize;
let height = height as usize;
Self {
width,
height,
addr,
emulator,
}
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
pub fn read(&self) -> Vec<u8> {
self.emulator.mem_read(self.addr, ( self.width * self.height * 4 ) as u32)
}
}
@@ -0,0 +1,3 @@
pub mod display;
pub mod framebuffer;
pub mod serial;
@@ -1,18 +1,8 @@
use std::{ use crate::EmulatorHandle;
collections::VecDeque, use crate::memory::PhysAddr;
sync::{ use crate::processor::Interrupt;
mpsc::{Receiver, Sender}, use std::collections::VecDeque;
}, use std::sync::mpsc::{Receiver, Sender};
};
use std::any::Any;
use egui::{Ui, WidgetText};
use egui_tiles::TileId;
use emulator_core::{memory::{
PhysAddr,
}, EmulatorHandle};
use emulator_core::processor::Interrupt;
use crate::ui::Action;
use crate::ui::panels::TilePane;
/// ## SERIAL LAYOUT: (from the perspective of code inside the ui) /// ## SERIAL LAYOUT: (from the perspective of code inside the ui)
/// - [base+0x00]: ser_out valid flag - set by kernel after writing a byte to serial /// - [base+0x00]: ser_out valid flag - set by kernel after writing a byte to serial
@@ -97,35 +87,4 @@ impl Serial {
self.read_buff.push(byte); self.read_buff.push(byte);
} }
} }
} }
impl TilePane for Serial {
fn title(&self) -> WidgetText {
"Serial".into()
}
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
self.update();
// Text field for input and a send button
ui.text_edit_multiline(&mut self.write_buff);
if ui.button("Send").clicked() {
self.write_buff.push(0 as char);
for byte in self.write_buff.bytes() {
let _ = self.write_tx.send(byte);
}
self.write_buff.clear();
}
// Display received data
ui.label(String::from_utf8(self.read_buff.clone()).unwrap());
if ui.button("Clear").clicked() {
self.read_buff.clear();
}
None
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
+4 -3
View File
@@ -15,6 +15,7 @@ use crate::processor::{Interrupt, RunningState, SharedState};
pub mod processor; pub mod processor;
pub mod memory; pub mod memory;
pub mod config; pub mod config;
pub mod devices;
#[derive(Debug)] #[derive(Debug)]
pub enum DsaError { pub enum DsaError {
@@ -41,7 +42,7 @@ pub trait DsaImplementation: Send {
/// IO mapping also /// IO mapping also
fn apply_io_mapping(&mut self, map: IoMap); fn apply_io_mapping(&mut self, map: IoMap);
/// List all memory devices /// List all memory io_devices
/// returns a copy of the IOmap (cannot be a reference since across-threads) /// returns a copy of the IOmap (cannot be a reference since across-threads)
fn get_io_map(&self) -> IoMap; fn get_io_map(&self) -> IoMap;
@@ -64,7 +65,7 @@ pub struct EmulatorHandle {
impl EmulatorHandle { impl EmulatorHandle {
pub fn read_byte(&mut self, addr: PhysAddr) -> u8 { pub fn read_byte(&self, addr: PhysAddr) -> u8 {
self.memory.read_byte(addr) self.memory.read_byte(addr)
} }
@@ -72,7 +73,7 @@ impl EmulatorHandle {
self.memory.write_byte(addr, val); self.memory.write_byte(addr, val);
} }
pub fn mem_read(&mut self, addr: PhysAddr, size: u32) -> Vec<u8> { pub fn mem_read(&self, addr: PhysAddr, size: u32) -> Vec<u8> {
self.memory.read_bytes(addr, size as usize).to_vec() self.memory.read_bytes(addr, size as usize).to_vec()
} }
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::memory::{FaultInfo, PhysAddr, VirtAddr, tlb::TLB}; use crate::memory::{FaultInfo, PhysAddr, VirtAddr, tlb::TLB};
pub struct MMU { pub(crate) struct MMU {
buffer: TLB, buffer: TLB,
enable_paging: bool, enable_paging: bool,
+2 -33
View File
@@ -2,32 +2,6 @@ use std::{
alloc::{Layout, alloc_zeroed, dealloc}, alloc::{Layout, alloc_zeroed, dealloc},
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
}; };
#[cfg(feature = "mainstore-arraymap")]
pub mod arraymap;
#[cfg(feature = "mainstore-arraymap")]
pub use arraymap::ArrayStore;
#[cfg(feature = "mainstore-hashmap")]
pub mod hashmap;
#[cfg(feature = "mainstore-hashmap")]
pub use hashmap::HashStore;
#[cfg(feature = "mainstore-stackarray")]
pub mod stackarray;
#[cfg(feature = "mainstore-stackarray")]
pub use stackarray::StackArrayStore;
#[cfg(feature = "mainstore-bulkalloc")]
pub mod bulkalloc;
#[cfg(feature = "mainstore-bulkalloc")]
pub use bulkalloc::BulkAllocStore;
// #[cfg(feature = "mainstore-prealloc")]
// pub mod prealloc;
// #[cfg(feature = "mainstore-prealloc")]
// pub use prealloc::PreAllocStore;
use crate::memory::PhysAddr; use crate::memory::PhysAddr;
#[repr(transparent)] #[repr(transparent)]
@@ -91,12 +65,7 @@ impl TryFrom<Vec<u8>> for Page {
} }
} }
#[inline(always)] pub(crate) struct MemoryBank {
const fn idx(addr: PhysAddr) -> (usize, usize) {
((addr >> 12) as usize, (addr & 0xFFF) as usize)
}
pub struct MemoryBank {
heap: *mut u8, heap: *mut u8,
} }
@@ -106,7 +75,7 @@ impl MemoryBank {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
heap: unsafe { heap: unsafe {
// allocate the full 32 bit address range. // allocate the full 32-bit address range.
alloc_zeroed(Layout::from_size_align(u32::MAX as usize, 4096).unwrap()) alloc_zeroed(Layout::from_size_align(u32::MAX as usize, 4096).unwrap())
}, },
} }
+10 -25
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "emulator_ui" name = "emulator_ui"
version = "0.3.0" version.workspace = true
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true
@@ -12,36 +12,21 @@ path = "src/main.rs"
name = "dsa" name = "dsa"
path = "src/lib.rs" path = "src/lib.rs"
[dev-dependencies]
criterion = { version = "0.8.2", features = ["html_reports"] }
[dependencies] [dependencies]
arc-swap = "1.8.2"
clap = { version = "4.5.60", features = ["derive"] }
fxhash = "0.2.1"
ron = "0.12.0"
serde = { version = "1.0.228", features = ["derive"] }
common = { path = "../../common" } common = { path = "../../common" }
dsa_formats = { path = "../../dsa_formats" }
assembler = { path = "../../assembler" }
emulator_core = { version = "0.3.0", path = "../emulator_core" }
disassembler = { version = "0.3.0", path = "../../disassembler" }
clap = { version = "4.5.60", features = ["derive"] }
ron = "0.12.0"
egui = "0.35.0" egui = "0.35.0"
eframe = "0.35.0" eframe = "0.35.0"
egui_tiles = "0.16.0" egui_tiles = "0.16.0"
egui_code_editor = "0.3.7" egui_code_editor = "0.3.7"
egui_file = "0.27.0" egui_file = "0.27.0"
dirs = "6.0.0"
assembler = { path = "../../assembler" }
egui_extras = "0.35.0" egui_extras = "0.35.0"
emulator_core = { version = "0.3.0", path = "../emulator_core" }
disassembler = { version = "0.3.0", path = "../../disassembler" }
egui-file-dialog = "0.14.1" egui-file-dialog = "0.14.1"
egui_commonmark = { version = "0.24.0", features = ["better_syntax_highlighting", "macros"] } egui_commonmark = { version = "0.24.0", features = ["better_syntax_highlighting", "macros"] }
[features]
default = ["mainstore-prealloc"]
# Memory Bank Features
mainstore-bulkalloc = [] # Fastest for Writes
mainstore-prealloc = [] # Fastest for Reads
mainstore-stackarray = [] # Slightly outperforms ArrayMap but requires a large stack
mainstore-arraymap = [] # Simple implementation
mainstore-hashmap = [] # Old implementation, no raw pointers. Uses a hashmap
+1 -1
View File
@@ -2,7 +2,7 @@ use std::{fs, path::PathBuf};
use clap::Parser; use clap::Parser;
use emulator_core::config::{IoMap, IoMapping, MemoryMap}; use emulator_core::config::{IoMap, MemoryMap};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
+15 -3
View File
@@ -1,6 +1,18 @@
#![feature(likely_unlikely)] use crate::ui::DamnSimpleUI;
#![feature(inherent_associated_types)] use eframe::NativeOptions;
#![feature(str_as_str)] use emulator_core::EmulatorController;
pub mod ui; pub mod ui;
pub mod args; pub mod args;
pub mod state;
pub fn run_app(mut controller: EmulatorController) -> eframe::Result<()> {
// initialise emulator
controller.run().unwrap();
eframe::run_native(
"Damn Simple Architecture",
NativeOptions::default(),
Box::new(|cc| Ok(Box::new(DamnSimpleUI::new(cc, controller)))),
)
}
+3 -45
View File
@@ -1,7 +1,7 @@
use clap::Parser; use clap::Parser;
use dsa::args::DsaArgs; use dsa::args::DsaArgs;
use dsa::ui::run_app; use dsa::run_app;
use emulator_core::{DsaImplementation, EmulatorController, processor::Emulator}; use emulator_core::{DsaImplementation, EmulatorController, processor::Emulator};
fn main() { fn main() {
@@ -9,49 +9,7 @@ fn main() {
let mut emulator = Emulator::new(); let mut emulator = Emulator::new();
emulator.apply_memory_map(args.get_memory_map()); emulator.apply_memory_map(args.get_memory_map());
let controller = EmulatorController::new(Box::new(emulator)); let controller = EmulatorController::new(Box::new(emulator));
// run_app(controller).unwrap();
// if let Some(bin) = args.get_binary() { }
// for (i, ch) in bin.chunks(4).enumerate() {
// let instruction = Instruction(u32::from_le_bytes(ch.try_into().unwrap()));
// let hex = ch
// .iter()
// .map(|b| format!("{:02X}", b))
// .collect::<Vec<_>>()
// .join(" ");
// let chars = ch
// .iter()
// .map(|b| {
// if b.is_ascii_graphic() {
// *b as char
// } else {
// '.'
// }
// })
// .collect::<String>();
// println!("{:04X}: {:<12} {:4} {:?}", i * 4, hex, chars, instruction);
// }
//
// let program: Vec<Page> = Page::paginate(&bin).collect();
//
// for (i, page) in program.iter().enumerate() {
// emulator.memory_mut().write_page(i as u32 * 4096, &page);
// }
// }
if args.headless() {
run_server(&args);
} else {
run_app(controller).unwrap()
}
}
fn run_server(args: &DsaArgs) {
if args.headless() {
println!("Running in headless mode");
} else {
println!("Running in GUI mode");
}
}
+29
View File
@@ -0,0 +1,29 @@
use crate::ui::process_manager::ProcessSpawner;
use egui_tiles::TileId;
use std::fmt::Debug;
use std::path::PathBuf;
/// the action enum forms the core of the application's state.
/// - Actions are used as the main way that Tiles inside the application pass state updates back to the parent UI,
/// specifically when those state updates involve other files. for example, pressing "build" in the editor would emit
/// a SpawnProcess action, running some task which on completion emits a LoadBinary action on the result
pub enum Action {
None,
RequestCloseTab(TileId),
CloseTab(TileId),
Quit,
OpenFile(PathBuf),
LoadBinary(DataSource),
SaveFile(PathBuf, TileId),
RequestSaveAs(TileId),
ShowError(String),
SpawnProcess(ProcessSpawner)
}
#[derive(Debug, Clone)]
pub enum DataSource {
File(PathBuf),
Data(Vec<u8>),
}
@@ -0,0 +1,43 @@
pub fn configure_appearance(ctx: &egui::Context) {
// configure appearance of UI elements
let mut visuals = egui::Visuals::dark();
visuals.window_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.panel_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.widgets.inactive.fg_stroke =
egui::Stroke::from((1.0, egui::Color32::from_rgb(255, 255, 255)));
visuals.widgets.inactive.bg_stroke =
egui::Stroke::from((1.0, egui::Color32::from_rgb(60, 60, 60)));
visuals.widgets.inactive.corner_radius = egui::CornerRadius::from(4);
visuals.widgets.inactive.bg_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.widgets.inactive.weak_bg_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.widgets.inactive.expansion = 1.0;
// tile resize handle
visuals.window_stroke = egui::Stroke::new(1.0, egui::Color32::from_rgb(45, 45, 45));
visuals.extreme_bg_color = egui::Color32::from_rgb(20, 20, 20);
ctx.set_visuals(visuals);
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert(
"JetBrains Mono Nerd Font".to_string(),
std::sync::Arc::new(egui::FontData::from_static(include_bytes!(
"../../font/JetBrainsMonoNerdFontMono_Regular.ttf",
))),
);
fonts
.families
.entry(egui::FontFamily::Proportional)
.or_default()
.insert(0, "JetBrains Mono Nerd Font".to_string());
fonts
.families
.entry(egui::FontFamily::Monospace)
.or_default()
.insert(0, "JetBrains Mono Nerd Font".to_string());
ctx.set_fonts(fonts);
}
@@ -1,697 +0,0 @@
use std::fmt::Write;
use std::fs::{self};
use std::io::Cursor;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::{
ffi::OsStr,
path::{Path, PathBuf},
};
pub mod syntax;
use super::Component;
use assembler::prelude::Assembler;
use common::formats::binary_dse::DseExecutable;
use common::prelude::Instruction;
use egui::{Align, Context, Key, Layout, Ui};
use egui_code_editor::{CodeEditor, ColorTheme, Completer, Syntax};
use egui_extras::{Column, TableBuilder};
use egui_file::FileDialog;
use emulator_core::{
memory::ram::{MemoryBank, Page},
processor::state::SharedState
};
use crate::ui::components::memory::MemoryViewer;
pub struct Editor {
state: Arc<SharedState>,
memory: MemoryBank,
// editor state
path: Option<PathBuf>,
unsaved: bool,
text: String,
buffer: String,
// editor widget
editor: CodeEditor,
completer: Completer,
syntax: Syntax,
// output / loading
output: Vec<u8>,
viewer: MemoryViewer,
load_offset: u32,
offset_str: String,
// file dialogs
open_file_dialog: Option<FileDialog>,
save_file_dialog: Option<FileDialog>,
// other
visible: bool,
show_output: bool,
error: Option<String>,
}
impl Component for Editor {
fn title(&self) -> &'static str {
"Editor"
}
fn visible(&mut self) -> &mut bool {
&mut self.visible
}
fn ui(&mut self, ui: &mut Ui) {
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(ui);
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| {
if self.show_output {
egui::Panel::right("Editor Output").resizable(false).show_inside(ui, |ui| {
self.viewer.ui(ui, self.output.as_slice(), 0);
});
}
self.render_editor(ui);
},
);
self.render_bottom_bar(ui);
});
}
}
impl Editor {
#[must_use]
pub fn new(state: Arc<SharedState>, memory: MemoryBank) -> Self {
Self {
memory,
state,
path: None,
text: String::new(),
buffer: String::new(),
completer: Completer::new_with_syntax(&Syntax::default()).with_user_words(),
syntax: Syntax::default(),
editor: CodeEditor::default()
.id_source("editor")
.with_fontsize(12.0)
.with_rows(0)
.with_theme(THEME)
.with_numlines(true),
output: Vec::new(),
viewer: MemoryViewer::new(),
unsaved: true,
visible: false,
load_offset: 0,
offset_str: String::new(),
error: None,
open_file_dialog: None,
save_file_dialog: None,
show_output: false,
}
}
fn filename(&self) -> &str {
if let Some(path) = &self.path {
return path
.file_name()
.unwrap_or_else(|| OsStr::new("Unnamed!"))
.to_str()
.unwrap_or_else(|| unreachable!("File name should be valid UTF-8."));
}
"Unnamed!"
}
fn extension(&self) -> &str {
if let Some(path) = &self.path {
return path
.extension()
.unwrap_or_else(|| OsStr::new("Unknown!"))
.to_str()
.unwrap_or_else(|| unreachable!("File name should be valid UTF-8."));
}
"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().initial_path(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() {
let mut dialog = FileDialog::open_file();
if let Some(p) = &self.path
&& let Some(path) = p.parent().map(Path::to_path_buf)
{
dialog.set_path(path);
} else {
dialog.set_path(work_dir);
}
dialog.open();
self.open_file_dialog = Some(dialog);
}
self.syntax = match self.extension() {
"dsa" => syntax::dsa(),
"dsc" => syntax::dsc(),
"rs" => Syntax::rust(),
_ => Syntax::default(),
};
self.completer = Completer::new_with_syntax(&self.syntax).with_user_words();
}
fn handle_file_dialogs(&mut self, ui: &Ui) {
// Handle open dialog
if let Some(dialog) = &mut self.open_file_dialog
&& dialog.show(ui).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(ui).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_editor(&mut self, ui: &mut Ui) {
let available_width = ui.available_width();
let mut editor = self.editor.clone().desired_width(available_width);
editor.show_with_completer(ui, &mut self.text, &self.syntax, &mut self.completer);
}
fn render_bottom_bar(&self, ui: &mut Ui) {
ui.horizontal(|ui| {
// error display
ui.label(
egui::RichText::new(self.error.clone().unwrap_or_default())
.color(egui::Color32::RED),
);
});
}
fn build(&mut self) {
if let Some(path) = &self.path {
match path.extension().and_then(|ext| ext.to_str()) {
Some("dsa") => {
let mut assembler = Assembler::new(path);
assembler.start(());
// Or block until done
self.output = match assembler.output() {
Ok(instructions) => instructions,
Err(e) => {
self.error = Some(e.to_string());
return;
}
};
}
// Some("dsc") => {
// let dsa_path = Path::new(path).with_extension("dsa");
// let mut compiler = Compiler::new(path);
// let is_lib = false;
// compiler.start(is_lib);
// if let Err(e) = compiler.write_result(&dsa_path) {
// self.error = Some(e.to_string());
// return;
// }
// let mut assembler = Assembler::new(&dsa_path);
// assembler.start(());
// // Or block until done
// self.output = match assembler.output() {
// Ok(instructions) => instructions,
// Err(e) => {
// self.error = Some(format!("Assembler error: {e}"));
// return;
// }
// };
// }
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, ui: &mut Ui) {
self.handle_file_dialogs(ui);
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
// ui.add_enabled(false, egui::Button::new("Open"));
if ui.button("Open").clicked() {
self.open();
}
// Saves the current file
// ui.add_enabled(false, egui::Button::new("Save"));
if ui.button("Save").clicked() {
self.save();
}
// // builds the current file
// ui.add_enabled(false, egui::Button::new("Build"));
if ui.button("Build").clicked() && !self.unsaved {
self.build();
}
// // Loads the generated binary into the assembler at the provided offset
// ui.add_enabled(false, egui::Button::new("Load"));
if ui.button("Load").clicked() {
if self.error.is_some() {
self.error = Some("Can't load program at invalid offset!".to_string());
}
let mut reader = Cursor::new(self.output.clone());
let Ok(executable) = DseExecutable::read(&mut reader) else {
self.error = Some("Failed to read program!".to_string());
return
};
let image: Vec<Page> = Page::paginate(&executable.to_memory_image()).collect();
self.state.reseting.store(true, Ordering::Relaxed);
self.memory.clear();
for (i, page) in image.iter().enumerate() {
self.memory.write_page(i as u32 * 4096, &page);
}
self.state.reseting.store(false, Ordering::Relaxed);
}
// 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());
}
}
ui.checkbox(&mut self.show_output, "Show Output");
});
}
}
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(),
)
}
pub const THEME: ColorTheme = ColorTheme {
name: "Theme",
dark: true,
bg: "1b1b1b",
cursor: "de5252", // fg4
selection: "28323B", // bg2
comments: "444444", // gray1
functions: "7CCCC7", // green1
keywords: "6C81E0", // red1
literals: "A3ABFF", // fg1
numerics: "8A46CF", // purple1
punctuation: "99C9C9", // orange1
strs: "618c84", // aqua1
types: "B8B9D4", // yellow1
special: "de5252", // blue1
};
//
// struct Output {
// pub data: Vec<u8>,
//
// show_bytes: bool,
// show_words: bool,
// show_ascii: bool,
// show_instruction: bool,
// show_decimal: bool,
// }
//
// impl Output {
// fn new() -> Self {
// Self {
// data: vec![],
// show_bytes: true,
// show_words: true,
// show_ascii: true,
// show_instruction: true,
// show_decimal: true,
// }
// }
//
// fn mut_data(&mut self) -> &mut Vec<u8> {
// &mut self.data
// }
//
// fn data(&self) -> &Vec<u8> {
// &self.data
// }
//
// fn ui(&mut self, ui: &mut Ui) {
// // Output area with synchronized scrolling
// egui::ScrollArea::vertical()
// .id_salt("output_scroll")
// .max_width(400.0)
// .show(ui, |ui| {
// if self.data.is_empty() {
// ui.label(
// egui::RichText::new("No output data")
// .font(egui::FontId::monospace(12.0))
// .color(egui::Color32::GRAY),
// );
// return;
// }
//
// ui.horizontal(|ui| {
// ui.toggle_value(&mut self.show_bytes, "Bytes");
// ui.toggle_value(&mut self.show_words, "Words");
// ui.toggle_value(&mut self.show_decimal, "Decimal");
// ui.toggle_value(&mut self.show_ascii, "Ascii");
// ui.toggle_value(&mut self.show_instruction, "Instructions");
// });
//
// ui.separator();
//
// let mut table = TableBuilder::new(ui)
// .resizable(false) // makes all columns resizable at once
// .column(Column::initial(55.0)); // addr
//
// if self.show_bytes {
// table = table.column(Column::initial(90.0));
// }
// if self.show_words {
// table = table.column(Column::initial(75.0));
// }
// if self.show_decimal {
// table = table.column(Column::initial(80.0));
// }
// if self.show_ascii {
// table = table.column(Column::initial(50.0));
// }
// if self.show_instruction {
// table = table.column(Column::initial(200.0).clip(true));
// }
//
// table
// .header(20.0, |mut header| {
// header.col(|ui| {
// ui.label("Addr");
// });
// if self.show_bytes {
// header.col(|ui| {
// ui.label("Bytes");
// });
// }
// if self.show_words {
// header.col(|ui| {
// ui.label("Word");
// });
// }
// if self.show_decimal {
// header.col(|ui| {
// ui.label("Decimal");
// });
// }
// if self.show_ascii {
// header.col(|ui| {
// ui.label("Ascii");
// });
// }
// if self.show_instruction {
// header.col(|ui| {
// ui.label("Instruction");
// });
// }
// })
// .body(|mut body| {
// // Process bytes in chunks of 4
// for (line_num, chunk) in self.data.chunks(4).enumerate() {
// body.row(18.0, |mut row| {
// let address = line_num * 4;
// let mut buf = [0u8; 4];
// buf[..chunk.len()].copy_from_slice(chunk);
// let value = u32::from_le_bytes(buf);
//
// // Address column
// row.col(|ui| {
// ui.label(
// egui::RichText::new(format!("0x{address:04X}"))
// .font(egui::FontId::monospace(12.0)),
// );
// });
//
// if self.show_bytes {
// row.col(|ui| {
// // Individual bytes column
// ui.label(
// egui::RichText::new(
// chunk
// .iter()
// .map(|b| format!("{b:02X}"))
// .collect::<Vec<_>>()
// .join(" "),
// )
// .font(egui::FontId::monospace(12.0))
// .color(egui::Color32::from_rgb(200, 200, 255)),
// );
// });
// }
//
// if self.show_words {
// row.col(|ui| {
// // Hex column
// ui.label(
// egui::RichText::new(format!("0x{value:08X}"))
// .font(egui::FontId::monospace(12.0))
// .color(egui::Color32::from_rgb(255, 100, 100)),
// );
// });
// }
//
// if self.show_decimal {
// row.col(|ui| {
// ui.label(
// egui::RichText::new(format!("{value}"))
// .font(egui::FontId::monospace(12.0))
// .color(egui::Color32::from_rgb(100, 255, 100)),
// );
// });
// }
//
// if self.show_ascii {
// let ascii = value.to_le_bytes();
// row.col(|ui| {
// ui.label(
// egui::RichText::new(String::from_utf8_lossy(&ascii))
// .font(egui::FontId::monospace(12.0))
// .color(egui::Color32::from_rgb(100, 255, 255)),
// );
// });
// }
//
// if self.show_instruction {
// row.col(|ui| {
// ui.label(
// egui::RichText::new(format!(
// "{:?}",
// Instruction(value)
// ))
// .font(egui::FontId::monospace(12.0))
// .color(egui::Color32::from_rgb(255, 100, 255)),
// );
// });
// }
// })
// }
// });
// });
// }
// }
@@ -1,18 +0,0 @@
pub mod controller;
pub mod display;
pub mod editor;
pub mod framebuffer;
pub mod memory;
pub mod registers;
pub mod serial;
pub mod disassembler;
pub trait Component {
/// The tab label shown in the tile header.
fn title(&self) -> &str;
fn visible(&mut self) -> &mut bool;
/// Draw the component's UI inside the provided `Ui`.
fn ui(&mut self, ui: &mut egui::Ui);
}
@@ -1,5 +1,3 @@
use crate::ui::ui_helpers::StatusIndicator;
use eframe::emath::Vec2;
use eframe::epaint::Color32; use eframe::epaint::Color32;
use common::prelude::Register; use common::prelude::Register;
use emulator_core::EmulatorHandle; use emulator_core::EmulatorHandle;
@@ -8,14 +6,12 @@ use crate::ui::ui_helpers::PaddedWidget;
pub struct Controller { pub struct Controller {
emulator: EmulatorHandle, emulator: EmulatorHandle,
visible: bool,
} }
impl Controller { impl Controller {
pub fn new(state: EmulatorHandle) -> Self { pub fn new(state: EmulatorHandle) -> Self {
Self { Self {
emulator: state, emulator: state,
visible: false,
} }
} }
@@ -92,8 +88,5 @@ impl Controller {
PaddedWidget::label(format!("Freq: {}MHz", mips)).ui(ui); PaddedWidget::label(format!("Freq: {}MHz", mips)).ui(ui);
}); });
}); });
} }
} }
+23 -111
View File
@@ -1,78 +1,34 @@
use crate::state::{Action, DataSource};
use crate::ui::controller::Controller; use crate::ui::controller::Controller;
use crate::ui::dialog::error_dialog::ErrorDialog; use crate::ui::dialogs::error_dialog::ErrorDialog;
use crate::ui::panels::about::AboutScreen; use crate::ui::tiles::about::AboutScreen;
use crate::ui::panels::disassembler::BinaryViewer; use crate::ui::tiles::disassembler::BinaryViewer;
use crate::ui::panels::display::Display; use crate::ui::tiles::memory::MemoryInspector;
use crate::ui::panels::framebuffer::FrameBuffer; use crate::ui::tiles::registers::Registers;
use crate::ui::panels::memory::MemoryInspector; use crate::ui::tiles::{CentralTiles, TilePane};
use crate::ui::panels::registers::Registers; use dialogs::confirm_dialog::ConfirmDialog;
use crate::ui::panels::serial::Serial; use dialogs::file_dialog::DsaFileDialog;
use crate::ui::panels::{CentralTiles, TilePane}; use dsa_formats::binary_dse::DseExecutable;
use common::formats::binary_dse::DseExecutable;
use common::process::TraceableProcess;
use dialog::confirm_dialog::ConfirmDialog;
use dialog::file_dialog::DsaFileDialog;
use eframe::NativeOptions;
use egui::{CentralPanel, Panel, Ui}; use egui::{CentralPanel, Panel, Ui};
use egui_tiles::TileId; use egui_tiles::TileId;
use emulator_core::EmulatorController; use emulator_core::EmulatorController;
use panels::PanelFactory; use emulator_core::devices::display::Display;
use panels::documentation::Documentation; use emulator_core::devices::serial::Serial;
use panels::editor::Editor; use tiles::PanelFactory;
use process_manager::{ProcessManager, ProcessSpawner}; use tiles::documentation::Documentation;
use std::fmt::Debug; use tiles::editor::Editor;
use tiles::io_devices::framebuffer::FrameBufferUi;
use process_manager::ProcessManager;
use std::fs::File; use std::fs::File;
use std::io::BufReader; use std::io::BufReader;
use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
mod dialog; mod dialogs;
mod panels; mod tiles;
mod ui_helpers; mod ui_helpers;
mod controller; mod controller;
pub mod process_manager; pub mod process_manager;
pub mod appearance;
pub fn run_app(mut controller: EmulatorController) -> eframe::Result<()> {
// initialise emulator
controller.run().unwrap();
eframe::run_native(
"Damn Simple Architecture",
NativeOptions::default(),
Box::new(|cc| Ok(Box::new(DamnSimpleUI::new(cc, controller)))),
)
}
trait FileFormat: Debug + Clone {
fn extension(&self) -> &'static str;
}
impl FileFormat for DseExecutable {
fn extension(&self) -> &'static str {
"dse"
}
}
#[derive(Debug, Clone)]
pub enum DataSource {
File(PathBuf),
Data(Vec<u8>),
}
pub enum Action {
None,
RequestCloseTab(TileId),
CloseTab(TileId),
Quit,
OpenFile(PathBuf),
// OpenCachedFile(Box<dyn FileFormat>),
LoadBinary(DataSource),
SaveFile(PathBuf, TileId),
RequestSaveAs(TileId),
ShowError(String),
SpawnProcess(ProcessSpawner)
}
pub struct DamnSimpleUI { pub struct DamnSimpleUI {
emulator: EmulatorController, emulator: EmulatorController,
@@ -91,7 +47,7 @@ pub struct DamnSimpleUI {
impl eframe::App for DamnSimpleUI { impl eframe::App for DamnSimpleUI {
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { fn ui(&mut self, ui: &mut Ui, frame: &mut eframe::Frame) {
ui.request_repaint_after(Duration::from_millis(16)); ui.request_repaint_after(Duration::from_millis(16));
if let Some(action) = self.confirm_dialog.show(ui) { if let Some(action) = self.confirm_dialog.show(ui) {
@@ -152,7 +108,7 @@ impl DamnSimpleUI {
cc: &eframe::CreationContext, cc: &eframe::CreationContext,
mut emulator: EmulatorController, mut emulator: EmulatorController,
) -> Self { ) -> Self {
configure_appearance(&cc.egui_ctx); appearance::configure_appearance(&cc.egui_ctx);
let handle = emulator.get_handle().clone(); let handle = emulator.get_handle().clone();
Self { Self {
central_tiles: CentralTiles::new(), central_tiles: CentralTiles::new(),
@@ -173,7 +129,7 @@ impl DamnSimpleUI {
}), }),
PanelFactory::register("Framebuffer", { PanelFactory::register("Framebuffer", {
let handle = handle.clone(); let handle = handle.clone();
move || FrameBuffer::new(320, 240, 0x30000, handle.clone()) move || FrameBufferUi::new(320, 240, 0x30000, handle.clone())
}), }),
PanelFactory::register("Serial", { PanelFactory::register("Serial", {
let handle = handle.clone(); let handle = handle.clone();
@@ -323,47 +279,3 @@ impl DamnSimpleUI {
} }
} }
pub fn configure_appearance(ctx: &egui::Context) {
// configure appearance of UI elements
let mut visuals = egui::Visuals::dark();
// --- your existing settings ---
visuals.window_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.panel_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.widgets.inactive.fg_stroke =
egui::Stroke::from((1.0, egui::Color32::from_rgb(255, 255, 255)));
visuals.widgets.inactive.bg_stroke =
egui::Stroke::from((1.0, egui::Color32::from_rgb(60, 60, 60)));
visuals.widgets.inactive.corner_radius = egui::CornerRadius::from(4);
visuals.widgets.inactive.bg_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.widgets.inactive.weak_bg_fill = egui::Color32::from_rgb(20, 20, 20);
visuals.widgets.inactive.expansion = 1.0;
// tile resize handle
visuals.window_stroke = egui::Stroke::new(1.0, egui::Color32::from_rgb(45, 45, 45));
visuals.extreme_bg_color = egui::Color32::from_rgb(20, 20, 20);
ctx.set_visuals(visuals);
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert(
"JetBrains Mono Nerd Font".to_string(),
std::sync::Arc::new(egui::FontData::from_static(include_bytes!(
"../../font/JetBrainsMonoNerdFontMono_Regular.ttf",
))),
);
fonts
.families
.entry(egui::FontFamily::Proportional)
.or_default()
.insert(0, "JetBrains Mono Nerd Font".to_string());
fonts
.families
.entry(egui::FontFamily::Monospace)
.or_default()
.insert(0, "JetBrains Mono Nerd Font".to_string());
ctx.set_fonts(fonts);
}
@@ -1,123 +0,0 @@
use std::any::Any;
use egui::{Ui, Vec2, WidgetText};
use egui_tiles::TileId;
use emulator_core::{memory::PhysAddr, EmulatorHandle};
use crate::ui::Action;
use crate::ui::panels::TilePane;
pub struct FrameBuffer {
width: usize,
height: usize,
addr: PhysAddr,
emulator: EmulatorHandle,
pub buffer: Vec<u8>,
texture: Option<egui::TextureHandle>,
}
impl FrameBuffer {
pub fn new(width: u32, height: u32, addr: PhysAddr, emulator: EmulatorHandle) -> Self {
let width = width as usize;
let height = height as usize;
Self {
width,
height,
addr,
emulator,
buffer: vec![0; width * height * 4],
texture: None,
}
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
fn internal_read(&mut self) -> Vec<u8> {
let byte_size = self.width * self.height * 4;
self.emulator.mem_read(self.addr, byte_size as u32)
}
pub fn changed(&mut self) -> bool {
let temp = self.internal_read();
if temp != self.buffer {
self.buffer = temp;
return true;
}
false
}
pub fn read(&self) -> &[u8] {
&self.buffer
}
/// Convert buffer to RGBA bytes for use with egui/wgpu textures
pub fn as_rgba_bytes(&self) -> Vec<u8> {
self.buffer
.iter()
.flat_map(|&pixel| pixel.to_le_bytes())
.collect()
}
}
impl TilePane for FrameBuffer {
fn title(&self) -> WidgetText {
"Framebuffer".into()
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
let changed = self.changed();
// get or create the texture
let texture = self.texture.get_or_insert_with(|| {
ui.load_texture(
"framebuffer",
egui::ColorImage {
size: [self.width, self.height],
source_size: Vec2::from([self.width as f32, self.height as f32]),
pixels: vec![egui::Color32::BLACK; self.width * self.height],
},
egui::TextureOptions::NEAREST,
)
});
if changed {
let pixels: Vec<egui::Color32> = self
.buffer
.as_chunks::<4>().0
.iter()
.map(|&[red, green, blue, alpha]| {
egui::Color32::from_rgba_premultiplied(red, green, blue, alpha)
})
.collect();
texture.set(
egui::ColorImage {
size: [self.width, self.height],
source_size: Vec2::from([self.width as f32, self.height as f32]),
pixels,
},
egui::TextureOptions::NEAREST,
);
}
// scale to fit available space while preserving aspect ratio
let available = ui.available_size();
let aspect = self.width as f32 / self.height as f32;
let size = if available.x / aspect <= available.y {
egui::vec2(available.x, available.x / aspect)
} else {
egui::vec2(available.y * aspect, available.y)
};
ui.image(egui::load::SizedTexture::new(texture.id(), size));
None
}
}
@@ -1,11 +1,10 @@
use crate::ui::Action; use crate::state::Action;
use crate::ui::ui_helpers::text::strip_ansi;
use crate::ui::ui_helpers::painted_tab;
use common::process::{ProcessAgent, TraceableProcess}; use common::process::{ProcessAgent, TraceableProcess};
use egui::Ui;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::sync::mpsc; use std::sync::mpsc;
use std::thread::JoinHandle; use std::thread::JoinHandle;
use crate::ui::ui_helpers::{close_button, painted_tab};
use crate::ui::ui_helpers::text::strip_ansi;
pub type ProcessSpawner = Box<dyn FnOnce(&mut ProcessManager) + Send>; pub type ProcessSpawner = Box<dyn FnOnce(&mut ProcessManager) + Send>;
@@ -1,5 +1,5 @@
use crate::ui::Action; use crate::state::Action;
use crate::ui::panels::TilePane; use crate::ui::tiles::TilePane;
use eframe::epaint::Direction; use eframe::epaint::Direction;
use egui::{Ui, WidgetText}; use egui::{Ui, WidgetText};
use egui_tiles::TileId; use egui_tiles::TileId;
@@ -2,8 +2,8 @@ use std::any::Any;
use std::fs::File; use std::fs::File;
use std::io::BufReader; use std::io::BufReader;
use std::path::Path; use std::path::Path;
use std::sync::mpsc;
use std::sync::Arc; use std::sync::Arc;
use std::sync::mpsc;
use std::thread; use std::thread;
use egui::{ScrollArea, Ui, WidgetText}; use egui::{ScrollArea, Ui, WidgetText};
@@ -11,12 +11,11 @@ use egui_extras::{Size, StripBuilder};
use egui_file::FileDialog; use egui_file::FileDialog;
use egui_tiles::TileId; use egui_tiles::TileId;
use common::formats::binary_dse::DseExecutable; use crate::state::{Action, DataSource};
use disassembler::{disassemble, Disassembly}; use crate::ui::tiles::TilePane;
use crate::ui::tiles::memory::MemoryViewer;
use crate::ui::panels::TilePane; use disassembler::{Disassembly, disassemble};
use crate::ui::{Action, DataSource}; use dsa_formats::binary_dse::DseExecutable;
use crate::ui::panels::memory::MemoryViewer;
#[derive(PartialEq, Clone, Copy)] #[derive(PartialEq, Clone, Copy)]
enum ViewMode { enum ViewMode {
@@ -1,11 +1,11 @@
use std::default::Default; use crate::state::Action;
use std::any::Any; use crate::ui::tiles::TilePane;
use std::path::Path; use egui::{Ui, WidgetText};
use egui::{Ui, Widget, WidgetText};
use egui_commonmark::{CommonMarkCache, CommonMarkViewer}; use egui_commonmark::{CommonMarkCache, CommonMarkViewer};
use egui_tiles::TileId; use egui_tiles::TileId;
use crate::ui::Action; use std::any::Any;
use crate::ui::panels::TilePane; use std::default::Default;
use std::path::Path;
pub struct Documentation { pub struct Documentation {
buffer: Option<String>, buffer: Option<String>,
@@ -1,21 +1,18 @@
use crate::ui::Action; use crate::state::Action;
use crate::ui::panels::TilePane; use crate::state::DataSource::Data;
use common::process::{ProcessAgent, TraceableProcess}; use crate::ui::tiles::TilePane;
use crate::ui::tiles::editor::syntax::SyntaxFrom;
use crate::ui::ui_helpers::PaddedWidget;
use assembler::prelude::Assembler;
use common::process::TraceableProcess;
use dsa_formats::binary_dse::DseExecutable;
use egui::{Color32, Key, Ui, Widget}; use egui::{Color32, Key, Ui, Widget};
use egui_code_editor::{CodeEditor, Completer, Syntax}; use egui_code_editor::{CodeEditor, Completer, Syntax};
use egui_tiles::TileId; use egui_tiles::TileId;
use std::any::Any; use std::any::Any;
use std::fs::File; use std::fs::File;
use std::io::Error;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::thread;
use std::time::Duration;
use assembler::prelude::Assembler;
use common::formats::binary_dse::DseExecutable;
use theme::THEME; use theme::THEME;
use crate::ui::DataSource::Data;
use crate::ui::panels::editor::syntax::SyntaxFrom;
use crate::ui::ui_helpers::PaddedWidget;
mod syntax; mod syntax;
mod theme; mod theme;
@@ -66,7 +63,14 @@ impl Editor {
&mut self.buffer &mut self.buffer
} }
pub fn build_cmd(&self, filename: PathBuf, on_complete: impl FnOnce(DseExecutable) -> Action + Send + 'static) -> Action { pub fn get_build_command(filename: PathBuf) -> impl TraceableProcess {
match filename.extension().unwrap().to_str().unwrap() {
"dsa" => Assembler::new(filename),
_ => todo!(),
}
}
pub fn create_build_action(&self, filename: PathBuf, on_complete: impl FnOnce(DseExecutable) -> Action + Send + 'static) -> Action {
Action::SpawnProcess(Box::new(move |pm| { Action::SpawnProcess(Box::new(move |pm| {
let builder = Assembler::new(filename); let builder = Assembler::new(filename);
pm.spawn( pm.spawn(
@@ -75,7 +79,6 @@ impl Editor {
move |result| match result { move |result| match result {
Ok(bin) => on_complete(bin), Ok(bin) => on_complete(bin),
Err(e) => Action::ShowError(e.to_string()), Err(e) => Action::ShowError(e.to_string()),
} }
); );
})) }))
@@ -101,7 +104,7 @@ impl TilePane for Editor {
return; return;
}; };
action = self.build_cmd( action = self.create_build_action(
filename, |res| { filename, |res| {
Action::LoadBinary(Data(res.to_memory_image())) Action::LoadBinary(Data(res.to_memory_image()))
} }
@@ -114,7 +117,7 @@ impl TilePane for Editor {
return; return;
}; };
action = self.build_cmd( action = self.create_build_action(
filename.clone(), move |res| { filename.clone(), move |res| {
let filename = filename.with_extension("dsb"); let filename = filename.with_extension("dsb");
let mut writer = File::create(&filename).unwrap(); let mut writer = File::create(&filename).unwrap();
@@ -124,8 +127,6 @@ impl TilePane for Editor {
); );
} }
PaddedWidget::label( PaddedWidget::label(
self.filename self.filename
.as_ref() .as_ref()
@@ -1,53 +1,9 @@
use std::any::Any; use crate::state::Action;
use crate::ui::tiles::TilePane;
use egui::{Color32, FontId, Ui, Vec2, WidgetText}; use egui::{Color32, FontId, Ui, Vec2, WidgetText};
use egui_tiles::TileId; use egui_tiles::TileId;
use emulator_core::{memory::ram::{MemoryBank, Page}, memory::PhysAddr, EmulatorHandle}; use emulator_core::devices::display::Display;
use crate::ui::Action; use std::any::Any;
use crate::ui::panels::TilePane;
pub struct Display {
width: u32,
height: u32,
addr: PhysAddr,
emulator: EmulatorHandle,
buffer: Vec<u8>,
}
impl Display {
pub fn new(width: u32, height: u32, addr: PhysAddr, emulator: EmulatorHandle) -> Self {
Self {
width,
height,
addr,
emulator,
buffer: Vec::new(),
}
}
pub fn width(&self) -> usize {
self.width as usize
}
pub fn height(&self) -> usize {
self.height as usize
}
pub fn changed(&mut self) -> bool {
let temp = self.read();
if temp != self.buffer {
self.buffer = temp;
return true;
}
false
}
pub fn read(&mut self) -> Vec<u8> {
self.emulator.mem_read(self.addr, self.width * self.height)
}
}
impl TilePane for Display { impl TilePane for Display {
fn title(&self) -> WidgetText { fn title(&self) -> WidgetText {
@@ -0,0 +1,79 @@
use crate::state::Action;
use crate::ui::tiles::TilePane;
use egui::{Ui, Vec2, WidgetText};
use egui_tiles::TileId;
use emulator_core::EmulatorHandle;
use emulator_core::devices::framebuffer::FrameBuffer;
use emulator_core::memory::PhysAddr;
use std::any::Any;
pub struct FrameBufferUi {
device: FrameBuffer,
texture: Option<egui::TextureHandle>,
}
impl FrameBufferUi {
pub fn new(width: u32, height: u32, addr: PhysAddr, emulator: EmulatorHandle) -> Self {
Self {
device: FrameBuffer::new(width, height, addr, emulator),
texture: None,
}
}
}
impl TilePane for FrameBufferUi {
fn title(&self) -> WidgetText {
"Framebuffer".into()
}
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
let frame = self.device.read();
let width = self.device.width();
let height = self.device.height();
// get or create the texture
let texture = self.texture.get_or_insert_with(|| {
ui.load_texture(
"framebuffer",
egui::ColorImage {
size: [width, height],
source_size: Vec2::from([width as f32, height as f32]),
pixels: vec![egui::Color32::BLACK; width * height],
},
egui::TextureOptions::NEAREST,
)
});
let pixels: Vec<egui::Color32> = frame
.as_chunks::<4>().0
.iter()
.map(|&[red, green, blue, _alpha]| {
egui::Color32::from_rgba_premultiplied(red, green, blue, 255)
})
.collect();
texture.set(
egui::ColorImage {
size: [width, height],
source_size: Vec2::from([width as f32, height as f32]),
pixels,
},
egui::TextureOptions::NEAREST,
);
// scale to fit available space while preserving aspect ratio
let available = ui.available_size();
let aspect = width as f32 / height as f32;
let size = if available.x / aspect <= available.y {
egui::vec2(available.x, available.x / aspect)
} else {
egui::vec2(available.y * aspect, available.y)
};
ui.image(egui::load::SizedTexture::new(texture.id(), size));
None
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
@@ -0,0 +1,3 @@
pub mod display;
pub mod framebuffer;
pub mod serial;
@@ -0,0 +1,37 @@
use crate::state::Action;
use crate::ui::tiles::TilePane;
use egui::{Ui, WidgetText};
use egui_tiles::TileId;
use emulator_core::devices::serial::Serial;
use std::any::Any;
impl TilePane for Serial {
fn title(&self) -> WidgetText {
"Serial".into()
}
fn ui(&mut self, ui: &mut Ui, _tile_id: TileId) -> Option<Action> {
self.update();
// Text field for input and a send button
ui.text_edit_multiline(&mut self.write_buff);
if ui.button("Send").clicked() {
self.write_buff.push(0 as char);
for byte in self.write_buff.bytes() {
let _ = self.write_tx.send(byte);
}
self.write_buff.clear();
}
// Display received data
ui.label(String::from_utf8(self.read_buff.clone()).unwrap());
if ui.button("Clear").clicked() {
self.read_buff.clear();
}
None
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
@@ -1,13 +1,12 @@
use std::any::Any; use crate::state::Action;
use std::num::ParseIntError; use crate::ui::tiles::TilePane;
use common::prelude::Instruction;
use egui::{Ui, WidgetText}; use egui::{Ui, WidgetText};
use egui_extras::{Column, TableBuilder}; use egui_extras::{Column, TableBuilder};
use egui_tiles::{Tile, TileId}; use egui_tiles::TileId;
use common::prelude::Instruction;
use emulator_core::EmulatorHandle; use emulator_core::EmulatorHandle;
use emulator_core::memory::ram::MemoryBank; use std::any::Any;
use crate::ui::Action; use std::num::ParseIntError;
use crate::ui::panels::TilePane;
pub struct MemoryInspector { pub struct MemoryInspector {
emulator: EmulatorHandle, emulator: EmulatorHandle,
@@ -2,19 +2,17 @@ pub mod documentation;
pub mod editor; pub mod editor;
pub mod about; pub mod about;
pub mod disassembler; pub mod disassembler;
pub mod display;
pub mod framebuffer;
pub mod memory; pub mod memory;
pub mod registers; pub mod registers;
pub mod serial; pub mod io_devices;
use std::any::Any; use crate::state::Action;
use crate::state::Action::RequestCloseTab;
use crate::ui::ui_helpers::painted_tab;
use eframe::epaint::{Color32, CornerRadius}; use eframe::epaint::{Color32, CornerRadius};
use egui::{Id, Response, Sense, Stroke, TextStyle, Ui, WidgetInfo, WidgetType}; use egui::{Id, Response, Stroke, Ui};
use egui_tiles::{Behavior, Container, SimplificationOptions, TabState, Tile, TileId, Tiles, Tree, UiResponse}; use egui_tiles::{Behavior, Container, SimplificationOptions, TabState, Tile, TileId, Tiles, Tree, UiResponse};
use crate::ui::Action; use std::any::Any;
use crate::ui::Action::RequestCloseTab;
use crate::ui::ui_helpers::{close_button_at, PaddedWidget, painted_tab};
/// Anything that can live inside a tab in the central tiled area. /// Anything that can live inside a tab in the central tiled area.
/// `as_any_mut` lets callers reach back into a specific pane's concrete /// `as_any_mut` lets callers reach back into a specific pane's concrete
@@ -1,11 +1,11 @@
use std::f32; use crate::state::Action;
use std::any::Any; use crate::ui::tiles::TilePane;
use common::prelude::Register;
use egui::{Ui, WidgetText}; use egui::{Ui, WidgetText};
use egui_tiles::TileId; use egui_tiles::TileId;
use common::prelude::Register;
use emulator_core::EmulatorHandle; use emulator_core::EmulatorHandle;
use crate::ui::Action; use std::any::Any;
use crate::ui::panels::TilePane; use std::f32;
pub struct Registers { pub struct Registers {
state: EmulatorHandle, state: EmulatorHandle,
+11
View File
@@ -1,3 +1,6 @@
include print: "./print.dsa"
dw DISPLAY: 0x30000 dw DISPLAY: 0x30000
dw stack: 0x10000 dw stack: 0x10000
@@ -16,17 +19,25 @@ dw COL_IDX: 0
// rg3 = y direction (1 = down, 0 = up) // rg3 = y direction (1 = down, 0 = up)
// rg5 = current colour // rg5 = current colour
db startup: "Starting animation..."
_init: _init:
ldw stack, bpr ldw stack, bpr
mov bpr, spr mov bpr, spr
start: start:
lwi startup, rg0
push rg0
call print::print
pop zero
lli 0, rg0 lli 0, rg0
lli 0, rg1 lli 0, rg1
lli 1, rg2 lli 1, rg2
lli 1, rg3 lli 1, rg3
lli 0, rg8 lli 0, rg8
stw rg8, COL_IDX stw rg8, COL_IDX
ldw COL0, rg5 ldw COL0, rg5
frame_loop: frame_loop:
Binary file not shown.