quality baseline

This commit is contained in:
bQUARKz 2026-02-18 07:31:29 +00:00
parent e2a970e69c
commit c7786fa8b0
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
90 changed files with 3076 additions and 1334 deletions

29
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: CI
on:
push:
branches: [ main, master ]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- name: Format check
run: cargo fmt -- --check
- name: Clippy
run: cargo clippy --workspace --all-features
- name: Test
run: cargo test --workspace --all-targets --all-features --no-fail-fast

68
Cargo.lock generated
View File

@ -25,7 +25,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"getrandom",
"getrandom 0.3.4",
"once_cell",
"version_check",
"zerocopy",
@ -669,6 +669,17 @@ dependencies = [
"windows-link",
]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "getrandom"
version = "0.3.4"
@ -879,7 +890,7 @@ version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom",
"getrandom 0.3.4",
"libc",
]
@ -1545,6 +1556,15 @@ dependencies = [
"portable-atomic",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "presser"
version = "0.3.1"
@ -1644,12 +1664,20 @@ dependencies = [
"serde_json",
]
[[package]]
name = "prometeu-test-support"
version = "0.1.0"
dependencies = [
"rand",
]
[[package]]
name = "prometeu-vm"
version = "0.1.0"
dependencies = [
"prometeu-bytecode",
"prometeu-hal",
"prometeu-test-support",
]
[[package]]
@ -1676,6 +1704,36 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "range-alloc"
version = "0.1.4"
@ -2162,6 +2220,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"

View File

@ -11,6 +11,7 @@ members = [
"crates/host/prometeu-host-desktop-winit",
"crates/tools/prometeu-cli",
"crates/dev/prometeu-test-support",
]
resolver = "2"

15
Makefile Normal file
View File

@ -0,0 +1,15 @@
.PHONY: fmt fmt-check clippy test ci
fmt:
cargo fmt
fmt-check:
cargo fmt -- --check
clippy:
cargo clippy --workspace --all-features
test:
cargo test --workspace --all-targets --all-features --no-fail-fast
ci: fmt-check clippy test

View File

@ -112,10 +112,5 @@ pub fn decode_next(pc: usize, bytes: &'_ [u8]) -> Result<DecodedInstr<'_>, Decod
});
}
Ok(DecodedInstr {
opcode,
pc,
next_pc: imm_end,
imm: &bytes[imm_start..imm_end],
})
Ok(DecodedInstr { opcode, pc, next_pc: imm_end, imm: &bytes[imm_start..imm_end] })
}

View File

