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

@ -47,4 +47,4 @@ pub struct TrapInfo {
pub pc: u32,
/// Optional source span information if debug symbols are available.
pub span: Option<SourceSpan>,
}
}

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

@ -3,7 +3,7 @@ use crate::opcode::OpCode;
use serde::{Deserialize, Serialize};
/// An entry in the Constant Pool.
///
///
/// The Constant Pool is a table of unique values used by the program.
/// Instead of embedding large data (like strings) directly in the instruction stream,
/// the bytecode uses `PushConst <index>` to load these values onto the stack.
@ -62,9 +62,9 @@ pub struct Export {
/// Represents the final serialized format of a PBS v0 module.
///
/// This structure is a pure data container for the PBS format. It does NOT
/// contain any linker-like logic (symbol resolution, patching, etc.).
/// All multi-module programs must be flattened and linked by the compiler
/// This structure is a pure data container for the PBS format. It does NOT
/// contain any linker-like logic (symbol resolution, patching, etc.).
/// All multi-module programs must be flattened and linked by the compiler
/// before being serialized into this format.
#[derive(Debug, Clone, PartialEq)]
pub struct BytecodeModule {
@ -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"
@ -104,7 +115,7 @@ impl BytecodeModule {
out.extend_from_slice(&[0u8; 20]);
let mut current_offset = 32 + (final_sections.len() as u32 * 12);
// Write section table
for (kind, data) in &final_sections {
let k: u32 = *kind;
@ -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,22 +240,34 @@ impl BytecodeLoader {
}
let endianness = bytes[6];
if endianness != 0 { // 0 = Little Endian
if endianness != 0 {
// 0 = Little Endian
return Err(LoadError::InvalidEndianness);
}
let section_count = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
let mut sections = Vec::new();
let mut pos = 32;
for _ in 0..section_count {
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() {
return Err(LoadError::SectionOutOfBounds);
@ -254,7 +282,7 @@ impl BytecodeLoader {
for j in i + 1..sections.len() {
let (_, o1, l1) = sections[i];
let (_, o2, l2) = sections[j];
if (o1 < o2 + l2) && (o2 < o1 + l1) {
return Err(LoadError::OverlappingSections);
}
@ -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
@ -309,7 +342,7 @@ fn parse_const_pool(data: &[u8]) -> Result<Vec<ConstantPoolEntry>, LoadError> {
let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
let mut cp = Vec::with_capacity(count);
let mut pos = 4;
for _ in 0..count {
if pos >= data.len() {
return Err(LoadError::UnexpectedEof);
@ -318,35 +351,52 @@ 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); }
let val = i64::from_le_bytes(data[pos..pos+8].try_into().unwrap());
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); }
let val = f64::from_le_bytes(data[pos..pos+8].try_into().unwrap());
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); }
let len = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap()) as usize;
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); }
let s = String::from_utf8_lossy(&data[pos..pos+len]).into_owned();
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); }
let val = i32::from_le_bytes(data[pos..pos+4].try_into().unwrap());
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;
}
@ -366,18 +416,18 @@ fn parse_functions(data: &[u8]) -> Result<Vec<FunctionMeta>, LoadError> {
let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
let mut functions = Vec::with_capacity(count);
let mut pos = 4;
for _ in 0..count {
if pos + 16 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let code_offset = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap());
let code_len = u32::from_le_bytes(data[pos+4..pos+8].try_into().unwrap());
let param_slots = u16::from_le_bytes(data[pos+8..pos+10].try_into().unwrap());
let local_slots = u16::from_le_bytes(data[pos+10..pos+12].try_into().unwrap());
let return_slots = u16::from_le_bytes(data[pos+12..pos+14].try_into().unwrap());
let max_stack_slots = u16::from_le_bytes(data[pos+14..pos+16].try_into().unwrap());
let code_offset = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
let code_len = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
let param_slots = u16::from_le_bytes(data[pos + 8..pos + 10].try_into().unwrap());
let local_slots = u16::from_le_bytes(data[pos + 10..pos + 12].try_into().unwrap());
let return_slots = u16::from_le_bytes(data[pos + 12..pos + 14].try_into().unwrap());
let max_stack_slots = u16::from_le_bytes(data[pos + 14..pos + 16].try_into().unwrap());
functions.push(FunctionMeta {
code_offset,
code_len,
@ -398,47 +448,47 @@ fn parse_debug_section(data: &[u8]) -> Result<DebugInfo, LoadError> {
if data.len() < 8 {
return Err(LoadError::MalformedSection);
}
let mut pos = 0;
// PC to Span table
let span_count = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap()) as usize;
let span_count = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
pos += 4;
let mut pc_to_span = Vec::with_capacity(span_count);
for _ in 0..span_count {
if pos + 16 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let pc = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap());
let file_id = u32::from_le_bytes(data[pos+4..pos+8].try_into().unwrap());
let start = u32::from_le_bytes(data[pos+8..pos+12].try_into().unwrap());
let end = u32::from_le_bytes(data[pos+12..pos+16].try_into().unwrap());
let pc = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
let file_id = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
let start = u32::from_le_bytes(data[pos + 8..pos + 12].try_into().unwrap());
let end = u32::from_le_bytes(data[pos + 12..pos + 16].try_into().unwrap());
pc_to_span.push((pc, SourceSpan { file_id, start, end }));
pos += 16;
}
// Function names table
if pos + 4 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let func_name_count = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap()) as usize;
let func_name_count = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
pos += 4;
let mut function_names = Vec::with_capacity(func_name_count);
for _ in 0..func_name_count {
if pos + 8 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let func_idx = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap());
let name_len = u32::from_le_bytes(data[pos+4..pos+8].try_into().unwrap()) as usize;
let func_idx = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
let name_len = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap()) as usize;
pos += 8;
if pos + name_len > data.len() {
return Err(LoadError::UnexpectedEof);
}
let name = String::from_utf8_lossy(&data[pos..pos+name_len]).into_owned();
let name = String::from_utf8_lossy(&data[pos..pos + name_len]).into_owned();
function_names.push((func_idx, name));
pos += name_len;
}
Ok(DebugInfo { pc_to_span, function_names })
}
@ -457,20 +507,19 @@ fn parse_exports(data: &[u8]) -> Result<Vec<Export>, LoadError> {
if pos + 8 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let func_idx = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap());
let name_len = u32::from_le_bytes(data[pos+4..pos+8].try_into().unwrap()) as usize;
let func_idx = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
let name_len = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap()) as usize;
pos += 8;
if pos + name_len > data.len() {
return Err(LoadError::UnexpectedEof);
}
let symbol = String::from_utf8_lossy(&data[pos..pos+name_len]).into_owned();
let symbol = String::from_utf8_lossy(&data[pos..pos + name_len]).into_owned();
exports.push(Export { symbol, func_idx });
pos += name_len;
}
Ok(exports)
}
fn validate_module(module: &BytecodeModule) -> Result<(), LoadError> {
for func in &module.functions {
// Opcode stream bounds
@ -485,22 +534,35 @@ fn validate_module(module: &BytecodeModule) -> Result<(), LoadError> {
if pos + 2 > module.code.len() {
break; // Unexpected EOF in middle of opcode, maybe should be error
}
let op_val = u16::from_le_bytes([module.code[pos], module.code[pos+1]]);
let op_val = u16::from_le_bytes([module.code[pos], module.code[pos + 1]]);
let opcode = OpCode::try_from(op_val).map_err(|_| LoadError::InvalidOpcode)?;
pos += 2;
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 => {
@ -567,7 +629,7 @@ mod tests {
data.extend_from_slice(&1u32.to_le_bytes());
data.extend_from_slice(&80u32.to_le_bytes());
data.extend_from_slice(&32u32.to_le_bytes());
// Ensure data is long enough for the offsets
data.resize(256, 0);
@ -581,7 +643,7 @@ mod tests {
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&64u32.to_le_bytes());
data.extend_from_slice(&1000u32.to_le_bytes());
data.resize(256, 0);
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::SectionOutOfBounds));
@ -594,21 +656,21 @@ mod tests {
data.extend_from_slice(&1u32.to_le_bytes());
data.extend_from_slice(&64u32.to_le_bytes());
data.extend_from_slice(&20u32.to_le_bytes());
// Section 2: Code, Kind 2, Offset 128, Length 10
data.extend_from_slice(&2u32.to_le_bytes());
data.extend_from_slice(&128u32.to_le_bytes());
data.extend_from_slice(&10u32.to_le_bytes());
data.resize(256, 0);
// Setup functions section
let func_data_start = 64;
data[func_data_start..func_data_start+4].copy_from_slice(&1u32.to_le_bytes()); // 1 function
data[func_data_start..func_data_start + 4].copy_from_slice(&1u32.to_le_bytes()); // 1 function
let entry_start = func_data_start + 4;
data[entry_start..entry_start+4].copy_from_slice(&5u32.to_le_bytes()); // code_offset = 5
data[entry_start+4..entry_start+8].copy_from_slice(&10u32.to_le_bytes()); // code_len = 10
// 5 + 10 = 15 > 10 (code section length)
data[entry_start..entry_start + 4].copy_from_slice(&5u32.to_le_bytes()); // code_offset = 5
data[entry_start + 4..entry_start + 8].copy_from_slice(&10u32.to_le_bytes()); // code_len = 10
// 5 + 10 = 15 > 10 (code section length)
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::InvalidFunctionIndex));
}
@ -620,17 +682,17 @@ mod tests {
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&64u32.to_le_bytes());
data.extend_from_slice(&4u32.to_le_bytes());
// Section 2: Code, Kind 2, Offset 128, Length 6 (PushConst 0)
data.extend_from_slice(&2u32.to_le_bytes());
data.extend_from_slice(&128u32.to_le_bytes());
data.extend_from_slice(&6u32.to_le_bytes());
data.resize(256, 0);
// Setup empty CP
data[64..68].copy_from_slice(&0u32.to_le_bytes());
// Setup code with PushConst 0
data[128..130].copy_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
data[130..134].copy_from_slice(&0u32.to_le_bytes());

View File

@ -1,5 +1,5 @@
/// Represents a single instruction in the Prometeu Virtual Machine.
///
///
/// Each OpCode is encoded as a 16-bit unsigned integer (u16) in the bytecode.
/// The PVM is a stack-based machine, meaning most instructions take their
/// operands from the top of the stack and push their results back onto it.
@ -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,16 +1,16 @@
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.
///
/// Under the Prometeu architecture, the ProgramImage is a "closed-world" artifact
/// produced by the compiler. All linking, relocation, and symbol resolution
/// Under the Prometeu architecture, the ProgramImage is a "closed-world" artifact
/// produced by the compiler. All linking, relocation, and symbol resolution
/// MUST be performed by the compiler before this image is created.
///
/// The runtime (VM) assumes this image is authoritative and performs no
/// The runtime (VM) assumes this image is authoritative and performs no
/// additional linking or fixups.
#[derive(Debug, Clone, Default)]
pub struct ProgramImage {
@ -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,33 +39,27 @@ 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) {
message = format!("{} (in function {})", message, func_name);
}
}
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,49 +67,50 @@ 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 {
Value::Null => ConstantPoolEntry::Null,
Value::Int64(v) => ConstantPoolEntry::Int64(*v),
Value::Float(v) => ConstantPoolEntry::Float64(*v),
Value::Boolean(v) => ConstantPoolEntry::Boolean(*v),
Value::String(v) => ConstantPoolEntry::String(v.clone()),
Value::Int32(v) => ConstantPoolEntry::Int32(*v),
Value::Bounded(v) => ConstantPoolEntry::Int32(*v as i32),
Value::Gate(_) => ConstantPoolEntry::Null,
}).collect();
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),
Value::Boolean(v) => ConstantPoolEntry::Boolean(*v),
Value::String(v) => ConstantPoolEntry::String(v.clone()),
Value::Int32(v) => ConstantPoolEntry::Int32(*v),
Value::Bounded(v) => ConstantPoolEntry::Int32(*v as i32),
Value::Gate(_) => ConstantPoolEntry::Null,
})
.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

