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
|
||||
@ -47,4 +47,4 @@ pub struct TrapInfo {
|
||||
pub pc: u32,
|
||||
/// Optional source span information if debug symbols are available.
|
||||
pub span: Option<SourceSpan>,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -3,7 +3,7 @@ use crate::opcode::OpCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// An entry in the Constant Pool.
|
||||
///
|
||||
///
|
||||
/// The Constant Pool is a table of unique values used by the program.
|
||||
/// Instead of embedding large data (like strings) directly in the instruction stream,
|
||||
/// the bytecode uses `PushConst <index>` to load these values onto the stack.
|
||||
@ -62,9 +62,9 @@ pub struct Export {
|
||||
|
||||
/// Represents the final serialized format of a PBS v0 module.
|
||||
///
|
||||
/// This structure is a pure data container for the PBS format. It does NOT
|
||||
/// contain any linker-like logic (symbol resolution, patching, etc.).
|
||||
/// All multi-module programs must be flattened and linked by the compiler
|
||||
/// This structure is a pure data container for the PBS format. It does NOT
|
||||
/// contain any linker-like logic (symbol resolution, patching, etc.).
|
||||
/// All multi-module programs must be flattened and linked by the compiler
|
||||
/// before being serialized into this format.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct BytecodeModule {
|
||||
@ -81,15 +81,26 @@ impl BytecodeModule {
|
||||
let cp_data = self.serialize_const_pool();
|
||||
let func_data = self.serialize_functions();
|
||||
let code_data = self.code.clone();
|
||||
let debug_data = self.debug_info.as_ref().map(|di| self.serialize_debug(di)).unwrap_or_default();
|
||||
let debug_data =
|
||||
self.debug_info.as_ref().map(|di| self.serialize_debug(di)).unwrap_or_default();
|
||||
let export_data = self.serialize_exports();
|
||||
|
||||
let mut final_sections = Vec::new();
|
||||
if !cp_data.is_empty() { final_sections.push((0, cp_data)); }
|
||||
if !func_data.is_empty() { final_sections.push((1, func_data)); }
|
||||
if !code_data.is_empty() { final_sections.push((2, code_data)); }
|
||||
if !debug_data.is_empty() { final_sections.push((3, debug_data)); }
|
||||
if !export_data.is_empty() { final_sections.push((4, export_data)); }
|
||||
if !cp_data.is_empty() {
|
||||
final_sections.push((0, cp_data));
|
||||
}
|
||||
if !func_data.is_empty() {
|
||||
final_sections.push((1, func_data));
|
||||
}
|
||||
if !code_data.is_empty() {
|
||||
final_sections.push((2, code_data));
|
||||
}
|
||||
if !debug_data.is_empty() {
|
||||
final_sections.push((3, debug_data));
|
||||
}
|
||||
if !export_data.is_empty() {
|
||||
final_sections.push((4, export_data));
|
||||
}
|
||||
|
||||
let mut out = Vec::new();
|
||||
// Magic "PBS\0"
|
||||
@ -104,7 +115,7 @@ impl BytecodeModule {
|
||||
out.extend_from_slice(&[0u8; 20]);
|
||||
|
||||
let mut current_offset = 32 + (final_sections.len() as u32 * 12);
|
||||
|
||||
|
||||
// Write section table
|
||||
for (kind, data) in &final_sections {
|
||||
let k: u32 = *kind;
|
||||
@ -123,7 +134,9 @@ impl BytecodeModule {
|
||||
}
|
||||
|
||||
fn serialize_const_pool(&self) -> Vec<u8> {
|
||||
if self.const_pool.is_empty() { return Vec::new(); }
|
||||
if self.const_pool.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&(self.const_pool.len() as u32).to_le_bytes());
|
||||
for entry in &self.const_pool {
|
||||
@ -157,7 +170,9 @@ impl BytecodeModule {
|
||||
}
|
||||
|
||||
fn serialize_functions(&self) -> Vec<u8> {
|
||||
if self.functions.is_empty() { return Vec::new(); }
|
||||
if self.functions.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&(self.functions.len() as u32).to_le_bytes());
|
||||
for f in &self.functions {
|
||||
@ -191,7 +206,9 @@ impl BytecodeModule {
|
||||
}
|
||||
|
||||
fn serialize_exports(&self) -> Vec<u8> {
|
||||
if self.exports.is_empty() { return Vec::new(); }
|
||||
if self.exports.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&(self.exports.len() as u32).to_le_bytes());
|
||||
for exp in &self.exports {
|
||||
@ -202,7 +219,6 @@ impl BytecodeModule {
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub struct BytecodeLoader;
|
||||
@ -224,22 +240,34 @@ impl BytecodeLoader {
|
||||
}
|
||||
|
||||
let endianness = bytes[6];
|
||||
if endianness != 0 { // 0 = Little Endian
|
||||
if endianness != 0 {
|
||||
// 0 = Little Endian
|
||||
return Err(LoadError::InvalidEndianness);
|
||||
}
|
||||
|
||||
let section_count = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
|
||||
|
||||
|
||||
let mut sections = Vec::new();
|
||||
let mut pos = 32;
|
||||
for _ in 0..section_count {
|
||||
if pos + 12 > bytes.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let kind = u32::from_le_bytes([bytes[pos], bytes[pos+1], bytes[pos+2], bytes[pos+3]]);
|
||||
let offset = u32::from_le_bytes([bytes[pos+4], bytes[pos+5], bytes[pos+6], bytes[pos+7]]);
|
||||
let length = u32::from_le_bytes([bytes[pos+8], bytes[pos+9], bytes[pos+10], bytes[pos+11]]);
|
||||
|
||||
let kind =
|
||||
u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]]);
|
||||
let offset = u32::from_le_bytes([
|
||||
bytes[pos + 4],
|
||||
bytes[pos + 5],
|
||||
bytes[pos + 6],
|
||||
bytes[pos + 7],
|
||||
]);
|
||||
let length = u32::from_le_bytes([
|
||||
bytes[pos + 8],
|
||||
bytes[pos + 9],
|
||||
bytes[pos + 10],
|
||||
bytes[pos + 11],
|
||||
]);
|
||||
|
||||
// Basic bounds check
|
||||
if (offset as usize) + (length as usize) > bytes.len() {
|
||||
return Err(LoadError::SectionOutOfBounds);
|
||||
@ -254,7 +282,7 @@ impl BytecodeLoader {
|
||||
for j in i + 1..sections.len() {
|
||||
let (_, o1, l1) = sections[i];
|
||||
let (_, o2, l2) = sections[j];
|
||||
|
||||
|
||||
if (o1 < o2 + l2) && (o2 < o1 + l1) {
|
||||
return Err(LoadError::OverlappingSections);
|
||||
}
|
||||
@ -273,19 +301,24 @@ impl BytecodeLoader {
|
||||
for (kind, offset, length) in sections {
|
||||
let section_data = &bytes[offset as usize..(offset + length) as usize];
|
||||
match kind {
|
||||
0 => { // Const Pool
|
||||
0 => {
|
||||
// Const Pool
|
||||
module.const_pool = parse_const_pool(section_data)?;
|
||||
}
|
||||
1 => { // Functions
|
||||
1 => {
|
||||
// Functions
|
||||
module.functions = parse_functions(section_data)?;
|
||||
}
|
||||
2 => { // Code
|
||||
2 => {
|
||||
// Code
|
||||
module.code = section_data.to_vec();
|
||||
}
|
||||
3 => { // Debug Info
|
||||
3 => {
|
||||
// Debug Info
|
||||
module.debug_info = Some(parse_debug_section(section_data)?);
|
||||
}
|
||||
4 => { // Exports
|
||||
4 => {
|
||||
// Exports
|
||||
module.exports = parse_exports(section_data)?;
|
||||
}
|
||||
_ => {} // Skip unknown or optional sections
|
||||
@ -309,7 +342,7 @@ fn parse_const_pool(data: &[u8]) -> Result<Vec<ConstantPoolEntry>, LoadError> {
|
||||
let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
|
||||
let mut cp = Vec::with_capacity(count);
|
||||
let mut pos = 4;
|
||||
|
||||
|
||||
for _ in 0..count {
|
||||
if pos >= data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
@ -318,35 +351,52 @@ fn parse_const_pool(data: &[u8]) -> Result<Vec<ConstantPoolEntry>, LoadError> {
|
||||
pos += 1;
|
||||
match tag {
|
||||
0 => cp.push(ConstantPoolEntry::Null),
|
||||
1 => { // Int64
|
||||
if pos + 8 > data.len() { return Err(LoadError::UnexpectedEof); }
|
||||
let val = i64::from_le_bytes(data[pos..pos+8].try_into().unwrap());
|
||||
1 => {
|
||||
// Int64
|
||||
if pos + 8 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let val = i64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
|
||||
cp.push(ConstantPoolEntry::Int64(val));
|
||||
pos += 8;
|
||||
}
|
||||
2 => { // Float64
|
||||
if pos + 8 > data.len() { return Err(LoadError::UnexpectedEof); }
|
||||
let val = f64::from_le_bytes(data[pos..pos+8].try_into().unwrap());
|
||||
2 => {
|
||||
// Float64
|
||||
if pos + 8 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let val = f64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
|
||||
cp.push(ConstantPoolEntry::Float64(val));
|
||||
pos += 8;
|
||||
}
|
||||
3 => { // Boolean
|
||||
if pos >= data.len() { return Err(LoadError::UnexpectedEof); }
|
||||
3 => {
|
||||
// Boolean
|
||||
if pos >= data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
cp.push(ConstantPoolEntry::Boolean(data[pos] != 0));
|
||||
pos += 1;
|
||||
}
|
||||
4 => { // String
|
||||
if pos + 4 > data.len() { return Err(LoadError::UnexpectedEof); }
|
||||
let len = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap()) as usize;
|
||||
4 => {
|
||||
// String
|
||||
if pos + 4 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let len = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
|
||||
pos += 4;
|
||||
if pos + len > data.len() { return Err(LoadError::UnexpectedEof); }
|
||||
let s = String::from_utf8_lossy(&data[pos..pos+len]).into_owned();
|
||||
if pos + len > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let s = String::from_utf8_lossy(&data[pos..pos + len]).into_owned();
|
||||
cp.push(ConstantPoolEntry::String(s));
|
||||
pos += len;
|
||||
}
|
||||
5 => { // Int32
|
||||
if pos + 4 > data.len() { return Err(LoadError::UnexpectedEof); }
|
||||
let val = i32::from_le_bytes(data[pos..pos+4].try_into().unwrap());
|
||||
5 => {
|
||||
// Int32
|
||||
if pos + 4 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let val = i32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
|
||||
cp.push(ConstantPoolEntry::Int32(val));
|
||||
pos += 4;
|
||||
}
|
||||
@ -366,18 +416,18 @@ fn parse_functions(data: &[u8]) -> Result<Vec<FunctionMeta>, LoadError> {
|
||||
let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
|
||||
let mut functions = Vec::with_capacity(count);
|
||||
let mut pos = 4;
|
||||
|
||||
|
||||
for _ in 0..count {
|
||||
if pos + 16 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let code_offset = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap());
|
||||
let code_len = u32::from_le_bytes(data[pos+4..pos+8].try_into().unwrap());
|
||||
let param_slots = u16::from_le_bytes(data[pos+8..pos+10].try_into().unwrap());
|
||||
let local_slots = u16::from_le_bytes(data[pos+10..pos+12].try_into().unwrap());
|
||||
let return_slots = u16::from_le_bytes(data[pos+12..pos+14].try_into().unwrap());
|
||||
let max_stack_slots = u16::from_le_bytes(data[pos+14..pos+16].try_into().unwrap());
|
||||
|
||||
let code_offset = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
|
||||
let code_len = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
|
||||
let param_slots = u16::from_le_bytes(data[pos + 8..pos + 10].try_into().unwrap());
|
||||
let local_slots = u16::from_le_bytes(data[pos + 10..pos + 12].try_into().unwrap());
|
||||
let return_slots = u16::from_le_bytes(data[pos + 12..pos + 14].try_into().unwrap());
|
||||
let max_stack_slots = u16::from_le_bytes(data[pos + 14..pos + 16].try_into().unwrap());
|
||||
|
||||
functions.push(FunctionMeta {
|
||||
code_offset,
|
||||
code_len,
|
||||
@ -398,47 +448,47 @@ fn parse_debug_section(data: &[u8]) -> Result<DebugInfo, LoadError> {
|
||||
if data.len() < 8 {
|
||||
return Err(LoadError::MalformedSection);
|
||||
}
|
||||
|
||||
|
||||
let mut pos = 0;
|
||||
|
||||
|
||||
// PC to Span table
|
||||
let span_count = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap()) as usize;
|
||||
let span_count = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
|
||||
pos += 4;
|
||||
let mut pc_to_span = Vec::with_capacity(span_count);
|
||||
for _ in 0..span_count {
|
||||
if pos + 16 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let pc = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap());
|
||||
let file_id = u32::from_le_bytes(data[pos+4..pos+8].try_into().unwrap());
|
||||
let start = u32::from_le_bytes(data[pos+8..pos+12].try_into().unwrap());
|
||||
let end = u32::from_le_bytes(data[pos+12..pos+16].try_into().unwrap());
|
||||
let pc = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
|
||||
let file_id = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
|
||||
let start = u32::from_le_bytes(data[pos + 8..pos + 12].try_into().unwrap());
|
||||
let end = u32::from_le_bytes(data[pos + 12..pos + 16].try_into().unwrap());
|
||||
pc_to_span.push((pc, SourceSpan { file_id, start, end }));
|
||||
pos += 16;
|
||||
}
|
||||
|
||||
|
||||
// Function names table
|
||||
if pos + 4 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let func_name_count = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap()) as usize;
|
||||
let func_name_count = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
|
||||
pos += 4;
|
||||
let mut function_names = Vec::with_capacity(func_name_count);
|
||||
for _ in 0..func_name_count {
|
||||
if pos + 8 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let func_idx = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap());
|
||||
let name_len = u32::from_le_bytes(data[pos+4..pos+8].try_into().unwrap()) as usize;
|
||||
let func_idx = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
|
||||
let name_len = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap()) as usize;
|
||||
pos += 8;
|
||||
if pos + name_len > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let name = String::from_utf8_lossy(&data[pos..pos+name_len]).into_owned();
|
||||
let name = String::from_utf8_lossy(&data[pos..pos + name_len]).into_owned();
|
||||
function_names.push((func_idx, name));
|
||||
pos += name_len;
|
||||
}
|
||||
|
||||
|
||||
Ok(DebugInfo { pc_to_span, function_names })
|
||||
}
|
||||
|
||||
@ -457,20 +507,19 @@ fn parse_exports(data: &[u8]) -> Result<Vec<Export>, LoadError> {
|
||||
if pos + 8 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let func_idx = u32::from_le_bytes(data[pos..pos+4].try_into().unwrap());
|
||||
let name_len = u32::from_le_bytes(data[pos+4..pos+8].try_into().unwrap()) as usize;
|
||||
let func_idx = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
|
||||
let name_len = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap()) as usize;
|
||||
pos += 8;
|
||||
if pos + name_len > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let symbol = String::from_utf8_lossy(&data[pos..pos+name_len]).into_owned();
|
||||
let symbol = String::from_utf8_lossy(&data[pos..pos + name_len]).into_owned();
|
||||
exports.push(Export { symbol, func_idx });
|
||||
pos += name_len;
|
||||
}
|
||||
Ok(exports)
|
||||
}
|
||||
|
||||
|
||||
fn validate_module(module: &BytecodeModule) -> Result<(), LoadError> {
|
||||
for func in &module.functions {
|
||||
// Opcode stream bounds
|
||||
@ -485,22 +534,35 @@ fn validate_module(module: &BytecodeModule) -> Result<(), LoadError> {
|
||||
if pos + 2 > module.code.len() {
|
||||
break; // Unexpected EOF in middle of opcode, maybe should be error
|
||||
}
|
||||
let op_val = u16::from_le_bytes([module.code[pos], module.code[pos+1]]);
|
||||
let op_val = u16::from_le_bytes([module.code[pos], module.code[pos + 1]]);
|
||||
let opcode = OpCode::try_from(op_val).map_err(|_| LoadError::InvalidOpcode)?;
|
||||
pos += 2;
|
||||
|
||||
match opcode {
|
||||
OpCode::PushConst => {
|
||||
if pos + 4 > module.code.len() { return Err(LoadError::UnexpectedEof); }
|
||||
let idx = u32::from_le_bytes(module.code[pos..pos+4].try_into().unwrap()) as usize;
|
||||
if pos + 4 > module.code.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let idx =
|
||||
u32::from_le_bytes(module.code[pos..pos + 4].try_into().unwrap()) as usize;
|
||||
if idx >= module.const_pool.len() {
|
||||
return Err(LoadError::InvalidConstIndex);
|
||||
}
|
||||
pos += 4;
|
||||
}
|
||||
OpCode::PushI32 | OpCode::PushBounded | OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue
|
||||
| OpCode::GetGlobal | OpCode::SetGlobal | OpCode::GetLocal | OpCode::SetLocal
|
||||
| OpCode::PopN | OpCode::Syscall | OpCode::GateLoad | OpCode::GateStore => {
|
||||
OpCode::PushI32
|
||||
| OpCode::PushBounded
|
||||
| OpCode::Jmp
|
||||
| OpCode::JmpIfFalse
|
||||
| OpCode::JmpIfTrue
|
||||
| OpCode::GetGlobal
|
||||
| OpCode::SetGlobal
|
||||
| OpCode::GetLocal
|
||||
| OpCode::SetLocal
|
||||
| OpCode::PopN
|
||||
| OpCode::Syscall
|
||||
| OpCode::GateLoad
|
||||
| OpCode::GateStore => {
|
||||
pos += 4;
|
||||
}
|
||||
OpCode::PushI64 | OpCode::PushF64 => {
|
||||
@ -567,7 +629,7 @@ mod tests {
|
||||
data.extend_from_slice(&1u32.to_le_bytes());
|
||||
data.extend_from_slice(&80u32.to_le_bytes());
|
||||
data.extend_from_slice(&32u32.to_le_bytes());
|
||||
|
||||
|
||||
// Ensure data is long enough for the offsets
|
||||
data.resize(256, 0);
|
||||
|
||||
@ -581,7 +643,7 @@ mod tests {
|
||||
data.extend_from_slice(&0u32.to_le_bytes());
|
||||
data.extend_from_slice(&64u32.to_le_bytes());
|
||||
data.extend_from_slice(&1000u32.to_le_bytes());
|
||||
|
||||
|
||||
data.resize(256, 0);
|
||||
|
||||
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::SectionOutOfBounds));
|
||||
@ -594,21 +656,21 @@ mod tests {
|
||||
data.extend_from_slice(&1u32.to_le_bytes());
|
||||
data.extend_from_slice(&64u32.to_le_bytes());
|
||||
data.extend_from_slice(&20u32.to_le_bytes());
|
||||
|
||||
|
||||
// Section 2: Code, Kind 2, Offset 128, Length 10
|
||||
data.extend_from_slice(&2u32.to_le_bytes());
|
||||
data.extend_from_slice(&128u32.to_le_bytes());
|
||||
data.extend_from_slice(&10u32.to_le_bytes());
|
||||
|
||||
data.resize(256, 0);
|
||||
|
||||
|
||||
// Setup functions section
|
||||
let func_data_start = 64;
|
||||
data[func_data_start..func_data_start+4].copy_from_slice(&1u32.to_le_bytes()); // 1 function
|
||||
data[func_data_start..func_data_start + 4].copy_from_slice(&1u32.to_le_bytes()); // 1 function
|
||||
let entry_start = func_data_start + 4;
|
||||
data[entry_start..entry_start+4].copy_from_slice(&5u32.to_le_bytes()); // code_offset = 5
|
||||
data[entry_start+4..entry_start+8].copy_from_slice(&10u32.to_le_bytes()); // code_len = 10
|
||||
// 5 + 10 = 15 > 10 (code section length)
|
||||
data[entry_start..entry_start + 4].copy_from_slice(&5u32.to_le_bytes()); // code_offset = 5
|
||||
data[entry_start + 4..entry_start + 8].copy_from_slice(&10u32.to_le_bytes()); // code_len = 10
|
||||
// 5 + 10 = 15 > 10 (code section length)
|
||||
|
||||
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::InvalidFunctionIndex));
|
||||
}
|
||||
@ -620,17 +682,17 @@ mod tests {
|
||||
data.extend_from_slice(&0u32.to_le_bytes());
|
||||
data.extend_from_slice(&64u32.to_le_bytes());
|
||||
data.extend_from_slice(&4u32.to_le_bytes());
|
||||
|
||||
|
||||
// Section 2: Code, Kind 2, Offset 128, Length 6 (PushConst 0)
|
||||
data.extend_from_slice(&2u32.to_le_bytes());
|
||||
data.extend_from_slice(&128u32.to_le_bytes());
|
||||
data.extend_from_slice(&6u32.to_le_bytes());
|
||||
|
||||
data.resize(256, 0);
|
||||
|
||||
|
||||
// Setup empty CP
|
||||
data[64..68].copy_from_slice(&0u32.to_le_bytes());
|
||||
|
||||
|
||||
// Setup code with PushConst 0
|
||||
data[128..130].copy_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||
data[130..134].copy_from_slice(&0u32.to_le_bytes());
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/// Represents a single instruction in the Prometeu Virtual Machine.
|
||||
///
|
||||
///
|
||||
/// Each OpCode is encoded as a 16-bit unsigned integer (u16) in the bytecode.
|
||||
/// The PVM is a stack-based machine, meaning most instructions take their
|
||||
/// operands from the top of the stack and push their results back onto it.
|
||||
@ -7,7 +7,6 @@
|
||||
#[repr(u16)]
|
||||
pub enum OpCode {
|
||||
// --- 6.1 Execution Control ---
|
||||
|
||||
/// No operation. Does nothing for 1 cycle.
|
||||
Nop = 0x00,
|
||||
/// Stops the Virtual Machine execution immediately.
|
||||
@ -27,7 +26,6 @@ pub enum OpCode {
|
||||
Trap = 0x05,
|
||||
|
||||
// --- 6.2 Stack Manipulation ---
|
||||
|
||||
/// Loads a constant from the Constant Pool into the stack.
|
||||
/// Operand: index (u32)
|
||||
/// Stack: [] -> [value]
|
||||
@ -62,7 +60,6 @@ pub enum OpCode {
|
||||
PushBounded = 0x19,
|
||||
|
||||
// --- 6.3 Arithmetic ---
|
||||
|
||||
/// Adds the two top values (a + b).
|
||||
/// Stack: [a, b] -> [result]
|
||||
Add = 0x20,
|
||||
@ -86,7 +83,6 @@ pub enum OpCode {
|
||||
IntToBoundChecked = 0x26,
|
||||
|
||||
// --- 6.4 Comparison and Logic ---
|
||||
|
||||
/// Checks if a equals b.
|
||||
/// Stack: [a, b] -> [bool]
|
||||
Eq = 0x30,
|
||||
@ -134,7 +130,6 @@ pub enum OpCode {
|
||||
Neg = 0x3E,
|
||||
|
||||
// --- 6.5 Variables ---
|
||||
|
||||
/// Loads a value from a global variable slot.
|
||||
/// Operand: slot_index (u32)
|
||||
/// Stack: [] -> [value]
|
||||
@ -153,7 +148,6 @@ pub enum OpCode {
|
||||
SetLocal = 0x43,
|
||||
|
||||
// --- 6.6 Functions ---
|
||||
|
||||
/// Calls a function by its index in the function table.
|
||||
/// Operand: func_id (u32)
|
||||
/// Stack: [arg0, arg1, ...] -> [return_slots...]
|
||||
@ -167,7 +161,6 @@ pub enum OpCode {
|
||||
PopScope = 0x53,
|
||||
|
||||
// --- 6.7 HIP (Heap Interface Protocol) ---
|
||||
|
||||
/// Allocates `slots` slots on the heap with the given `type_id`.
|
||||
/// Operands: type_id (u32), slots (u32)
|
||||
/// Stack: [] -> [gate]
|
||||
@ -208,7 +201,6 @@ pub enum OpCode {
|
||||
GateRelease = 0x6A,
|
||||
|
||||
// --- 6.8 Peripherals and System ---
|
||||
|
||||
/// Invokes a system function (Firmware/OS).
|
||||
/// Operand: syscall_id (u32)
|
||||
/// Stack: [args...] -> [results...] (depends on syscall)
|
||||
|
||||
@ -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,16 +1,16 @@
|
||||
use crate::abi::TrapInfo;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use crate::model::{BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta};
|
||||
use crate::value::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Represents a fully linked, executable PBS program image.
|
||||
///
|
||||
/// Under the Prometeu architecture, the ProgramImage is a "closed-world" artifact
|
||||
/// produced by the compiler. All linking, relocation, and symbol resolution
|
||||
/// Under the Prometeu architecture, the ProgramImage is a "closed-world" artifact
|
||||
/// produced by the compiler. All linking, relocation, and symbol resolution
|
||||
/// MUST be performed by the compiler before this image is created.
|
||||
///
|
||||
/// The runtime (VM) assumes this image is authoritative and performs no
|
||||
/// The runtime (VM) assumes this image is authoritative and performs no
|
||||
/// additional linking or fixups.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ProgramImage {
|
||||
@ -22,7 +22,13 @@ pub struct ProgramImage {
|
||||
}
|
||||
|
||||
impl ProgramImage {
|
||||
pub fn new(rom: Vec<u8>, constant_pool: Vec<Value>, functions: Vec<FunctionMeta>, debug_info: Option<DebugInfo>, exports: HashMap<String, u32>) -> Self {
|
||||
pub fn new(
|
||||
rom: Vec<u8>,
|
||||
constant_pool: Vec<Value>,
|
||||
functions: Vec<FunctionMeta>,
|
||||
debug_info: Option<DebugInfo>,
|
||||
exports: HashMap<String, u32>,
|
||||
) -> Self {
|
||||
Self {
|
||||
rom: Arc::from(rom),
|
||||
constant_pool: Arc::from(constant_pool),
|
||||
@ -33,33 +39,27 @@ impl ProgramImage {
|
||||
}
|
||||
|
||||
pub fn create_trap(&self, code: u32, opcode: u16, mut message: String, pc: u32) -> TrapInfo {
|
||||
let span = self.debug_info.as_ref().and_then(|di| {
|
||||
di.pc_to_span.iter().find(|(p, _)| *p == pc).map(|(_, s)| s.clone())
|
||||
});
|
||||
let span = self
|
||||
.debug_info
|
||||
.as_ref()
|
||||
.and_then(|di| di.pc_to_span.iter().find(|(p, _)| *p == pc).map(|(_, s)| s.clone()));
|
||||
|
||||
if let Some(func_idx) = self.find_function_index(pc) {
|
||||
if let Some(func_name) = self.get_function_name(func_idx) {
|
||||
message = format!("{} (in function {})", message, func_name);
|
||||
}
|
||||
}
|
||||
|
||||
TrapInfo {
|
||||
code,
|
||||
opcode,
|
||||
message,
|
||||
pc,
|
||||
span,
|
||||
}
|
||||
|
||||
TrapInfo { code, opcode, message, pc, span }
|
||||
}
|
||||
|
||||
pub fn find_function_index(&self, pc: u32) -> Option<usize> {
|
||||
self.functions.iter().position(|f| {
|
||||
pc >= f.code_offset && pc < (f.code_offset + f.code_len)
|
||||
})
|
||||
self.functions.iter().position(|f| pc >= f.code_offset && pc < (f.code_offset + f.code_len))
|
||||
}
|
||||
|
||||
pub fn get_function_name(&self, func_idx: usize) -> Option<&str> {
|
||||
self.debug_info.as_ref()
|
||||
self.debug_info
|
||||
.as_ref()
|
||||
.and_then(|di| di.function_names.iter().find(|(idx, _)| *idx as usize == func_idx))
|
||||
.map(|(_, name)| name.as_str())
|
||||
}
|
||||
@ -67,49 +67,50 @@ impl ProgramImage {
|
||||
|
||||
impl From<BytecodeModule> for ProgramImage {
|
||||
fn from(module: BytecodeModule) -> Self {
|
||||
let constant_pool: Vec<Value> = module.const_pool.iter().map(|entry| {
|
||||
match entry {
|
||||
let constant_pool: Vec<Value> = module
|
||||
.const_pool
|
||||
.iter()
|
||||
.map(|entry| match entry {
|
||||
ConstantPoolEntry::Null => Value::Null,
|
||||
ConstantPoolEntry::Int64(v) => Value::Int64(*v),
|
||||
ConstantPoolEntry::Float64(v) => Value::Float(*v),
|
||||
ConstantPoolEntry::Boolean(v) => Value::Boolean(*v),
|
||||
ConstantPoolEntry::String(v) => Value::String(v.clone()),
|
||||
ConstantPoolEntry::Int32(v) => Value::Int32(*v),
|
||||
}
|
||||
}).collect();
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut exports = HashMap::new();
|
||||
for export in module.exports {
|
||||
exports.insert(export.symbol, export.func_idx);
|
||||
}
|
||||
|
||||
ProgramImage::new(
|
||||
module.code,
|
||||
constant_pool,
|
||||
module.functions,
|
||||
module.debug_info,
|
||||
exports,
|
||||
)
|
||||
ProgramImage::new(module.code, constant_pool, module.functions, module.debug_info, exports)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProgramImage> for BytecodeModule {
|
||||
fn from(program: ProgramImage) -> Self {
|
||||
let const_pool = program.constant_pool.iter().map(|v| match v {
|
||||
Value::Null => ConstantPoolEntry::Null,
|
||||
Value::Int64(v) => ConstantPoolEntry::Int64(*v),
|
||||
Value::Float(v) => ConstantPoolEntry::Float64(*v),
|
||||
Value::Boolean(v) => ConstantPoolEntry::Boolean(*v),
|
||||
Value::String(v) => ConstantPoolEntry::String(v.clone()),
|
||||
Value::Int32(v) => ConstantPoolEntry::Int32(*v),
|
||||
Value::Bounded(v) => ConstantPoolEntry::Int32(*v as i32),
|
||||
Value::Gate(_) => ConstantPoolEntry::Null,
|
||||
}).collect();
|
||||
let const_pool = program
|
||||
.constant_pool
|
||||
.iter()
|
||||
.map(|v| match v {
|
||||
Value::Null => ConstantPoolEntry::Null,
|
||||
Value::Int64(v) => ConstantPoolEntry::Int64(*v),
|
||||
Value::Float(v) => ConstantPoolEntry::Float64(*v),
|
||||
Value::Boolean(v) => ConstantPoolEntry::Boolean(*v),
|
||||
Value::String(v) => ConstantPoolEntry::String(v.clone()),
|
||||
Value::Int32(v) => ConstantPoolEntry::Int32(*v),
|
||||
Value::Bounded(v) => ConstantPoolEntry::Int32(*v as i32),
|
||||
Value::Gate(_) => ConstantPoolEntry::Null,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let exports = program.exports.iter().map(|(symbol, &func_idx)| Export {
|
||||
symbol: symbol.clone(),
|
||||
func_idx,
|
||||
}).collect();
|
||||
let exports = program
|
||||
.exports
|
||||
.iter()
|
||||
.map(|(symbol, &func_idx)| Export { symbol: symbol.clone(), func_idx })
|
||||
.collect();
|
||||
|
||||
BytecodeModule {
|
||||
version: 0,
|
||||
|
||||
@ -2,10 +2,10 @@ use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Ordering;
|
||||
|
||||
/// Represents any piece of data that can be stored on the VM stack or in globals.
|
||||
///
|
||||
/// The PVM is "dynamically typed" at the bytecode level, meaning a single
|
||||
/// `Value` enum can hold different primitive types. The VM performs
|
||||
/// automatic type promotion (e.g., adding an Int32 to a Float64 results
|
||||
///
|
||||
/// The PVM is "dynamically typed" at the bytecode level, meaning a single
|
||||
/// `Value` enum can hold different primitive types. The VM performs
|
||||
/// automatic type promotion (e.g., adding an Int32 to a Float64 results
|
||||
/// in a Float64) to ensure mathematical correctness.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::thread;
|
||||
use std::time::Instant;
|
||||
use prometeu_hal::asset::{AssetEntry, BankStats, BankType, HandleId, LoadStatus, PreloadEntry, SlotRef, SlotStats};
|
||||
use crate::memory_banks::{SoundBankPoolInstaller, TileBankPoolInstaller};
|
||||
use prometeu_hal::AssetBridge;
|
||||
use prometeu_hal::asset::{
|
||||
AssetEntry, BankStats, BankType, HandleId, LoadStatus, PreloadEntry, SlotRef, SlotStats,
|
||||
};
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::sample::Sample;
|
||||
use prometeu_hal::sound_bank::SoundBank;
|
||||
use prometeu_hal::tile_bank::{TileBank, TileSize};
|
||||
use crate::memory_banks::{SoundBankPoolInstaller, TileBankPoolInstaller};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::thread;
|
||||
use std::time::Instant;
|
||||
|
||||
/// Resident metadata for a decoded/materialized asset inside a BankPolicy.
|
||||
#[derive(Debug)]
|
||||
@ -21,7 +23,6 @@ pub struct ResidentEntry<T> {
|
||||
|
||||
// /// Pin count (optional): if > 0, entry should not be evicted by policy.
|
||||
// pub pins: u32,
|
||||
|
||||
/// Telemetry / profiling fields (optional but useful).
|
||||
pub loads: u64,
|
||||
pub last_used: Instant,
|
||||
@ -129,16 +130,41 @@ struct LoadHandleInfo {
|
||||
}
|
||||
|
||||
impl AssetBridge for AssetManager {
|
||||
fn initialize_for_cartridge(&self, assets: Vec<AssetEntry>, preload: Vec<PreloadEntry>, assets_data: Vec<u8>) { self.initialize_for_cartridge(assets, preload, assets_data) }
|
||||
fn load(&self, asset_name: &str, slot: SlotRef) -> Result<HandleId, String> { self.load(asset_name, slot) }
|
||||
fn status(&self, handle: HandleId) -> LoadStatus { self.status(handle) }
|
||||
fn commit(&self, handle: HandleId) { self.commit(handle) }
|
||||
fn cancel(&self, handle: HandleId) { self.cancel(handle) }
|
||||
fn apply_commits(&self) { self.apply_commits() }
|
||||
fn bank_info(&self, kind: BankType) -> BankStats { self.bank_info(kind) }
|
||||
fn slot_info(&self, slot: SlotRef) -> SlotStats { self.slot_info(slot) }
|
||||
fn find_slot_by_name(&self, asset_name: &str, kind: BankType) -> Option<u8> { self.find_slot_by_name(asset_name, kind) }
|
||||
fn shutdown(&self) { self.shutdown() }
|
||||
fn initialize_for_cartridge(
|
||||
&self,
|
||||
assets: Vec<AssetEntry>,
|
||||
preload: Vec<PreloadEntry>,
|
||||
assets_data: Vec<u8>,
|
||||
) {
|
||||
self.initialize_for_cartridge(assets, preload, assets_data)
|
||||
}
|
||||
fn load(&self, asset_name: &str, slot: SlotRef) -> Result<HandleId, String> {
|
||||
self.load(asset_name, slot)
|
||||
}
|
||||
fn status(&self, handle: HandleId) -> LoadStatus {
|
||||
self.status(handle)
|
||||
}
|
||||
fn commit(&self, handle: HandleId) {
|
||||
self.commit(handle)
|
||||
}
|
||||
fn cancel(&self, handle: HandleId) {
|
||||
self.cancel(handle)
|
||||
}
|
||||
fn apply_commits(&self) {
|
||||
self.apply_commits()
|
||||
}
|
||||
fn bank_info(&self, kind: BankType) -> BankStats {
|
||||
self.bank_info(kind)
|
||||
}
|
||||
fn slot_info(&self, slot: SlotRef) -> SlotStats {
|
||||
self.slot_info(slot)
|
||||
}
|
||||
fn find_slot_by_name(&self, asset_name: &str, kind: BankType) -> Option<u8> {
|
||||
self.find_slot_by_name(asset_name, kind)
|
||||
}
|
||||
fn shutdown(&self) {
|
||||
self.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetManager {
|
||||
@ -171,7 +197,12 @@ impl AssetManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize_for_cartridge(&self, assets: Vec<AssetEntry>, preload: Vec<PreloadEntry>, assets_data: Vec<u8>) {
|
||||
pub fn initialize_for_cartridge(
|
||||
&self,
|
||||
assets: Vec<AssetEntry>,
|
||||
preload: Vec<PreloadEntry>,
|
||||
assets_data: Vec<u8>,
|
||||
) {
|
||||
self.shutdown();
|
||||
{
|
||||
let mut asset_map = self.assets.write().unwrap();
|
||||
@ -190,45 +221,70 @@ impl AssetManager {
|
||||
let entry_opt = {
|
||||
let assets = self.assets.read().unwrap();
|
||||
let name_to_id = self.name_to_id.read().unwrap();
|
||||
name_to_id.get(&item.asset_name)
|
||||
.and_then(|id| assets.get(id))
|
||||
.cloned()
|
||||
name_to_id.get(&item.asset_name).and_then(|id| assets.get(id)).cloned()
|
||||
};
|
||||
|
||||
if let Some(entry) = entry_opt {
|
||||
let slot_index = item.slot;
|
||||
match entry.bank_type {
|
||||
BankType::TILES => {
|
||||
if let Ok(bank) = Self::perform_load_tile_bank(&entry, self.assets_data.clone()) {
|
||||
if let Ok(bank) =
|
||||
Self::perform_load_tile_bank(&entry, self.assets_data.clone())
|
||||
{
|
||||
let bank_arc = Arc::new(bank);
|
||||
self.gfx_policy.put_resident(entry.asset_id, Arc::clone(&bank_arc), entry.decoded_size as usize);
|
||||
self.gfx_policy.put_resident(
|
||||
entry.asset_id,
|
||||
Arc::clone(&bank_arc),
|
||||
entry.decoded_size as usize,
|
||||
);
|
||||
self.gfx_installer.install_tile_bank(slot_index, bank_arc);
|
||||
let mut slots = self.gfx_slots.write().unwrap();
|
||||
if slot_index < slots.len() {
|
||||
slots[slot_index] = Some(entry.asset_id);
|
||||
}
|
||||
println!("[AssetManager] Preloaded tile asset '{}' (id: {}) into slot {}", entry.asset_name, entry.asset_id, slot_index);
|
||||
println!(
|
||||
"[AssetManager] Preloaded tile asset '{}' (id: {}) into slot {}",
|
||||
entry.asset_name, entry.asset_id, slot_index
|
||||
);
|
||||
} else {
|
||||
eprintln!("[AssetManager] Failed to preload tile asset '{}'", entry.asset_name);
|
||||
eprintln!(
|
||||
"[AssetManager] Failed to preload tile asset '{}'",
|
||||
entry.asset_name
|
||||
);
|
||||
}
|
||||
}
|
||||
BankType::SOUNDS => {
|
||||
if let Ok(bank) = Self::perform_load_sound_bank(&entry, self.assets_data.clone()) {
|
||||
if let Ok(bank) =
|
||||
Self::perform_load_sound_bank(&entry, self.assets_data.clone())
|
||||
{
|
||||
let bank_arc = Arc::new(bank);
|
||||
self.sound_policy.put_resident(entry.asset_id, Arc::clone(&bank_arc), entry.decoded_size as usize);
|
||||
self.sound_policy.put_resident(
|
||||
entry.asset_id,
|
||||
Arc::clone(&bank_arc),
|
||||
entry.decoded_size as usize,
|
||||
);
|
||||
self.sound_installer.install_sound_bank(slot_index, bank_arc);
|
||||
let mut slots = self.sound_slots.write().unwrap();
|
||||
if slot_index < slots.len() {
|
||||
slots[slot_index] = Some(entry.asset_id);
|
||||
}
|
||||
println!("[AssetManager] Preloaded sound asset '{}' (id: {}) into slot {}", entry.asset_name, entry.asset_id, slot_index);
|
||||
println!(
|
||||
"[AssetManager] Preloaded sound asset '{}' (id: {}) into slot {}",
|
||||
entry.asset_name, entry.asset_id, slot_index
|
||||
);
|
||||
} else {
|
||||
eprintln!("[AssetManager] Failed to preload sound asset '{}'", entry.asset_name);
|
||||
eprintln!(
|
||||
"[AssetManager] Failed to preload sound asset '{}'",
|
||||
entry.asset_name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("[AssetManager] Preload failed: asset '{}' not found in table", item.asset_name);
|
||||
eprintln!(
|
||||
"[AssetManager] Preload failed: asset '{}' not found in table",
|
||||
item.asset_name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -237,7 +293,9 @@ impl AssetManager {
|
||||
let entry = {
|
||||
let assets = self.assets.read().unwrap();
|
||||
let name_to_id = self.name_to_id.read().unwrap();
|
||||
let id = name_to_id.get(asset_name).ok_or_else(|| format!("Asset not found: {}", asset_name))?;
|
||||
let id = name_to_id
|
||||
.get(asset_name)
|
||||
.ok_or_else(|| format!("Asset not found: {}", asset_name))?;
|
||||
assets.get(id).ok_or_else(|| format!("Asset ID {} not found in table", id))?.clone()
|
||||
};
|
||||
let asset_id = entry.asset_id;
|
||||
@ -256,31 +314,33 @@ impl AssetManager {
|
||||
if let Some(bank) = self.gfx_policy.get_resident(asset_id) {
|
||||
self.gfx_policy.stage(handle_id, bank);
|
||||
true
|
||||
} else { false }
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
BankType::SOUNDS => {
|
||||
if let Some(bank) = self.sound_policy.get_resident(asset_id) {
|
||||
self.sound_policy.stage(handle_id, bank);
|
||||
true
|
||||
} else { false }
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if already_resident {
|
||||
self.handles.write().unwrap().insert(handle_id, LoadHandleInfo {
|
||||
_asset_id: asset_id,
|
||||
slot,
|
||||
status: LoadStatus::READY,
|
||||
});
|
||||
self.handles.write().unwrap().insert(
|
||||
handle_id,
|
||||
LoadHandleInfo { _asset_id: asset_id, slot, status: LoadStatus::READY },
|
||||
);
|
||||
return Ok(handle_id);
|
||||
}
|
||||
|
||||
// Not resident, start loading
|
||||
self.handles.write().unwrap().insert(handle_id, LoadHandleInfo {
|
||||
_asset_id: asset_id,
|
||||
slot,
|
||||
status: LoadStatus::PENDING,
|
||||
});
|
||||
self.handles.write().unwrap().insert(
|
||||
handle_id,
|
||||
LoadHandleInfo { _asset_id: asset_id, slot, status: LoadStatus::PENDING },
|
||||
);
|
||||
|
||||
let handles = self.handles.clone();
|
||||
let assets_data = self.assets_data.clone();
|
||||
@ -319,7 +379,10 @@ impl AssetManager {
|
||||
existing.loads += 1;
|
||||
Arc::clone(&existing.value)
|
||||
} else {
|
||||
let entry = ResidentEntry::new(Arc::clone(&bank_arc), entry_clone.decoded_size as usize);
|
||||
let entry = ResidentEntry::new(
|
||||
Arc::clone(&bank_arc),
|
||||
entry_clone.decoded_size as usize,
|
||||
);
|
||||
map.insert(asset_id, entry);
|
||||
bank_arc
|
||||
}
|
||||
@ -327,11 +390,15 @@ impl AssetManager {
|
||||
gfx_policy_staging.write().unwrap().insert(handle_id, resident_arc);
|
||||
let mut handles_map = handles.write().unwrap();
|
||||
if let Some(h) = handles_map.get_mut(&handle_id) {
|
||||
if h.status == LoadStatus::LOADING { h.status = LoadStatus::READY; }
|
||||
if h.status == LoadStatus::LOADING {
|
||||
h.status = LoadStatus::READY;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut handles_map = handles.write().unwrap();
|
||||
if let Some(h) = handles_map.get_mut(&handle_id) { h.status = LoadStatus::ERROR; }
|
||||
if let Some(h) = handles_map.get_mut(&handle_id) {
|
||||
h.status = LoadStatus::ERROR;
|
||||
}
|
||||
}
|
||||
}
|
||||
BankType::SOUNDS => {
|
||||
@ -345,7 +412,10 @@ impl AssetManager {
|
||||
existing.loads += 1;
|
||||
Arc::clone(&existing.value)
|
||||
} else {
|
||||
let entry = ResidentEntry::new(Arc::clone(&bank_arc), entry_clone.decoded_size as usize);
|
||||
let entry = ResidentEntry::new(
|
||||
Arc::clone(&bank_arc),
|
||||
entry_clone.decoded_size as usize,
|
||||
);
|
||||
map.insert(asset_id, entry);
|
||||
bank_arc
|
||||
}
|
||||
@ -353,11 +423,15 @@ impl AssetManager {
|
||||
sound_policy_staging.write().unwrap().insert(handle_id, resident_arc);
|
||||
let mut handles_map = handles.write().unwrap();
|
||||
if let Some(h) = handles_map.get_mut(&handle_id) {
|
||||
if h.status == LoadStatus::LOADING { h.status = LoadStatus::READY; }
|
||||
if h.status == LoadStatus::LOADING {
|
||||
h.status = LoadStatus::READY;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut handles_map = handles.write().unwrap();
|
||||
if let Some(h) = handles_map.get_mut(&handle_id) { h.status = LoadStatus::ERROR; }
|
||||
if let Some(h) = handles_map.get_mut(&handle_id) {
|
||||
h.status = LoadStatus::ERROR;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -366,16 +440,19 @@ impl AssetManager {
|
||||
Ok(handle_id)
|
||||
}
|
||||
|
||||
fn perform_load_tile_bank(entry: &AssetEntry, assets_data: Arc<RwLock<Vec<u8>>>) -> Result<TileBank, String> {
|
||||
fn perform_load_tile_bank(
|
||||
entry: &AssetEntry,
|
||||
assets_data: Arc<RwLock<Vec<u8>>>,
|
||||
) -> Result<TileBank, String> {
|
||||
if entry.codec != "RAW" {
|
||||
return Err(format!("Unsupported codec: {}", entry.codec));
|
||||
}
|
||||
|
||||
let assets_data = assets_data.read().unwrap();
|
||||
|
||||
|
||||
let start = entry.offset as usize;
|
||||
let end = start + entry.size as usize;
|
||||
|
||||
|
||||
if end > assets_data.len() {
|
||||
return Err("Asset offset/size out of bounds".to_string());
|
||||
}
|
||||
@ -383,9 +460,12 @@ impl AssetManager {
|
||||
let buffer = &assets_data[start..end];
|
||||
|
||||
// Decode TILEBANK metadata
|
||||
let tile_size_val = entry.metadata.get("tile_size").and_then(|v| v.as_u64()).ok_or("Missing tile_size")?;
|
||||
let width = entry.metadata.get("width").and_then(|v| v.as_u64()).ok_or("Missing width")? as usize;
|
||||
let height = entry.metadata.get("height").and_then(|v| v.as_u64()).ok_or("Missing height")? as usize;
|
||||
let tile_size_val =
|
||||
entry.metadata.get("tile_size").and_then(|v| v.as_u64()).ok_or("Missing tile_size")?;
|
||||
let width =
|
||||
entry.metadata.get("width").and_then(|v| v.as_u64()).ok_or("Missing width")? as usize;
|
||||
let height =
|
||||
entry.metadata.get("height").and_then(|v| v.as_u64()).ok_or("Missing height")? as usize;
|
||||
|
||||
let tile_size = match tile_size_val {
|
||||
8 => TileSize::Size8,
|
||||
@ -406,42 +486,41 @@ impl AssetManager {
|
||||
for p in 0..64 {
|
||||
for c in 0..16 {
|
||||
let offset = (p * 16 + c) * 2;
|
||||
let color_raw = u16::from_le_bytes([palette_data[offset], palette_data[offset + 1]]);
|
||||
let color_raw =
|
||||
u16::from_le_bytes([palette_data[offset], palette_data[offset + 1]]);
|
||||
palettes[p][c] = Color(color_raw);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(TileBank {
|
||||
tile_size,
|
||||
width,
|
||||
height,
|
||||
pixel_indices,
|
||||
palettes,
|
||||
})
|
||||
Ok(TileBank { tile_size, width, height, pixel_indices, palettes })
|
||||
}
|
||||
|
||||
fn perform_load_sound_bank(entry: &AssetEntry, assets_data: Arc<RwLock<Vec<u8>>>) -> Result<SoundBank, String> {
|
||||
fn perform_load_sound_bank(
|
||||
entry: &AssetEntry,
|
||||
assets_data: Arc<RwLock<Vec<u8>>>,
|
||||
) -> Result<SoundBank, String> {
|
||||
if entry.codec != "RAW" {
|
||||
return Err(format!("Unsupported codec: {}", entry.codec));
|
||||
}
|
||||
|
||||
let assets_data = assets_data.read().unwrap();
|
||||
|
||||
|
||||
let start = entry.offset as usize;
|
||||
let end = start + entry.size as usize;
|
||||
|
||||
|
||||
if end > assets_data.len() {
|
||||
return Err("Asset offset/size out of bounds".to_string());
|
||||
}
|
||||
|
||||
let buffer = &assets_data[start..end];
|
||||
|
||||
let sample_rate = entry.metadata.get("sample_rate").and_then(|v| v.as_u64()).unwrap_or(44100) as u32;
|
||||
|
||||
let sample_rate =
|
||||
entry.metadata.get("sample_rate").and_then(|v| v.as_u64()).unwrap_or(44100) as u32;
|
||||
|
||||
let mut data = Vec::with_capacity(buffer.len() / 2);
|
||||
for i in (0..buffer.len()).step_by(2) {
|
||||
if i + 1 < buffer.len() {
|
||||
data.push(i16::from_le_bytes([buffer[i], buffer[i+1]]));
|
||||
data.push(i16::from_le_bytes([buffer[i], buffer[i + 1]]));
|
||||
}
|
||||
}
|
||||
|
||||
@ -526,7 +605,7 @@ impl AssetManager {
|
||||
let staging = self.gfx_policy.staging.read().unwrap();
|
||||
let assets = self.assets.read().unwrap();
|
||||
let handles = self.handles.read().unwrap();
|
||||
|
||||
|
||||
for (handle_id, _) in staging.iter() {
|
||||
if let Some(h) = handles.get(handle_id) {
|
||||
if let Some(entry) = assets.get(&h._asset_id) {
|
||||
@ -540,7 +619,9 @@ impl AssetManager {
|
||||
{
|
||||
let slots = self.gfx_slots.read().unwrap();
|
||||
for s in slots.iter() {
|
||||
if s.is_some() { slots_occupied += 1; }
|
||||
if s.is_some() {
|
||||
slots_occupied += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -567,7 +648,7 @@ impl AssetManager {
|
||||
let staging = self.sound_policy.staging.read().unwrap();
|
||||
let assets = self.assets.read().unwrap();
|
||||
let handles = self.handles.read().unwrap();
|
||||
|
||||
|
||||
for (handle_id, _) in staging.iter() {
|
||||
if let Some(h) = handles.get(handle_id) {
|
||||
if let Some(entry) = assets.get(&h._asset_id) {
|
||||
@ -581,7 +662,9 @@ impl AssetManager {
|
||||
{
|
||||
let slots = self.sound_slots.read().unwrap();
|
||||
for s in slots.iter() {
|
||||
if s.is_some() { slots_occupied += 1; }
|
||||
if s.is_some() {
|
||||
slots_occupied += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -602,9 +685,13 @@ impl AssetManager {
|
||||
BankType::TILES => {
|
||||
let slots = self.gfx_slots.read().unwrap();
|
||||
let asset_id = slots.get(slot.index).and_then(|s| s.clone());
|
||||
|
||||
|
||||
let (bytes, asset_name) = if let Some(id) = &asset_id {
|
||||
let bytes = self.gfx_policy.resident.read().unwrap()
|
||||
let bytes = self
|
||||
.gfx_policy
|
||||
.resident
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(id)
|
||||
.map(|entry| entry.bytes)
|
||||
.unwrap_or(0);
|
||||
@ -614,19 +701,18 @@ impl AssetManager {
|
||||
(0, None)
|
||||
};
|
||||
|
||||
SlotStats {
|
||||
asset_id,
|
||||
asset_name,
|
||||
generation: 0,
|
||||
resident_bytes: bytes,
|
||||
}
|
||||
SlotStats { asset_id, asset_name, generation: 0, resident_bytes: bytes }
|
||||
}
|
||||
BankType::SOUNDS => {
|
||||
let slots = self.sound_slots.read().unwrap();
|
||||
let asset_id = slots.get(slot.index).and_then(|s| s.clone());
|
||||
|
||||
|
||||
let (bytes, asset_name) = if let Some(id) = &asset_id {
|
||||
let bytes = self.sound_policy.resident.read().unwrap()
|
||||
let bytes = self
|
||||
.sound_policy
|
||||
.resident
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(id)
|
||||
.map(|entry| entry.bytes)
|
||||
.unwrap_or(0);
|
||||
@ -636,12 +722,7 @@ impl AssetManager {
|
||||
(0, None)
|
||||
};
|
||||
|
||||
SlotStats {
|
||||
asset_id,
|
||||
asset_name,
|
||||
generation: 0,
|
||||
resident_bytes: bytes,
|
||||
}
|
||||
SlotStats { asset_id, asset_name, generation: 0, resident_bytes: bytes }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -663,7 +744,7 @@ impl AssetManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn shutdown(&self) {
|
||||
self.gfx_policy.clear();
|
||||
self.sound_policy.clear();
|
||||
@ -705,18 +786,18 @@ mod tests {
|
||||
|
||||
let am = AssetManager::new(vec![asset_entry], data, gfx_installer, sound_installer);
|
||||
let slot = SlotRef::gfx(0);
|
||||
|
||||
|
||||
let handle = am.load("test_tiles", slot).expect("Should start loading");
|
||||
|
||||
|
||||
let mut status = am.status(handle);
|
||||
let start = Instant::now();
|
||||
while status != LoadStatus::READY && start.elapsed().as_secs() < 5 {
|
||||
thread::sleep(std::time::Duration::from_millis(10));
|
||||
status = am.status(handle);
|
||||
}
|
||||
|
||||
|
||||
assert_eq!(status, LoadStatus::READY);
|
||||
|
||||
|
||||
{
|
||||
let staging = am.gfx_policy.staging.read().unwrap();
|
||||
assert!(staging.contains_key(&handle));
|
||||
@ -724,7 +805,7 @@ mod tests {
|
||||
|
||||
am.commit(handle);
|
||||
am.apply_commits();
|
||||
|
||||
|
||||
assert_eq!(am.status(handle), LoadStatus::COMMITTED);
|
||||
assert!(banks.tile_bank_slot(0).is_some());
|
||||
}
|
||||
@ -754,13 +835,13 @@ mod tests {
|
||||
};
|
||||
|
||||
let am = AssetManager::new(vec![asset_entry], data, gfx_installer, sound_installer);
|
||||
|
||||
|
||||
let handle1 = am.load("test_tiles", SlotRef::gfx(0)).unwrap();
|
||||
let start = Instant::now();
|
||||
while am.status(handle1) != LoadStatus::READY && start.elapsed().as_secs() < 5 {
|
||||
thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
|
||||
|
||||
let handle2 = am.load("test_tiles", SlotRef::gfx(1)).unwrap();
|
||||
assert_eq!(am.status(handle2), LoadStatus::READY);
|
||||
|
||||
@ -794,19 +875,19 @@ mod tests {
|
||||
|
||||
let am = AssetManager::new(vec![asset_entry], data, gfx_installer, sound_installer);
|
||||
let slot = SlotRef::audio(0);
|
||||
|
||||
|
||||
let handle = am.load("test_sound", slot).expect("Should start loading");
|
||||
|
||||
|
||||
let start = Instant::now();
|
||||
while am.status(handle) != LoadStatus::READY && start.elapsed().as_secs() < 5 {
|
||||
thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
|
||||
|
||||
assert_eq!(am.status(handle), LoadStatus::READY);
|
||||
|
||||
|
||||
am.commit(handle);
|
||||
am.apply_commits();
|
||||
|
||||
|
||||
assert_eq!(am.status(handle), LoadStatus::COMMITTED);
|
||||
assert!(banks.sound_bank_slot(0).is_some());
|
||||
}
|
||||
@ -818,7 +899,7 @@ mod tests {
|
||||
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
||||
|
||||
let data = vec![0u8; 200];
|
||||
|
||||
|
||||
let asset_entry = AssetEntry {
|
||||
asset_id: 2,
|
||||
asset_name: "preload_sound".to_string(),
|
||||
@ -832,17 +913,15 @@ mod tests {
|
||||
}),
|
||||
};
|
||||
|
||||
let preload = vec![
|
||||
PreloadEntry { asset_name: "preload_sound".to_string(), slot: 5 }
|
||||
];
|
||||
let preload = vec![PreloadEntry { asset_name: "preload_sound".to_string(), slot: 5 }];
|
||||
|
||||
let am = AssetManager::new(vec![], vec![], gfx_installer, sound_installer);
|
||||
|
||||
|
||||
// Before init, slot 5 is empty
|
||||
assert!(banks.sound_bank_slot(5).is_none());
|
||||
|
||||
am.initialize_for_cartridge(vec![asset_entry], preload, data);
|
||||
|
||||
|
||||
// After init, slot 5 should be occupied because of preload
|
||||
assert!(banks.sound_bank_slot(5).is_some());
|
||||
assert_eq!(am.slot_info(SlotRef::audio(5)).asset_id, Some(2));
|
||||
@ -868,9 +947,7 @@ mod tests {
|
||||
metadata: serde_json::json!({ "tile_size": 16, "width": 16, "height": 16 }),
|
||||
};
|
||||
|
||||
let preload = vec![
|
||||
PreloadEntry { asset_name: "my_tiles".to_string(), slot: 3 }
|
||||
];
|
||||
let preload = vec![PreloadEntry { asset_name: "my_tiles".to_string(), slot: 3 }];
|
||||
|
||||
let am = AssetManager::new(vec![], vec![], gfx_installer, sound_installer);
|
||||
am.initialize_for_cartridge(vec![asset_entry], preload, data);
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
use std::sync::Arc;
|
||||
use prometeu_hal::AudioBridge;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Maximum number of simultaneous audio voices supported by the hardware.
|
||||
pub const MAX_CHANNELS: usize = 16;
|
||||
/// Standard sample rate for the final audio output.
|
||||
pub const OUTPUT_SAMPLE_RATE: u32 = 48000;
|
||||
|
||||
use crate::memory_banks::SoundBankPoolAccess;
|
||||
/// Looping mode for samples (re-exported from the hardware contract).
|
||||
pub use prometeu_hal::LoopMode;
|
||||
use prometeu_hal::sample::Sample;
|
||||
use crate::memory_banks::SoundBankPoolAccess;
|
||||
|
||||
/// State of a single playback voice (channel).
|
||||
///
|
||||
///
|
||||
/// The Core maintains this state to provide information to the App (e.g., is_playing),
|
||||
/// but the actual real-time mixing is performed by the Host using commands.
|
||||
pub struct Channel {
|
||||
@ -50,7 +50,7 @@ impl Default for Channel {
|
||||
}
|
||||
|
||||
/// Commands sent from the Core to the Host audio backend.
|
||||
///
|
||||
///
|
||||
/// Because the Core logic runs at 60Hz and Audio is generated at 48kHz,
|
||||
/// we use an asynchronous command queue to synchronize them.
|
||||
pub enum AudioCommand {
|
||||
@ -65,24 +65,13 @@ pub enum AudioCommand {
|
||||
loop_mode: LoopMode,
|
||||
},
|
||||
/// Immediately stop playback on a voice.
|
||||
Stop {
|
||||
voice_id: usize,
|
||||
},
|
||||
Stop { voice_id: usize },
|
||||
/// Update volume of an ongoing playback.
|
||||
SetVolume {
|
||||
voice_id: usize,
|
||||
volume: u8,
|
||||
},
|
||||
SetVolume { voice_id: usize, volume: u8 },
|
||||
/// Update panning of an ongoing playback.
|
||||
SetPan {
|
||||
voice_id: usize,
|
||||
pan: u8,
|
||||
},
|
||||
SetPan { voice_id: usize, pan: u8 },
|
||||
/// Update pitch of an ongoing playback.
|
||||
SetPitch {
|
||||
voice_id: usize,
|
||||
pitch: f64,
|
||||
},
|
||||
SetPitch { voice_id: usize, pitch: f64 },
|
||||
/// Pause all audio processing.
|
||||
MasterPause,
|
||||
/// Resume audio processing.
|
||||
@ -90,12 +79,12 @@ pub enum AudioCommand {
|
||||
}
|
||||
|
||||
/// PROMETEU Audio Subsystem.
|
||||
///
|
||||
/// Models a multi-channel PCM sampler (SPU).
|
||||
///
|
||||
/// Models a multi-channel PCM sampler (SPU).
|
||||
/// The audio system in Prometeu is **command-based**. This means the Core
|
||||
/// doesn't generate raw audio samples; instead, it sends high-level commands
|
||||
/// (like `Play`, `Stop`, `SetVolume`) to a queue. The physical host backend
|
||||
/// (e.g., CPAL on desktop) then consumes these commands and performs the
|
||||
/// doesn't generate raw audio samples; instead, it sends high-level commands
|
||||
/// (like `Play`, `Stop`, `SetVolume`) to a queue. The physical host backend
|
||||
/// (e.g., CPAL on desktop) then consumes these commands and performs the
|
||||
/// actual mixing at the native hardware sample rate.
|
||||
///
|
||||
/// ### Key Features:
|
||||
@ -103,7 +92,7 @@ pub enum AudioCommand {
|
||||
/// - **Sample-based Synthesis**: Plays PCM data stored in SoundBanks.
|
||||
/// - **Stereo Output**: 48kHz output target.
|
||||
pub struct Audio {
|
||||
/// Local state of the hardware voices. This state is used for logic
|
||||
/// Local state of the hardware voices. This state is used for logic
|
||||
/// (e.g., checking if a sound is still playing) and is synchronized with the Host.
|
||||
pub voices: [Channel; MAX_CHANNELS],
|
||||
/// Queue of pending commands to be processed by the Host mixer.
|
||||
@ -114,20 +103,57 @@ pub struct Audio {
|
||||
}
|
||||
|
||||
impl AudioBridge for Audio {
|
||||
fn play(&mut self, bank_id: u8, sample_id: u16, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: prometeu_hal::LoopMode) {
|
||||
let lm = match loop_mode { prometeu_hal::LoopMode::Off => LoopMode::Off, prometeu_hal::LoopMode::On => LoopMode::On };
|
||||
fn play(
|
||||
&mut self,
|
||||
bank_id: u8,
|
||||
sample_id: u16,
|
||||
voice_id: usize,
|
||||
volume: u8,
|
||||
pan: u8,
|
||||
pitch: f64,
|
||||
priority: u8,
|
||||
loop_mode: prometeu_hal::LoopMode,
|
||||
) {
|
||||
let lm = match loop_mode {
|
||||
prometeu_hal::LoopMode::Off => LoopMode::Off,
|
||||
prometeu_hal::LoopMode::On => LoopMode::On,
|
||||
};
|
||||
self.play(bank_id, sample_id, voice_id, volume, pan, pitch, priority, lm)
|
||||
}
|
||||
fn play_sample(&mut self, sample: Arc<Sample>, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: prometeu_hal::LoopMode) {
|
||||
let lm = match loop_mode { prometeu_hal::LoopMode::Off => LoopMode::Off, prometeu_hal::LoopMode::On => LoopMode::On };
|
||||
fn play_sample(
|
||||
&mut self,
|
||||
sample: Arc<Sample>,
|
||||
voice_id: usize,
|
||||
volume: u8,
|
||||
pan: u8,
|
||||
pitch: f64,
|
||||
priority: u8,
|
||||
loop_mode: prometeu_hal::LoopMode,
|
||||
) {
|
||||
let lm = match loop_mode {
|
||||
prometeu_hal::LoopMode::Off => LoopMode::Off,
|
||||
prometeu_hal::LoopMode::On => LoopMode::On,
|
||||
};
|
||||
self.play_sample(sample, voice_id, volume, pan, pitch, priority, lm)
|
||||
}
|
||||
fn stop(&mut self, voice_id: usize) { self.stop(voice_id) }
|
||||
fn set_volume(&mut self, voice_id: usize, volume: u8) { self.set_volume(voice_id, volume) }
|
||||
fn set_pan(&mut self, voice_id: usize, pan: u8) { self.set_pan(voice_id, pan) }
|
||||
fn set_pitch(&mut self, voice_id: usize, pitch: f64) { self.set_pitch(voice_id, pitch) }
|
||||
fn is_playing(&self, voice_id: usize) -> bool { self.is_playing(voice_id) }
|
||||
fn clear_commands(&mut self) { self.clear_commands() }
|
||||
fn stop(&mut self, voice_id: usize) {
|
||||
self.stop(voice_id)
|
||||
}
|
||||
fn set_volume(&mut self, voice_id: usize, volume: u8) {
|
||||
self.set_volume(voice_id, volume)
|
||||
}
|
||||
fn set_pan(&mut self, voice_id: usize, pan: u8) {
|
||||
self.set_pan(voice_id, pan)
|
||||
}
|
||||
fn set_pitch(&mut self, voice_id: usize, pitch: f64) {
|
||||
self.set_pitch(voice_id, pitch)
|
||||
}
|
||||
fn is_playing(&self, voice_id: usize) -> bool {
|
||||
self.is_playing(voice_id)
|
||||
}
|
||||
fn clear_commands(&mut self) {
|
||||
self.clear_commands()
|
||||
}
|
||||
}
|
||||
|
||||
impl Audio {
|
||||
@ -140,25 +166,51 @@ impl Audio {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play(&mut self, bank_id: u8, sample_id: u16, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: LoopMode) {
|
||||
pub fn play(
|
||||
&mut self,
|
||||
bank_id: u8,
|
||||
sample_id: u16,
|
||||
voice_id: usize,
|
||||
volume: u8,
|
||||
pan: u8,
|
||||
pitch: f64,
|
||||
priority: u8,
|
||||
loop_mode: LoopMode,
|
||||
) {
|
||||
if voice_id >= MAX_CHANNELS {
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve the sample from the hardware pools
|
||||
let sample = self.sound_banks.sound_bank_slot(bank_id as usize)
|
||||
let sample = self
|
||||
.sound_banks
|
||||
.sound_bank_slot(bank_id as usize)
|
||||
.and_then(|bank| bank.samples.get(sample_id as usize).map(Arc::clone));
|
||||
|
||||
|
||||
if let Some(s) = sample {
|
||||
println!("[Audio] Resolved sample from bank {} sample {}. Playing on voice {}.", bank_id, sample_id, voice_id);
|
||||
println!(
|
||||
"[Audio] Resolved sample from bank {} sample {}. Playing on voice {}.",
|
||||
bank_id, sample_id, voice_id
|
||||
);
|
||||
self.play_sample(s, voice_id, volume, pan, pitch, priority, loop_mode);
|
||||
} else {
|
||||
eprintln!("[Audio] Failed to resolve sample from bank {} sample {}.", bank_id, sample_id);
|
||||
eprintln!(
|
||||
"[Audio] Failed to resolve sample from bank {} sample {}.",
|
||||
bank_id, sample_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play_sample(&mut self, sample: Arc<Sample>, voice_id: usize, volume: u8, pan: u8, pitch: f64, priority: u8, loop_mode: LoopMode) {
|
||||
pub fn play_sample(
|
||||
&mut self,
|
||||
sample: Arc<Sample>,
|
||||
voice_id: usize,
|
||||
volume: u8,
|
||||
pan: u8,
|
||||
pitch: f64,
|
||||
priority: u8,
|
||||
loop_mode: LoopMode,
|
||||
) {
|
||||
if voice_id >= MAX_CHANNELS {
|
||||
return;
|
||||
}
|
||||
@ -217,11 +269,7 @@ impl Audio {
|
||||
}
|
||||
|
||||
pub fn is_playing(&self, voice_id: usize) -> bool {
|
||||
if voice_id < MAX_CHANNELS {
|
||||
self.voices[voice_id].active
|
||||
} else {
|
||||
false
|
||||
}
|
||||
if voice_id < MAX_CHANNELS { self.voices[voice_id].active } else { false }
|
||||
}
|
||||
|
||||
/// Clears the command queue. The Host should consume this every frame.
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
use crate::memory_banks::TileBankPoolAccess;
|
||||
use std::sync::Arc;
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::GfxBridge;
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::sprite::Sprite;
|
||||
use prometeu_hal::tile::Tile;
|
||||
use prometeu_hal::tile_bank::{TileBank, TileSize};
|
||||
use prometeu_hal::tile_layer::{HudTileLayer, ScrollableTileLayer, TileMap};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Blending modes inspired by classic 16-bit hardware.
|
||||
/// Defines how source pixels are combined with existing pixels in the framebuffer.
|
||||
///
|
||||
///
|
||||
/// ### Usage Example:
|
||||
/// // Draw a semi-transparent blue rectangle
|
||||
/// gfx.fill_rect_blend(10, 10, 50, 50, Color::BLUE, BlendMode::Half);
|
||||
@ -31,10 +31,10 @@ pub enum BlendMode {
|
||||
/// PROMETEU Graphics Subsystem (GFX).
|
||||
///
|
||||
/// Models a specialized graphics chip with a fixed resolution, double buffering,
|
||||
/// and a multi-layered tile/sprite architecture.
|
||||
///
|
||||
/// The GFX system works by composing several "layers" into a single 16-bit
|
||||
/// RGB565 framebuffer. It supports hardware-accelerated primitives (lines, rects)
|
||||
/// and a multi-layered tile/sprite architecture.
|
||||
///
|
||||
/// The GFX system works by composing several "layers" into a single 16-bit
|
||||
/// RGB565 framebuffer. It supports hardware-accelerated primitives (lines, rects)
|
||||
/// and specialized console features like background scrolling and sprite sorting.
|
||||
///
|
||||
/// ### Layer Composition Order (back to front):
|
||||
@ -79,44 +79,139 @@ pub struct Gfx {
|
||||
}
|
||||
|
||||
impl GfxBridge for Gfx {
|
||||
fn size(&self) -> (usize, usize) { self.size() }
|
||||
fn front_buffer(&self) -> &[u16] { self.front_buffer() }
|
||||
fn clear(&mut self, color: Color) { self.clear(color) }
|
||||
fn fill_rect_blend(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color, mode: prometeu_hal::BlendMode) { let m = match mode { prometeu_hal::BlendMode::None => BlendMode::None, prometeu_hal::BlendMode::Half => BlendMode::Half, prometeu_hal::BlendMode::HalfPlus => BlendMode::HalfPlus, prometeu_hal::BlendMode::HalfMinus => BlendMode::HalfMinus, prometeu_hal::BlendMode::Full => BlendMode::Full }; self.fill_rect_blend(x, y, w, h, color, m) }
|
||||
fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) { self.fill_rect(x, y, w, h, color) }
|
||||
fn draw_pixel(&mut self, x: i32, y: i32, color: Color) { self.draw_pixel(x, y, color) }
|
||||
fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) { self.draw_line(x0, y0, x1, y1, color) }
|
||||
fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) { self.draw_circle(xc, yc, r, color) }
|
||||
fn draw_circle_points(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color) { self.draw_circle_points(xc, yc, x, y, color) }
|
||||
fn fill_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) { self.fill_circle(xc, yc, r, color) }
|
||||
fn draw_circle_lines(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color) { self.draw_circle_lines(xc, yc, x, y, color) }
|
||||
fn draw_disc(&mut self, x: i32, y: i32, r: i32, border_color: Color, fill_color: Color) { self.draw_disc(x, y, r, border_color, fill_color) }
|
||||
fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) { self.draw_rect(x, y, w, h, color) }
|
||||
fn draw_square(&mut self, x: i32, y: i32, w: i32, h: i32, border_color: Color, fill_color: Color) { self.draw_square(x, y, w, h, border_color, fill_color) }
|
||||
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color) { self.draw_horizontal_line(x0, x1, y, color) }
|
||||
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color) { self.draw_vertical_line(x, y0, y1, color) }
|
||||
fn present(&mut self) { self.present() }
|
||||
fn render_all(&mut self) { self.render_all() }
|
||||
fn render_layer(&mut self, layer_idx: usize) { self.render_layer(layer_idx) }
|
||||
fn render_hud(&mut self) { self.render_hud() }
|
||||
fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) { self.draw_text(x, y, text, color) }
|
||||
fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color) { self.draw_char(x, y, c, color) }
|
||||
fn size(&self) -> (usize, usize) {
|
||||
self.size()
|
||||
}
|
||||
fn front_buffer(&self) -> &[u16] {
|
||||
self.front_buffer()
|
||||
}
|
||||
fn clear(&mut self, color: Color) {
|
||||
self.clear(color)
|
||||
}
|
||||
fn fill_rect_blend(
|
||||
&mut self,
|
||||
x: i32,
|
||||
y: i32,
|
||||
w: i32,
|
||||
h: i32,
|
||||
color: Color,
|
||||
mode: prometeu_hal::BlendMode,
|
||||
) {
|
||||
let m = match mode {
|
||||
prometeu_hal::BlendMode::None => BlendMode::None,
|
||||
prometeu_hal::BlendMode::Half => BlendMode::Half,
|
||||
prometeu_hal::BlendMode::HalfPlus => BlendMode::HalfPlus,
|
||||
prometeu_hal::BlendMode::HalfMinus => BlendMode::HalfMinus,
|
||||
prometeu_hal::BlendMode::Full => BlendMode::Full,
|
||||
};
|
||||
self.fill_rect_blend(x, y, w, h, color, m)
|
||||
}
|
||||
fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) {
|
||||
self.fill_rect(x, y, w, h, color)
|
||||
}
|
||||
fn draw_pixel(&mut self, x: i32, y: i32, color: Color) {
|
||||
self.draw_pixel(x, y, color)
|
||||
}
|
||||
fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) {
|
||||
self.draw_line(x0, y0, x1, y1, color)
|
||||
}
|
||||
fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
|
||||
self.draw_circle(xc, yc, r, color)
|
||||
}
|
||||
fn draw_circle_points(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color) {
|
||||
self.draw_circle_points(xc, yc, x, y, color)
|
||||
}
|
||||
fn fill_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
|
||||
self.fill_circle(xc, yc, r, color)
|
||||
}
|
||||
fn draw_circle_lines(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color) {
|
||||
self.draw_circle_lines(xc, yc, x, y, color)
|
||||
}
|
||||
fn draw_disc(&mut self, x: i32, y: i32, r: i32, border_color: Color, fill_color: Color) {
|
||||
self.draw_disc(x, y, r, border_color, fill_color)
|
||||
}
|
||||
fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) {
|
||||
self.draw_rect(x, y, w, h, color)
|
||||
}
|
||||
fn draw_square(
|
||||
&mut self,
|
||||
x: i32,
|
||||
y: i32,
|
||||
w: i32,
|
||||
h: i32,
|
||||
border_color: Color,
|
||||
fill_color: Color,
|
||||
) {
|
||||
self.draw_square(x, y, w, h, border_color, fill_color)
|
||||
}
|
||||
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color) {
|
||||
self.draw_horizontal_line(x0, x1, y, color)
|
||||
}
|
||||
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color) {
|
||||
self.draw_vertical_line(x, y0, y1, color)
|
||||
}
|
||||
fn present(&mut self) {
|
||||
self.present()
|
||||
}
|
||||
fn render_all(&mut self) {
|
||||
self.render_all()
|
||||
}
|
||||
fn render_layer(&mut self, layer_idx: usize) {
|
||||
self.render_layer(layer_idx)
|
||||
}
|
||||
fn render_hud(&mut self) {
|
||||
self.render_hud()
|
||||
}
|
||||
fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) {
|
||||
self.draw_text(x, y, text, color)
|
||||
}
|
||||
fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color) {
|
||||
self.draw_char(x, y, c, color)
|
||||
}
|
||||
|
||||
fn layer(&self, index: usize) -> &ScrollableTileLayer { &self.layers[index] }
|
||||
fn layer_mut(&mut self, index: usize) -> &mut ScrollableTileLayer { &mut self.layers[index] }
|
||||
fn hud(&self) -> &HudTileLayer { &self.hud }
|
||||
fn hud_mut(&mut self) -> &mut HudTileLayer { &mut self.hud }
|
||||
fn sprite(&self, index: usize) -> &Sprite { &self.sprites[index] }
|
||||
fn sprite_mut(&mut self, index: usize) -> &mut Sprite { &mut self.sprites[index] }
|
||||
fn layer(&self, index: usize) -> &ScrollableTileLayer {
|
||||
&self.layers[index]
|
||||
}
|
||||
fn layer_mut(&mut self, index: usize) -> &mut ScrollableTileLayer {
|
||||
&mut self.layers[index]
|
||||
}
|
||||
fn hud(&self) -> &HudTileLayer {
|
||||
&self.hud
|
||||
}
|
||||
fn hud_mut(&mut self) -> &mut HudTileLayer {
|
||||
&mut self.hud
|
||||
}
|
||||
fn sprite(&self, index: usize) -> &Sprite {
|
||||
&self.sprites[index]
|
||||
}
|
||||
fn sprite_mut(&mut self, index: usize) -> &mut Sprite {
|
||||
&mut self.sprites[index]
|
||||
}
|
||||
|
||||
fn scene_fade_level(&self) -> u8 { self.scene_fade_level }
|
||||
fn set_scene_fade_level(&mut self, level: u8) { self.scene_fade_level = level; }
|
||||
fn scene_fade_color(&self) -> Color { self.scene_fade_color }
|
||||
fn set_scene_fade_color(&mut self, color: Color) { self.scene_fade_color = color; }
|
||||
fn hud_fade_level(&self) -> u8 { self.hud_fade_level }
|
||||
fn set_hud_fade_level(&mut self, level: u8) { self.hud_fade_level = level; }
|
||||
fn hud_fade_color(&self) -> Color { self.hud_fade_color }
|
||||
fn set_hud_fade_color(&mut self, color: Color) { self.hud_fade_color = color; }
|
||||
fn scene_fade_level(&self) -> u8 {
|
||||
self.scene_fade_level
|
||||
}
|
||||
fn set_scene_fade_level(&mut self, level: u8) {
|
||||
self.scene_fade_level = level;
|
||||
}
|
||||
fn scene_fade_color(&self) -> Color {
|
||||
self.scene_fade_color
|
||||
}
|
||||
fn set_scene_fade_color(&mut self, color: Color) {
|
||||
self.scene_fade_color = color;
|
||||
}
|
||||
fn hud_fade_level(&self) -> u8 {
|
||||
self.hud_fade_level
|
||||
}
|
||||
fn set_hud_fade_level(&mut self, level: u8) {
|
||||
self.hud_fade_level = level;
|
||||
}
|
||||
fn hud_fade_color(&self) -> Color {
|
||||
self.hud_fade_color
|
||||
}
|
||||
fn set_hud_fade_color(&mut self, color: Color) {
|
||||
self.hud_fade_color = color;
|
||||
}
|
||||
}
|
||||
|
||||
impl Gfx {
|
||||
@ -187,7 +282,9 @@ impl Gfx {
|
||||
color: Color,
|
||||
mode: BlendMode,
|
||||
) {
|
||||
if color == Color::COLOR_KEY { return; }
|
||||
if color == Color::COLOR_KEY {
|
||||
return;
|
||||
}
|
||||
|
||||
let fw = self.w as i32;
|
||||
let fh = self.h as i32;
|
||||
@ -216,7 +313,9 @@ impl Gfx {
|
||||
|
||||
/// Draws a single pixel.
|
||||
pub fn draw_pixel(&mut self, x: i32, y: i32, color: Color) {
|
||||
if color == Color::COLOR_KEY { return; }
|
||||
if color == Color::COLOR_KEY {
|
||||
return;
|
||||
}
|
||||
if x >= 0 && x < self.w as i32 && y >= 0 && y < self.h as i32 {
|
||||
self.back[y as usize * self.w + x as usize] = color.0;
|
||||
}
|
||||
@ -224,7 +323,9 @@ impl Gfx {
|
||||
|
||||
/// Draws a line between two points using Bresenham's algorithm.
|
||||
pub fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) {
|
||||
if color == Color::COLOR_KEY { return; }
|
||||
if color == Color::COLOR_KEY {
|
||||
return;
|
||||
}
|
||||
|
||||
let dx = (x1 - x0).abs();
|
||||
let sx = if x0 < x1 { 1 } else { -1 };
|
||||
@ -237,7 +338,9 @@ impl Gfx {
|
||||
|
||||
loop {
|
||||
self.draw_pixel(x, y, color);
|
||||
if x == x1 && y == y1 { break; }
|
||||
if x == x1 && y == y1 {
|
||||
break;
|
||||
}
|
||||
let e2 = 2 * err;
|
||||
if e2 >= dy {
|
||||
err += dy;
|
||||
@ -252,9 +355,13 @@ impl Gfx {
|
||||
|
||||
/// Draws a circle outline using Midpoint Circle Algorithm.
|
||||
pub fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
|
||||
if color == Color::COLOR_KEY { return; }
|
||||
if color == Color::COLOR_KEY {
|
||||
return;
|
||||
}
|
||||
|
||||
if r < 0 { return; }
|
||||
if r < 0 {
|
||||
return;
|
||||
}
|
||||
let mut x = 0;
|
||||
let mut y = r;
|
||||
let mut d = 3 - 2 * r;
|
||||
@ -284,9 +391,13 @@ impl Gfx {
|
||||
|
||||
/// Draws a filled circle.
|
||||
pub fn fill_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
|
||||
if color == Color::COLOR_KEY { return; }
|
||||
if color == Color::COLOR_KEY {
|
||||
return;
|
||||
}
|
||||
|
||||
if r < 0 { return; }
|
||||
if r < 0 {
|
||||
return;
|
||||
}
|
||||
let mut x = 0;
|
||||
let mut y = r;
|
||||
let mut d = 3 - 2 * r;
|
||||
@ -318,9 +429,13 @@ impl Gfx {
|
||||
|
||||
/// Draws a rectangle outline.
|
||||
pub fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) {
|
||||
if color == Color::COLOR_KEY { return; }
|
||||
if color == Color::COLOR_KEY {
|
||||
return;
|
||||
}
|
||||
|
||||
if w <= 0 || h <= 0 { return; }
|
||||
if w <= 0 || h <= 0 {
|
||||
return;
|
||||
}
|
||||
self.draw_horizontal_line(x, x + w - 1, y, color);
|
||||
self.draw_horizontal_line(x, x + w - 1, y + h - 1, color);
|
||||
self.draw_vertical_line(x, y, y + h - 1, color);
|
||||
@ -328,30 +443,50 @@ impl Gfx {
|
||||
}
|
||||
|
||||
/// Draws a square (filled rectangle with border).
|
||||
pub fn draw_square(&mut self, x: i32, y: i32, w: i32, h: i32, border_color: Color, fill_color: Color) {
|
||||
pub fn draw_square(
|
||||
&mut self,
|
||||
x: i32,
|
||||
y: i32,
|
||||
w: i32,
|
||||
h: i32,
|
||||
border_color: Color,
|
||||
fill_color: Color,
|
||||
) {
|
||||
self.fill_rect(x, y, w, h, fill_color);
|
||||
self.draw_rect(x, y, w, h, border_color);
|
||||
}
|
||||
|
||||
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color) {
|
||||
if color == Color::COLOR_KEY { return; }
|
||||
if color == Color::COLOR_KEY {
|
||||
return;
|
||||
}
|
||||
|
||||
if y < 0 || y >= self.h as i32 { return; }
|
||||
if y < 0 || y >= self.h as i32 {
|
||||
return;
|
||||
}
|
||||
let start = x0.max(0);
|
||||
let end = x1.min(self.w as i32 - 1);
|
||||
if start > end { return; }
|
||||
if start > end {
|
||||
return;
|
||||
}
|
||||
for x in start..=end {
|
||||
self.back[y as usize * self.w + x as usize] = color.0;
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color) {
|
||||
if color == Color::COLOR_KEY { return; }
|
||||
if color == Color::COLOR_KEY {
|
||||
return;
|
||||
}
|
||||
|
||||
if x < 0 || x >= self.w as i32 { return; }
|
||||
if x < 0 || x >= self.w as i32 {
|
||||
return;
|
||||
}
|
||||
let start = y0.max(0);
|
||||
let end = y1.min(self.h as i32 - 1);
|
||||
if start > end { return; }
|
||||
if start > end {
|
||||
return;
|
||||
}
|
||||
for y in start..=end {
|
||||
self.back[y as usize * self.w + x as usize] = color.0;
|
||||
}
|
||||
@ -365,7 +500,7 @@ impl Gfx {
|
||||
|
||||
/// The main rendering pipeline.
|
||||
///
|
||||
/// This method composes the final frame by rasterizing layers and sprites in the
|
||||
/// This method composes the final frame by rasterizing layers and sprites in the
|
||||
/// correct priority order into the back buffer.
|
||||
/// Follows the hardware model where layers and sprites are composed every frame.
|
||||
pub fn render_all(&mut self) {
|
||||
@ -382,18 +517,40 @@ impl Gfx {
|
||||
}
|
||||
|
||||
// 1. Priority 0 sprites: drawn at the very back, behind everything else.
|
||||
Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[0], &self.sprites, &*self.tile_banks);
|
||||
Self::draw_bucket_on_buffer(
|
||||
&mut self.back,
|
||||
self.w,
|
||||
self.h,
|
||||
&self.priority_buckets[0],
|
||||
&self.sprites,
|
||||
&*self.tile_banks,
|
||||
);
|
||||
|
||||
// 2. Main layers and prioritized sprites.
|
||||
// Order: Layer 0 -> Sprites 1 -> Layer 1 -> Sprites 2 ...
|
||||
for i in 0..self.layers.len() {
|
||||
let bank_id = self.layers[i].bank_id as usize;
|
||||
if let Some(bank) = self.tile_banks.tile_bank_slot(bank_id) {
|
||||
Self::draw_tile_map(&mut self.back, self.w, self.h, &self.layers[i].map, &bank, self.layers[i].scroll_x, self.layers[i].scroll_y);
|
||||
Self::draw_tile_map(
|
||||
&mut self.back,
|
||||
self.w,
|
||||
self.h,
|
||||
&self.layers[i].map,
|
||||
&bank,
|
||||
self.layers[i].scroll_x,
|
||||
self.layers[i].scroll_y,
|
||||
);
|
||||
}
|
||||
|
||||
// Draw sprites that belong to this depth level
|
||||
Self::draw_bucket_on_buffer(&mut self.back, self.w, self.h, &self.priority_buckets[i + 1], &self.sprites, &*self.tile_banks);
|
||||
Self::draw_bucket_on_buffer(
|
||||
&mut self.back,
|
||||
self.w,
|
||||
self.h,
|
||||
&self.priority_buckets[i + 1],
|
||||
&self.sprites,
|
||||
&*self.tile_banks,
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Scene Fade: Applies a color blend to the entire world (excluding HUD).
|
||||
@ -408,7 +565,9 @@ impl Gfx {
|
||||
|
||||
/// Renders a specific game layer.
|
||||
pub fn render_layer(&mut self, layer_idx: usize) {
|
||||
if layer_idx >= self.layers.len() { return; }
|
||||
if layer_idx >= self.layers.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
let bank_id = self.layers[layer_idx].bank_id as usize;
|
||||
let scroll_x = self.layers[layer_idx].scroll_x;
|
||||
@ -419,7 +578,15 @@ impl Gfx {
|
||||
_ => return,
|
||||
};
|
||||
|
||||
Self::draw_tile_map(&mut self.back, self.w, self.h, &self.layers[layer_idx].map, &bank, scroll_x, scroll_y);
|
||||
Self::draw_tile_map(
|
||||
&mut self.back,
|
||||
self.w,
|
||||
self.h,
|
||||
&self.layers[layer_idx].map,
|
||||
&bank,
|
||||
scroll_x,
|
||||
scroll_y,
|
||||
);
|
||||
}
|
||||
|
||||
/// Renders the HUD (fixed position, no scroll).
|
||||
@ -427,7 +594,13 @@ impl Gfx {
|
||||
Self::render_hud_with_pool(&mut self.back, self.w, self.h, &self.hud, &*self.tile_banks);
|
||||
}
|
||||
|
||||
fn render_hud_with_pool(back: &mut [u16], w: usize, h: usize, hud: &HudTileLayer, tile_banks: &dyn TileBankPoolAccess) {
|
||||
fn render_hud_with_pool(
|
||||
back: &mut [u16],
|
||||
w: usize,
|
||||
h: usize,
|
||||
hud: &HudTileLayer,
|
||||
tile_banks: &dyn TileBankPoolAccess,
|
||||
) {
|
||||
let bank_id = hud.bank_id as usize;
|
||||
let bank = match tile_banks.tile_bank_slot(bank_id) {
|
||||
Some(b) => b,
|
||||
@ -445,7 +618,7 @@ impl Gfx {
|
||||
map: &TileMap,
|
||||
bank: &TileBank,
|
||||
scroll_x: i32,
|
||||
scroll_y: i32
|
||||
scroll_y: i32,
|
||||
) {
|
||||
let tile_size = bank.tile_size as usize;
|
||||
|
||||
@ -469,34 +642,58 @@ impl Gfx {
|
||||
let map_y = (start_tile_y + ty as i32) as usize;
|
||||
|
||||
// Bounds check: don't draw if the camera is outside the map.
|
||||
if map_x >= map.width || map_y >= map.height { continue; }
|
||||
if map_x >= map.width || map_y >= map.height {
|
||||
continue;
|
||||
}
|
||||
|
||||
let tile = map.tiles[map_y * map.width + map_x];
|
||||
|
||||
|
||||
// Optimized skip for empty (ID 0) tiles.
|
||||
if tile.id == 0 { continue; }
|
||||
if tile.id == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 5. Project the tile pixels to screen space.
|
||||
let screen_tile_x = (tx as i32 * tile_size as i32) - fine_scroll_x;
|
||||
let screen_tile_y = (ty as i32 * tile_size as i32) - fine_scroll_y;
|
||||
|
||||
Self::draw_tile_pixels(back, screen_w, screen_h, screen_tile_x, screen_tile_y, tile, bank);
|
||||
Self::draw_tile_pixels(
|
||||
back,
|
||||
screen_w,
|
||||
screen_h,
|
||||
screen_tile_x,
|
||||
screen_tile_y,
|
||||
tile,
|
||||
bank,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal helper to copy a single tile's pixels to the framebuffer.
|
||||
/// Handles flipping and palette resolution.
|
||||
fn draw_tile_pixels(back: &mut [u16], screen_w: usize, screen_h: usize, x: i32, y: i32, tile: Tile, bank: &TileBank) {
|
||||
fn draw_tile_pixels(
|
||||
back: &mut [u16],
|
||||
screen_w: usize,
|
||||
screen_h: usize,
|
||||
x: i32,
|
||||
y: i32,
|
||||
tile: Tile,
|
||||
bank: &TileBank,
|
||||
) {
|
||||
let size = bank.tile_size as usize;
|
||||
|
||||
for local_y in 0..size {
|
||||
let world_y = y + local_y as i32;
|
||||
if world_y < 0 || world_y >= screen_h as i32 { continue; }
|
||||
if world_y < 0 || world_y >= screen_h as i32 {
|
||||
continue;
|
||||
}
|
||||
|
||||
for local_x in 0..size {
|
||||
let world_x = x + local_x as i32;
|
||||
if world_x < 0 || world_x >= screen_w as i32 { continue; }
|
||||
if world_x < 0 || world_x >= screen_w as i32 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle flip flags by reversing the fetch coordinates.
|
||||
let fetch_x = if tile.flip_x { size - 1 - local_x } else { local_x };
|
||||
@ -506,7 +703,9 @@ impl Gfx {
|
||||
let px_index = bank.get_pixel_index(tile.id, fetch_x, fetch_y);
|
||||
|
||||
// 2. Hardware rule: Color index 0 is always fully transparent.
|
||||
if px_index == 0 { continue; }
|
||||
if px_index == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Resolve the virtual index to a real RGB565 color using the tile's assigned palette.
|
||||
let color = bank.resolve_color(tile.palette_id, px_index);
|
||||
@ -538,7 +737,7 @@ impl Gfx {
|
||||
screen_w: usize,
|
||||
screen_h: usize,
|
||||
sprite: &Sprite,
|
||||
bank: &TileBank
|
||||
bank: &TileBank,
|
||||
) {
|
||||
// ... (same bounds/clipping calculation we already had) ...
|
||||
let size = bank.tile_size as usize;
|
||||
@ -559,7 +758,9 @@ impl Gfx {
|
||||
let px_index = bank.get_pixel_index(sprite.tile.id, fetch_x, fetch_y);
|
||||
|
||||
// 2. Transparency
|
||||
if px_index == 0 { continue; }
|
||||
if px_index == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Resolve color via palette (from the tile inside the sprite)
|
||||
let color = bank.resolve_color(sprite.tile.palette_id, px_index);
|
||||
@ -572,7 +773,9 @@ impl Gfx {
|
||||
/// Applies the fade effect to the entire back buffer.
|
||||
/// level: 0 (full color) to 31 (visible)
|
||||
fn apply_fade_to_buffer(back: &mut [u16], level: u8, fade_color: Color) {
|
||||
if level >= 31 { return; } // Fully visible, skip processing
|
||||
if level >= 31 {
|
||||
return;
|
||||
} // Fully visible, skip processing
|
||||
|
||||
let weight = level as u16;
|
||||
let inv_weight = 31 - weight;
|
||||
@ -674,7 +877,7 @@ mod tests {
|
||||
let mut gfx = Gfx::new(10, 10, banks);
|
||||
gfx.draw_pixel(5, 5, Color::WHITE);
|
||||
assert_eq!(gfx.back[5 * 10 + 5], Color::WHITE.0);
|
||||
|
||||
|
||||
// Out of bounds should not panic
|
||||
gfx.draw_pixel(-1, -1, Color::WHITE);
|
||||
gfx.draw_pixel(10, 10, Color::WHITE);
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
use std::sync::Arc;
|
||||
use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge};
|
||||
use crate::asset::AssetManager;
|
||||
use crate::audio::Audio;
|
||||
use crate::gfx::Gfx;
|
||||
use crate::memory_banks::{MemoryBanks, SoundBankPoolAccess, SoundBankPoolInstaller, TileBankPoolAccess, TileBankPoolInstaller};
|
||||
use crate::memory_banks::{
|
||||
MemoryBanks, SoundBankPoolAccess, SoundBankPoolInstaller, TileBankPoolAccess,
|
||||
TileBankPoolInstaller,
|
||||
};
|
||||
use crate::pad::Pad;
|
||||
use crate::touch::Touch;
|
||||
use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Aggregate structure for all virtual hardware peripherals.
|
||||
///
|
||||
///
|
||||
/// This struct represents the "Mainboard" of the PROMETEU console.
|
||||
/// It acts as a container for all hardware subsystems. In the Prometeu
|
||||
/// architecture, hardware is decoupled from the OS and VM, allowing
|
||||
/// It acts as a container for all hardware subsystems. In the Prometeu
|
||||
/// architecture, hardware is decoupled from the OS and VM, allowing
|
||||
/// for easier testing and different host implementations (Desktop, Web, etc.).
|
||||
///
|
||||
/// ### Console Specifications:
|
||||
@ -33,20 +36,40 @@ pub struct Hardware {
|
||||
}
|
||||
|
||||
impl HardwareBridge for Hardware {
|
||||
fn gfx(&self) -> &dyn GfxBridge { &self.gfx }
|
||||
fn gfx_mut(&mut self) -> &mut dyn GfxBridge { &mut self.gfx }
|
||||
fn gfx(&self) -> &dyn GfxBridge {
|
||||
&self.gfx
|
||||
}
|
||||
fn gfx_mut(&mut self) -> &mut dyn GfxBridge {
|
||||
&mut self.gfx
|
||||
}
|
||||
|
||||
fn audio(&self) -> &dyn AudioBridge { &self.audio }
|
||||
fn audio_mut(&mut self) -> &mut dyn AudioBridge { &mut self.audio }
|
||||
fn audio(&self) -> &dyn AudioBridge {
|
||||
&self.audio
|
||||
}
|
||||
fn audio_mut(&mut self) -> &mut dyn AudioBridge {
|
||||
&mut self.audio
|
||||
}
|
||||
|
||||
fn pad(&self) -> &dyn PadBridge { &self.pad }
|
||||
fn pad_mut(&mut self) -> &mut dyn PadBridge { &mut self.pad }
|
||||
fn pad(&self) -> &dyn PadBridge {
|
||||
&self.pad
|
||||
}
|
||||
fn pad_mut(&mut self) -> &mut dyn PadBridge {
|
||||
&mut self.pad
|
||||
}
|
||||
|
||||
fn touch(&self) -> &dyn TouchBridge { &self.touch }
|
||||
fn touch_mut(&mut self) -> &mut dyn TouchBridge { &mut self.touch }
|
||||
fn touch(&self) -> &dyn TouchBridge {
|
||||
&self.touch
|
||||
}
|
||||
fn touch_mut(&mut self) -> &mut dyn TouchBridge {
|
||||
&mut self.touch
|
||||
}
|
||||
|
||||
fn assets(&self) -> &dyn AssetBridge { &self.assets }
|
||||
fn assets_mut(&mut self) -> &mut dyn AssetBridge { &mut self.assets }
|
||||
fn assets(&self) -> &dyn AssetBridge {
|
||||
&self.assets
|
||||
}
|
||||
fn assets_mut(&mut self) -> &mut dyn AssetBridge {
|
||||
&mut self.assets
|
||||
}
|
||||
}
|
||||
|
||||
impl Hardware {
|
||||
@ -59,7 +82,11 @@ impl Hardware {
|
||||
pub fn new() -> Self {
|
||||
let memory_banks = Arc::new(MemoryBanks::new());
|
||||
Self {
|
||||
gfx: Gfx::new(Self::W, Self::H, Arc::clone(&memory_banks) as Arc<dyn TileBankPoolAccess>),
|
||||
gfx: Gfx::new(
|
||||
Self::W,
|
||||
Self::H,
|
||||
Arc::clone(&memory_banks) as Arc<dyn TileBankPoolAccess>,
|
||||
),
|
||||
audio: Audio::new(Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolAccess>),
|
||||
pad: Pad::default(),
|
||||
touch: Touch::default(),
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
mod asset;
|
||||
mod audio;
|
||||
mod gfx;
|
||||
pub mod hardware;
|
||||
mod memory_banks;
|
||||
mod pad;
|
||||
mod touch;
|
||||
mod audio;
|
||||
mod memory_banks;
|
||||
pub mod hardware;
|
||||
|
||||
pub use crate::asset::AssetManager;
|
||||
pub use crate::audio::{Audio, AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};
|
||||
pub use crate::gfx::Gfx;
|
||||
pub use crate::memory_banks::MemoryBanks;
|
||||
pub use crate::pad::Pad;
|
||||
pub use crate::pad::Pad;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use std::sync::{Arc, RwLock};
|
||||
use prometeu_hal::sound_bank::SoundBank;
|
||||
use prometeu_hal::tile_bank::TileBank;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
/// Non-generic interface for peripherals to access graphical tile banks.
|
||||
pub trait TileBankPoolAccess: Send + Sync {
|
||||
@ -31,7 +31,7 @@ pub trait SoundBankPoolInstaller: Send + Sync {
|
||||
}
|
||||
|
||||
/// Centralized container for all hardware memory banks.
|
||||
///
|
||||
///
|
||||
/// MemoryBanks represent the actual hardware slot state.
|
||||
/// Peripherals consume this state via narrow, non-generic traits.
|
||||
/// AssetManager coordinates residency and installs assets into these slots.
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use prometeu_hal::{InputSignals, PadBridge};
|
||||
use prometeu_hal::button::Button;
|
||||
use prometeu_hal::{InputSignals, PadBridge};
|
||||
|
||||
#[derive(Default, Clone, Copy, Debug)]
|
||||
pub struct Pad {
|
||||
@ -10,31 +10,59 @@ pub struct Pad {
|
||||
|
||||
pub a: Button, // ps: square
|
||||
pub b: Button, // ps: circle
|
||||
pub x: Button, // ps: triangle
|
||||
pub x: Button, // ps: triangle
|
||||
pub y: Button, // ps: cross
|
||||
pub l: Button, // ps: R
|
||||
pub r: Button, // ps: L
|
||||
|
||||
pub start: Button,
|
||||
pub select: Button,
|
||||
pub select: Button,
|
||||
}
|
||||
|
||||
impl PadBridge for Pad {
|
||||
fn begin_frame(&mut self, signals: &InputSignals) { self.begin_frame(signals) }
|
||||
fn any(&self) -> bool { self.any() }
|
||||
fn begin_frame(&mut self, signals: &InputSignals) {
|
||||
self.begin_frame(signals)
|
||||
}
|
||||
fn any(&self) -> bool {
|
||||
self.any()
|
||||
}
|
||||
|
||||
fn up(&self) -> &Button { &self.up }
|
||||
fn down(&self) -> &Button { &self.down }
|
||||
fn left(&self) -> &Button { &self.left }
|
||||
fn right(&self) -> &Button { &self.right }
|
||||
fn a(&self) -> &Button { &self.a }
|
||||
fn b(&self) -> &Button { &self.b }
|
||||
fn x(&self) -> &Button { &self.x }
|
||||
fn y(&self) -> &Button { &self.y }
|
||||
fn l(&self) -> &Button { &self.l }
|
||||
fn r(&self) -> &Button { &self.r }
|
||||
fn start(&self) -> &Button { &self.start }
|
||||
fn select(&self) -> &Button { &self.select }
|
||||
fn up(&self) -> &Button {
|
||||
&self.up
|
||||
}
|
||||
fn down(&self) -> &Button {
|
||||
&self.down
|
||||
}
|
||||
fn left(&self) -> &Button {
|
||||
&self.left
|
||||
}
|
||||
fn right(&self) -> &Button {
|
||||
&self.right
|
||||
}
|
||||
fn a(&self) -> &Button {
|
||||
&self.a
|
||||
}
|
||||
fn b(&self) -> &Button {
|
||||
&self.b
|
||||
}
|
||||
fn x(&self) -> &Button {
|
||||
&self.x
|
||||
}
|
||||
fn y(&self) -> &Button {
|
||||
&self.y
|
||||
}
|
||||
fn l(&self) -> &Button {
|
||||
&self.l
|
||||
}
|
||||
fn r(&self) -> &Button {
|
||||
&self.r
|
||||
}
|
||||
fn start(&self) -> &Button {
|
||||
&self.start
|
||||
}
|
||||
fn select(&self) -> &Button {
|
||||
&self.select
|
||||
}
|
||||
}
|
||||
|
||||
impl Pad {
|
||||
@ -43,18 +71,18 @@ impl Pad {
|
||||
self.down.begin_frame(signals.down_signal);
|
||||
self.left.begin_frame(signals.left_signal);
|
||||
self.right.begin_frame(signals.right_signal);
|
||||
|
||||
|
||||
self.a.begin_frame(signals.a_signal);
|
||||
self.b.begin_frame(signals.b_signal);
|
||||
self.x.begin_frame(signals.x_signal);
|
||||
self.y.begin_frame(signals.y_signal);
|
||||
self.l.begin_frame(signals.l_signal);
|
||||
self.r.begin_frame(signals.r_signal);
|
||||
|
||||
|
||||
self.start.begin_frame(signals.start_signal);
|
||||
self.select.begin_frame(signals.select_signal);
|
||||
self.select.begin_frame(signals.select_signal);
|
||||
}
|
||||
|
||||
|
||||
pub fn any(&self) -> bool {
|
||||
self.a.down
|
||||
|| self.b.down
|
||||
@ -66,5 +94,3 @@ impl Pad {
|
||||
|| self.select.down
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
// use prometeu_drivers::hardware::Hardware;
|
||||
// use prometeu_hal::{HostContext, HostReturn, NativeInterface};
|
||||
// use prometeu_vm::{LogicalFrameEndingReason, VirtualMachine};
|
||||
//
|
||||
//
|
||||
// struct MockNative;
|
||||
// impl NativeInterface for MockNative {
|
||||
// fn syscall(&mut self, id: u32, _args: &[Value], ret: &mut HostReturn, _ctx: &mut HostContext) -> Result<(), VmFault> {
|
||||
@ -26,30 +26,30 @@
|
||||
// Ok(())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// #[test]
|
||||
// fn test_canonical_cartridge_heartbeat() {
|
||||
// let mut pbc_path = Path::new("../../test-cartridges/canonical/golden/program.pbc").to_path_buf();
|
||||
// if !pbc_path.exists() {
|
||||
// pbc_path = Path::new("test-cartridges/canonical/golden/program.pbc").to_path_buf();
|
||||
// }
|
||||
//
|
||||
//
|
||||
// let pbc_bytes = fs::read(pbc_path).expect("Failed to read canonical PBC. Did you run the generation test?");
|
||||
//
|
||||
//
|
||||
// // Determine entrypoint from the compiled module exports
|
||||
// let entry_symbol = "src/main/modules:frame";
|
||||
//
|
||||
//
|
||||
// let mut vm = VirtualMachine::new(vec![], vec![]);
|
||||
// vm.initialize(pbc_bytes, entry_symbol).expect("Failed to initialize VM with canonical cartridge");
|
||||
// vm.prepare_call(entry_symbol);
|
||||
//
|
||||
//
|
||||
// let mut native = MockNative;
|
||||
// let mut hw = Hardware::new();
|
||||
// let mut ctx = HostContext::new(Some(&mut hw));
|
||||
//
|
||||
//
|
||||
// // Run for a reasonable budget
|
||||
// let report = vm.run_budget(1000, &mut native, &mut ctx).expect("VM failed to run");
|
||||
//
|
||||
//
|
||||
// // Acceptance criteria:
|
||||
// // 1. No traps
|
||||
// match report.reason {
|
||||
@ -61,7 +61,7 @@
|
||||
// LogicalFrameEndingReason::BudgetExhausted => {},
|
||||
// LogicalFrameEndingReason::Breakpoint => {},
|
||||
// }
|
||||
//
|
||||
//
|
||||
// // 2. Deterministic output state (if any)
|
||||
// // Observação: a PVM agora sinaliza FRAME_SYNC imediatamente antes do RET do entry point.
|
||||
// // Quando o motivo é FRAME_SYNC, não exigimos pilha vazia (a limpeza final ocorre após o RET).
|
||||
@ -69,6 +69,6 @@
|
||||
// if !matches!(report.reason, LogicalFrameEndingReason::FrameSync) {
|
||||
// assert_eq!(vm.operand_stack.len(), 0, "Stack should be empty after frame() execution");
|
||||
// }
|
||||
//
|
||||
//
|
||||
// println!("Heartbeat test passed!");
|
||||
// }
|
||||
|
||||
@ -1,23 +1,23 @@
|
||||
use prometeu_hal::telemetry::CertificationConfig;
|
||||
use prometeu_hal::{HardwareBridge, InputSignals};
|
||||
use prometeu_hal::cartridge::Cartridge;
|
||||
use prometeu_system::{PrometeuHub, VirtualMachineRuntime};
|
||||
use prometeu_vm::VirtualMachine;
|
||||
use crate::firmware::boot_target::BootTarget;
|
||||
use crate::firmware::firmware_state::{FirmwareState, LoadCartridgeStep, ResetStep};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use prometeu_hal::cartridge::Cartridge;
|
||||
use prometeu_hal::telemetry::CertificationConfig;
|
||||
use prometeu_hal::{HardwareBridge, InputSignals};
|
||||
use prometeu_system::{PrometeuHub, VirtualMachineRuntime};
|
||||
use prometeu_vm::VirtualMachine;
|
||||
|
||||
/// PROMETEU Firmware.
|
||||
///
|
||||
/// The central orchestrator of the console. The firmware acts as the
|
||||
///
|
||||
/// The central orchestrator of the console. The firmware acts as the
|
||||
/// "Control Unit", managing the high-level state machine of the system.
|
||||
///
|
||||
/// It is responsible for transitioning between different modes of operation,
|
||||
/// such as showing the splash screen, running the Hub (launcher), or
|
||||
/// It is responsible for transitioning between different modes of operation,
|
||||
/// such as showing the splash screen, running the Hub (launcher), or
|
||||
/// executing a game/app.
|
||||
///
|
||||
/// ### Execution Loop:
|
||||
/// The firmware is designed to be ticked once per frame (60Hz). During each
|
||||
/// The firmware is designed to be ticked once per frame (60Hz). During each
|
||||
/// tick, it:
|
||||
/// 1. Updates peripherals with the latest input signals.
|
||||
/// 2. Delegates the logic update to the current active state.
|
||||
@ -74,13 +74,18 @@ impl Firmware {
|
||||
self.change_state(next_state, signals, hw);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Transitions the system to a new state, handling lifecycle hooks.
|
||||
pub fn change_state(&mut self, new_state: FirmwareState, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
|
||||
pub fn change_state(
|
||||
&mut self,
|
||||
new_state: FirmwareState,
|
||||
signals: &InputSignals,
|
||||
hw: &mut dyn HardwareBridge,
|
||||
) {
|
||||
self.on_exit(signals, hw);
|
||||
self.state = new_state;
|
||||
self.state_initialized = false;
|
||||
|
||||
|
||||
// Enter the new state immediately to avoid "empty" frames during transitions.
|
||||
self.on_enter(signals, hw);
|
||||
self.state_initialized = true;
|
||||
@ -109,7 +114,11 @@ impl Firmware {
|
||||
|
||||
/// Dispatches the `on_update` event to the current state implementation.
|
||||
/// Returns an optional `FirmwareState` if a transition is requested.
|
||||
fn on_update(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) -> Option<FirmwareState> {
|
||||
fn on_update(
|
||||
&mut self,
|
||||
signals: &InputSignals,
|
||||
hw: &mut dyn HardwareBridge,
|
||||
) -> Option<FirmwareState> {
|
||||
let mut req = PrometeuContext {
|
||||
vm: &mut self.vm,
|
||||
os: &mut self.os,
|
||||
@ -149,9 +158,9 @@ impl Firmware {
|
||||
FirmwareState::AppCrashes(s) => s.on_exit(&mut req),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn load_cartridge(&mut self, cartridge: Cartridge) {
|
||||
self.state = FirmwareState::LoadCartridge(LoadCartridgeStep { cartridge });
|
||||
self.state_initialized = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,4 +15,4 @@ pub enum FirmwareState {
|
||||
LoadCartridge(LoadCartridgeStep),
|
||||
GameRunning(GameRunningStep),
|
||||
AppCrashes(AppCrashesStep),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
use prometeu_hal::color::Color;
|
||||
use crate::firmware::firmware_state::{FirmwareState, LaunchHubStep};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppCrashesStep {
|
||||
@ -27,9 +27,9 @@ impl AppCrashesStep {
|
||||
if ctx.hw.pad().start().down {
|
||||
return Some(FirmwareState::LaunchHub(LaunchHubStep));
|
||||
}
|
||||
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GameRunningStep;
|
||||
@ -12,7 +12,7 @@ impl GameRunningStep {
|
||||
|
||||
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
||||
let result = ctx.os.tick(ctx.vm, ctx.signals, ctx.hw);
|
||||
|
||||
|
||||
if !ctx.os.logical_frame_active {
|
||||
ctx.hw.gfx_mut().present();
|
||||
}
|
||||
|
||||
@ -27,15 +27,15 @@ impl HubHomeStep {
|
||||
|
||||
// Renders the System App window borders
|
||||
ctx.hub.render(ctx.os, ctx.hw);
|
||||
|
||||
|
||||
ctx.hw.gfx_mut().present();
|
||||
|
||||
|
||||
if let Some(err) = error {
|
||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { error: err }));
|
||||
}
|
||||
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
use crate::firmware::boot_target::BootTarget;
|
||||
use crate::firmware::firmware_state::{FirmwareState, HubHomeStep, LoadCartridgeStep};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
use prometeu_hal::cartridge_loader::CartridgeLoader;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LaunchHubStep;
|
||||
@ -22,7 +22,12 @@ impl LaunchHubStep {
|
||||
return Some(FirmwareState::LoadCartridge(LoadCartridgeStep { cartridge }));
|
||||
}
|
||||
Err(e) => {
|
||||
ctx.os.log(LogLevel::Error, LogSource::Pos, 0, format!("Failed to auto-load cartridge: {:?}", e));
|
||||
ctx.os.log(
|
||||
LogLevel::Error,
|
||||
LogSource::Pos,
|
||||
0,
|
||||
format!("Failed to auto-load cartridge: {:?}", e),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -30,4 +35,4 @@ impl LaunchHubStep {
|
||||
}
|
||||
|
||||
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
use crate::firmware::firmware_state::{FirmwareState, GameRunningStep, HubHomeStep};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
use prometeu_hal::cartridge::{AppMode, Cartridge};
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
use prometeu_hal::window::Rect;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -12,13 +12,18 @@ pub struct LoadCartridgeStep {
|
||||
|
||||
impl LoadCartridgeStep {
|
||||
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
|
||||
ctx.os.log(LogLevel::Info, LogSource::Pos, 0, format!("Loading cartridge: {}", self.cartridge.title));
|
||||
|
||||
ctx.os.log(
|
||||
LogLevel::Info,
|
||||
LogSource::Pos,
|
||||
0,
|
||||
format!("Loading cartridge: {}", self.cartridge.title),
|
||||
);
|
||||
|
||||
// Initialize Asset Manager
|
||||
ctx.hw.assets_mut().initialize_for_cartridge(
|
||||
self.cartridge.asset_table.clone(),
|
||||
self.cartridge.preload.clone(),
|
||||
self.cartridge.assets.clone()
|
||||
self.cartridge.assets.clone(),
|
||||
);
|
||||
|
||||
ctx.os.initialize_vm(ctx.vm, &self.cartridge);
|
||||
@ -29,10 +34,10 @@ impl LoadCartridgeStep {
|
||||
let id = ctx.hub.window_manager.add_window(
|
||||
self.cartridge.title.clone(),
|
||||
Rect { x: 40, y: 20, w: 240, h: 140 },
|
||||
Color::WHITE
|
||||
Color::WHITE,
|
||||
);
|
||||
ctx.hub.window_manager.set_focus(id);
|
||||
|
||||
|
||||
// System apps do not change the firmware state to GameRunning.
|
||||
// They run in the background or via windows in the Hub.
|
||||
return Some(FirmwareState::HubHome(HubHomeStep));
|
||||
@ -42,4 +47,4 @@ impl LoadCartridgeStep {
|
||||
}
|
||||
|
||||
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
use crate::firmware::boot_target::BootTarget;
|
||||
use crate::firmware::firmware_state::{FirmwareState, LaunchHubStep, SplashScreenStep};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResetStep;
|
||||
@ -20,4 +20,4 @@ impl ResetStep {
|
||||
}
|
||||
|
||||
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use crate::firmware::firmware_state::{FirmwareState, LaunchHubStep};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SplashScreenStep {
|
||||
@ -17,7 +17,7 @@ impl SplashScreenStep {
|
||||
|
||||
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
||||
const ANIMATION_DURATION: u32 = 60; // 1 second at 60fps
|
||||
const TOTAL_DURATION: u32 = 240; // 4 seconds total (updated from 2s based on total_duration logic)
|
||||
const TOTAL_DURATION: u32 = 240; // 4 seconds total (updated from 2s based on total_duration logic)
|
||||
|
||||
// Update peripherals for input
|
||||
ctx.hw.pad_mut().begin_frame(ctx.signals);
|
||||
@ -28,7 +28,7 @@ impl SplashScreenStep {
|
||||
// Draw expanding square
|
||||
let (sw, sh) = ctx.hw.gfx().size();
|
||||
let max_size = (sw.min(sh) as i32 / 2).max(1);
|
||||
|
||||
|
||||
let current_size = if self.frame < ANIMATION_DURATION {
|
||||
(max_size * (self.frame as i32 + 1)) / ANIMATION_DURATION as i32
|
||||
} else {
|
||||
@ -37,7 +37,7 @@ impl SplashScreenStep {
|
||||
|
||||
let x = (sw as i32 - current_size) / 2;
|
||||
let y = (sh as i32 - current_size) / 2;
|
||||
|
||||
|
||||
ctx.hw.gfx_mut().fill_rect(x, y, current_size, current_size, Color::WHITE);
|
||||
ctx.hw.gfx_mut().present();
|
||||
|
||||
@ -57,4 +57,4 @@ impl SplashScreenStep {
|
||||
}
|
||||
|
||||
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
@ -10,4 +10,4 @@ pub struct PrometeuContext<'a> {
|
||||
pub boot_target: &'a BootTarget,
|
||||
pub signals: &'a InputSignals,
|
||||
pub hw: &'a mut dyn HardwareBridge,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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" {
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
/// Represents a 16-bit color in the RGB565 format.
|
||||
///
|
||||
///
|
||||
/// The RGB565 format is a common high-color representation for embedded systems:
|
||||
/// - **Red**: 5 bits (0..31)
|
||||
/// - **Green**: 6 bits (0..63)
|
||||
/// - **Blue**: 5 bits (0..31)
|
||||
///
|
||||
/// Prometeu does not have a hardware alpha channel. Transparency is achieved
|
||||
/// by using a specific color key (Magenta / 0xF81F) which the GFX engine
|
||||
/// Prometeu does not have a hardware alpha channel. Transparency is achieved
|
||||
/// by using a specific color key (Magenta / 0xF81F) which the GFX engine
|
||||
/// skips during rendering.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Color(pub u16);
|
||||
@ -55,7 +55,7 @@ impl Color {
|
||||
pub const fn gray_scale(c: u8) -> Self {
|
||||
Self::rgb(c, c, c)
|
||||
}
|
||||
|
||||
|
||||
pub const fn from_raw(raw: u16) -> Self {
|
||||
Self(raw)
|
||||
}
|
||||
@ -72,5 +72,4 @@ impl Color {
|
||||
let hex = r8 << 16 | g8 << 8 | b8;
|
||||
hex as i32
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
@ -55,7 +59,7 @@ mod tests {
|
||||
service.log(100, 1, LogLevel::Info, LogSource::Pos, 0, "Log 1".to_string());
|
||||
service.log(110, 1, LogLevel::Info, LogSource::Pos, 0, "Log 2".to_string());
|
||||
service.log(120, 1, LogLevel::Info, LogSource::Pos, 0, "Log 3".to_string());
|
||||
|
||||
|
||||
assert_eq!(service.events.len(), 3);
|
||||
assert_eq!(service.events[0].msg, "Log 1");
|
||||
|
||||
|
||||
@ -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,9 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
use crate::sample::Sample;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A container for audio assets.
|
||||
///
|
||||
/// A SoundBank stores multiple audio samples that can be played by the
|
||||
///
|
||||
/// A SoundBank stores multiple audio samples that can be played by the
|
||||
/// audio subsystem.
|
||||
pub struct SoundBank {
|
||||
pub samples: Vec<Arc<Sample>>,
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
/// Enumeration of all System Calls (Syscalls) available in the Prometeu environment.
|
||||
///
|
||||
/// Syscalls are the primary mechanism for a program running in the Virtual Machine
|
||||
/// to interact with the outside world (Hardware, OS, Filesystem).
|
||||
///
|
||||
/// Syscalls are the primary mechanism for a program running in the Virtual Machine
|
||||
/// to interact with the outside world (Hardware, OS, Filesystem).
|
||||
///
|
||||
/// Each Syscall has a unique 32-bit ID. The IDs are grouped by category:
|
||||
/// - **0x0xxx**: System & OS Control
|
||||
/// - **0x1xxx**: Graphics (GFX)
|
||||
@ -301,8 +301,8 @@ impl Syscall {
|
||||
|
||||
// --- FS ---
|
||||
Self::FsOpen => 1,
|
||||
Self::FsRead => 1, // bytes read
|
||||
Self::FsWrite => 1, // bytes written
|
||||
Self::FsRead => 1, // bytes read
|
||||
Self::FsWrite => 1, // bytes written
|
||||
Self::FsClose => 0,
|
||||
Self::FsListDir => 1, // entries count/handle (TBD)
|
||||
Self::FsExists => 1,
|
||||
|
||||
@ -38,7 +38,12 @@ impl Certifier {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
pub fn evaluate(&self, telemetry: &TelemetryFrame, log_service: &mut LogService, ts_ms: u64) -> usize {
|
||||
pub fn evaluate(
|
||||
&self,
|
||||
telemetry: &TelemetryFrame,
|
||||
log_service: &mut LogService,
|
||||
ts_ms: u64,
|
||||
) -> usize {
|
||||
if !self.config.enabled {
|
||||
return 0;
|
||||
}
|
||||
@ -53,7 +58,10 @@ impl Certifier {
|
||||
LogLevel::Warn,
|
||||
LogSource::Pos,
|
||||
0xCA01,
|
||||
format!("Cert: cycles_used exceeded budget ({} > {})", telemetry.cycles_used, budget),
|
||||
format!(
|
||||
"Cert: cycles_used exceeded budget ({} > {})",
|
||||
telemetry.cycles_used, budget
|
||||
),
|
||||
);
|
||||
violations += 1;
|
||||
}
|
||||
@ -67,7 +75,10 @@ impl Certifier {
|
||||
LogLevel::Warn,
|
||||
LogSource::Pos,
|
||||
0xCA02,
|
||||
format!("Cert: syscalls per frame exceeded limit ({} > {})", telemetry.syscalls, limit),
|
||||
format!(
|
||||
"Cert: syscalls per frame exceeded limit ({} > {})",
|
||||
telemetry.syscalls, limit
|
||||
),
|
||||
);
|
||||
violations += 1;
|
||||
}
|
||||
@ -81,7 +92,10 @@ impl Certifier {
|
||||
LogLevel::Warn,
|
||||
LogSource::Pos,
|
||||
0xCA03,
|
||||
format!("Cert: host_cpu_time_us exceeded limit ({} > {})", telemetry.host_cpu_time_us, limit),
|
||||
format!(
|
||||
"Cert: host_cpu_time_us exceeded limit ({} > {})",
|
||||
telemetry.host_cpu_time_us, limit
|
||||
),
|
||||
);
|
||||
violations += 1;
|
||||
}
|
||||
@ -106,15 +120,15 @@ mod tests {
|
||||
};
|
||||
let cert = Certifier::new(config);
|
||||
let mut ls = LogService::new(10);
|
||||
|
||||
|
||||
let mut tel = TelemetryFrame::default();
|
||||
tel.cycles_used = 150;
|
||||
tel.syscalls = 10;
|
||||
tel.host_cpu_time_us = 500;
|
||||
|
||||
|
||||
let violations = cert.evaluate(&tel, &mut ls, 1000);
|
||||
assert_eq!(violations, 2);
|
||||
|
||||
|
||||
let logs = ls.get_recent(10);
|
||||
assert_eq!(logs.len(), 2);
|
||||
assert!(logs[0].msg.contains("cycles_used"));
|
||||
|
||||
@ -12,9 +12,9 @@ pub enum TileSize {
|
||||
}
|
||||
|
||||
/// A container for graphical assets.
|
||||
///
|
||||
/// A TileBank stores both the raw pixel data (as palette indices) and the
|
||||
/// color palettes themselves. This encapsulates all the information needed
|
||||
///
|
||||
/// A TileBank stores both the raw pixel data (as palette indices) and the
|
||||
/// color palettes themselves. This encapsulates all the information needed
|
||||
/// to render a set of tiles.
|
||||
pub struct TileBank {
|
||||
/// Dimension of each individual tile in the bank.
|
||||
@ -64,7 +64,7 @@ impl TileBank {
|
||||
|
||||
/// Maps a 4-bit index to a real RGB565 Color using the specified palette.
|
||||
pub fn resolve_color(&self, palette_id: u8, pixel_index: u8) -> Color {
|
||||
// Hardware Rule: Index 0 is always transparent.
|
||||
// Hardware Rule: Index 0 is always transparent.
|
||||
// We use Magenta as the 'transparent' signal color during composition.
|
||||
if pixel_index == 0 {
|
||||
return Color::COLOR_KEY;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -3,4 +3,4 @@ pub enum VmFault {
|
||||
Trap(u32, String),
|
||||
Panic(String),
|
||||
Unavailable,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,3 +1,3 @@
|
||||
mod prometeu_hub;
|
||||
|
||||
pub use prometeu_hub::PrometeuHub;
|
||||
pub use prometeu_hub::PrometeuHub;
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
use crate::programs::prometeu_hub::window_manager::WindowManager;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::HardwareBridge;
|
||||
use prometeu_hal::window::Rect;
|
||||
use crate::VirtualMachineRuntime;
|
||||
use crate::programs::prometeu_hub::window_manager::WindowManager;
|
||||
use prometeu_hal::HardwareBridge;
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
use prometeu_hal::window::Rect;
|
||||
|
||||
/// PrometeuHub: Launcher and system UI environment.
|
||||
pub struct PrometeuHub {
|
||||
@ -12,9 +12,7 @@ pub struct PrometeuHub {
|
||||
|
||||
impl PrometeuHub {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
window_manager: WindowManager::new(),
|
||||
}
|
||||
Self { window_manager: WindowManager::new() }
|
||||
}
|
||||
|
||||
pub fn init(&mut self) {
|
||||
@ -28,16 +26,29 @@ impl PrometeuHub {
|
||||
|
||||
if hw.pad().a().pressed {
|
||||
os.log(LogLevel::Debug, LogSource::Hub, 0, "window A opened".to_string());
|
||||
next_window = Some(("Green Window".to_string(), Rect { x: 0, y: 0, w: 160, h: 90 }, Color::GREEN));
|
||||
next_window = Some((
|
||||
"Green Window".to_string(),
|
||||
Rect { x: 0, y: 0, w: 160, h: 90 },
|
||||
Color::GREEN,
|
||||
));
|
||||
} else if hw.pad().b().pressed {
|
||||
os.log(LogLevel::Debug, LogSource::Hub, 0, "window B opened".to_string());
|
||||
next_window = Some(("Indigo Window".to_string(), Rect { x: 160, y: 0, w: 160, h: 90 }, Color::INDIGO));
|
||||
next_window = Some((
|
||||
"Indigo Window".to_string(),
|
||||
Rect { x: 160, y: 0, w: 160, h: 90 },
|
||||
Color::INDIGO,
|
||||
));
|
||||
} else if hw.pad().x().pressed {
|
||||
os.log(LogLevel::Debug, LogSource::Hub, 0, "window X opened".to_string());
|
||||
next_window = Some(("Yellow Window".to_string(), Rect { x: 0, y: 90, w: 160, h: 90 }, Color::YELLOW));
|
||||
next_window = Some((
|
||||
"Yellow Window".to_string(),
|
||||
Rect { x: 0, y: 90, w: 160, h: 90 },
|
||||
Color::YELLOW,
|
||||
));
|
||||
} else if hw.pad().y().pressed {
|
||||
os.log(LogLevel::Debug, LogSource::Hub, 0, "window Y opened".to_string());
|
||||
next_window = Some(("Red Window".to_string(), Rect { x: 160, y: 90, w: 160, h: 90 }, Color::RED));
|
||||
next_window =
|
||||
Some(("Red Window".to_string(), Rect { x: 160, y: 90, w: 160, h: 90 }, Color::RED));
|
||||
}
|
||||
|
||||
if let Some((title, rect, color)) = next_window {
|
||||
@ -58,4 +69,4 @@ impl PrometeuHub {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,21 +9,12 @@ pub struct WindowManager {
|
||||
|
||||
impl WindowManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
windows: Vec::new(),
|
||||
focused: None,
|
||||
}
|
||||
Self { windows: Vec::new(), focused: None }
|
||||
}
|
||||
|
||||
pub fn add_window(&mut self, title: String, viewport: Rect, color: Color) -> WindowId {
|
||||
let id = WindowId(self.windows.len() as u32);
|
||||
let window = Window {
|
||||
id,
|
||||
viewport,
|
||||
has_focus: false,
|
||||
title,
|
||||
color,
|
||||
};
|
||||
let window = Window { id, viewport, has_focus: false, title, color };
|
||||
self.windows.push(window);
|
||||
id
|
||||
}
|
||||
@ -55,8 +46,13 @@ mod tests {
|
||||
#[test]
|
||||
fn test_window_manager_focus() {
|
||||
let mut wm = WindowManager::new();
|
||||
let id1 = wm.add_window("Window 1".to_string(), Rect { x: 0, y: 0, w: 10, h: 10 }, Color::WHITE);
|
||||
let id2 = wm.add_window("Window 2".to_string(), Rect { x: 10, y: 10, w: 10, h: 10 }, Color::WHITE);
|
||||
let id1 =
|
||||
wm.add_window("Window 1".to_string(), Rect { x: 0, y: 0, w: 10, h: 10 }, Color::WHITE);
|
||||
let id2 = wm.add_window(
|
||||
"Window 2".to_string(),
|
||||
Rect { x: 10, y: 10, w: 10, h: 10 },
|
||||
Color::WHITE,
|
||||
);
|
||||
|
||||
assert_eq!(wm.windows.len(), 2);
|
||||
assert_eq!(wm.focused, None);
|
||||
@ -75,7 +71,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_window_manager_remove_window() {
|
||||
let mut wm = WindowManager::new();
|
||||
let id = wm.add_window("Window".to_string(), Rect { x: 0, y: 0, w: 10, h: 10 }, Color::WHITE);
|
||||
let id =
|
||||
wm.add_window("Window".to_string(), Rect { x: 0, y: 0, w: 10, h: 10 }, Color::WHITE);
|
||||
wm.set_focus(id);
|
||||
assert_eq!(wm.focused, Some(id));
|
||||
|
||||
@ -83,4 +80,4 @@ mod tests {
|
||||
assert_eq!(wm.windows.len(), 0);
|
||||
assert_eq!(wm.focused, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
use crate::fs::{FsBackend, FsEntry, FsError};
|
||||
|
||||
/// Virtual Filesystem (VFS) interface for Prometeu.
|
||||
///
|
||||
///
|
||||
/// The VFS provides a sandboxed, unified path interface for user applications.
|
||||
/// Instead of interacting directly with the host's disk, the VM uses
|
||||
/// normalized paths (e.g., `/user/save.dat`).
|
||||
///
|
||||
/// The actual storage is provided by an `FsBackend`, which can be a real
|
||||
/// Instead of interacting directly with the host's disk, the VM uses
|
||||
/// normalized paths (e.g., `/user/save.dat`).
|
||||
///
|
||||
/// The actual storage is provided by an `FsBackend`, which can be a real
|
||||
/// directory on disk, an in-memory map, or even a network resource.
|
||||
pub struct VirtualFS {
|
||||
/// The active storage implementation.
|
||||
@ -88,22 +88,21 @@ mod tests {
|
||||
|
||||
impl MockBackend {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
files: HashMap::new(),
|
||||
healthy: true,
|
||||
}
|
||||
Self { files: HashMap::new(), healthy: true }
|
||||
}
|
||||
}
|
||||
|
||||
impl FsBackend for MockBackend {
|
||||
fn mount(&mut self) -> Result<(), FsError> { Ok(()) }
|
||||
fn mount(&mut self) -> Result<(), FsError> {
|
||||
Ok(())
|
||||
}
|
||||
fn unmount(&mut self) {}
|
||||
fn list_dir(&self, _path: &str) -> Result<Vec<FsEntry>, FsError> {
|
||||
Ok(self.files.keys().map(|name| FsEntry {
|
||||
name: name.clone(),
|
||||
is_dir: false,
|
||||
size: 0,
|
||||
}).collect())
|
||||
Ok(self
|
||||
.files
|
||||
.keys()
|
||||
.map(|name| FsEntry { name: name.clone(), is_dir: false, size: 0 })
|
||||
.collect())
|
||||
}
|
||||
fn read_file(&self, path: &str) -> Result<Vec<u8>, FsError> {
|
||||
self.files.get(path).cloned().ok_or(FsError::NotFound)
|
||||
@ -128,18 +127,18 @@ mod tests {
|
||||
fn test_virtual_fs_operations() {
|
||||
let mut vfs = VirtualFS::new();
|
||||
let backend = MockBackend::new();
|
||||
|
||||
|
||||
vfs.mount(Box::new(backend)).unwrap();
|
||||
|
||||
|
||||
let test_file = "/user/test.txt";
|
||||
let content = b"hello world";
|
||||
|
||||
|
||||
vfs.write_file(test_file, content).unwrap();
|
||||
assert!(vfs.exists(test_file));
|
||||
|
||||
|
||||
let read_content = vfs.read_file(test_file).unwrap();
|
||||
assert_eq!(read_content, content);
|
||||
|
||||
|
||||
vfs.delete(test_file).unwrap();
|
||||
assert!(!vfs.exists(test_file));
|
||||
}
|
||||
@ -149,7 +148,7 @@ mod tests {
|
||||
let mut vfs = VirtualFS::new();
|
||||
let mut backend = MockBackend::new();
|
||||
backend.healthy = false;
|
||||
|
||||
|
||||
vfs.mount(Box::new(backend)).unwrap();
|
||||
assert!(!vfs.is_healthy());
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
pub mod fs;
|
||||
pub mod fs;
|
||||
|
||||
@ -1,21 +1,20 @@
|
||||
use prometeu_hal::syscalls::Syscall;
|
||||
use crate::fs::{FsBackend, FsState, VirtualFS};
|
||||
use prometeu_hal::{HardwareBridge, InputSignals};
|
||||
use prometeu_hal::log::{LogLevel, LogService, LogSource};
|
||||
use prometeu_hal::telemetry::{CertificationConfig, Certifier, TelemetryFrame};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Instant;
|
||||
use prometeu_bytecode::{Value, TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE};
|
||||
use prometeu_hal::{expect_bool, expect_int, HostContext, HostReturn, NativeInterface, SyscallId};
|
||||
use prometeu_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value};
|
||||
use prometeu_hal::asset::{BankType, LoadStatus, SlotRef};
|
||||
use prometeu_hal::button::Button;
|
||||
use prometeu_hal::cartridge::{AppMode, Cartridge};
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::log::{LogLevel, LogService, LogSource};
|
||||
use prometeu_hal::sprite::Sprite;
|
||||
use prometeu_hal::syscalls::Syscall;
|
||||
use prometeu_hal::telemetry::{CertificationConfig, Certifier, TelemetryFrame};
|
||||
use prometeu_hal::tile::Tile;
|
||||
use prometeu_hal::vm_fault::VmFault;
|
||||
use prometeu_hal::{HardwareBridge, InputSignals};
|
||||
use prometeu_hal::{HostContext, HostReturn, NativeInterface, SyscallId, expect_bool, expect_int};
|
||||
use prometeu_vm::{LogicalFrameEndingReason, VirtualMachine};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::time::Instant;
|
||||
|
||||
pub struct VirtualMachineRuntime {
|
||||
/// Host Tick Index: Incremented on every host hardware update (usually 60Hz).
|
||||
@ -128,7 +127,12 @@ impl VirtualMachineRuntime {
|
||||
match self.fs.mount(backend) {
|
||||
Ok(_) => {
|
||||
self.fs_state = FsState::Mounted;
|
||||
self.log(LogLevel::Info, LogSource::Fs, 0, "Filesystem mounted successfully".to_string());
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
LogSource::Fs,
|
||||
0,
|
||||
"Filesystem mounted successfully".to_string(),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
let err_msg = format!("Failed to mount filesystem: {:?}", e);
|
||||
@ -146,7 +150,12 @@ impl VirtualMachineRuntime {
|
||||
fn update_fs(&mut self) {
|
||||
if self.fs_state == FsState::Mounted {
|
||||
if !self.fs.is_healthy() {
|
||||
self.log(LogLevel::Error, LogSource::Fs, 0, "Filesystem became unhealthy, unmounting".to_string());
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
LogSource::Fs,
|
||||
0,
|
||||
"Filesystem became unhealthy, unmounting".to_string(),
|
||||
);
|
||||
self.unmount_fs();
|
||||
}
|
||||
}
|
||||
@ -173,7 +182,12 @@ impl VirtualMachineRuntime {
|
||||
self.current_entrypoint = cartridge.entrypoint.clone();
|
||||
}
|
||||
Err(e) => {
|
||||
self.log(LogLevel::Error, LogSource::Vm, 0, format!("Failed to initialize VM: {:?}", e));
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
LogSource::Vm,
|
||||
0,
|
||||
format!("Failed to initialize VM: {:?}", e),
|
||||
);
|
||||
// Fail fast: no program is installed, no app id is switched.
|
||||
// We don't update current_app_id or other fields.
|
||||
}
|
||||
@ -181,7 +195,11 @@ impl VirtualMachineRuntime {
|
||||
}
|
||||
|
||||
/// Executes a single VM instruction (Debug).
|
||||
pub fn debug_step_instruction(&mut self, vm: &mut VirtualMachine, hw: &mut dyn HardwareBridge) -> Option<String> {
|
||||
pub fn debug_step_instruction(
|
||||
&mut self,
|
||||
vm: &mut VirtualMachine,
|
||||
hw: &mut dyn HardwareBridge,
|
||||
) -> Option<String> {
|
||||
let mut ctx = HostContext::new(Some(hw));
|
||||
match vm.step(self, &mut ctx) {
|
||||
Ok(_) => None,
|
||||
@ -198,7 +216,12 @@ impl VirtualMachineRuntime {
|
||||
/// This method is responsible for managing the logical frame lifecycle.
|
||||
/// A single host tick might execute a full logical frame, part of it,
|
||||
/// or multiple frames depending on the configured slices.
|
||||
pub fn tick(&mut self, vm: &mut VirtualMachine, signals: &InputSignals, hw: &mut dyn HardwareBridge) -> Option<String> {
|
||||
pub fn tick(
|
||||
&mut self,
|
||||
vm: &mut VirtualMachine,
|
||||
signals: &InputSignals,
|
||||
hw: &mut dyn HardwareBridge,
|
||||
) -> Option<String> {
|
||||
let start = Instant::now();
|
||||
self.tick_index += 1;
|
||||
|
||||
@ -228,7 +251,11 @@ impl VirtualMachineRuntime {
|
||||
// Reset telemetry for the new logical frame
|
||||
self.telemetry_current = TelemetryFrame {
|
||||
frame_index: self.logical_frame_index,
|
||||
cycles_budget: self.certifier.config.cycles_budget_per_frame.unwrap_or(Self::CYCLES_PER_LOGICAL_FRAME),
|
||||
cycles_budget: self
|
||||
.certifier
|
||||
.config
|
||||
.cycles_budget_per_frame
|
||||
.unwrap_or(Self::CYCLES_PER_LOGICAL_FRAME),
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
@ -247,7 +274,8 @@ impl VirtualMachineRuntime {
|
||||
|
||||
match run_result {
|
||||
Ok(run) => {
|
||||
self.logical_frame_remaining_cycles = self.logical_frame_remaining_cycles.saturating_sub(run.cycles_used);
|
||||
self.logical_frame_remaining_cycles =
|
||||
self.logical_frame_remaining_cycles.saturating_sub(run.cycles_used);
|
||||
|
||||
// Accumulate metrics for telemetry and certification
|
||||
self.telemetry_current.cycles_used += run.cycles_used;
|
||||
@ -257,7 +285,12 @@ impl VirtualMachineRuntime {
|
||||
if run.reason == LogicalFrameEndingReason::Breakpoint {
|
||||
self.paused = true;
|
||||
self.debug_step_request = false;
|
||||
self.log(LogLevel::Info, LogSource::Vm, 0xDEB1, format!("Breakpoint hit at PC 0x{:X}", vm.pc));
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
LogSource::Vm,
|
||||
0xDEB1,
|
||||
format!("Breakpoint hit at PC 0x{:X}", vm.pc),
|
||||
);
|
||||
}
|
||||
|
||||
// Handle Panics
|
||||
@ -268,18 +301,24 @@ impl VirtualMachineRuntime {
|
||||
}
|
||||
|
||||
// 4. Frame Finalization (FRAME_SYNC reached or Entrypoint returned)
|
||||
if run.reason == LogicalFrameEndingReason::FrameSync ||
|
||||
run.reason == LogicalFrameEndingReason::EndOfRom {
|
||||
if run.reason == LogicalFrameEndingReason::FrameSync
|
||||
|| run.reason == LogicalFrameEndingReason::EndOfRom
|
||||
{
|
||||
// All drawing commands for this frame are now complete.
|
||||
// Finalize the framebuffer.
|
||||
hw.gfx_mut().render_all();
|
||||
|
||||
// Finalize frame telemetry
|
||||
self.telemetry_current.host_cpu_time_us = start.elapsed().as_micros() as u64;
|
||||
self.telemetry_current.host_cpu_time_us =
|
||||
start.elapsed().as_micros() as u64;
|
||||
|
||||
// Evaluate CAP (Execution Budget Compliance)
|
||||
let ts_ms = self.boot_time.elapsed().as_millis() as u64;
|
||||
self.telemetry_current.violations = self.certifier.evaluate(&self.telemetry_current, &mut self.log_service, ts_ms) as u32;
|
||||
self.telemetry_current.violations = self.certifier.evaluate(
|
||||
&self.telemetry_current,
|
||||
&mut self.log_service,
|
||||
ts_ms,
|
||||
) as u32;
|
||||
|
||||
// Latch telemetry for the Host/Debugger to read.
|
||||
self.telemetry_last = self.telemetry_current;
|
||||
@ -324,7 +363,9 @@ impl VirtualMachineRuntime {
|
||||
self.telemetry_current.audio_slots_occupied = audio_stats.slots_occupied as u32;
|
||||
|
||||
// If the frame ended exactly in this tick, we update the final real time in the latch.
|
||||
if !self.logical_frame_active && self.telemetry_last.frame_index == self.logical_frame_index.wrapping_sub(1) {
|
||||
if !self.logical_frame_active
|
||||
&& self.telemetry_last.frame_index == self.logical_frame_index.wrapping_sub(1)
|
||||
{
|
||||
self.telemetry_last.host_cpu_time_us = self.last_frame_cpu_time_us;
|
||||
self.telemetry_last.cycles_budget = self.telemetry_current.cycles_budget;
|
||||
self.telemetry_last.gfx_used_bytes = self.telemetry_current.gfx_used_bytes;
|
||||
@ -343,7 +384,6 @@ impl VirtualMachineRuntime {
|
||||
self.logs_written_this_frame.clear();
|
||||
}
|
||||
|
||||
|
||||
// Helper for syscalls
|
||||
fn syscall_log_write(&mut self, level_val: i64, tag: u16, msg: String) -> Result<(), VmFault> {
|
||||
let level = match level_val {
|
||||
@ -361,7 +401,12 @@ impl VirtualMachineRuntime {
|
||||
if count >= Self::MAX_LOGS_PER_FRAME {
|
||||
if count == Self::MAX_LOGS_PER_FRAME {
|
||||
self.logs_written_this_frame.insert(app_id, count + 1);
|
||||
self.log(LogLevel::Warn, LogSource::App { app_id }, 0, "App exceeded log limit per frame".to_string());
|
||||
self.log(
|
||||
LogLevel::Warn,
|
||||
LogSource::App { app_id },
|
||||
0,
|
||||
"App exceeded log limit per frame".to_string(),
|
||||
);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
@ -434,11 +479,17 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
/// - 0x5000: Logging
|
||||
///
|
||||
/// Each syscall returns the number of virtual cycles it consumed.
|
||||
fn syscall(&mut self, id: SyscallId, args: &[Value], ret: &mut HostReturn, ctx: &mut HostContext) -> Result<(), VmFault> {
|
||||
fn syscall(
|
||||
&mut self,
|
||||
id: SyscallId,
|
||||
args: &[Value],
|
||||
ret: &mut HostReturn,
|
||||
ctx: &mut HostContext,
|
||||
) -> Result<(), VmFault> {
|
||||
self.telemetry_current.syscalls += 1;
|
||||
let syscall = Syscall::from_u32(id).ok_or_else(|| VmFault::Trap(TRAP_INVALID_SYSCALL, format!(
|
||||
"Unknown syscall: 0x{:08X}", id
|
||||
)))?;
|
||||
let syscall = Syscall::from_u32(id).ok_or_else(|| {
|
||||
VmFault::Trap(TRAP_INVALID_SYSCALL, format!("Unknown syscall: 0x{:08X}", id))
|
||||
})?;
|
||||
|
||||
// Handle hardware-less syscalls first
|
||||
match syscall {
|
||||
@ -457,7 +508,6 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
|
||||
match syscall {
|
||||
// --- System Syscalls ---
|
||||
|
||||
Syscall::SystemHasCart => unreachable!(),
|
||||
Syscall::SystemRunCart => unreachable!(),
|
||||
|
||||
@ -535,7 +585,10 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
}
|
||||
// gfx.set_sprite(asset_name, id, x, y, tile_id, palette_id, active, flip_x, flip_y, priority)
|
||||
Syscall::GfxSetSprite => {
|
||||
let asset_name = match args.get(0).ok_or_else(|| VmFault::Panic("Missing asset_name".into()))? {
|
||||
let asset_name = match args
|
||||
.get(0)
|
||||
.ok_or_else(|| VmFault::Panic("Missing asset_name".into()))?
|
||||
{
|
||||
Value::String(s) => s.clone(),
|
||||
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_name".into())),
|
||||
};
|
||||
@ -549,7 +602,8 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
let flip_y = expect_bool(args, 8)?;
|
||||
let priority = expect_int(args, 9)? as u8;
|
||||
|
||||
let bank_id = hw.assets().find_slot_by_name(&asset_name, BankType::TILES).unwrap_or(0);
|
||||
let bank_id =
|
||||
hw.assets().find_slot_by_name(&asset_name, BankType::TILES).unwrap_or(0);
|
||||
|
||||
if index < 512 {
|
||||
*hw.gfx_mut().sprite_mut(index) = Sprite {
|
||||
@ -569,7 +623,10 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
Syscall::GfxDrawText => {
|
||||
let x = expect_int(args, 0)? as i32;
|
||||
let y = expect_int(args, 1)? as i32;
|
||||
let msg = match args.get(2).ok_or_else(|| VmFault::Panic("Missing message".into()))? {
|
||||
let msg = match args
|
||||
.get(2)
|
||||
.ok_or_else(|| VmFault::Panic("Missing message".into()))?
|
||||
{
|
||||
Value::String(s) => s.clone(),
|
||||
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string message".into())),
|
||||
};
|
||||
@ -583,7 +640,10 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
Syscall::GfxClear565 => {
|
||||
let color_val = expect_int(args, 0)? as u32;
|
||||
if color_val > 0xFFFF {
|
||||
return Err(VmFault::Trap(TRAP_OOB, "Color value out of bounds (bounded)".into()));
|
||||
return Err(VmFault::Trap(
|
||||
TRAP_OOB,
|
||||
"Color value out of bounds (bounded)".into(),
|
||||
));
|
||||
}
|
||||
let color = Color::from_raw(color_val as u16);
|
||||
hw.gfx_mut().clear(color);
|
||||
@ -655,9 +715,18 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
Syscall::InputPadSnapshot => {
|
||||
let pad = hw.pad();
|
||||
for btn in [
|
||||
pad.up(), pad.down(), pad.left(), pad.right(),
|
||||
pad.a(), pad.b(), pad.x(), pad.y(),
|
||||
pad.l(), pad.r(), pad.start(), pad.select(),
|
||||
pad.up(),
|
||||
pad.down(),
|
||||
pad.left(),
|
||||
pad.right(),
|
||||
pad.a(),
|
||||
pad.b(),
|
||||
pad.x(),
|
||||
pad.y(),
|
||||
pad.l(),
|
||||
pad.r(),
|
||||
pad.start(),
|
||||
pad.select(),
|
||||
] {
|
||||
ret.push_bool(btn.pressed);
|
||||
ret.push_bool(btn.released);
|
||||
@ -783,7 +852,10 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
let voice_id = expect_int(args, 1)? as usize;
|
||||
let volume = expect_int(args, 2)? as u8;
|
||||
let pan = expect_int(args, 3)? as u8;
|
||||
let pitch = match args.get(4).ok_or_else(|| VmFault::Panic("Missing pitch".into()))? {
|
||||
let pitch = match args
|
||||
.get(4)
|
||||
.ok_or_else(|| VmFault::Panic("Missing pitch".into()))?
|
||||
{
|
||||
Value::Float(f) => *f,
|
||||
Value::Int32(i) => *i as f64,
|
||||
Value::Int64(i) => *i as f64,
|
||||
@ -791,14 +863,26 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected number for pitch".into())),
|
||||
};
|
||||
|
||||
hw.audio_mut().play(0, sample_id as u16, voice_id, volume, pan, pitch, 0, prometeu_hal::LoopMode::Off);
|
||||
hw.audio_mut().play(
|
||||
0,
|
||||
sample_id as u16,
|
||||
voice_id,
|
||||
volume,
|
||||
pan,
|
||||
pitch,
|
||||
0,
|
||||
prometeu_hal::LoopMode::Off,
|
||||
);
|
||||
ret.push_null();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// audio.play(asset_name, sample_id, voice_id, volume, pan, pitch, loop_mode)
|
||||
Syscall::AudioPlay => {
|
||||
let asset_name = match args.get(0).ok_or_else(|| VmFault::Panic("Missing asset_name".into()))? {
|
||||
let asset_name = match args
|
||||
.get(0)
|
||||
.ok_or_else(|| VmFault::Panic("Missing asset_name".into()))?
|
||||
{
|
||||
Value::String(s) => s.clone(),
|
||||
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_name".into())),
|
||||
};
|
||||
@ -806,7 +890,10 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
let voice_id = expect_int(args, 2)? as usize;
|
||||
let volume = expect_int(args, 3)? as u8;
|
||||
let pan = expect_int(args, 4)? as u8;
|
||||
let pitch = match args.get(5).ok_or_else(|| VmFault::Panic("Missing pitch".into()))? {
|
||||
let pitch = match args
|
||||
.get(5)
|
||||
.ok_or_else(|| VmFault::Panic("Missing pitch".into()))?
|
||||
{
|
||||
Value::Float(f) => *f,
|
||||
Value::Int32(i) => *i as f64,
|
||||
Value::Int64(i) => *i as f64,
|
||||
@ -818,7 +905,8 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
_ => prometeu_hal::LoopMode::On,
|
||||
};
|
||||
|
||||
let bank_id = hw.assets().find_slot_by_name(&asset_name, BankType::SOUNDS).unwrap_or(0);
|
||||
let bank_id =
|
||||
hw.assets().find_slot_by_name(&asset_name, BankType::SOUNDS).unwrap_or(0);
|
||||
|
||||
hw.audio_mut().play(bank_id, sample_id, voice_id, volume, pan, pitch, 0, loop_mode);
|
||||
ret.push_null();
|
||||
@ -846,7 +934,10 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
// FS_READ(handle) -> content
|
||||
Syscall::FsRead => {
|
||||
let handle = expect_int(args, 0)? as u32;
|
||||
let path = self.open_files.get(&handle).ok_or_else(|| VmFault::Panic("Invalid handle".into()))?;
|
||||
let path = self
|
||||
.open_files
|
||||
.get(&handle)
|
||||
.ok_or_else(|| VmFault::Panic("Invalid handle".into()))?;
|
||||
match self.fs.read_file(path) {
|
||||
Ok(data) => {
|
||||
let s = String::from_utf8_lossy(&data).into_owned();
|
||||
@ -859,11 +950,17 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
// FS_WRITE(handle, content)
|
||||
Syscall::FsWrite => {
|
||||
let handle = expect_int(args, 0)? as u32;
|
||||
let content = match args.get(1).ok_or_else(|| VmFault::Panic("Missing content".into()))? {
|
||||
let content = match args
|
||||
.get(1)
|
||||
.ok_or_else(|| VmFault::Panic("Missing content".into()))?
|
||||
{
|
||||
Value::String(s) => s.as_bytes().to_vec(),
|
||||
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string content".into())),
|
||||
};
|
||||
let path = self.open_files.get(&handle).ok_or_else(|| VmFault::Panic("Invalid handle".into()))?;
|
||||
let path = self
|
||||
.open_files
|
||||
.get(&handle)
|
||||
.ok_or_else(|| VmFault::Panic("Invalid handle".into()))?;
|
||||
match self.fs.write_file(path, &content) {
|
||||
Ok(_) => ret.push_bool(true),
|
||||
Err(_) => ret.push_bool(false),
|
||||
@ -919,7 +1016,10 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
// LOG_WRITE(level, msg)
|
||||
Syscall::LogWrite => {
|
||||
let level = expect_int(args, 0)?;
|
||||
let msg = match args.get(1).ok_or_else(|| VmFault::Panic("Missing message".into()))? {
|
||||
let msg = match args
|
||||
.get(1)
|
||||
.ok_or_else(|| VmFault::Panic("Missing message".into()))?
|
||||
{
|
||||
Value::String(s) => s.clone(),
|
||||
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string message".into())),
|
||||
};
|
||||
@ -931,7 +1031,10 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
Syscall::LogWriteTag => {
|
||||
let level = expect_int(args, 0)?;
|
||||
let tag = expect_int(args, 1)? as u16;
|
||||
let msg = match args.get(2).ok_or_else(|| VmFault::Panic("Missing message".into()))? {
|
||||
let msg = match args
|
||||
.get(2)
|
||||
.ok_or_else(|| VmFault::Panic("Missing message".into()))?
|
||||
{
|
||||
Value::String(s) => s.clone(),
|
||||
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string message".into())),
|
||||
};
|
||||
@ -942,7 +1045,10 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
|
||||
// --- Asset Syscalls ---
|
||||
Syscall::AssetLoad => {
|
||||
let asset_id = match args.get(0).ok_or_else(|| VmFault::Panic("Missing asset_id".into()))? {
|
||||
let asset_id = match args
|
||||
.get(0)
|
||||
.ok_or_else(|| VmFault::Panic("Missing asset_id".into()))?
|
||||
{
|
||||
Value::String(s) => s.clone(),
|
||||
_ => return Err(VmFault::Trap(TRAP_TYPE, "Expected string asset_id".into())),
|
||||
};
|
||||
@ -1018,4 +1124,4 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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" }
|
||||
|
||||
@ -2,4 +2,4 @@ pub struct CallFrame {
|
||||
pub return_pc: u32,
|
||||
pub stack_base: usize,
|
||||
pub func_idx: usize,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,3 +1,3 @@
|
||||
pub struct ScopeFrame {
|
||||
pub scope_stack_base: usize,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
use prometeu_hal::syscalls::Syscall;
|
||||
use prometeu_bytecode::{decode_next, DecodeError};
|
||||
use prometeu_bytecode::FunctionMeta;
|
||||
use prometeu_bytecode::OpCode;
|
||||
use prometeu_bytecode::OpCodeSpecExt;
|
||||
use prometeu_bytecode::FunctionMeta;
|
||||
use prometeu_bytecode::{compute_function_layouts, FunctionLayout};
|
||||
use prometeu_bytecode::{DecodeError, decode_next};
|
||||
use prometeu_bytecode::{FunctionLayout, compute_function_layouts};
|
||||
use prometeu_hal::syscalls::Syscall;
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@ -71,14 +71,23 @@ impl Verifier {
|
||||
while pc < func_code.len() {
|
||||
valid_pc.insert(pc);
|
||||
let instr = decode_next(pc, func_code).map_err(|e| match e {
|
||||
DecodeError::UnknownOpcode { pc: _, opcode } =>
|
||||
VerifierError::UnknownOpcode { pc: func_start + pc, opcode },
|
||||
DecodeError::TruncatedOpcode { pc: _ } =>
|
||||
VerifierError::TruncatedOpcode { pc: func_start + pc },
|
||||
DecodeError::TruncatedImmediate { pc: _, opcode, need, have } =>
|
||||
VerifierError::TruncatedImmediate { pc: func_start + pc, opcode, need, have },
|
||||
DecodeError::ImmediateSizeMismatch { pc: _, opcode, expected, actual } =>
|
||||
VerifierError::TruncatedImmediate { pc: func_start + pc, opcode, need: expected, have: actual },
|
||||
DecodeError::UnknownOpcode { pc: _, opcode } => {
|
||||
VerifierError::UnknownOpcode { pc: func_start + pc, opcode }
|
||||
}
|
||||
DecodeError::TruncatedOpcode { pc: _ } => {
|
||||
VerifierError::TruncatedOpcode { pc: func_start + pc }
|
||||
}
|
||||
DecodeError::TruncatedImmediate { pc: _, opcode, need, have } => {
|
||||
VerifierError::TruncatedImmediate { pc: func_start + pc, opcode, need, have }
|
||||
}
|
||||
DecodeError::ImmediateSizeMismatch { pc: _, opcode, expected, actual } => {
|
||||
VerifierError::TruncatedImmediate {
|
||||
pc: func_start + pc,
|
||||
opcode,
|
||||
need: expected,
|
||||
have: actual,
|
||||
}
|
||||
}
|
||||
})?;
|
||||
pc = instr.next_pc;
|
||||
}
|
||||
@ -113,9 +122,7 @@ impl Verifier {
|
||||
})?;
|
||||
(callee.param_slots, callee.return_slots)
|
||||
}
|
||||
OpCode::Ret => {
|
||||
(func.return_slots, 0)
|
||||
}
|
||||
OpCode::Ret => (func.return_slots, 0),
|
||||
OpCode::Syscall => {
|
||||
let id = instr.imm_u32().unwrap();
|
||||
let syscall = Syscall::from_u32(id).ok_or_else(|| {
|
||||
@ -127,7 +134,10 @@ impl Verifier {
|
||||
};
|
||||
|
||||
if in_height < pops {
|
||||
return Err(VerifierError::StackUnderflow { pc: func_start + pc, opcode: instr.opcode });
|
||||
return Err(VerifierError::StackUnderflow {
|
||||
pc: func_start + pc,
|
||||
opcode: instr.opcode,
|
||||
});
|
||||
}
|
||||
|
||||
let out_height = in_height - pops + pushes;
|
||||
@ -135,7 +145,11 @@ impl Verifier {
|
||||
|
||||
if instr.opcode == OpCode::Ret {
|
||||
if in_height != func.return_slots {
|
||||
return Err(VerifierError::BadRetStackHeight { pc: func_start + pc, height: in_height, expected: func.return_slots });
|
||||
return Err(VerifierError::BadRetStackHeight {
|
||||
pc: func_start + pc,
|
||||
height: in_height,
|
||||
expected: func.return_slots,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,7 +157,8 @@ impl Verifier {
|
||||
if spec.is_branch {
|
||||
// Canonical contract: branch immediate is RELATIVE to function start.
|
||||
let target_rel = instr.imm_u32().unwrap() as usize;
|
||||
let func_end_abs = layouts.get(func_idx).map(|l| l.end).unwrap_or_else(|| code.len());
|
||||
let func_end_abs =
|
||||
layouts.get(func_idx).map(|l| l.end).unwrap_or_else(|| code.len());
|
||||
let func_len = func_end_abs - func_start;
|
||||
|
||||
if target_rel > func_len {
|
||||
@ -168,23 +183,38 @@ impl Verifier {
|
||||
target_abs_expected,
|
||||
is_boundary_target_rel
|
||||
);
|
||||
return Err(VerifierError::InvalidJumpTarget { pc: pc_abs, target: target_abs_expected });
|
||||
return Err(VerifierError::InvalidJumpTarget {
|
||||
pc: pc_abs,
|
||||
target: target_abs_expected,
|
||||
});
|
||||
}
|
||||
|
||||
if target_rel == func_len {
|
||||
// salto para o fim da função
|
||||
if out_height != func.return_slots {
|
||||
return Err(VerifierError::BadRetStackHeight { pc: func_start + pc, height: out_height, expected: func.return_slots });
|
||||
return Err(VerifierError::BadRetStackHeight {
|
||||
pc: func_start + pc,
|
||||
height: out_height,
|
||||
expected: func.return_slots,
|
||||
});
|
||||
}
|
||||
// caminho termina aqui
|
||||
} else {
|
||||
if !valid_pc.contains(&target_rel) {
|
||||
return Err(VerifierError::JumpToMidInstruction { pc: func_start + pc, target: func_start + target_rel });
|
||||
return Err(VerifierError::JumpToMidInstruction {
|
||||
pc: func_start + pc,
|
||||
target: func_start + target_rel,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(&existing_height) = stack_height_in.get(&target_rel) {
|
||||
if existing_height != out_height {
|
||||
return Err(VerifierError::StackMismatchJoin { pc: func_start + pc, target: func_start + target_rel, height_in: out_height, height_target: existing_height });
|
||||
return Err(VerifierError::StackMismatchJoin {
|
||||
pc: func_start + pc,
|
||||
target: func_start + target_rel,
|
||||
height_in: out_height,
|
||||
height_target: existing_height,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
stack_height_in.insert(target_rel, out_height);
|
||||
@ -199,7 +229,12 @@ impl Verifier {
|
||||
if next_pc < func_len {
|
||||
if let Some(&existing_height) = stack_height_in.get(&next_pc) {
|
||||
if existing_height != out_height {
|
||||
return Err(VerifierError::StackMismatchJoin { pc: func_start + pc, target: func_start + next_pc, height_in: out_height, height_target: existing_height });
|
||||
return Err(VerifierError::StackMismatchJoin {
|
||||
pc: func_start + pc,
|
||||
target: func_start + next_pc,
|
||||
height_in: out_height,
|
||||
height_target: existing_height,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
stack_height_in.insert(next_pc, out_height);
|
||||
@ -221,18 +256,15 @@ mod tests {
|
||||
fn test_verifier_underflow() {
|
||||
// OpCode::Add (2 bytes)
|
||||
let code = vec![OpCode::Add as u8, 0x00];
|
||||
let functions = vec![FunctionMeta {
|
||||
code_offset: 0,
|
||||
code_len: 2,
|
||||
..Default::default()
|
||||
}];
|
||||
let functions = vec![FunctionMeta { code_offset: 0, code_len: 2, ..Default::default() }];
|
||||
let res = Verifier::verify(&code, &functions);
|
||||
assert_eq!(res, Err(VerifierError::StackUnderflow { pc: 0, opcode: OpCode::Add }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verifier_dup_underflow() {
|
||||
let code = vec![(OpCode::Dup as u16).to_le_bytes()[0], (OpCode::Dup as u16).to_le_bytes()[1]];
|
||||
let code =
|
||||
vec![(OpCode::Dup as u16).to_le_bytes()[0], (OpCode::Dup as u16).to_le_bytes()[1]];
|
||||
let functions = vec![FunctionMeta { code_offset: 0, code_len: 2, ..Default::default() }];
|
||||
let res = Verifier::verify(&code, &functions);
|
||||
assert_eq!(res, Err(VerifierError::StackUnderflow { pc: 0, opcode: OpCode::Dup }));
|
||||
@ -243,11 +275,7 @@ mod tests {
|
||||
// Jmp (2 bytes) + 100u32 (4 bytes)
|
||||
let mut code = vec![OpCode::Jmp as u8, 0x00];
|
||||
code.extend_from_slice(&100u32.to_le_bytes());
|
||||
let functions = vec![FunctionMeta {
|
||||
code_offset: 0,
|
||||
code_len: 6,
|
||||
..Default::default()
|
||||
}];
|
||||
let functions = vec![FunctionMeta { code_offset: 0, code_len: 6, ..Default::default() }];
|
||||
let res = Verifier::verify(&code, &functions);
|
||||
assert_eq!(res, Err(VerifierError::InvalidJumpTarget { pc: 0, target: 100 }));
|
||||
}
|
||||
@ -261,12 +289,8 @@ mod tests {
|
||||
code.push(OpCode::Jmp as u8);
|
||||
code.push(0x00);
|
||||
code.extend_from_slice(&1u32.to_le_bytes());
|
||||
|
||||
let functions = vec![FunctionMeta {
|
||||
code_offset: 0,
|
||||
code_len: 12,
|
||||
..Default::default()
|
||||
}];
|
||||
|
||||
let functions = vec![FunctionMeta { code_offset: 0, code_len: 12, ..Default::default() }];
|
||||
let res = Verifier::verify(&code, &functions);
|
||||
assert_eq!(res, Err(VerifierError::JumpToMidInstruction { pc: 6, target: 1 }));
|
||||
}
|
||||
@ -295,11 +319,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_verifier_truncation_opcode() {
|
||||
let code = vec![OpCode::PushI32 as u8]; // Truncated u16 opcode
|
||||
let functions = vec![FunctionMeta {
|
||||
code_offset: 0,
|
||||
code_len: 1,
|
||||
..Default::default()
|
||||
}];
|
||||
let functions = vec![FunctionMeta { code_offset: 0, code_len: 1, ..Default::default() }];
|
||||
let res = Verifier::verify(&code, &functions);
|
||||
assert_eq!(res, Err(VerifierError::TruncatedOpcode { pc: 0 }));
|
||||
}
|
||||
@ -308,13 +328,17 @@ mod tests {
|
||||
fn test_verifier_truncation_immediate() {
|
||||
let mut code = vec![OpCode::PushI32 as u8, 0x00];
|
||||
code.push(0x42); // Only 1 byte of 4-byte immediate
|
||||
let functions = vec![FunctionMeta {
|
||||
code_offset: 0,
|
||||
code_len: 3,
|
||||
..Default::default()
|
||||
}];
|
||||
let functions = vec![FunctionMeta { code_offset: 0, code_len: 3, ..Default::default() }];
|
||||
let res = Verifier::verify(&code, &functions);
|
||||
assert_eq!(res, Err(VerifierError::TruncatedImmediate { pc: 0, opcode: OpCode::PushI32, need: 4, have: 1 }));
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(VerifierError::TruncatedImmediate {
|
||||
pc: 0,
|
||||
opcode: OpCode::PushI32,
|
||||
need: 4,
|
||||
have: 1
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -326,26 +350,41 @@ mod tests {
|
||||
// 15: PushI32 1
|
||||
// 21: Jmp 27
|
||||
// 27: Nop
|
||||
|
||||
|
||||
let mut code = Vec::new();
|
||||
code.push(OpCode::PushBool as u8); code.push(0x00); code.push(1); // 0: PushBool (3 bytes)
|
||||
code.push(OpCode::JmpIfTrue as u8); code.push(0x00); code.extend_from_slice(&15u32.to_le_bytes()); // 3: JmpIfTrue (6 bytes)
|
||||
code.push(OpCode::Jmp as u8); code.push(0x00); code.extend_from_slice(&27u32.to_le_bytes()); // 9: Jmp (6 bytes)
|
||||
code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes()); // 15: PushI32 (6 bytes)
|
||||
code.push(OpCode::Jmp as u8); code.push(0x00); code.extend_from_slice(&27u32.to_le_bytes()); // 21: Jmp (6 bytes)
|
||||
code.push(OpCode::Nop as u8); code.push(0x00); // 27: Nop (2 bytes)
|
||||
|
||||
let functions = vec![FunctionMeta {
|
||||
code_offset: 0,
|
||||
code_len: 29,
|
||||
..Default::default()
|
||||
}];
|
||||
code.push(OpCode::PushBool as u8);
|
||||
code.push(0x00);
|
||||
code.push(1); // 0: PushBool (3 bytes)
|
||||
code.push(OpCode::JmpIfTrue as u8);
|
||||
code.push(0x00);
|
||||
code.extend_from_slice(&15u32.to_le_bytes()); // 3: JmpIfTrue (6 bytes)
|
||||
code.push(OpCode::Jmp as u8);
|
||||
code.push(0x00);
|
||||
code.extend_from_slice(&27u32.to_le_bytes()); // 9: Jmp (6 bytes)
|
||||
code.push(OpCode::PushI32 as u8);
|
||||
code.push(0x00);
|
||||
code.extend_from_slice(&1u32.to_le_bytes()); // 15: PushI32 (6 bytes)
|
||||
code.push(OpCode::Jmp as u8);
|
||||
code.push(0x00);
|
||||
code.extend_from_slice(&27u32.to_le_bytes()); // 21: Jmp (6 bytes)
|
||||
code.push(OpCode::Nop as u8);
|
||||
code.push(0x00); // 27: Nop (2 bytes)
|
||||
|
||||
let functions = vec![FunctionMeta { code_offset: 0, code_len: 29, ..Default::default() }];
|
||||
let res = Verifier::verify(&code, &functions);
|
||||
// Path 0->3->9->27: height 1-1+0 = 0.
|
||||
// Path 0->3->15->21->27: height 1-1+1 = 1.
|
||||
// Mismatch at 27: 0 vs 1.
|
||||
|
||||
assert_eq!(res, Err(VerifierError::StackMismatchJoin { pc: 21, target: 27, height_in: 1, height_target: 0 }));
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(VerifierError::StackMismatchJoin {
|
||||
pc: 21,
|
||||
target: 27,
|
||||
height_in: 1,
|
||||
height_target: 0
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -356,7 +395,7 @@ mod tests {
|
||||
code.extend_from_slice(&1u32.to_le_bytes());
|
||||
code.push(OpCode::Ret as u8);
|
||||
code.push(0x00);
|
||||
|
||||
|
||||
let functions = vec![FunctionMeta {
|
||||
code_offset: 0,
|
||||
code_len: 8,
|
||||
@ -374,11 +413,17 @@ mod tests {
|
||||
// Add
|
||||
// Ret
|
||||
let mut code = Vec::new();
|
||||
code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&1u32.to_le_bytes());
|
||||
code.push(OpCode::PushI32 as u8); code.push(0x00); code.extend_from_slice(&2u32.to_le_bytes());
|
||||
code.push(OpCode::Add as u8); code.push(0x00);
|
||||
code.push(OpCode::Ret as u8); code.push(0x00);
|
||||
|
||||
code.push(OpCode::PushI32 as u8);
|
||||
code.push(0x00);
|
||||
code.extend_from_slice(&1u32.to_le_bytes());
|
||||
code.push(OpCode::PushI32 as u8);
|
||||
code.push(0x00);
|
||||
code.extend_from_slice(&2u32.to_le_bytes());
|
||||
code.push(OpCode::Add as u8);
|
||||
code.push(0x00);
|
||||
code.push(OpCode::Ret as u8);
|
||||
code.push(0x00);
|
||||
|
||||
let functions = vec![FunctionMeta {
|
||||
code_offset: 0,
|
||||
code_len: 16,
|
||||
@ -392,14 +437,11 @@ mod tests {
|
||||
#[test]
|
||||
fn test_verifier_invalid_syscall_id() {
|
||||
let mut code = Vec::new();
|
||||
code.push(OpCode::Syscall as u8); code.push(0x00);
|
||||
code.push(OpCode::Syscall as u8);
|
||||
code.push(0x00);
|
||||
code.extend_from_slice(&0xDEADBEEFu32.to_le_bytes()); // Unknown ID
|
||||
|
||||
let functions = vec![FunctionMeta {
|
||||
code_offset: 0,
|
||||
code_len: 6,
|
||||
..Default::default()
|
||||
}];
|
||||
|
||||
let functions = vec![FunctionMeta { code_offset: 0, code_len: 6, ..Default::default() }];
|
||||
let res = Verifier::verify(&code, &functions);
|
||||
assert_eq!(res, Err(VerifierError::InvalidSyscallId { pc: 0, id: 0xDEADBEEF }));
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
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,15 +1,15 @@
|
||||
use prometeu_hal::debugger_protocol::*;
|
||||
use prometeu_drivers::hardware::Hardware;
|
||||
use prometeu_firmware::{BootTarget, Firmware};
|
||||
use prometeu_hal::cartridge_loader::CartridgeLoader;
|
||||
use prometeu_hal::debugger_protocol::*;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use prometeu_drivers::hardware::Hardware;
|
||||
use prometeu_hal::cartridge_loader::CartridgeLoader;
|
||||
|
||||
/// Host-side implementation of the PROMETEU DevTools Protocol.
|
||||
///
|
||||
///
|
||||
/// This component acts as a TCP server that allows external tools (like the
|
||||
/// Prometeu Debugger) to observe and control the execution of the virtual machine.
|
||||
///
|
||||
///
|
||||
/// Communication is based on JSONL (JSON lines) over TCP.
|
||||
pub struct HostDebugger {
|
||||
/// If true, the VM will not start execution until a 'start' command is received.
|
||||
@ -42,12 +42,12 @@ impl HostDebugger {
|
||||
if let BootTarget::Cartridge { path, debug: true, debug_port } = boot_target {
|
||||
self.waiting_for_start = true;
|
||||
|
||||
// Pre-load cartridge metadata so the Handshake message can contain
|
||||
// Pre-load cartridge metadata so the Handshake message can contain
|
||||
// valid information about the App being debugged.
|
||||
if let Ok(cartridge) = CartridgeLoader::load(path) {
|
||||
firmware.os.initialize_vm(&mut firmware.vm, &cartridge);
|
||||
}
|
||||
|
||||
|
||||
match TcpListener::bind(format!("127.0.0.1:{}", debug_port)) {
|
||||
Ok(listener) => {
|
||||
// Set listener to non-blocking so it doesn't halt the main loop.
|
||||
@ -59,7 +59,7 @@ impl HostDebugger {
|
||||
eprintln!("[Debugger] Failed to bind to port {}: {}", debug_port, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
println!("[Debugger] (Or press D to start execution)");
|
||||
}
|
||||
}
|
||||
@ -94,9 +94,9 @@ impl HostDebugger {
|
||||
if self.stream.is_none() {
|
||||
println!("[Debugger] Connection received!");
|
||||
stream.set_nonblocking(true).expect("Cannot set non-blocking on stream");
|
||||
|
||||
|
||||
self.stream = Some(stream);
|
||||
|
||||
|
||||
// Immediately send the Handshake message to identify the Runtime and App.
|
||||
let handshake = DebugResponse::Handshake {
|
||||
protocol_version: DEVTOOLS_PROTOCOL_VERSION,
|
||||
@ -130,13 +130,15 @@ impl HostDebugger {
|
||||
Ok(n) => {
|
||||
let data = &buf[..n];
|
||||
let msg = String::from_utf8_lossy(data);
|
||||
|
||||
|
||||
self.stream = Some(stream);
|
||||
|
||||
|
||||
// Support multiple JSON messages in a single TCP packet.
|
||||
for line in msg.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() { continue; }
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Ok(cmd) = serde_json::from_str::<DebugCommand>(trimmed) {
|
||||
self.handle_command(cmd, firmware, hardware);
|
||||
}
|
||||
@ -154,7 +156,7 @@ impl HostDebugger {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 3. Push events (logs, telemetry) to the client.
|
||||
if self.stream.is_some() {
|
||||
self.stream_events(firmware);
|
||||
@ -162,7 +164,12 @@ impl HostDebugger {
|
||||
}
|
||||
|
||||
/// Dispatches a specific DebugCommand to the system components.
|
||||
fn handle_command(&mut self, cmd: DebugCommand, firmware: &mut Firmware, hardware: &mut Hardware) {
|
||||
fn handle_command(
|
||||
&mut self,
|
||||
cmd: DebugCommand,
|
||||
firmware: &mut Firmware,
|
||||
hardware: &mut Hardware,
|
||||
) {
|
||||
match cmd {
|
||||
DebugCommand::Ok | DebugCommand::Start => {
|
||||
if self.waiting_for_start {
|
||||
@ -189,9 +196,8 @@ impl HostDebugger {
|
||||
}
|
||||
DebugCommand::GetState => {
|
||||
// Return detailed VM register and stack state.
|
||||
let stack_top = firmware.vm.operand_stack.iter()
|
||||
.rev().take(10).cloned().collect();
|
||||
|
||||
let stack_top = firmware.vm.operand_stack.iter().rev().take(10).cloned().collect();
|
||||
|
||||
let resp = DebugResponse::GetState {
|
||||
pc: firmware.vm.pc,
|
||||
stack_top,
|
||||
@ -219,13 +225,13 @@ impl HostDebugger {
|
||||
let new_events = firmware.os.log_service.get_after(self.last_log_seq);
|
||||
for event in new_events {
|
||||
self.last_log_seq = event.seq;
|
||||
|
||||
|
||||
// Map specific internal log tags to protocol events.
|
||||
if event.tag == 0xDEB1 {
|
||||
self.send_event(DebugEvent::BreakpointHit {
|
||||
pc: firmware.vm.pc,
|
||||
frame_index: firmware.os.logical_frame_index,
|
||||
});
|
||||
self.send_event(DebugEvent::BreakpointHit {
|
||||
pc: firmware.vm.pc,
|
||||
frame_index: firmware.os.logical_frame_index,
|
||||
});
|
||||
}
|
||||
|
||||
// Map Certification tags (0xCA01-0xCA03) to 'Cert' protocol events.
|
||||
@ -266,27 +272,27 @@ impl HostDebugger {
|
||||
msg: event.msg.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 2. Send telemetry snapshots at the completion of every frame.
|
||||
let current_frame = firmware.os.logical_frame_index;
|
||||
if current_frame > self.last_telemetry_frame {
|
||||
let tel = &firmware.os.telemetry_last;
|
||||
self.send_event(DebugEvent::Telemetry {
|
||||
frame_index: tel.frame_index,
|
||||
vm_steps: tel.vm_steps,
|
||||
syscalls: tel.syscalls,
|
||||
cycles: tel.cycles_used,
|
||||
cycles_budget: tel.cycles_budget,
|
||||
host_cpu_time_us: tel.host_cpu_time_us,
|
||||
violations: tel.violations,
|
||||
gfx_used_bytes: tel.gfx_used_bytes,
|
||||
gfx_inflight_bytes: tel.gfx_inflight_bytes,
|
||||
gfx_slots_occupied: tel.gfx_slots_occupied,
|
||||
audio_used_bytes: tel.audio_used_bytes,
|
||||
audio_inflight_bytes: tel.audio_inflight_bytes,
|
||||
audio_slots_occupied: tel.audio_slots_occupied,
|
||||
});
|
||||
self.last_telemetry_frame = current_frame;
|
||||
let tel = &firmware.os.telemetry_last;
|
||||
self.send_event(DebugEvent::Telemetry {
|
||||
frame_index: tel.frame_index,
|
||||
vm_steps: tel.vm_steps,
|
||||
syscalls: tel.syscalls,
|
||||
cycles: tel.cycles_used,
|
||||
cycles_budget: tel.cycles_budget,
|
||||
host_cpu_time_us: tel.host_cpu_time_us,
|
||||
violations: tel.violations,
|
||||
gfx_used_bytes: tel.gfx_used_bytes,
|
||||
gfx_inflight_bytes: tel.gfx_inflight_bytes,
|
||||
gfx_slots_occupied: tel.gfx_slots_occupied,
|
||||
audio_used_bytes: tel.audio_used_bytes,
|
||||
audio_inflight_bytes: tel.audio_inflight_bytes,
|
||||
audio_slots_occupied: tel.audio_slots_occupied,
|
||||
});
|
||||
self.last_telemetry_frame = current_frame;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use prometeu_system::fs::{FsBackend, FsEntry, FsError};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use prometeu_system::fs::{FsBackend, FsEntry, FsError};
|
||||
|
||||
pub struct HostDirBackend {
|
||||
root: PathBuf,
|
||||
@ -90,7 +90,11 @@ mod tests {
|
||||
|
||||
fn get_temp_dir(name: &str) -> PathBuf {
|
||||
let mut path = env::temp_dir();
|
||||
path.push(format!("prometeu_host_test_{}_{}", name, std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()));
|
||||
path.push(format!(
|
||||
"prometeu_host_test_{}_{}",
|
||||
name,
|
||||
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()
|
||||
));
|
||||
fs::create_dir_all(&path).unwrap();
|
||||
path
|
||||
}
|
||||
@ -99,14 +103,14 @@ mod tests {
|
||||
fn test_host_dir_backend_mount_and_dirs() {
|
||||
let root = get_temp_dir("mount");
|
||||
let mut backend = HostDirBackend::new(root.clone());
|
||||
|
||||
|
||||
backend.mount().unwrap();
|
||||
|
||||
|
||||
assert!(root.join("system").is_dir());
|
||||
assert!(root.join("apps").is_dir());
|
||||
assert!(root.join("media").is_dir());
|
||||
assert!(root.join("user").is_dir());
|
||||
|
||||
|
||||
let _ = fs::remove_dir_all(root);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -35,7 +35,7 @@ impl HostConsoleSink {
|
||||
"[{:06}ms][{}][{}][{}] {}",
|
||||
event.ts_ms, event.frame, level_str, source_str, event.msg
|
||||
);
|
||||
|
||||
|
||||
self.last_seq = Some(event.seq);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
prometeu_host_desktop_winit::run()
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,11 @@ use crate::stats::HostStats;
|
||||
use crate::utilities::draw_rgb565_to_rgba8;
|
||||
use pixels::wgpu::PresentMode;
|
||||
use pixels::{Pixels, PixelsBuilder, SurfaceTexture};
|
||||
use prometeu_drivers::AudioCommand;
|
||||
use prometeu_drivers::hardware::Hardware;
|
||||
use prometeu_firmware::{BootTarget, Firmware};
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::telemetry::CertificationConfig;
|
||||
use std::time::{Duration, Instant};
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::dpi::LogicalSize;
|
||||
@ -15,14 +19,10 @@ use winit::event::{ElementState, WindowEvent};
|
||||
use winit::event_loop::{ActiveEventLoop, ControlFlow};
|
||||
use winit::keyboard::{KeyCode, PhysicalKey};
|
||||
use winit::window::{Window, WindowAttributes, WindowId};
|
||||
use prometeu_hal::telemetry::CertificationConfig;
|
||||
use prometeu_drivers::hardware::Hardware;
|
||||
use prometeu_drivers::AudioCommand;
|
||||
use prometeu_hal::color::Color;
|
||||
|
||||
/// The Desktop implementation of the PROMETEU Runtime.
|
||||
///
|
||||
/// This struct acts as the physical "chassis" of the virtual console. It is
|
||||
///
|
||||
/// This struct acts as the physical "chassis" of the virtual console. It is
|
||||
/// responsible for:
|
||||
/// - Creating and managing the OS window (via `winit`).
|
||||
/// - Initializing the GPU-accelerated framebuffer (via `pixels`).
|
||||
@ -130,8 +130,18 @@ impl HostRunner {
|
||||
let color_warn = Color::RED;
|
||||
|
||||
self.hardware.gfx.fill_rect(5, 5, 175, 100, color_bg);
|
||||
self.hardware.gfx.draw_text(10, 10, &format!("FPS: {:.1}", self.stats.current_fps), color_text);
|
||||
self.hardware.gfx.draw_text(10, 18, &format!("HOST: {:.2}MS", tel.host_cpu_time_us as f64 / 1000.0), color_text);
|
||||
self.hardware.gfx.draw_text(
|
||||
10,
|
||||
10,
|
||||
&format!("FPS: {:.1}", self.stats.current_fps),
|
||||
color_text,
|
||||
);
|
||||
self.hardware.gfx.draw_text(
|
||||
10,
|
||||
18,
|
||||
&format!("HOST: {:.2}MS", tel.host_cpu_time_us as f64 / 1000.0),
|
||||
color_text,
|
||||
);
|
||||
self.hardware.gfx.draw_text(10, 26, &format!("STEPS: {}", tel.vm_steps), color_text);
|
||||
self.hardware.gfx.draw_text(10, 34, &format!("SYSC: {}", tel.syscalls), color_text);
|
||||
|
||||
@ -140,25 +150,60 @@ impl HostRunner {
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
self.hardware.gfx.draw_text(10, 42, &format!("CYC: {}/{} ({:.1}%)", tel.cycles_used, tel.cycles_budget, cycles_pct), color_text);
|
||||
self.hardware.gfx.draw_text(
|
||||
10,
|
||||
42,
|
||||
&format!("CYC: {}/{} ({:.1}%)", tel.cycles_used, tel.cycles_budget, cycles_pct),
|
||||
color_text,
|
||||
);
|
||||
|
||||
self.hardware.gfx.draw_text(10, 50, &format!("GFX: {}K/16M ({}S)", tel.gfx_used_bytes / 1024, tel.gfx_slots_occupied), color_text);
|
||||
self.hardware.gfx.draw_text(
|
||||
10,
|
||||
50,
|
||||
&format!("GFX: {}K/16M ({}S)", tel.gfx_used_bytes / 1024, tel.gfx_slots_occupied),
|
||||
color_text,
|
||||
);
|
||||
if tel.gfx_inflight_bytes > 0 {
|
||||
self.hardware.gfx.draw_text(10, 58, &format!("LOAD GFX: {}KB", tel.gfx_inflight_bytes / 1024), color_warn);
|
||||
self.hardware.gfx.draw_text(
|
||||
10,
|
||||
58,
|
||||
&format!("LOAD GFX: {}KB", tel.gfx_inflight_bytes / 1024),
|
||||
color_warn,
|
||||
);
|
||||
}
|
||||
|
||||
self.hardware.gfx.draw_text(10, 66, &format!("AUD: {}K/32M ({}S)", tel.audio_used_bytes / 1024, tel.audio_slots_occupied), color_text);
|
||||
self.hardware.gfx.draw_text(
|
||||
10,
|
||||
66,
|
||||
&format!("AUD: {}K/32M ({}S)", tel.audio_used_bytes / 1024, tel.audio_slots_occupied),
|
||||
color_text,
|
||||
);
|
||||
if tel.audio_inflight_bytes > 0 {
|
||||
self.hardware.gfx.draw_text(10, 74, &format!("LOAD AUD: {}KB", tel.audio_inflight_bytes / 1024), color_warn);
|
||||
self.hardware.gfx.draw_text(
|
||||
10,
|
||||
74,
|
||||
&format!("LOAD AUD: {}KB", tel.audio_inflight_bytes / 1024),
|
||||
color_warn,
|
||||
);
|
||||
}
|
||||
|
||||
let cert_color = if tel.violations > 0 { color_warn } else { color_text };
|
||||
self.hardware.gfx.draw_text(10, 82, &format!("CERT LAST: {}", tel.violations), cert_color);
|
||||
|
||||
if tel.violations > 0 {
|
||||
if let Some(event) = self.firmware.os.log_service.get_recent(10).into_iter().rev().find(|e| e.tag >= 0xCA01 && e.tag <= 0xCA03) {
|
||||
if let Some(event) = self
|
||||
.firmware
|
||||
.os
|
||||
.log_service
|
||||
.get_recent(10)
|
||||
.into_iter()
|
||||
.rev()
|
||||
.find(|e| e.tag >= 0xCA01 && e.tag <= 0xCA03)
|
||||
{
|
||||
let mut msg = event.msg.clone();
|
||||
if msg.len() > 30 { msg.truncate(30); }
|
||||
if msg.len() > 30 {
|
||||
msg.truncate(30);
|
||||
}
|
||||
self.hardware.gfx.draw_text(10, 90, &msg, color_warn);
|
||||
}
|
||||
}
|
||||
@ -183,11 +228,12 @@ impl ApplicationHandler for HostRunner {
|
||||
let size = window.inner_size();
|
||||
let surface_texture = SurfaceTexture::new(size.width, size.height, window);
|
||||
|
||||
let mut pixels = PixelsBuilder::new(Hardware::W as u32, Hardware::H as u32, surface_texture)
|
||||
.present_mode(PresentMode::Fifo) // activate vsync
|
||||
.build()
|
||||
.expect("failed to create Pixels");
|
||||
|
||||
let mut pixels =
|
||||
PixelsBuilder::new(Hardware::W as u32, Hardware::H as u32, surface_texture)
|
||||
.present_mode(PresentMode::Fifo) // activate vsync
|
||||
.build()
|
||||
.expect("failed to create Pixels");
|
||||
|
||||
pixels.frame_mut().fill(0);
|
||||
|
||||
self.pixels = Some(pixels);
|
||||
@ -197,7 +243,6 @@ impl ApplicationHandler for HostRunner {
|
||||
event_loop.set_control_flow(ControlFlow::Poll);
|
||||
}
|
||||
|
||||
|
||||
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
|
||||
self.input.handle_event(&event, self.window());
|
||||
|
||||
@ -232,7 +277,6 @@ impl ApplicationHandler for HostRunner {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
WindowEvent::KeyboardInput { event, .. } => {
|
||||
if let PhysicalKey::Code(code) = event.physical_key {
|
||||
let is_down = event.state == ElementState::Pressed;
|
||||
@ -270,13 +314,13 @@ impl ApplicationHandler for HostRunner {
|
||||
}
|
||||
|
||||
// 3. Timing Management (The heart of determinism).
|
||||
// We measure the elapsed time since the last iteration and add it to an
|
||||
// accumulator. We then execute exactly as many 60Hz slices as the
|
||||
// We measure the elapsed time since the last iteration and add it to an
|
||||
// accumulator. We then execute exactly as many 60Hz slices as the
|
||||
// accumulator allows.
|
||||
let now = Instant::now();
|
||||
let mut frame_delta = now.duration_since(self.last_frame_time);
|
||||
|
||||
// Safety cap: if the OS freezes or we fall behind too much, we don't try
|
||||
// Safety cap: if the OS freezes or we fall behind too much, we don't try
|
||||
// to catch up indefinitely (avoiding the "death spiral").
|
||||
if frame_delta > Duration::from_millis(100) {
|
||||
frame_delta = Duration::from_millis(100);
|
||||
@ -293,16 +337,13 @@ impl ApplicationHandler for HostRunner {
|
||||
}
|
||||
|
||||
// Sync pause state with audio.
|
||||
// We do this AFTER firmware.tick to avoid MasterPause/Resume commands
|
||||
// We do this AFTER firmware.tick to avoid MasterPause/Resume commands
|
||||
// being cleared by the OS if a new logical frame starts in this tick.
|
||||
let is_paused = self.firmware.os.paused || self.debugger.waiting_for_start;
|
||||
if is_paused != self.last_paused_state {
|
||||
self.last_paused_state = is_paused;
|
||||
let cmd = if is_paused {
|
||||
AudioCommand::MasterPause
|
||||
} else {
|
||||
AudioCommand::MasterResume
|
||||
};
|
||||
let cmd =
|
||||
if is_paused { AudioCommand::MasterPause } else { AudioCommand::MasterResume };
|
||||
self.hardware.audio.commands.push(cmd);
|
||||
}
|
||||
|
||||
@ -331,7 +372,7 @@ impl ApplicationHandler for HostRunner {
|
||||
// 5. Rendering the Telemetry Overlay (if enabled).
|
||||
if self.overlay_enabled {
|
||||
// We temporarily swap buffers to draw over the current image.
|
||||
self.hardware.gfx.present();
|
||||
self.hardware.gfx.present();
|
||||
self.display_dbg_overlay();
|
||||
self.hardware.gfx.present();
|
||||
}
|
||||
@ -345,9 +386,9 @@ impl ApplicationHandler for HostRunner {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use prometeu_firmware::BootTarget;
|
||||
use prometeu_hal::debugger_protocol::DEVTOOLS_PROTOCOL_VERSION;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpStream;
|
||||
use prometeu_hal::debugger_protocol::DEVTOOLS_PROTOCOL_VERSION;
|
||||
|
||||
#[test]
|
||||
fn test_debug_port_opens() {
|
||||
@ -364,10 +405,11 @@ mod tests {
|
||||
|
||||
// Check if we can connect
|
||||
{
|
||||
let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect");
|
||||
let mut stream =
|
||||
TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect");
|
||||
// Short sleep to ensure the OS processes
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
|
||||
|
||||
// Simulates the loop to accept the connection
|
||||
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
|
||||
assert!(runner.debugger.stream.is_some(), "Stream should have been kept open");
|
||||
@ -375,24 +417,36 @@ mod tests {
|
||||
// Handshake Check
|
||||
let mut buf = [0u8; 2048];
|
||||
let n = stream.read(&mut buf).expect("Should read handshake");
|
||||
let resp: serde_json::Value = serde_json::from_slice(&buf[..n]).expect("Handshake should be valid JSON");
|
||||
let resp: serde_json::Value =
|
||||
serde_json::from_slice(&buf[..n]).expect("Handshake should be valid JSON");
|
||||
assert_eq!(resp["type"], "handshake");
|
||||
assert_eq!(resp["protocol_version"], DEVTOOLS_PROTOCOL_VERSION);
|
||||
|
||||
// Send start via JSON
|
||||
stream.write_all(b"{\"type\":\"start\"}\n").expect("Connection should be open for writing");
|
||||
stream
|
||||
.write_all(b"{\"type\":\"start\"}\n")
|
||||
.expect("Connection should be open for writing");
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
|
||||
|
||||
// Process the received command
|
||||
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
|
||||
assert!(!runner.debugger.waiting_for_start, "Execution should have started after start command");
|
||||
assert!(runner.debugger.listener.is_some(), "Listener should remain open for reconnections");
|
||||
assert!(
|
||||
!runner.debugger.waiting_for_start,
|
||||
"Execution should have started after start command"
|
||||
);
|
||||
assert!(
|
||||
runner.debugger.listener.is_some(),
|
||||
"Listener should remain open for reconnections"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Now that the stream is out of the test scope, the runner should detect closure on next check
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
|
||||
assert!(runner.debugger.stream.is_none(), "Stream should have been closed after client disconnected");
|
||||
assert!(
|
||||
runner.debugger.stream.is_none(),
|
||||
"Stream should have been closed after client disconnected"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -407,7 +461,8 @@ mod tests {
|
||||
|
||||
// 1. Connect and start
|
||||
{
|
||||
let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 1");
|
||||
let mut stream =
|
||||
TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 1");
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
|
||||
assert!(runner.debugger.stream.is_some());
|
||||
@ -427,10 +482,13 @@ mod tests {
|
||||
// 3. Try to reconnect - SHOULD FAIL currently, but we want it to WORK
|
||||
let stream2 = TcpStream::connect(format!("127.0.0.1:{}", port));
|
||||
assert!(stream2.is_ok(), "Should accept new connection even after start");
|
||||
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
|
||||
assert!(runner.debugger.stream.is_some(), "Stream should have been accepted on reconnection");
|
||||
assert!(
|
||||
runner.debugger.stream.is_some(),
|
||||
"Stream should have been accepted on reconnection"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -444,22 +502,27 @@ mod tests {
|
||||
});
|
||||
|
||||
// 1. First connection
|
||||
let mut _stream1 = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 1");
|
||||
let mut _stream1 =
|
||||
TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 1");
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
|
||||
assert!(runner.debugger.stream.is_some());
|
||||
|
||||
// 2. Second connection
|
||||
let mut stream2 = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect 2 (OS accepts)");
|
||||
let mut stream2 = TcpStream::connect(format!("127.0.0.1:{}", port))
|
||||
.expect("Should connect 2 (OS accepts)");
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware); // Should accept and close stream2
|
||||
|
||||
|
||||
// Check if stream2 was closed by the server
|
||||
let mut buf = [0u8; 10];
|
||||
stream2.set_read_timeout(Some(std::time::Duration::from_millis(100))).unwrap();
|
||||
let res = stream2.read(&mut buf);
|
||||
assert!(matches!(res, Ok(0)) || res.is_err(), "Second connection should be closed by server");
|
||||
|
||||
assert!(
|
||||
matches!(res, Ok(0)) || res.is_err(),
|
||||
"Second connection should be closed by server"
|
||||
);
|
||||
|
||||
assert!(runner.debugger.stream.is_some(), "First connection should continue active");
|
||||
}
|
||||
|
||||
@ -479,7 +542,7 @@ mod tests {
|
||||
|
||||
use std::io::BufRead;
|
||||
let mut reader = std::io::BufReader::new(stream);
|
||||
|
||||
|
||||
let mut line = String::new();
|
||||
reader.read_line(&mut line).expect("Should read handshake");
|
||||
assert!(line.contains("handshake"));
|
||||
@ -487,15 +550,17 @@ mod tests {
|
||||
// Send getState
|
||||
reader.get_mut().write_all(b"{\"type\":\"getState\"}\n").expect("Should write getState");
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
|
||||
|
||||
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
|
||||
|
||||
// Check if response received (may have events/logs before)
|
||||
loop {
|
||||
line.clear();
|
||||
reader.read_line(&mut line).expect("Should read line");
|
||||
if line.is_empty() { break; }
|
||||
|
||||
if line.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Ok(resp) = serde_json::from_str::<serde_json::Value>(&line) {
|
||||
if resp["type"] == "getState" {
|
||||
return;
|
||||
@ -517,10 +582,11 @@ mod tests {
|
||||
|
||||
// 1. Connect and pause
|
||||
{
|
||||
let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect");
|
||||
let mut stream =
|
||||
TcpStream::connect(format!("127.0.0.1:{}", port)).expect("Should connect");
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
|
||||
|
||||
|
||||
stream.write_all(b"{\"type\":\"pause\"}\n").expect("Should write pause");
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
|
||||
@ -530,7 +596,7 @@ mod tests {
|
||||
// 2. Disconnect (stream goes out of scope)
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
runner.debugger.check_commands(&mut runner.firmware, &mut runner.hardware);
|
||||
|
||||
|
||||
// 3. Check if unpaused
|
||||
assert!(!runner.firmware.os.paused, "VM should have unpaused after disconnect");
|
||||
assert!(!runner.debugger.waiting_for_start, "VM should have left waiting_for_start state");
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use prometeu_drivers::hardware::Hardware;
|
||||
use prometeu_firmware::Firmware;
|
||||
use std::time::{Duration, Instant};
|
||||
use winit::window::Window;
|
||||
use prometeu_drivers::hardware::Hardware;
|
||||
|
||||
pub struct HostStats {
|
||||
pub last_stats_update: Instant,
|
||||
@ -31,15 +31,22 @@ impl HostStats {
|
||||
self.audio_load_samples += 1;
|
||||
}
|
||||
|
||||
pub fn update(&mut self, now: Instant, window: Option<&Window>, _hardware: &Hardware, firmware: &Firmware) {
|
||||
pub fn update(
|
||||
&mut self,
|
||||
now: Instant,
|
||||
window: Option<&Window>,
|
||||
_hardware: &Hardware,
|
||||
firmware: &Firmware,
|
||||
) {
|
||||
let stats_elapsed = now.duration_since(self.last_stats_update);
|
||||
if stats_elapsed >= Duration::from_secs(1) {
|
||||
self.current_fps = self.frames_since_last_update as f64 / stats_elapsed.as_secs_f64();
|
||||
|
||||
|
||||
if let Some(window) = window {
|
||||
// Fixed comparison always against 60Hz, keep even when doing CPU stress tests
|
||||
let frame_budget_us = 16666.0;
|
||||
let cpu_load_core = (firmware.os.last_frame_cpu_time_us as f64 / frame_budget_us) * 100.0;
|
||||
let cpu_load_core =
|
||||
(firmware.os.last_frame_cpu_time_us as f64 / frame_budget_us) * 100.0;
|
||||
|
||||
let cpu_load_audio = if self.audio_load_samples > 0 {
|
||||
(self.audio_load_accum_us as f64 / stats_elapsed.as_micros() as f64) * 100.0
|
||||
@ -49,7 +56,12 @@ impl HostStats {
|
||||
|
||||
let title = format!(
|
||||
"PROMETEU | GFX: {:.1} KB | FPS: {:.1} | Load: {:.1}% (C) + {:.1}% (A) | Frame: tick {} logical {}",
|
||||
0, self.current_fps, cpu_load_core, cpu_load_audio, firmware.os.tick_index, firmware.os.logical_frame_index
|
||||
0,
|
||||
self.current_fps,
|
||||
cpu_load_core,
|
||||
cpu_load_audio,
|
||||
firmware.os.tick_index,
|
||||
firmware.os.logical_frame_index
|
||||
);
|
||||
window.set_title(&title);
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -4,10 +4,10 @@ use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
/// PROMETEU Dispatcher (CLI).
|
||||
///
|
||||
/// The main entry point for the user. This binary does not implement
|
||||
/// compilation or execution logic itself; instead, it acts as a smart
|
||||
/// front-end that locates and dispatches commands to specialized
|
||||
///
|
||||
/// The main entry point for the user. This binary does not implement
|
||||
/// compilation or execution logic itself; instead, it acts as a smart
|
||||
/// front-end that locates and dispatches commands to specialized
|
||||
/// components like `prometeu-host-desktop-winit` or `prometeu-build-pipeline`.
|
||||
#[derive(Parser)]
|
||||
#[command(name = "prometeu")]
|
||||
@ -141,7 +141,8 @@ fn dispatch(exe_dir: &Path, bin_name: &str, args: &[&str]) {
|
||||
"prometeuc" => "build/verify c",
|
||||
"prometeup" => "pack/verify p",
|
||||
_ => bin_name,
|
||||
}, exe_dir.display()
|
||||
},
|
||||
exe_dir.display()
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
@ -168,9 +169,6 @@ fn execute_bin(bin_path: &Path, args: &[&str]) {
|
||||
}
|
||||
|
||||
fn not_implemented(cmd: &str, _bin_name: &str) {
|
||||
eprintln!(
|
||||
"prometeu: command '{}' is not yet available in this distribution",
|
||||
cmd
|
||||
);
|
||||
eprintln!("prometeu: command '{}' is not yet available in this distribution", cmd);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
25
docs/ARCHITECTURE.md
Normal file
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