@ -18,7 +18,10 @@ pub struct FunctionLayout {
/// then using the next function's start as the current end; the last
/// function ends at `code_len_total`.
/// - The returned vector is indexed by the original function indices.
pub fn compute_function_layouts(functions: &[FunctionMeta], code_len_total: usize) -> Vec<FunctionLayout> {
pub fn compute_function_layouts(
functions: &[FunctionMeta],
code_len_total: usize,
) -> Vec<FunctionLayout> {
// Build index array and sort by start offset (stable to preserve relative order).
let mut idxs: Vec<usize> = (0..functions.len()).collect();
idxs.sort_by_key(|&i| functions[i].code_offset as usize);
@ -28,7 +31,14 @@ pub fn compute_function_layouts(functions: &[FunctionMeta], code_len_total: usiz
if let [a, b] = *w {
let sa = functions[a].code_offset as usize;
let sb = functions[b].code_offset as usize;
debug_assert!(sa < sb, "Function code_offset must be strictly increasing: {} vs {} (indices {} and {})", sa, sb, a, b);
debug_assert!(
sa < sb,
"Function code_offset must be strictly increasing: {} vs {} (indices {} and {})",
sa,
sb,
a,
b
);
}
}
@ -78,7 +88,11 @@ mod tests {
for i in 0..3 {
let l = &layouts[i];
assert_eq!(l.end - l.start, (funcs.get(i + 1).map(|n| n.code_offset as usize).unwrap_or(40)) - (funcs[i].code_offset as usize));
assert_eq!(
l.end - l.start,
(funcs.get(i + 1).map(|n| n.code_offset as usize).unwrap_or(40))
- (funcs[i].code_offset as usize)
);
}
}
}

View File

@ -1,33 +1,21 @@
mod abi;
mod decoder;
mod layout;
mod model;
mod opcode;
mod opcode_spec;
mod abi;
mod layout;
mod decoder;
mod model;
mod value;
mod program_image;
mod value;
pub use abi::{
TrapInfo,
TRAP_INVALID_LOCAL,
TRAP_OOB,
TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DEAD_GATE, TRAP_DIV_ZERO, TRAP_INVALID_FUNC,
TRAP_INVALID_GATE, TRAP_INVALID_LOCAL, TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_STACK_UNDERFLOW,
TRAP_TYPE,
TRAP_BAD_RET_SLOTS,
TRAP_DEAD_GATE,
TRAP_DIV_ZERO,
TRAP_INVALID_FUNC,
TRAP_INVALID_GATE,
TRAP_STACK_UNDERFLOW,
TRAP_INVALID_SYSCALL,
};
pub use model::{
BytecodeLoader,
FunctionMeta,
LoadError,
};
pub use value::Value;
pub use opcode_spec::OpCodeSpecExt;
pub use opcode::OpCode;
pub use decoder::{decode_next, DecodeError};
pub use layout::{compute_function_layouts, FunctionLayout};
pub use model::{BytecodeLoader, FunctionMeta, LoadError};
pub use opcode::OpCode;
pub use opcode_spec::OpCodeSpecExt;
pub use program_image::ProgramImage;
pub use value::Value;

View File

@ -81,15 +81,26 @@ impl BytecodeModule {
let cp_data = self.serialize_const_pool();
let func_data = self.serialize_functions();
let code_data = self.code.clone();
let debug_data = self.debug_info.as_ref().map(|di| self.serialize_debug(di)).unwrap_or_default();
let debug_data =
self.debug_info.as_ref().map(|di| self.serialize_debug(di)).unwrap_or_default();
let export_data = self.serialize_exports();
let mut final_sections = Vec::new();
if !cp_data.is_empty() { final_sections.push((0, cp_data)); }
if !func_data.is_empty() { final_sections.push((1, func_data)); }
if !code_data.is_empty() { final_sections.push((2, code_data)); }
if !debug_data.is_empty() { final_sections.push((3, debug_data)); }
if !export_data.is_empty() { final_sections.push((4, export_data)); }
if !cp_data.is_empty() {
final_sections.push((0, cp_data));
}
if !func_data.is_empty() {
final_sections.push((1, func_data));
}
if !code_data.is_empty() {
final_sections.push((2, code_data));
}
if !debug_data.is_empty() {
final_sections.push((3, debug_data));
}
if !export_data.is_empty() {
final_sections.push((4, export_data));
}
let mut out = Vec::new();
// Magic "PBS\0"
@ -123,7 +134,9 @@ impl BytecodeModule {
}
fn serialize_const_pool(&self) -> Vec<u8> {
if self.const_pool.is_empty() { return Vec::new(); }
if self.const_pool.is_empty() {
return Vec::new();
}
let mut data = Vec::new();
data.extend_from_slice(&(self.const_pool.len() as u32).to_le_bytes());
for entry in &self.const_pool {
@ -157,7 +170,9 @@ impl BytecodeModule {
}
fn serialize_functions(&self) -> Vec<u8> {
if self.functions.is_empty() { return Vec::new(); }
if self.functions.is_empty() {
return Vec::new();
}
let mut data = Vec::new();
data.extend_from_slice(&(self.functions.len() as u32).to_le_bytes());
for f in &self.functions {
@ -191,7 +206,9 @@ impl BytecodeModule {
}
fn serialize_exports(&self) -> Vec<u8> {
if self.exports.is_empty() { return Vec::new(); }
if self.exports.is_empty() {
return Vec::new();
}
let mut data = Vec::new();
data.extend_from_slice(&(self.exports.len() as u32).to_le_bytes());
for exp in &self.exports {
@ -202,7 +219,6 @@ impl BytecodeModule {
}
data
}
}
pub struct BytecodeLoader;
@ -224,7 +240,8 @@ impl BytecodeLoader {
}
let endianness = bytes[6];
if endianness != 0 { // 0 = Little Endian
if endianness != 0 {
// 0 = Little Endian
return Err(LoadError::InvalidEndianness);
}
@ -236,9 +253,20 @@ impl BytecodeLoader {
if pos + 12 > bytes.len() {
return Err(LoadError::UnexpectedEof);
}
let kind = u32::from_le_bytes([bytes[pos], bytes[pos+1], bytes[pos+2], bytes[pos+3]]);
let offset = u32::from_le_bytes([bytes[pos+4], bytes[pos+5], bytes[pos+6], bytes[pos+7]]);
let length = u32::from_le_bytes([bytes[pos+8], bytes[pos+9], bytes[pos+10], bytes[pos+11]]);
let kind =
u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]]);
let offset = u32::from_le_bytes([
bytes[pos + 4],
bytes[pos + 5],
bytes[pos + 6],
bytes[pos + 7],
]);
let length = u32::from_le_bytes([
bytes[pos + 8],
bytes[pos + 9],
bytes[pos + 10],
bytes[pos + 11],
]);
// Basic bounds check
if (offset as usize) + (length as usize) > bytes.len() {
@ -273,19 +301,24 @@ impl BytecodeLoader {
for (kind, offset, length) in sections {
let section_data = &bytes[offset as usize..(offset + length) as usize];
match kind {
0 => { // Const Pool
0 => {
// Const Pool
module.const_pool = parse_const_pool(section_data)?;
}
1 => { // Functions
1 => {
// Functions
module.functions = parse_functions(section_data)?;
}
2 => { // Code
2 => {
// Code
module.code = section_data.to_vec();
}
3 => { // Debug Info
3 => {
// Debug Info
module.debug_info = Some(parse_debug_section(section_data)?);
}
4 => { // Exports
4 => {
// Exports
module.exports = parse_exports(section_data)?;
}
_ => {} // Skip unknown or optional sections
@ -318,34 +351,51 @@ fn parse_const_pool(data: &[u8]) -> Result<Vec<ConstantPoolEntry>, LoadError> {
pos += 1;
match tag {
0 => cp.push(ConstantPoolEntry::Null),
1 => { // Int64
if pos + 8 > data.len() { return Err(LoadError::UnexpectedEof); }
1 => {
// Int64
if pos + 8 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let val = i64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
cp.push(ConstantPoolEntry::Int64(val));
pos += 8;
}
2 => { // Float64
if pos + 8 > data.len() { return Err(LoadError::UnexpectedEof); }
2 => {
// Float64
if pos + 8 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let val = f64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
cp.push(ConstantPoolEntry::Float64(val));
pos += 8;
}
3 => { // Boolean
if pos >= data.len() { return Err(LoadError::UnexpectedEof); }
3 => {
// Boolean
if pos >= data.len() {
return Err(LoadError::UnexpectedEof);
}
cp.push(ConstantPoolEntry::Boolean(data[pos] != 0));
pos += 1;
}
4 => { // String
if pos + 4 > data.len() { return Err(LoadError::UnexpectedEof); }
4 => {
// String
if pos + 4 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let len = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
pos += 4;
if pos + len > data.len() { return Err(LoadError::UnexpectedEof); }
if pos + len > data.len() {
return Err(LoadError::UnexpectedEof);
}
let s = String::from_utf8_lossy(&data[pos..pos + len]).into_owned();
cp.push(ConstantPoolEntry::String(s));
pos += len;
}
5 => { // Int32
if pos + 4 > data.len() { return Err(LoadError::UnexpectedEof); }
5 => {
// Int32
if pos + 4 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let val = i32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
cp.push(ConstantPoolEntry::Int32(val));
pos += 4;
@ -470,7 +520,6 @@ fn parse_exports(data: &[u8]) -> Result<Vec<Export>, LoadError> {
Ok(exports)
}
fn validate_module(module: &BytecodeModule) -> Result<(), LoadError> {
for func in &module.functions {
// Opcode stream bounds
@ -491,16 +540,29 @@ fn validate_module(module: &BytecodeModule) -> Result<(), LoadError> {
match opcode {
OpCode::PushConst => {
if pos + 4 > module.code.len() { return Err(LoadError::UnexpectedEof); }
let idx = u32::from_le_bytes(module.code[pos..pos+4].try_into().unwrap()) as usize;
if pos + 4 > module.code.len() {
return Err(LoadError::UnexpectedEof);
}
let idx =
u32::from_le_bytes(module.code[pos..pos + 4].try_into().unwrap()) as usize;
if idx >= module.const_pool.len() {
return Err(LoadError::InvalidConstIndex);
}
pos += 4;
}
OpCode::PushI32 | OpCode::PushBounded | OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue
| OpCode::GetGlobal | OpCode::SetGlobal | OpCode::GetLocal | OpCode::SetLocal
| OpCode::PopN | OpCode::Syscall | OpCode::GateLoad | OpCode::GateStore => {
OpCode::PushI32
| OpCode::PushBounded
| OpCode::Jmp
| OpCode::JmpIfFalse
| OpCode::JmpIfTrue
| OpCode::GetGlobal
| OpCode::SetGlobal
| OpCode::GetLocal
| OpCode::SetLocal
| OpCode::PopN
| OpCode::Syscall
| OpCode::GateLoad
| OpCode::GateStore => {
pos += 4;
}
OpCode::PushI64 | OpCode::PushF64 => {

View File

@ -7,7 +7,6 @@
#[repr(u16)]
pub enum OpCode {
// --- 6.1 Execution Control ---
/// No operation. Does nothing for 1 cycle.
Nop = 0x00,
/// Stops the Virtual Machine execution immediately.
@ -27,7 +26,6 @@ pub enum OpCode {
Trap = 0x05,
// --- 6.2 Stack Manipulation ---
/// Loads a constant from the Constant Pool into the stack.
/// Operand: index (u32)
/// Stack: [] -> [value]
@ -62,7 +60,6 @@ pub enum OpCode {
PushBounded = 0x19,
// --- 6.3 Arithmetic ---
/// Adds the two top values (a + b).
/// Stack: [a, b] -> [result]
Add = 0x20,
@ -86,7 +83,6 @@ pub enum OpCode {
IntToBoundChecked = 0x26,
// --- 6.4 Comparison and Logic ---
/// Checks if a equals b.
/// Stack: [a, b] -> [bool]
Eq = 0x30,
@ -134,7 +130,6 @@ pub enum OpCode {
Neg = 0x3E,
// --- 6.5 Variables ---
/// Loads a value from a global variable slot.
/// Operand: slot_index (u32)
/// Stack: [] -> [value]
@ -153,7 +148,6 @@ pub enum OpCode {
SetLocal = 0x43,
// --- 6.6 Functions ---
/// Calls a function by its index in the function table.
/// Operand: func_id (u32)
/// Stack: [arg0, arg1, ...] -> [return_slots...]
@ -167,7 +161,6 @@ pub enum OpCode {
PopScope = 0x53,
// --- 6.7 HIP (Heap Interface Protocol) ---
/// Allocates `slots` slots on the heap with the given `type_id`.
/// Operands: type_id (u32), slots (u32)
/// Stack: [] -> [gate]
@ -208,7 +201,6 @@ pub enum OpCode {
GateRelease = 0x6A,
// --- 6.8 Peripherals and System ---
/// Invokes a system function (Firmware/OS).
/// Operand: syscall_id (u32)
/// Stack: [args...] -> [results...] (depends on syscall)

View File

@ -20,65 +20,537 @@ pub trait OpCodeSpecExt {
impl OpCodeSpecExt for OpCode {
fn spec(&self) -> OpcodeSpec {
match self {
OpCode::Nop => OpcodeSpec { name: "NOP", imm_bytes: 0, pops: 0, pushes: 0, is_branch: false, is_terminator: false, may_trap: false },
OpCode::Halt => OpcodeSpec { name: "HALT", imm_bytes: 0, pops: 0, pushes: 0, is_branch: false, is_terminator: true, may_trap: false },
OpCode::Jmp => OpcodeSpec { name: "JMP", imm_bytes: 4, pops: 0, pushes: 0, is_branch: true, is_terminator: true, may_trap: false },
OpCode::JmpIfFalse => OpcodeSpec { name: "JMP_IF_FALSE", imm_bytes: 4, pops: 1, pushes: 0, is_branch: true, is_terminator: false, may_trap: true },
OpCode::JmpIfTrue => OpcodeSpec { name: "JMP_IF_TRUE", imm_bytes: 4, pops: 1, pushes: 0, is_branch: true, is_terminator: false, may_trap: true },
OpCode::Trap => OpcodeSpec { name: "TRAP", imm_bytes: 0, pops: 0, pushes: 0, is_branch: false, is_terminator: true, may_trap: true },
OpCode::PushConst => OpcodeSpec { name: "PUSH_CONST", imm_bytes: 4, pops: 0, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::Pop => OpcodeSpec { name: "POP", imm_bytes: 0, pops: 1, pushes: 0, is_branch: false, is_terminator: false, may_trap: false },
OpCode::PopN => OpcodeSpec { name: "POP_N", imm_bytes: 4, pops: 0, pushes: 0, is_branch: false, is_terminator: false, may_trap: false },
OpCode::Dup => OpcodeSpec { name: "DUP", imm_bytes: 0, pops: 1, pushes: 2, is_branch: false, is_terminator: false, may_trap: false },
OpCode::Swap => OpcodeSpec { name: "SWAP", imm_bytes: 0, pops: 2, pushes: 2, is_branch: false, is_terminator: false, may_trap: false },
OpCode::PushI64 => OpcodeSpec { name: "PUSH_I64", imm_bytes: 8, pops: 0, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::PushF64 => OpcodeSpec { name: "PUSH_F64", imm_bytes: 8, pops: 0, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::PushBool => OpcodeSpec { name: "PUSH_BOOL", imm_bytes: 1, pops: 0, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::PushI32 => OpcodeSpec { name: "PUSH_I32", imm_bytes: 4, pops: 0, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::PushBounded => OpcodeSpec { name: "PUSH_BOUNDED", imm_bytes: 4, pops: 0, pushes: 1, is_branch: false, is_terminator: false, may_trap: true },
OpCode::Add => OpcodeSpec { name: "ADD", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: true },
OpCode::Sub => OpcodeSpec { name: "SUB", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: true },
OpCode::Mul => OpcodeSpec { name: "MUL", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: true },
OpCode::Div => OpcodeSpec { name: "DIV", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: true },
OpCode::Mod => OpcodeSpec { name: "MOD", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: true },
OpCode::BoundToInt => OpcodeSpec { name: "BOUND_TO_INT", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::IntToBoundChecked => OpcodeSpec { name: "INT_TO_BOUND_CHECKED", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: true },
OpCode::Eq => OpcodeSpec { name: "EQ", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::Neq => OpcodeSpec { name: "NEQ", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::Lt => OpcodeSpec { name: "LT", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::Gt => OpcodeSpec { name: "GT", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::And => OpcodeSpec { name: "AND", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::Or => OpcodeSpec { name: "OR", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::Not => OpcodeSpec { name: "NOT", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::BitAnd => OpcodeSpec { name: "BIT_AND", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::BitOr => OpcodeSpec { name: "BIT_OR", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::BitXor => OpcodeSpec { name: "BIT_XOR", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::Shl => OpcodeSpec { name: "SHL", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::Shr => OpcodeSpec { name: "SHR", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::Lte => OpcodeSpec { name: "LTE", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::Gte => OpcodeSpec { name: "GTE", imm_bytes: 0, pops: 2, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::Neg => OpcodeSpec { name: "NEG", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::GetGlobal => OpcodeSpec { name: "GET_GLOBAL", imm_bytes: 4, pops: 0, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::SetGlobal => OpcodeSpec { name: "SET_GLOBAL", imm_bytes: 4, pops: 1, pushes: 0, is_branch: false, is_terminator: false, may_trap: false },
OpCode::GetLocal => OpcodeSpec { name: "GET_LOCAL", imm_bytes: 4, pops: 0, pushes: 1, is_branch: false, is_terminator: false, may_trap: false },
OpCode::SetLocal => OpcodeSpec { name: "SET_LOCAL", imm_bytes: 4, pops: 1, pushes: 0, is_branch: false, is_terminator: false, may_trap: false },
OpCode::Call => OpcodeSpec { name: "CALL", imm_bytes: 4, pops: 0, pushes: 0, is_branch: false, is_terminator: false, may_trap: true },
OpCode::Ret => OpcodeSpec { name: "RET", imm_bytes: 0, pops: 0, pushes: 0, is_branch: false, is_terminator: true, may_trap: false },
OpCode::PushScope => OpcodeSpec { name: "PUSH_SCOPE", imm_bytes: 0, pops: 0, pushes: 0, is_branch: false, is_terminator: false, may_trap: false },
OpCode::PopScope => OpcodeSpec { name: "POP_SCOPE", imm_bytes: 0, pops: 0, pushes: 0, is_branch: false, is_terminator: false, may_trap: false },
OpCode::Alloc => OpcodeSpec { name: "ALLOC", imm_bytes: 8, pops: 0, pushes: 1, is_branch: false, is_terminator: false, may_trap: true },
OpCode::GateLoad => OpcodeSpec { name: "GATE_LOAD", imm_bytes: 4, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: true },
OpCode::GateStore => OpcodeSpec { name: "GATE_STORE", imm_bytes: 4, pops: 2, pushes: 0, is_branch: false, is_terminator: false, may_trap: true },
OpCode::GateBeginPeek => OpcodeSpec { name: "GATE_BEGIN_PEEK", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: true },
OpCode::GateEndPeek => OpcodeSpec { name: "GATE_END_PEEK", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: true },
OpCode::GateBeginBorrow => OpcodeSpec { name: "GATE_BEGIN_BORROW", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: true },
OpCode::GateEndBorrow => OpcodeSpec { name: "GATE_END_BORROW", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: true },
OpCode::GateBeginMutate => OpcodeSpec { name: "GATE_BEGIN_MUTATE", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: true },
OpCode::GateEndMutate => OpcodeSpec { name: "GATE_END_MUTATE", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: true },
OpCode::GateRetain => OpcodeSpec { name: "GATE_RETAIN", imm_bytes: 0, pops: 1, pushes: 1, is_branch: false, is_terminator: false, may_trap: true },
OpCode::GateRelease => OpcodeSpec { name: "GATE_RELEASE", imm_bytes: 0, pops: 1, pushes: 0, is_branch: false, is_terminator: false, may_trap: true },
OpCode::Syscall => OpcodeSpec { name: "SYSCALL", imm_bytes: 4, pops: 0, pushes: 0, is_branch: false, is_terminator: false, may_trap: true },
OpCode::FrameSync => OpcodeSpec { name: "FRAME_SYNC", imm_bytes: 0, pops: 0, pushes: 0, is_branch: false, is_terminator: false, may_trap: false },
OpCode::Nop => OpcodeSpec {
name: "NOP",
imm_bytes: 0,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Halt => OpcodeSpec {
name: "HALT",
imm_bytes: 0,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: true,
may_trap: false,
},
OpCode::Jmp => OpcodeSpec {
name: "JMP",
imm_bytes: 4,
pops: 0,
pushes: 0,
is_branch: true,
is_terminator: true,
may_trap: false,
},
OpCode::JmpIfFalse => OpcodeSpec {
name: "JMP_IF_FALSE",
imm_bytes: 4,
pops: 1,
pushes: 0,
is_branch: true,
is_terminator: false,
may_trap: true,
},
OpCode::JmpIfTrue => OpcodeSpec {
name: "JMP_IF_TRUE",
imm_bytes: 4,
pops: 1,
pushes: 0,
is_branch: true,
is_terminator: false,
may_trap: true,
},
OpCode::Trap => OpcodeSpec {
name: "TRAP",
imm_bytes: 0,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: true,
may_trap: true,
},
OpCode::PushConst => OpcodeSpec {
name: "PUSH_CONST",
imm_bytes: 4,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Pop => OpcodeSpec {
name: "POP",
imm_bytes: 0,
pops: 1,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::PopN => OpcodeSpec {
name: "POP_N",
imm_bytes: 4,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Dup => OpcodeSpec {
name: "DUP",
imm_bytes: 0,
pops: 1,
pushes: 2,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Swap => OpcodeSpec {
name: "SWAP",
imm_bytes: 0,
pops: 2,
pushes: 2,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::PushI64 => OpcodeSpec {
name: "PUSH_I64",
imm_bytes: 8,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::PushF64 => OpcodeSpec {
name: "PUSH_F64",
imm_bytes: 8,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::PushBool => OpcodeSpec {
name: "PUSH_BOOL",
imm_bytes: 1,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::PushI32 => OpcodeSpec {
name: "PUSH_I32",
imm_bytes: 4,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::PushBounded => OpcodeSpec {
name: "PUSH_BOUNDED",
imm_bytes: 4,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::Add => OpcodeSpec {
name: "ADD",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::Sub => OpcodeSpec {
name: "SUB",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::Mul => OpcodeSpec {
name: "MUL",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::Div => OpcodeSpec {
name: "DIV",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::Mod => OpcodeSpec {
name: "MOD",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::BoundToInt => OpcodeSpec {
name: "BOUND_TO_INT",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::IntToBoundChecked => OpcodeSpec {
name: "INT_TO_BOUND_CHECKED",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::Eq => OpcodeSpec {
name: "EQ",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Neq => OpcodeSpec {
name: "NEQ",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Lt => OpcodeSpec {
name: "LT",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Gt => OpcodeSpec {
name: "GT",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::And => OpcodeSpec {
name: "AND",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Or => OpcodeSpec {
name: "OR",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Not => OpcodeSpec {
name: "NOT",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::BitAnd => OpcodeSpec {
name: "BIT_AND",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::BitOr => OpcodeSpec {
name: "BIT_OR",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::BitXor => OpcodeSpec {
name: "BIT_XOR",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Shl => OpcodeSpec {
name: "SHL",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Shr => OpcodeSpec {
name: "SHR",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Lte => OpcodeSpec {
name: "LTE",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Gte => OpcodeSpec {
name: "GTE",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Neg => OpcodeSpec {
name: "NEG",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::GetGlobal => OpcodeSpec {
name: "GET_GLOBAL",
imm_bytes: 4,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::SetGlobal => OpcodeSpec {
name: "SET_GLOBAL",
imm_bytes: 4,
pops: 1,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::GetLocal => OpcodeSpec {
name: "GET_LOCAL",
imm_bytes: 4,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::SetLocal => OpcodeSpec {
name: "SET_LOCAL",
imm_bytes: 4,
pops: 1,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Call => OpcodeSpec {
name: "CALL",
imm_bytes: 4,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::Ret => OpcodeSpec {
name: "RET",
imm_bytes: 0,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: true,
may_trap: false,
},
OpCode::PushScope => OpcodeSpec {
name: "PUSH_SCOPE",
imm_bytes: 0,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::PopScope => OpcodeSpec {
name: "POP_SCOPE",
imm_bytes: 0,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
},
OpCode::Alloc => OpcodeSpec {
name: "ALLOC",
imm_bytes: 8,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::GateLoad => OpcodeSpec {
name: "GATE_LOAD",
imm_bytes: 4,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::GateStore => OpcodeSpec {
name: "GATE_STORE",
imm_bytes: 4,
pops: 2,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::GateBeginPeek => OpcodeSpec {
name: "GATE_BEGIN_PEEK",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::GateEndPeek => OpcodeSpec {
name: "GATE_END_PEEK",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::GateBeginBorrow => OpcodeSpec {
name: "GATE_BEGIN_BORROW",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::GateEndBorrow => OpcodeSpec {
name: "GATE_END_BORROW",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::GateBeginMutate => OpcodeSpec {
name: "GATE_BEGIN_MUTATE",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::GateEndMutate => OpcodeSpec {
name: "GATE_END_MUTATE",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::GateRetain => OpcodeSpec {
name: "GATE_RETAIN",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::GateRelease => OpcodeSpec {
name: "GATE_RELEASE",
imm_bytes: 0,
pops: 1,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::Syscall => OpcodeSpec {
name: "SYSCALL",
imm_bytes: 4,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: true,
},
OpCode::FrameSync => OpcodeSpec {
name: "FRAME_SYNC",
imm_bytes: 0,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
},
}
}
}

View File

@ -1,8 +1,8 @@
use crate::abi::TrapInfo;
use std::collections::HashMap;
use std::sync::Arc;
use crate::model::{BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta};
use crate::value::Value;
use std::collections::HashMap;
use std::sync::Arc;
/// Represents a fully linked, executable PBS program image.
///
@ -22,7 +22,13 @@ pub struct ProgramImage {
}
impl ProgramImage {
pub fn new(rom: Vec<u8>, constant_pool: Vec<Value>, functions: Vec<FunctionMeta>, debug_info: Option<DebugInfo>, exports: HashMap<String, u32>) -> Self {
pub fn new(
rom: Vec<u8>,
constant_pool: Vec<Value>,
functions: Vec<FunctionMeta>,
debug_info: Option<DebugInfo>,
exports: HashMap<String, u32>,
) -> Self {
Self {
rom: Arc::from(rom),
constant_pool: Arc::from(constant_pool),
@ -33,9 +39,10 @@ impl ProgramImage {
}
pub fn create_trap(&self, code: u32, opcode: u16, mut message: String, pc: u32) -> TrapInfo {
let span = self.debug_info.as_ref().and_then(|di| {
di.pc_to_span.iter().find(|(p, _)| *p == pc).map(|(_, s)| s.clone())
});
let span = self
.debug_info
.as_ref()
.and_then(|di| di.pc_to_span.iter().find(|(p, _)| *p == pc).map(|(_, s)| s.clone()));
if let Some(func_idx) = self.find_function_index(pc) {
if let Some(func_name) = self.get_function_name(func_idx) {
@ -43,23 +50,16 @@ impl ProgramImage {
}
}
TrapInfo {
code,
opcode,
message,
pc,
span,
}
TrapInfo { code, opcode, message, pc, span }
}
pub fn find_function_index(&self, pc: u32) -> Option<usize> {
self.functions.iter().position(|f| {
pc >= f.code_offset && pc < (f.code_offset + f.code_len)
})
self.functions.iter().position(|f| pc >= f.code_offset && pc < (f.code_offset + f.code_len))
}
pub fn get_function_name(&self, func_idx: usize) -> Option<&str> {
self.debug_info.as_ref()
self.debug_info
.as_ref()
.and_then(|di| di.function_names.iter().find(|(idx, _)| *idx as usize == func_idx))
.map(|(_, name)| name.as_str())
}
@ -67,35 +67,34 @@ impl ProgramImage {
impl From<BytecodeModule> for ProgramImage {
fn from(module: BytecodeModule) -> Self {
let constant_pool: Vec<Value> = module.const_pool.iter().map(|entry| {
match entry {
let constant_pool: Vec<Value> = module
.const_pool
.iter()
.map(|entry| match entry {
ConstantPoolEntry::Null => Value::Null,
ConstantPoolEntry::Int64(v) => Value::Int64(*v),
ConstantPoolEntry::Float64(v) => Value::Float(*v),
ConstantPoolEntry::Boolean(v) => Value::Boolean(*v),
ConstantPoolEntry::String(v) => Value::String(v.clone()),
ConstantPoolEntry::Int32(v) => Value::Int32(*v),
}
}).collect();
})
.collect();
let mut exports = HashMap::new();
for export in module.exports {
exports.insert(export.symbol, export.func_idx);
}
ProgramImage::new(
module.code,
constant_pool,
module.functions,
module.debug_info,
exports,
)
ProgramImage::new(module.code, constant_pool, module.functions, module.debug_info, exports)
}
}
impl From<ProgramImage> for BytecodeModule {
fn from(program: ProgramImage) -> Self {
let const_pool = program.constant_pool.iter().map(|v| match v {
let const_pool = program
.constant_pool
.iter()
.map(|v| match v {
Value::Null => ConstantPoolEntry::Null,
Value::Int64(v) => ConstantPoolEntry::Int64(*v),
Value::Float(v) => ConstantPoolEntry::Float64(*v),
@ -104,12 +103,14 @@ impl From<ProgramImage> for BytecodeModule {
Value::Int32(v) => ConstantPoolEntry::Int32(*v),
Value::Bounded(v) => ConstantPoolEntry::Int32(*v as i32),
Value::Gate(_) => ConstantPoolEntry::Null,
}).collect();
})
.collect();
let exports = program.exports.iter().map(|(symbol, &func_idx)| Export {
symbol: symbol.clone(),
func_idx,
}).collect();
let exports = program
.exports
.iter()
.map(|(symbol, &func_idx)| Export { symbol: symbol.clone(), func_idx })
.collect();
BytecodeModule {
version: 0,

View File

@ -1,14 +1,16 @@
use std::collections::HashMap;
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::time::Instant;
use prometeu_hal::asset::{AssetEntry, BankStats, BankType, HandleId, LoadStatus, PreloadEntry, SlotRef, SlotStats};
use crate::memory_banks::{SoundBankPoolInstaller, TileBankPoolInstaller};
use prometeu_hal::AssetBridge;
use prometeu_hal::asset::{
AssetEntry, BankStats, BankType, HandleId, LoadStatus, PreloadEntry, SlotRef, SlotStats,
};
use prometeu_hal::color::Color;
use prometeu_hal::sample::Sample;
use prometeu_hal::sound_bank::SoundBank;
use prometeu_hal::tile_bank::{TileBank, TileSize};
use crate::memory_banks::{SoundBankPoolInstaller, TileBankPoolInstaller};
use std::collections::HashMap;
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::time::Instant;
/// Resident metadata for a decoded/materialized asset inside a BankPolicy.
#[derive(Debug)]
@ -21,7 +23,6 @@ pub struct ResidentEntry<T> {
// /// Pin count (optional): if > 0, entry should not be evicted by policy.
// pub pins: u32,
/// Telemetry / profiling fields (optional but useful).
pub loads: u64,
pub last_used: Instant,
@ -129,16 +130,41 @@ struct LoadHandleInfo {
}
impl AssetBridge for AssetManager {
fn initialize_for_cartridge(&self, assets: Vec<AssetEntry>, preload: Vec<PreloadEntry>, assets_data: Vec<u8>) { self.initialize_for_cartridge(assets, preload, assets_data) }
fn load(&self, asset_name: &str, slot: SlotRef) -> Result<HandleId, String> { self.load(asset_name, slot) }
fn status(&self, handle: HandleId) -> LoadStatus { self.status(handle) }
fn commit(&self, handle: HandleId) { self.commit(handle) }
fn cancel(&self, handle: HandleId) { self.cancel(handle) }
fn apply_commits(&self) { self.apply_commits() }
fn bank_info(&self, kind: BankType) -> BankStats { self.bank_info(kind) }
fn slot_info(&self, slot: SlotRef) -> SlotStats { self.slot_info(slot) }
fn find_slot_by_name(&self, asset_name: &str, kind: BankType) -> Option<u8> { self.find_slot_by_name(asset_name, kind) }
fn shutdown(&self) { self.shutdown() }
fn initialize_for_cartridge(
&self,
assets: Vec<AssetEntry>,
preload: Vec<PreloadEntry>,
assets_data: Vec<u8>,
) {
self.initialize_for_cartridge(assets, preload, assets_data)
}
fn load(&self, asset_name: &str, slot: SlotRef) -> Result<HandleId, String> {
self.load(asset_name, slot)
}
fn status(&self, handle: HandleId) -> LoadStatus {
self.status(handle)
}
fn commit(&self, handle: HandleId) {
self.commit(handle)
}
fn cancel(&self, handle: HandleId) {
self.cancel(handle)
}
fn apply_commits(&self) {
self.apply_commits()
}
fn bank_info(&self, kind: BankType) -> BankStats {
self.bank_info(kind)
}
fn slot_info(&self, slot: SlotRef) -> SlotStats {
self.slot_info(slot)
}
fn find_slot_by_name(&self, asset_name: &str, kind: BankType) -> Option<u8> {
self.find_slot_by_name(asset_name, kind)
}
fn shutdown(&self) {
self.shutdown()
}
}
impl AssetManager {
@ -171,7 +197,12 @@ impl AssetManager {
}
}
pub fn initialize_for_cartridge(&self, assets: Vec<AssetEntry>, preload: Vec<PreloadEntry>, assets_data: Vec<u8>) {
pub fn initialize_for_cartridge(
&self,
assets: Vec<AssetEntry>,
preload: Vec<PreloadEntry>,
assets_data: Vec<u8>,
) {
self.shutdown();
{
let mut asset_map = self.assets.write().unwrap();
@ -190,45 +221,70 @@ impl AssetManager {
let entry_opt = {
let assets = self.assets.read().unwrap();
let name_to_id = self.name_to_id.read().unwrap();
name_to_id.get(&item.asset_name)
.and_then(|id| assets.get(id))
.cloned()
name_to_id.get(&item.asset_name).and_then(|id| assets.get(id)).cloned()
};
if let Some(entry) = entry_opt {
let slot_index = item.slot;
match entry.bank_type {
BankType::TILES => {
if let Ok(bank) = Self::perform_load_tile_bank(&entry, self.assets_data.clone()) {
if let Ok(bank) =
Self::perform_load_tile_bank(&entry, self.assets_data.clone())
{
let bank_arc = Arc::new(bank);
self.gfx_policy.put_resident(entry.asset_id, Arc::clone(&bank_arc), entry.decoded_size as usize);
self.gfx_policy.put_resident(
entry.asset_id,
Arc::clone(&bank_arc),
entry.decoded_size as usize,
);
self.gfx_installer.install_tile_bank(slot_index, bank_arc);
let mut slots = self.gfx_slots.write().unwrap();
if slot_index < slots.len() {
slots[slot_index] = Some(entry.asset_id);
}
println!("[AssetManager] Preloaded tile asset '{}' (id: {}) into slot {}", entry.asset_name, entry.asset_id, slot_index);
println!(
"[AssetManager] Preloaded tile asset '{}' (id: {}) into slot {}",
entry.asset_name, entry.asset_id, slot_index
);
} else {
eprintln!("[AssetManager] Failed to preload tile asset '{}'", entry.asset_name);
eprintln!(
"[AssetManager] Failed to preload tile asset '{}'",
entry.asset_name
);
}
}
BankType::SOUNDS => {
if let Ok(bank) = Self::perform_load_sound_bank(&entry, self.assets_data.clone()) {
if let Ok(bank) =
Self::perform_load_sound_bank(&entry, self.assets_data.clone())
{
let bank_arc = Arc::new(bank);
self.sound_policy.put_resident(entry.asset_id, Arc::clone(&bank_arc), entry.decoded_size as usize);
self.sound_policy.put_resident(
entry.asset_id,
Arc::clone(&bank_arc),
entry.decoded_size as usize,
);
self.sound_installer.install_sound_bank(slot_index, bank_arc);
let mut slots = self.sound_slots.write().unwrap();
if slot_index < slots.len() {
slots[slot_index] = Some(entry.asset_id);
}
println!("[AssetManager] Preloaded sound asset '{}' (id: {}) into slot {}", entry.asset_name, entry.asset_id, slot_index);
println!(
"[AssetManager] Preloaded sound asset '{}' (id: {}) into slot {}",
entry.asset_name, entry.asset_id, slot_index
);
} else {
eprintln!("[AssetManager] Failed to preload sound asset '{}'", entry.asset_name);
eprintln!(
"[AssetManager] Failed to preload sound asset '{}'",
entry.asset_name
);
}
}
}
} else {
eprintln!("[AssetManager] Preload failed: asset '{}' not found in table", item.asset_name);
eprintln!(
"[AssetManager] Preload failed: asset '{}' not found in table",
item.asset_name
);
}
}
}
@ -237,7 +293,9 @@ impl AssetManager {
let entry = {
let assets = self.assets.read().unwrap();
let name_to_id = self.name_to_id.read().unwrap();
let id = name_to_id.get(asset_name).ok_or_else(|| format!("Asset not found: {}", asset_name))?;
let id = name_to_id
.get(asset_name)
.ok_or_else(|| format!("Asset not found: {}", asset_name))?;
assets.get(id).ok_or_else(|| format!("Asset ID {} not found in table", id))?.clone()
};
let asset_id = entry.asset_id;
@ -256,31 +314,33 @@ impl AssetManager {
if let Some(bank) = self.gfx_policy.get_resident(asset_id) {
self.gfx_policy.stage(handle_id, bank);
true
} else { false }
} else {
false
}
}
BankType::SOUNDS => {
if let Some(bank) = self.sound_policy.get_resident(asset_id) {
self.sound_policy.stage(handle_id, bank);
true
} else { false }
} else {
false
}
}
};
if already_resident {
self.handles.write().unwrap().insert(handle_id, LoadHandleInfo {
_asset_id: asset_id,
slot,
status: LoadStatus::READY,
});
self.handles.write().unwrap().insert(
handle_id,
LoadHandleInfo { _asset_id: asset_id, slot, status: LoadStatus::READY },
);
return Ok(handle_id);
}
// Not resident, start loading
self.handles.write().unwrap().insert(handle_id, LoadHandleInfo {
_asset_id: asset_id,
slot,
status: LoadStatus::PENDING,
});
self.handles.write().unwrap().insert(
handle_id,
LoadHandleInfo { _asset_id: asset_id, slot, status: LoadStatus::PENDING },
);
let handles = self.handles.clone();
let assets_data = self.assets_data.clone();
@ -319,7 +379,10 @@ impl AssetManager {
existing.loads += 1;
Arc::clone(&existing.value)
} else {
let entry = ResidentEntry::new(Arc::clone(&bank_arc), entry_clone.decoded_size as usize);
let entry = ResidentEntry::new(
Arc::clone(&bank_arc),
entry_clone.decoded_size as usize,
);
map.insert(asset_id, entry);
bank_arc
}
@ -327,11 +390,15 @@ impl AssetManager {
gfx_policy_staging.write().unwrap().insert(handle_id, resident_arc);
let mut handles_map = handles.write().unwrap();
if let Some(h) = handles_map.get_mut(&handle_id) {
if h.status == LoadStatus::LOADING { h.status = LoadStatus::READY; }
if h.status == LoadStatus::LOADING {
h.status = LoadStatus::READY;
}
}
} else {
let mut handles_map = handles.write().unwrap();
if let Some(h) = handles_map.get_mut(&handle_id) { h.status = LoadStatus::ERROR; }
if let Some(h) = handles_map.get_mut(&handle_id) {
h.status = LoadStatus::ERROR;
}
}
}
BankType::SOUNDS => {
@ -345,7 +412,10 @@ impl AssetManager {
existing.loads += 1;
Arc::clone(&existing.value)
} else {
let entry = ResidentEntry::new(Arc::clone(&bank_arc), entry_clone.decoded_size as usize);
let entry = ResidentEntry::new(
Arc::clone(&bank_arc),
entry_clone.decoded_size as usize,
);
map.insert(asset_id, entry);
bank_arc
}
@ -353,11 +423,15 @@ impl AssetManager {
sound_policy_staging.write().unwrap().insert(handle_id, resident_arc);
let mut handles_map = handles.write().unwrap();
if let Some(h) = handles_map.get_mut(&handle_id) {
if h.status == LoadStatus::LOADING { h.status = LoadStatus::READY; }
if h.status == LoadStatus::LOADING {
h.status = LoadStatus::READY;
}
}
} else {
let mut handles_map = handles.write().unwrap();
if let Some(h) = handles_map.get_mut(&handle_id) { h.status = LoadStatus::ERROR; }
if let Some(h) = handles_map.get_mut(&handle_id) {
h.status = LoadStatus::ERROR;
}
}
}
}
@ -366,7 +440,10 @@ impl AssetManager {
Ok(handle_id)
}
fn perform_load_tile_bank(entry: &AssetEntry, assets_data: Arc<RwLock<Vec<u8>>>) -> Result<TileBank, String> {
fn perform_load_tile_bank(
entry: &AssetEntry,
assets_data: Arc<RwLock<Vec<u8>>>,
) -> Result<TileBank, String> {
if entry.codec != "RAW" {
return Err(format!("Unsupported codec: {}", entry.codec));
}
@ -383,9 +460,12 @@ impl AssetManager {
let buffer = &assets_data[start..end];
// Decode TILEBANK metadata
let tile_size_val = entry.metadata.get("tile_size").and_then(|v| v.as_u64()).ok_or("Missing tile_size")?;
let width = entry.metadata.get("width").and_then(|v| v.as_u64()).ok_or("Missing width")? as usize;
let height = entry.metadata.get("height").and_then(|v| v.as_u64()).ok_or("Missing height")? as usize;
let tile_size_val =
entry.metadata.get("tile_size").and_then(|v| v.as_u64()).ok_or("Missing tile_size")?;
let width =
entry.metadata.get("width").and_then(|v| v.as_u64()).ok_or("Missing width")? as usize;
let height =
entry.metadata.get("height").and_then(|v| v.as_u64()).ok_or("Missing height")? as usize;
let tile_size = match tile_size_val {
8 => TileSize::Size8,
@ -406,21 +486,19 @@ impl AssetManager {
for p in 0..64 {
for c in 0..16 {
let offset = (p * 16 + c) * 2;
let color_raw = u16::from_le_bytes([palette_data[offset], palette_data[offset + 1]]);
let color_raw =
u16::from_le_bytes([palette_data[offset], palette_data[offset + 1]]);
palettes[p][c] = Color(color_raw);
}
}
Ok(TileBank {
tile_size,
width,
height,
pixel_indices,
palettes,
})
Ok(TileBank { tile_size, width, height, pixel_indices, palettes })
}
fn perform_load_sound_bank(entry: &AssetEntry, assets_data: Arc<RwLock<Vec<u8>>>) -> Result<SoundBank, String> {
fn perform_load_sound_bank(
entry: &AssetEntry,
assets_data: Arc<RwLock<Vec<u8>>>,
) -> Result<SoundBank, String> {
if entry.codec != "RAW" {
return Err(format!("Unsupported codec: {}", entry.codec));
}
@ -436,7 +514,8 @@ impl AssetManager {
let buffer = &assets_data[start..end];
let sample_rate = entry.metadata.get("sample_rate").and_then(|v| v.as_u64()).unwrap_or(44100) as u32;
let sample_rate =
entry.metadata.get("sample_rate").and_then(|v| v.as_u64()).unwrap_or(44100) as u32;
let mut data = Vec::with_capacity(buffer.len() / 2);
for i in (0..buffer.len()).step_by(2) {
@ -540,7 +619,9 @@ impl AssetManager {
{
let slots = self.gfx_slots.read().unwrap();
for s in slots.iter() {
if s.is_some() { slots_occupied += 1; }
if s.is_some() {
slots_occupied += 1;
}
}
}
@ -581,7 +662,9 @@ impl AssetManager {
{
let slots = self.sound_slots.read().unwrap();
for s in slots.iter() {
if s.is_some() { slots_occupied += 1; }
if s.is_some() {
slots_occupied += 1;
}
}
}
@ -604,7 +687,11 @@ impl AssetManager {
let asset_id = slots.get(slot.index).and_then(|s| s.clone());
let (bytes, asset_name) = if let Some(id) = &asset_id {
let bytes = self.gfx_policy.resident.read().unwrap()
let bytes = self
.gfx_policy
.resident
.read()
.unwrap()
.get(id)
.map(|entry| entry.bytes)
.unwrap_or(0);
@ -614,19 +701,18 @@ impl AssetManager {
(0, None)
};
SlotStats {
asset_id,
asset_name,
generation: 0,
resident_bytes: bytes,
}
SlotStats { asset_id, asset_name, generation: 0, resident_bytes: bytes }
}
BankType::SOUNDS => {
let slots = self.sound_slots.read().unwrap();
let asset_id = slots.get(slot.index).and_then(|s| s.clone());
let (bytes, asset_name) = if let Some(id) = &asset_id {
let bytes = self.sound_policy.resident.read().unwrap()
let bytes = self
.sound_policy
.resident
.read()
.unwrap()
.get(id)
.map(|entry| entry.bytes)
.unwrap_or(0);
@ -636,12 +722,7 @@ impl AssetManager {
(0, None)
};
SlotStats {
asset_id,
asset_name,
generation: 0,
resident_bytes: bytes,
}
SlotStats { asset_id, asset_name, generation: 0, resident_bytes: bytes }
}
}
}
@ -832,9 +913,7 @@ mod tests {
}),
};
let preload = vec![
PreloadEntry { asset_name: "preload_sound".to_string(), slot: 5 }
];
let preload = vec![PreloadEntry { asset_name: "preload_sound".to_string(), slot: 5 }];
let am = AssetManager::new(vec![], vec![], gfx_installer, sound_installer);
@ -868,9 +947,7 @@ mod tests {
metadata: serde_json::json!({ "tile_size": 16, "width": 16, "height": 16 }),
};
let preload = vec![
PreloadEntry { asset_name: "my_tiles".to_string(), slot: 3 }
];
let preload = vec![PreloadEntry { asset_name: "my_tiles".to_string(), slot: 3 }];
let am = AssetManager::new(vec![], vec![], gfx_installer, sound_installer);
am.initialize_for_cartridge(vec![asset_entry], preload, data);

View File

@ -1,15 +1,15 @@
use std::sync::Arc;
use prometeu_hal::AudioBridge;
use std::sync::Arc;
/// Maximum number of simultaneous audio voices supported by the hardware.
pub const MAX_CHANNELS: usize = 16;
/// Standard sample rate for the final audio output.
pub const OUTPUT_SAMPLE_RATE: u32 = 48000;
use crate::memory_banks::SoundBankPoolAccess;
/// Looping mode for samples (re-exported from the hardware contract).
pub use prometeu_hal::LoopMode;
use prometeu_hal::sample::Sample;
use crate::memory_banks::SoundBankPoolAccess;
/// State of a single playback voice (channel).
///
@ -65,24 +65,13 @@ pub enum AudioCommand {
loop_mode: LoopMode,
},
/// Immediately stop playback on a voice.
Stop {
voice_id: usize,
},
Stop { voice_id: usize },
/// Update volume of an ongoing playback.
SetVolume {
voice_id: usize,
volume: u8,
},
SetVolume { voice_id: usize, volume: u8 },
/// Update panning of an ongoing playback.
SetPan {
voice_id: usize,
pan: u8,
},
SetPan { voice_id: usize, pan: u8 },
/// Update pitch of an ongoing playback.
SetPitch {
voice_id: usize,
pitch: f64,
},
SetPitch { voice_id: usize, pitch: f64 },
/// Pause all audio processing.
MasterPause,
/// Resume audio processing.
@ -114,20 +103,57 @@ pub struct Audio {
}
impl AudioBridge for Audio {
fn play(&mut self, bank_id: u8, sample_id: u16, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: prometeu_hal::LoopMode) {
let lm = match loop_mode { prometeu_hal::LoopMode::Off => LoopMode::Off, prometeu_hal::LoopMode::On => LoopMode::On };
fn play(
&mut self,
bank_id: u8,
sample_id: u16,
voice_id: usize,
volume: u8,
pan: u8,
pitch: f64,
priority: u8,
loop_mode: prometeu_hal::LoopMode,
) {
let lm = match loop_mode {
prometeu_hal::LoopMode::Off => LoopMode::Off,
prometeu_hal::LoopMode::On => LoopMode::On,
};
self.play(bank_id, sample_id, voice_id, volume, pan, pitch, priority, lm)
}
fn play_sample(&mut self, sample: Arc<Sample>, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: prometeu_hal::LoopMode) {
let lm = match loop_mode { prometeu_hal::LoopMode::Off => LoopMode::Off, prometeu_hal::LoopMode::On => LoopMode::On };
fn play_sample(
&mut self,
sample: Arc<Sample>,
voice_id: usize,
volume: u8,
pan: u8,
pitch: f64,
priority: u8,
loop_mode: prometeu_hal::LoopMode,
) {
let lm = match loop_mode {
prometeu_hal::LoopMode::Off => LoopMode::Off,
prometeu_hal::LoopMode::On => LoopMode::On,
};
self.play_sample(sample, voice_id, volume, pan, pitch, priority, lm)
}
fn stop(&mut self, voice_id: usize) { self.stop(voice_id) }
fn set_volume(&mut self, voice_id: usize, volume: u8) { self.set_volume(voice_id, volume) }
fn set_pan(&mut self, voice_id: usize, pan: u8) { self.set_pan(voice_id, pan) }
fn set_pitch(&mut self, voice_id: usize, pitch: f64) { self.set_pitch(voice_id, pitch) }
fn is_playing(&self, voice_id: usize) -> bool { self.is_playing(voice_id) }
fn clear_commands(&mut self) { self.clear_commands() }
fn stop(&mut self, voice_id: usize) {
self.stop(voice_id)
}
fn set_volume(&mut self, voice_id: usize, volume: u8) {
self.set_volume(voice_id, volume)
}
fn set_pan(&mut self, voice_id: usize, pan: u8) {
self.set_pan(voice_id, pan)
}
fn set_pitch(&mut self, voice_id: usize, pitch: f64) {
self.set_pitch(voice_id, pitch)
}
fn is_playing(&self, voice_id: usize) -> bool {
self.is_playing(voice_id)
}
fn clear_commands(&mut self) {
self.clear_commands()
}
}
impl Audio {
@ -140,25 +166,51 @@ impl Audio {
}
}
pub fn play(&mut self, bank_id: u8, sample_id: u16, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: LoopMode) {
pub fn play(
&mut self,
bank_id: u8,
sample_id: u16,
voice_id: usize,
volume: u8,
pan: u8,
pitch: f64,
priority: u8,
loop_mode: LoopMode,
) {
if voice_id >= MAX_CHANNELS {
return;
}
// Resolve the sample from the hardware pools
let sample = self.sound_banks.sound_bank_slot(bank_id as usize)
let sample = self
.sound_banks
.sound_bank_slot(bank_id as usize)
.and_then(|bank| bank.samples.get(sample_id as usize).map(Arc::clone));
if let Some(s) = sample {
println!("[Audio] Resolved sample from bank {} sample {}. Playing on voice {}.", bank_id, sample_id, voice_id);
println!(
"[Audio] Resolved sample from bank {} sample {}. Playing on voice {}.",
bank_id, sample_id, voice_id
);
self.play_sample(s, voice_id, volume, pan, pitch, priority, loop_mode);
} else {
eprintln!("[Audio] Failed to resolve sample from bank {} sample {}.", bank_id, sample_id);
eprintln!(
"[Audio] Failed to resolve sample from bank {} sample {}.",
bank_id, sample_id
);
}
}
pub fn play_sample(&mut self, sample: Arc<Sample>, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: LoopMode) {
pub fn play_sample(
&mut self,
sample: Arc<Sample>,
voice_id: usize,
volume: u8,
pan: u8,
pitch: f64,
priority: u8,
loop_mode: LoopMode,
) {
if voice_id >= MAX_CHANNELS {
return;
}
@ -217,11 +269,7 @@ impl Audio {
}
pub fn is_playing(&self, voice_id: usize) -> bool {
if voice_id < MAX_CHANNELS {
self.voices[voice_id].active
} else {
false
}
if voice_id < MAX_CHANNELS { self.voices[voice_id].active } else { false }
}
/// Clears the command queue. The Host should consume this every frame.

View File

@ -1,11 +1,11 @@
use crate::memory_banks::TileBankPoolAccess;
use std::sync::Arc;
use prometeu_hal::color::Color;
use prometeu_hal::GfxBridge;
use prometeu_hal::color::Color;
use prometeu_hal::sprite::Sprite;
use prometeu_hal::tile::Tile;
use prometeu_hal::tile_bank::{TileBank, TileSize};
use prometeu_hal::tile_layer::{HudTileLayer, ScrollableTileLayer, TileMap};
use std::sync::Arc;
/// Blending modes inspired by classic 16-bit hardware.
/// Defines how source pixels are combined with existing pixels in the framebuffer.
@ -79,44 +79,139 @@ pub struct Gfx {
}
impl GfxBridge for Gfx {
fn size(&self) -> (usize, usize) { self.size() }
fn front_buffer(&self) -> &[u16] { self.front_buffer() }
fn clear(&mut self, color: Color) { self.clear(color) }
fn fill_rect_blend(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color, mode: prometeu_hal::BlendMode) { let m = match mode { prometeu_hal::BlendMode::None => BlendMode::None, prometeu_hal::BlendMode::Half => BlendMode::Half, prometeu_hal::BlendMode::HalfPlus => BlendMode::HalfPlus, prometeu_hal::BlendMode::HalfMinus => BlendMode::HalfMinus, prometeu_hal::BlendMode::Full => BlendMode::Full }; self.fill_rect_blend(x, y, w, h, color, m) }
fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) { self.fill_rect(x, y, w, h, color) }
fn draw_pixel(&mut self, x: i32, y: i32, color: Color) { self.draw_pixel(x, y, color) }
fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) { self.draw_line(x0, y0, x1, y1, color) }
fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) { self.draw_circle(xc, yc, r, color) }
fn draw_circle_points(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color) { self.draw_circle_points(xc, yc, x, y, color) }
fn fill_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) { self.fill_circle(xc, yc, r, color) }
fn draw_circle_lines(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color) { self.draw_circle_lines(xc, yc, x, y, color) }
fn draw_disc(&mut self, x: i32, y: i32, r: i32, border_color: Color, fill_color: Color) { self.draw_disc(x, y, r, border_color, fill_color) }
fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) { self.draw_rect(x, y, w, h, color) }
fn draw_square(&mut self, x: i32, y: i32, w: i32, h: i32, border_color: Color, fill_color: Color) { self.draw_square(x, y, w, h, border_color, fill_color) }
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color) { self.draw_horizontal_line(x0, x1, y, color) }
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color) { self.draw_vertical_line(x, y0, y1, color) }
fn present(&mut self) { self.present() }
fn render_all(&mut self) { self.render_all() }
fn render_layer(&mut self, layer_idx: usize) { self.render_layer(layer_idx) }
fn render_hud(&mut self) { self.render_hud() }
fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) { self.draw_text(x, y, text, color) }
fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color) { self.draw_char(x, y, c, color) }
fn size(&self) -> (usize, usize) {
self.size()
}
fn front_buffer(&self) -> &[u16] {
self.front_buffer()
}
fn clear(&mut self, color: Color) {
self.clear(color)
}
fn fill_rect_blend(
&mut self,
x: i32,
y: i32,
w: i32,
h: i32,
color: Color,
mode: prometeu_hal::BlendMode,
) {
let m = match mode {
prometeu_hal::BlendMode::None => BlendMode::None,
prometeu_hal::BlendMode::Half => BlendMode::Half,
prometeu_hal::BlendMode::HalfPlus => BlendMode::HalfPlus,
prometeu_hal::BlendMode::HalfMinus => BlendMode::HalfMinus,
prometeu_hal::BlendMode::Full => BlendMode::Full,
};
self.fill_rect_blend(x, y, w, h, color, m)
}
fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) {
self.fill_rect(x, y, w, h, color)
}
fn draw_pixel(&mut self, x: i32, y: i32, color: Color) {
self.draw_pixel(x, y, color)
}
fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) {
self.draw_line(x0, y0, x1, y1, color)
}
fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
self.draw_circle(xc, yc, r, color)
}
fn draw_circle_points(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color) {
self.draw_circle_points(xc, yc, x, y, color)
}
fn fill_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
self.fill_circle(xc, yc, r, color)
}
fn draw_circle_lines(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color) {
self.draw_circle_lines(xc, yc, x, y, color)
}
fn draw_disc(&mut self, x: i32, y: i32, r: i32, border_color: Color, fill_color: Color) {
self.draw_disc(x, y, r, border_color, fill_color)
}
fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) {
self.draw_rect(x, y, w, h, color)
}
fn draw_square(
&mut self,
x: i32,
y: i32,
w: i32,
h: i32,
border_color: Color,
fill_color: Color,
) {
self.draw_square(x, y, w, h, border_color, fill_color)
}
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color) {
self.draw_horizontal_line(x0, x1, y, color)
}
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color) {
self.draw_vertical_line(x, y0, y1, color)
}
fn present(&mut self) {
self.present()
}
fn render_all(&mut self) {
self.render_all()
}
fn render_layer(&mut self, layer_idx: usize) {
self.render_layer(layer_idx)
}
fn render_hud(&mut self) {
self.render_hud()
}
fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) {
self.draw_text(x, y, text, color)
}
fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color) {
self.draw_char(x, y, c, color)
}
fn layer(&self, index: usize) -> &ScrollableTileLayer { &self.layers[index] }
fn layer_mut(&mut self, index: usize) -> &mut ScrollableTileLayer { &mut self.layers[index] }
fn hud(&self) -> &HudTileLayer { &self.hud }
fn hud_mut(&mut self) -> &mut HudTileLayer { &mut self.hud }
fn sprite(&self, index: usize) -> &Sprite { &self.sprites[index] }
fn sprite_mut(&mut self, index: usize) -> &mut Sprite { &mut self.sprites[index] }
fn layer(&self, index: usize) -> &ScrollableTileLayer {
&self.layers[index]
}
fn layer_mut(&mut self, index: usize) -> &mut ScrollableTileLayer {
&mut self.layers[index]
}
fn hud(&self) -> &HudTileLayer {
&self.hud
}
fn hud_mut(&mut self) -> &mut HudTileLayer {
&mut self.hud
}
fn sprite(&self, index: usize) -> &Sprite {
&self.sprites[index]
}
fn sprite_mut(&mut self, index: usize) -> &mut Sprite {
&mut self.sprites[index]
}
fn scene_fade_level(&self) -> u8 { self.scene_fade_level }
fn set_scene_fade_level(&mut self, level: u8) { self.scene_fade_level = level; }
fn scene_fade_color(&self) -> Color { self.scene_fade_color }
fn set_scene_fade_color(&mut self, color: Color) { self.scene_fade_color = color; }
fn hud_fade_level(&self) -> u8 { self.hud_fade_level }
fn set_hud_fade_level(&mut self, level: u8) { self.hud_fade_level = level; }
fn hud_fade_color(&self) -> Color { self.hud_fade_color }
fn set_hud_fade_color(&mut self, color: Color) { self.hud_fade_color = color; }
fn scene_fade_level(&self) -> u8 {
self.scene_fade_level
}
fn set_scene_fade_level(&mut self, level: u8) {
self.scene_fade_level = level;
}
fn scene_fade_color(&self) -> Color {
self.scene_fade_color
}
fn set_scene_fade_color(&mut self, color: Color) {
self.scene_fade_color = color;
}
fn hud_fade_level(&self) -> u8 {
self.hud_fade_level
}
fn set_hud_fade_level(&mut self, level: u8) {
self.hud_fade_level = level;
}
fn hud_fade_color(&self) -> Color {
self.hud_fade_color
}
fn set_hud_fade_color(&mut self, color: Color) {
self.hud_fade_color = color;
}
}
impl Gfx {
@ -187,7 +282,9 @@ impl Gfx {
color: Color,
mode: BlendMode,
) {
if color == Color::COLOR_KEY { return; }
if color == Color::COLOR_KEY {
return;
}
let fw = self.w as i32;
let fh = self.h as i32;
@ -216,7 +313,9 @@ impl Gfx {
/// Draws a single pixel.
pub fn draw_pixel(&mut self, x: i32, y: i32, color: Color) {
if color == Color::COLOR_KEY { return; }
if color == Color::COLOR_KEY {
return;
}
if x >= 0 && x < self.w as i32 && y >= 0 && y < self.h as i32 {
self.back[y as usize * self.w + x as usize] = color.0;
}
@ -224,7 +323,9 @@ impl Gfx {
/// Draws a line between two points using Bresenham's algorithm.
pub fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) {
if color == Color::COLOR_KEY { return; }
if color == Color::COLOR_KEY {
return;
}
let dx = (x1 - x0).abs();
let sx = if x0 < x1 { 1 } else { -1 };
@ -237,7 +338,9 @@ impl Gfx {
loop {
self.draw_pixel(x, y, color);
if x == x1 && y == y1 { break; }
if x == x1 && y == y1 {
break;
}
let e2 = 2 * err;
if e2 >= dy {
err += dy;
@ -252,9 +355,13 @@ impl Gfx {
/// Draws a circle outline using Midpoint Circle Algorithm.
pub fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
if color == Color::COLOR_KEY { return; }
if color == Color::COLOR_KEY {
return;
}
if r < 0 { return; }
if r < 0 {
return;
}
let mut x = 0;
let mut y = r;
let mut d = 3 - 2 * r;
@ -284,9 +391,13 @@ impl Gfx {
/// Draws a filled circle.
pub fn fill_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
if color == Color::COLOR_KEY { return; }
if color == Color::COLOR_KEY {
return;
}
if r < 0 { return; }
if r < 0 {
return;
}
let mut x = 0;
let mut y = r;
let mut d = 3 - 2 * r;
@ -318,9 +429,13 @@ impl Gfx {
/// Draws a rectangle outline.
pub fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) {
if color == Color::COLOR_KEY { return; }
if color == Color::COLOR_KEY {
return;
}
if w <= 0 || h <= 0 { return; }
if w <= 0 || h <= 0 {
return;
}
self.draw_horizontal_line(x, x + w - 1, y, color);
self.draw_horizontal_line(x, x + w - 1, y + h - 1, color);
self.draw_vertical_line(x, y, y + h - 1, color);
@ -328,30 +443,50 @@ impl Gfx {
}
/// Draws a square (filled rectangle with border).
pub fn draw_square(&mut self, x: i32, y: i32, w: i32, h: i32, border_color: Color, fill_color: Color) {
pub fn draw_square(
&mut self,
x: i32,
y: i32,
w: i32,
h: i32,
border_color: Color,
fill_color: Color,
) {
self.fill_rect(x, y, w, h, fill_color);
self.draw_rect(x, y, w, h, border_color);
}
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color) {
if color == Color::COLOR_KEY { return; }
if color == Color::COLOR_KEY {
return;
}
if y < 0 || y >= self.h as i32 { return; }
if y < 0 || y >= self.h as i32 {
return;
}
let start = x0.max(0);
let end = x1.min(self.w as i32 - 1);
if start > end { return; }
if start > end {
return;
}
for x in start..=end {
self.back[y as usize * self.w + x as usize] = color.0;
}
}
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color) {
if color == Color::COLOR_KEY { return; }
if color == Color::COLOR_KEY {
return;
}
if x < 0 || x >= self.w as i32 { return; }
if x < 0 || x >= self.w as i32 {
return;
}
let start = y0.max(0);
let end = y1.min(self.h as i32 - 1);
if start > end { return; }
if start > end {
return;
}
for y in start..=end {
self.back[y as usize * self.w + x as usize] = color.0;
}
@ -382,18 +517,40 @@ impl Gfx {
}
// 1. Priority 0 sprites: drawn at the very back, behind everything else.
Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[0], &self.sprites, &*self.tile_banks);
Self::draw_bucket_on_buffer(
&mut self.back,
self.w,
self.h,
&self.priority_buckets[0],
&self.sprites,
&*self.tile_banks,
);
// 2. Main layers and prioritized sprites.
// Order: Layer 0 -> Sprites 1 -> Layer 1 -> Sprites 2 ...
for i in 0..self.layers.len() {
let bank_id = self.layers[i].bank_id as usize;
if let Some(bank) = self.tile_banks.tile_bank_slot(bank_id) {
Self::draw_tile_map(&mut self.back, self.w, self.h, &self.layers[i].map, &bank, self.layers[i].scroll_x, self.layers[i].scroll_y);
Self::draw_tile_map(
&mut self.back,
self.w,
self.h,
&self.layers[i].map,
&bank,
self.layers[i].scroll_x,
self.layers[i].scroll_y,
);
}
// Draw sprites that belong to this depth level
Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[i + 1], &self.sprites, &*self.tile_banks);
Self::draw_bucket_on_buffer(
&mut self.back,
self.w,
self.h,
&self.priority_buckets[i + 1],
&self.sprites,
&*self.tile_banks,
);
}
// 4. Scene Fade: Applies a color blend to the entire world (excluding HUD).
@ -408,7 +565,9 @@ impl Gfx {
/// Renders a specific game layer.
pub fn render_layer(&mut self, layer_idx: usize) {
if layer_idx >= self.layers.len() { return; }
if layer_idx >= self.layers.len() {
return;
}
let bank_id = self.layers[layer_idx].bank_id as usize;
let scroll_x = self.layers[layer_idx].scroll_x;
@ -419,7 +578,15 @@ impl Gfx {
_ => return,
};
Self::draw_tile_map(&mut self.back, self.w, self.h, &self.layers[layer_idx].map, &bank, scroll_x, scroll_y);
Self::draw_tile_map(
&mut self.back,
self.w,
self.h,
&self.layers[layer_idx].map,
&bank,
scroll_x,
scroll_y,
);
}
/// Renders the HUD (fixed position, no scroll).
@ -427,7 +594,13 @@ impl Gfx {
Self::render_hud_with_pool(&mut self.back, self.w, self.h, &self.hud, &*self.tile_banks);
}
fn render_hud_with_pool(back: &mut [u16], w: usize, h: usize, hud: &HudTileLayer, tile_banks: &dyn TileBankPoolAccess) {
fn render_hud_with_pool(
back: &mut [u16],
w: usize,
h: usize,
hud: &HudTileLayer,
tile_banks: &dyn TileBankPoolAccess,
) {
let bank_id = hud.bank_id as usize;
let bank = match tile_banks.tile_bank_slot(bank_id) {
Some(b) => b,
@ -445,7 +618,7 @@ impl Gfx {
map: &TileMap,
bank: &TileBank,
scroll_x: i32,
scroll_y: i32
scroll_y: i32,
) {
let tile_size = bank.tile_size as usize;
@ -469,34 +642,58 @@ impl Gfx {
let map_y = (start_tile_y + ty as i32) as usize;
// Bounds check: don't draw if the camera is outside the map.
if map_x >= map.width || map_y >= map.height { continue; }
if map_x >= map.width || map_y >= map.height {
continue;
}
let tile = map.tiles[map_y * map.width + map_x];
// Optimized skip for empty (ID 0) tiles.
if tile.id == 0 { continue; }
if tile.id == 0 {
continue;
}
// 5. Project the tile pixels to screen space.
let screen_tile_x = (tx as i32 * tile_size as i32) - fine_scroll_x;
let screen_tile_y = (ty as i32 * tile_size as i32) - fine_scroll_y;
Self::draw_tile_pixels(back, screen_w, screen_h, screen_tile_x, screen_tile_y, tile, bank);
Self::draw_tile_pixels(
back,
screen_w,
screen_h,
screen_tile_x,
screen_tile_y,
tile,
bank,
);
}
}
}
/// Internal helper to copy a single tile's pixels to the framebuffer.
/// Handles flipping and palette resolution.
fn draw_tile_pixels(back: &mut [u16], screen_w: usize, screen_h: usize, x: i32, y: i32, tile: Tile, bank: &TileBank) {
fn draw_tile_pixels(
back: &mut [u16],
screen_w: usize,
screen_h: usize,
x: i32,
y: i32,
tile: Tile,
bank: &TileBank,
) {
let size = bank.tile_size as usize;
for local_y in 0..size {
let world_y = y + local_y as i32;
if world_y < 0 || world_y >= screen_h as i32 { continue; }
if world_y < 0 || world_y >= screen_h as i32 {
continue;
}
for local_x in 0..size {
let world_x = x + local_x as i32;
if world_x < 0 || world_x >= screen_w as i32 { continue; }
if world_x < 0 || world_x >= screen_w as i32 {
continue;
}
// Handle flip flags by reversing the fetch coordinates.
let fetch_x = if tile.flip_x { size - 1 - local_x } else { local_x };
@ -506,7 +703,9 @@ impl Gfx {
let px_index = bank.get_pixel_index(tile.id, fetch_x, fetch_y);
// 2. Hardware rule: Color index 0 is always fully transparent.
if px_index == 0 { continue; }
if px_index == 0 {
continue;
}
// 3. Resolve the virtual index to a real RGB565 color using the tile's assigned palette.
let color = bank.resolve_color(tile.palette_id, px_index);
@ -538,7 +737,7 @@ impl Gfx {
screen_w: usize,
screen_h: usize,
sprite: &Sprite,
bank: &TileBank
bank: &TileBank,
) {
// ... (same bounds/clipping calculation we already had) ...
let size = bank.tile_size as usize;
@ -559,7 +758,9 @@ impl Gfx {
let px_index = bank.get_pixel_index(sprite.tile.id, fetch_x, fetch_y);
// 2. Transparency
if px_index == 0 { continue; }
if px_index == 0 {
continue;
}
// 3. Resolve color via palette (from the tile inside the sprite)
let color = bank.resolve_color(sprite.tile.palette_id, px_index);
@ -572,7 +773,9 @@ impl Gfx {
/// Applies the fade effect to the entire back buffer.
/// level: 0 (full color) to 31 (visible)
fn apply_fade_to_buffer(back: &mut [u16], level: u8, fade_color: Color) {
if level >= 31 { return; } // Fully visible, skip processing
if level >= 31 {
return;
} // Fully visible, skip processing
let weight = level as u16;
let inv_weight = 31 - weight;

View File

@ -1,11 +1,14 @@
use std::sync::Arc;
use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge};
use crate::asset::AssetManager;
use crate::audio::Audio;
use crate::gfx::Gfx;
use crate::memory_banks::{MemoryBanks, SoundBankPoolAccess, SoundBankPoolInstaller, TileBankPoolAccess, TileBankPoolInstaller};
use crate::memory_banks::{
MemoryBanks, SoundBankPoolAccess, SoundBankPoolInstaller, TileBankPoolAccess,
TileBankPoolInstaller,
};
use crate::pad::Pad;
use crate::touch::Touch;
use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge};
use std::sync::Arc;
/// Aggregate structure for all virtual hardware peripherals.
///
@ -33,20 +36,40 @@ pub struct Hardware {
}
impl HardwareBridge for Hardware {
fn gfx(&self) -> &dyn GfxBridge { &self.gfx }
fn gfx_mut(&mut self) -> &mut dyn GfxBridge { &mut self.gfx }
fn gfx(&self) -> &dyn GfxBridge {
&self.gfx
}
fn gfx_mut(&mut self) -> &mut dyn GfxBridge {
&mut self.gfx
}
fn audio(&self) -> &dyn AudioBridge { &self.audio }
fn audio_mut(&mut self) -> &mut dyn AudioBridge { &mut self.audio }
fn audio(&self) -> &dyn AudioBridge {
&self.audio
}
fn audio_mut(&mut self) -> &mut dyn AudioBridge {
&mut self.audio
}
fn pad(&self) -> &dyn PadBridge { &self.pad }
fn pad_mut(&mut self) -> &mut dyn PadBridge { &mut self.pad }
fn pad(&self) -> &dyn PadBridge {
&self.pad
}
fn pad_mut(&mut self) -> &mut dyn PadBridge {
&mut self.pad
}
fn touch(&self) -> &dyn TouchBridge { &self.touch }
fn touch_mut(&mut self) -> &mut dyn TouchBridge { &mut self.touch }
fn touch(&self) -> &dyn TouchBridge {
&self.touch
}
fn touch_mut(&mut self) -> &mut dyn TouchBridge {
&mut self.touch
}
fn assets(&self) -> &dyn AssetBridge { &self.assets }
fn assets_mut(&mut self) -> &mut dyn AssetBridge { &mut self.assets }
fn assets(&self) -> &dyn AssetBridge {
&self.assets
}
fn assets_mut(&mut self) -> &mut dyn AssetBridge {
&mut self.assets
}
}
impl Hardware {
@ -59,7 +82,11 @@ impl Hardware {
pub fn new() -> Self {
let memory_banks = Arc::new(MemoryBanks::new());
Self {
gfx: Gfx::new(Self::W, Self::H, Arc::clone(&memory_banks) as Arc<dyn TileBankPoolAccess>),
gfx: Gfx::new(
Self::W,
Self::H,
Arc::clone(&memory_banks) as Arc<dyn TileBankPoolAccess>,
),
audio: Audio::new(Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolAccess>),
pad: Pad::default(),
touch: Touch::default(),

View File

@ -1,10 +1,10 @@
mod asset;
mod audio;
mod gfx;
pub mod hardware;
mod memory_banks;
mod pad;
mod touch;
mod audio;
mod memory_banks;
pub mod hardware;
pub use crate::asset::AssetManager;
pub use crate::audio::{Audio, AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};

View File

@ -1,6 +1,6 @@
use std::sync::{Arc, RwLock};
use prometeu_hal::sound_bank::SoundBank;
use prometeu_hal::tile_bank::TileBank;
use std::sync::{Arc, RwLock};
/// Non-generic interface for peripherals to access graphical tile banks.
pub trait TileBankPoolAccess: Send + Sync {

View File

@ -1,5 +1,5 @@
use prometeu_hal::{InputSignals, PadBridge};
use prometeu_hal::button::Button;
use prometeu_hal::{InputSignals, PadBridge};
#[derive(Default, Clone, Copy, Debug)]
pub struct Pad {
@ -20,21 +20,49 @@ pub struct Pad {
}
impl PadBridge for Pad {
fn begin_frame(&mut self, signals: &InputSignals) { self.begin_frame(signals) }
fn any(&self) -> bool { self.any() }
fn begin_frame(&mut self, signals: &InputSignals) {
self.begin_frame(signals)
}
fn any(&self) -> bool {
self.any()
}
fn up(&self) -> &Button { &self.up }
fn down(&self) -> &Button { &self.down }
fn left(&self) -> &Button { &self.left }
fn right(&self) -> &Button { &self.right }
fn a(&self) -> &Button { &self.a }
fn b(&self) -> &Button { &self.b }
fn x(&self) -> &Button { &self.x }
fn y(&self) -> &Button { &self.y }
fn l(&self) -> &Button { &self.l }
fn r(&self) -> &Button { &self.r }
fn start(&self) -> &Button { &self.start }
fn select(&self) -> &Button { &self.select }
fn up(&self) -> &Button {
&self.up
}
fn down(&self) -> &Button {
&self.down
}
fn left(&self) -> &Button {
&self.left
}
fn right(&self) -> &Button {
&self.right
}
fn a(&self) -> &Button {
&self.a
}
fn b(&self) -> &Button {
&self.b
}
fn x(&self) -> &Button {
&self.x
}
fn y(&self) -> &Button {
&self.y
}
fn l(&self) -> &Button {
&self.l
}
fn r(&self) -> &Button {
&self.r
}
fn start(&self) -> &Button {
&self.start
}
fn select(&self) -> &Button {
&self.select
}
}
impl Pad {
@ -66,5 +94,3 @@ impl Pad {
|| self.select.down
}
}

View File

@ -1,5 +1,5 @@
use prometeu_hal::{InputSignals, TouchBridge};
use prometeu_hal::button::Button;
use prometeu_hal::{InputSignals, TouchBridge};
#[derive(Default, Clone, Copy, Debug)]
pub struct Touch {
@ -18,8 +18,16 @@ impl Touch {
}
impl TouchBridge for Touch {
fn begin_frame(&mut self, signals: &InputSignals) { self.begin_frame(signals) }
fn f(&self) -> &Button { &self.f }
fn x(&self) -> i32 { self.x }
fn y(&self) -> i32 { self.y }
fn begin_frame(&mut self, signals: &InputSignals) {
self.begin_frame(signals)
}
fn f(&self) -> &Button {
&self.f
}
fn x(&self) -> i32 {
self.x
}
fn y(&self) -> i32 {
self.y
}
}

View File

@ -1,11 +1,11 @@
use prometeu_hal::telemetry::CertificationConfig;
use prometeu_hal::{HardwareBridge, InputSignals};
use prometeu_hal::cartridge::Cartridge;
use prometeu_system::{PrometeuHub, VirtualMachineRuntime};
use prometeu_vm::VirtualMachine;
use crate::firmware::boot_target::BootTarget;
use crate::firmware::firmware_state::{FirmwareState, LoadCartridgeStep, ResetStep};
use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::cartridge::Cartridge;
use prometeu_hal::telemetry::CertificationConfig;
use prometeu_hal::{HardwareBridge, InputSignals};
use prometeu_system::{PrometeuHub, VirtualMachineRuntime};
use prometeu_vm::VirtualMachine;
/// PROMETEU Firmware.
///
@ -76,7 +76,12 @@ impl Firmware {
}
/// Transitions the system to a new state, handling lifecycle hooks.
pub fn change_state(&mut self, new_state: FirmwareState, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
pub fn change_state(
&mut self,
new_state: FirmwareState,
signals: &InputSignals,
hw: &mut dyn HardwareBridge,
) {
self.on_exit(signals, hw);
self.state = new_state;
self.state_initialized = false;
@ -109,7 +114,11 @@ impl Firmware {
/// Dispatches the `on_update` event to the current state implementation.
/// Returns an optional `FirmwareState` if a transition is requested.
fn on_update(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) -> Option<FirmwareState> {
fn on_update(
&mut self,
signals: &InputSignals,
hw: &mut dyn HardwareBridge,
) -> Option<FirmwareState> {
let mut req = PrometeuContext {
vm: &mut self.vm,
os: &mut self.os,

View File

@ -1,7 +1,7 @@
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::color::Color;
use crate::firmware::firmware_state::{FirmwareState, LaunchHubStep};
use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::color::Color;
use prometeu_hal::log::{LogLevel, LogSource};
#[derive(Debug, Clone)]
pub struct AppCrashesStep {

View File

@ -1,6 +1,6 @@
use prometeu_hal::log::{LogLevel, LogSource};
use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState};
use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::log::{LogLevel, LogSource};
#[derive(Debug, Clone)]
pub struct GameRunningStep;

View File

@ -1,8 +1,8 @@
use crate::firmware::boot_target::BootTarget;
use crate::firmware::firmware_state::{FirmwareState, HubHomeStep, LoadCartridgeStep};
use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::cartridge_loader::CartridgeLoader;
use prometeu_hal::log::{LogLevel, LogSource};
#[derive(Debug, Clone)]
pub struct LaunchHubStep;
@ -22,7 +22,12 @@ impl LaunchHubStep {
return Some(FirmwareState::LoadCartridge(LoadCartridgeStep { cartridge }));
}
Err(e) => {
ctx.os.log(LogLevel::Error, LogSource::Pos, 0, format!("Failed to auto-load cartridge: {:?}", e));
ctx.os.log(
LogLevel::Error,
LogSource::Pos,
0,
format!("Failed to auto-load cartridge: {:?}", e),
);
}
}
}

View File

@ -1,8 +1,8 @@
use crate::firmware::firmware_state::{FirmwareState, GameRunningStep, HubHomeStep};
use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::cartridge::{AppMode, Cartridge};
use prometeu_hal::color::Color;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::window::Rect;
#[derive(Debug, Clone)]
@ -12,13 +12,18 @@ pub struct LoadCartridgeStep {
impl LoadCartridgeStep {
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
ctx.os.log(LogLevel::Info, LogSource::Pos, 0, format!("Loading cartridge: {}", self.cartridge.title));
ctx.os.log(
LogLevel::Info,
LogSource::Pos,
0,
format!("Loading cartridge: {}", self.cartridge.title),
);
// Initialize Asset Manager
ctx.hw.assets_mut().initialize_for_cartridge(
self.cartridge.asset_table.clone(),
self.cartridge.preload.clone(),
self.cartridge.assets.clone()
self.cartridge.assets.clone(),
);
ctx.os.initialize_vm(ctx.vm, &self.cartridge);
@ -29,7 +34,7 @@ impl LoadCartridgeStep {
let id = ctx.hub.window_manager.add_window(
self.cartridge.title.clone(),
Rect { x: 40, y: 20, w: 240, h: 140 },
Color::WHITE
Color::WHITE,
);
ctx.hub.window_manager.set_focus(id);

View File

@ -1,7 +1,7 @@
use prometeu_hal::log::{LogLevel, LogSource};
use crate::firmware::boot_target::BootTarget;
use crate::firmware::firmware_state::{FirmwareState, LaunchHubStep, SplashScreenStep};
use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::log::{LogLevel, LogSource};
#[derive(Debug, Clone)]
pub struct ResetStep;

View File

@ -1,7 +1,7 @@
use crate::firmware::firmware_state::{FirmwareState, LaunchHubStep};
use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::color::Color;
use prometeu_hal::log::{LogLevel, LogSource};
#[derive(Debug, Clone)]
pub struct SplashScreenStep {

View File

@ -1,14 +1,14 @@
mod boot_target;
mod firmware;
pub mod firmware_state;
mod boot_target;
pub(crate) mod firmware_step_crash_screen;
pub(crate) mod firmware_step_game_running;
pub(crate) mod firmware_step_hub_home;
pub(crate) mod firmware_step_launch_hub;
pub(crate) mod firmware_step_load_cartridge;
pub(crate) mod firmware_step_reset;
pub(crate) mod firmware_step_splash_screen;
pub(crate) mod firmware_step_launch_hub;
pub(crate) mod firmware_step_hub_home;
pub(crate) mod firmware_step_load_cartridge;
pub(crate) mod firmware_step_game_running;
pub(crate) mod firmware_step_crash_screen;
mod prometeu_context;
pub use boot_target::BootTarget;

View File

@ -1,7 +1,7 @@
use crate::firmware::boot_target::BootTarget;
use prometeu_hal::{HardwareBridge, InputSignals};
use prometeu_system::{PrometeuHub, VirtualMachineRuntime};
use prometeu_vm::VirtualMachine;
use crate::firmware::boot_target::BootTarget;
pub struct PrometeuContext<'a> {
pub vm: &'a mut VirtualMachine,

View File

@ -65,16 +65,10 @@ pub struct SlotRef {
impl SlotRef {
pub fn gfx(index: usize) -> Self {
Self {
asset_type: BankType::TILES,
index,
}
Self { asset_type: BankType::TILES, index }
}
pub fn audio(index: usize) -> Self {
Self {
asset_type: BankType::SOUNDS,
index,
}
Self { asset_type: BankType::SOUNDS, index }
}
}

View File

@ -1,4 +1,6 @@
use crate::asset::{AssetEntry, BankStats, BankType, HandleId, LoadStatus, PreloadEntry, SlotRef, SlotStats};
use crate::asset::{
AssetEntry, BankStats, BankType, HandleId, LoadStatus, PreloadEntry, SlotRef, SlotStats,
};
pub trait AssetBridge {
fn initialize_for_cartridge(

View File

@ -1,5 +1,5 @@
use std::sync::Arc;
use crate::sample::Sample;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoopMode {

View File

@ -1,6 +1,6 @@
use crate::cartridge::{Cartridge, CartridgeDTO, CartridgeError, CartridgeManifest};
use std::fs;
use std::path::Path;
use crate::cartridge::{Cartridge, CartridgeDTO, CartridgeError, CartridgeManifest};
pub struct CartridgeLoader;
@ -30,8 +30,10 @@ impl DirectoryCartridgeLoader {
return Err(CartridgeError::InvalidManifest);
}
let manifest_content = fs::read_to_string(manifest_path).map_err(|_| CartridgeError::IoError)?;
let manifest: CartridgeManifest = serde_json::from_str(&manifest_content).map_err(|_| CartridgeError::InvalidManifest)?;
let manifest_content =
fs::read_to_string(manifest_path).map_err(|_| CartridgeError::IoError)?;
let manifest: CartridgeManifest =
serde_json::from_str(&manifest_content).map_err(|_| CartridgeError::InvalidManifest)?;
// Additional validation as per requirements
if manifest.magic != "PMTU" {

View File

@ -72,5 +72,4 @@ impl Color {
let hex = r8 << 16 | g8 << 8 | b8;
hex as i32
}
}

View File

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use prometeu_bytecode::Value;
use crate::cartridge::AppMode;
use prometeu_bytecode::Value;
use serde::{Deserialize, Serialize};
pub const DEVTOOLS_PROTOCOL_VERSION: u32 = 1;
@ -33,22 +33,11 @@ pub enum DebugCommand {
#[serde(tag = "type")]
pub enum DebugResponse {
#[serde(rename = "handshake")]
Handshake {
protocol_version: u32,
runtime_version: String,
cartridge: HandshakeCartridge,
},
Handshake { protocol_version: u32, runtime_version: String, cartridge: HandshakeCartridge },
#[serde(rename = "getState")]
GetState {
pc: usize,
stack_top: Vec<Value>,
frame_index: u64,
app_id: u32,
},
GetState { pc: usize, stack_top: Vec<Value>, frame_index: u64, app_id: u32 },
#[serde(rename = "breakpoints")]
Breakpoints {
pcs: Vec<usize>,
},
Breakpoints { pcs: Vec<usize> },
}
#[derive(Debug, Serialize, Deserialize)]
@ -63,16 +52,9 @@ pub struct HandshakeCartridge {
#[serde(tag = "event")]
pub enum DebugEvent {
#[serde(rename = "breakpointHit")]
BreakpointHit {
pc: usize,
frame_index: u64,
},
BreakpointHit { pc: usize, frame_index: u64 },
#[serde(rename = "log")]
Log {
level: String,
source: String,
msg: String,
},
Log { level: String, source: String, msg: String },
#[serde(rename = "telemetry")]
Telemetry {
frame_index: u64,
@ -90,12 +72,7 @@ pub enum DebugEvent {
audio_slots_occupied: u32,
},
#[serde(rename = "cert")]
Cert {
rule: String,
used: u64,
limit: u64,
frame_index: u64,
},
Cert { rule: String, used: u64, limit: u64, frame_index: u64 },
}
#[cfg(test)]

View File

@ -25,7 +25,15 @@ pub trait GfxBridge {
fn draw_circle_lines(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color);
fn draw_disc(&mut self, x: i32, y: i32, r: i32, border_color: Color, fill_color: Color);
fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color);
fn draw_square(&mut self, x: i32, y: i32, w: i32, h: i32, border_color: Color, fill_color: Color);
fn draw_square(
&mut self,
x: i32,
y: i32,
w: i32,
h: i32,
border_color: Color,
fill_color: Color,
);
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color);
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color);
fn present(&mut self);

View File

@ -1,8 +1,8 @@
use prometeu_bytecode::{Value, TRAP_OOB};
use crate::vm_fault::VmFault;
use prometeu_bytecode::{TRAP_OOB, Value};
pub struct HostReturn<'a> {
stack: &'a mut Vec<Value>
stack: &'a mut Vec<Value>,
}
impl<'a> HostReturn<'a> {

View File

@ -1,31 +1,31 @@
pub mod asset;
pub mod asset_bridge;
pub mod audio_bridge;
pub mod button;
pub mod cartridge;
pub mod cartridge_loader;
pub mod color;
pub mod debugger_protocol;
pub mod gfx_bridge;
pub mod hardware_bridge;
pub mod host_context;
pub mod host_return;
pub mod input_signals;
pub mod log;
pub mod native_helpers;
pub mod native_interface;
pub mod pad_bridge;
pub mod touch_bridge;
pub mod native_helpers;
pub mod input_signals;
pub mod cartridge;
pub mod cartridge_loader;
pub mod debugger_protocol;
pub mod asset;
pub mod color;
pub mod button;
pub mod tile;
pub mod tile_layer;
pub mod tile_bank;
pub mod sample;
pub mod sound_bank;
pub mod sprite;
pub mod sample;
pub mod window;
pub mod syscalls;
pub mod telemetry;
pub mod log;
pub mod tile;
pub mod tile_bank;
pub mod tile_layer;
pub mod touch_bridge;
pub mod vm_fault;
pub mod window;
pub use asset_bridge::AssetBridge;
pub use audio_bridge::{AudioBridge, LoopMode};
@ -34,7 +34,7 @@ pub use hardware_bridge::HardwareBridge;
pub use host_context::{HostContext, HostContextProvider};
pub use host_return::HostReturn;
pub use input_signals::InputSignals;
pub use native_helpers::{expect_bool, expect_bounded, expect_int};
pub use native_interface::{NativeInterface, SyscallId};
pub use pad_bridge::PadBridge;
pub use touch_bridge::TouchBridge;
pub use native_helpers::{expect_bool, expect_bounded, expect_int};

View File

@ -1,5 +1,5 @@
use std::collections::VecDeque;
use crate::log::{LogEvent, LogLevel, LogSource};
use std::collections::VecDeque;
pub struct LogService {
events: VecDeque<LogEvent>,
@ -9,14 +9,18 @@ pub struct LogService {
impl LogService {
pub fn new(capacity: usize) -> Self {
Self {
events: VecDeque::with_capacity(capacity),
capacity,
next_seq: 0,
}
Self { events: VecDeque::with_capacity(capacity), capacity, next_seq: 0 }
}
pub fn log(&mut self, ts_ms: u64, frame: u64, level: LogLevel, source: LogSource, tag: u16, msg: String) {
pub fn log(
&mut self,
ts_ms: u64,
frame: u64,
level: LogLevel,
source: LogSource,
tag: u16,
msg: String,
) {
if self.events.len() >= self.capacity {
self.events.pop_front();
}

View File

@ -1,7 +1,7 @@
mod log_level;
mod log_source;
mod log_event;
mod log_level;
mod log_service;
mod log_source;
pub use log_event::LogEvent;
pub use log_level::LogLevel;

View File

@ -1,5 +1,5 @@
use prometeu_bytecode::{Value, TRAP_TYPE};
use crate::vm_fault::VmFault;
use prometeu_bytecode::{TRAP_TYPE, Value};
pub fn expect_bounded(args: &[Value], idx: usize) -> Result<u32, VmFault> {
args.get(idx)

View File

@ -1,7 +1,7 @@
use prometeu_bytecode::Value;
use crate::host_context::HostContext;
use crate::host_return::HostReturn;
use crate::vm_fault::VmFault;
use prometeu_bytecode::Value;
pub type SyscallId = u32;
@ -11,5 +11,11 @@ pub trait NativeInterface {
/// ABI Rule: Arguments for the syscall are passed in `args`.
///
/// Returns are written via `ret`.
fn syscall(&mut self, id: SyscallId, args: &[Value], ret: &mut HostReturn, ctx: &mut HostContext) -> Result<(), VmFault>;
fn syscall(
&mut self,
id: SyscallId,
args: &[Value],
ret: &mut HostReturn,
ctx: &mut HostContext,
) -> Result<(), VmFault>;
}

View File

@ -7,12 +7,7 @@ pub struct Sample {
impl Sample {
pub fn new(sample_rate: u32, data: Vec<i16>) -> Self {
Self {
sample_rate,
data,
loop_start: None,
loop_end: None,
}
Self { sample_rate, data, loop_start: None, loop_end: None }
}
pub fn with_loop(mut self, start: u32, end: u32) -> Self {

View File

@ -1,5 +1,5 @@
use std::sync::Arc;
use crate::sample::Sample;
use std::sync::Arc;
/// A container for audio assets.
///

View File

@ -38,7 +38,12 @@ impl Certifier {
Self { config }
}
pub fn evaluate(&self, telemetry: &TelemetryFrame, log_service: &mut LogService, ts_ms: u64) -> usize {
pub fn evaluate(
&self,
telemetry: &TelemetryFrame,
log_service: &mut LogService,
ts_ms: u64,
) -> usize {
if !self.config.enabled {
return 0;
}
@ -53,7 +58,10 @@ impl Certifier {
LogLevel::Warn,
LogSource::Pos,
0xCA01,
format!("Cert: cycles_used exceeded budget ({} > {})", telemetry.cycles_used, budget),
format!(
"Cert: cycles_used exceeded budget ({} > {})",
telemetry.cycles_used, budget
),
);
violations += 1;
}
@ -67,7 +75,10 @@ impl Certifier {
LogLevel::Warn,
LogSource::Pos,
0xCA02,
format!("Cert: syscalls per frame exceeded limit ({} > {})", telemetry.syscalls, limit),
format!(
"Cert: syscalls per frame exceeded limit ({} > {})",
telemetry.syscalls, limit
),
);
violations += 1;
}
@ -81,7 +92,10 @@ impl Certifier {
LogLevel::Warn,
LogSource::Pos,
0xCA03,
format!("Cert: host_cpu_time_us exceeded limit ({} > {})", telemetry.host_cpu_time_us, limit),
format!(
"Cert: host_cpu_time_us exceeded limit ({} > {})",
telemetry.host_cpu_time_us, limit
),
);
violations += 1;
}

View File

@ -10,11 +10,7 @@ pub struct TileMap {
impl TileMap {
fn create(width: usize, height: usize) -> Self {
Self {
width,
height,
tiles: vec![Tile::default(); width * height],
}
Self { width, height, tiles: vec![Tile::default(); width * height] }
}
pub fn set_tile(&mut self, x: usize, y: usize, tile: Tile) {
@ -24,7 +20,6 @@ impl TileMap {
}
}
pub struct TileLayer {
pub bank_id: u8,
pub tile_size: TileSize,
@ -33,11 +28,7 @@ pub struct TileLayer {
impl TileLayer {
fn create(width: usize, height: usize, tile_size: TileSize) -> Self {
Self {
bank_id: 0,
tile_size,
map: TileMap::create(width, height),
}
Self { bank_id: 0, tile_size, map: TileMap::create(width, height) }
}
}
@ -62,11 +53,7 @@ pub struct ScrollableTileLayer {
impl ScrollableTileLayer {
pub fn new(width: usize, height: usize, tile_size: TileSize) -> Self {
Self {
layer: TileLayer::create(width, height, tile_size),
scroll_x: 0,
scroll_y: 0,
}
Self { layer: TileLayer::create(width, height, tile_size), scroll_x: 0, scroll_y: 0 }
}
}
@ -89,9 +76,7 @@ pub struct HudTileLayer {
impl HudTileLayer {
pub fn new(width: usize, height: usize) -> Self {
Self {
layer: TileLayer::create(width, height, Size8),
}
Self { layer: TileLayer::create(width, height, Size8) }
}
}

View File

@ -1,5 +1,5 @@
use crate::input_signals::InputSignals;
use crate::button::Button;
use crate::input_signals::InputSignals;
pub trait TouchBridge {
fn begin_frame(&mut self, signals: &InputSignals);

View File

@ -1,6 +1,6 @@
mod virtual_machine_runtime;
mod services;
mod programs;
mod services;
mod virtual_machine_runtime;
pub use programs::PrometeuHub;
pub use services::fs;

View File

@ -1,9 +1,9 @@
use crate::programs::prometeu_hub::window_manager::WindowManager;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::color::Color;
use prometeu_hal::HardwareBridge;
use prometeu_hal::window::Rect;
use crate::VirtualMachineRuntime;
use crate::programs::prometeu_hub::window_manager::WindowManager;
use prometeu_hal::HardwareBridge;
use prometeu_hal::color::Color;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::window::Rect;
/// PrometeuHub: Launcher and system UI environment.
pub struct PrometeuHub {
@ -12,9 +12,7 @@ pub struct PrometeuHub {
impl PrometeuHub {
pub fn new() -> Self {
Self {
window_manager: WindowManager::new(),
}
Self { window_manager: WindowManager::new() }
}
pub fn init(&mut self) {
@ -28,16 +26,29 @@ impl PrometeuHub {
if hw.pad().a().pressed {
os.log(LogLevel::Debug, LogSource::Hub, 0, "window A opened".to_string());
next_window = Some(("Green Window".to_string(), Rect { x: 0, y: 0, w: 160, h: 90 }, Color::GREEN));
next_window = Some((
"Green Window".to_string(),
Rect { x: 0, y: 0, w: 160, h: 90 },
Color::GREEN,
));
} else if hw.pad().b().pressed {
os.log(LogLevel::Debug, LogSource::Hub, 0, "window B opened".to_string());
next_window = Some(("Indigo Window".to_string(), Rect { x: 160, y: 0, w: 160, h: 90 }, Color::INDIGO));
next_window = Some((
"Indigo Window".to_string(),
Rect { x: 160, y: 0, w: 160, h: 90 },
Color::INDIGO,
));
} else if hw.pad().x().pressed {
os.log(LogLevel::Debug, LogSource::Hub, 0, "window X opened".to_string());
next_window = Some(("Yellow Window".to_string(), Rect { x: 0, y: 90, w: 160, h: 90 }, Color::YELLOW));
next_window = Some((
"Yellow Window".to_string(),
Rect { x: 0, y: 90, w: 160, h: 90 },
Color::YELLOW,
));
} else if hw.pad().y().pressed {
os.log(LogLevel::Debug, LogSource::Hub, 0, "window Y opened".to_string());
next_window = Some(("Red Window".to_string(), Rect { x: 160, y: 90, w: 160, h: 90 }, Color::RED));
next_window =
Some(("Red Window".to_string(), Rect { x: 160, y: 90, w: 160, h: 90 }, Color::RED));
}
if let Some((title, rect, color)) = next_window {

View File

@ -9,21 +9,12 @@ pub struct WindowManager {
impl WindowManager {
pub fn new() -> Self {
Self {
windows: Vec::new(),
focused: None,
}
Self { windows: Vec::new(), focused: None }
}
pub fn add_window(&mut self, title: String, viewport: Rect, color: Color) -> WindowId {
let id = WindowId(self.windows.len() as u32);
let window = Window {
id,
viewport,
has_focus: false,
title,
color,
};
let window = Window { id, viewport, has_focus: false, title, color };
self.windows.push(window);
id
}
@ -55,8 +46,13 @@ mod tests {
#[test]
fn test_window_manager_focus() {
let mut wm = WindowManager::new();
let id1 = wm.add_window("Window 1".to_string(), Rect { x: 0, y: 0, w: 10, h: 10 }, Color::WHITE);
let id2 = wm.add_window("Window 2".to_string(), Rect { x: 10, y: 10, w: 10, h: 10 }, Color::WHITE);
let id1 =
wm.add_window("Window 1".to_string(), Rect { x: 0, y: 0, w: 10, h: 10 }, Color::WHITE);
let id2 = wm.add_window(
"Window 2".to_string(),
Rect { x: 10, y: 10, w: 10, h: 10 },
Color::WHITE,
);
assert_eq!(wm.windows.len(), 2);
assert_eq!(wm.focused, None);
@ -75,7 +71,8 @@ mod tests {
#[test]
fn test_window_manager_remove_window() {
let mut wm = WindowManager::new();
let id = wm.add_window("Window".to_string(), Rect { x: 0, y: 0, w: 10, h: 10 }, Color::WHITE);
let id =
wm.add_window("Window".to_string(), Rect { x: 0, y: 0, w: 10, h: 10 }, Color::WHITE);
wm.set_focus(id);
assert_eq!(wm.focused, Some(id));

View File

@ -8,5 +8,7 @@ pub trait FsBackend: Send + Sync {
fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), FsError>;
fn delete(&mut self, path: &str) -> Result<(), FsError>;
fn exists(&self, path: &str) -> bool;
fn is_healthy(&self) -> bool { true }
fn is_healthy(&self) -> bool {
true
}
}

View File

@ -1,7 +1,7 @@
mod fs_backend;
mod fs_entry;
mod fs_error;
mod fs_state;
mod fs_entry;
mod fs_backend;
mod virtual_fs;
pub use fs_backend::FsBackend;

View File

@ -88,22 +88,21 @@ mod tests {
impl MockBackend {
fn new() -> Self {
Self {
files: HashMap::new(),
healthy: true,
}
Self { files: HashMap::new(), healthy: true }
}
}
impl FsBackend for MockBackend {
fn mount(&mut self) -> Result<(), FsError> { Ok(()) }
fn mount(&mut self) -> Result<(), FsError> {
Ok(())
}
fn unmount(&mut self) {}
fn list_dir(&self, _path: &str) -> Result<Vec<FsEntry>, FsError> {
Ok(self.files.keys().map(|name| FsEntry {
name: name.clone(),
is_dir: false,
size: 0,
}).collect())
Ok(self
.files
.keys()
.map(|name| FsEntry { name: name.clone(), is_dir: false, size: 0 })
.collect())
}
fn read_file(&self, path: &str) -> Result<Vec<u8>, FsError> {
self.files.get(path).cloned().ok_or(FsError::NotFound)

View File

@ -1,21 +1,20 @@
use prometeu_hal::syscalls::Syscall;
use crate::fs::{FsBackend, FsState, VirtualFS};
use prometeu_hal::{HardwareBridge, InputSignals};
use prometeu_hal::log::{LogLevel, LogService, LogSource};
use prometeu_hal::telemetry::{CertificationConfig, Certifier, TelemetryFrame};
use std::collections::HashMap;
use std::time::Instant;
use prometeu_bytecode::{Value, TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE};
use prometeu_hal::{expect_bool, expect_int, HostContext, HostReturn, NativeInterface, SyscallId};
use prometeu_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value};
use prometeu_hal::asset::{BankType, LoadStatus, SlotRef};
use prometeu_hal::button::Button;
use prometeu_hal::cartridge::{AppMode, Cartridge};
use prometeu_hal::color::Color;
use prometeu_hal::log::{LogLevel, LogService, LogSource};
use prometeu_hal::sprite::Sprite;
use prometeu_hal::syscalls::Syscall;
use prometeu_hal::telemetry::{CertificationConfig, Certifier, TelemetryFrame};
use prometeu_hal::tile::Tile;
use prometeu_hal::vm_fault::VmFault;
use prometeu_hal::{HardwareBridge, InputSignals};
use prometeu_hal::{HostContext, HostReturn, NativeInterface, SyscallId, expect_bool, expect_int};
use prometeu_vm::{LogicalFrameEndingReason, VirtualMachine};
use std::collections::HashMap;
use std::time::Instant;
pub struct VirtualMachineRuntime {
/// Host Tick Index: Incremented on every host hardware update (usually 60Hz).
@ -128,7 +127,12 @@ impl VirtualMachineRuntime {
match self.fs.mount(backend) {
Ok(_) => {
self.fs_state = FsState::Mounted;
self.log(LogLevel::Info, LogSource::Fs, 0, "Filesystem mounted successfully".to_string());
self.log(
LogLevel::Info,
LogSource::Fs,
0,
"Filesystem mounted successfully".to_string(),
);
}
Err(e) => {
let err_msg = format!("Failed to mount filesystem: {:?}", e);
@ -146,7 +150,12 @@ impl VirtualMachineRuntime {
fn update_fs(&mut self) {
if self.fs_state == FsState::Mounted {
if !self.fs.is_healthy() {
self.log(LogLevel::Error, LogSource::Fs, 0, "Filesystem became unhealthy, unmounting".to_string());
self.log(
LogLevel::Error,
LogSource::Fs,
0,
"Filesystem became unhealthy, unmounting".to_string(),
);
self.unmount_fs();
}
}
@ -173,7 +182,12 @@ impl VirtualMachineRuntime {
self.current_entrypoint = cartridge.entrypoint.clone();
}
Err(e) => {
self.log(LogLevel::Error, LogSource::Vm, 0, format!("Failed to initialize VM: {:?}", e));
self.log(
LogLevel::Error,
LogSource::Vm,
0,
format!("Failed to initialize VM: {:?}", e),
);
// Fail fast: no program is installed, no app id is switched.
// We don't update current_app_id or other fields.
}
@ -181,7 +195,11 @@ impl VirtualMachineRuntime {
}
/// Executes a single VM instruction (Debug).
pub fn debug_step_instruction(&mut self, vm: &mut VirtualMachine, hw: &mut dyn HardwareBridge) -> Option<String> {
pub fn debug_step_instruction(
&mut self,
vm: &mut VirtualMachine,
hw: &mut dyn HardwareBridge,
) -> Option<String> {
let mut ctx = HostContext::new(Some(hw));
match vm.step(self, &mut ctx) {
Ok(_) => None,
@ -198,7 +216,12 @@ impl VirtualMachineRuntime {
/// This method is responsible for managing the logical frame lifecycle.
/// A single host tick might execute a full logical frame, part of it,
/// or multiple frames depending on the configured slices.
pub fn tick(&mut self, vm: &mut VirtualMachine, signals: &InputSignals, hw: &mut dyn HardwareBridge) -> Option<String> {
pub fn tick(
&mut self,
vm: &mut VirtualMachine,
signals: &InputSignals,
hw: &mut dyn HardwareBridge,
) -> Option<String> {
let start = Instant::now();
self.tick_index += 1;
@ -228,7 +251,11 @@ impl VirtualMachineRuntime {
// Reset telemetry for the new logical frame
self.telemetry_current = TelemetryFrame {
frame_index: self.logical_frame_index,
cycles_budget: self.certifier.config.cycles_budget_per_frame.unwrap_or(Self::CYCLES_PER_LOGICAL_FRAME),
cycles_budget: self
.certifier
.config
.cycles_budget_per_frame
.unwrap_or(Self::CYCLES_PER_LOGICAL_FRAME),
..Default::default()
};
}
@ -247,7 +274,8 @@ impl VirtualMachineRuntime {
match run_result {
Ok(run) => {
self.logical_frame_remaining_cycles = self.logical_frame_remaining_cycles.saturating_sub(run.cycles_used);
self.logical_frame_remaining_cycles =
self.logical_frame_remaining_cycles.saturating_sub(run.cycles_used);
// Accumulate metrics for telemetry and certification
self.telemetry_current.cycles_used += run.cycles_used;
@ -257,7 +285,12 @@ impl VirtualMachineRuntime {
if run.reason == LogicalFrameEndingReason::Breakpoint {
self.paused = true;
self.debug_step_request = false;
self.log(LogLevel::Info, LogSource::Vm, 0xDEB1, format!("Breakpoint hit at PC 0x{:X}", vm.pc));
self.log(
LogLevel::Info,
LogSource::Vm,
0xDEB1,
format!("Breakpoint hit at PC 0x{:X}", vm.pc),
);
}
// Handle Panics
@ -268,18 +301,24 @@ impl VirtualMachineRuntime {
}
// 4. Frame Finalization (FRAME_SYNC reached or Entrypoint returned)
if run.reason == LogicalFrameEndingReason::FrameSync ||
run.reason == LogicalFrameEndingReason::EndOfRom {
if run.reason == LogicalFrameEndingReason::FrameSync
|| run.reason == LogicalFrameEndingReason::EndOfRom
{
// All drawing commands for this frame are now complete.
// Finalize the framebuffer.
hw.gfx_mut().render_all();
// Finalize frame telemetry
self.telemetry_current.host_cpu_time_us = start.elapsed().as_micros() as u64;
self.telemetry_current.host_cpu_time_us =
start.elapsed().as_micros() as u64;
// Evaluate CAP (Execution Budget Compliance)
let ts_ms = self.boot_time.elapsed().as_millis() as u64;
self.telemetry_current.violations = self.certifier.evaluate(&self.telemetry_current, &mut self.log_service, ts_ms) as u32;
self.telemetry_current.violations = self.certifier.evaluate(
&self.telemetry_current,
&mut self.log_service,
ts_ms,
) as u32;
// Latch telemetry for the Host/Debugger to read.
self.telemetry_last = self.telemetry_current;
@ -324,7 +363,9 @@ impl VirtualMachineRuntime {
self.telemetry_current.audio_slots_occupied = audio_stats.slots_occupied as u32;
// If the frame ended exactly in this tick, we update the final real time in the latch.
if !self.logical_frame_active && self.telemetry_last.frame_index == self.logical_frame_index.wrapping_sub(1) {
if !self.logical_frame_active
&& self.telemetry_last.frame_index == self.logical_frame_index.wrapping_sub(1)
{
self.telemetry_last.host_cpu_time_us = self.last_frame_cpu_time_us;
self.telemetry_last.cycles_budget = self.telemetry_current.cycles_budget;
self.telemetry_last.gfx_used_bytes = self.telemetry_current.gfx_used_bytes;
@ -343,7 +384,6 @@ impl VirtualMachineRuntime {
self.logs_written_this_frame.clear();
}
// Helper for syscalls
fn syscall_log_write(&mut self, level_val: i64, tag: u16, msg: String) -> Result<(), VmFault> {
let level = match level_val {
@ -361,7 +401,12 @@ impl VirtualMachineRuntime {
if count >= Self::MAX_LOGS_PER_FRAME {
if count == Self::MAX_LOGS_PER_FRAME {
self.logs_written_this_frame.insert(app_id, count + 1);
self.log(LogLevel::Warn, LogSource::App { app_id }, 0, "App exceeded log limit per frame".to_string());
self.log(
LogLevel::Warn,
LogSource::App { app_id },
0,
"App exceeded log limit per frame".to_string(),
);
}
return Ok(());
}
@ -434,11 +479,17 @@ impl NativeInterface for VirtualMachineRuntime {
/// - 0x5000: Logging
///
/// Each syscall returns the number of virtual cycles it consumed.
fn syscall(&mut self, id: SyscallId, args: &[Value], ret: &mut HostReturn, ctx: &mut HostContext) -> Result<(), VmFault> {
fn syscall(
&mut self,
id: SyscallId,
args: &[Value],
ret: &mut HostReturn,
ctx: &mut HostContext,
) -> Result<(), VmFault> {
self.telemetry_current.syscalls += 1;
let syscall = Syscall::from_u32(id).ok_or_else(|| VmFault::Trap(TRAP_INVALID_SYSCALL, format!(
"Unknown syscall: 0x{:08X}", id
)))?;
let syscall = Syscall::from_u32(id).ok_or_else(|| {
VmFault::Trap(TRAP_INVALID_SYSCALL, format!("Unknown syscall: 0x{:08X}", id))
})?;
// Handle hardware-less syscalls first
match syscall {
@ -457,7 +508,6 @@ impl NativeInterface for VirtualMachineRuntime {
match syscall {
// --- System Syscalls ---
Syscall::SystemHasCart => unreachable!(),
Syscall::SystemRunCart => unreachable!(),
@ -535,7 +585,10 @@ impl NativeInterface for VirtualMachineRuntime {
}
// gfx.set_sprite(asset_name, id, x, y, tile_id, palette_id, active, flip_x, flip_y, priority)
Syscall::GfxSetSprite => {
let asset_name = match args.get(0).ok_or_else(|| VmFault::Panic("Missing asset_name".into()))? {
let asset_name = match args
.get(0)
.ok_or_else(|| VmFault::Panic("Missing asset_name".into()))?
{
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_name".into())),
};
@ -549,7 +602,8 @@ impl NativeInterface for VirtualMachineRuntime {
let flip_y = expect_bool(args, 8)?;
let priority = expect_int(args, 9)? as u8;
let bank_id = hw.assets().find_slot_by_name(&asset_name, BankType::TILES).unwrap_or(0);
let bank_id =
hw.assets().find_slot_by_name(&asset_name, BankType::TILES).unwrap_or(0);
if index < 512 {
*hw.gfx_mut().sprite_mut(index) = Sprite {
@ -569,7 +623,10 @@ impl NativeInterface for VirtualMachineRuntime {
Syscall::GfxDrawText => {
let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? as i32;
let msg = match args.get(2).ok_or_else(|| VmFault::Panic("Missing message".into()))? {
let msg = match args
.get(2)
.ok_or_else(|| VmFault::Panic("Missing message".into()))?
{
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string message".into())),
};
@ -583,7 +640,10 @@ impl NativeInterface for VirtualMachineRuntime {
Syscall::GfxClear565 => {
let color_val = expect_int(args, 0)? as u32;
if color_val > 0xFFFF {
return Err(VmFault::Trap(TRAP_OOB, "Color value out of bounds (bounded)".into()));
return Err(VmFault::Trap(
TRAP_OOB,
"Color value out of bounds (bounded)".into(),
));
}
let color = Color::from_raw(color_val as u16);
hw.gfx_mut().clear(color);
@ -655,9 +715,18 @@ impl NativeInterface for VirtualMachineRuntime {
Syscall::InputPadSnapshot => {
let pad = hw.pad();
for btn in [
pad.up(), pad.down(), pad.left(), pad.right(),
pad.a(), pad.b(), pad.x(), pad.y(),
pad.l(), pad.r(), pad.start(), pad.select(),
pad.up(),
pad.down(),
pad.left(),
pad.right(),
pad.a(),
pad.b(),
pad.x(),
pad.y(),
pad.l(),
pad.r(),
pad.start(),
pad.select(),
] {
ret.push_bool(btn.pressed);
ret.push_bool(btn.released);
@ -783,7 +852,10 @@ impl NativeInterface for VirtualMachineRuntime {
let voice_id = expect_int(args, 1)? as usize;
let volume = expect_int(args, 2)? as u8;
let pan = expect_int(args, 3)? as u8;
let pitch = match args.get(4).ok_or_else(|| VmFault::Panic("Missing pitch".into()))? {
let pitch = match args
.get(4)
.ok_or_else(|| VmFault::Panic("Missing pitch".into()))?
{
Value::Float(f) => *f,
Value::Int32(i) => *i as f64,
Value::Int64(i) => *i as f64,
@ -791,14 +863,26 @@ impl NativeInterface for VirtualMachineRuntime {
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected number for pitch".into())),
};
hw.audio_mut().play(0, sample_id as u16, voice_id, volume, pan, pitch, 0, prometeu_hal::LoopMode::Off);
hw.audio_mut().play(
0,
sample_id as u16,
voice_id,
volume,
pan,
pitch,
0,
prometeu_hal::LoopMode::Off,
);
ret.push_null();
Ok(())
}
// audio.play(asset_name, sample_id, voice_id, volume, pan, pitch, loop_mode)
Syscall::AudioPlay => {
let asset_name = match args.get(0).ok_or_else(|| VmFault::Panic("Missing asset_name".into()))? {
let asset_name = match args
.get(0)
.ok_or_else(|| VmFault::Panic("Missing asset_name".into()))?
{
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_name".into())),
};
@ -806,7 +890,10 @@ impl NativeInterface for VirtualMachineRuntime {
let voice_id = expect_int(args, 2)? as usize;
let volume = expect_int(args, 3)? as u8;
let pan = expect_int(args, 4)? as u8;
let pitch = match args.get(5).ok_or_else(|| VmFault::Panic("Missing pitch".into()))? {
let pitch = match args
.get(5)
.ok_or_else(|| VmFault::Panic("Missing pitch".into()))?
{
Value::Float(f) => *f,
Value::Int32(i) => *i as f64,
Value::Int64(i) => *i as f64,
@ -818,7 +905,8 @@ impl NativeInterface for VirtualMachineRuntime {
_ => prometeu_hal::LoopMode::On,
};
let bank_id = hw.assets().find_slot_by_name(&asset_name, BankType::SOUNDS).unwrap_or(0);
let bank_id =
hw.assets().find_slot_by_name(&asset_name, BankType::SOUNDS).unwrap_or(0);
hw.audio_mut().play(bank_id, sample_id, voice_id, volume, pan, pitch, 0, loop_mode);
ret.push_null();
@ -846,7 +934,10 @@ impl NativeInterface for VirtualMachineRuntime {
// FS_READ(handle) -> content
Syscall::FsRead => {
let handle = expect_int(args, 0)? as u32;
let path = self.open_files.get(&handle).ok_or_else(|| VmFault::Panic("Invalid handle".into()))?;
let path = self
.open_files
.get(&handle)
.ok_or_else(|| VmFault::Panic("Invalid handle".into()))?;
match self.fs.read_file(path) {
Ok(data) => {
let s = String::from_utf8_lossy(&data).into_owned();
@ -859,11 +950,17 @@ impl NativeInterface for VirtualMachineRuntime {
// FS_WRITE(handle, content)
Syscall::FsWrite => {
let handle = expect_int(args, 0)? as u32;
let content = match args.get(1).ok_or_else(|| VmFault::Panic("Missing content".into()))? {
let content = match args
.get(1)
.ok_or_else(|| VmFault::Panic("Missing content".into()))?
{
Value::String(s) => s.as_bytes().to_vec(),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string content".into())),
};
let path = self.open_files.get(&handle).ok_or_else(|| VmFault::Panic("Invalid handle".into()))?;
let path = self
.open_files
.get(&handle)
.ok_or_else(|| VmFault::Panic("Invalid handle".into()))?;
match self.fs.write_file(path, &content) {
Ok(_) => ret.push_bool(true),
Err(_) => ret.push_bool(false),
@ -919,7 +1016,10 @@ impl NativeInterface for VirtualMachineRuntime {
// LOG_WRITE(level, msg)
Syscall::LogWrite => {
let level = expect_int(args, 0)?;
let msg = match args.get(1).ok_or_else(|| VmFault::Panic("Missing message".into()))? {
let msg = match args
.get(1)
.ok_or_else(|| VmFault::Panic("Missing message".into()))?
{
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string message".into())),
};
@ -931,7 +1031,10 @@ impl NativeInterface for VirtualMachineRuntime {
Syscall::LogWriteTag => {
let level = expect_int(args, 0)?;
let tag = expect_int(args, 1)? as u16;
let msg = match args.get(2).ok_or_else(|| VmFault::Panic("Missing message".into()))? {
let msg = match args
.get(2)
.ok_or_else(|| VmFault::Panic("Missing message".into()))?
{
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string message".into())),
};
@ -942,7 +1045,10 @@ impl NativeInterface for VirtualMachineRuntime {
// --- Asset Syscalls ---
Syscall::AssetLoad => {
let asset_id = match args.get(0).ok_or_else(|| VmFault::Panic("Missing asset_id".into()))? {
let asset_id = match args
.get(0)
.ok_or_else(|| VmFault::Panic("Missing asset_id".into()))?
{
Value::String(s) => s.clone(),
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_id".into())),
};

View File

@ -7,3 +7,6 @@ license.workspace = true
[dependencies]
prometeu-bytecode = { path = "../prometeu-bytecode" }
prometeu-hal = { path = "../prometeu-hal" }
[dev-dependencies]
prometeu-test-support = { path = "../../dev/prometeu-test-support" }

View File

@ -1,11 +1,9 @@
mod virtual_machine;
mod call_frame;
mod scope_frame;
pub mod local_addressing;
mod scope_frame;
pub mod verifier;
mod virtual_machine;
pub mod vm_init_error;
pub use prometeu_hal::{
HostContext, HostReturn, NativeInterface, SyscallId,
};
pub use prometeu_hal::{HostContext, HostReturn, NativeInterface, SyscallId};
pub use virtual_machine::{BudgetReport, LogicalFrameEndingReason, VirtualMachine};

View File

@ -1,6 +1,6 @@
use crate::call_frame::CallFrame;
use prometeu_bytecode::{TrapInfo, TRAP_INVALID_LOCAL};
use prometeu_bytecode::FunctionMeta;
use prometeu_bytecode::{TRAP_INVALID_LOCAL, TrapInfo};
/// Computes the absolute stack index for the start of the current frame's locals (including args).
pub fn local_base(frame: &CallFrame) -> usize {
@ -14,7 +14,12 @@ pub fn local_index(frame: &CallFrame, slot: u32) -> usize {
/// Validates that a local slot index is within the valid range for the function.
/// Range: 0 <= slot < (param_slots + local_slots)
pub fn check_local_slot(meta: &FunctionMeta, slot: u32, opcode: u16, pc: u32) -> Result<(), TrapInfo> {
pub fn check_local_slot(
meta: &FunctionMeta,
slot: u32,
opcode: u16,
pc: u32,
) -> Result<(), TrapInfo> {
let limit = meta.param_slots as u32 + meta.local_slots as u32;
if slot < limit {
Ok(())

View File

@ -1,9 +1,9 @@
use prometeu_hal::syscalls::Syscall;
use prometeu_bytecode::{decode_next, DecodeError};
use prometeu_bytecode::FunctionMeta;
use prometeu_bytecode::OpCode;
use prometeu_bytecode::OpCodeSpecExt;
use prometeu_bytecode::FunctionMeta;
use prometeu_bytecode::{compute_function_layouts, FunctionLayout};
use prometeu_bytecode::{DecodeError, decode_next};
use prometeu_bytecode::{FunctionLayout, compute_function_layouts};
use prometeu_hal::syscalls::Syscall;
use std::collections::{HashMap, HashSet, VecDeque};
#[derive(Debug, Clone, PartialEq, Eq)]
@ -71,14 +71,23 @@ impl Verifier {
while pc < func_code.len() {
valid_pc.insert(pc);
let instr = decode_next(pc, func_code).map_err(|e| match e {
DecodeError::UnknownOpcode { pc: _, opcode } =>
VerifierError::UnknownOpcode { pc: func_start + pc, opcode },
DecodeError::TruncatedOpcode { pc: _ } =>
VerifierError::TruncatedOpcode { pc: func_start + pc },
DecodeError::TruncatedImmediate { pc: _, opcode, need, have } =>
VerifierError::TruncatedImmediate { pc: func_start + pc, opcode, need, have },
DecodeError::ImmediateSizeMismatch { pc: _, opcode, expected, actual } =>
VerifierError::TruncatedImmediate { pc: func_start + pc, opcode, need: expected, have: actual },
DecodeError::UnknownOpcode { pc: _, opcode } => {
VerifierError::UnknownOpcode { pc: func_start + pc, opcode }
}
DecodeError::TruncatedOpcode { pc: _ } => {
VerifierError::TruncatedOpcode { pc: func_start + pc }
}
DecodeError::TruncatedImmediate { pc: _, opcode, need, have } => {
VerifierError::TruncatedImmediate { pc: func_start + pc, opcode, need, have }
}
DecodeError::ImmediateSizeMismatch { pc: _, opcode, expected, actual } => {
VerifierError::TruncatedImmediate {
pc: func_start + pc,
opcode,
need: expected,
have: actual,
}
}
})?;
pc = instr.next_pc;
}
@ -113,9 +122,7 @@ impl Verifier {
})?;
(callee.param_slots, callee.return_slots)
}
OpCode::Ret => {
(func.return_slots, 0)
}
OpCode::Ret => (func.return_slots, 0),
OpCode::Syscall => {
let id = instr.imm_u32().unwrap();
let syscall = Syscall::from_u32(id).ok_or_else(|| {
@ -127,7 +134,10 @@ impl Verifier {
};
if in_height < pops {
return Err(VerifierError::StackUnderflow { pc: func_start + pc, opcode: instr.opcode });
return Err(VerifierError::StackUnderflow {
pc: func_start + pc,
opcode: instr.opcode,
});
}
let out_height = in_height - pops + pushes;
@ -135,7 +145,11 @@ impl Verifier {
if instr.opcode == OpCode::Ret {
if in_height != func.return_slots {
return Err(VerifierError::BadRetStackHeight { pc: func_start + pc, height: in_height, expected: func.return_slots });
return Err(VerifierError::BadRetStackHeight {
pc: func_start + pc,
height: in_height,
expected: func.return_slots,
});
}
}
@ -143,7 +157,8 @@ impl Verifier {
if spec.is_branch {
// Canonical contract: branch immediate is RELATIVE to function start.
let target_rel = instr.imm_u32().unwrap() as usize;
let func_end_abs = layouts.get(func_idx).map(|l| l.end).unwrap_or_else(|| code.len());
let func_end_abs =
layouts.get(func_idx).map(|l| l.end).unwrap_or_else(|| code.len());
let func_len = func_end_abs - func_start;
if target_rel > func_len {
@ -168,23 +183,38 @@ impl Verifier {
target_abs_expected,
is_boundary_target_rel
);
return Err(VerifierError::InvalidJumpTarget { pc: pc_abs, target: target_abs_expected });
return Err(VerifierError::InvalidJumpTarget {
pc: pc_abs,
target: target_abs_expected,
});
}
if target_rel == func_len {
// salto para o fim da função
if out_height != func.return_slots {
return Err(VerifierError::BadRetStackHeight { pc: func_start + pc, height: out_height, expected: func.return_slots });
return Err(VerifierError::BadRetStackHeight {
pc: func_start + pc,
height: out_height,
expected: func.return_slots,
});
}
// caminho termina aqui
} else {
if !valid_pc.contains(&target_rel) {
return Err(VerifierError::JumpToMidInstruction { pc: func_start + pc, target: func_start + target_rel });
return Err(VerifierError::JumpToMidInstruction {
pc: func_start + pc,
target: func_start + target_rel,
});
}
if let Some(&existing_height) = stack_height_in.get(&target_rel) {
if existing_height != out_height {
return Err(VerifierError::StackMismatchJoin { pc: func_start + pc, target: func_start + target_rel, height_in: out_height, height_target: existing_height });
return Err(VerifierError::StackMismatchJoin {
pc: func_start + pc,
target: func_start + target_rel,
height_in: out_height,
height_target: existing_height,
});
}
} else {
stack_height_in.insert(target_rel, out_height);
@ -199,7 +229,12 @@ impl Verifier {
if next_pc < func_len {
if let Some(&existing_height) = stack_height_in.get(&next_pc) {
if existing_height != out_height {
return Err(VerifierError::StackMismatchJoin { pc: func_start + pc, target: func_start + next_pc, height_in: out_height, height_target: existing_height });
return Err(VerifierError::StackMismatchJoin {
pc: func_start + pc,
target: func_start + next_pc,
height_in: out_height,
height_target: existing_height,
});
}
} else {
stack_height_in.insert(next_pc, out_height);
@ -221,18 +256,15 @@ mod tests {
fn test_verifier_underflow() {
// OpCode::Add (2 bytes)
let code = vec![OpCode::Add as u8, 0x00];
let functions = vec![FunctionMeta {
code_offset: 0,
code_len: 2,
..Default::default()
}];
let functions = vec![FunctionMeta { code_offset: 0, code_len: 2, ..Default::default() }];
let res = Verifier::verify(&code, &functions);
assert_eq!(res, Err(VerifierError::StackUnderflow { pc: 0, opcode: OpCode::Add }));
}
#[test]
fn test_verifier_dup_underflow() {
let code = vec![(OpCode::Dup as u16).to_le_bytes()[0], (OpCode::Dup as u16).to_le_bytes()[1]];
let code =
vec![(OpCode::Dup as u16).to_le_bytes()[0], (OpCode::Dup as u16).to_le_bytes()[1]];
let functions = vec![FunctionMeta { code_offset: 0, code_len: 2, ..Default::default() }];
let res = Verifier::verify(&code, &functions);
assert_eq!(res, Err(VerifierError::StackUnderflow { pc: 0, opcode: OpCode::Dup }));
@ -243,11 +275,7 @@ mod tests {
// Jmp (2 bytes) + 100u32 (4 bytes)
let mut code = vec![OpCode::Jmp as u8, 0x00];
code.extend_from_slice(&100u32.to_le_bytes());
let functions = vec![FunctionMeta {
code_offset: 0,
code_len: 6,
..Default::default()
}];
let functions = vec![FunctionMeta { code_offset: 0, code_len: 6, ..Default::default() }];
let res = Verifier::verify(&code, &functions);
assert_eq!(res, Err(VerifierError::InvalidJumpTarget { pc: 0, target: 100 }));
}
@ -262,11 +290,7 @@ mod tests {
code.push(0x00);
code.extend_from_slice(&1u32.to_le_bytes());
let functions = vec![FunctionMeta {
code_offset: 0,
code_len: 12,
..Default::default()
}];
let functions = vec![FunctionMeta { code_offset: 0, code_len: 12, ..Default::default() }];
let res = Verifier::verify(&code, &functions);
assert_eq!(res, Err(VerifierError::JumpToMidInstruction { pc: 6, target: 1 }));
}
@ -295,11 +319,7 @@ mod tests {
#[test]
fn test_verifier_truncation_opcode() {
let code = vec![OpCode::PushI32 as u8]; // Truncated u16 opcode
let functions = vec![FunctionMeta {
code_offset: 0,
code_len: 1,
..Default::default()
}];
let functions = vec![FunctionMeta { code_offset: 0, code_len: 1, ..Default::default() }];
let res = Verifier::verify(&code, &functions);
assert_eq!(res, Err(VerifierError::TruncatedOpcode { pc: 0 }));
}
@ -308,13 +328,17 @@ mod tests {
fn test_verifier_truncation_immediate() {
let mut code = vec![OpCode::PushI32 as u8, 0x00];
code.push(0x42); // Only 1 byte of 4-byte immediate
let functions = vec![FunctionMeta {
code_offset: 0,
code_len: 3,
..Default::default()
}];
let functions = vec![FunctionMeta { code_offset: 0, code_len: 3, ..Default::default() }];
let res = Verifier::verify(&code, &functions);
assert_eq!(res, Err(VerifierError::TruncatedImmediate { pc: 0, opcode: OpCode::PushI32, need: 4, have: 1 }));
assert_eq!(
res,
Err(VerifierError::TruncatedImmediate {
pc: 0,
opcode: OpCode::PushI32,
need: 4,
have: 1
})
);
}
#[test]
@ -328,24 +352,39 @@ mod tests {
// 27: Nop
let mut code = Vec::new();
code.push(OpCode::PushBool as u8); code.push(0x00); code.push(1); // 0: PushBool (3 bytes)
code.push(OpCode::JmpIfTrue as u8); code.push(0x00); code.extend_from_slice(&15u32.to_le_bytes()); // 3: JmpIfTrue (6 bytes)
code.push(OpCode::Jmp as u8); code.push(0x00); code.extend_from_slice(&27u32.to_le_bytes()); // 9: Jmp (6 bytes)
code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes()); // 15: PushI32 (6 bytes)
code.push(OpCode::Jmp as u8); code.push(0x00); code.extend_from_slice(&27u32.to_le_bytes()); // 21: Jmp (6 bytes)
code.push(OpCode::Nop as u8); code.push(0x00); // 27: Nop (2 bytes)
code.push(OpCode::PushBool as u8);
code.push(0x00);
code.push(1); // 0: PushBool (3 bytes)
code.push(OpCode::JmpIfTrue as u8);
code.push(0x00);
code.extend_from_slice(&15u32.to_le_bytes()); // 3: JmpIfTrue (6 bytes)
code.push(OpCode::Jmp as u8);
code.push(0x00);
code.extend_from_slice(&27u32.to_le_bytes()); // 9: Jmp (6 bytes)
code.push(OpCode::PushI32 as u8);
code.push(0x00);
code.extend_from_slice(&1u32.to_le_bytes()); // 15: PushI32 (6 bytes)
code.push(OpCode::Jmp as u8);
code.push(0x00);
code.extend_from_slice(&27u32.to_le_bytes()); // 21: Jmp (6 bytes)
code.push(OpCode::Nop as u8);
code.push(0x00); // 27: Nop (2 bytes)
let functions = vec![FunctionMeta {
code_offset: 0,
code_len: 29,
..Default::default()
}];
let functions = vec![FunctionMeta { code_offset: 0, code_len: 29, ..Default::default() }];
let res = Verifier::verify(&code, &functions);
// Path 0->3->9->27: height 1-1+0 = 0.
// Path 0->3->15->21->27: height 1-1+1 = 1.
// Mismatch at 27: 0 vs 1.
assert_eq!(res, Err(VerifierError::StackMismatchJoin { pc: 21, target: 27, height_in: 1, height_target: 0 }));
assert_eq!(
res,
Err(VerifierError::StackMismatchJoin {
pc: 21,
target: 27,
height_in: 1,
height_target: 0
})
);
}
#[test]
@ -374,10 +413,16 @@ mod tests {
// Add
// Ret
let mut code = Vec::new();
code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes());
code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&2u32.to_le_bytes());
code.push(OpCode::Add as u8); code.push(0x00);
code.push(OpCode::Ret as u8); code.push(0x00);
code.push(OpCode::PushI32 as u8);
code.push(0x00);
code.extend_from_slice(&1u32.to_le_bytes());
code.push(OpCode::PushI32 as u8);
code.push(0x00);
code.extend_from_slice(&2u32.to_le_bytes());
code.push(OpCode::Add as u8);
code.push(0x00);
code.push(OpCode::Ret as u8);
code.push(0x00);
let functions = vec![FunctionMeta {
code_offset: 0,
@ -392,14 +437,11 @@ mod tests {
#[test]
fn test_verifier_invalid_syscall_id() {
let mut code = Vec::new();
code.push(OpCode::Syscall as u8); code.push(0x00);
code.push(OpCode::Syscall as u8);
code.push(0x00);
code.extend_from_slice(&0xDEADBEEFu32.to_le_bytes()); // Unknown ID
let functions = vec![FunctionMeta {
code_offset: 0,
code_len: 6,
..Default::default()
}];
let functions = vec![FunctionMeta { code_offset: 0, code_len: 6, ..Default::default() }];
let res = Verifier::verify(&code, &functions);
assert_eq!(res, Err(VerifierError::InvalidSyscallId { pc: 0, id: 0xDEADBEEF }));
}

View File

@ -3,10 +3,13 @@ use crate::scope_frame::ScopeFrame;
use crate::verifier::Verifier;
use crate::vm_init_error::VmInitError;
use crate::{HostContext, NativeInterface};
use prometeu_bytecode::{TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DEAD_GATE, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_INVALID_GATE, TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_STACK_UNDERFLOW, TRAP_TYPE};
use prometeu_bytecode::OpCode;
use prometeu_bytecode::ProgramImage;
use prometeu_bytecode::Value;
use prometeu_bytecode::{
TRAP_BAD_RET_SLOTS, TRAP_DEAD_GATE, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_INVALID_GATE,
TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_STACK_UNDERFLOW, TRAP_TYPE, TrapInfo,
};
use prometeu_hal::vm_fault::VmFault;
/// Reason why the Virtual Machine stopped execution during a specific run.
@ -126,7 +129,13 @@ impl VirtualMachine {
call_stack: Vec::new(),
scope_stack: Vec::new(),
globals: Vec::new(),
program: ProgramImage::new(rom, constant_pool, vec![], None, std::collections::HashMap::new()),
program: ProgramImage::new(
rom,
constant_pool,
vec![],
None,
std::collections::HashMap::new(),
),
heap: Vec::new(),
gate_pool: Vec::new(),
cycles: 0,
@ -137,7 +146,11 @@ impl VirtualMachine {
/// Resets the VM state and loads a new program.
/// This is typically called by the Firmware when starting a new App/Cartridge.
pub fn initialize(&mut self, program_bytes: Vec<u8>, entrypoint: &str) -> Result<(), VmInitError> {
pub fn initialize(
&mut self,
program_bytes: Vec<u8>,
entrypoint: &str,
) -> Result<(), VmInitError> {
// Fail fast: reset state upfront. If we return early with an error,
// the VM is left in a "halted and empty" state.
self.program = ProgramImage::default();
@ -169,7 +182,9 @@ impl VirtualMachine {
program
}
Err(prometeu_bytecode::LoadError::InvalidVersion) => return Err(VmInitError::UnsupportedFormat),
Err(prometeu_bytecode::LoadError::InvalidVersion) => {
return Err(VmInitError::UnsupportedFormat);
}
Err(e) => {
return Err(VmInitError::ImageLoadFailed(e));
}
@ -182,13 +197,17 @@ impl VirtualMachine {
let pc = if entrypoint.is_empty() {
program.functions.get(0).map(|f| f.code_offset as usize).unwrap_or(0)
} else if let Ok(func_idx) = entrypoint.parse::<usize>() {
program.functions.get(func_idx)
program
.functions
.get(func_idx)
.map(|f| f.code_offset as usize)
.ok_or(VmInitError::EntrypointNotFound)?
} else {
// Try to resolve as a symbol name from the exports map
if let Some(&func_idx) = program.exports.get(entrypoint) {
program.functions.get(func_idx as usize)
program
.functions
.get(func_idx as usize)
.map(|f| f.code_offset as usize)
.ok_or(VmInitError::EntrypointNotFound)?
} else {
@ -211,9 +230,7 @@ impl VirtualMachine {
idx
} else {
// Try to resolve as a symbol name
self.program.exports.get(entrypoint)
.map(|&idx| idx as usize)
.ok_or(()).unwrap_or(0) // Default to 0 if not found
self.program.exports.get(entrypoint).map(|&idx| idx as usize).ok_or(()).unwrap_or(0) // Default to 0 if not found
};
let callee = self.program.functions.get(func_idx).cloned().unwrap_or_default();
@ -311,7 +328,10 @@ impl VirtualMachine {
// Integrity check: ensure real progress is being made to avoid infinite loops
// caused by zero-cycle instructions or stuck PC.
if self.pc == pc_before && self.cycles == cycles_before && !self.halted {
ending_reason = Some(LogicalFrameEndingReason::Panic(format!("VM stuck at PC 0x{:08X}", self.pc)));
ending_reason = Some(LogicalFrameEndingReason::Panic(format!(
"VM stuck at PC 0x{:08X}",
self.pc
)));
break;
}
}
@ -339,10 +359,7 @@ impl VirtualMachine {
if self.pc + 2 > self.program.rom.len() {
return Err("Unexpected end of ROM".into());
}
let bytes = [
self.program.rom[self.pc],
self.program.rom[self.pc + 1],
];
let bytes = [self.program.rom[self.pc], self.program.rom[self.pc + 1]];
Ok(u16::from_le_bytes(bytes))
}
@ -352,7 +369,11 @@ impl VirtualMachine {
/// 1. Fetch: Read the opcode from memory.
/// 2. Decode: Identify what operation to perform.
/// 3. Execute: Perform the operation, updating stacks, memory, or calling peripherals.
pub fn step(&mut self, native: &mut dyn NativeInterface, ctx: &mut HostContext) -> Result<(), LogicalFrameEndingReason> {
pub fn step(
&mut self,
native: &mut dyn NativeInterface,
ctx: &mut HostContext,
) -> Result<(), LogicalFrameEndingReason> {
if self.halted || self.pc >= self.program.rom.len() {
return Ok(());
}
@ -373,35 +394,66 @@ impl VirtualMachine {
self.halted = true;
}
OpCode::Jmp => {
let target = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize;
let func_start = self.call_stack.last().map(|f| self.program.functions[f.func_idx].code_offset as usize).unwrap_or(0);
let target = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize;
let func_start = self
.call_stack
.last()
.map(|f| self.program.functions[f.func_idx].code_offset as usize)
.unwrap_or(0);
self.pc = func_start + target;
}
OpCode::JmpIfFalse => {
let target = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize;
let target = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize;
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
match val {
Value::Boolean(false) => {
let func_start = self.call_stack.last().map(|f| self.program.functions[f.func_idx].code_offset as usize).unwrap_or(0);
let func_start = self
.call_stack
.last()
.map(|f| self.program.functions[f.func_idx].code_offset as usize)
.unwrap_or(0);
self.pc = func_start + target;
}
Value::Boolean(true) => {}
_ => {
return Err(self.trap(TRAP_TYPE, opcode as u16, format!("Expected boolean for JMP_IF_FALSE, got {:?}", val), start_pc as u32));
return Err(self.trap(
TRAP_TYPE,
opcode as u16,
format!("Expected boolean for JMP_IF_FALSE, got {:?}", val),
start_pc as u32,
));
}
}
}
OpCode::JmpIfTrue => {
let target = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize;
let target = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize;
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
match val {
Value::Boolean(true) => {
let func_start = self.call_stack.last().map(|f| self.program.functions[f.func_idx].code_offset as usize).unwrap_or(0);
let func_start = self
.call_stack
.last()
.map(|f| self.program.functions[f.func_idx].code_offset as usize)
.unwrap_or(0);
self.pc = func_start + target;
}
Value::Boolean(false) => {}
_ => {
return Err(self.trap(TRAP_TYPE, opcode as u16, format!("Expected boolean for JMP_IF_TRUE, got {:?}", val), start_pc as u32));
return Err(self.trap(
TRAP_TYPE,
opcode as u16,
format!("Expected boolean for JMP_IF_TRUE, got {:?}", val),
start_pc as u32,
));
}
}
}
@ -409,38 +461,60 @@ impl VirtualMachine {
// Handled in run_budget for interruption
}
OpCode::PushConst => {
let idx = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize;
let val = self.program.constant_pool.get(idx).cloned().ok_or_else(|| LogicalFrameEndingReason::Panic("Invalid constant index".into()))?;
let idx = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize;
let val = self.program.constant_pool.get(idx).cloned().ok_or_else(|| {
LogicalFrameEndingReason::Panic("Invalid constant index".into())
})?;
self.push(val);
}
OpCode::PushI64 => {
let val = instr.imm_i64().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
let val = instr
.imm_i64()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
self.push(Value::Int64(val));
}
OpCode::PushI32 => {
let val = instr.imm_i32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
let val = instr
.imm_i32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
self.push(Value::Int32(val));
}
OpCode::PushBounded => {
let val = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
let val = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
if val > 0xFFFF {
return Err(self.trap(TRAP_OOB, opcode as u16, format!("Bounded value overflow: {} > 0xFFFF", val), start_pc as u32));
return Err(self.trap(
TRAP_OOB,
opcode as u16,
format!("Bounded value overflow: {} > 0xFFFF", val),
start_pc as u32,
));
}
self.push(Value::Bounded(val));
}
OpCode::PushF64 => {
let val = instr.imm_f64().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
let val = instr
.imm_f64()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
self.push(Value::Float(val));
}
OpCode::PushBool => {
let val = instr.imm_u8().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
let val = instr
.imm_u8()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
self.push(Value::Boolean(val != 0));
}
OpCode::Pop => {
self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
}
OpCode::PopN => {
let n = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
let n = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
for _ in 0..n {
self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
}
@ -461,7 +535,9 @@ impl VirtualMachine {
}
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_add(*b))),
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_add(*b))),
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((*a as i64).wrapping_add(*b))),
(Value::Int32(a), Value::Int64(b)) => {
Ok(Value::Int64((*a as i64).wrapping_add(*b)))
}
(Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_add(*b as i64))),
(Value::Float(a), Value::Float(b)) => Ok(Value::Float(a + b)),
(Value::Int32(a), Value::Float(b)) => Ok(Value::Float(*a as f64 + b)),
@ -471,7 +547,10 @@ impl VirtualMachine {
(Value::Bounded(a), Value::Bounded(b)) => {
let res = a.saturating_add(*b);
if res > 0xFFFF {
Err(OpError::Trap(TRAP_OOB, format!("Bounded addition overflow: {} + {} = {}", a, b, res)))
Err(OpError::Trap(
TRAP_OOB,
format!("Bounded addition overflow: {} + {} = {}", a, b, res),
))
} else {
Ok(Value::Bounded(res))
}
@ -490,7 +569,10 @@ impl VirtualMachine {
(Value::Float(a), Value::Int64(b)) => Ok(Value::Float(a - b as f64)),
(Value::Bounded(a), Value::Bounded(b)) => {
if a < b {
Err(OpError::Trap(TRAP_OOB, format!("Bounded subtraction underflow: {} - {} < 0", a, b)))
Err(OpError::Trap(
TRAP_OOB,
format!("Bounded subtraction underflow: {} - {} < 0", a, b),
))
} else {
Ok(Value::Bounded(a - b))
}
@ -510,7 +592,10 @@ impl VirtualMachine {
(Value::Bounded(a), Value::Bounded(b)) => {
let res = a as u64 * b as u64;
if res > 0xFFFF {
Err(OpError::Trap(TRAP_OOB, format!("Bounded multiplication overflow: {} * {} = {}", a, b, res)))
Err(OpError::Trap(
TRAP_OOB,
format!("Bounded multiplication overflow: {} * {} = {}", a, b, res),
))
} else {
Ok(Value::Bounded(res as u32))
}
@ -520,25 +605,37 @@ impl VirtualMachine {
OpCode::Div => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
(Value::Int32(a), Value::Int32(b)) => {
if b == 0 {
return Err(OpError::Trap(TRAP_DIV_ZERO, "Integer division by zero".into()));
return Err(OpError::Trap(
TRAP_DIV_ZERO,
"Integer division by zero".into(),
));
}
Ok(Value::Int32(a / b))
}
(Value::Int64(a), Value::Int64(b)) => {
if b == 0 {
return Err(OpError::Trap(TRAP_DIV_ZERO, "Integer division by zero".into()));
return Err(OpError::Trap(
TRAP_DIV_ZERO,
"Integer division by zero".into(),
));
}
Ok(Value::Int64(a / b))
}
(Value::Int32(a), Value::Int64(b)) => {
if b == 0 {
return Err(OpError::Trap(TRAP_DIV_ZERO, "Integer division by zero".into()));
return Err(OpError::Trap(
TRAP_DIV_ZERO,
"Integer division by zero".into(),
));
}
Ok(Value::Int64(a as i64 / b))
}
(Value::Int64(a), Value::Int32(b)) => {
if b == 0 {
return Err(OpError::Trap(TRAP_DIV_ZERO, "Integer division by zero".into()));
return Err(OpError::Trap(
TRAP_DIV_ZERO,
"Integer division by zero".into(),
));
}
Ok(Value::Int64(a / b as i64))
}
@ -574,7 +671,10 @@ impl VirtualMachine {
}
(Value::Bounded(a), Value::Bounded(b)) => {
if b == 0 {
return Err(OpError::Trap(TRAP_DIV_ZERO, "Bounded division by zero".into()));
return Err(OpError::Trap(
TRAP_DIV_ZERO,
"Bounded division by zero".into(),
));
}
Ok(Value::Bounded(a / b))
}
@ -606,19 +706,34 @@ impl VirtualMachine {
if let Value::Bounded(b) = val {
self.push(Value::Int64(b as i64));
} else {
return Err(LogicalFrameEndingReason::Panic("Expected bounded for BOUND_TO_INT".into()));
return Err(LogicalFrameEndingReason::Panic(
"Expected bounded for BOUND_TO_INT".into(),
));
}
}
OpCode::IntToBoundChecked => {
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
let int_val = val.as_integer().ok_or_else(|| LogicalFrameEndingReason::Panic("Expected integer for INT_TO_BOUND_CHECKED".into()))?;
let int_val = val.as_integer().ok_or_else(|| {
LogicalFrameEndingReason::Panic(
"Expected integer for INT_TO_BOUND_CHECKED".into(),
)
})?;
if int_val < 0 || int_val > 0xFFFF {
return Err(self.trap(TRAP_OOB, OpCode::IntToBoundChecked as u16, format!("Integer to bounded conversion out of range: {}", int_val), start_pc as u32));
return Err(self.trap(
TRAP_OOB,
OpCode::IntToBoundChecked as u16,
format!("Integer to bounded conversion out of range: {}", int_val),
start_pc as u32,
));
}
self.push(Value::Bounded(int_val as u32));
}
OpCode::Eq => self.binary_op(opcode, start_pc as u32, |a, b| Ok(Value::Boolean(a == b)))?,
OpCode::Neq => self.binary_op(opcode, start_pc as u32, |a, b| Ok(Value::Boolean(a != b)))?,
OpCode::Eq => {
self.binary_op(opcode, start_pc as u32, |a, b| Ok(Value::Boolean(a == b)))?
}
OpCode::Neq => {
self.binary_op(opcode, start_pc as u32, |a, b| Ok(Value::Boolean(a != b)))?
}
OpCode::Lt => self.binary_op(opcode, start_pc as u32, |a, b| {
a.partial_cmp(&b)
.map(|o| Value::Boolean(o == std::cmp::Ordering::Less))
@ -679,14 +794,18 @@ impl VirtualMachine {
OpCode::Shl => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_shl(b as u32))),
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_shl(b as u32))),
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_shl(b as u32))),
(Value::Int32(a), Value::Int64(b)) => {
Ok(Value::Int64((a as i64).wrapping_shl(b as u32)))
}
(Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_shl(b as u32))),
_ => Err(OpError::Panic("Invalid types for Shl".into())),
})?,
OpCode::Shr => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_shr(b as u32))),
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_shr(b as u32))),
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_shr(b as u32))),
(Value::Int32(a), Value::Int64(b)) => {
Ok(Value::Int64((a as i64).wrapping_shr(b as u32)))
}
(Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_shr(b as u32))),
_ => Err(OpError::Panic("Invalid types for Shr".into())),
})?,
@ -696,16 +815,26 @@ impl VirtualMachine {
Value::Int32(a) => self.push(Value::Int32(a.wrapping_neg())),
Value::Int64(a) => self.push(Value::Int64(a.wrapping_neg())),
Value::Float(a) => self.push(Value::Float(-a)),
_ => return Err(LogicalFrameEndingReason::Panic("Invalid type for Neg".into())),
_ => {
return Err(LogicalFrameEndingReason::Panic("Invalid type for Neg".into()));
}
}
}
OpCode::GetGlobal => {
let idx = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize;
let val = self.globals.get(idx).cloned().ok_or_else(|| LogicalFrameEndingReason::Panic("Invalid global index".into()))?;
let idx = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize;
let val = self.globals.get(idx).cloned().ok_or_else(|| {
LogicalFrameEndingReason::Panic("Invalid global index".into())
})?;
self.push(val);
}
OpCode::SetGlobal => {
let idx = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize;
let idx = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize;
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
if idx >= self.globals.len() {
self.globals.resize(idx + 1, Value::Null);
@ -713,39 +842,75 @@ impl VirtualMachine {
self.globals[idx] = val;
}
OpCode::GetLocal => {
let slot = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
let frame = self.call_stack.last().ok_or_else(|| LogicalFrameEndingReason::Panic("No active call frame".into()))?;
let slot = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
let frame = self.call_stack.last().ok_or_else(|| {
LogicalFrameEndingReason::Panic("No active call frame".into())
})?;
let func = &self.program.functions[frame.func_idx];
crate::local_addressing::check_local_slot(func, slot, opcode as u16, start_pc as u32)
.map_err(|trap_info| self.trap(trap_info.code, trap_info.opcode, trap_info.message, trap_info.pc))?;
crate::local_addressing::check_local_slot(
func,
slot,
opcode as u16,
start_pc as u32,
)
.map_err(|trap_info| {
self.trap(trap_info.code, trap_info.opcode, trap_info.message, trap_info.pc)
})?;
let stack_idx = crate::local_addressing::local_index(frame, slot);
let val = self.operand_stack.get(stack_idx).cloned().ok_or_else(|| LogicalFrameEndingReason::Panic("Internal error: validated local slot not found in stack".into()))?;
let val = self.operand_stack.get(stack_idx).cloned().ok_or_else(|| {
LogicalFrameEndingReason::Panic(
"Internal error: validated local slot not found in stack".into(),
)
})?;
self.push(val);
}
OpCode::SetLocal => {
let slot = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
let slot = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
let frame = self.call_stack.last().ok_or_else(|| LogicalFrameEndingReason::Panic("No active call frame".into()))?;
let frame = self.call_stack.last().ok_or_else(|| {
LogicalFrameEndingReason::Panic("No active call frame".into())
})?;
let func = &self.program.functions[frame.func_idx];
crate::local_addressing::check_local_slot(func, slot, opcode as u16, start_pc as u32)
.map_err(|trap_info| self.trap(trap_info.code, trap_info.opcode, trap_info.message, trap_info.pc))?;
crate::local_addressing::check_local_slot(
func,
slot,
opcode as u16,
start_pc as u32,
)
.map_err(|trap_info| {
self.trap(trap_info.code, trap_info.opcode, trap_info.message, trap_info.pc)
})?;
let stack_idx = crate::local_addressing::local_index(frame, slot);
self.operand_stack[stack_idx] = val;
}
OpCode::Call => {
let func_id = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize;
let func_id = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize;
let callee = self.program.functions.get(func_id).ok_or_else(|| {
self.trap(TRAP_INVALID_FUNC, opcode as u16, format!("Invalid func_id {}", func_id), start_pc as u32)
self.trap(
TRAP_INVALID_FUNC,
opcode as u16,
format!("Invalid func_id {}", func_id),
start_pc as u32,
)
})?;
if self.operand_stack.len() < callee.param_slots as usize {
return Err(LogicalFrameEndingReason::Panic(format!(
"Stack underflow during CALL to func {}: expected at least {} arguments, got {}",
func_id, callee.param_slots, self.operand_stack.len()
func_id,
callee.param_slots,
self.operand_stack.len()
)));
}
@ -764,12 +929,17 @@ impl VirtualMachine {
self.pc = callee.code_offset as usize;
}
OpCode::Ret => {
let frame = self.call_stack.pop().ok_or_else(|| LogicalFrameEndingReason::Panic("Call stack underflow".into()))?;
let frame = self.call_stack.pop().ok_or_else(|| {
LogicalFrameEndingReason::Panic("Call stack underflow".into())
})?;
let func = &self.program.functions[frame.func_idx];
let return_slots = func.return_slots as usize;
let current_height = self.operand_stack.len();
let expected_height = frame.stack_base + func.param_slots as usize + func.local_slots as usize + return_slots;
let expected_height = frame.stack_base
+ func.param_slots as usize
+ func.local_slots as usize
+ return_slots;
if current_height != expected_height {
return Err(self.trap(TRAP_BAD_RET_SLOTS, opcode as u16, format!(
@ -792,17 +962,19 @@ impl VirtualMachine {
self.pc = frame.return_pc as usize;
}
OpCode::PushScope => {
self.scope_stack.push(ScopeFrame {
scope_stack_base: self.operand_stack.len(),
});
self.scope_stack.push(ScopeFrame { scope_stack_base: self.operand_stack.len() });
}
OpCode::PopScope => {
let frame = self.scope_stack.pop().ok_or_else(|| LogicalFrameEndingReason::Panic("Scope stack underflow".into()))?;
let frame = self.scope_stack.pop().ok_or_else(|| {
LogicalFrameEndingReason::Panic("Scope stack underflow".into())
})?;
self.operand_stack.truncate(frame.scope_stack_base);
}
OpCode::Alloc => {
// Allocate a new gate with given type and number of slots.
let (type_id, slots_u32) = instr.imm_u32x2().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
let (type_id, slots_u32) = instr
.imm_u32x2()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
let slots = slots_u32 as usize;
// Bump-allocate on the heap and zero-initialize with Null.
@ -812,12 +984,8 @@ impl VirtualMachine {
}
// Insert entry into gate pool; GateId is index in this pool.
let entry = GateEntry {
alive: true,
base: base_idx as u32,
slots: slots_u32,
type_id,
};
let entry =
GateEntry { alive: true, base: base_idx as u32, slots: slots_u32, type_id };
let gate_id = self.gate_pool.len() as u32;
self.gate_pool.push(entry);
@ -825,7 +993,10 @@ impl VirtualMachine {
self.push(Value::Gate(gate_id as usize));
}
OpCode::GateLoad => {
let offset = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize;
let offset = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize;
let ref_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
if let Value::Gate(gid_usize) = ref_val {
let gid = gid_usize as GateId;
@ -835,20 +1006,38 @@ impl VirtualMachine {
// bounds check against slots
let off_u32 = offset as u32;
if off_u32 >= entry.slots {
return Err(self.trap(TRAP_OOB, OpCode::GateLoad as u16, format!("Out-of-bounds heap access at offset {}", offset), start_pc as u32));
return Err(self.trap(
TRAP_OOB,
OpCode::GateLoad as u16,
format!("Out-of-bounds heap access at offset {}", offset),
start_pc as u32,
));
}
let heap_idx = entry.base as usize + offset;
let val = self.heap.get(heap_idx).cloned().ok_or_else(|| {
// Should not happen if pool and heap are consistent, but keep defensive.
self.trap(TRAP_OOB, OpCode::GateLoad as u16, format!("Out-of-bounds heap access at offset {}", offset), start_pc as u32)
self.trap(
TRAP_OOB,
OpCode::GateLoad as u16,
format!("Out-of-bounds heap access at offset {}", offset),
start_pc as u32,
)
})?;
self.push(val);
} else {
return Err(self.trap(TRAP_TYPE, OpCode::GateLoad as u16, "Expected gate handle for GATE_LOAD".to_string(), start_pc as u32));
return Err(self.trap(
TRAP_TYPE,
OpCode::GateLoad as u16,
"Expected gate handle for GATE_LOAD".to_string(),
start_pc as u32,
));
}
}
OpCode::GateStore => {
let offset = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize;
let offset = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
as usize;
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
let ref_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
if let Value::Gate(gid_usize) = ref_val {
@ -858,30 +1047,54 @@ impl VirtualMachine {
.map_err(|t| t)?;
let off_u32 = offset as u32;
if off_u32 >= entry.slots {
return Err(self.trap(TRAP_OOB, OpCode::GateStore as u16, format!("Out-of-bounds heap access at offset {}", offset), start_pc as u32));
return Err(self.trap(
TRAP_OOB,
OpCode::GateStore as u16,
format!("Out-of-bounds heap access at offset {}", offset),
start_pc as u32,
));
}
let heap_idx = entry.base as usize + offset;
if heap_idx >= self.heap.len() {
return Err(self.trap(TRAP_OOB, OpCode::GateStore as u16, format!("Out-of-bounds heap access at offset {}", offset), start_pc as u32));
return Err(self.trap(
TRAP_OOB,
OpCode::GateStore as u16,
format!("Out-of-bounds heap access at offset {}", offset),
start_pc as u32,
));
}
self.heap[heap_idx] = val;
} else {
return Err(self.trap(TRAP_TYPE, OpCode::GateStore as u16, "Expected gate handle for GATE_STORE".to_string(), start_pc as u32));
return Err(self.trap(
TRAP_TYPE,
OpCode::GateStore as u16,
"Expected gate handle for GATE_STORE".to_string(),
start_pc as u32,
));
}
}
OpCode::GateBeginPeek | OpCode::GateEndPeek |
OpCode::GateBeginBorrow | OpCode::GateEndBorrow |
OpCode::GateBeginMutate | OpCode::GateEndMutate |
OpCode::GateRetain => {
}
OpCode::GateBeginPeek
| OpCode::GateEndPeek
| OpCode::GateBeginBorrow
| OpCode::GateEndBorrow
| OpCode::GateBeginMutate
| OpCode::GateEndMutate
| OpCode::GateRetain => {}
OpCode::GateRelease => {
self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
}
OpCode::Syscall => {
let pc_at_syscall = start_pc as u32;
let id = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
let id = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
let syscall = prometeu_hal::syscalls::Syscall::from_u32(id).ok_or_else(|| {
self.trap(TRAP_INVALID_SYSCALL, OpCode::Syscall as u16, format!("Unknown syscall: 0x{:08X}", id), pc_at_syscall)
self.trap(
TRAP_INVALID_SYSCALL,
OpCode::Syscall as u16,
format!("Unknown syscall: 0x{:08X}", id),
pc_at_syscall,
)
})?;
let args_count = syscall.args_count();
@ -889,7 +1102,12 @@ impl VirtualMachine {
let mut args = Vec::with_capacity(args_count);
for _ in 0..args_count {
let v = self.pop().map_err(|_e| {
self.trap(TRAP_STACK_UNDERFLOW, OpCode::Syscall as u16, "Syscall argument stack underflow".to_string(), pc_at_syscall)
self.trap(
TRAP_STACK_UNDERFLOW,
OpCode::Syscall as u16,
"Syscall argument stack underflow".to_string(),
pc_at_syscall,
)
})?;
args.push(v);
}
@ -898,9 +1116,13 @@ impl VirtualMachine {
let stack_height_before = self.operand_stack.len();
let mut ret = crate::HostReturn::new(&mut self.operand_stack);
native.syscall(id, &args, &mut ret, ctx).map_err(|fault| match fault {
VmFault::Trap(code, msg) => self.trap(code, OpCode::Syscall as u16, msg, pc_at_syscall),
VmFault::Trap(code, msg) => {
self.trap(code, OpCode::Syscall as u16, msg, pc_at_syscall)
}
VmFault::Panic(msg) => LogicalFrameEndingReason::Panic(msg),
VmFault::Unavailable => LogicalFrameEndingReason::Panic("Host feature unavailable".into()),
VmFault::Unavailable => {
LogicalFrameEndingReason::Panic("Host feature unavailable".into())
}
})?;
let stack_height_after = self.operand_stack.len();
@ -908,7 +1130,10 @@ impl VirtualMachine {
if results_pushed != syscall.results_count() {
return Err(LogicalFrameEndingReason::Panic(format!(
"Syscall {} (0x{:08X}) results mismatch: expected {}, got {}",
syscall.name(), id, syscall.results_count(), results_pushed
syscall.name(),
id,
syscall.results_count(),
results_pushed
)));
}
}
@ -923,15 +1148,15 @@ impl VirtualMachine {
}
/// Resolves a `GateId` to an immutable reference to its `GateEntry` or returns a trap reason.
fn resolve_gate(&self, gate_id: GateId, opcode: u16, pc: u32) -> Result<&GateEntry, LogicalFrameEndingReason> {
fn resolve_gate(
&self,
gate_id: GateId,
opcode: u16,
pc: u32,
) -> Result<&GateEntry, LogicalFrameEndingReason> {
let idx = gate_id as usize;
let entry = self.gate_pool.get(idx).ok_or_else(|| {
self.trap(
TRAP_INVALID_GATE,
opcode,
format!("Invalid gate id: {}", gate_id),
pc,
)
self.trap(TRAP_INVALID_GATE, opcode, format!("Invalid gate id: {}", gate_id), pc)
})?;
if !entry.alive {
return Err(self.trap(
@ -944,7 +1169,13 @@ impl VirtualMachine {
Ok(entry)
}
pub fn trap(&self, code: u32, opcode: u16, message: String, pc: u32) -> LogicalFrameEndingReason {
pub fn trap(
&self,
code: u32,
opcode: u16,
message: String,
pc: u32,
) -> LogicalFrameEndingReason {
LogicalFrameEndingReason::Trap(self.program.create_trap(code, opcode, message, pc))
}
@ -973,7 +1204,12 @@ impl VirtualMachine {
self.operand_stack.last().ok_or("Stack underflow".into())
}
fn binary_op<F>(&mut self, opcode: OpCode, start_pc: u32, f: F) -> Result<(), LogicalFrameEndingReason>
fn binary_op<F>(
&mut self,
opcode: OpCode,
start_pc: u32,
f: F,
) -> Result<(), LogicalFrameEndingReason>
where
F: FnOnce(Value, Value) -> Result<Value, OpError>,
{
@ -1010,7 +1246,13 @@ mod tests {
struct MockNative;
impl NativeInterface for MockNative {
fn syscall(&mut self, _id: u32, _args: &[Value], _ret: &mut HostReturn, _ctx: &mut HostContext) -> Result<(), VmFault> {
fn syscall(
&mut self,
_id: u32,
_args: &[Value],
_ret: &mut HostReturn,
_ctx: &mut HostContext,
) -> Result<(), VmFault> {
Ok(())
}
}
@ -1254,7 +1496,13 @@ mod tests {
];
let mut vm = VirtualMachine {
program: ProgramImage::new(rom, vec![], functions, None, std::collections::HashMap::new()),
program: ProgramImage::new(
rom,
vec![],
functions,
None,
std::collections::HashMap::new(),
),
..Default::default()
};
vm.prepare_call("0");
@ -1299,7 +1547,13 @@ mod tests {
];
let mut vm = VirtualMachine {
program: ProgramImage::new(rom, vec![], functions, None, std::collections::HashMap::new()),
program: ProgramImage::new(
rom,
vec![],
functions,
None,
std::collections::HashMap::new(),
),
..Default::default()
};
vm.prepare_call("0");
@ -1338,7 +1592,13 @@ mod tests {
];
let mut vm2 = VirtualMachine {
program: ProgramImage::new(rom2, vec![], functions2, None, std::collections::HashMap::new()),
program: ProgramImage::new(
rom2,
vec![],
functions2,
None,
std::collections::HashMap::new(),
),
..Default::default()
};
vm2.prepare_call("0");
@ -1447,7 +1707,13 @@ mod tests {
];
let mut vm = VirtualMachine {
program: ProgramImage::new(rom, vec![], functions, None, std::collections::HashMap::new()),
program: ProgramImage::new(
rom,
vec![],
functions,
None,
std::collections::HashMap::new(),
),
..Default::default()
};
vm.prepare_call("0");
@ -1782,7 +2048,7 @@ mod tests {
0x17, 0x00, // PushI32
0x00, 0x00, 0x00, 0x00, // value 0
0x11, 0x00, // Pop
0x51, 0x00 // Ret
0x51, 0x00, // Ret
];
let mut vm = VirtualMachine::new(rom.clone(), vec![]);
vm.program.functions = std::sync::Arc::from(vec![prometeu_bytecode::FunctionMeta {
@ -1793,7 +2059,15 @@ mod tests {
let mut ctx = HostContext::new(None);
struct TestNative;
impl NativeInterface for TestNative {
fn syscall(&mut self, _id: u32, _args: &[Value], _ret: &mut HostReturn, _ctx: &mut HostContext) -> Result<(), VmFault> { Ok(()) }
fn syscall(
&mut self,
_id: u32,
_args: &[Value],
_ret: &mut HostReturn,
_ctx: &mut HostContext,
) -> Result<(), VmFault> {
Ok(())
}
}
let mut native = TestNative;
@ -1811,7 +2085,13 @@ mod tests {
struct MultiReturnNative;
impl NativeInterface for MultiReturnNative {
fn syscall(&mut self, _id: u32, _args: &[Value], ret: &mut HostReturn, _ctx: &mut HostContext) -> Result<(), VmFault> {
fn syscall(
&mut self,
_id: u32,
_args: &[Value],
ret: &mut HostReturn,
_ctx: &mut HostContext,
) -> Result<(), VmFault> {
ret.push_bool(true);
ret.push_int(42);
ret.push_bounded(255)?;
@ -1845,7 +2125,13 @@ mod tests {
struct VoidReturnNative;
impl NativeInterface for VoidReturnNative {
fn syscall(&mut self, _id: u32, _args: &[Value], _ret: &mut HostReturn, _ctx: &mut HostContext) -> Result<(), VmFault> {
fn syscall(
&mut self,
_id: u32,
_args: &[Value],
_ret: &mut HostReturn,
_ctx: &mut HostContext,
) -> Result<(), VmFault> {
Ok(())
}
}
@ -1879,7 +2165,13 @@ mod tests {
struct ArgCheckNative;
impl NativeInterface for ArgCheckNative {
fn syscall(&mut self, _id: u32, args: &[Value], _ret: &mut HostReturn, _ctx: &mut HostContext) -> Result<(), VmFault> {
fn syscall(
&mut self,
_id: u32,
args: &[Value],
_ret: &mut HostReturn,
_ctx: &mut HostContext,
) -> Result<(), VmFault> {
expect_int(args, 0)?;
Ok(())
}
@ -1967,7 +2259,13 @@ mod tests {
struct BadNative;
impl NativeInterface for BadNative {
fn syscall(&mut self, _id: u32, _args: &[Value], ret: &mut HostReturn, _ctx: &mut HostContext) -> Result<(), VmFault> {
fn syscall(
&mut self,
_id: u32,
_args: &[Value],
ret: &mut HostReturn,
_ctx: &mut HostContext,
) -> Result<(), VmFault> {
// Wrong: GfxClear565 is void but we push something
ret.push_int(42);
Ok(())
@ -2029,7 +2327,7 @@ mod tests {
let res = vm.initialize(header, "");
match res {
Err(VmInitError::ImageLoadFailed(prometeu_bytecode::LoadError::UnexpectedEof)) => {},
Err(VmInitError::ImageLoadFailed(prometeu_bytecode::LoadError::UnexpectedEof)) => {}
_ => panic!("Expected PbsV0LoadFailed(UnexpectedEof), got {:?}", res),
}
}
@ -2065,7 +2363,6 @@ mod tests {
assert_eq!(vm.cycles, 0);
}
#[test]
fn test_calling_convention_add() {
let mut native = MockNative;

View File

@ -0,0 +1,29 @@
//! Minimal, robust smoke test for the VM crate.
//!
//! Intentionally does not assert legacy ISA behavior. It only ensures that:
//! - The VM type can be instantiated without panicking.
//! - Deterministic test utilities work as expected (reproducible RNG).
use prometeu_test_support::{next_u64, rng_from_seed};
use prometeu_vm::VirtualMachine;
#[test]
fn vm_instantiation_is_stable() {
// Create a VM with empty ROM and empty constant pool.
let vm = VirtualMachine::new(vec![], vec![]);
// Basic invariant checks that should remain stable across refactors.
assert_eq!(vm.pc, 0);
assert!(!vm.halted);
}
#[test]
fn deterministic_rng_sequences_match() {
// Demonstrate deterministic behavior without relying on specific values.
let mut a = rng_from_seed(12345);
let mut b = rng_from_seed(12345);
for _ in 0..8 {
// Same seed => same sequence
assert_eq!(next_u64(&mut a), next_u64(&mut b));
}
}

View File

@ -0,0 +1,8 @@
[package]
name = "prometeu-test-support"
version = "0.1.0"
edition = "2024"
license.workspace = true
[dependencies]
rand = { version = "0.8", features = ["std", "std_rng"] }

View File

@ -0,0 +1,108 @@
//! Test-only utilities for deterministic behavior in unit tests.
//!
//! This crate is intended to be used as a dev-dependency only. It provides:
//! - Seeded RNG helpers for reproducible randomness in tests.
//! - A tiny deterministic clock abstraction for tests that need to reason about time.
use rand::{RngCore, SeedableRng, rngs::StdRng};
/// Builds a `StdRng` from a u64 seed in a deterministic way.
///
/// This expands the u64 seed into a 32-byte array (little-endian repeated)
/// to initialize `StdRng`.
pub fn rng_from_seed(seed: u64) -> StdRng {
let le = seed.to_le_bytes();
let mut buf = [0u8; 32];
// Repeat the 8-byte seed 4 times to fill 32 bytes
for i in 0..4 {
buf[i * 8..(i + 1) * 8].copy_from_slice(&le);
}
StdRng::from_seed(buf)
}
/// Convenience helper that returns a RNG with a fixed well-known seed.
pub fn deterministic_rng() -> StdRng {
rng_from_seed(0xC0FFEE_5EED)
}
/// Returns the next u64 from the provided RNG.
pub fn next_u64(rng: &mut StdRng) -> u64 {
rng.next_u64()
}
/// Collects `n` u64 values from the RNG.
pub fn take_n_u64(rng: &mut StdRng, n: usize) -> Vec<u64> {
let mut out = Vec::with_capacity(n);
for _ in 0..n {
out.push(rng.next_u64());
}
out
}
/// Simple deterministic clock abstraction for tests.
pub trait Clock {
fn now_millis(&self) -> u64;
}
/// A clock that always returns a fixed instant.
pub struct FixedClock {
now: u64,
}
impl FixedClock {
pub fn new(now: u64) -> Self {
Self { now }
}
}
impl Clock for FixedClock {
fn now_millis(&self) -> u64 {
self.now
}
}
/// A clock that advances by a constant step each tick.
pub struct TickingClock {
now: u64,
step: u64,
}
impl TickingClock {
pub fn new(start: u64, step: u64) -> Self {
Self { now: start, step }
}
pub fn tick(&mut self) {
self.now = self.now.saturating_add(self.step);
}
}
impl Clock for TickingClock {
fn now_millis(&self) -> u64 {
self.now
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rng_reproducible_sequences() {
let mut a = rng_from_seed(42);
let mut b = rng_from_seed(42);
for _ in 0..10 {
assert_eq!(a.next_u64(), b.next_u64());
}
}
#[test]
fn ticking_clock_advances_deterministically() {
let mut c = TickingClock::new(1000, 16);
assert_eq!(c.now_millis(), 1000);
c.tick();
assert_eq!(c.now_millis(), 1016);
c.tick();
assert_eq!(c.now_millis(), 1032);
}
}

View File

@ -1,10 +1,10 @@
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use prometeu_drivers::{AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};
use ringbuf::traits::{Consumer, Producer, Split};
use prometeu_hal::LoopMode;
use ringbuf::HeapRb;
use ringbuf::traits::{Consumer, Producer, Split};
use std::sync::Arc;
use std::time::Duration;
use prometeu_hal::LoopMode;
pub struct HostAudio {
pub producer: Option<ringbuf::wrap::CachingProd<Arc<HeapRb<AudioCommand>>>>,
@ -14,18 +14,12 @@ pub struct HostAudio {
impl HostAudio {
pub fn new() -> Self {
Self {
producer: None,
perf_consumer: None,
_stream: None,
}
Self { producer: None, perf_consumer: None, _stream: None }
}
pub fn init(&mut self) {
let host = cpal::default_host();
let device = host
.default_output_device()
.expect("no output device available");
let device = host.default_output_device().expect("no output device available");
let config = cpal::StreamConfig {
channels: 2,
@ -94,26 +88,17 @@ pub struct AudioMixer {
impl AudioMixer {
pub fn new() -> Self {
Self {
voices: Default::default(),
last_processing_time: Duration::ZERO,
paused: false,
}
Self { voices: Default::default(), last_processing_time: Duration::ZERO, paused: false }
}
pub fn process_command(&mut self, cmd: AudioCommand) {
match cmd {
AudioCommand::Play {
sample,
voice_id,
volume,
pan,
pitch,
priority,
loop_mode,
} => {
AudioCommand::Play { sample, voice_id, volume, pan, pitch, priority, loop_mode } => {
if voice_id < MAX_CHANNELS {
println!("[AudioMixer] Playing voice {}: vol={}, pitch={}, loop={:?}", voice_id, volume, pitch, loop_mode);
println!(
"[AudioMixer] Playing voice {}: vol={}, pitch={}, loop={:?}",
voice_id, volume, pitch, loop_mode
);
self.voices[voice_id] = Channel {
sample: Some(sample),
active: true,
@ -212,7 +197,8 @@ impl AudioMixer {
voice.pos += step;
let end_pos = sample_data.loop_end.map(|e| e as f64).unwrap_or(sample_data.data.len() as f64);
let end_pos =
sample_data.loop_end.map(|e| e as f64).unwrap_or(sample_data.data.len() as f64);
if voice.pos >= end_pos {
if voice.loop_mode == LoopMode::On {

View File

@ -2,16 +2,17 @@ use prometeu_hal::telemetry::CertificationConfig;
pub fn load_cap_config(path: &str) -> Option<CertificationConfig> {
let content = std::fs::read_to_string(path).ok()?;
let mut config = CertificationConfig {
enabled: true,
..Default::default()
};
let mut config = CertificationConfig { enabled: true, ..Default::default() };
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') { continue; }
if line.is_empty() || line.starts_with('#') {
continue;
}
let parts: Vec<&str> = line.split('=').collect();
if parts.len() != 2 { continue; }
if parts.len() != 2 {
continue;
}
let key = parts[0].trim();
let val = parts[1].trim();

View File

@ -1,9 +1,9 @@
use prometeu_hal::debugger_protocol::*;
use prometeu_drivers::hardware::Hardware;
use prometeu_firmware::{BootTarget, Firmware};
use prometeu_hal::cartridge_loader::CartridgeLoader;
use prometeu_hal::debugger_protocol::*;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use prometeu_drivers::hardware::Hardware;
use prometeu_hal::cartridge_loader::CartridgeLoader;
/// Host-side implementation of the PROMETEU DevTools Protocol.
///
@ -136,7 +136,9 @@ impl HostDebugger {
// Support multiple JSON messages in a single TCP packet.
for line in msg.lines() {
let trimmed = line.trim();
if trimmed.is_empty() { continue; }
if trimmed.is_empty() {
continue;
}
if let Ok(cmd) = serde_json::from_str::<DebugCommand>(trimmed) {
self.handle_command(cmd, firmware, hardware);
}
@ -162,7 +164,12 @@ impl HostDebugger {
}
/// Dispatches a specific DebugCommand to the system components.
fn handle_command(&mut self, cmd: DebugCommand, firmware: &mut Firmware, hardware: &mut Hardware) {
fn handle_command(
&mut self,
cmd: DebugCommand,
firmware: &mut Firmware,
hardware: &mut Hardware,
) {
match cmd {
DebugCommand::Ok | DebugCommand::Start => {
if self.waiting_for_start {
@ -189,8 +196,7 @@ impl HostDebugger {
}
DebugCommand::GetState => {
// Return detailed VM register and stack state.
let stack_top = firmware.vm.operand_stack.iter()
.rev().take(10).cloned().collect();
let stack_top = firmware.vm.operand_stack.iter().rev().take(10).cloned().collect();
let resp = DebugResponse::GetState {
pc: firmware.vm.pc,

View File

@ -1,6 +1,6 @@
use prometeu_system::fs::{FsBackend, FsEntry, FsError};
use std::fs;
use std::path::PathBuf;
use prometeu_system::fs::{FsBackend, FsEntry, FsError};
pub struct HostDirBackend {
root: PathBuf,
@ -90,7 +90,11 @@ mod tests {
fn get_temp_dir(name: &str) -> PathBuf {
let mut path = env::temp_dir();
path.push(format!("prometeu_host_test_{}_{}", name, std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()));
path.push(format!(
"prometeu_host_test_{}_{}",
name,
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()
));
fs::create_dir_all(&path).unwrap();
path
}

View File

@ -1,8 +1,8 @@
use prometeu_drivers::hardware::Hardware;
use prometeu_hal::InputSignals;
use winit::event::{ElementState, MouseButton, WindowEvent};
use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::Window;
use prometeu_drivers::hardware::Hardware;
use prometeu_hal::InputSignals;
pub struct HostInputHandler {
pub signals: InputSignals,
@ -10,9 +10,7 @@ pub struct HostInputHandler {
impl HostInputHandler {
pub fn new() -> Self {
Self {
signals: InputSignals::default(),
}
Self { signals: InputSignals::default() }
}
pub fn handle_event(&mut self, event: &WindowEvent, window: &Window) {
@ -35,7 +33,9 @@ impl HostInputHandler {
KeyCode::KeyE => self.signals.r_signal = is_down,
KeyCode::KeyZ => self.signals.start_signal = is_down,
KeyCode::ShiftLeft | KeyCode::ShiftRight => self.signals.select_signal = is_down,
KeyCode::ShiftLeft | KeyCode::ShiftRight => {
self.signals.select_signal = is_down
}
_ => {}
}

View File

@ -1,11 +1,11 @@
pub mod audio;
pub mod runner;
pub mod fs_backend;
pub mod log_sink;
pub mod debugger;
pub mod stats;
pub mod input;
pub mod cap;
pub mod debugger;
pub mod fs_backend;
pub mod input;
pub mod log_sink;
pub mod runner;
pub mod stats;
pub mod utilities;
use cap::load_cap_config;
@ -45,17 +45,9 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
let cap_config = cli.cap.as_ref().and_then(|path| load_cap_config(path));
let boot_target = if let Some(path) = cli.debug {
BootTarget::Cartridge {
path,
debug: true,
debug_port: cli.port,
}
BootTarget::Cartridge { path, debug: true, debug_port: cli.port }
} else if let Some(path) = cli.run {
BootTarget::Cartridge {
path,
debug: false,
debug_port: 7777,
}
BootTarget::Cartridge { path, debug: false, debug_port: 7777 }
} else {
BootTarget::Hub
};

View File

@ -7,7 +7,11 @@ use crate::stats::HostStats;
use crate::utilities::draw_rgb565_to_rgba8;
use pixels::wgpu::PresentMode;
use pixels::{Pixels, PixelsBuilder, SurfaceTexture};
use prometeu_drivers::AudioCommand;
use prometeu_drivers::hardware::Hardware;
use prometeu_firmware::{BootTarget, Firmware};
use prometeu_hal::color::Color;
use prometeu_hal::telemetry::CertificationConfig;
use std::time::{Duration, Instant};
use winit::application::ApplicationHandler;
use winit::dpi::LogicalSize;
@ -15,10 +19,6 @@ use winit::event::{ElementState, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow};
use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::{Window, WindowAttributes, WindowId};
use prometeu_hal::telemetry::CertificationConfig;
use prometeu_drivers::hardware::Hardware;
use prometeu_drivers::AudioCommand;
use prometeu_hal::color::Color;
/// The Desktop implementation of the PROMETEU Runtime.
///
@ -130,8 +130,18 @@ impl HostRunner {
let color_warn = Color::RED;
self.hardware.gfx.fill_rect(5, 5, 175, 100, color_bg);
self.hardware.gfx.draw_text(10, 10, &format!("FPS: {:.1}", self.stats.current_fps), color_text);
self.hardware.gfx.draw_text(10, 18, &format!("HOST: {:.2}MS", tel.host_cpu_time_us as f64 / 1000.0), color_text);
self.hardware.gfx.draw_text(
10,
10,
&format!("FPS: {:.1}", self.stats.current_fps),
color_text,
);
self.hardware.gfx.draw_text(
10,
18,
&format!("HOST: {:.2}MS", tel.host_cpu_time_us as f64 / 1000.0),
color_text,
);
self.hardware.gfx.draw_text(10, 26, &format!("STEPS: {}", tel.vm_steps), color_text);
self.hardware.gfx.draw_text(10, 34, &format!("SYSC: {}", tel.syscalls), color_text);
@ -140,25 +150,60 @@ impl HostRunner {
} else {
0.0
};
self.hardware.gfx.draw_text(10, 42, &format!("CYC: {}/{} ({:.1}%)", tel.cycles_used, tel.cycles_budget, cycles_pct), color_text);
self.hardware.gfx.draw_text(
10,
42,
&format!("CYC: {}/{} ({:.1}%)", tel.cycles_used, tel.cycles_budget, cycles_pct),
color_text,
);
self.hardware.gfx.draw_text(10, 50, &format!("GFX: {}K/16M ({}S)", tel.gfx_used_bytes / 1024, tel.gfx_slots_occupied), color_text);
self.hardware.gfx.draw_text(
10,
50,
&format!("GFX: {}K/16M ({}S)", tel.gfx_used_bytes / 1024, tel.gfx_slots_occupied),
color_text,
);
if tel.gfx_inflight_bytes > 0 {
self.hardware.gfx.draw_text(10, 58, &format!("LOAD GFX: {}KB", tel.gfx_inflight_bytes / 1024), color_warn);
self.hardware.gfx.draw_text(
10,
58,
&format!("LOAD GFX: {}KB", tel.gfx_inflight_bytes / 1024),
color_warn,
);
}
self.hardware.gfx.draw_text(10, 66, &format!("AUD: {}K/32M ({}S)", tel.audio_used_bytes / 1024, tel.audio_slots_occupied), color_text);
self.hardware.gfx.draw_text(
10,
66,
&format!("AUD: {}K/32M ({}S)", tel.audio_used_bytes / 1024, tel.audio_slots_occupied),
color_text,
);
if tel.audio_inflight_bytes > 0 {
self.hardware.gfx.draw_text(10, 74, &format!("LOAD AUD: {}KB", tel.audio_inflight_bytes / 1024), color_warn);
self.hardware.gfx.draw_text(
10,
74,
&format!("LOAD AUD: {}KB", tel.audio_inflight_bytes / 1024),
color_warn,
);
}
let cert_color = if tel.violations > 0 { color_warn } else { color_text };
self.hardware.gfx.draw_text(10, 82, &format!("CERT LAST: {}", tel.violations), cert_color);
if tel.violations > 0 {
if let Some(event) = self.firmware.os.log_service.get_recent(10).into_iter().rev().find(|e| e.tag >= 0xCA01 && e.tag <= 0xCA03) {
if let Some(event) = self
.firmware
.os
.log_service
.get_recent(10)
.into_iter()
.rev()
.find(|e| e.tag >= 0xCA01 && e.tag <= 0xCA03)
{
let mut msg = event.msg.clone();
if msg.len() > 30 { msg.truncate(30); }
if msg.len() > 30 {
msg.truncate(30);
}
self.hardware.gfx.draw_text(10, 90, &msg, color_warn);
}
}
@ -183,7 +228,8 @@ impl ApplicationHandler for HostRunner {
let size = window.inner_size();
let surface_texture = SurfaceTexture::new(size.width, size.height, window);
let mut pixels = PixelsBuilder::new(Hardware::W as u32, Hardware::H as u32, surface_texture)
let mut pixels =
PixelsBuilder::new(Hardware::W as u32, Hardware::H as u32, surface_texture)
.present_mode(PresentMode::Fifo) // activate vsync
.build()
.expect("failed to create Pixels");
@ -197,7 +243,6 @@ impl ApplicationHandler for HostRunner {
event_loop.set_control_flow(ControlFlow::Poll);
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
self.input.handle_event(&event, self.window());
@ -232,7 +277,6 @@ impl ApplicationHandler for HostRunner {
}
}
WindowEvent::KeyboardInput { event, .. } => {
if let PhysicalKey::Code(code) = event.physical_key {
let is_down = event.state == ElementState::Pressed;
@ -298,11 +342,8 @@ impl ApplicationHandler for HostRunner {
let is_paused = self.firmware.os.paused || self.debugger.waiting_for_start;
if is_paused != self.last_paused_state {
self.last_paused_state = is_paused;
let cmd = if is_paused {
AudioCommand::MasterPause
} else {
AudioCommand::MasterResume
};
let cmd =
if is_paused { AudioCommand::MasterPause } else { AudioCommand::MasterResume };
self.hardware.audio.commands.push(cmd);
}
@ -345,9 +386,9 @@ impl ApplicationHandler for HostRunner {
mod tests {
use super::*;
use prometeu_firmware::BootTarget;
use prometeu_hal::debugger_protocol::DEVTOOLS_PROTOCOL_VERSION;
use std::io::{Read, Write};
use std::net::TcpStream;
use prometeu_hal::debugger_protocol::DEVTOOLS_PROTOCOL_VERSION;
#[test]
fn test_debug_port_opens() {
@ -364,7 +405,8 @@ mod tests {
// Check if we can connect
{
let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect");
let mut stream =
TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect");
// Short sleep to ensure the OS processes
std::thread::sleep(std::time::Duration::from_millis(100));
@ -375,24 +417,36 @@ mod tests {
// Handshake Check
let mut buf = [0u8; 2048];
let n = stream.read(&mut buf).expect("Should read handshake");
let resp: serde_json::Value = serde_json::from_slice(&buf[..n]).expect("Handshake should be valid JSON");
let resp: serde_json::Value =
serde_json::from_slice(&buf[..n]).expect("Handshake should be valid JSON");
assert_eq!(resp["type"], "handshake");
assert_eq!(resp["protocol_version"], DEVTOOLS_PROTOCOL_VERSION);
// Send start via JSON
stream.write_all(b"{\"type\":\"start\"}\n").expect("Connection should be open for writing");
stream
.write_all(b"{\"type\":\"start\"}\n")
.expect("Connection should be open for writing");
std::thread::sleep(std::time::Duration::from_millis(50));
// Process the received command
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
assert!(!runner.debugger.waiting_for_start, "Execution should have started after start command");
assert!(runner.debugger.listener.is_some(), "Listener should remain open for reconnections");
assert!(
!runner.debugger.waiting_for_start,
"Execution should have started after start command"
);
assert!(
runner.debugger.listener.is_some(),
"Listener should remain open for reconnections"
);
}
// Now that the stream is out of the test scope, the runner should detect closure on next check
std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
assert!(runner.debugger.stream.is_none(), "Stream should have been closed after client disconnected");
assert!(
runner.debugger.stream.is_none(),
"Stream should have been closed after client disconnected"
);
}
#[test]
@ -407,7 +461,8 @@ mod tests {
// 1. Connect and start
{
let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 1");
let mut stream =
TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 1");
std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
assert!(runner.debugger.stream.is_some());
@ -430,7 +485,10 @@ mod tests {
std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
assert!(runner.debugger.stream.is_some(), "Stream should have been accepted on reconnection");
assert!(
runner.debugger.stream.is_some(),
"Stream should have been accepted on reconnection"
);
}
#[test]
@ -444,13 +502,15 @@ mod tests {
});
// 1. First connection
let mut _stream1 = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 1");
let mut _stream1 =
TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 1");
std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
assert!(runner.debugger.stream.is_some());
// 2. Second connection
let mut stream2 = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 2 (OS accepts)");
let mut stream2 = TcpStream::connect(format!("127.0.0.1:{}", port))
.expect("Should connect 2 (OS accepts)");
std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware); // Should accept and close stream2
@ -458,7 +518,10 @@ mod tests {
let mut buf = [0u8; 10];
stream2.set_read_timeout(Some(std::time::Duration::from_millis(100))).unwrap();
let res = stream2.read(&mut buf);
assert!(matches!(res, Ok(0)) || res.is_err(), "Second connection should be closed by server");
assert!(
matches!(res, Ok(0)) || res.is_err(),
"Second connection should be closed by server"
);
assert!(runner.debugger.stream.is_some(), "First connection should continue active");
}
@ -494,7 +557,9 @@ mod tests {
loop {
line.clear();
reader.read_line(&mut line).expect("Should read line");
if line.is_empty() { break; }
if line.is_empty() {
break;
}
if let Ok(resp) = serde_json::from_str::<serde_json::Value>(&line) {
if resp["type"] == "getState" {
@ -517,7 +582,8 @@ mod tests {
// 1. Connect and pause
{
let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect");
let mut stream =
TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect");
std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);

View File

@ -1,7 +1,7 @@
use prometeu_drivers::hardware::Hardware;
use prometeu_firmware::Firmware;
use std::time::{Duration, Instant};
use winit::window::Window;
use prometeu_drivers::hardware::Hardware;
pub struct HostStats {
pub last_stats_update: Instant,
@ -31,7 +31,13 @@ impl HostStats {
self.audio_load_samples += 1;
}
pub fn update(&mut self, now: Instant, window: Option<&Window>, _hardware: &Hardware, firmware: &Firmware) {
pub fn update(
&mut self,
now: Instant,
window: Option<&Window>,
_hardware: &Hardware,
firmware: &Firmware,
) {
let stats_elapsed = now.duration_since(self.last_stats_update);
if stats_elapsed >= Duration::from_secs(1) {
self.current_fps = self.frames_since_last_update as f64 / stats_elapsed.as_secs_f64();
@ -39,7 +45,8 @@ impl HostStats {
if let Some(window) = window {
// Fixed comparison always against 60Hz, keep even when doing CPU stress tests
let frame_budget_us = 16666.0;
let cpu_load_core = (firmware.os.last_frame_cpu_time_us as f64 / frame_budget_us) * 100.0;
let cpu_load_core =
(firmware.os.last_frame_cpu_time_us as f64 / frame_budget_us) * 100.0;
let cpu_load_audio = if self.audio_load_samples > 0 {
(self.audio_load_accum_us as f64 / stats_elapsed.as_micros() as f64) * 100.0
@ -49,7 +56,12 @@ impl HostStats {
let title = format!(
"PROMETEU | GFX: {:.1} KB | FPS: {:.1} | Load: {:.1}% (C) + {:.1}% (A) | Frame: tick {} logical {}",
0, self.current_fps, cpu_load_core, cpu_load_audio, firmware.os.tick_index, firmware.os.logical_frame_index
0,
self.current_fps,
cpu_load_core,
cpu_load_audio,
firmware.os.tick_index,
firmware.os.logical_frame_index
);
window.set_title(&title);
}

View File

@ -1 +1,3 @@
fn main() -> Result<(), Box<dyn std::error::Error>> { prometeu_host_desktop_winit::run() }
fn main() -> Result<(), Box<dyn std::error::Error>> {
prometeu_host_desktop_winit::run()
}

View File

@ -141,7 +141,8 @@ fn dispatch(exe_dir: &Path, bin_name: &str, args: &[&str]) {
"prometeuc" => "build/verify c",
"prometeup" => "pack/verify p",
_ => bin_name,
}, exe_dir.display()
},
exe_dir.display()
);
std::process::exit(1);
}
@ -168,9 +169,6 @@ fn execute_bin(bin_path: &Path, args: &[&str]) {
}
fn not_implemented(cmd: &str, _bin_name: &str) {
eprintln!(
"prometeu: command '{}' is not yet available in this distribution",
cmd
);
eprintln!("prometeu: command '{}' is not yet available in this distribution", cmd);
std::process::exit(1);
}

25
docs/ARCHITECTURE.md Normal file
View File

@ -0,0 +1,25 @@
# Prometeu Runtime — Architecture (Reset Invariants)
This document captures the high-level invariants for the reset cycle. It is a stub and will
evolve as we formalize the new ISA/VM specs. The goal is to define non-negotiable properties
that guide refactors without forcing legacy compatibility.
Core invariants
---------------
- Stack-based VM with heap-allocated objects.
- Garbage Collection happens at safepoints, primarily at `FRAME_SYNC`.
- Closures are first-class (user functions). Syscalls are callable but not first-class values.
- Coroutines are the only concurrency model.
- No backward compatibility: old bytecode formats, shims, or legacy bridges are out of scope.
Scope of this stage
-------------------
- Establish tooling baselines (fmt, clippy, CI) and minimal smoke tests.
- Avoid encoding legacy ISA semantics in tests; keep tests focused on build confidence.
- Production code must remain free from test-only hooks; use dev-only utilities for determinism.
Out of scope (for now)
----------------------
- Detailed ISA definition and instruction semantics beyond what is needed to compile and run
smoke-level validations.
- Performance tuning or GC algorithm selection.

View File

@ -1,15 +0,0 @@
# PROMETEU Documentation
This directory contains the technical documentation and specifications for the PROMETEU project.
## Content
### 📜 [Specifications (Specs)](./specs)
Detailed documentation on system architecture, cartridge format, VM instruction set, and more.
- [Topic Index](specs/hardware/topics/table-of-contents.md)
### 🐞 [Debugger](./debugger)
Documentation on debugging tools and how to integrate new tools into the ecosystem.
### 🔌 [DevTools Protocol](../devtools)
Definition of the communication protocol used for real-time debugging and inspection.

49
docs/STYLE.md Normal file
View File

@ -0,0 +1,49 @@
# Prometeu Runtime — Style Guide
This document defines the baseline code style and commenting policy for the reset cycle.
Goals:
- Professional, consistent, and readable code.
- Deterministic and actionable error messages.
- English-only comments and docs.
General principles
------------------
- Rust edition: use the crates configured edition (currently 2024 across new crates).
- Keep modules small and cohesive; prefer explicit interfaces over glob imports.
- Avoid premature abstraction; refactor when duplication becomes meaningful.
Comments and documentation
--------------------------
- Language: English only.
- Use `///` for public API documentation comments and module-level docs (`//!`).
- Use `//` for local/internal notes that do not need to surface in docs.
- Write comments that explain “why,” not “what” (the code already shows “what”).
- Keep comments up to date with behavior; outdated comments should be removed or fixed.
Error messages
--------------
- Be clear, actionable, and deterministic.
- Include the failing condition and the expected invariant when useful.
- Avoid leaking internal jargon; prefer user-understandable phrasing.
- Do not rely on timing or nondeterminism for error reproduction.
Naming conventions
------------------
- Crates and modules: `kebab-case` for crates, `snake_case` for modules and files.
- Types and traits: `PascalCase` (e.g., `VirtualMachine`, `BytecodeLoader`).
- Functions, methods, and variables: `snake_case`.
- Constants and statics: `SCREAMING_SNAKE_CASE`.
- Avoid abbreviations unless they are widely recognized (e.g., `pc`, `vm`).
Documentation structure
-----------------------
- Each public crate should have a crate-level docs section describing its purpose.
- Prefer small examples in docs that compile (use `rustdoc` code blocks when feasible).
- Keep module/file headers brief and focused.
Formatting and linting
----------------------
- `cargo fmt` is the source of truth for formatting.
- `cargo clippy` should run clean on the default toolchain. Where a lint is intentionally
violated, add a focused `#[allow(lint_name)]` with a short rationale.

View File

@ -1,19 +0,0 @@
### Phase 03 — Frontend API Boundary (Canon Contract)
This document codifies the FE/BE boundary invariants for Phase 03.
- BE is the source of truth. The `frontend-api` crate defines canonical models that all Frontends must produce.
- No string protocols across layers. Strings are only for display/debug. No hidden prefixes like `svc:` or `@dep:`.
- No FE implementation imports from other FE implementations.
- No BE imports PBS modules (hard boundary). The Backend consumes only canonical data structures from `frontend-api`.
- Overload resolution is signature-based. Arity alone is not sufficient; use canonical signatures/keys.
Implementation notes (PBS):
- The PBS adapter must not synthesize ownership or module info from string prefixes. All owner/module data should come from canonical types.
- Export/import surfaces are expressed exclusively via `frontend-api` types (e.g., `ItemName`, `ProjectAlias`, `ModulePath`, `ImportRef`, `ExportItem`).
Enforcement:
- A test in `prometeu-build-pipeline` scans `src/backend/**` to ensure no references to `frontends::pbs` are introduced.
- Code review should reject any PRs that reintroduce prefix-based heuristics or FE-to-FE coupling.

3
rustfmt.toml Normal file
View File

@ -0,0 +1,3 @@
max_width = 100
use_small_heuristics = "Max"
newline_style = "Unix"