@ -2,10 +2,10 @@ use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
/// Represents any piece of data that can be stored on the VM stack or in globals.
///
/// The PVM is "dynamically typed" at the bytecode level, meaning a single
/// `Value` enum can hold different primitive types. The VM performs
/// automatic type promotion (e.g., adding an Int32 to a Float64 results
///
/// The PVM is "dynamically typed" at the bytecode level, meaning a single
/// `Value` enum can hold different primitive types. The VM performs
/// automatic type promotion (e.g., adding an Int32 to a Float64 results
/// in a Float64) to ensure mathematical correctness.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]

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,16 +440,19 @@ 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));
}
let assets_data = assets_data.read().unwrap();
let start = entry.offset as usize;
let end = start + entry.size as usize;
if end > assets_data.len() {
return Err("Asset offset/size out of bounds".to_string());
}
@ -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,42 +486,41 @@ 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));
}
let assets_data = assets_data.read().unwrap();
let start = entry.offset as usize;
let end = start + entry.size as usize;
if end > assets_data.len() {
return Err("Asset offset/size out of bounds".to_string());
}
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) {
if i + 1 < buffer.len() {
data.push(i16::from_le_bytes([buffer[i], buffer[i+1]]));
data.push(i16::from_le_bytes([buffer[i], buffer[i + 1]]));
}
}
@ -526,7 +605,7 @@ impl AssetManager {
let staging = self.gfx_policy.staging.read().unwrap();
let assets = self.assets.read().unwrap();
let handles = self.handles.read().unwrap();
for (handle_id, _) in staging.iter() {
if let Some(h) = handles.get(handle_id) {
if let Some(entry) = assets.get(&h._asset_id) {
@ -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;
}
}
}
@ -567,7 +648,7 @@ impl AssetManager {
let staging = self.sound_policy.staging.read().unwrap();
let assets = self.assets.read().unwrap();
let handles = self.handles.read().unwrap();
for (handle_id, _) in staging.iter() {
if let Some(h) = handles.get(handle_id) {
if let Some(entry) = assets.get(&h._asset_id) {
@ -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;
}
}
}
@ -602,9 +685,13 @@ impl AssetManager {
BankType::TILES => {
let slots = self.gfx_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.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 }
}
}
}
@ -663,7 +744,7 @@ impl AssetManager {
}
}
}
pub fn shutdown(&self) {
self.gfx_policy.clear();
self.sound_policy.clear();
@ -705,18 +786,18 @@ mod tests {
let am = AssetManager::new(vec![asset_entry], data, gfx_installer, sound_installer);
let slot = SlotRef::gfx(0);
let handle = am.load("test_tiles", slot).expect("Should start loading");
let mut status = am.status(handle);
let start = Instant::now();
while status != LoadStatus::READY && start.elapsed().as_secs() < 5 {
thread::sleep(std::time::Duration::from_millis(10));
status = am.status(handle);
}
assert_eq!(status, LoadStatus::READY);
{
let staging = am.gfx_policy.staging.read().unwrap();
assert!(staging.contains_key(&handle));
@ -724,7 +805,7 @@ mod tests {
am.commit(handle);
am.apply_commits();
assert_eq!(am.status(handle), LoadStatus::COMMITTED);
assert!(banks.tile_bank_slot(0).is_some());
}
@ -754,13 +835,13 @@ mod tests {
};
let am = AssetManager::new(vec![asset_entry], data, gfx_installer, sound_installer);
let handle1 = am.load("test_tiles", SlotRef::gfx(0)).unwrap();
let start = Instant::now();
while am.status(handle1) != LoadStatus::READY && start.elapsed().as_secs() < 5 {
thread::sleep(std::time::Duration::from_millis(10));
}
let handle2 = am.load("test_tiles", SlotRef::gfx(1)).unwrap();
assert_eq!(am.status(handle2), LoadStatus::READY);
@ -794,19 +875,19 @@ mod tests {
let am = AssetManager::new(vec![asset_entry], data, gfx_installer, sound_installer);
let slot = SlotRef::audio(0);
let handle = am.load("test_sound", slot).expect("Should start loading");
let start = Instant::now();
while am.status(handle) != LoadStatus::READY && start.elapsed().as_secs() < 5 {
thread::sleep(std::time::Duration::from_millis(10));
}
assert_eq!(am.status(handle), LoadStatus::READY);
am.commit(handle);
am.apply_commits();
assert_eq!(am.status(handle), LoadStatus::COMMITTED);
assert!(banks.sound_bank_slot(0).is_some());
}
@ -818,7 +899,7 @@ mod tests {
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
let data = vec![0u8; 200];
let asset_entry = AssetEntry {
asset_id: 2,
asset_name: "preload_sound".to_string(),
@ -832,17 +913,15 @@ 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);
// Before init, slot 5 is empty
assert!(banks.sound_bank_slot(5).is_none());
am.initialize_for_cartridge(vec![asset_entry], preload, data);
// After init, slot 5 should be occupied because of preload
assert!(banks.sound_bank_slot(5).is_some());
assert_eq!(am.slot_info(SlotRef::audio(5)).asset_id, Some(2));
@ -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,18 +1,18 @@
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).
///
///
/// The Core maintains this state to provide information to the App (e.g., is_playing),
/// but the actual real-time mixing is performed by the Host using commands.
pub struct Channel {
@ -50,7 +50,7 @@ impl Default for Channel {
}
/// Commands sent from the Core to the Host audio backend.
///
///
/// Because the Core logic runs at 60Hz and Audio is generated at 48kHz,
/// we use an asynchronous command queue to synchronize them.
pub enum AudioCommand {
@ -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.
@ -90,12 +79,12 @@ pub enum AudioCommand {
}
/// PROMETEU Audio Subsystem.
///
/// Models a multi-channel PCM sampler (SPU).
///
/// Models a multi-channel PCM sampler (SPU).
/// The audio system in Prometeu is **command-based**. This means the Core
/// doesn't generate raw audio samples; instead, it sends high-level commands
/// (like `Play`, `Stop`, `SetVolume`) to a queue. The physical host backend
/// (e.g., CPAL on desktop) then consumes these commands and performs the
/// doesn't generate raw audio samples; instead, it sends high-level commands
/// (like `Play`, `Stop`, `SetVolume`) to a queue. The physical host backend
/// (e.g., CPAL on desktop) then consumes these commands and performs the
/// actual mixing at the native hardware sample rate.
///
/// ### Key Features:
@ -103,7 +92,7 @@ pub enum AudioCommand {
/// - **Sample-based Synthesis**: Plays PCM data stored in SoundBanks.
/// - **Stereo Output**: 48kHz output target.
pub struct Audio {
/// Local state of the hardware voices. This state is used for logic
/// Local state of the hardware voices. This state is used for logic
/// (e.g., checking if a sound is still playing) and is synchronized with the Host.
pub voices: [Channel; MAX_CHANNELS],
/// Queue of pending commands to be processed by the Host mixer.
@ -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,15 +1,15 @@
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.
///
///
/// ### Usage Example:
/// // Draw a semi-transparent blue rectangle
/// gfx.fill_rect_blend(10, 10, 50, 50, Color::BLUE, BlendMode::Half);
@ -31,10 +31,10 @@ pub enum BlendMode {
/// PROMETEU Graphics Subsystem (GFX).
///
/// Models a specialized graphics chip with a fixed resolution, double buffering,
/// and a multi-layered tile/sprite architecture.
///
/// The GFX system works by composing several "layers" into a single 16-bit
/// RGB565 framebuffer. It supports hardware-accelerated primitives (lines, rects)
/// and a multi-layered tile/sprite architecture.
///
/// The GFX system works by composing several "layers" into a single 16-bit
/// RGB565 framebuffer. It supports hardware-accelerated primitives (lines, rects)
/// and specialized console features like background scrolling and sprite sorting.
///
/// ### Layer Composition Order (back to front):
@ -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;
}
@ -365,7 +500,7 @@ impl Gfx {
/// The main rendering pipeline.
///
/// This method composes the final frame by rasterizing layers and sprites in the
/// This method composes the final frame by rasterizing layers and sprites in the
/// correct priority order into the back buffer.
/// Follows the hardware model where layers and sprites are composed every frame.
pub fn render_all(&mut self) {
@ -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;
@ -674,7 +877,7 @@ mod tests {
let mut gfx = Gfx::new(10, 10, banks);
gfx.draw_pixel(5, 5, Color::WHITE);
assert_eq!(gfx.back[5 * 10 + 5], Color::WHITE.0);
// Out of bounds should not panic
gfx.draw_pixel(-1, -1, Color::WHITE);
gfx.draw_pixel(10, 10, Color::WHITE);

View File

@ -1,17 +1,20 @@
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.
///
///
/// This struct represents the "Mainboard" of the PROMETEU console.
/// It acts as a container for all hardware subsystems. In the Prometeu
/// architecture, hardware is decoupled from the OS and VM, allowing
/// It acts as a container for all hardware subsystems. In the Prometeu
/// architecture, hardware is decoupled from the OS and VM, allowing
/// for easier testing and different host implementations (Desktop, Web, etc.).
///
/// ### Console Specifications:
@ -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,13 +1,13 @@
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};
pub use crate::gfx::Gfx;
pub use crate::memory_banks::MemoryBanks;
pub use crate::pad::Pad;
pub use crate::pad::Pad;

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 {
@ -31,7 +31,7 @@ pub trait SoundBankPoolInstaller: Send + Sync {
}
/// Centralized container for all hardware memory banks.
///
///
/// MemoryBanks represent the actual hardware slot state.
/// Peripherals consume this state via narrow, non-generic traits.
/// AssetManager coordinates residency and installs assets into these slots.

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 {
@ -10,31 +10,59 @@ pub struct Pad {
pub a: Button, // ps: square
pub b: Button, // ps: circle
pub x: Button, // ps: triangle
pub x: Button, // ps: triangle
pub y: Button, // ps: cross
pub l: Button, // ps: R
pub r: Button, // ps: L
pub start: Button,
pub select: Button,
pub select: Button,
}
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 {
@ -43,18 +71,18 @@ impl Pad {
self.down.begin_frame(signals.down_signal);
self.left.begin_frame(signals.left_signal);
self.right.begin_frame(signals.right_signal);
self.a.begin_frame(signals.a_signal);
self.b.begin_frame(signals.b_signal);
self.x.begin_frame(signals.x_signal);
self.y.begin_frame(signals.y_signal);
self.l.begin_frame(signals.l_signal);
self.r.begin_frame(signals.r_signal);
self.start.begin_frame(signals.start_signal);
self.select.begin_frame(signals.select_signal);
self.select.begin_frame(signals.select_signal);
}
pub fn any(&self) -> bool {
self.a.down
|| self.b.down
@ -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

@ -4,7 +4,7 @@
// use prometeu_drivers::hardware::Hardware;
// use prometeu_hal::{HostContext, HostReturn, NativeInterface};
// use prometeu_vm::{LogicalFrameEndingReason, VirtualMachine};
//
//
// struct MockNative;
// impl NativeInterface for MockNative {
// fn syscall(&mut self, id: u32, _args: &[Value], ret: &mut HostReturn, _ctx: &mut HostContext) -> Result<(), VmFault> {
@ -26,30 +26,30 @@
// Ok(())
// }
// }
//
//
// #[test]
// fn test_canonical_cartridge_heartbeat() {
// let mut pbc_path = Path::new("../../test-cartridges/canonical/golden/program.pbc").to_path_buf();
// if !pbc_path.exists() {
// pbc_path = Path::new("test-cartridges/canonical/golden/program.pbc").to_path_buf();
// }
//
//
// let pbc_bytes = fs::read(pbc_path).expect("Failed to read canonical PBC. Did you run the generation test?");
//
//
// // Determine entrypoint from the compiled module exports
// let entry_symbol = "src/main/modules:frame";
//
//
// let mut vm = VirtualMachine::new(vec![], vec![]);
// vm.initialize(pbc_bytes, entry_symbol).expect("Failed to initialize VM with canonical cartridge");
// vm.prepare_call(entry_symbol);
//
//
// let mut native = MockNative;
// let mut hw = Hardware::new();
// let mut ctx = HostContext::new(Some(&mut hw));
//
//
// // Run for a reasonable budget
// let report = vm.run_budget(1000, &mut native, &mut ctx).expect("VM failed to run");
//
//
// // Acceptance criteria:
// // 1. No traps
// match report.reason {
@ -61,7 +61,7 @@
// LogicalFrameEndingReason::BudgetExhausted => {},
// LogicalFrameEndingReason::Breakpoint => {},
// }
//
//
// // 2. Deterministic output state (if any)
// // Observação: a PVM agora sinaliza FRAME_SYNC imediatamente antes do RET do entry point.
// // Quando o motivo é FRAME_SYNC, não exigimos pilha vazia (a limpeza final ocorre após o RET).
@ -69,6 +69,6 @@
// if !matches!(report.reason, LogicalFrameEndingReason::FrameSync) {
// assert_eq!(vm.operand_stack.len(), 0, "Stack should be empty after frame() execution");
// }
//
//
// println!("Heartbeat test passed!");
// }

View File

@ -1,23 +1,23 @@
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.
///
/// The central orchestrator of the console. The firmware acts as the
///
/// The central orchestrator of the console. The firmware acts as the
/// "Control Unit", managing the high-level state machine of the system.
///
/// It is responsible for transitioning between different modes of operation,
/// such as showing the splash screen, running the Hub (launcher), or
/// It is responsible for transitioning between different modes of operation,
/// such as showing the splash screen, running the Hub (launcher), or
/// executing a game/app.
///
/// ### Execution Loop:
/// The firmware is designed to be ticked once per frame (60Hz). During each
/// The firmware is designed to be ticked once per frame (60Hz). During each
/// tick, it:
/// 1. Updates peripherals with the latest input signals.
/// 2. Delegates the logic update to the current active state.
@ -74,13 +74,18 @@ impl Firmware {
self.change_state(next_state, signals, hw);
}
}
/// 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;
// Enter the new state immediately to avoid "empty" frames during transitions.
self.on_enter(signals, hw);
self.state_initialized = true;
@ -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,
@ -149,9 +158,9 @@ impl Firmware {
FirmwareState::AppCrashes(s) => s.on_exit(&mut req),
}
}
pub fn load_cartridge(&mut self, cartridge: Cartridge) {
self.state = FirmwareState::LoadCartridge(LoadCartridgeStep { cartridge });
self.state_initialized = false;
}
}
}

View File

@ -15,4 +15,4 @@ pub enum FirmwareState {
LoadCartridge(LoadCartridgeStep),
GameRunning(GameRunningStep),
AppCrashes(AppCrashesStep),
}
}

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 {
@ -27,9 +27,9 @@ impl AppCrashesStep {
if ctx.hw.pad().start().down {
return Some(FirmwareState::LaunchHub(LaunchHubStep));
}
None
}
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
}
}

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;
@ -12,7 +12,7 @@ impl GameRunningStep {
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
let result = ctx.os.tick(ctx.vm, ctx.signals, ctx.hw);
if !ctx.os.logical_frame_active {
ctx.hw.gfx_mut().present();
}

View File

@ -27,15 +27,15 @@ impl HubHomeStep {
// Renders the System App window borders
ctx.hub.render(ctx.os, ctx.hw);
ctx.hw.gfx_mut().present();
if let Some(err) = error {
return Some(FirmwareState::AppCrashes(AppCrashesStep { error: err }));
}
None
}
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
}
}

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),
);
}
}
}
@ -30,4 +35,4 @@ impl LaunchHubStep {
}
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
}
}

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,10 +34,10 @@ 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);
// System apps do not change the firmware state to GameRunning.
// They run in the background or via windows in the Hub.
return Some(FirmwareState::HubHome(HubHomeStep));
@ -42,4 +47,4 @@ impl LoadCartridgeStep {
}
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
}
}

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;
@ -20,4 +20,4 @@ impl ResetStep {
}
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
}
}

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 {
@ -17,7 +17,7 @@ impl SplashScreenStep {
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
const ANIMATION_DURATION: u32 = 60; // 1 second at 60fps
const TOTAL_DURATION: u32 = 240; // 4 seconds total (updated from 2s based on total_duration logic)
const TOTAL_DURATION: u32 = 240; // 4 seconds total (updated from 2s based on total_duration logic)
// Update peripherals for input
ctx.hw.pad_mut().begin_frame(ctx.signals);
@ -28,7 +28,7 @@ impl SplashScreenStep {
// Draw expanding square
let (sw, sh) = ctx.hw.gfx().size();
let max_size = (sw.min(sh) as i32 / 2).max(1);
let current_size = if self.frame < ANIMATION_DURATION {
(max_size * (self.frame as i32 + 1)) / ANIMATION_DURATION as i32
} else {
@ -37,7 +37,7 @@ impl SplashScreenStep {
let x = (sw as i32 - current_size) / 2;
let y = (sh as i32 - current_size) / 2;
ctx.hw.gfx_mut().fill_rect(x, y, current_size, current_size, Color::WHITE);
ctx.hw.gfx_mut().present();
@ -57,4 +57,4 @@ impl SplashScreenStep {
}
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
}
}

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,
@ -10,4 +10,4 @@ pub struct PrometeuContext<'a> {
pub boot_target: &'a BootTarget,
pub signals: &'a InputSignals,
pub hw: &'a mut dyn HardwareBridge,
}
}

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

@ -1,12 +1,12 @@
/// Represents a 16-bit color in the RGB565 format.
///
///
/// The RGB565 format is a common high-color representation for embedded systems:
/// - **Red**: 5 bits (0..31)
/// - **Green**: 6 bits (0..63)
/// - **Blue**: 5 bits (0..31)
///
/// Prometeu does not have a hardware alpha channel. Transparency is achieved
/// by using a specific color key (Magenta / 0xF81F) which the GFX engine
/// Prometeu does not have a hardware alpha channel. Transparency is achieved
/// by using a specific color key (Magenta / 0xF81F) which the GFX engine
/// skips during rendering.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Color(pub u16);
@ -55,7 +55,7 @@ impl Color {
pub const fn gray_scale(c: u8) -> Self {
Self::rgb(c, c, c)
}
pub const fn from_raw(raw: u16) -> Self {
Self(raw)
}
@ -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();
}
@ -55,7 +59,7 @@ mod tests {
service.log(100, 1, LogLevel::Info, LogSource::Pos, 0, "Log 1".to_string());
service.log(110, 1, LogLevel::Info, LogSource::Pos, 0, "Log 2".to_string());
service.log(120, 1, LogLevel::Info, LogSource::Pos, 0, "Log 3".to_string());
assert_eq!(service.events.len(), 3);
assert_eq!(service.events[0].msg, "Log 1");

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,9 +1,9 @@
use std::sync::Arc;
use crate::sample::Sample;
use std::sync::Arc;
/// A container for audio assets.
///
/// A SoundBank stores multiple audio samples that can be played by the
///
/// A SoundBank stores multiple audio samples that can be played by the
/// audio subsystem.
pub struct SoundBank {
pub samples: Vec<Arc<Sample>>,

View File

@ -1,8 +1,8 @@
/// Enumeration of all System Calls (Syscalls) available in the Prometeu environment.
///
/// Syscalls are the primary mechanism for a program running in the Virtual Machine
/// to interact with the outside world (Hardware, OS, Filesystem).
///
/// Syscalls are the primary mechanism for a program running in the Virtual Machine
/// to interact with the outside world (Hardware, OS, Filesystem).
///
/// Each Syscall has a unique 32-bit ID. The IDs are grouped by category:
/// - **0x0xxx**: System & OS Control
/// - **0x1xxx**: Graphics (GFX)
@ -301,8 +301,8 @@ impl Syscall {
// --- FS ---
Self::FsOpen => 1,
Self::FsRead => 1, // bytes read
Self::FsWrite => 1, // bytes written
Self::FsRead => 1, // bytes read
Self::FsWrite => 1, // bytes written
Self::FsClose => 0,
Self::FsListDir => 1, // entries count/handle (TBD)
Self::FsExists => 1,

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;
}
@ -106,15 +120,15 @@ mod tests {
};
let cert = Certifier::new(config);
let mut ls = LogService::new(10);
let mut tel = TelemetryFrame::default();
tel.cycles_used = 150;
tel.syscalls = 10;
tel.host_cpu_time_us = 500;
let violations = cert.evaluate(&tel, &mut ls, 1000);
assert_eq!(violations, 2);
let logs = ls.get_recent(10);
assert_eq!(logs.len(), 2);
assert!(logs[0].msg.contains("cycles_used"));

View File

@ -12,9 +12,9 @@ pub enum TileSize {
}
/// A container for graphical assets.
///
/// A TileBank stores both the raw pixel data (as palette indices) and the
/// color palettes themselves. This encapsulates all the information needed
///
/// A TileBank stores both the raw pixel data (as palette indices) and the
/// color palettes themselves. This encapsulates all the information needed
/// to render a set of tiles.
pub struct TileBank {
/// Dimension of each individual tile in the bank.
@ -64,7 +64,7 @@ impl TileBank {
/// Maps a 4-bit index to a real RGB565 Color using the specified palette.
pub fn resolve_color(&self, palette_id: u8, pixel_index: u8) -> Color {
// Hardware Rule: Index 0 is always transparent.
// Hardware Rule: Index 0 is always transparent.
// We use Magenta as the 'transparent' signal color during composition.
if pixel_index == 0 {
return Color::COLOR_KEY;

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

@ -3,4 +3,4 @@ pub enum VmFault {
Trap(u32, String),
Panic(String),
Unavailable,
}
}

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,3 +1,3 @@
mod prometeu_hub;
pub use prometeu_hub::PrometeuHub;
pub use prometeu_hub::PrometeuHub;

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 {
@ -58,4 +69,4 @@ impl PrometeuHub {
);
}
}
}
}

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));
@ -83,4 +80,4 @@ mod tests {
assert_eq!(wm.windows.len(), 0);
assert_eq!(wm.focused, None);
}
}
}

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

