quality baseline
This commit is contained in:
parent
e2a970e69c
commit
c7786fa8b0
29
.github/workflows/ci.yml
vendored
Normal file
29
.github/workflows/ci.yml
vendored
Normal 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
68
Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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
15
Makefile
Normal 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
|
||||
@ -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] })
|
||||
}
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -81,15 +81,26 @@ impl BytecodeModule {
|
||||
let cp_data = self.serialize_const_pool();
|
||||
let func_data = self.serialize_functions();
|
||||
let code_data = self.code.clone();
|
||||
let debug_data = self.debug_info.as_ref().map(|di| self.serialize_debug(di)).unwrap_or_default();
|
||||
let debug_data =
|
||||
self.debug_info.as_ref().map(|di| self.serialize_debug(di)).unwrap_or_default();
|
||||
let export_data = self.serialize_exports();
|
||||
|
||||
let mut final_sections = Vec::new();
|
||||
if !cp_data.is_empty() { final_sections.push((0, cp_data)); }
|
||||
if !func_data.is_empty() { final_sections.push((1, func_data)); }
|
||||
if !code_data.is_empty() { final_sections.push((2, code_data)); }
|
||||
if !debug_data.is_empty() { final_sections.push((3, debug_data)); }
|
||||
if !export_data.is_empty() { final_sections.push((4, export_data)); }
|
||||
if !cp_data.is_empty() {
|
||||
final_sections.push((0, cp_data));
|
||||
}
|
||||
if !func_data.is_empty() {
|
||||
final_sections.push((1, func_data));
|
||||
}
|
||||
if !code_data.is_empty() {
|
||||
final_sections.push((2, code_data));
|
||||
}
|
||||
if !debug_data.is_empty() {
|
||||
final_sections.push((3, debug_data));
|
||||
}
|
||||
if !export_data.is_empty() {
|
||||
final_sections.push((4, export_data));
|
||||
}
|
||||
|
||||
let mut out = Vec::new();
|
||||
// Magic "PBS\0"
|
||||
@ -123,7 +134,9 @@ impl BytecodeModule {
|
||||
}
|
||||
|
||||
fn serialize_const_pool(&self) -> Vec<u8> {
|
||||
if self.const_pool.is_empty() { return Vec::new(); }
|
||||
if self.const_pool.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&(self.const_pool.len() as u32).to_le_bytes());
|
||||
for entry in &self.const_pool {
|
||||
@ -157,7 +170,9 @@ impl BytecodeModule {
|
||||
}
|
||||
|
||||
fn serialize_functions(&self) -> Vec<u8> {
|
||||
if self.functions.is_empty() { return Vec::new(); }
|
||||
if self.functions.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&(self.functions.len() as u32).to_le_bytes());
|
||||
for f in &self.functions {
|
||||
@ -191,7 +206,9 @@ impl BytecodeModule {
|
||||
}
|
||||
|
||||
fn serialize_exports(&self) -> Vec<u8> {
|
||||
if self.exports.is_empty() { return Vec::new(); }
|
||||
if self.exports.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&(self.exports.len() as u32).to_le_bytes());
|
||||
for exp in &self.exports {
|
||||
@ -202,7 +219,6 @@ impl BytecodeModule {
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub struct BytecodeLoader;
|
||||
@ -224,7 +240,8 @@ impl BytecodeLoader {
|
||||
}
|
||||
|
||||
let endianness = bytes[6];
|
||||
if endianness != 0 { // 0 = Little Endian
|
||||
if endianness != 0 {
|
||||
// 0 = Little Endian
|
||||
return Err(LoadError::InvalidEndianness);
|
||||
}
|
||||
|
||||
@ -236,9 +253,20 @@ impl BytecodeLoader {
|
||||
if pos + 12 > bytes.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let kind = u32::from_le_bytes([bytes[pos], bytes[pos+1], bytes[pos+2], bytes[pos+3]]);
|
||||
let offset = u32::from_le_bytes([bytes[pos+4], bytes[pos+5], bytes[pos+6], bytes[pos+7]]);
|
||||
let length = u32::from_le_bytes([bytes[pos+8], bytes[pos+9], bytes[pos+10], bytes[pos+11]]);
|
||||
let kind =
|
||||
u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]]);
|
||||
let offset = u32::from_le_bytes([
|
||||
bytes[pos + 4],
|
||||
bytes[pos + 5],
|
||||
bytes[pos + 6],
|
||||
bytes[pos + 7],
|
||||
]);
|
||||
let length = u32::from_le_bytes([
|
||||
bytes[pos + 8],
|
||||
bytes[pos + 9],
|
||||
bytes[pos + 10],
|
||||
bytes[pos + 11],
|
||||
]);
|
||||
|
||||
// Basic bounds check
|
||||
if (offset as usize) + (length as usize) > bytes.len() {
|
||||
@ -273,19 +301,24 @@ impl BytecodeLoader {
|
||||
for (kind, offset, length) in sections {
|
||||
let section_data = &bytes[offset as usize..(offset + length) as usize];
|
||||
match kind {
|
||||
0 => { // Const Pool
|
||||
0 => {
|
||||
// Const Pool
|
||||
module.const_pool = parse_const_pool(section_data)?;
|
||||
}
|
||||
1 => { // Functions
|
||||
1 => {
|
||||
// Functions
|
||||
module.functions = parse_functions(section_data)?;
|
||||
}
|
||||
2 => { // Code
|
||||
2 => {
|
||||
// Code
|
||||
module.code = section_data.to_vec();
|
||||
}
|
||||
3 => { // Debug Info
|
||||
3 => {
|
||||
// Debug Info
|
||||
module.debug_info = Some(parse_debug_section(section_data)?);
|
||||
}
|
||||
4 => { // Exports
|
||||
4 => {
|
||||
// Exports
|
||||
module.exports = parse_exports(section_data)?;
|
||||
}
|
||||
_ => {} // Skip unknown or optional sections
|
||||
@ -318,34 +351,51 @@ fn parse_const_pool(data: &[u8]) -> Result<Vec<ConstantPoolEntry>, LoadError> {
|
||||
pos += 1;
|
||||
match tag {
|
||||
0 => cp.push(ConstantPoolEntry::Null),
|
||||
1 => { // Int64
|
||||
if pos + 8 > data.len() { return Err(LoadError::UnexpectedEof); }
|
||||
1 => {
|
||||
// Int64
|
||||
if pos + 8 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let val = i64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
|
||||
cp.push(ConstantPoolEntry::Int64(val));
|
||||
pos += 8;
|
||||
}
|
||||
2 => { // Float64
|
||||
if pos + 8 > data.len() { return Err(LoadError::UnexpectedEof); }
|
||||
2 => {
|
||||
// Float64
|
||||
if pos + 8 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let val = f64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
|
||||
cp.push(ConstantPoolEntry::Float64(val));
|
||||
pos += 8;
|
||||
}
|
||||
3 => { // Boolean
|
||||
if pos >= data.len() { return Err(LoadError::UnexpectedEof); }
|
||||
3 => {
|
||||
// Boolean
|
||||
if pos >= data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
cp.push(ConstantPoolEntry::Boolean(data[pos] != 0));
|
||||
pos += 1;
|
||||
}
|
||||
4 => { // String
|
||||
if pos + 4 > data.len() { return Err(LoadError::UnexpectedEof); }
|
||||
4 => {
|
||||
// String
|
||||
if pos + 4 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let len = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
|
||||
pos += 4;
|
||||
if pos + len > data.len() { return Err(LoadError::UnexpectedEof); }
|
||||
if pos + len > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let s = String::from_utf8_lossy(&data[pos..pos + len]).into_owned();
|
||||
cp.push(ConstantPoolEntry::String(s));
|
||||
pos += len;
|
||||
}
|
||||
5 => { // Int32
|
||||
if pos + 4 > data.len() { return Err(LoadError::UnexpectedEof); }
|
||||
5 => {
|
||||
// Int32
|
||||
if pos + 4 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let val = i32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
|
||||
cp.push(ConstantPoolEntry::Int32(val));
|
||||
pos += 4;
|
||||
@ -470,7 +520,6 @@ fn parse_exports(data: &[u8]) -> Result<Vec<Export>, LoadError> {
|
||||
Ok(exports)
|
||||
}
|
||||
|
||||
|
||||
fn validate_module(module: &BytecodeModule) -> Result<(), LoadError> {
|
||||
for func in &module.functions {
|
||||
// Opcode stream bounds
|
||||
@ -491,16 +540,29 @@ fn validate_module(module: &BytecodeModule) -> Result<(), LoadError> {
|
||||
|
||||
match opcode {
|
||||
OpCode::PushConst => {
|
||||
if pos + 4 > module.code.len() { return Err(LoadError::UnexpectedEof); }
|
||||
let idx = u32::from_le_bytes(module.code[pos..pos+4].try_into().unwrap()) as usize;
|
||||
if pos + 4 > module.code.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let idx =
|
||||
u32::from_le_bytes(module.code[pos..pos + 4].try_into().unwrap()) as usize;
|
||||
if idx >= module.const_pool.len() {
|
||||
return Err(LoadError::InvalidConstIndex);
|
||||
}
|
||||
pos += 4;
|
||||
}
|
||||
OpCode::PushI32 | OpCode::PushBounded | OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue
|
||||
| OpCode::GetGlobal | OpCode::SetGlobal | OpCode::GetLocal | OpCode::SetLocal
|
||||
| OpCode::PopN | OpCode::Syscall | OpCode::GateLoad | OpCode::GateStore => {
|
||||
OpCode::PushI32
|
||||
| OpCode::PushBounded
|
||||
| OpCode::Jmp
|
||||
| OpCode::JmpIfFalse
|
||||
| OpCode::JmpIfTrue
|
||||
| OpCode::GetGlobal
|
||||
| OpCode::SetGlobal
|
||||
| OpCode::GetLocal
|
||||
| OpCode::SetLocal
|
||||
| OpCode::PopN
|
||||
| OpCode::Syscall
|
||||
| OpCode::GateLoad
|
||||
| OpCode::GateStore => {
|
||||
pos += 4;
|
||||
}
|
||||
OpCode::PushI64 | OpCode::PushF64 => {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
use crate::abi::TrapInfo;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use crate::model::{BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta};
|
||||
use crate::value::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Represents a fully linked, executable PBS program image.
|
||||
///
|
||||
@ -22,7 +22,13 @@ pub struct ProgramImage {
|
||||
}
|
||||
|
||||
impl ProgramImage {
|
||||
pub fn new(rom: Vec<u8>, constant_pool: Vec<Value>, functions: Vec<FunctionMeta>, debug_info: Option<DebugInfo>, exports: HashMap<String, u32>) -> Self {
|
||||
pub fn new(
|
||||
rom: Vec<u8>,
|
||||
constant_pool: Vec<Value>,
|
||||
functions: Vec<FunctionMeta>,
|
||||
debug_info: Option<DebugInfo>,
|
||||
exports: HashMap<String, u32>,
|
||||
) -> Self {
|
||||
Self {
|
||||
rom: Arc::from(rom),
|
||||
constant_pool: Arc::from(constant_pool),
|
||||
@ -33,9 +39,10 @@ impl ProgramImage {
|
||||
}
|
||||
|
||||
pub fn create_trap(&self, code: u32, opcode: u16, mut message: String, pc: u32) -> TrapInfo {
|
||||
let span = self.debug_info.as_ref().and_then(|di| {
|
||||
di.pc_to_span.iter().find(|(p, _)| *p == pc).map(|(_, s)| s.clone())
|
||||
});
|
||||
let span = self
|
||||
.debug_info
|
||||
.as_ref()
|
||||
.and_then(|di| di.pc_to_span.iter().find(|(p, _)| *p == pc).map(|(_, s)| s.clone()));
|
||||
|
||||
if let Some(func_idx) = self.find_function_index(pc) {
|
||||
if let Some(func_name) = self.get_function_name(func_idx) {
|
||||
@ -43,23 +50,16 @@ impl ProgramImage {
|
||||
}
|
||||
}
|
||||
|
||||
TrapInfo {
|
||||
code,
|
||||
opcode,
|
||||
message,
|
||||
pc,
|
||||
span,
|
||||
}
|
||||
TrapInfo { code, opcode, message, pc, span }
|
||||
}
|
||||
|
||||
pub fn find_function_index(&self, pc: u32) -> Option<usize> {
|
||||
self.functions.iter().position(|f| {
|
||||
pc >= f.code_offset && pc < (f.code_offset + f.code_len)
|
||||
})
|
||||
self.functions.iter().position(|f| pc >= f.code_offset && pc < (f.code_offset + f.code_len))
|
||||
}
|
||||
|
||||
pub fn get_function_name(&self, func_idx: usize) -> Option<&str> {
|
||||
self.debug_info.as_ref()
|
||||
self.debug_info
|
||||
.as_ref()
|
||||
.and_then(|di| di.function_names.iter().find(|(idx, _)| *idx as usize == func_idx))
|
||||
.map(|(_, name)| name.as_str())
|
||||
}
|
||||
@ -67,35 +67,34 @@ impl ProgramImage {
|
||||
|
||||
impl From<BytecodeModule> for ProgramImage {
|
||||
fn from(module: BytecodeModule) -> Self {
|
||||
let constant_pool: Vec<Value> = module.const_pool.iter().map(|entry| {
|
||||
match entry {
|
||||
let constant_pool: Vec<Value> = module
|
||||
.const_pool
|
||||
.iter()
|
||||
.map(|entry| match entry {
|
||||
ConstantPoolEntry::Null => Value::Null,
|
||||
ConstantPoolEntry::Int64(v) => Value::Int64(*v),
|
||||
ConstantPoolEntry::Float64(v) => Value::Float(*v),
|
||||
ConstantPoolEntry::Boolean(v) => Value::Boolean(*v),
|
||||
ConstantPoolEntry::String(v) => Value::String(v.clone()),
|
||||
ConstantPoolEntry::Int32(v) => Value::Int32(*v),
|
||||
}
|
||||
}).collect();
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut exports = HashMap::new();
|
||||
for export in module.exports {
|
||||
exports.insert(export.symbol, export.func_idx);
|
||||
}
|
||||
|
||||
ProgramImage::new(
|
||||
module.code,
|
||||
constant_pool,
|
||||
module.functions,
|
||||
module.debug_info,
|
||||
exports,
|
||||
)
|
||||
ProgramImage::new(module.code, constant_pool, module.functions, module.debug_info, exports)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProgramImage> for BytecodeModule {
|
||||
fn from(program: ProgramImage) -> Self {
|
||||
let const_pool = program.constant_pool.iter().map(|v| match v {
|
||||
let const_pool = program
|
||||
.constant_pool
|
||||
.iter()
|
||||
.map(|v| match v {
|
||||
Value::Null => ConstantPoolEntry::Null,
|
||||
Value::Int64(v) => ConstantPoolEntry::Int64(*v),
|
||||
Value::Float(v) => ConstantPoolEntry::Float64(*v),
|
||||
@ -104,12 +103,14 @@ impl From<ProgramImage> for BytecodeModule {
|
||||
Value::Int32(v) => ConstantPoolEntry::Int32(*v),
|
||||
Value::Bounded(v) => ConstantPoolEntry::Int32(*v as i32),
|
||||
Value::Gate(_) => ConstantPoolEntry::Null,
|
||||
}).collect();
|
||||
})
|
||||
.collect();
|
||||
|
||||
let exports = program.exports.iter().map(|(symbol, &func_idx)| Export {
|
||||
symbol: symbol.clone(),
|
||||
func_idx,
|
||||
}).collect();
|
||||
let exports = program
|
||||
.exports
|
||||
.iter()
|
||||
.map(|(symbol, &func_idx)| Export { symbol: symbol.clone(), func_idx })
|
||||
.collect();
|
||||
|
||||
BytecodeModule {
|
||||
version: 0,
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::thread;
|
||||
use std::time::Instant;
|
||||
use prometeu_hal::asset::{AssetEntry, BankStats, BankType, HandleId, LoadStatus, PreloadEntry, SlotRef, SlotStats};
|
||||
use crate::memory_banks::{SoundBankPoolInstaller, TileBankPoolInstaller};
|
||||
use prometeu_hal::AssetBridge;
|
||||
use prometeu_hal::asset::{
|
||||
AssetEntry, BankStats, BankType, HandleId, LoadStatus, PreloadEntry, SlotRef, SlotStats,
|
||||
};
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::sample::Sample;
|
||||
use prometeu_hal::sound_bank::SoundBank;
|
||||
use prometeu_hal::tile_bank::{TileBank, TileSize};
|
||||
use crate::memory_banks::{SoundBankPoolInstaller, TileBankPoolInstaller};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::thread;
|
||||
use std::time::Instant;
|
||||
|
||||
/// Resident metadata for a decoded/materialized asset inside a BankPolicy.
|
||||
#[derive(Debug)]
|
||||
@ -21,7 +23,6 @@ pub struct ResidentEntry<T> {
|
||||
|
||||
// /// Pin count (optional): if > 0, entry should not be evicted by policy.
|
||||
// pub pins: u32,
|
||||
|
||||
/// Telemetry / profiling fields (optional but useful).
|
||||
pub loads: u64,
|
||||
pub last_used: Instant,
|
||||
@ -129,16 +130,41 @@ struct LoadHandleInfo {
|
||||
}
|
||||
|
||||
impl AssetBridge for AssetManager {
|
||||
fn initialize_for_cartridge(&self, assets: Vec<AssetEntry>, preload: Vec<PreloadEntry>, assets_data: Vec<u8>) { self.initialize_for_cartridge(assets, preload, assets_data) }
|
||||
fn load(&self, asset_name: &str, slot: SlotRef) -> Result<HandleId, String> { self.load(asset_name, slot) }
|
||||
fn status(&self, handle: HandleId) -> LoadStatus { self.status(handle) }
|
||||
fn commit(&self, handle: HandleId) { self.commit(handle) }
|
||||
fn cancel(&self, handle: HandleId) { self.cancel(handle) }
|
||||
fn apply_commits(&self) { self.apply_commits() }
|
||||
fn bank_info(&self, kind: BankType) -> BankStats { self.bank_info(kind) }
|
||||
fn slot_info(&self, slot: SlotRef) -> SlotStats { self.slot_info(slot) }
|
||||
fn find_slot_by_name(&self, asset_name: &str, kind: BankType) -> Option<u8> { self.find_slot_by_name(asset_name, kind) }
|
||||
fn shutdown(&self) { self.shutdown() }
|
||||
fn initialize_for_cartridge(
|
||||
&self,
|
||||
assets: Vec<AssetEntry>,
|
||||
preload: Vec<PreloadEntry>,
|
||||
assets_data: Vec<u8>,
|
||||
) {
|
||||
self.initialize_for_cartridge(assets, preload, assets_data)
|
||||
}
|
||||
fn load(&self, asset_name: &str, slot: SlotRef) -> Result<HandleId, String> {
|
||||
self.load(asset_name, slot)
|
||||
}
|
||||
fn status(&self, handle: HandleId) -> LoadStatus {
|
||||
self.status(handle)
|
||||
}
|
||||
fn commit(&self, handle: HandleId) {
|
||||
self.commit(handle)
|
||||
}
|
||||
fn cancel(&self, handle: HandleId) {
|
||||
self.cancel(handle)
|
||||
}
|
||||
fn apply_commits(&self) {
|
||||
self.apply_commits()
|
||||
}
|
||||
fn bank_info(&self, kind: BankType) -> BankStats {
|
||||
self.bank_info(kind)
|
||||
}
|
||||
fn slot_info(&self, slot: SlotRef) -> SlotStats {
|
||||
self.slot_info(slot)
|
||||
}
|
||||
fn find_slot_by_name(&self, asset_name: &str, kind: BankType) -> Option<u8> {
|
||||
self.find_slot_by_name(asset_name, kind)
|
||||
}
|
||||
fn shutdown(&self) {
|
||||
self.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetManager {
|
||||
@ -171,7 +197,12 @@ impl AssetManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize_for_cartridge(&self, assets: Vec<AssetEntry>, preload: Vec<PreloadEntry>, assets_data: Vec<u8>) {
|
||||
pub fn initialize_for_cartridge(
|
||||
&self,
|
||||
assets: Vec<AssetEntry>,
|
||||
preload: Vec<PreloadEntry>,
|
||||
assets_data: Vec<u8>,
|
||||
) {
|
||||
self.shutdown();
|
||||
{
|
||||
let mut asset_map = self.assets.write().unwrap();
|
||||
@ -190,45 +221,70 @@ impl AssetManager {
|
||||
let entry_opt = {
|
||||
let assets = self.assets.read().unwrap();
|
||||
let name_to_id = self.name_to_id.read().unwrap();
|
||||
name_to_id.get(&item.asset_name)
|
||||
.and_then(|id| assets.get(id))
|
||||
.cloned()
|
||||
name_to_id.get(&item.asset_name).and_then(|id| assets.get(id)).cloned()
|
||||
};
|
||||
|
||||
if let Some(entry) = entry_opt {
|
||||
let slot_index = item.slot;
|
||||
match entry.bank_type {
|
||||
BankType::TILES => {
|
||||
if let Ok(bank) = Self::perform_load_tile_bank(&entry, self.assets_data.clone()) {
|
||||
if let Ok(bank) =
|
||||
Self::perform_load_tile_bank(&entry, self.assets_data.clone())
|
||||
{
|
||||
let bank_arc = Arc::new(bank);
|
||||
self.gfx_policy.put_resident(entry.asset_id, Arc::clone(&bank_arc), entry.decoded_size as usize);
|
||||
self.gfx_policy.put_resident(
|
||||
entry.asset_id,
|
||||
Arc::clone(&bank_arc),
|
||||
entry.decoded_size as usize,
|
||||
);
|
||||
self.gfx_installer.install_tile_bank(slot_index, bank_arc);
|
||||
let mut slots = self.gfx_slots.write().unwrap();
|
||||
if slot_index < slots.len() {
|
||||
slots[slot_index] = Some(entry.asset_id);
|
||||
}
|
||||
println!("[AssetManager] Preloaded tile asset '{}' (id: {}) into slot {}", entry.asset_name, entry.asset_id, slot_index);
|
||||
println!(
|
||||
"[AssetManager] Preloaded tile asset '{}' (id: {}) into slot {}",
|
||||
entry.asset_name, entry.asset_id, slot_index
|
||||
);
|
||||
} else {
|
||||
eprintln!("[AssetManager] Failed to preload tile asset '{}'", entry.asset_name);
|
||||
eprintln!(
|
||||
"[AssetManager] Failed to preload tile asset '{}'",
|
||||
entry.asset_name
|
||||
);
|
||||
}
|
||||
}
|
||||
BankType::SOUNDS => {
|
||||
if let Ok(bank) = Self::perform_load_sound_bank(&entry, self.assets_data.clone()) {
|
||||
if let Ok(bank) =
|
||||
Self::perform_load_sound_bank(&entry, self.assets_data.clone())
|
||||
{
|
||||
let bank_arc = Arc::new(bank);
|
||||
self.sound_policy.put_resident(entry.asset_id, Arc::clone(&bank_arc), entry.decoded_size as usize);
|
||||
self.sound_policy.put_resident(
|
||||
entry.asset_id,
|
||||
Arc::clone(&bank_arc),
|
||||
entry.decoded_size as usize,
|
||||
);
|
||||
self.sound_installer.install_sound_bank(slot_index, bank_arc);
|
||||
let mut slots = self.sound_slots.write().unwrap();
|
||||
if slot_index < slots.len() {
|
||||
slots[slot_index] = Some(entry.asset_id);
|
||||
}
|
||||
println!("[AssetManager] Preloaded sound asset '{}' (id: {}) into slot {}", entry.asset_name, entry.asset_id, slot_index);
|
||||
println!(
|
||||
"[AssetManager] Preloaded sound asset '{}' (id: {}) into slot {}",
|
||||
entry.asset_name, entry.asset_id, slot_index
|
||||
);
|
||||
} else {
|
||||
eprintln!("[AssetManager] Failed to preload sound asset '{}'", entry.asset_name);
|
||||
eprintln!(
|
||||
"[AssetManager] Failed to preload sound asset '{}'",
|
||||
entry.asset_name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("[AssetManager] Preload failed: asset '{}' not found in table", item.asset_name);
|
||||
eprintln!(
|
||||
"[AssetManager] Preload failed: asset '{}' not found in table",
|
||||
item.asset_name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -237,7 +293,9 @@ impl AssetManager {
|
||||
let entry = {
|
||||
let assets = self.assets.read().unwrap();
|
||||
let name_to_id = self.name_to_id.read().unwrap();
|
||||
let id = name_to_id.get(asset_name).ok_or_else(|| format!("Asset not found: {}", asset_name))?;
|
||||
let id = name_to_id
|
||||
.get(asset_name)
|
||||
.ok_or_else(|| format!("Asset not found: {}", asset_name))?;
|
||||
assets.get(id).ok_or_else(|| format!("Asset ID {} not found in table", id))?.clone()
|
||||
};
|
||||
let asset_id = entry.asset_id;
|
||||
@ -256,31 +314,33 @@ impl AssetManager {
|
||||
if let Some(bank) = self.gfx_policy.get_resident(asset_id) {
|
||||
self.gfx_policy.stage(handle_id, bank);
|
||||
true
|
||||
} else { false }
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
BankType::SOUNDS => {
|
||||
if let Some(bank) = self.sound_policy.get_resident(asset_id) {
|
||||
self.sound_policy.stage(handle_id, bank);
|
||||
true
|
||||
} else { false }
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if already_resident {
|
||||
self.handles.write().unwrap().insert(handle_id, LoadHandleInfo {
|
||||
_asset_id: asset_id,
|
||||
slot,
|
||||
status: LoadStatus::READY,
|
||||
});
|
||||
self.handles.write().unwrap().insert(
|
||||
handle_id,
|
||||
LoadHandleInfo { _asset_id: asset_id, slot, status: LoadStatus::READY },
|
||||
);
|
||||
return Ok(handle_id);
|
||||
}
|
||||
|
||||
// Not resident, start loading
|
||||
self.handles.write().unwrap().insert(handle_id, LoadHandleInfo {
|
||||
_asset_id: asset_id,
|
||||
slot,
|
||||
status: LoadStatus::PENDING,
|
||||
});
|
||||
self.handles.write().unwrap().insert(
|
||||
handle_id,
|
||||
LoadHandleInfo { _asset_id: asset_id, slot, status: LoadStatus::PENDING },
|
||||
);
|
||||
|
||||
let handles = self.handles.clone();
|
||||
let assets_data = self.assets_data.clone();
|
||||
@ -319,7 +379,10 @@ impl AssetManager {
|
||||
existing.loads += 1;
|
||||
Arc::clone(&existing.value)
|
||||
} else {
|
||||
let entry = ResidentEntry::new(Arc::clone(&bank_arc), entry_clone.decoded_size as usize);
|
||||
let entry = ResidentEntry::new(
|
||||
Arc::clone(&bank_arc),
|
||||
entry_clone.decoded_size as usize,
|
||||
);
|
||||
map.insert(asset_id, entry);
|
||||
bank_arc
|
||||
}
|
||||
@ -327,11 +390,15 @@ impl AssetManager {
|
||||
gfx_policy_staging.write().unwrap().insert(handle_id, resident_arc);
|
||||
let mut handles_map = handles.write().unwrap();
|
||||
if let Some(h) = handles_map.get_mut(&handle_id) {
|
||||
if h.status == LoadStatus::LOADING { h.status = LoadStatus::READY; }
|
||||
if h.status == LoadStatus::LOADING {
|
||||
h.status = LoadStatus::READY;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut handles_map = handles.write().unwrap();
|
||||
if let Some(h) = handles_map.get_mut(&handle_id) { h.status = LoadStatus::ERROR; }
|
||||
if let Some(h) = handles_map.get_mut(&handle_id) {
|
||||
h.status = LoadStatus::ERROR;
|
||||
}
|
||||
}
|
||||
}
|
||||
BankType::SOUNDS => {
|
||||
@ -345,7 +412,10 @@ impl AssetManager {
|
||||
existing.loads += 1;
|
||||
Arc::clone(&existing.value)
|
||||
} else {
|
||||
let entry = ResidentEntry::new(Arc::clone(&bank_arc), entry_clone.decoded_size as usize);
|
||||
let entry = ResidentEntry::new(
|
||||
Arc::clone(&bank_arc),
|
||||
entry_clone.decoded_size as usize,
|
||||
);
|
||||
map.insert(asset_id, entry);
|
||||
bank_arc
|
||||
}
|
||||
@ -353,11 +423,15 @@ impl AssetManager {
|
||||
sound_policy_staging.write().unwrap().insert(handle_id, resident_arc);
|
||||
let mut handles_map = handles.write().unwrap();
|
||||
if let Some(h) = handles_map.get_mut(&handle_id) {
|
||||
if h.status == LoadStatus::LOADING { h.status = LoadStatus::READY; }
|
||||
if h.status == LoadStatus::LOADING {
|
||||
h.status = LoadStatus::READY;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut handles_map = handles.write().unwrap();
|
||||
if let Some(h) = handles_map.get_mut(&handle_id) { h.status = LoadStatus::ERROR; }
|
||||
if let Some(h) = handles_map.get_mut(&handle_id) {
|
||||
h.status = LoadStatus::ERROR;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -366,7 +440,10 @@ impl AssetManager {
|
||||
Ok(handle_id)
|
||||
}
|
||||
|
||||
fn perform_load_tile_bank(entry: &AssetEntry, assets_data: Arc<RwLock<Vec<u8>>>) -> Result<TileBank, String> {
|
||||
fn perform_load_tile_bank(
|
||||
entry: &AssetEntry,
|
||||
assets_data: Arc<RwLock<Vec<u8>>>,
|
||||
) -> Result<TileBank, String> {
|
||||
if entry.codec != "RAW" {
|
||||
return Err(format!("Unsupported codec: {}", entry.codec));
|
||||
}
|
||||
@ -383,9 +460,12 @@ impl AssetManager {
|
||||
let buffer = &assets_data[start..end];
|
||||
|
||||
// Decode TILEBANK metadata
|
||||
let tile_size_val = entry.metadata.get("tile_size").and_then(|v| v.as_u64()).ok_or("Missing tile_size")?;
|
||||
let width = entry.metadata.get("width").and_then(|v| v.as_u64()).ok_or("Missing width")? as usize;
|
||||
let height = entry.metadata.get("height").and_then(|v| v.as_u64()).ok_or("Missing height")? as usize;
|
||||
let tile_size_val =
|
||||
entry.metadata.get("tile_size").and_then(|v| v.as_u64()).ok_or("Missing tile_size")?;
|
||||
let width =
|
||||
entry.metadata.get("width").and_then(|v| v.as_u64()).ok_or("Missing width")? as usize;
|
||||
let height =
|
||||
entry.metadata.get("height").and_then(|v| v.as_u64()).ok_or("Missing height")? as usize;
|
||||
|
||||
let tile_size = match tile_size_val {
|
||||
8 => TileSize::Size8,
|
||||
@ -406,21 +486,19 @@ impl AssetManager {
|
||||
for p in 0..64 {
|
||||
for c in 0..16 {
|
||||
let offset = (p * 16 + c) * 2;
|
||||
let color_raw = u16::from_le_bytes([palette_data[offset], palette_data[offset + 1]]);
|
||||
let color_raw =
|
||||
u16::from_le_bytes([palette_data[offset], palette_data[offset + 1]]);
|
||||
palettes[p][c] = Color(color_raw);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(TileBank {
|
||||
tile_size,
|
||||
width,
|
||||
height,
|
||||
pixel_indices,
|
||||
palettes,
|
||||
})
|
||||
Ok(TileBank { tile_size, width, height, pixel_indices, palettes })
|
||||
}
|
||||
|
||||
fn perform_load_sound_bank(entry: &AssetEntry, assets_data: Arc<RwLock<Vec<u8>>>) -> Result<SoundBank, String> {
|
||||
fn perform_load_sound_bank(
|
||||
entry: &AssetEntry,
|
||||
assets_data: Arc<RwLock<Vec<u8>>>,
|
||||
) -> Result<SoundBank, String> {
|
||||
if entry.codec != "RAW" {
|
||||
return Err(format!("Unsupported codec: {}", entry.codec));
|
||||
}
|
||||
@ -436,7 +514,8 @@ impl AssetManager {
|
||||
|
||||
let buffer = &assets_data[start..end];
|
||||
|
||||
let sample_rate = entry.metadata.get("sample_rate").and_then(|v| v.as_u64()).unwrap_or(44100) as u32;
|
||||
let sample_rate =
|
||||
entry.metadata.get("sample_rate").and_then(|v| v.as_u64()).unwrap_or(44100) as u32;
|
||||
|
||||
let mut data = Vec::with_capacity(buffer.len() / 2);
|
||||
for i in (0..buffer.len()).step_by(2) {
|
||||
@ -540,7 +619,9 @@ impl AssetManager {
|
||||
{
|
||||
let slots = self.gfx_slots.read().unwrap();
|
||||
for s in slots.iter() {
|
||||
if s.is_some() { slots_occupied += 1; }
|
||||
if s.is_some() {
|
||||
slots_occupied += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -581,7 +662,9 @@ impl AssetManager {
|
||||
{
|
||||
let slots = self.sound_slots.read().unwrap();
|
||||
for s in slots.iter() {
|
||||
if s.is_some() { slots_occupied += 1; }
|
||||
if s.is_some() {
|
||||
slots_occupied += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -604,7 +687,11 @@ impl AssetManager {
|
||||
let asset_id = slots.get(slot.index).and_then(|s| s.clone());
|
||||
|
||||
let (bytes, asset_name) = if let Some(id) = &asset_id {
|
||||
let bytes = self.gfx_policy.resident.read().unwrap()
|
||||
let bytes = self
|
||||
.gfx_policy
|
||||
.resident
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(id)
|
||||
.map(|entry| entry.bytes)
|
||||
.unwrap_or(0);
|
||||
@ -614,19 +701,18 @@ impl AssetManager {
|
||||
(0, None)
|
||||
};
|
||||
|
||||
SlotStats {
|
||||
asset_id,
|
||||
asset_name,
|
||||
generation: 0,
|
||||
resident_bytes: bytes,
|
||||
}
|
||||
SlotStats { asset_id, asset_name, generation: 0, resident_bytes: bytes }
|
||||
}
|
||||
BankType::SOUNDS => {
|
||||
let slots = self.sound_slots.read().unwrap();
|
||||
let asset_id = slots.get(slot.index).and_then(|s| s.clone());
|
||||
|
||||
let (bytes, asset_name) = if let Some(id) = &asset_id {
|
||||
let bytes = self.sound_policy.resident.read().unwrap()
|
||||
let bytes = self
|
||||
.sound_policy
|
||||
.resident
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(id)
|
||||
.map(|entry| entry.bytes)
|
||||
.unwrap_or(0);
|
||||
@ -636,12 +722,7 @@ impl AssetManager {
|
||||
(0, None)
|
||||
};
|
||||
|
||||
SlotStats {
|
||||
asset_id,
|
||||
asset_name,
|
||||
generation: 0,
|
||||
resident_bytes: bytes,
|
||||
}
|
||||
SlotStats { asset_id, asset_name, generation: 0, resident_bytes: bytes }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -832,9 +913,7 @@ mod tests {
|
||||
}),
|
||||
};
|
||||
|
||||
let preload = vec![
|
||||
PreloadEntry { asset_name: "preload_sound".to_string(), slot: 5 }
|
||||
];
|
||||
let preload = vec![PreloadEntry { asset_name: "preload_sound".to_string(), slot: 5 }];
|
||||
|
||||
let am = AssetManager::new(vec![], vec![], gfx_installer, sound_installer);
|
||||
|
||||
@ -868,9 +947,7 @@ mod tests {
|
||||
metadata: serde_json::json!({ "tile_size": 16, "width": 16, "height": 16 }),
|
||||
};
|
||||
|
||||
let preload = vec![
|
||||
PreloadEntry { asset_name: "my_tiles".to_string(), slot: 3 }
|
||||
];
|
||||
let preload = vec![PreloadEntry { asset_name: "my_tiles".to_string(), slot: 3 }];
|
||||
|
||||
let am = AssetManager::new(vec![], vec![], gfx_installer, sound_installer);
|
||||
am.initialize_for_cartridge(vec![asset_entry], preload, data);
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
use std::sync::Arc;
|
||||
use prometeu_hal::AudioBridge;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Maximum number of simultaneous audio voices supported by the hardware.
|
||||
pub const MAX_CHANNELS: usize = 16;
|
||||
/// Standard sample rate for the final audio output.
|
||||
pub const OUTPUT_SAMPLE_RATE: u32 = 48000;
|
||||
|
||||
use crate::memory_banks::SoundBankPoolAccess;
|
||||
/// Looping mode for samples (re-exported from the hardware contract).
|
||||
pub use prometeu_hal::LoopMode;
|
||||
use prometeu_hal::sample::Sample;
|
||||
use crate::memory_banks::SoundBankPoolAccess;
|
||||
|
||||
/// State of a single playback voice (channel).
|
||||
///
|
||||
@ -65,24 +65,13 @@ pub enum AudioCommand {
|
||||
loop_mode: LoopMode,
|
||||
},
|
||||
/// Immediately stop playback on a voice.
|
||||
Stop {
|
||||
voice_id: usize,
|
||||
},
|
||||
Stop { voice_id: usize },
|
||||
/// Update volume of an ongoing playback.
|
||||
SetVolume {
|
||||
voice_id: usize,
|
||||
volume: u8,
|
||||
},
|
||||
SetVolume { voice_id: usize, volume: u8 },
|
||||
/// Update panning of an ongoing playback.
|
||||
SetPan {
|
||||
voice_id: usize,
|
||||
pan: u8,
|
||||
},
|
||||
SetPan { voice_id: usize, pan: u8 },
|
||||
/// Update pitch of an ongoing playback.
|
||||
SetPitch {
|
||||
voice_id: usize,
|
||||
pitch: f64,
|
||||
},
|
||||
SetPitch { voice_id: usize, pitch: f64 },
|
||||
/// Pause all audio processing.
|
||||
MasterPause,
|
||||
/// Resume audio processing.
|
||||
@ -114,20 +103,57 @@ pub struct Audio {
|
||||
}
|
||||
|
||||
impl AudioBridge for Audio {
|
||||
fn play(&mut self, bank_id: u8, sample_id: u16, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: prometeu_hal::LoopMode) {
|
||||
let lm = match loop_mode { prometeu_hal::LoopMode::Off => LoopMode::Off, prometeu_hal::LoopMode::On => LoopMode::On };
|
||||
fn play(
|
||||
&mut self,
|
||||
bank_id: u8,
|
||||
sample_id: u16,
|
||||
voice_id: usize,
|
||||
volume: u8,
|
||||
pan: u8,
|
||||
pitch: f64,
|
||||
priority: u8,
|
||||
loop_mode: prometeu_hal::LoopMode,
|
||||
) {
|
||||
let lm = match loop_mode {
|
||||
prometeu_hal::LoopMode::Off => LoopMode::Off,
|
||||
prometeu_hal::LoopMode::On => LoopMode::On,
|
||||
};
|
||||
self.play(bank_id, sample_id, voice_id, volume, pan, pitch, priority, lm)
|
||||
}
|
||||
fn play_sample(&mut self, sample: Arc<Sample>, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: prometeu_hal::LoopMode) {
|
||||
let lm = match loop_mode { prometeu_hal::LoopMode::Off => LoopMode::Off, prometeu_hal::LoopMode::On => LoopMode::On };
|
||||
fn play_sample(
|
||||
&mut self,
|
||||
sample: Arc<Sample>,
|
||||
voice_id: usize,
|
||||
volume: u8,
|
||||
pan: u8,
|
||||
pitch: f64,
|
||||
priority: u8,
|
||||
loop_mode: prometeu_hal::LoopMode,
|
||||
) {
|
||||
let lm = match loop_mode {
|
||||
prometeu_hal::LoopMode::Off => LoopMode::Off,
|
||||
prometeu_hal::LoopMode::On => LoopMode::On,
|
||||
};
|
||||
self.play_sample(sample, voice_id, volume, pan, pitch, priority, lm)
|
||||
}
|
||||
fn stop(&mut self, voice_id: usize) { self.stop(voice_id) }
|
||||
fn set_volume(&mut self, voice_id: usize, volume: u8) { self.set_volume(voice_id, volume) }
|
||||
fn set_pan(&mut self, voice_id: usize, pan: u8) { self.set_pan(voice_id, pan) }
|
||||
fn set_pitch(&mut self, voice_id: usize, pitch: f64) { self.set_pitch(voice_id, pitch) }
|
||||
fn is_playing(&self, voice_id: usize) -> bool { self.is_playing(voice_id) }
|
||||
fn clear_commands(&mut self) { self.clear_commands() }
|
||||
fn stop(&mut self, voice_id: usize) {
|
||||
self.stop(voice_id)
|
||||
}
|
||||
fn set_volume(&mut self, voice_id: usize, volume: u8) {
|
||||
self.set_volume(voice_id, volume)
|
||||
}
|
||||
fn set_pan(&mut self, voice_id: usize, pan: u8) {
|
||||
self.set_pan(voice_id, pan)
|
||||
}
|
||||
fn set_pitch(&mut self, voice_id: usize, pitch: f64) {
|
||||
self.set_pitch(voice_id, pitch)
|
||||
}
|
||||
fn is_playing(&self, voice_id: usize) -> bool {
|
||||
self.is_playing(voice_id)
|
||||
}
|
||||
fn clear_commands(&mut self) {
|
||||
self.clear_commands()
|
||||
}
|
||||
}
|
||||
|
||||
impl Audio {
|
||||
@ -140,25 +166,51 @@ impl Audio {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play(&mut self, bank_id: u8, sample_id: u16, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: LoopMode) {
|
||||
pub fn play(
|
||||
&mut self,
|
||||
bank_id: u8,
|
||||
sample_id: u16,
|
||||
voice_id: usize,
|
||||
volume: u8,
|
||||
pan: u8,
|
||||
pitch: f64,
|
||||
priority: u8,
|
||||
loop_mode: LoopMode,
|
||||
) {
|
||||
if voice_id >= MAX_CHANNELS {
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve the sample from the hardware pools
|
||||
let sample = self.sound_banks.sound_bank_slot(bank_id as usize)
|
||||
let sample = self
|
||||
.sound_banks
|
||||
.sound_bank_slot(bank_id as usize)
|
||||
.and_then(|bank| bank.samples.get(sample_id as usize).map(Arc::clone));
|
||||
|
||||
|
||||
if let Some(s) = sample {
|
||||
println!("[Audio] Resolved sample from bank {} sample {}. Playing on voice {}.", bank_id, sample_id, voice_id);
|
||||
println!(
|
||||
"[Audio] Resolved sample from bank {} sample {}. Playing on voice {}.",
|
||||
bank_id, sample_id, voice_id
|
||||
);
|
||||
self.play_sample(s, voice_id, volume, pan, pitch, priority, loop_mode);
|
||||
} else {
|
||||
eprintln!("[Audio] Failed to resolve sample from bank {} sample {}.", bank_id, sample_id);
|
||||
eprintln!(
|
||||
"[Audio] Failed to resolve sample from bank {} sample {}.",
|
||||
bank_id, sample_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play_sample(&mut self, sample: Arc<Sample>, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: LoopMode) {
|
||||
pub fn play_sample(
|
||||
&mut self,
|
||||
sample: Arc<Sample>,
|
||||
voice_id: usize,
|
||||
volume: u8,
|
||||
pan: u8,
|
||||
pitch: f64,
|
||||
priority: u8,
|
||||
loop_mode: LoopMode,
|
||||
) {
|
||||
if voice_id >= MAX_CHANNELS {
|
||||
return;
|
||||
}
|
||||
@ -217,11 +269,7 @@ impl Audio {
|
||||
}
|
||||
|
||||
pub fn is_playing(&self, voice_id: usize) -> bool {
|
||||
if voice_id < MAX_CHANNELS {
|
||||
self.voices[voice_id].active
|
||||
} else {
|
||||
false
|
||||
}
|
||||
if voice_id < MAX_CHANNELS { self.voices[voice_id].active } else { false }
|
||||
}
|
||||
|
||||
/// Clears the command queue. The Host should consume this every frame.
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
use crate::memory_banks::TileBankPoolAccess;
|
||||
use std::sync::Arc;
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::GfxBridge;
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::sprite::Sprite;
|
||||
use prometeu_hal::tile::Tile;
|
||||
use prometeu_hal::tile_bank::{TileBank, TileSize};
|
||||
use prometeu_hal::tile_layer::{HudTileLayer, ScrollableTileLayer, TileMap};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Blending modes inspired by classic 16-bit hardware.
|
||||
/// Defines how source pixels are combined with existing pixels in the framebuffer.
|
||||
@ -79,44 +79,139 @@ pub struct Gfx {
|
||||
}
|
||||
|
||||
impl GfxBridge for Gfx {
|
||||
fn size(&self) -> (usize, usize) { self.size() }
|
||||
fn front_buffer(&self) -> &[u16] { self.front_buffer() }
|
||||
fn clear(&mut self, color: Color) { self.clear(color) }
|
||||
fn fill_rect_blend(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color, mode: prometeu_hal::BlendMode) { let m = match mode { prometeu_hal::BlendMode::None => BlendMode::None, prometeu_hal::BlendMode::Half => BlendMode::Half, prometeu_hal::BlendMode::HalfPlus => BlendMode::HalfPlus, prometeu_hal::BlendMode::HalfMinus => BlendMode::HalfMinus, prometeu_hal::BlendMode::Full => BlendMode::Full }; self.fill_rect_blend(x, y, w, h, color, m) }
|
||||
fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) { self.fill_rect(x, y, w, h, color) }
|
||||
fn draw_pixel(&mut self, x: i32, y: i32, color: Color) { self.draw_pixel(x, y, color) }
|
||||
fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) { self.draw_line(x0, y0, x1, y1, color) }
|
||||
fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) { self.draw_circle(xc, yc, r, color) }
|
||||
fn draw_circle_points(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color) { self.draw_circle_points(xc, yc, x, y, color) }
|
||||
fn fill_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) { self.fill_circle(xc, yc, r, color) }
|
||||
fn draw_circle_lines(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color) { self.draw_circle_lines(xc, yc, x, y, color) }
|
||||
fn draw_disc(&mut self, x: i32, y: i32, r: i32, border_color: Color, fill_color: Color) { self.draw_disc(x, y, r, border_color, fill_color) }
|
||||
fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) { self.draw_rect(x, y, w, h, color) }
|
||||
fn draw_square(&mut self, x: i32, y: i32, w: i32, h: i32, border_color: Color, fill_color: Color) { self.draw_square(x, y, w, h, border_color, fill_color) }
|
||||
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color) { self.draw_horizontal_line(x0, x1, y, color) }
|
||||
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color) { self.draw_vertical_line(x, y0, y1, color) }
|
||||
fn present(&mut self) { self.present() }
|
||||
fn render_all(&mut self) { self.render_all() }
|
||||
fn render_layer(&mut self, layer_idx: usize) { self.render_layer(layer_idx) }
|
||||
fn render_hud(&mut self) { self.render_hud() }
|
||||
fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) { self.draw_text(x, y, text, color) }
|
||||
fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color) { self.draw_char(x, y, c, color) }
|
||||
fn size(&self) -> (usize, usize) {
|
||||
self.size()
|
||||
}
|
||||
fn front_buffer(&self) -> &[u16] {
|
||||
self.front_buffer()
|
||||
}
|
||||
fn clear(&mut self, color: Color) {
|
||||
self.clear(color)
|
||||
}
|
||||
fn fill_rect_blend(
|
||||
&mut self,
|
||||
x: i32,
|
||||
y: i32,
|
||||
w: i32,
|
||||
h: i32,
|
||||
color: Color,
|
||||
mode: prometeu_hal::BlendMode,
|
||||
) {
|
||||
let m = match mode {
|
||||
prometeu_hal::BlendMode::None => BlendMode::None,
|
||||
prometeu_hal::BlendMode::Half => BlendMode::Half,
|
||||
prometeu_hal::BlendMode::HalfPlus => BlendMode::HalfPlus,
|
||||
prometeu_hal::BlendMode::HalfMinus => BlendMode::HalfMinus,
|
||||
prometeu_hal::BlendMode::Full => BlendMode::Full,
|
||||
};
|
||||
self.fill_rect_blend(x, y, w, h, color, m)
|
||||
}
|
||||
fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) {
|
||||
self.fill_rect(x, y, w, h, color)
|
||||
}
|
||||
fn draw_pixel(&mut self, x: i32, y: i32, color: Color) {
|
||||
self.draw_pixel(x, y, color)
|
||||
}
|
||||
fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) {
|
||||
self.draw_line(x0, y0, x1, y1, color)
|
||||
}
|
||||
fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
|
||||
self.draw_circle(xc, yc, r, color)
|
||||
}
|
||||
fn draw_circle_points(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color) {
|
||||
self.draw_circle_points(xc, yc, x, y, color)
|
||||
}
|
||||
fn fill_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
|
||||
self.fill_circle(xc, yc, r, color)
|
||||
}
|
||||
fn draw_circle_lines(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color) {
|
||||
self.draw_circle_lines(xc, yc, x, y, color)
|
||||
}
|
||||
fn draw_disc(&mut self, x: i32, y: i32, r: i32, border_color: Color, fill_color: Color) {
|
||||
self.draw_disc(x, y, r, border_color, fill_color)
|
||||
}
|
||||
fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) {
|
||||
self.draw_rect(x, y, w, h, color)
|
||||
}
|
||||
fn draw_square(
|
||||
&mut self,
|
||||
x: i32,
|
||||
y: i32,
|
||||
w: i32,
|
||||
h: i32,
|
||||
border_color: Color,
|
||||
fill_color: Color,
|
||||
) {
|
||||
self.draw_square(x, y, w, h, border_color, fill_color)
|
||||
}
|
||||
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color) {
|
||||
self.draw_horizontal_line(x0, x1, y, color)
|
||||
}
|
||||
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color) {
|
||||
self.draw_vertical_line(x, y0, y1, color)
|
||||
}
|
||||
fn present(&mut self) {
|
||||
self.present()
|
||||
}
|
||||
fn render_all(&mut self) {
|
||||
self.render_all()
|
||||
}
|
||||
fn render_layer(&mut self, layer_idx: usize) {
|
||||
self.render_layer(layer_idx)
|
||||
}
|
||||
fn render_hud(&mut self) {
|
||||
self.render_hud()
|
||||
}
|
||||
fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) {
|
||||
self.draw_text(x, y, text, color)
|
||||
}
|
||||
fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color) {
|
||||
self.draw_char(x, y, c, color)
|
||||
}
|
||||
|
||||
fn layer(&self, index: usize) -> &ScrollableTileLayer { &self.layers[index] }
|
||||
fn layer_mut(&mut self, index: usize) -> &mut ScrollableTileLayer { &mut self.layers[index] }
|
||||
fn hud(&self) -> &HudTileLayer { &self.hud }
|
||||
fn hud_mut(&mut self) -> &mut HudTileLayer { &mut self.hud }
|
||||
fn sprite(&self, index: usize) -> &Sprite { &self.sprites[index] }
|
||||
fn sprite_mut(&mut self, index: usize) -> &mut Sprite { &mut self.sprites[index] }
|
||||
fn layer(&self, index: usize) -> &ScrollableTileLayer {
|
||||
&self.layers[index]
|
||||
}
|
||||
fn layer_mut(&mut self, index: usize) -> &mut ScrollableTileLayer {
|
||||
&mut self.layers[index]
|
||||
}
|
||||
fn hud(&self) -> &HudTileLayer {
|
||||
&self.hud
|
||||
}
|
||||
fn hud_mut(&mut self) -> &mut HudTileLayer {
|
||||
&mut self.hud
|
||||
}
|
||||
fn sprite(&self, index: usize) -> &Sprite {
|
||||
&self.sprites[index]
|
||||
}
|
||||
fn sprite_mut(&mut self, index: usize) -> &mut Sprite {
|
||||
&mut self.sprites[index]
|
||||
}
|
||||
|
||||
fn scene_fade_level(&self) -> u8 { self.scene_fade_level }
|
||||
fn set_scene_fade_level(&mut self, level: u8) { self.scene_fade_level = level; }
|
||||
fn scene_fade_color(&self) -> Color { self.scene_fade_color }
|
||||
fn set_scene_fade_color(&mut self, color: Color) { self.scene_fade_color = color; }
|
||||
fn hud_fade_level(&self) -> u8 { self.hud_fade_level }
|
||||
fn set_hud_fade_level(&mut self, level: u8) { self.hud_fade_level = level; }
|
||||
fn hud_fade_color(&self) -> Color { self.hud_fade_color }
|
||||
fn set_hud_fade_color(&mut self, color: Color) { self.hud_fade_color = color; }
|
||||
fn scene_fade_level(&self) -> u8 {
|
||||
self.scene_fade_level
|
||||
}
|
||||
fn set_scene_fade_level(&mut self, level: u8) {
|
||||
self.scene_fade_level = level;
|
||||
}
|
||||
fn scene_fade_color(&self) -> Color {
|
||||
self.scene_fade_color
|
||||
}
|
||||
fn set_scene_fade_color(&mut self, color: Color) {
|
||||
self.scene_fade_color = color;
|
||||
}
|
||||
fn hud_fade_level(&self) -> u8 {
|
||||
self.hud_fade_level
|
||||
}
|
||||
fn set_hud_fade_level(&mut self, level: u8) {
|
||||
self.hud_fade_level = level;
|
||||
}
|
||||
fn hud_fade_color(&self) -> Color {
|
||||
self.hud_fade_color
|
||||
}
|
||||
fn set_hud_fade_color(&mut self, color: Color) {
|
||||
self.hud_fade_color = color;
|
||||
}
|
||||
}
|
||||
|
||||
impl Gfx {
|
||||
@ -187,7 +282,9 @@ impl Gfx {
|
||||
color: Color,
|
||||
mode: BlendMode,
|
||||
) {
|
||||
if color == Color::COLOR_KEY { return; }
|
||||
if color == Color::COLOR_KEY {
|
||||
return;
|
||||
}
|
||||
|
||||
let fw = self.w as i32;
|
||||
let fh = self.h as i32;
|
||||
@ -216,7 +313,9 @@ impl Gfx {
|
||||
|
||||
/// Draws a single pixel.
|
||||
pub fn draw_pixel(&mut self, x: i32, y: i32, color: Color) {
|
||||
if color == Color::COLOR_KEY { return; }
|
||||
if color == Color::COLOR_KEY {
|
||||
return;
|
||||
}
|
||||
if x >= 0 && x < self.w as i32 && y >= 0 && y < self.h as i32 {
|
||||
self.back[y as usize * self.w + x as usize] = color.0;
|
||||
}
|
||||
@ -224,7 +323,9 @@ impl Gfx {
|
||||
|
||||
/// Draws a line between two points using Bresenham's algorithm.
|
||||
pub fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) {
|
||||
if color == Color::COLOR_KEY { return; }
|
||||
if color == Color::COLOR_KEY {
|
||||
return;
|
||||
}
|
||||
|
||||
let dx = (x1 - x0).abs();
|
||||
let sx = if x0 < x1 { 1 } else { -1 };
|
||||
@ -237,7 +338,9 @@ impl Gfx {
|
||||
|
||||
loop {
|
||||
self.draw_pixel(x, y, color);
|
||||
if x == x1 && y == y1 { break; }
|
||||
if x == x1 && y == y1 {
|
||||
break;
|
||||
}
|
||||
let e2 = 2 * err;
|
||||
if e2 >= dy {
|
||||
err += dy;
|
||||
@ -252,9 +355,13 @@ impl Gfx {
|
||||
|
||||
/// Draws a circle outline using Midpoint Circle Algorithm.
|
||||
pub fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
|
||||
if color == Color::COLOR_KEY { return; }
|
||||
if color == Color::COLOR_KEY {
|
||||
return;
|
||||
}
|
||||
|
||||
if r < 0 { return; }
|
||||
if r < 0 {
|
||||
return;
|
||||
}
|
||||
let mut x = 0;
|
||||
let mut y = r;
|
||||
let mut d = 3 - 2 * r;
|
||||
@ -284,9 +391,13 @@ impl Gfx {
|
||||
|
||||
/// Draws a filled circle.
|
||||
pub fn fill_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
|
||||
if color == Color::COLOR_KEY { return; }
|
||||
if color == Color::COLOR_KEY {
|
||||
return;
|
||||
}
|
||||
|
||||
if r < 0 { return; }
|
||||
if r < 0 {
|
||||
return;
|
||||
}
|
||||
let mut x = 0;
|
||||
let mut y = r;
|
||||
let mut d = 3 - 2 * r;
|
||||
@ -318,9 +429,13 @@ impl Gfx {
|
||||
|
||||
/// Draws a rectangle outline.
|
||||
pub fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) {
|
||||
if color == Color::COLOR_KEY { return; }
|
||||
if color == Color::COLOR_KEY {
|
||||
return;
|
||||
}
|
||||
|
||||
if w <= 0 || h <= 0 { return; }
|
||||
if w <= 0 || h <= 0 {
|
||||
return;
|
||||
}
|
||||
self.draw_horizontal_line(x, x + w - 1, y, color);
|
||||
self.draw_horizontal_line(x, x + w - 1, y + h - 1, color);
|
||||
self.draw_vertical_line(x, y, y + h - 1, color);
|
||||
@ -328,30 +443,50 @@ impl Gfx {
|
||||
}
|
||||
|
||||
/// Draws a square (filled rectangle with border).
|
||||
pub fn draw_square(&mut self, x: i32, y: i32, w: i32, h: i32, border_color: Color, fill_color: Color) {
|
||||
pub fn draw_square(
|
||||
&mut self,
|
||||
x: i32,
|
||||
y: i32,
|
||||
w: i32,
|
||||
h: i32,
|
||||
border_color: Color,
|
||||
fill_color: Color,
|
||||
) {
|
||||
self.fill_rect(x, y, w, h, fill_color);
|
||||
self.draw_rect(x, y, w, h, border_color);
|
||||
}
|
||||
|
||||
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color) {
|
||||
if color == Color::COLOR_KEY { return; }
|
||||
if color == Color::COLOR_KEY {
|
||||
return;
|
||||
}
|
||||
|
||||
if y < 0 || y >= self.h as i32 { return; }
|
||||
if y < 0 || y >= self.h as i32 {
|
||||
return;
|
||||
}
|
||||
let start = x0.max(0);
|
||||
let end = x1.min(self.w as i32 - 1);
|
||||
if start > end { return; }
|
||||
if start > end {
|
||||
return;
|
||||
}
|
||||
for x in start..=end {
|
||||
self.back[y as usize * self.w + x as usize] = color.0;
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color) {
|
||||
if color == Color::COLOR_KEY { return; }
|
||||
if color == Color::COLOR_KEY {
|
||||
return;
|
||||
}
|
||||
|
||||
if x < 0 || x >= self.w as i32 { return; }
|
||||
if x < 0 || x >= self.w as i32 {
|
||||
return;
|
||||
}
|
||||
let start = y0.max(0);
|
||||
let end = y1.min(self.h as i32 - 1);
|
||||
if start > end { return; }
|
||||
if start > end {
|
||||
return;
|
||||
}
|
||||
for y in start..=end {
|
||||
self.back[y as usize * self.w + x as usize] = color.0;
|
||||
}
|
||||
@ -382,18 +517,40 @@ impl Gfx {
|
||||
}
|
||||
|
||||
// 1. Priority 0 sprites: drawn at the very back, behind everything else.
|
||||
Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[0], &self.sprites, &*self.tile_banks);
|
||||
Self::draw_bucket_on_buffer(
|
||||
&mut self.back,
|
||||
self.w,
|
||||
self.h,
|
||||
&self.priority_buckets[0],
|
||||
&self.sprites,
|
||||
&*self.tile_banks,
|
||||
);
|
||||
|
||||
// 2. Main layers and prioritized sprites.
|
||||
// Order: Layer 0 -> Sprites 1 -> Layer 1 -> Sprites 2 ...
|
||||
for i in 0..self.layers.len() {
|
||||
let bank_id = self.layers[i].bank_id as usize;
|
||||
if let Some(bank) = self.tile_banks.tile_bank_slot(bank_id) {
|
||||
Self::draw_tile_map(&mut self.back, self.w, self.h, &self.layers[i].map, &bank, self.layers[i].scroll_x, self.layers[i].scroll_y);
|
||||
Self::draw_tile_map(
|
||||
&mut self.back,
|
||||
self.w,
|
||||
self.h,
|
||||
&self.layers[i].map,
|
||||
&bank,
|
||||
self.layers[i].scroll_x,
|
||||
self.layers[i].scroll_y,
|
||||
);
|
||||
}
|
||||
|
||||
// Draw sprites that belong to this depth level
|
||||
Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[i + 1], &self.sprites, &*self.tile_banks);
|
||||
Self::draw_bucket_on_buffer(
|
||||
&mut self.back,
|
||||
self.w,
|
||||
self.h,
|
||||
&self.priority_buckets[i + 1],
|
||||
&self.sprites,
|
||||
&*self.tile_banks,
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Scene Fade: Applies a color blend to the entire world (excluding HUD).
|
||||
@ -408,7 +565,9 @@ impl Gfx {
|
||||
|
||||
/// Renders a specific game layer.
|
||||
pub fn render_layer(&mut self, layer_idx: usize) {
|
||||
if layer_idx >= self.layers.len() { return; }
|
||||
if layer_idx >= self.layers.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
let bank_id = self.layers[layer_idx].bank_id as usize;
|
||||
let scroll_x = self.layers[layer_idx].scroll_x;
|
||||
@ -419,7 +578,15 @@ impl Gfx {
|
||||
_ => return,
|
||||
};
|
||||
|
||||
Self::draw_tile_map(&mut self.back, self.w, self.h, &self.layers[layer_idx].map, &bank, scroll_x, scroll_y);
|
||||
Self::draw_tile_map(
|
||||
&mut self.back,
|
||||
self.w,
|
||||
self.h,
|
||||
&self.layers[layer_idx].map,
|
||||
&bank,
|
||||
scroll_x,
|
||||
scroll_y,
|
||||
);
|
||||
}
|
||||
|
||||
/// Renders the HUD (fixed position, no scroll).
|
||||
@ -427,7 +594,13 @@ impl Gfx {
|
||||
Self::render_hud_with_pool(&mut self.back, self.w, self.h, &self.hud, &*self.tile_banks);
|
||||
}
|
||||
|
||||
fn render_hud_with_pool(back: &mut [u16], w: usize, h: usize, hud: &HudTileLayer, tile_banks: &dyn TileBankPoolAccess) {
|
||||
fn render_hud_with_pool(
|
||||
back: &mut [u16],
|
||||
w: usize,
|
||||
h: usize,
|
||||
hud: &HudTileLayer,
|
||||
tile_banks: &dyn TileBankPoolAccess,
|
||||
) {
|
||||
let bank_id = hud.bank_id as usize;
|
||||
let bank = match tile_banks.tile_bank_slot(bank_id) {
|
||||
Some(b) => b,
|
||||
@ -445,7 +618,7 @@ impl Gfx {
|
||||
map: &TileMap,
|
||||
bank: &TileBank,
|
||||
scroll_x: i32,
|
||||
scroll_y: i32
|
||||
scroll_y: i32,
|
||||
) {
|
||||
let tile_size = bank.tile_size as usize;
|
||||
|
||||
@ -469,34 +642,58 @@ impl Gfx {
|
||||
let map_y = (start_tile_y + ty as i32) as usize;
|
||||
|
||||
// Bounds check: don't draw if the camera is outside the map.
|
||||
if map_x >= map.width || map_y >= map.height { continue; }
|
||||
if map_x >= map.width || map_y >= map.height {
|
||||
continue;
|
||||
}
|
||||
|
||||
let tile = map.tiles[map_y * map.width + map_x];
|
||||
|
||||
// Optimized skip for empty (ID 0) tiles.
|
||||
if tile.id == 0 { continue; }
|
||||
if tile.id == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 5. Project the tile pixels to screen space.
|
||||
let screen_tile_x = (tx as i32 * tile_size as i32) - fine_scroll_x;
|
||||
let screen_tile_y = (ty as i32 * tile_size as i32) - fine_scroll_y;
|
||||
|
||||
Self::draw_tile_pixels(back, screen_w, screen_h, screen_tile_x, screen_tile_y, tile, bank);
|
||||
Self::draw_tile_pixels(
|
||||
back,
|
||||
screen_w,
|
||||
screen_h,
|
||||
screen_tile_x,
|
||||
screen_tile_y,
|
||||
tile,
|
||||
bank,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal helper to copy a single tile's pixels to the framebuffer.
|
||||
/// Handles flipping and palette resolution.
|
||||
fn draw_tile_pixels(back: &mut [u16], screen_w: usize, screen_h: usize, x: i32, y: i32, tile: Tile, bank: &TileBank) {
|
||||
fn draw_tile_pixels(
|
||||
back: &mut [u16],
|
||||
screen_w: usize,
|
||||
screen_h: usize,
|
||||
x: i32,
|
||||
y: i32,
|
||||
tile: Tile,
|
||||
bank: &TileBank,
|
||||
) {
|
||||
let size = bank.tile_size as usize;
|
||||
|
||||
for local_y in 0..size {
|
||||
let world_y = y + local_y as i32;
|
||||
if world_y < 0 || world_y >= screen_h as i32 { continue; }
|
||||
if world_y < 0 || world_y >= screen_h as i32 {
|
||||
continue;
|
||||
}
|
||||
|
||||
for local_x in 0..size {
|
||||
let world_x = x + local_x as i32;
|
||||
if world_x < 0 || world_x >= screen_w as i32 { continue; }
|
||||
if world_x < 0 || world_x >= screen_w as i32 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle flip flags by reversing the fetch coordinates.
|
||||
let fetch_x = if tile.flip_x { size - 1 - local_x } else { local_x };
|
||||
@ -506,7 +703,9 @@ impl Gfx {
|
||||
let px_index = bank.get_pixel_index(tile.id, fetch_x, fetch_y);
|
||||
|
||||
// 2. Hardware rule: Color index 0 is always fully transparent.
|
||||
if px_index == 0 { continue; }
|
||||
if px_index == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Resolve the virtual index to a real RGB565 color using the tile's assigned palette.
|
||||
let color = bank.resolve_color(tile.palette_id, px_index);
|
||||
@ -538,7 +737,7 @@ impl Gfx {
|
||||
screen_w: usize,
|
||||
screen_h: usize,
|
||||
sprite: &Sprite,
|
||||
bank: &TileBank
|
||||
bank: &TileBank,
|
||||
) {
|
||||
// ... (same bounds/clipping calculation we already had) ...
|
||||
let size = bank.tile_size as usize;
|
||||
@ -559,7 +758,9 @@ impl Gfx {
|
||||
let px_index = bank.get_pixel_index(sprite.tile.id, fetch_x, fetch_y);
|
||||
|
||||
// 2. Transparency
|
||||
if px_index == 0 { continue; }
|
||||
if px_index == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Resolve color via palette (from the tile inside the sprite)
|
||||
let color = bank.resolve_color(sprite.tile.palette_id, px_index);
|
||||
@ -572,7 +773,9 @@ impl Gfx {
|
||||
/// Applies the fade effect to the entire back buffer.
|
||||
/// level: 0 (full color) to 31 (visible)
|
||||
fn apply_fade_to_buffer(back: &mut [u16], level: u8, fade_color: Color) {
|
||||
if level >= 31 { return; } // Fully visible, skip processing
|
||||
if level >= 31 {
|
||||
return;
|
||||
} // Fully visible, skip processing
|
||||
|
||||
let weight = level as u16;
|
||||
let inv_weight = 31 - weight;
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
use std::sync::Arc;
|
||||
use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge};
|
||||
use crate::asset::AssetManager;
|
||||
use crate::audio::Audio;
|
||||
use crate::gfx::Gfx;
|
||||
use crate::memory_banks::{MemoryBanks, SoundBankPoolAccess, SoundBankPoolInstaller, TileBankPoolAccess, TileBankPoolInstaller};
|
||||
use crate::memory_banks::{
|
||||
MemoryBanks, SoundBankPoolAccess, SoundBankPoolInstaller, TileBankPoolAccess,
|
||||
TileBankPoolInstaller,
|
||||
};
|
||||
use crate::pad::Pad;
|
||||
use crate::touch::Touch;
|
||||
use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Aggregate structure for all virtual hardware peripherals.
|
||||
///
|
||||
@ -33,20 +36,40 @@ pub struct Hardware {
|
||||
}
|
||||
|
||||
impl HardwareBridge for Hardware {
|
||||
fn gfx(&self) -> &dyn GfxBridge { &self.gfx }
|
||||
fn gfx_mut(&mut self) -> &mut dyn GfxBridge { &mut self.gfx }
|
||||
fn gfx(&self) -> &dyn GfxBridge {
|
||||
&self.gfx
|
||||
}
|
||||
fn gfx_mut(&mut self) -> &mut dyn GfxBridge {
|
||||
&mut self.gfx
|
||||
}
|
||||
|
||||
fn audio(&self) -> &dyn AudioBridge { &self.audio }
|
||||
fn audio_mut(&mut self) -> &mut dyn AudioBridge { &mut self.audio }
|
||||
fn audio(&self) -> &dyn AudioBridge {
|
||||
&self.audio
|
||||
}
|
||||
fn audio_mut(&mut self) -> &mut dyn AudioBridge {
|
||||
&mut self.audio
|
||||
}
|
||||
|
||||
fn pad(&self) -> &dyn PadBridge { &self.pad }
|
||||
fn pad_mut(&mut self) -> &mut dyn PadBridge { &mut self.pad }
|
||||
fn pad(&self) -> &dyn PadBridge {
|
||||
&self.pad
|
||||
}
|
||||
fn pad_mut(&mut self) -> &mut dyn PadBridge {
|
||||
&mut self.pad
|
||||
}
|
||||
|
||||
fn touch(&self) -> &dyn TouchBridge { &self.touch }
|
||||
fn touch_mut(&mut self) -> &mut dyn TouchBridge { &mut self.touch }
|
||||
fn touch(&self) -> &dyn TouchBridge {
|
||||
&self.touch
|
||||
}
|
||||
fn touch_mut(&mut self) -> &mut dyn TouchBridge {
|
||||
&mut self.touch
|
||||
}
|
||||
|
||||
fn assets(&self) -> &dyn AssetBridge { &self.assets }
|
||||
fn assets_mut(&mut self) -> &mut dyn AssetBridge { &mut self.assets }
|
||||
fn assets(&self) -> &dyn AssetBridge {
|
||||
&self.assets
|
||||
}
|
||||
fn assets_mut(&mut self) -> &mut dyn AssetBridge {
|
||||
&mut self.assets
|
||||
}
|
||||
}
|
||||
|
||||
impl Hardware {
|
||||
@ -59,7 +82,11 @@ impl Hardware {
|
||||
pub fn new() -> Self {
|
||||
let memory_banks = Arc::new(MemoryBanks::new());
|
||||
Self {
|
||||
gfx: Gfx::new(Self::W, Self::H, Arc::clone(&memory_banks) as Arc<dyn TileBankPoolAccess>),
|
||||
gfx: Gfx::new(
|
||||
Self::W,
|
||||
Self::H,
|
||||
Arc::clone(&memory_banks) as Arc<dyn TileBankPoolAccess>,
|
||||
),
|
||||
audio: Audio::new(Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolAccess>),
|
||||
pad: Pad::default(),
|
||||
touch: Touch::default(),
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
mod asset;
|
||||
mod audio;
|
||||
mod gfx;
|
||||
pub mod hardware;
|
||||
mod memory_banks;
|
||||
mod pad;
|
||||
mod touch;
|
||||
mod audio;
|
||||
mod memory_banks;
|
||||
pub mod hardware;
|
||||
|
||||
pub use crate::asset::AssetManager;
|
||||
pub use crate::audio::{Audio, AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use prometeu_hal::{InputSignals, PadBridge};
|
||||
use prometeu_hal::button::Button;
|
||||
use prometeu_hal::{InputSignals, PadBridge};
|
||||
|
||||
#[derive(Default, Clone, Copy, Debug)]
|
||||
pub struct Pad {
|
||||
@ -20,21 +20,49 @@ pub struct Pad {
|
||||
}
|
||||
|
||||
impl PadBridge for Pad {
|
||||
fn begin_frame(&mut self, signals: &InputSignals) { self.begin_frame(signals) }
|
||||
fn any(&self) -> bool { self.any() }
|
||||
fn begin_frame(&mut self, signals: &InputSignals) {
|
||||
self.begin_frame(signals)
|
||||
}
|
||||
fn any(&self) -> bool {
|
||||
self.any()
|
||||
}
|
||||
|
||||
fn up(&self) -> &Button { &self.up }
|
||||
fn down(&self) -> &Button { &self.down }
|
||||
fn left(&self) -> &Button { &self.left }
|
||||
fn right(&self) -> &Button { &self.right }
|
||||
fn a(&self) -> &Button { &self.a }
|
||||
fn b(&self) -> &Button { &self.b }
|
||||
fn x(&self) -> &Button { &self.x }
|
||||
fn y(&self) -> &Button { &self.y }
|
||||
fn l(&self) -> &Button { &self.l }
|
||||
fn r(&self) -> &Button { &self.r }
|
||||
fn start(&self) -> &Button { &self.start }
|
||||
fn select(&self) -> &Button { &self.select }
|
||||
fn up(&self) -> &Button {
|
||||
&self.up
|
||||
}
|
||||
fn down(&self) -> &Button {
|
||||
&self.down
|
||||
}
|
||||
fn left(&self) -> &Button {
|
||||
&self.left
|
||||
}
|
||||
fn right(&self) -> &Button {
|
||||
&self.right
|
||||
}
|
||||
fn a(&self) -> &Button {
|
||||
&self.a
|
||||
}
|
||||
fn b(&self) -> &Button {
|
||||
&self.b
|
||||
}
|
||||
fn x(&self) -> &Button {
|
||||
&self.x
|
||||
}
|
||||
fn y(&self) -> &Button {
|
||||
&self.y
|
||||
}
|
||||
fn l(&self) -> &Button {
|
||||
&self.l
|
||||
}
|
||||
fn r(&self) -> &Button {
|
||||
&self.r
|
||||
}
|
||||
fn start(&self) -> &Button {
|
||||
&self.start
|
||||
}
|
||||
fn select(&self) -> &Button {
|
||||
&self.select
|
||||
}
|
||||
}
|
||||
|
||||
impl Pad {
|
||||
@ -66,5 +94,3 @@ impl Pad {
|
||||
|| self.select.down
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
use prometeu_hal::telemetry::CertificationConfig;
|
||||
use prometeu_hal::{HardwareBridge, InputSignals};
|
||||
use prometeu_hal::cartridge::Cartridge;
|
||||
use prometeu_system::{PrometeuHub, VirtualMachineRuntime};
|
||||
use prometeu_vm::VirtualMachine;
|
||||
use crate::firmware::boot_target::BootTarget;
|
||||
use crate::firmware::firmware_state::{FirmwareState, LoadCartridgeStep, ResetStep};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use prometeu_hal::cartridge::Cartridge;
|
||||
use prometeu_hal::telemetry::CertificationConfig;
|
||||
use prometeu_hal::{HardwareBridge, InputSignals};
|
||||
use prometeu_system::{PrometeuHub, VirtualMachineRuntime};
|
||||
use prometeu_vm::VirtualMachine;
|
||||
|
||||
/// PROMETEU Firmware.
|
||||
///
|
||||
@ -76,7 +76,12 @@ impl Firmware {
|
||||
}
|
||||
|
||||
/// Transitions the system to a new state, handling lifecycle hooks.
|
||||
pub fn change_state(&mut self, new_state: FirmwareState, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
|
||||
pub fn change_state(
|
||||
&mut self,
|
||||
new_state: FirmwareState,
|
||||
signals: &InputSignals,
|
||||
hw: &mut dyn HardwareBridge,
|
||||
) {
|
||||
self.on_exit(signals, hw);
|
||||
self.state = new_state;
|
||||
self.state_initialized = false;
|
||||
@ -109,7 +114,11 @@ impl Firmware {
|
||||
|
||||
/// Dispatches the `on_update` event to the current state implementation.
|
||||
/// Returns an optional `FirmwareState` if a transition is requested.
|
||||
fn on_update(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) -> Option<FirmwareState> {
|
||||
fn on_update(
|
||||
&mut self,
|
||||
signals: &InputSignals,
|
||||
hw: &mut dyn HardwareBridge,
|
||||
) -> Option<FirmwareState> {
|
||||
let mut req = PrometeuContext {
|
||||
vm: &mut self.vm,
|
||||
os: &mut self.os,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
use crate::firmware::firmware_state::{FirmwareState, GameRunningStep, HubHomeStep};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
use prometeu_hal::cartridge::{AppMode, Cartridge};
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
use prometeu_hal::window::Rect;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -12,13 +12,18 @@ pub struct LoadCartridgeStep {
|
||||
|
||||
impl LoadCartridgeStep {
|
||||
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
|
||||
ctx.os.log(LogLevel::Info, LogSource::Pos, 0, format!("Loading cartridge: {}", self.cartridge.title));
|
||||
ctx.os.log(
|
||||
LogLevel::Info,
|
||||
LogSource::Pos,
|
||||
0,
|
||||
format!("Loading cartridge: {}", self.cartridge.title),
|
||||
);
|
||||
|
||||
// Initialize Asset Manager
|
||||
ctx.hw.assets_mut().initialize_for_cartridge(
|
||||
self.cartridge.asset_table.clone(),
|
||||
self.cartridge.preload.clone(),
|
||||
self.cartridge.assets.clone()
|
||||
self.cartridge.assets.clone(),
|
||||
);
|
||||
|
||||
ctx.os.initialize_vm(ctx.vm, &self.cartridge);
|
||||
@ -29,7 +34,7 @@ impl LoadCartridgeStep {
|
||||
let id = ctx.hub.window_manager.add_window(
|
||||
self.cartridge.title.clone(),
|
||||
Rect { x: 40, y: 20, w: 240, h: 140 },
|
||||
Color::WHITE
|
||||
Color::WHITE,
|
||||
);
|
||||
ctx.hub.window_manager.set_focus(id);
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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" {
|
||||
|
||||
@ -72,5 +72,4 @@ impl Color {
|
||||
let hex = r8 << 16 | g8 << 8 | b8;
|
||||
hex as i32
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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};
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
use crate::sample::Sample;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A container for audio assets.
|
||||
///
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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));
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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())),
|
||||
};
|
||||
|
||||
@ -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" }
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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(())
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
use prometeu_hal::syscalls::Syscall;
|
||||
use prometeu_bytecode::{decode_next, DecodeError};
|
||||
use prometeu_bytecode::FunctionMeta;
|
||||
use prometeu_bytecode::OpCode;
|
||||
use prometeu_bytecode::OpCodeSpecExt;
|
||||
use prometeu_bytecode::FunctionMeta;
|
||||
use prometeu_bytecode::{compute_function_layouts, FunctionLayout};
|
||||
use prometeu_bytecode::{DecodeError, decode_next};
|
||||
use prometeu_bytecode::{FunctionLayout, compute_function_layouts};
|
||||
use prometeu_hal::syscalls::Syscall;
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@ -71,14 +71,23 @@ impl Verifier {
|
||||
while pc < func_code.len() {
|
||||
valid_pc.insert(pc);
|
||||
let instr = decode_next(pc, func_code).map_err(|e| match e {
|
||||
DecodeError::UnknownOpcode { pc: _, opcode } =>
|
||||
VerifierError::UnknownOpcode { pc: func_start + pc, opcode },
|
||||
DecodeError::TruncatedOpcode { pc: _ } =>
|
||||
VerifierError::TruncatedOpcode { pc: func_start + pc },
|
||||
DecodeError::TruncatedImmediate { pc: _, opcode, need, have } =>
|
||||
VerifierError::TruncatedImmediate { pc: func_start + pc, opcode, need, have },
|
||||
DecodeError::ImmediateSizeMismatch { pc: _, opcode, expected, actual } =>
|
||||
VerifierError::TruncatedImmediate { pc: func_start + pc, opcode, need: expected, have: actual },
|
||||
DecodeError::UnknownOpcode { pc: _, opcode } => {
|
||||
VerifierError::UnknownOpcode { pc: func_start + pc, opcode }
|
||||
}
|
||||
DecodeError::TruncatedOpcode { pc: _ } => {
|
||||
VerifierError::TruncatedOpcode { pc: func_start + pc }
|
||||
}
|
||||
DecodeError::TruncatedImmediate { pc: _, opcode, need, have } => {
|
||||
VerifierError::TruncatedImmediate { pc: func_start + pc, opcode, need, have }
|
||||
}
|
||||
DecodeError::ImmediateSizeMismatch { pc: _, opcode, expected, actual } => {
|
||||
VerifierError::TruncatedImmediate {
|
||||
pc: func_start + pc,
|
||||
opcode,
|
||||
need: expected,
|
||||
have: actual,
|
||||
}
|
||||
}
|
||||
})?;
|
||||
pc = instr.next_pc;
|
||||
}
|
||||
@ -113,9 +122,7 @@ impl Verifier {
|
||||
})?;
|
||||
(callee.param_slots, callee.return_slots)
|
||||
}
|
||||
OpCode::Ret => {
|
||||
(func.return_slots, 0)
|
||||
}
|
||||
OpCode::Ret => (func.return_slots, 0),
|
||||
OpCode::Syscall => {
|
||||
let id = instr.imm_u32().unwrap();
|
||||
let syscall = Syscall::from_u32(id).ok_or_else(|| {
|
||||
@ -127,7 +134,10 @@ impl Verifier {
|
||||
};
|
||||
|
||||
if in_height < pops {
|
||||
return Err(VerifierError::StackUnderflow { pc: func_start + pc, opcode: instr.opcode });
|
||||
return Err(VerifierError::StackUnderflow {
|
||||
pc: func_start + pc,
|
||||
opcode: instr.opcode,
|
||||
});
|
||||
}
|
||||
|
||||
let out_height = in_height - pops + pushes;
|
||||
@ -135,7 +145,11 @@ impl Verifier {
|
||||
|
||||
if instr.opcode == OpCode::Ret {
|
||||
if in_height != func.return_slots {
|
||||
return Err(VerifierError::BadRetStackHeight { pc: func_start + pc, height: in_height, expected: func.return_slots });
|
||||
return Err(VerifierError::BadRetStackHeight {
|
||||
pc: func_start + pc,
|
||||
height: in_height,
|
||||
expected: func.return_slots,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,7 +157,8 @@ impl Verifier {
|
||||
if spec.is_branch {
|
||||
// Canonical contract: branch immediate is RELATIVE to function start.
|
||||
let target_rel = instr.imm_u32().unwrap() as usize;
|
||||
let func_end_abs = layouts.get(func_idx).map(|l| l.end).unwrap_or_else(|| code.len());
|
||||
let func_end_abs =
|
||||
layouts.get(func_idx).map(|l| l.end).unwrap_or_else(|| code.len());
|
||||
let func_len = func_end_abs - func_start;
|
||||
|
||||
if target_rel > func_len {
|
||||
@ -168,23 +183,38 @@ impl Verifier {
|
||||
target_abs_expected,
|
||||
is_boundary_target_rel
|
||||
);
|
||||
return Err(VerifierError::InvalidJumpTarget { pc: pc_abs, target: target_abs_expected });
|
||||
return Err(VerifierError::InvalidJumpTarget {
|
||||
pc: pc_abs,
|
||||
target: target_abs_expected,
|
||||
});
|
||||
}
|
||||
|
||||
if target_rel == func_len {
|
||||
// salto para o fim da função
|
||||
if out_height != func.return_slots {
|
||||
return Err(VerifierError::BadRetStackHeight { pc: func_start + pc, height: out_height, expected: func.return_slots });
|
||||
return Err(VerifierError::BadRetStackHeight {
|
||||
pc: func_start + pc,
|
||||
height: out_height,
|
||||
expected: func.return_slots,
|
||||
});
|
||||
}
|
||||
// caminho termina aqui
|
||||
} else {
|
||||
if !valid_pc.contains(&target_rel) {
|
||||
return Err(VerifierError::JumpToMidInstruction { pc: func_start + pc, target: func_start + target_rel });
|
||||
return Err(VerifierError::JumpToMidInstruction {
|
||||
pc: func_start + pc,
|
||||
target: func_start + target_rel,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(&existing_height) = stack_height_in.get(&target_rel) {
|
||||
if existing_height != out_height {
|
||||
return Err(VerifierError::StackMismatchJoin { pc: func_start + pc, target: func_start + target_rel, height_in: out_height, height_target: existing_height });
|
||||
return Err(VerifierError::StackMismatchJoin {
|
||||
pc: func_start + pc,
|
||||
target: func_start + target_rel,
|
||||
height_in: out_height,
|
||||
height_target: existing_height,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
stack_height_in.insert(target_rel, out_height);
|
||||
@ -199,7 +229,12 @@ impl Verifier {
|
||||
if next_pc < func_len {
|
||||
if let Some(&existing_height) = stack_height_in.get(&next_pc) {
|
||||
if existing_height != out_height {
|
||||
return Err(VerifierError::StackMismatchJoin { pc: func_start + pc, target: func_start + next_pc, height_in: out_height, height_target: existing_height });
|
||||
return Err(VerifierError::StackMismatchJoin {
|
||||
pc: func_start + pc,
|
||||
target: func_start + next_pc,
|
||||
height_in: out_height,
|
||||
height_target: existing_height,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
stack_height_in.insert(next_pc, out_height);
|
||||
@ -221,18 +256,15 @@ mod tests {
|
||||
fn test_verifier_underflow() {
|
||||
// OpCode::Add (2 bytes)
|
||||
let code = vec![OpCode::Add as u8, 0x00];
|
||||
let functions = vec![FunctionMeta {
|
||||
code_offset: 0,
|
||||
code_len: 2,
|
||||
..Default::default()
|
||||
}];
|
||||
let functions = vec![FunctionMeta { code_offset: 0, code_len: 2, ..Default::default() }];
|
||||
let res = Verifier::verify(&code, &functions);
|
||||
assert_eq!(res, Err(VerifierError::StackUnderflow { pc: 0, opcode: OpCode::Add }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verifier_dup_underflow() {
|
||||
let code = vec![(OpCode::Dup as u16).to_le_bytes()[0], (OpCode::Dup as u16).to_le_bytes()[1]];
|
||||
let code =
|
||||
vec![(OpCode::Dup as u16).to_le_bytes()[0], (OpCode::Dup as u16).to_le_bytes()[1]];
|
||||
let functions = vec![FunctionMeta { code_offset: 0, code_len: 2, ..Default::default() }];
|
||||
let res = Verifier::verify(&code, &functions);
|
||||
assert_eq!(res, Err(VerifierError::StackUnderflow { pc: 0, opcode: OpCode::Dup }));
|
||||
@ -243,11 +275,7 @@ mod tests {
|
||||
// Jmp (2 bytes) + 100u32 (4 bytes)
|
||||
let mut code = vec![OpCode::Jmp as u8, 0x00];
|
||||
code.extend_from_slice(&100u32.to_le_bytes());
|
||||
let functions = vec![FunctionMeta {
|
||||
code_offset: 0,
|
||||
code_len: 6,
|
||||
..Default::default()
|
||||
}];
|
||||
let functions = vec![FunctionMeta { code_offset: 0, code_len: 6, ..Default::default() }];
|
||||
let res = Verifier::verify(&code, &functions);
|
||||
assert_eq!(res, Err(VerifierError::InvalidJumpTarget { pc: 0, target: 100 }));
|
||||
}
|
||||
@ -262,11 +290,7 @@ mod tests {
|
||||
code.push(0x00);
|
||||
code.extend_from_slice(&1u32.to_le_bytes());
|
||||
|
||||
let functions = vec![FunctionMeta {
|
||||
code_offset: 0,
|
||||
code_len: 12,
|
||||
..Default::default()
|
||||
}];
|
||||
let functions = vec![FunctionMeta { code_offset: 0, code_len: 12, ..Default::default() }];
|
||||
let res = Verifier::verify(&code, &functions);
|
||||
assert_eq!(res, Err(VerifierError::JumpToMidInstruction { pc: 6, target: 1 }));
|
||||
}
|
||||
@ -295,11 +319,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_verifier_truncation_opcode() {
|
||||
let code = vec![OpCode::PushI32 as u8]; // Truncated u16 opcode
|
||||
let functions = vec![FunctionMeta {
|
||||
code_offset: 0,
|
||||
code_len: 1,
|
||||
..Default::default()
|
||||
}];
|
||||
let functions = vec![FunctionMeta { code_offset: 0, code_len: 1, ..Default::default() }];
|
||||
let res = Verifier::verify(&code, &functions);
|
||||
assert_eq!(res, Err(VerifierError::TruncatedOpcode { pc: 0 }));
|
||||
}
|
||||
@ -308,13 +328,17 @@ mod tests {
|
||||
fn test_verifier_truncation_immediate() {
|
||||
let mut code = vec![OpCode::PushI32 as u8, 0x00];
|
||||
code.push(0x42); // Only 1 byte of 4-byte immediate
|
||||
let functions = vec![FunctionMeta {
|
||||
code_offset: 0,
|
||||
code_len: 3,
|
||||
..Default::default()
|
||||
}];
|
||||
let functions = vec![FunctionMeta { code_offset: 0, code_len: 3, ..Default::default() }];
|
||||
let res = Verifier::verify(&code, &functions);
|
||||
assert_eq!(res, Err(VerifierError::TruncatedImmediate { pc: 0, opcode: OpCode::PushI32, need: 4, have: 1 }));
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(VerifierError::TruncatedImmediate {
|
||||
pc: 0,
|
||||
opcode: OpCode::PushI32,
|
||||
need: 4,
|
||||
have: 1
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -328,24 +352,39 @@ mod tests {
|
||||
// 27: Nop
|
||||
|
||||
let mut code = Vec::new();
|
||||
code.push(OpCode::PushBool as u8); code.push(0x00); code.push(1); // 0: PushBool (3 bytes)
|
||||
code.push(OpCode::JmpIfTrue as u8); code.push(0x00); code.extend_from_slice(&15u32.to_le_bytes()); // 3: JmpIfTrue (6 bytes)
|
||||
code.push(OpCode::Jmp as u8); code.push(0x00); code.extend_from_slice(&27u32.to_le_bytes()); // 9: Jmp (6 bytes)
|
||||
code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes()); // 15: PushI32 (6 bytes)
|
||||
code.push(OpCode::Jmp as u8); code.push(0x00); code.extend_from_slice(&27u32.to_le_bytes()); // 21: Jmp (6 bytes)
|
||||
code.push(OpCode::Nop as u8); code.push(0x00); // 27: Nop (2 bytes)
|
||||
code.push(OpCode::PushBool as u8);
|
||||
code.push(0x00);
|
||||
code.push(1); // 0: PushBool (3 bytes)
|
||||
code.push(OpCode::JmpIfTrue as u8);
|
||||
code.push(0x00);
|
||||
code.extend_from_slice(&15u32.to_le_bytes()); // 3: JmpIfTrue (6 bytes)
|
||||
code.push(OpCode::Jmp as u8);
|
||||
code.push(0x00);
|
||||
code.extend_from_slice(&27u32.to_le_bytes()); // 9: Jmp (6 bytes)
|
||||
code.push(OpCode::PushI32 as u8);
|
||||
code.push(0x00);
|
||||
code.extend_from_slice(&1u32.to_le_bytes()); // 15: PushI32 (6 bytes)
|
||||
code.push(OpCode::Jmp as u8);
|
||||
code.push(0x00);
|
||||
code.extend_from_slice(&27u32.to_le_bytes()); // 21: Jmp (6 bytes)
|
||||
code.push(OpCode::Nop as u8);
|
||||
code.push(0x00); // 27: Nop (2 bytes)
|
||||
|
||||
let functions = vec![FunctionMeta {
|
||||
code_offset: 0,
|
||||
code_len: 29,
|
||||
..Default::default()
|
||||
}];
|
||||
let functions = vec![FunctionMeta { code_offset: 0, code_len: 29, ..Default::default() }];
|
||||
let res = Verifier::verify(&code, &functions);
|
||||
// Path 0->3->9->27: height 1-1+0 = 0.
|
||||
// Path 0->3->15->21->27: height 1-1+1 = 1.
|
||||
// Mismatch at 27: 0 vs 1.
|
||||
|
||||
assert_eq!(res, Err(VerifierError::StackMismatchJoin { pc: 21, target: 27, height_in: 1, height_target: 0 }));
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(VerifierError::StackMismatchJoin {
|
||||
pc: 21,
|
||||
target: 27,
|
||||
height_in: 1,
|
||||
height_target: 0
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -374,10 +413,16 @@ mod tests {
|
||||
// Add
|
||||
// Ret
|
||||
let mut code = Vec::new();
|
||||
code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes());
|
||||
code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&2u32.to_le_bytes());
|
||||
code.push(OpCode::Add as u8); code.push(0x00);
|
||||
code.push(OpCode::Ret as u8); code.push(0x00);
|
||||
code.push(OpCode::PushI32 as u8);
|
||||
code.push(0x00);
|
||||
code.extend_from_slice(&1u32.to_le_bytes());
|
||||
code.push(OpCode::PushI32 as u8);
|
||||
code.push(0x00);
|
||||
code.extend_from_slice(&2u32.to_le_bytes());
|
||||
code.push(OpCode::Add as u8);
|
||||
code.push(0x00);
|
||||
code.push(OpCode::Ret as u8);
|
||||
code.push(0x00);
|
||||
|
||||
let functions = vec![FunctionMeta {
|
||||
code_offset: 0,
|
||||
@ -392,14 +437,11 @@ mod tests {
|
||||
#[test]
|
||||
fn test_verifier_invalid_syscall_id() {
|
||||
let mut code = Vec::new();
|
||||
code.push(OpCode::Syscall as u8); code.push(0x00);
|
||||
code.push(OpCode::Syscall as u8);
|
||||
code.push(0x00);
|
||||
code.extend_from_slice(&0xDEADBEEFu32.to_le_bytes()); // Unknown ID
|
||||
|
||||
let functions = vec![FunctionMeta {
|
||||
code_offset: 0,
|
||||
code_len: 6,
|
||||
..Default::default()
|
||||
}];
|
||||
let functions = vec![FunctionMeta { code_offset: 0, code_len: 6, ..Default::default() }];
|
||||
let res = Verifier::verify(&code, &functions);
|
||||
assert_eq!(res, Err(VerifierError::InvalidSyscallId { pc: 0, id: 0xDEADBEEF }));
|
||||
}
|
||||
|
||||
@ -3,10 +3,13 @@ use crate::scope_frame::ScopeFrame;
|
||||
use crate::verifier::Verifier;
|
||||
use crate::vm_init_error::VmInitError;
|
||||
use crate::{HostContext, NativeInterface};
|
||||
use prometeu_bytecode::{TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DEAD_GATE, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_INVALID_GATE, TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_STACK_UNDERFLOW, TRAP_TYPE};
|
||||
use prometeu_bytecode::OpCode;
|
||||
use prometeu_bytecode::ProgramImage;
|
||||
use prometeu_bytecode::Value;
|
||||
use prometeu_bytecode::{
|
||||
TRAP_BAD_RET_SLOTS, TRAP_DEAD_GATE, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_INVALID_GATE,
|
||||
TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_STACK_UNDERFLOW, TRAP_TYPE, TrapInfo,
|
||||
};
|
||||
use prometeu_hal::vm_fault::VmFault;
|
||||
|
||||
/// Reason why the Virtual Machine stopped execution during a specific run.
|
||||
@ -126,7 +129,13 @@ impl VirtualMachine {
|
||||
call_stack: Vec::new(),
|
||||
scope_stack: Vec::new(),
|
||||
globals: Vec::new(),
|
||||
program: ProgramImage::new(rom, constant_pool, vec![], None, std::collections::HashMap::new()),
|
||||
program: ProgramImage::new(
|
||||
rom,
|
||||
constant_pool,
|
||||
vec![],
|
||||
None,
|
||||
std::collections::HashMap::new(),
|
||||
),
|
||||
heap: Vec::new(),
|
||||
gate_pool: Vec::new(),
|
||||
cycles: 0,
|
||||
@ -137,7 +146,11 @@ impl VirtualMachine {
|
||||
|
||||
/// Resets the VM state and loads a new program.
|
||||
/// This is typically called by the Firmware when starting a new App/Cartridge.
|
||||
pub fn initialize(&mut self, program_bytes: Vec<u8>, entrypoint: &str) -> Result<(), VmInitError> {
|
||||
pub fn initialize(
|
||||
&mut self,
|
||||
program_bytes: Vec<u8>,
|
||||
entrypoint: &str,
|
||||
) -> Result<(), VmInitError> {
|
||||
// Fail fast: reset state upfront. If we return early with an error,
|
||||
// the VM is left in a "halted and empty" state.
|
||||
self.program = ProgramImage::default();
|
||||
@ -169,7 +182,9 @@ impl VirtualMachine {
|
||||
|
||||
program
|
||||
}
|
||||
Err(prometeu_bytecode::LoadError::InvalidVersion) => return Err(VmInitError::UnsupportedFormat),
|
||||
Err(prometeu_bytecode::LoadError::InvalidVersion) => {
|
||||
return Err(VmInitError::UnsupportedFormat);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(VmInitError::ImageLoadFailed(e));
|
||||
}
|
||||
@ -182,13 +197,17 @@ impl VirtualMachine {
|
||||
let pc = if entrypoint.is_empty() {
|
||||
program.functions.get(0).map(|f| f.code_offset as usize).unwrap_or(0)
|
||||
} else if let Ok(func_idx) = entrypoint.parse::<usize>() {
|
||||
program.functions.get(func_idx)
|
||||
program
|
||||
.functions
|
||||
.get(func_idx)
|
||||
.map(|f| f.code_offset as usize)
|
||||
.ok_or(VmInitError::EntrypointNotFound)?
|
||||
} else {
|
||||
// Try to resolve as a symbol name from the exports map
|
||||
if let Some(&func_idx) = program.exports.get(entrypoint) {
|
||||
program.functions.get(func_idx as usize)
|
||||
program
|
||||
.functions
|
||||
.get(func_idx as usize)
|
||||
.map(|f| f.code_offset as usize)
|
||||
.ok_or(VmInitError::EntrypointNotFound)?
|
||||
} else {
|
||||
@ -211,9 +230,7 @@ impl VirtualMachine {
|
||||
idx
|
||||
} else {
|
||||
// Try to resolve as a symbol name
|
||||
self.program.exports.get(entrypoint)
|
||||
.map(|&idx| idx as usize)
|
||||
.ok_or(()).unwrap_or(0) // Default to 0 if not found
|
||||
self.program.exports.get(entrypoint).map(|&idx| idx as usize).ok_or(()).unwrap_or(0) // Default to 0 if not found
|
||||
};
|
||||
|
||||
let callee = self.program.functions.get(func_idx).cloned().unwrap_or_default();
|
||||
@ -311,7 +328,10 @@ impl VirtualMachine {
|
||||
// Integrity check: ensure real progress is being made to avoid infinite loops
|
||||
// caused by zero-cycle instructions or stuck PC.
|
||||
if self.pc == pc_before && self.cycles == cycles_before && !self.halted {
|
||||
ending_reason = Some(LogicalFrameEndingReason::Panic(format!("VM stuck at PC 0x{:08X}", self.pc)));
|
||||
ending_reason = Some(LogicalFrameEndingReason::Panic(format!(
|
||||
"VM stuck at PC 0x{:08X}",
|
||||
self.pc
|
||||
)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -339,10 +359,7 @@ impl VirtualMachine {
|
||||
if self.pc + 2 > self.program.rom.len() {
|
||||
return Err("Unexpected end of ROM".into());
|
||||
}
|
||||
let bytes = [
|
||||
self.program.rom[self.pc],
|
||||
self.program.rom[self.pc + 1],
|
||||
];
|
||||
let bytes = [self.program.rom[self.pc], self.program.rom[self.pc + 1]];
|
||||
Ok(u16::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
@ -352,7 +369,11 @@ impl VirtualMachine {
|
||||
/// 1. Fetch: Read the opcode from memory.
|
||||
/// 2. Decode: Identify what operation to perform.
|
||||
/// 3. Execute: Perform the operation, updating stacks, memory, or calling peripherals.
|
||||
pub fn step(&mut self, native: &mut dyn NativeInterface, ctx: &mut HostContext) -> Result<(), LogicalFrameEndingReason> {
|
||||
pub fn step(
|
||||
&mut self,
|
||||
native: &mut dyn NativeInterface,
|
||||
ctx: &mut HostContext,
|
||||
) -> Result<(), LogicalFrameEndingReason> {
|
||||
if self.halted || self.pc >= self.program.rom.len() {
|
||||
return Ok(());
|
||||
}
|
||||
@ -373,35 +394,66 @@ impl VirtualMachine {
|
||||
self.halted = true;
|
||||
}
|
||||
OpCode::Jmp => {
|
||||
let target = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize;
|
||||
let func_start = self.call_stack.last().map(|f| self.program.functions[f.func_idx].code_offset as usize).unwrap_or(0);
|
||||
let target = instr
|
||||
.imm_u32()
|
||||
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
|
||||
as usize;
|
||||
let func_start = self
|
||||
.call_stack
|
||||
.last()
|
||||
.map(|f| self.program.functions[f.func_idx].code_offset as usize)
|
||||
.unwrap_or(0);
|
||||
self.pc = func_start + target;
|
||||
}
|
||||
OpCode::JmpIfFalse => {
|
||||
let target = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize;
|
||||
let target = instr
|
||||
.imm_u32()
|
||||
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
|
||||
as usize;
|
||||
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||
match val {
|
||||
Value::Boolean(false) => {
|
||||
let func_start = self.call_stack.last().map(|f| self.program.functions[f.func_idx].code_offset as usize).unwrap_or(0);
|
||||
let func_start = self
|
||||
.call_stack
|
||||
.last()
|
||||
.map(|f| self.program.functions[f.func_idx].code_offset as usize)
|
||||
.unwrap_or(0);
|
||||
self.pc = func_start + target;
|
||||
}
|
||||
Value::Boolean(true) => {}
|
||||
_ => {
|
||||
return Err(self.trap(TRAP_TYPE, opcode as u16, format!("Expected boolean for JMP_IF_FALSE, got {:?}", val), start_pc as u32));
|
||||
return Err(self.trap(
|
||||
TRAP_TYPE,
|
||||
opcode as u16,
|
||||
format!("Expected boolean for JMP_IF_FALSE, got {:?}", val),
|
||||
start_pc as u32,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
OpCode::JmpIfTrue => {
|
||||
let target = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize;
|
||||
let target = instr
|
||||
.imm_u32()
|
||||
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
|
||||
as usize;
|
||||
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||
match val {
|
||||
Value::Boolean(true) => {
|
||||
let func_start = self.call_stack.last().map(|f| self.program.functions[f.func_idx].code_offset as usize).unwrap_or(0);
|
||||
let func_start = self
|
||||
.call_stack
|
||||
.last()
|
||||
.map(|f| self.program.functions[f.func_idx].code_offset as usize)
|
||||
.unwrap_or(0);
|
||||
self.pc = func_start + target;
|
||||
}
|
||||
Value::Boolean(false) => {}
|
||||
_ => {
|
||||
return Err(self.trap(TRAP_TYPE, opcode as u16, format!("Expected boolean for JMP_IF_TRUE, got {:?}", val), start_pc as u32));
|
||||
return Err(self.trap(
|
||||
TRAP_TYPE,
|
||||
opcode as u16,
|
||||
format!("Expected boolean for JMP_IF_TRUE, got {:?}", val),
|
||||
start_pc as u32,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -409,38 +461,60 @@ impl VirtualMachine {
|
||||
// Handled in run_budget for interruption
|
||||
}
|
||||
OpCode::PushConst => {
|
||||
let idx = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize;
|
||||
let val = self.program.constant_pool.get(idx).cloned().ok_or_else(|| LogicalFrameEndingReason::Panic("Invalid constant index".into()))?;
|
||||
let idx = instr
|
||||
.imm_u32()
|
||||
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
|
||||
as usize;
|
||||
let val = self.program.constant_pool.get(idx).cloned().ok_or_else(|| {
|
||||
LogicalFrameEndingReason::Panic("Invalid constant index".into())
|
||||
})?;
|
||||
self.push(val);
|
||||
}
|
||||
OpCode::PushI64 => {
|
||||
let val = instr.imm_i64().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
|
||||
let val = instr
|
||||
.imm_i64()
|
||||
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
|
||||
self.push(Value::Int64(val));
|
||||
}
|
||||
OpCode::PushI32 => {
|
||||
let val = instr.imm_i32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
|
||||
let val = instr
|
||||
.imm_i32()
|
||||
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
|
||||
self.push(Value::Int32(val));
|
||||
}
|
||||
OpCode::PushBounded => {
|
||||
let val = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
|
||||
let val = instr
|
||||
.imm_u32()
|
||||
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
|
||||
if val > 0xFFFF {
|
||||
return Err(self.trap(TRAP_OOB, opcode as u16, format!("Bounded value overflow: {} > 0xFFFF", val), start_pc as u32));
|
||||
return Err(self.trap(
|
||||
TRAP_OOB,
|
||||
opcode as u16,
|
||||
format!("Bounded value overflow: {} > 0xFFFF", val),
|
||||
start_pc as u32,
|
||||
));
|
||||
}
|
||||
self.push(Value::Bounded(val));
|
||||
}
|
||||
OpCode::PushF64 => {
|
||||
let val = instr.imm_f64().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
|
||||
let val = instr
|
||||
.imm_f64()
|
||||
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
|
||||
self.push(Value::Float(val));
|
||||
}
|
||||
OpCode::PushBool => {
|
||||
let val = instr.imm_u8().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
|
||||
let val = instr
|
||||
.imm_u8()
|
||||
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
|
||||
self.push(Value::Boolean(val != 0));
|
||||
}
|
||||
OpCode::Pop => {
|
||||
self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||
}
|
||||
OpCode::PopN => {
|
||||
let n = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
|
||||
let n = instr
|
||||
.imm_u32()
|
||||
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
|
||||
for _ in 0..n {
|
||||
self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||
}
|
||||
@ -461,7 +535,9 @@ impl VirtualMachine {
|
||||
}
|
||||
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_add(*b))),
|
||||
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_add(*b))),
|
||||
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((*a as i64).wrapping_add(*b))),
|
||||
(Value::Int32(a), Value::Int64(b)) => {
|
||||
Ok(Value::Int64((*a as i64).wrapping_add(*b)))
|
||||
}
|
||||
(Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_add(*b as i64))),
|
||||
(Value::Float(a), Value::Float(b)) => Ok(Value::Float(a + b)),
|
||||
(Value::Int32(a), Value::Float(b)) => Ok(Value::Float(*a as f64 + b)),
|
||||
@ -471,7 +547,10 @@ impl VirtualMachine {
|
||||
(Value::Bounded(a), Value::Bounded(b)) => {
|
||||
let res = a.saturating_add(*b);
|
||||
if res > 0xFFFF {
|
||||
Err(OpError::Trap(TRAP_OOB, format!("Bounded addition overflow: {} + {} = {}", a, b, res)))
|
||||
Err(OpError::Trap(
|
||||
TRAP_OOB,
|
||||
format!("Bounded addition overflow: {} + {} = {}", a, b, res),
|
||||
))
|
||||
} else {
|
||||
Ok(Value::Bounded(res))
|
||||
}
|
||||
@ -490,7 +569,10 @@ impl VirtualMachine {
|
||||
(Value::Float(a), Value::Int64(b)) => Ok(Value::Float(a - b as f64)),
|
||||
(Value::Bounded(a), Value::Bounded(b)) => {
|
||||
if a < b {
|
||||
Err(OpError::Trap(TRAP_OOB, format!("Bounded subtraction underflow: {} - {} < 0", a, b)))
|
||||
Err(OpError::Trap(
|
||||
TRAP_OOB,
|
||||
format!("Bounded subtraction underflow: {} - {} < 0", a, b),
|
||||
))
|
||||
} else {
|
||||
Ok(Value::Bounded(a - b))
|
||||
}
|
||||
@ -510,7 +592,10 @@ impl VirtualMachine {
|
||||
(Value::Bounded(a), Value::Bounded(b)) => {
|
||||
let res = a as u64 * b as u64;
|
||||
if res > 0xFFFF {
|
||||
Err(OpError::Trap(TRAP_OOB, format!("Bounded multiplication overflow: {} * {} = {}", a, b, res)))
|
||||
Err(OpError::Trap(
|
||||
TRAP_OOB,
|
||||
format!("Bounded multiplication overflow: {} * {} = {}", a, b, res),
|
||||
))
|
||||
} else {
|
||||
Ok(Value::Bounded(res as u32))
|
||||
}
|
||||
@ -520,25 +605,37 @@ impl VirtualMachine {
|
||||
OpCode::Div => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
|
||||
(Value::Int32(a), Value::Int32(b)) => {
|
||||
if b == 0 {
|
||||
return Err(OpError::Trap(TRAP_DIV_ZERO, "Integer division by zero".into()));
|
||||
return Err(OpError::Trap(
|
||||
TRAP_DIV_ZERO,
|
||||
"Integer division by zero".into(),
|
||||
));
|
||||
}
|
||||
Ok(Value::Int32(a / b))
|
||||
}
|
||||
(Value::Int64(a), Value::Int64(b)) => {
|
||||
if b == 0 {
|
||||
return Err(OpError::Trap(TRAP_DIV_ZERO, "Integer division by zero".into()));
|
||||
return Err(OpError::Trap(
|
||||
TRAP_DIV_ZERO,
|
||||
"Integer division by zero".into(),
|
||||
));
|
||||
}
|
||||
Ok(Value::Int64(a / b))
|
||||
}
|
||||
(Value::Int32(a), Value::Int64(b)) => {
|
||||
if b == 0 {
|
||||
return Err(OpError::Trap(TRAP_DIV_ZERO, "Integer division by zero".into()));
|
||||
return Err(OpError::Trap(
|
||||
TRAP_DIV_ZERO,
|
||||
"Integer division by zero".into(),
|
||||
));
|
||||
}
|
||||
Ok(Value::Int64(a as i64 / b))
|
||||
}
|
||||
(Value::Int64(a), Value::Int32(b)) => {
|
||||
if b == 0 {
|
||||
return Err(OpError::Trap(TRAP_DIV_ZERO, "Integer division by zero".into()));
|
||||
return Err(OpError::Trap(
|
||||
TRAP_DIV_ZERO,
|
||||
"Integer division by zero".into(),
|
||||
));
|
||||
}
|
||||
Ok(Value::Int64(a / b as i64))
|
||||
}
|
||||
@ -574,7 +671,10 @@ impl VirtualMachine {
|
||||
}
|
||||
(Value::Bounded(a), Value::Bounded(b)) => {
|
||||
if b == 0 {
|
||||
return Err(OpError::Trap(TRAP_DIV_ZERO, "Bounded division by zero".into()));
|
||||
return Err(OpError::Trap(
|
||||
TRAP_DIV_ZERO,
|
||||
"Bounded division by zero".into(),
|
||||
));
|
||||
}
|
||||
Ok(Value::Bounded(a / b))
|
||||
}
|
||||
@ -606,19 +706,34 @@ impl VirtualMachine {
|
||||
if let Value::Bounded(b) = val {
|
||||
self.push(Value::Int64(b as i64));
|
||||
} else {
|
||||
return Err(LogicalFrameEndingReason::Panic("Expected bounded for BOUND_TO_INT".into()));
|
||||
return Err(LogicalFrameEndingReason::Panic(
|
||||
"Expected bounded for BOUND_TO_INT".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
OpCode::IntToBoundChecked => {
|
||||
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||
let int_val = val.as_integer().ok_or_else(|| LogicalFrameEndingReason::Panic("Expected integer for INT_TO_BOUND_CHECKED".into()))?;
|
||||
let int_val = val.as_integer().ok_or_else(|| {
|
||||
LogicalFrameEndingReason::Panic(
|
||||
"Expected integer for INT_TO_BOUND_CHECKED".into(),
|
||||
)
|
||||
})?;
|
||||
if int_val < 0 || int_val > 0xFFFF {
|
||||
return Err(self.trap(TRAP_OOB, OpCode::IntToBoundChecked as u16, format!("Integer to bounded conversion out of range: {}", int_val), start_pc as u32));
|
||||
return Err(self.trap(
|
||||
TRAP_OOB,
|
||||
OpCode::IntToBoundChecked as u16,
|
||||
format!("Integer to bounded conversion out of range: {}", int_val),
|
||||
start_pc as u32,
|
||||
));
|
||||
}
|
||||
self.push(Value::Bounded(int_val as u32));
|
||||
}
|
||||
OpCode::Eq => self.binary_op(opcode, start_pc as u32, |a, b| Ok(Value::Boolean(a == b)))?,
|
||||
OpCode::Neq => self.binary_op(opcode, start_pc as u32, |a, b| Ok(Value::Boolean(a != b)))?,
|
||||
OpCode::Eq => {
|
||||
self.binary_op(opcode, start_pc as u32, |a, b| Ok(Value::Boolean(a == b)))?
|
||||
}
|
||||
OpCode::Neq => {
|
||||
self.binary_op(opcode, start_pc as u32, |a, b| Ok(Value::Boolean(a != b)))?
|
||||
}
|
||||
OpCode::Lt => self.binary_op(opcode, start_pc as u32, |a, b| {
|
||||
a.partial_cmp(&b)
|
||||
.map(|o| Value::Boolean(o == std::cmp::Ordering::Less))
|
||||
@ -679,14 +794,18 @@ impl VirtualMachine {
|
||||
OpCode::Shl => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
|
||||
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_shl(b as u32))),
|
||||
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_shl(b as u32))),
|
||||
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_shl(b as u32))),
|
||||
(Value::Int32(a), Value::Int64(b)) => {
|
||||
Ok(Value::Int64((a as i64).wrapping_shl(b as u32)))
|
||||
}
|
||||
(Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_shl(b as u32))),
|
||||
_ => Err(OpError::Panic("Invalid types for Shl".into())),
|
||||
})?,
|
||||
OpCode::Shr => self.binary_op(opcode, start_pc as u32, |a, b| match (a, b) {
|
||||
(Value::Int32(a), Value::Int32(b)) => Ok(Value::Int32(a.wrapping_shr(b as u32))),
|
||||
(Value::Int64(a), Value::Int64(b)) => Ok(Value::Int64(a.wrapping_shr(b as u32))),
|
||||
(Value::Int32(a), Value::Int64(b)) => Ok(Value::Int64((a as i64).wrapping_shr(b as u32))),
|
||||
(Value::Int32(a), Value::Int64(b)) => {
|
||||
Ok(Value::Int64((a as i64).wrapping_shr(b as u32)))
|
||||
}
|
||||
(Value::Int64(a), Value::Int32(b)) => Ok(Value::Int64(a.wrapping_shr(b as u32))),
|
||||
_ => Err(OpError::Panic("Invalid types for Shr".into())),
|
||||
})?,
|
||||
@ -696,16 +815,26 @@ impl VirtualMachine {
|
||||
Value::Int32(a) => self.push(Value::Int32(a.wrapping_neg())),
|
||||
Value::Int64(a) => self.push(Value::Int64(a.wrapping_neg())),
|
||||
Value::Float(a) => self.push(Value::Float(-a)),
|
||||
_ => return Err(LogicalFrameEndingReason::Panic("Invalid type for Neg".into())),
|
||||
_ => {
|
||||
return Err(LogicalFrameEndingReason::Panic("Invalid type for Neg".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
OpCode::GetGlobal => {
|
||||
let idx = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize;
|
||||
let val = self.globals.get(idx).cloned().ok_or_else(|| LogicalFrameEndingReason::Panic("Invalid global index".into()))?;
|
||||
let idx = instr
|
||||
.imm_u32()
|
||||
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
|
||||
as usize;
|
||||
let val = self.globals.get(idx).cloned().ok_or_else(|| {
|
||||
LogicalFrameEndingReason::Panic("Invalid global index".into())
|
||||
})?;
|
||||
self.push(val);
|
||||
}
|
||||
OpCode::SetGlobal => {
|
||||
let idx = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize;
|
||||
let idx = instr
|
||||
.imm_u32()
|
||||
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
|
||||
as usize;
|
||||
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||
if idx >= self.globals.len() {
|
||||
self.globals.resize(idx + 1, Value::Null);
|
||||
@ -713,39 +842,75 @@ impl VirtualMachine {
|
||||
self.globals[idx] = val;
|
||||
}
|
||||
OpCode::GetLocal => {
|
||||
let slot = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
|
||||
let frame = self.call_stack.last().ok_or_else(|| LogicalFrameEndingReason::Panic("No active call frame".into()))?;
|
||||
let slot = instr
|
||||
.imm_u32()
|
||||
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
|
||||
let frame = self.call_stack.last().ok_or_else(|| {
|
||||
LogicalFrameEndingReason::Panic("No active call frame".into())
|
||||
})?;
|
||||
let func = &self.program.functions[frame.func_idx];
|
||||
|
||||
crate::local_addressing::check_local_slot(func, slot, opcode as u16, start_pc as u32)
|
||||
.map_err(|trap_info| self.trap(trap_info.code, trap_info.opcode, trap_info.message, trap_info.pc))?;
|
||||
crate::local_addressing::check_local_slot(
|
||||
func,
|
||||
slot,
|
||||
opcode as u16,
|
||||
start_pc as u32,
|
||||
)
|
||||
.map_err(|trap_info| {
|
||||
self.trap(trap_info.code, trap_info.opcode, trap_info.message, trap_info.pc)
|
||||
})?;
|
||||
|
||||
let stack_idx = crate::local_addressing::local_index(frame, slot);
|
||||
let val = self.operand_stack.get(stack_idx).cloned().ok_or_else(|| LogicalFrameEndingReason::Panic("Internal error: validated local slot not found in stack".into()))?;
|
||||
let val = self.operand_stack.get(stack_idx).cloned().ok_or_else(|| {
|
||||
LogicalFrameEndingReason::Panic(
|
||||
"Internal error: validated local slot not found in stack".into(),
|
||||
)
|
||||
})?;
|
||||
self.push(val);
|
||||
}
|
||||
OpCode::SetLocal => {
|
||||
let slot = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
|
||||
let slot = instr
|
||||
.imm_u32()
|
||||
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
|
||||
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||
let frame = self.call_stack.last().ok_or_else(|| LogicalFrameEndingReason::Panic("No active call frame".into()))?;
|
||||
let frame = self.call_stack.last().ok_or_else(|| {
|
||||
LogicalFrameEndingReason::Panic("No active call frame".into())
|
||||
})?;
|
||||
let func = &self.program.functions[frame.func_idx];
|
||||
|
||||
crate::local_addressing::check_local_slot(func, slot, opcode as u16, start_pc as u32)
|
||||
.map_err(|trap_info| self.trap(trap_info.code, trap_info.opcode, trap_info.message, trap_info.pc))?;
|
||||
crate::local_addressing::check_local_slot(
|
||||
func,
|
||||
slot,
|
||||
opcode as u16,
|
||||
start_pc as u32,
|
||||
)
|
||||
.map_err(|trap_info| {
|
||||
self.trap(trap_info.code, trap_info.opcode, trap_info.message, trap_info.pc)
|
||||
})?;
|
||||
|
||||
let stack_idx = crate::local_addressing::local_index(frame, slot);
|
||||
self.operand_stack[stack_idx] = val;
|
||||
}
|
||||
OpCode::Call => {
|
||||
let func_id = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize;
|
||||
let func_id = instr
|
||||
.imm_u32()
|
||||
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
|
||||
as usize;
|
||||
let callee = self.program.functions.get(func_id).ok_or_else(|| {
|
||||
self.trap(TRAP_INVALID_FUNC, opcode as u16, format!("Invalid func_id {}", func_id), start_pc as u32)
|
||||
self.trap(
|
||||
TRAP_INVALID_FUNC,
|
||||
opcode as u16,
|
||||
format!("Invalid func_id {}", func_id),
|
||||
start_pc as u32,
|
||||
)
|
||||
})?;
|
||||
|
||||
if self.operand_stack.len() < callee.param_slots as usize {
|
||||
return Err(LogicalFrameEndingReason::Panic(format!(
|
||||
"Stack underflow during CALL to func {}: expected at least {} arguments, got {}",
|
||||
func_id, callee.param_slots, self.operand_stack.len()
|
||||
func_id,
|
||||
callee.param_slots,
|
||||
self.operand_stack.len()
|
||||
)));
|
||||
}
|
||||
|
||||
@ -764,12 +929,17 @@ impl VirtualMachine {
|
||||
self.pc = callee.code_offset as usize;
|
||||
}
|
||||
OpCode::Ret => {
|
||||
let frame = self.call_stack.pop().ok_or_else(|| LogicalFrameEndingReason::Panic("Call stack underflow".into()))?;
|
||||
let frame = self.call_stack.pop().ok_or_else(|| {
|
||||
LogicalFrameEndingReason::Panic("Call stack underflow".into())
|
||||
})?;
|
||||
let func = &self.program.functions[frame.func_idx];
|
||||
let return_slots = func.return_slots as usize;
|
||||
|
||||
let current_height = self.operand_stack.len();
|
||||
let expected_height = frame.stack_base + func.param_slots as usize + func.local_slots as usize + return_slots;
|
||||
let expected_height = frame.stack_base
|
||||
+ func.param_slots as usize
|
||||
+ func.local_slots as usize
|
||||
+ return_slots;
|
||||
|
||||
if current_height != expected_height {
|
||||
return Err(self.trap(TRAP_BAD_RET_SLOTS, opcode as u16, format!(
|
||||
@ -792,17 +962,19 @@ impl VirtualMachine {
|
||||
self.pc = frame.return_pc as usize;
|
||||
}
|
||||
OpCode::PushScope => {
|
||||
self.scope_stack.push(ScopeFrame {
|
||||
scope_stack_base: self.operand_stack.len(),
|
||||
});
|
||||
self.scope_stack.push(ScopeFrame { scope_stack_base: self.operand_stack.len() });
|
||||
}
|
||||
OpCode::PopScope => {
|
||||
let frame = self.scope_stack.pop().ok_or_else(|| LogicalFrameEndingReason::Panic("Scope stack underflow".into()))?;
|
||||
let frame = self.scope_stack.pop().ok_or_else(|| {
|
||||
LogicalFrameEndingReason::Panic("Scope stack underflow".into())
|
||||
})?;
|
||||
self.operand_stack.truncate(frame.scope_stack_base);
|
||||
}
|
||||
OpCode::Alloc => {
|
||||
// Allocate a new gate with given type and number of slots.
|
||||
let (type_id, slots_u32) = instr.imm_u32x2().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
|
||||
let (type_id, slots_u32) = instr
|
||||
.imm_u32x2()
|
||||
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
|
||||
let slots = slots_u32 as usize;
|
||||
|
||||
// Bump-allocate on the heap and zero-initialize with Null.
|
||||
@ -812,12 +984,8 @@ impl VirtualMachine {
|
||||
}
|
||||
|
||||
// Insert entry into gate pool; GateId is index in this pool.
|
||||
let entry = GateEntry {
|
||||
alive: true,
|
||||
base: base_idx as u32,
|
||||
slots: slots_u32,
|
||||
type_id,
|
||||
};
|
||||
let entry =
|
||||
GateEntry { alive: true, base: base_idx as u32, slots: slots_u32, type_id };
|
||||
let gate_id = self.gate_pool.len() as u32;
|
||||
self.gate_pool.push(entry);
|
||||
|
||||
@ -825,7 +993,10 @@ impl VirtualMachine {
|
||||
self.push(Value::Gate(gate_id as usize));
|
||||
}
|
||||
OpCode::GateLoad => {
|
||||
let offset = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize;
|
||||
let offset = instr
|
||||
.imm_u32()
|
||||
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
|
||||
as usize;
|
||||
let ref_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||
if let Value::Gate(gid_usize) = ref_val {
|
||||
let gid = gid_usize as GateId;
|
||||
@ -835,20 +1006,38 @@ impl VirtualMachine {
|
||||
// bounds check against slots
|
||||
let off_u32 = offset as u32;
|
||||
if off_u32 >= entry.slots {
|
||||
return Err(self.trap(TRAP_OOB, OpCode::GateLoad as u16, format!("Out-of-bounds heap access at offset {}", offset), start_pc as u32));
|
||||
return Err(self.trap(
|
||||
TRAP_OOB,
|
||||
OpCode::GateLoad as u16,
|
||||
format!("Out-of-bounds heap access at offset {}", offset),
|
||||
start_pc as u32,
|
||||
));
|
||||
}
|
||||
let heap_idx = entry.base as usize + offset;
|
||||
let val = self.heap.get(heap_idx).cloned().ok_or_else(|| {
|
||||
// Should not happen if pool and heap are consistent, but keep defensive.
|
||||
self.trap(TRAP_OOB, OpCode::GateLoad as u16, format!("Out-of-bounds heap access at offset {}", offset), start_pc as u32)
|
||||
self.trap(
|
||||
TRAP_OOB,
|
||||
OpCode::GateLoad as u16,
|
||||
format!("Out-of-bounds heap access at offset {}", offset),
|
||||
start_pc as u32,
|
||||
)
|
||||
})?;
|
||||
self.push(val);
|
||||
} else {
|
||||
return Err(self.trap(TRAP_TYPE, OpCode::GateLoad as u16, "Expected gate handle for GATE_LOAD".to_string(), start_pc as u32));
|
||||
return Err(self.trap(
|
||||
TRAP_TYPE,
|
||||
OpCode::GateLoad as u16,
|
||||
"Expected gate handle for GATE_LOAD".to_string(),
|
||||
start_pc as u32,
|
||||
));
|
||||
}
|
||||
}
|
||||
OpCode::GateStore => {
|
||||
let offset = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as usize;
|
||||
let offset = instr
|
||||
.imm_u32()
|
||||
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?
|
||||
as usize;
|
||||
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||
let ref_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||
if let Value::Gate(gid_usize) = ref_val {
|
||||
@ -858,30 +1047,54 @@ impl VirtualMachine {
|
||||
.map_err(|t| t)?;
|
||||
let off_u32 = offset as u32;
|
||||
if off_u32 >= entry.slots {
|
||||
return Err(self.trap(TRAP_OOB, OpCode::GateStore as u16, format!("Out-of-bounds heap access at offset {}", offset), start_pc as u32));
|
||||
return Err(self.trap(
|
||||
TRAP_OOB,
|
||||
OpCode::GateStore as u16,
|
||||
format!("Out-of-bounds heap access at offset {}", offset),
|
||||
start_pc as u32,
|
||||
));
|
||||
}
|
||||
let heap_idx = entry.base as usize + offset;
|
||||
if heap_idx >= self.heap.len() {
|
||||
return Err(self.trap(TRAP_OOB, OpCode::GateStore as u16, format!("Out-of-bounds heap access at offset {}", offset), start_pc as u32));
|
||||
return Err(self.trap(
|
||||
TRAP_OOB,
|
||||
OpCode::GateStore as u16,
|
||||
format!("Out-of-bounds heap access at offset {}", offset),
|
||||
start_pc as u32,
|
||||
));
|
||||
}
|
||||
self.heap[heap_idx] = val;
|
||||
} else {
|
||||
return Err(self.trap(TRAP_TYPE, OpCode::GateStore as u16, "Expected gate handle for GATE_STORE".to_string(), start_pc as u32));
|
||||
return Err(self.trap(
|
||||
TRAP_TYPE,
|
||||
OpCode::GateStore as u16,
|
||||
"Expected gate handle for GATE_STORE".to_string(),
|
||||
start_pc as u32,
|
||||
));
|
||||
}
|
||||
}
|
||||
OpCode::GateBeginPeek | OpCode::GateEndPeek |
|
||||
OpCode::GateBeginBorrow | OpCode::GateEndBorrow |
|
||||
OpCode::GateBeginMutate | OpCode::GateEndMutate |
|
||||
OpCode::GateRetain => {
|
||||
}
|
||||
OpCode::GateBeginPeek
|
||||
| OpCode::GateEndPeek
|
||||
| OpCode::GateBeginBorrow
|
||||
| OpCode::GateEndBorrow
|
||||
| OpCode::GateBeginMutate
|
||||
| OpCode::GateEndMutate
|
||||
| OpCode::GateRetain => {}
|
||||
OpCode::GateRelease => {
|
||||
self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||
}
|
||||
OpCode::Syscall => {
|
||||
let pc_at_syscall = start_pc as u32;
|
||||
let id = instr.imm_u32().map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
|
||||
let id = instr
|
||||
.imm_u32()
|
||||
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
|
||||
let syscall = prometeu_hal::syscalls::Syscall::from_u32(id).ok_or_else(|| {
|
||||
self.trap(TRAP_INVALID_SYSCALL, OpCode::Syscall as u16, format!("Unknown syscall: 0x{:08X}", id), pc_at_syscall)
|
||||
self.trap(
|
||||
TRAP_INVALID_SYSCALL,
|
||||
OpCode::Syscall as u16,
|
||||
format!("Unknown syscall: 0x{:08X}", id),
|
||||
pc_at_syscall,
|
||||
)
|
||||
})?;
|
||||
|
||||
let args_count = syscall.args_count();
|
||||
@ -889,7 +1102,12 @@ impl VirtualMachine {
|
||||
let mut args = Vec::with_capacity(args_count);
|
||||
for _ in 0..args_count {
|
||||
let v = self.pop().map_err(|_e| {
|
||||
self.trap(TRAP_STACK_UNDERFLOW, OpCode::Syscall as u16, "Syscall argument stack underflow".to_string(), pc_at_syscall)
|
||||
self.trap(
|
||||
TRAP_STACK_UNDERFLOW,
|
||||
OpCode::Syscall as u16,
|
||||
"Syscall argument stack underflow".to_string(),
|
||||
pc_at_syscall,
|
||||
)
|
||||
})?;
|
||||
args.push(v);
|
||||
}
|
||||
@ -898,9 +1116,13 @@ impl VirtualMachine {
|
||||
let stack_height_before = self.operand_stack.len();
|
||||
let mut ret = crate::HostReturn::new(&mut self.operand_stack);
|
||||
native.syscall(id, &args, &mut ret, ctx).map_err(|fault| match fault {
|
||||
VmFault::Trap(code, msg) => self.trap(code, OpCode::Syscall as u16, msg, pc_at_syscall),
|
||||
VmFault::Trap(code, msg) => {
|
||||
self.trap(code, OpCode::Syscall as u16, msg, pc_at_syscall)
|
||||
}
|
||||
VmFault::Panic(msg) => LogicalFrameEndingReason::Panic(msg),
|
||||
VmFault::Unavailable => LogicalFrameEndingReason::Panic("Host feature unavailable".into()),
|
||||
VmFault::Unavailable => {
|
||||
LogicalFrameEndingReason::Panic("Host feature unavailable".into())
|
||||
}
|
||||
})?;
|
||||
|
||||
let stack_height_after = self.operand_stack.len();
|
||||
@ -908,7 +1130,10 @@ impl VirtualMachine {
|
||||
if results_pushed != syscall.results_count() {
|
||||
return Err(LogicalFrameEndingReason::Panic(format!(
|
||||
"Syscall {} (0x{:08X}) results mismatch: expected {}, got {}",
|
||||
syscall.name(), id, syscall.results_count(), results_pushed
|
||||
syscall.name(),
|
||||
id,
|
||||
syscall.results_count(),
|
||||
results_pushed
|
||||
)));
|
||||
}
|
||||
}
|
||||
@ -923,15 +1148,15 @@ impl VirtualMachine {
|
||||
}
|
||||
|
||||
/// Resolves a `GateId` to an immutable reference to its `GateEntry` or returns a trap reason.
|
||||
fn resolve_gate(&self, gate_id: GateId, opcode: u16, pc: u32) -> Result<&GateEntry, LogicalFrameEndingReason> {
|
||||
fn resolve_gate(
|
||||
&self,
|
||||
gate_id: GateId,
|
||||
opcode: u16,
|
||||
pc: u32,
|
||||
) -> Result<&GateEntry, LogicalFrameEndingReason> {
|
||||
let idx = gate_id as usize;
|
||||
let entry = self.gate_pool.get(idx).ok_or_else(|| {
|
||||
self.trap(
|
||||
TRAP_INVALID_GATE,
|
||||
opcode,
|
||||
format!("Invalid gate id: {}", gate_id),
|
||||
pc,
|
||||
)
|
||||
self.trap(TRAP_INVALID_GATE, opcode, format!("Invalid gate id: {}", gate_id), pc)
|
||||
})?;
|
||||
if !entry.alive {
|
||||
return Err(self.trap(
|
||||
@ -944,7 +1169,13 @@ impl VirtualMachine {
|
||||
Ok(entry)
|
||||
}
|
||||
|
||||
pub fn trap(&self, code: u32, opcode: u16, message: String, pc: u32) -> LogicalFrameEndingReason {
|
||||
pub fn trap(
|
||||
&self,
|
||||
code: u32,
|
||||
opcode: u16,
|
||||
message: String,
|
||||
pc: u32,
|
||||
) -> LogicalFrameEndingReason {
|
||||
LogicalFrameEndingReason::Trap(self.program.create_trap(code, opcode, message, pc))
|
||||
}
|
||||
|
||||
@ -973,7 +1204,12 @@ impl VirtualMachine {
|
||||
self.operand_stack.last().ok_or("Stack underflow".into())
|
||||
}
|
||||
|
||||
fn binary_op<F>(&mut self, opcode: OpCode, start_pc: u32, f: F) -> Result<(), LogicalFrameEndingReason>
|
||||
fn binary_op<F>(
|
||||
&mut self,
|
||||
opcode: OpCode,
|
||||
start_pc: u32,
|
||||
f: F,
|
||||
) -> Result<(), LogicalFrameEndingReason>
|
||||
where
|
||||
F: FnOnce(Value, Value) -> Result<Value, OpError>,
|
||||
{
|
||||
@ -1010,7 +1246,13 @@ mod tests {
|
||||
|
||||
struct MockNative;
|
||||
impl NativeInterface for MockNative {
|
||||
fn syscall(&mut self, _id: u32, _args: &[Value], _ret: &mut HostReturn, _ctx: &mut HostContext) -> Result<(), VmFault> {
|
||||
fn syscall(
|
||||
&mut self,
|
||||
_id: u32,
|
||||
_args: &[Value],
|
||||
_ret: &mut HostReturn,
|
||||
_ctx: &mut HostContext,
|
||||
) -> Result<(), VmFault> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -1254,7 +1496,13 @@ mod tests {
|
||||
];
|
||||
|
||||
let mut vm = VirtualMachine {
|
||||
program: ProgramImage::new(rom, vec![], functions, None, std::collections::HashMap::new()),
|
||||
program: ProgramImage::new(
|
||||
rom,
|
||||
vec![],
|
||||
functions,
|
||||
None,
|
||||
std::collections::HashMap::new(),
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
vm.prepare_call("0");
|
||||
@ -1299,7 +1547,13 @@ mod tests {
|
||||
];
|
||||
|
||||
let mut vm = VirtualMachine {
|
||||
program: ProgramImage::new(rom, vec![], functions, None, std::collections::HashMap::new()),
|
||||
program: ProgramImage::new(
|
||||
rom,
|
||||
vec![],
|
||||
functions,
|
||||
None,
|
||||
std::collections::HashMap::new(),
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
vm.prepare_call("0");
|
||||
@ -1338,7 +1592,13 @@ mod tests {
|
||||
];
|
||||
|
||||
let mut vm2 = VirtualMachine {
|
||||
program: ProgramImage::new(rom2, vec![], functions2, None, std::collections::HashMap::new()),
|
||||
program: ProgramImage::new(
|
||||
rom2,
|
||||
vec![],
|
||||
functions2,
|
||||
None,
|
||||
std::collections::HashMap::new(),
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
vm2.prepare_call("0");
|
||||
@ -1447,7 +1707,13 @@ mod tests {
|
||||
];
|
||||
|
||||
let mut vm = VirtualMachine {
|
||||
program: ProgramImage::new(rom, vec![], functions, None, std::collections::HashMap::new()),
|
||||
program: ProgramImage::new(
|
||||
rom,
|
||||
vec![],
|
||||
functions,
|
||||
None,
|
||||
std::collections::HashMap::new(),
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
vm.prepare_call("0");
|
||||
@ -1782,7 +2048,7 @@ mod tests {
|
||||
0x17, 0x00, // PushI32
|
||||
0x00, 0x00, 0x00, 0x00, // value 0
|
||||
0x11, 0x00, // Pop
|
||||
0x51, 0x00 // Ret
|
||||
0x51, 0x00, // Ret
|
||||
];
|
||||
let mut vm = VirtualMachine::new(rom.clone(), vec![]);
|
||||
vm.program.functions = std::sync::Arc::from(vec![prometeu_bytecode::FunctionMeta {
|
||||
@ -1793,7 +2059,15 @@ mod tests {
|
||||
let mut ctx = HostContext::new(None);
|
||||
struct TestNative;
|
||||
impl NativeInterface for TestNative {
|
||||
fn syscall(&mut self, _id: u32, _args: &[Value], _ret: &mut HostReturn, _ctx: &mut HostContext) -> Result<(), VmFault> { Ok(()) }
|
||||
fn syscall(
|
||||
&mut self,
|
||||
_id: u32,
|
||||
_args: &[Value],
|
||||
_ret: &mut HostReturn,
|
||||
_ctx: &mut HostContext,
|
||||
) -> Result<(), VmFault> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
let mut native = TestNative;
|
||||
|
||||
@ -1811,7 +2085,13 @@ mod tests {
|
||||
|
||||
struct MultiReturnNative;
|
||||
impl NativeInterface for MultiReturnNative {
|
||||
fn syscall(&mut self, _id: u32, _args: &[Value], ret: &mut HostReturn, _ctx: &mut HostContext) -> Result<(), VmFault> {
|
||||
fn syscall(
|
||||
&mut self,
|
||||
_id: u32,
|
||||
_args: &[Value],
|
||||
ret: &mut HostReturn,
|
||||
_ctx: &mut HostContext,
|
||||
) -> Result<(), VmFault> {
|
||||
ret.push_bool(true);
|
||||
ret.push_int(42);
|
||||
ret.push_bounded(255)?;
|
||||
@ -1845,7 +2125,13 @@ mod tests {
|
||||
|
||||
struct VoidReturnNative;
|
||||
impl NativeInterface for VoidReturnNative {
|
||||
fn syscall(&mut self, _id: u32, _args: &[Value], _ret: &mut HostReturn, _ctx: &mut HostContext) -> Result<(), VmFault> {
|
||||
fn syscall(
|
||||
&mut self,
|
||||
_id: u32,
|
||||
_args: &[Value],
|
||||
_ret: &mut HostReturn,
|
||||
_ctx: &mut HostContext,
|
||||
) -> Result<(), VmFault> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -1879,7 +2165,13 @@ mod tests {
|
||||
|
||||
struct ArgCheckNative;
|
||||
impl NativeInterface for ArgCheckNative {
|
||||
fn syscall(&mut self, _id: u32, args: &[Value], _ret: &mut HostReturn, _ctx: &mut HostContext) -> Result<(), VmFault> {
|
||||
fn syscall(
|
||||
&mut self,
|
||||
_id: u32,
|
||||
args: &[Value],
|
||||
_ret: &mut HostReturn,
|
||||
_ctx: &mut HostContext,
|
||||
) -> Result<(), VmFault> {
|
||||
expect_int(args, 0)?;
|
||||
Ok(())
|
||||
}
|
||||
@ -1967,7 +2259,13 @@ mod tests {
|
||||
|
||||
struct BadNative;
|
||||
impl NativeInterface for BadNative {
|
||||
fn syscall(&mut self, _id: u32, _args: &[Value], ret: &mut HostReturn, _ctx: &mut HostContext) -> Result<(), VmFault> {
|
||||
fn syscall(
|
||||
&mut self,
|
||||
_id: u32,
|
||||
_args: &[Value],
|
||||
ret: &mut HostReturn,
|
||||
_ctx: &mut HostContext,
|
||||
) -> Result<(), VmFault> {
|
||||
// Wrong: GfxClear565 is void but we push something
|
||||
ret.push_int(42);
|
||||
Ok(())
|
||||
@ -2029,7 +2327,7 @@ mod tests {
|
||||
|
||||
let res = vm.initialize(header, "");
|
||||
match res {
|
||||
Err(VmInitError::ImageLoadFailed(prometeu_bytecode::LoadError::UnexpectedEof)) => {},
|
||||
Err(VmInitError::ImageLoadFailed(prometeu_bytecode::LoadError::UnexpectedEof)) => {}
|
||||
_ => panic!("Expected PbsV0LoadFailed(UnexpectedEof), got {:?}", res),
|
||||
}
|
||||
}
|
||||
@ -2065,7 +2363,6 @@ mod tests {
|
||||
assert_eq!(vm.cycles, 0);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_calling_convention_add() {
|
||||
let mut native = MockNative;
|
||||
|
||||
29
crates/console/prometeu-vm/tests/smoke.rs
Normal file
29
crates/console/prometeu-vm/tests/smoke.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
8
crates/dev/prometeu-test-support/Cargo.toml
Normal file
8
crates/dev/prometeu-test-support/Cargo.toml
Normal 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"] }
|
||||
108
crates/dev/prometeu-test-support/src/lib.rs
Normal file
108
crates/dev/prometeu-test-support/src/lib.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
use prometeu_hal::debugger_protocol::*;
|
||||
use prometeu_drivers::hardware::Hardware;
|
||||
use prometeu_firmware::{BootTarget, Firmware};
|
||||
use prometeu_hal::cartridge_loader::CartridgeLoader;
|
||||
use prometeu_hal::debugger_protocol::*;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use prometeu_drivers::hardware::Hardware;
|
||||
use prometeu_hal::cartridge_loader::CartridgeLoader;
|
||||
|
||||
/// Host-side implementation of the PROMETEU DevTools Protocol.
|
||||
///
|
||||
@ -136,7 +136,9 @@ impl HostDebugger {
|
||||
// Support multiple JSON messages in a single TCP packet.
|
||||
for line in msg.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() { continue; }
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Ok(cmd) = serde_json::from_str::<DebugCommand>(trimmed) {
|
||||
self.handle_command(cmd, firmware, hardware);
|
||||
}
|
||||
@ -162,7 +164,12 @@ impl HostDebugger {
|
||||
}
|
||||
|
||||
/// Dispatches a specific DebugCommand to the system components.
|
||||
fn handle_command(&mut self, cmd: DebugCommand, firmware: &mut Firmware, hardware: &mut Hardware) {
|
||||
fn handle_command(
|
||||
&mut self,
|
||||
cmd: DebugCommand,
|
||||
firmware: &mut Firmware,
|
||||
hardware: &mut Hardware,
|
||||
) {
|
||||
match cmd {
|
||||
DebugCommand::Ok | DebugCommand::Start => {
|
||||
if self.waiting_for_start {
|
||||
@ -189,8 +196,7 @@ impl HostDebugger {
|
||||
}
|
||||
DebugCommand::GetState => {
|
||||
// Return detailed VM register and stack state.
|
||||
let stack_top = firmware.vm.operand_stack.iter()
|
||||
.rev().take(10).cloned().collect();
|
||||
let stack_top = firmware.vm.operand_stack.iter().rev().take(10).cloned().collect();
|
||||
|
||||
let resp = DebugResponse::GetState {
|
||||
pc: firmware.vm.pc,
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -7,7 +7,11 @@ use crate::stats::HostStats;
|
||||
use crate::utilities::draw_rgb565_to_rgba8;
|
||||
use pixels::wgpu::PresentMode;
|
||||
use pixels::{Pixels, PixelsBuilder, SurfaceTexture};
|
||||
use prometeu_drivers::AudioCommand;
|
||||
use prometeu_drivers::hardware::Hardware;
|
||||
use prometeu_firmware::{BootTarget, Firmware};
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::telemetry::CertificationConfig;
|
||||
use std::time::{Duration, Instant};
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::dpi::LogicalSize;
|
||||
@ -15,10 +19,6 @@ use winit::event::{ElementState, WindowEvent};
|
||||
use winit::event_loop::{ActiveEventLoop, ControlFlow};
|
||||
use winit::keyboard::{KeyCode, PhysicalKey};
|
||||
use winit::window::{Window, WindowAttributes, WindowId};
|
||||
use prometeu_hal::telemetry::CertificationConfig;
|
||||
use prometeu_drivers::hardware::Hardware;
|
||||
use prometeu_drivers::AudioCommand;
|
||||
use prometeu_hal::color::Color;
|
||||
|
||||
/// The Desktop implementation of the PROMETEU Runtime.
|
||||
///
|
||||
@ -130,8 +130,18 @@ impl HostRunner {
|
||||
let color_warn = Color::RED;
|
||||
|
||||
self.hardware.gfx.fill_rect(5, 5, 175, 100, color_bg);
|
||||
self.hardware.gfx.draw_text(10, 10, &format!("FPS: {:.1}", self.stats.current_fps), color_text);
|
||||
self.hardware.gfx.draw_text(10, 18, &format!("HOST: {:.2}MS", tel.host_cpu_time_us as f64 / 1000.0), color_text);
|
||||
self.hardware.gfx.draw_text(
|
||||
10,
|
||||
10,
|
||||
&format!("FPS: {:.1}", self.stats.current_fps),
|
||||
color_text,
|
||||
);
|
||||
self.hardware.gfx.draw_text(
|
||||
10,
|
||||
18,
|
||||
&format!("HOST: {:.2}MS", tel.host_cpu_time_us as f64 / 1000.0),
|
||||
color_text,
|
||||
);
|
||||
self.hardware.gfx.draw_text(10, 26, &format!("STEPS: {}", tel.vm_steps), color_text);
|
||||
self.hardware.gfx.draw_text(10, 34, &format!("SYSC: {}", tel.syscalls), color_text);
|
||||
|
||||
@ -140,25 +150,60 @@ impl HostRunner {
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
self.hardware.gfx.draw_text(10, 42, &format!("CYC: {}/{} ({:.1}%)", tel.cycles_used, tel.cycles_budget, cycles_pct), color_text);
|
||||
self.hardware.gfx.draw_text(
|
||||
10,
|
||||
42,
|
||||
&format!("CYC: {}/{} ({:.1}%)", tel.cycles_used, tel.cycles_budget, cycles_pct),
|
||||
color_text,
|
||||
);
|
||||
|
||||
self.hardware.gfx.draw_text(10, 50, &format!("GFX: {}K/16M ({}S)", tel.gfx_used_bytes / 1024, tel.gfx_slots_occupied), color_text);
|
||||
self.hardware.gfx.draw_text(
|
||||
10,
|
||||
50,
|
||||
&format!("GFX: {}K/16M ({}S)", tel.gfx_used_bytes / 1024, tel.gfx_slots_occupied),
|
||||
color_text,
|
||||
);
|
||||
if tel.gfx_inflight_bytes > 0 {
|
||||
self.hardware.gfx.draw_text(10, 58, &format!("LOAD GFX: {}KB", tel.gfx_inflight_bytes / 1024), color_warn);
|
||||
self.hardware.gfx.draw_text(
|
||||
10,
|
||||
58,
|
||||
&format!("LOAD GFX: {}KB", tel.gfx_inflight_bytes / 1024),
|
||||
color_warn,
|
||||
);
|
||||
}
|
||||
|
||||
self.hardware.gfx.draw_text(10, 66, &format!("AUD: {}K/32M ({}S)", tel.audio_used_bytes / 1024, tel.audio_slots_occupied), color_text);
|
||||
self.hardware.gfx.draw_text(
|
||||
10,
|
||||
66,
|
||||
&format!("AUD: {}K/32M ({}S)", tel.audio_used_bytes / 1024, tel.audio_slots_occupied),
|
||||
color_text,
|
||||
);
|
||||
if tel.audio_inflight_bytes > 0 {
|
||||
self.hardware.gfx.draw_text(10, 74, &format!("LOAD AUD: {}KB", tel.audio_inflight_bytes / 1024), color_warn);
|
||||
self.hardware.gfx.draw_text(
|
||||
10,
|
||||
74,
|
||||
&format!("LOAD AUD: {}KB", tel.audio_inflight_bytes / 1024),
|
||||
color_warn,
|
||||
);
|
||||
}
|
||||
|
||||
let cert_color = if tel.violations > 0 { color_warn } else { color_text };
|
||||
self.hardware.gfx.draw_text(10, 82, &format!("CERT LAST: {}", tel.violations), cert_color);
|
||||
|
||||
if tel.violations > 0 {
|
||||
if let Some(event) = self.firmware.os.log_service.get_recent(10).into_iter().rev().find(|e| e.tag >= 0xCA01 && e.tag <= 0xCA03) {
|
||||
if let Some(event) = self
|
||||
.firmware
|
||||
.os
|
||||
.log_service
|
||||
.get_recent(10)
|
||||
.into_iter()
|
||||
.rev()
|
||||
.find(|e| e.tag >= 0xCA01 && e.tag <= 0xCA03)
|
||||
{
|
||||
let mut msg = event.msg.clone();
|
||||
if msg.len() > 30 { msg.truncate(30); }
|
||||
if msg.len() > 30 {
|
||||
msg.truncate(30);
|
||||
}
|
||||
self.hardware.gfx.draw_text(10, 90, &msg, color_warn);
|
||||
}
|
||||
}
|
||||
@ -183,7 +228,8 @@ impl ApplicationHandler for HostRunner {
|
||||
let size = window.inner_size();
|
||||
let surface_texture = SurfaceTexture::new(size.width, size.height, window);
|
||||
|
||||
let mut pixels = PixelsBuilder::new(Hardware::W as u32, Hardware::H as u32, surface_texture)
|
||||
let mut pixels =
|
||||
PixelsBuilder::new(Hardware::W as u32, Hardware::H as u32, surface_texture)
|
||||
.present_mode(PresentMode::Fifo) // activate vsync
|
||||
.build()
|
||||
.expect("failed to create Pixels");
|
||||
@ -197,7 +243,6 @@ impl ApplicationHandler for HostRunner {
|
||||
event_loop.set_control_flow(ControlFlow::Poll);
|
||||
}
|
||||
|
||||
|
||||
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
|
||||
self.input.handle_event(&event, self.window());
|
||||
|
||||
@ -232,7 +277,6 @@ impl ApplicationHandler for HostRunner {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
WindowEvent::KeyboardInput { event, .. } => {
|
||||
if let PhysicalKey::Code(code) = event.physical_key {
|
||||
let is_down = event.state == ElementState::Pressed;
|
||||
@ -298,11 +342,8 @@ impl ApplicationHandler for HostRunner {
|
||||
let is_paused = self.firmware.os.paused || self.debugger.waiting_for_start;
|
||||
if is_paused != self.last_paused_state {
|
||||
self.last_paused_state = is_paused;
|
||||
let cmd = if is_paused {
|
||||
AudioCommand::MasterPause
|
||||
} else {
|
||||
AudioCommand::MasterResume
|
||||
};
|
||||
let cmd =
|
||||
if is_paused { AudioCommand::MasterPause } else { AudioCommand::MasterResume };
|
||||
self.hardware.audio.commands.push(cmd);
|
||||
}
|
||||
|
||||
@ -345,9 +386,9 @@ impl ApplicationHandler for HostRunner {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use prometeu_firmware::BootTarget;
|
||||
use prometeu_hal::debugger_protocol::DEVTOOLS_PROTOCOL_VERSION;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpStream;
|
||||
use prometeu_hal::debugger_protocol::DEVTOOLS_PROTOCOL_VERSION;
|
||||
|
||||
#[test]
|
||||
fn test_debug_port_opens() {
|
||||
@ -364,7 +405,8 @@ mod tests {
|
||||
|
||||
// Check if we can connect
|
||||
{
|
||||
let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect");
|
||||
let mut stream =
|
||||
TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect");
|
||||
// Short sleep to ensure the OS processes
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
|
||||
@ -375,24 +417,36 @@ mod tests {
|
||||
// Handshake Check
|
||||
let mut buf = [0u8; 2048];
|
||||
let n = stream.read(&mut buf).expect("Should read handshake");
|
||||
let resp: serde_json::Value = serde_json::from_slice(&buf[..n]).expect("Handshake should be valid JSON");
|
||||
let resp: serde_json::Value =
|
||||
serde_json::from_slice(&buf[..n]).expect("Handshake should be valid JSON");
|
||||
assert_eq!(resp["type"], "handshake");
|
||||
assert_eq!(resp["protocol_version"], DEVTOOLS_PROTOCOL_VERSION);
|
||||
|
||||
// Send start via JSON
|
||||
stream.write_all(b"{\"type\":\"start\"}\n").expect("Connection should be open for writing");
|
||||
stream
|
||||
.write_all(b"{\"type\":\"start\"}\n")
|
||||
.expect("Connection should be open for writing");
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
|
||||
// Process the received command
|
||||
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
|
||||
assert!(!runner.debugger.waiting_for_start, "Execution should have started after start command");
|
||||
assert!(runner.debugger.listener.is_some(), "Listener should remain open for reconnections");
|
||||
assert!(
|
||||
!runner.debugger.waiting_for_start,
|
||||
"Execution should have started after start command"
|
||||
);
|
||||
assert!(
|
||||
runner.debugger.listener.is_some(),
|
||||
"Listener should remain open for reconnections"
|
||||
);
|
||||
}
|
||||
|
||||
// Now that the stream is out of the test scope, the runner should detect closure on next check
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
|
||||
assert!(runner.debugger.stream.is_none(), "Stream should have been closed after client disconnected");
|
||||
assert!(
|
||||
runner.debugger.stream.is_none(),
|
||||
"Stream should have been closed after client disconnected"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -407,7 +461,8 @@ mod tests {
|
||||
|
||||
// 1. Connect and start
|
||||
{
|
||||
let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 1");
|
||||
let mut stream =
|
||||
TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 1");
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
|
||||
assert!(runner.debugger.stream.is_some());
|
||||
@ -430,7 +485,10 @@ mod tests {
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
|
||||
assert!(runner.debugger.stream.is_some(), "Stream should have been accepted on reconnection");
|
||||
assert!(
|
||||
runner.debugger.stream.is_some(),
|
||||
"Stream should have been accepted on reconnection"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -444,13 +502,15 @@ mod tests {
|
||||
});
|
||||
|
||||
// 1. First connection
|
||||
let mut _stream1 = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 1");
|
||||
let mut _stream1 =
|
||||
TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 1");
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
|
||||
assert!(runner.debugger.stream.is_some());
|
||||
|
||||
// 2. Second connection
|
||||
let mut stream2 = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 2 (OS accepts)");
|
||||
let mut stream2 = TcpStream::connect(format!("127.0.0.1:{}", port))
|
||||
.expect("Should connect 2 (OS accepts)");
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware); // Should accept and close stream2
|
||||
|
||||
@ -458,7 +518,10 @@ mod tests {
|
||||
let mut buf = [0u8; 10];
|
||||
stream2.set_read_timeout(Some(std::time::Duration::from_millis(100))).unwrap();
|
||||
let res = stream2.read(&mut buf);
|
||||
assert!(matches!(res, Ok(0)) || res.is_err(), "Second connection should be closed by server");
|
||||
assert!(
|
||||
matches!(res, Ok(0)) || res.is_err(),
|
||||
"Second connection should be closed by server"
|
||||
);
|
||||
|
||||
assert!(runner.debugger.stream.is_some(), "First connection should continue active");
|
||||
}
|
||||
@ -494,7 +557,9 @@ mod tests {
|
||||
loop {
|
||||
line.clear();
|
||||
reader.read_line(&mut line).expect("Should read line");
|
||||
if line.is_empty() { break; }
|
||||
if line.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Ok(resp) = serde_json::from_str::<serde_json::Value>(&line) {
|
||||
if resp["type"] == "getState" {
|
||||
@ -517,7 +582,8 @@ mod tests {
|
||||
|
||||
// 1. Connect and pause
|
||||
{
|
||||
let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect");
|
||||
let mut stream =
|
||||
TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect");
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use prometeu_drivers::hardware::Hardware;
|
||||
use prometeu_firmware::Firmware;
|
||||
use std::time::{Duration, Instant};
|
||||
use winit::window::Window;
|
||||
use prometeu_drivers::hardware::Hardware;
|
||||
|
||||
pub struct HostStats {
|
||||
pub last_stats_update: Instant,
|
||||
@ -31,7 +31,13 @@ impl HostStats {
|
||||
self.audio_load_samples += 1;
|
||||
}
|
||||
|
||||
pub fn update(&mut self, now: Instant, window: Option<&Window>, _hardware: &Hardware, firmware: &Firmware) {
|
||||
pub fn update(
|
||||
&mut self,
|
||||
now: Instant,
|
||||
window: Option<&Window>,
|
||||
_hardware: &Hardware,
|
||||
firmware: &Firmware,
|
||||
) {
|
||||
let stats_elapsed = now.duration_since(self.last_stats_update);
|
||||
if stats_elapsed >= Duration::from_secs(1) {
|
||||
self.current_fps = self.frames_since_last_update as f64 / stats_elapsed.as_secs_f64();
|
||||
@ -39,7 +45,8 @@ impl HostStats {
|
||||
if let Some(window) = window {
|
||||
// Fixed comparison always against 60Hz, keep even when doing CPU stress tests
|
||||
let frame_budget_us = 16666.0;
|
||||
let cpu_load_core = (firmware.os.last_frame_cpu_time_us as f64 / frame_budget_us) * 100.0;
|
||||
let cpu_load_core =
|
||||
(firmware.os.last_frame_cpu_time_us as f64 / frame_budget_us) * 100.0;
|
||||
|
||||
let cpu_load_audio = if self.audio_load_samples > 0 {
|
||||
(self.audio_load_accum_us as f64 / stats_elapsed.as_micros() as f64) * 100.0
|
||||
@ -49,7 +56,12 @@ impl HostStats {
|
||||
|
||||
let title = format!(
|
||||
"PROMETEU | GFX: {:.1} KB | FPS: {:.1} | Load: {:.1}% (C) + {:.1}% (A) | Frame: tick {} logical {}",
|
||||
0, self.current_fps, cpu_load_core, cpu_load_audio, firmware.os.tick_index, firmware.os.logical_frame_index
|
||||
0,
|
||||
self.current_fps,
|
||||
cpu_load_core,
|
||||
cpu_load_audio,
|
||||
firmware.os.tick_index,
|
||||
firmware.os.logical_frame_index
|
||||
);
|
||||
window.set_title(&title);
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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
25
docs/ARCHITECTURE.md
Normal 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.
|
||||
@ -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
49
docs/STYLE.md
Normal 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 crate’s 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.
|
||||
@ -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
3
rustfmt.toml
Normal file
@ -0,0 +1,3 @@
|
||||
max_width = 100
|
||||
use_small_heuristics = "Max"
|
||||
newline_style = "Unix"
|
||||
Loading…
x
Reference in New Issue
Block a user