@ -1,12 +1,12 @@
use crate::fs::{FsBackend, FsEntry, FsError};
/// Virtual Filesystem (VFS) interface for Prometeu.
///
///
/// The VFS provides a sandboxed, unified path interface for user applications.
/// Instead of interacting directly with the host's disk, the VM uses
/// normalized paths (e.g., `/user/save.dat`).
///
/// The actual storage is provided by an `FsBackend`, which can be a real
/// Instead of interacting directly with the host's disk, the VM uses
/// normalized paths (e.g., `/user/save.dat`).
///
/// The actual storage is provided by an `FsBackend`, which can be a real
/// directory on disk, an in-memory map, or even a network resource.
pub struct VirtualFS {
/// The active storage implementation.
@ -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)
@ -128,18 +127,18 @@ mod tests {
fn test_virtual_fs_operations() {
let mut vfs = VirtualFS::new();
let backend = MockBackend::new();
vfs.mount(Box::new(backend)).unwrap();
let test_file = "/user/test.txt";
let content = b"hello world";
vfs.write_file(test_file, content).unwrap();
assert!(vfs.exists(test_file));
let read_content = vfs.read_file(test_file).unwrap();
assert_eq!(read_content, content);
vfs.delete(test_file).unwrap();
assert!(!vfs.exists(test_file));
}
@ -149,7 +148,7 @@ mod tests {
let mut vfs = VirtualFS::new();
let mut backend = MockBackend::new();
backend.healthy = false;
vfs.mount(Box::new(backend)).unwrap();
assert!(!vfs.is_healthy());
}

View File

@ -1 +1 @@
pub mod fs;
pub mod fs;

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())),
};
@ -1018,4 +1124,4 @@ impl NativeInterface for VirtualMachineRuntime {
}
}
}
}
}

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

@ -2,4 +2,4 @@ pub struct CallFrame {
pub return_pc: u32,
pub stack_base: usize,
pub func_idx: usize,
}
}

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,3 +1,3 @@
pub struct ScopeFrame {
pub scope_stack_base: usize,
}
}

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 }));
}
@ -261,12 +289,8 @@ mod tests {
code.push(OpCode::Jmp as u8);
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]
@ -326,26 +350,41 @@ mod tests {
// 15: PushI32 1
// 21: Jmp 27
// 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)
let functions = vec![FunctionMeta {
code_offset: 0,
code_len: 29,
..Default::default()
}];
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 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]
@ -356,7 +395,7 @@ mod tests {
code.extend_from_slice(&1u32.to_le_bytes());
code.push(OpCode::Ret as u8);
code.push(0x00);
let functions = vec![FunctionMeta {
code_offset: 0,
code_len: 8,
@ -374,11 +413,17 @@ 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,
code_len: 16,
@ -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 }));
}

File diff suppressed because it is too large Load Diff

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,15 +1,15 @@
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.
///
///
/// This component acts as a TCP server that allows external tools (like the
/// Prometeu Debugger) to observe and control the execution of the virtual machine.
///
///
/// Communication is based on JSONL (JSON lines) over TCP.
pub struct HostDebugger {
/// If true, the VM will not start execution until a 'start' command is received.
@ -42,12 +42,12 @@ impl HostDebugger {
if let BootTarget::Cartridge { path, debug: true, debug_port } = boot_target {
self.waiting_for_start = true;
// Pre-load cartridge metadata so the Handshake message can contain
// Pre-load cartridge metadata so the Handshake message can contain
// valid information about the App being debugged.
if let Ok(cartridge) = CartridgeLoader::load(path) {
firmware.os.initialize_vm(&mut firmware.vm, &cartridge);
}
match TcpListener::bind(format!("127.0.0.1:{}", debug_port)) {
Ok(listener) => {
// Set listener to non-blocking so it doesn't halt the main loop.
@ -59,7 +59,7 @@ impl HostDebugger {
eprintln!("[Debugger] Failed to bind to port {}: {}", debug_port, e);
}
}
println!("[Debugger] (Or press D to start execution)");
}
}
@ -94,9 +94,9 @@ impl HostDebugger {
if self.stream.is_none() {
println!("[Debugger] Connection received!");
stream.set_nonblocking(true).expect("Cannot set non-blocking on stream");
self.stream = Some(stream);
// Immediately send the Handshake message to identify the Runtime and App.
let handshake = DebugResponse::Handshake {
protocol_version: DEVTOOLS_PROTOCOL_VERSION,
@ -130,13 +130,15 @@ impl HostDebugger {
Ok(n) => {
let data = &buf[..n];
let msg = String::from_utf8_lossy(data);
self.stream = Some(stream);
// 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);
}
@ -154,7 +156,7 @@ impl HostDebugger {
}
}
}
// 3. Push events (logs, telemetry) to the client.
if self.stream.is_some() {
self.stream_events(firmware);
@ -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,9 +196,8 @@ 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,
stack_top,
@ -219,13 +225,13 @@ impl HostDebugger {
let new_events = firmware.os.log_service.get_after(self.last_log_seq);
for event in new_events {
self.last_log_seq = event.seq;
// Map specific internal log tags to protocol events.
if event.tag == 0xDEB1 {
self.send_event(DebugEvent::BreakpointHit {
pc: firmware.vm.pc,
frame_index: firmware.os.logical_frame_index,
});
self.send_event(DebugEvent::BreakpointHit {
pc: firmware.vm.pc,
frame_index: firmware.os.logical_frame_index,
});
}
// Map Certification tags (0xCA01-0xCA03) to 'Cert' protocol events.
@ -266,27 +272,27 @@ impl HostDebugger {
msg: event.msg.clone(),
});
}
// 2. Send telemetry snapshots at the completion of every frame.
let current_frame = firmware.os.logical_frame_index;
if current_frame > self.last_telemetry_frame {
let tel = &firmware.os.telemetry_last;
self.send_event(DebugEvent::Telemetry {
frame_index: tel.frame_index,
vm_steps: tel.vm_steps,
syscalls: tel.syscalls,
cycles: tel.cycles_used,
cycles_budget: tel.cycles_budget,
host_cpu_time_us: tel.host_cpu_time_us,
violations: tel.violations,
gfx_used_bytes: tel.gfx_used_bytes,
gfx_inflight_bytes: tel.gfx_inflight_bytes,
gfx_slots_occupied: tel.gfx_slots_occupied,
audio_used_bytes: tel.audio_used_bytes,
audio_inflight_bytes: tel.audio_inflight_bytes,
audio_slots_occupied: tel.audio_slots_occupied,
});
self.last_telemetry_frame = current_frame;
let tel = &firmware.os.telemetry_last;
self.send_event(DebugEvent::Telemetry {
frame_index: tel.frame_index,
vm_steps: tel.vm_steps,
syscalls: tel.syscalls,
cycles: tel.cycles_used,
cycles_budget: tel.cycles_budget,
host_cpu_time_us: tel.host_cpu_time_us,
violations: tel.violations,
gfx_used_bytes: tel.gfx_used_bytes,
gfx_inflight_bytes: tel.gfx_inflight_bytes,
gfx_slots_occupied: tel.gfx_slots_occupied,
audio_used_bytes: tel.audio_used_bytes,
audio_inflight_bytes: tel.audio_inflight_bytes,
audio_slots_occupied: tel.audio_slots_occupied,
});
self.last_telemetry_frame = current_frame;
}
}
}

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
}
@ -99,14 +103,14 @@ mod tests {
fn test_host_dir_backend_mount_and_dirs() {
let root = get_temp_dir("mount");
let mut backend = HostDirBackend::new(root.clone());
backend.mount().unwrap();
assert!(root.join("system").is_dir());
assert!(root.join("apps").is_dir());
assert!(root.join("media").is_dir());
assert!(root.join("user").is_dir());
let _ = fs::remove_dir_all(root);
}
}

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

@ -35,7 +35,7 @@ impl HostConsoleSink {
"[{:06}ms][{}][{}][{}] {}",
event.ts_ms, event.frame, level_str, source_str, event.msg
);
self.last_seq = Some(event.seq);
}
}

View File

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

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,14 +19,10 @@ 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.
///
/// This struct acts as the physical "chassis" of the virtual console. It is
///
/// This struct acts as the physical "chassis" of the virtual console. It is
/// responsible for:
/// - Creating and managing the OS window (via `winit`).
/// - Initializing the GPU-accelerated framebuffer (via `pixels`).
@ -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,11 +228,12 @@ 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)
.present_mode(PresentMode::Fifo) // activate vsync
.build()
.expect("failed to create Pixels");
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");
pixels.frame_mut().fill(0);
self.pixels = Some(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;
@ -270,13 +314,13 @@ impl ApplicationHandler for HostRunner {
}
// 3. Timing Management (The heart of determinism).
// We measure the elapsed time since the last iteration and add it to an
// accumulator. We then execute exactly as many 60Hz slices as the
// We measure the elapsed time since the last iteration and add it to an
// accumulator. We then execute exactly as many 60Hz slices as the
// accumulator allows.
let now = Instant::now();
let mut frame_delta = now.duration_since(self.last_frame_time);
// Safety cap: if the OS freezes or we fall behind too much, we don't try
// Safety cap: if the OS freezes or we fall behind too much, we don't try
// to catch up indefinitely (avoiding the "death spiral").
if frame_delta > Duration::from_millis(100) {
frame_delta = Duration::from_millis(100);
@ -293,16 +337,13 @@ impl ApplicationHandler for HostRunner {
}
// Sync pause state with audio.
// We do this AFTER firmware.tick to avoid MasterPause/Resume commands
// We do this AFTER firmware.tick to avoid MasterPause/Resume commands
// being cleared by the OS if a new logical frame starts in this tick.
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);
}
@ -331,7 +372,7 @@ impl ApplicationHandler for HostRunner {
// 5. Rendering the Telemetry Overlay (if enabled).
if self.overlay_enabled {
// We temporarily swap buffers to draw over the current image.
self.hardware.gfx.present();
self.hardware.gfx.present();
self.display_dbg_overlay();
self.hardware.gfx.present();
}
@ -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,10 +405,11 @@ 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));
// Simulates the loop to accept the connection
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
assert!(runner.debugger.stream.is_some(), "Stream should have been kept open");
@ -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());
@ -427,10 +482,13 @@ mod tests {
// 3. Try to reconnect - SHOULD FAIL currently, but we want it to WORK
let stream2 = TcpStream::connect(format!("127.0.0.1:{}", port));
assert!(stream2.is_ok(), "Should accept new connection even after start");
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,22 +502,27 @@ 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
// Check if stream2 was closed by the server
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");
}
@ -479,7 +542,7 @@ mod tests {
use std::io::BufRead;
let mut reader = std::io::BufReader::new(stream);
let mut line = String::new();
reader.read_line(&mut line).expect("Should read handshake");
assert!(line.contains("handshake"));
@ -487,15 +550,17 @@ mod tests {
// Send getState
reader.get_mut().write_all(b"{\"type\":\"getState\"}\n").expect("Should write getState");
std::thread::sleep(std::time::Duration::from_millis(100));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
// Check if response received (may have events/logs before)
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" {
return;
@ -517,10 +582,11 @@ 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);
stream.write_all(b"{\"type\":\"pause\"}\n").expect("Should write pause");
std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
@ -530,7 +596,7 @@ mod tests {
// 2. Disconnect (stream goes out of scope)
std::thread::sleep(std::time::Duration::from_millis(50));
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
// 3. Check if unpaused
assert!(!runner.firmware.os.paused, "VM should have unpaused after disconnect");
assert!(!runner.debugger.waiting_for_start, "VM should have left waiting_for_start state");

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,15 +31,22 @@ 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();
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

@ -4,10 +4,10 @@ use std::path::{Path, PathBuf};
use std::process::Command;
/// PROMETEU Dispatcher (CLI).
///
/// The main entry point for the user. This binary does not implement
/// compilation or execution logic itself; instead, it acts as a smart
/// front-end that locates and dispatches commands to specialized
///
/// The main entry point for the user. This binary does not implement
/// compilation or execution logic itself; instead, it acts as a smart
/// front-end that locates and dispatches commands to specialized
/// components like `prometeu-host-desktop-winit` or `prometeu-build-pipeline`.
#[derive(Parser)]
#[command(name = "prometeu")]
@ -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"