This commit is contained in:
bQUARKz 2026-02-03 16:51:28 +00:00
parent bbe7ea4dbe
commit c5c1f68f7c
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
31 changed files with 1240 additions and 40 deletions

9
Cargo.lock generated
View File

@ -2188,6 +2188,14 @@ dependencies = [
"prometeu-runtime-desktop", "prometeu-runtime-desktop",
] ]
[[package]]
name = "prometeu-abi"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
]
[[package]] [[package]]
name = "prometeu-analysis" name = "prometeu-analysis"
version = "0.1.0" version = "0.1.0"
@ -2226,6 +2234,7 @@ dependencies = [
name = "prometeu-core" name = "prometeu-core"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"prometeu-abi",
"prometeu-bytecode", "prometeu-bytecode",
"serde", "serde",
"serde_json", "serde_json",

View File

@ -1,5 +1,6 @@
[workspace] [workspace]
members = [ members = [
"crates/prometeu-abi",
"crates/prometeu-core", "crates/prometeu-core",
"crates/prometeu-runtime-desktop", "crates/prometeu-runtime-desktop",
"crates/prometeu", "crates/prometeu",

View File

@ -0,0 +1,9 @@
[package]
name = "prometeu-abi"
version = "0.1.0"
edition = "2024"
license.workspace = true
[dependencies]
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"

View File

@ -0,0 +1,147 @@
use crate::model::AppMode;
use serde::{Deserialize, Serialize};
use crate::virtual_machine::Value;
pub const DEVTOOLS_PROTOCOL_VERSION: u32 = 1;
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum DebugCommand {
#[serde(rename = "ok")]
Ok,
#[serde(rename = "start")]
Start,
#[serde(rename = "pause")]
Pause,
#[serde(rename = "resume")]
Resume,
#[serde(rename = "step")]
Step,
#[serde(rename = "stepFrame")]
StepFrame,
#[serde(rename = "getState")]
GetState,
#[serde(rename = "setBreakpoint")]
SetBreakpoint { pc: usize },
#[serde(rename = "listBreakpoints")]
ListBreakpoints,
#[serde(rename = "clearBreakpoint")]
ClearBreakpoint { pc: usize },
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum DebugResponse {
#[serde(rename = "handshake")]
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,
},
#[serde(rename = "breakpoints")]
Breakpoints {
pcs: Vec<usize>,
},
}
#[derive(Debug, Serialize, Deserialize)]
pub struct HandshakeCartridge {
pub app_id: u32,
pub title: String,
pub app_version: String,
pub app_mode: AppMode,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "event")]
pub enum DebugEvent {
#[serde(rename = "breakpointHit")]
BreakpointHit {
pc: usize,
frame_index: u64,
},
#[serde(rename = "log")]
Log {
level: String,
source: String,
msg: String,
},
#[serde(rename = "telemetry")]
Telemetry {
frame_index: u64,
vm_steps: u32,
syscalls: u32,
cycles: u64,
cycles_budget: u64,
host_cpu_time_us: u64,
violations: u32,
gfx_used_bytes: usize,
gfx_inflight_bytes: usize,
gfx_slots_occupied: u32,
audio_used_bytes: usize,
audio_inflight_bytes: usize,
audio_slots_occupied: u32,
},
#[serde(rename = "cert")]
Cert {
rule: String,
used: u64,
limit: u64,
frame_index: u64,
},
}
#[cfg(test)]
mod tests {
use super::*;
use crate::virtual_machine::Value;
#[test]
fn test_telemetry_event_serialization() {
let event = DebugEvent::Telemetry {
frame_index: 10,
vm_steps: 100,
syscalls: 5,
cycles: 5000,
cycles_budget: 10000,
host_cpu_time_us: 1200,
violations: 0,
gfx_used_bytes: 1024,
gfx_inflight_bytes: 0,
gfx_slots_occupied: 1,
audio_used_bytes: 2048,
audio_inflight_bytes: 0,
audio_slots_occupied: 2,
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"event\":\"telemetry\""));
assert!(json.contains("\"cycles\":5000"));
assert!(json.contains("\"cycles_budget\":10000"));
}
#[test]
fn test_get_state_serialization() {
let resp = DebugResponse::GetState {
pc: 42,
stack_top: vec![Value::Int64(10), Value::String("test".into()), Value::Boolean(true)],
frame_index: 5,
app_id: 1,
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"type\":\"getState\""));
assert!(json.contains("\"pc\":42"));
assert!(json.contains("\"stack_top\":[10,\"test\",true]"));
assert!(json.contains("\"frame_index\":5"));
assert!(json.contains("\"app_id\":1"));
}
}

View File

@ -0,0 +1,12 @@
//! Prometeu ABI: tipos e contratos compartilhados entre os crates.
pub mod model;
pub mod log;
pub mod telemetry;
pub mod debugger_protocol;
// Tipos da VM que fazem parte do contrato (ex.: inspeção de pilha pelo debugger)
pub mod virtual_machine {
mod value;
pub use value::Value;
}

View File

@ -0,0 +1,13 @@
use crate::log::LogLevel;
use crate::log::LogSource;
#[derive(Debug, Clone)]
pub struct LogEvent {
pub seq: u64,
pub ts_ms: u64,
pub frame: u64,
pub level: LogLevel,
pub source: LogSource,
pub tag: u16,
pub msg: String,
}

View File

@ -0,0 +1,8 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}

View File

@ -0,0 +1,93 @@
use crate::log::{LogEvent, LogLevel, LogSource};
use std::collections::VecDeque;
pub struct LogService {
events: VecDeque<LogEvent>,
capacity: usize,
next_seq: u64,
}
impl LogService {
pub fn new(capacity: usize) -> Self {
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) {
if self.events.len() >= self.capacity {
self.events.pop_front();
}
self.events.push_back(LogEvent {
seq: self.next_seq,
ts_ms,
frame,
level,
source,
tag,
msg,
});
self.next_seq += 1;
}
pub fn get_recent(&self, n: usize) -> Vec<LogEvent> {
self.events.iter().rev().take(n).cloned().collect::<Vec<_>>().into_iter().rev().collect()
}
pub fn get_after(&self, seq: u64) -> Vec<LogEvent> {
self.events.iter().filter(|e| e.seq > seq).cloned().collect()
}
pub fn last_seq(&self) -> Option<u64> {
self.events.back().map(|e| e.seq)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ring_buffer_capacity() {
let mut service = LogService::new(3);
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");
service.log(130, 1, LogLevel::Info, LogSource::Pos, 0, "Log 4".to_string());
assert_eq!(service.events.len(), 3);
assert_eq!(service.events[0].msg, "Log 2");
assert_eq!(service.events[2].msg, "Log 4");
}
#[test]
fn test_get_recent() {
let mut service = LogService::new(10);
for i in 0..5 {
service.log(i as u64, 1, LogLevel::Info, LogSource::Pos, 0, format!("Log {}", i));
}
let recent = service.get_recent(2);
assert_eq!(recent.len(), 2);
assert_eq!(recent[0].msg, "Log 3");
assert_eq!(recent[1].msg, "Log 4");
}
#[test]
fn test_get_after() {
let mut service = LogService::new(10);
for i in 0..5 {
service.log(i as u64, 1, LogLevel::Info, LogSource::Pos, 0, format!("Log {}", i));
}
let after = service.get_after(2); // seqs are 0, 1, 2, 3, 4. Should return 3 and 4.
assert_eq!(after.len(), 2);
assert_eq!(after[0].msg, "Log 3");
assert_eq!(after[1].msg, "Log 4");
}
}

View File

@ -0,0 +1,8 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogSource {
Pos,
Hub,
Vm,
Fs,
App { app_id: u32 },
}

View File

@ -0,0 +1,9 @@
mod log_level;
mod log_source;
mod log_event;
mod log_service;
pub use log_level::LogLevel;
pub use log_source::LogSource;
pub use log_event::LogEvent;
pub use log_service::LogService;

View File

@ -0,0 +1,80 @@
use serde::{Deserialize, Serialize};
pub type HandleId = u32;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[allow(non_camel_case_types)]
pub enum BankType {
TILES,
SOUNDS,
// TILEMAPS,
// BLOBS,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AssetEntry {
pub asset_id: u32,
pub asset_name: String,
pub bank_type: BankType,
pub offset: u64,
pub size: u64,
pub decoded_size: u64,
pub codec: String, // e.g., "RAW"
pub metadata: serde_json::Value,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PreloadEntry {
pub asset_name: String,
pub slot: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum LoadStatus {
PENDING,
LOADING,
READY,
COMMITTED,
CANCELED,
ERROR,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BankStats {
pub total_bytes: usize,
pub used_bytes: usize,
pub free_bytes: usize,
pub inflight_bytes: usize,
pub slot_count: usize,
pub slots_occupied: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SlotStats {
pub asset_id: Option<u32>,
pub asset_name: Option<String>,
pub generation: u32,
pub resident_bytes: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SlotRef {
pub asset_type: BankType,
pub index: usize,
}
impl SlotRef {
pub fn gfx(index: usize) -> Self {
Self {
asset_type: BankType::TILES,
index,
}
}
pub fn audio(index: usize) -> Self {
Self {
asset_type: BankType::SOUNDS,
index,
}
}
}

View File

@ -0,0 +1,40 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum ButtonId {
Up = 0,
Down = 1,
Left = 2,
Right = 3,
A = 4,
B = 5,
X = 6,
Y = 7,
L = 8,
R = 9,
Start = 10,
Select = 11,
}
#[derive(Default, Clone, Copy, Debug)]
pub struct Button {
pub pressed: bool,
pub released: bool,
pub down: bool,
pub hold_frames: u32,
}
impl Button {
pub fn begin_frame(&mut self, is_down_now: bool) {
let was_down = self.down;
self.down = is_down_now.clone();
self.pressed = !was_down && self.down;
self.released = was_down && !self.down;
if self.down {
self.hold_frames = self.hold_frames.saturating_add(1);
} else {
self.hold_frames = 0;
}
}
}

View File

@ -0,0 +1,77 @@
use crate::model::asset::{AssetEntry, PreloadEntry};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum AppMode {
Game,
System,
}
#[derive(Debug, Clone)]
pub struct Cartridge {
pub app_id: u32,
pub title: String,
pub app_version: String,
pub app_mode: AppMode,
pub entrypoint: String,
pub program: Vec<u8>,
pub assets: Vec<u8>,
pub asset_table: Vec<AssetEntry>,
pub preload: Vec<PreloadEntry>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CartridgeDTO {
pub app_id: u32,
pub title: String,
pub app_version: String,
pub app_mode: AppMode,
pub entrypoint: String,
pub program: Vec<u8>,
pub assets: Vec<u8>,
#[serde(default)]
pub asset_table: Vec<AssetEntry>,
#[serde(default)]
pub preload: Vec<PreloadEntry>,
}
impl From<CartridgeDTO> for Cartridge {
fn from(dto: CartridgeDTO) -> Self {
Self {
app_id: dto.app_id,
title: dto.title,
app_version: dto.app_version,
app_mode: dto.app_mode,
entrypoint: dto.entrypoint,
program: dto.program,
assets: dto.assets,
asset_table: dto.asset_table,
preload: dto.preload,
}
}
}
#[derive(Debug)]
pub enum CartridgeError {
NotFound,
InvalidFormat,
InvalidManifest,
UnsupportedVersion,
MissingProgram,
IoError,
}
#[derive(Deserialize)]
pub struct CartridgeManifest {
pub magic: String,
pub cartridge_version: u32,
pub app_id: u32,
pub title: String,
pub app_version: String,
pub app_mode: AppMode,
pub entrypoint: String,
#[serde(default)]
pub asset_table: Vec<AssetEntry>,
#[serde(default)]
pub preload: Vec<PreloadEntry>,
}

View File

@ -0,0 +1,80 @@
use crate::model::cartridge::{Cartridge, CartridgeDTO, CartridgeError, CartridgeManifest};
use std::fs;
use std::path::Path;
pub struct CartridgeLoader;
impl CartridgeLoader {
pub fn load(path: impl AsRef<Path>) -> Result<Cartridge, CartridgeError> {
let path = path.as_ref();
if !path.exists() {
return Err(CartridgeError::NotFound);
}
if path.is_dir() {
DirectoryCartridgeLoader::load(path)
} else if path.extension().is_some_and(|ext| ext == "pmc") {
PackedCartridgeLoader::load(path)
} else {
Err(CartridgeError::InvalidFormat)
}
}
}
pub struct DirectoryCartridgeLoader;
impl DirectoryCartridgeLoader {
pub fn load(path: &Path) -> Result<Cartridge, CartridgeError> {
let manifest_path = path.join("manifest.json");
if !manifest_path.exists() {
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)?;
// Additional validation as per requirements
if manifest.magic != "PMTU" {
return Err(CartridgeError::InvalidManifest);
}
if manifest.cartridge_version != 1 {
return Err(CartridgeError::UnsupportedVersion);
}
let program_path = path.join("program.pbc");
if !program_path.exists() {
return Err(CartridgeError::MissingProgram);
}
let program = fs::read(program_path).map_err(|_| CartridgeError::IoError)?;
let assets_pa_path = path.join("assets.pa");
let assets = if assets_pa_path.exists() {
fs::read(assets_pa_path).map_err(|_| CartridgeError::IoError)?
} else {
Vec::new()
};
let dto = CartridgeDTO {
app_id: manifest.app_id,
title: manifest.title,
app_version: manifest.app_version,
app_mode: manifest.app_mode,
entrypoint: manifest.entrypoint,
program,
assets,
asset_table: manifest.asset_table,
preload: manifest.preload,
};
Ok(Cartridge::from(dto))
}
}
pub struct PackedCartridgeLoader;
impl PackedCartridgeLoader {
pub fn load(_path: &Path) -> Result<Cartridge, CartridgeError> {
Err(CartridgeError::InvalidFormat)
}
}

View File

@ -0,0 +1,76 @@
/// 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
/// skips during rendering.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Color(pub u16);
impl Color {
pub const BLACK: Self = Self::rgb(0, 0, 0); // 0x0000
pub const WHITE: Self = Self::rgb(255, 255, 255); // 0xFFFF
pub const RED: Self = Self::rgb(255, 0, 0); // 0xF800
pub const GREEN: Self = Self::rgb(0, 255, 0); // 0x07E0
pub const BLUE: Self = Self::rgb(0, 0, 255); // 0x001F
pub const YELLOW: Self = Self::rgb(255, 255, 0); // 0xFFE0
pub const ORANGE: Self = Self::rgb(255, 128, 0);
pub const INDIGO: Self = Self::rgb(75, 0, 130);
pub const GRAY: Self = Self::rgb(128, 128, 128);
pub const CYAN: Self = Self::rgb(0, 255, 255); // 0x07FF
pub const MAGENTA: Self = Self::rgb(255, 0, 255); // 0xF81F
pub const COLOR_KEY: Self = Self::MAGENTA;
pub const TRANSPARENT: Self = Self::MAGENTA;
/// Extracts channels in the native RGB565 range:
/// R: 0..31, G: 0..63, B: 0..31
#[inline(always)]
pub const fn unpack_to_native(px: u16) -> (u8, u8, u8) {
let r = ((px >> 11) & 0x1F) as u8;
let g = ((px >> 5) & 0x3F) as u8;
let b = (px & 0x1F) as u8;
(r, g, b)
}
/// Packs channels from the native RGB565 range into a pixel:
/// R: 0..31, G: 0..63, B: 0..31
#[inline(always)]
pub const fn pack_from_native(r: u8, g: u8, b: u8) -> u16 {
((r as u16 & 0x1F) << 11) | ((g as u16 & 0x3F) << 5) | (b as u16 & 0x1F)
}
/// Creates an RGB565 color from 8-bit components (0..255).
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
let r5 = (r as u16 >> 3) & 0x1F;
let g6 = (g as u16 >> 2) & 0x3F;
let b5 = (b as u16 >> 3) & 0x1F;
Self((r5 << 11) | (g6 << 5) | b5)
}
pub const fn gray_scale(c: u8) -> Self {
Self::rgb(c, c, c)
}
pub const fn from_raw(raw: u16) -> Self {
Self(raw)
}
pub const fn raw(self) -> u16 {
self.0
}
pub const fn hex(self) -> i32 {
let (r5, g6, b5) = Self::unpack_to_native(self.0);
let r8 = ((r5 as u32) << 3) | ((r5 as u32) >> 2);
let g8 = ((g6 as u32) << 2) | ((g6 as u32) >> 4);
let b8 = ((b5 as u32) << 3) | ((b5 as u32) >> 2);
let hex = r8 << 16 | g8 << 8 | b8;
hex as i32
}
}

View File

@ -0,0 +1,25 @@
mod asset;
mod color;
mod button;
mod tile;
mod tile_layer;
mod tile_bank;
mod sound_bank;
mod sprite;
mod sample;
mod cartridge;
mod cartridge_loader;
mod window;
pub use asset::{AssetEntry, BankStats, BankType, HandleId, LoadStatus, PreloadEntry, SlotRef, SlotStats};
pub use button::{Button, ButtonId};
pub use cartridge::{AppMode, Cartridge, CartridgeDTO, CartridgeError};
pub use cartridge_loader::{CartridgeLoader, DirectoryCartridgeLoader, PackedCartridgeLoader};
pub use color::Color;
pub use sample::Sample;
pub use sound_bank::SoundBank;
pub use sprite::Sprite;
pub use tile::Tile;
pub use tile_bank::{TileBank, TileSize};
pub use tile_layer::{HudTileLayer, ScrollableTileLayer, TileMap};
pub use window::{Rect, Window, WindowId};

View File

@ -0,0 +1,27 @@
pub struct Sample {
pub sample_rate: u32,
pub data: Vec<i16>,
pub loop_start: Option<u32>,
pub loop_end: Option<u32>,
}
impl Sample {
pub fn new(sample_rate: u32, data: Vec<i16>) -> Self {
Self {
sample_rate,
data,
loop_start: None,
loop_end: None,
}
}
pub fn with_loop(mut self, start: u32, end: u32) -> Self {
self.loop_start = Some(start);
self.loop_end = Some(end);
self
}
pub fn frames_len(&self) -> usize {
self.data.len()
}
}

View File

@ -0,0 +1,16 @@
use crate::model::Sample;
use std::sync::Arc;
/// A container for audio assets.
///
/// A SoundBank stores multiple audio samples that can be played by the
/// audio subsystem.
pub struct SoundBank {
pub samples: Vec<Arc<Sample>>,
}
impl SoundBank {
pub fn new(samples: Vec<Arc<Sample>>) -> Self {
Self { samples }
}
}

View File

@ -0,0 +1,13 @@
use crate::model::Tile;
#[derive(Clone, Copy, Debug, Default)]
pub struct Sprite {
pub tile: Tile,
pub x: i32,
pub y: i32,
pub bank_id: u8,
pub active: bool,
pub flip_x: bool,
pub flip_y: bool,
pub priority: u8,
}

View File

@ -0,0 +1,7 @@
#[derive(Clone, Copy, Debug, Default)]
pub struct Tile {
pub id: u16,
pub flip_x: bool,
pub flip_y: bool,
pub palette_id: u8,
}

View File

@ -0,0 +1,75 @@
use crate::model::Color;
/// Standard sizes for square tiles.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TileSize {
/// 8x8 pixels.
Size8 = 8,
/// 16x16 pixels.
Size16 = 16,
/// 32x32 pixels.
Size32 = 32,
}
/// 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
/// to render a set of tiles.
pub struct TileBank {
/// Dimension of each individual tile in the bank.
pub tile_size: TileSize,
/// Width of the full bank sheet in pixels.
pub width: usize,
/// Height of the full bank sheet in pixels.
pub height: usize,
/// Pixel data stored as 4-bit indices (packed into 8-bit values).
/// Index 0 is always reserved for transparency.
pub pixel_indices: Vec<u8>,
/// Table of 64 palettes, each containing 16 RGB565 colors, total of 1024 colors for a bank.
pub palettes: [[Color; 16]; 64],
}
impl TileBank {
/// Creates an empty tile bank with the specified dimensions.
pub fn new(tile_size: TileSize, width: usize, height: usize) -> Self {
Self {
tile_size,
width,
height,
pixel_indices: vec![0; width * height], // Index 0 = Transparent
palettes: [[Color::BLACK; 16]; 64],
}
}
/// Resolves a global tile ID and local pixel coordinates to a palette index.
/// tile_id: the tile index in the bank
/// local_x, local_y: the pixel position inside the tile (0 to tile_size-1)
pub fn get_pixel_index(&self, tile_id: u16, local_x: usize, local_y: usize) -> u8 {
let size = self.tile_size as usize;
let tiles_per_row = self.width / size;
let tile_x = (tile_id as usize % tiles_per_row) * size;
let tile_y = (tile_id as usize / tiles_per_row) * size;
let pixel_x = tile_x + local_x;
let pixel_y = tile_y + local_y;
if pixel_x < self.width && pixel_y < self.height {
self.pixel_indices[pixel_y * self.width + pixel_x]
} else {
0 // Default to transparent if out of bounds
}
}
/// 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.
// We use Magenta as the 'transparent' signal color during composition.
if pixel_index == 0 {
return Color::COLOR_KEY;
}
self.palettes[palette_id as usize][pixel_index as usize]
}
}

View File

@ -0,0 +1,109 @@
use crate::model::tile_bank::TileSize;
use crate::model::Tile;
use crate::model::TileSize::Size8;
pub struct TileMap {
pub width: usize,
pub height: usize,
pub tiles: Vec<Tile>,
}
impl TileMap {
fn create(width: usize, height: usize) -> Self {
Self {
width,
height,
tiles: vec![Tile::default(); width * height],
}
}
pub fn set_tile(&mut self, x: usize, y: usize, tile: Tile) {
if x < self.width && y < self.height {
self.tiles[y * self.width + x] = tile;
}
}
}
pub struct TileLayer {
pub bank_id: u8,
pub tile_size: TileSize,
pub map: TileMap,
}
impl TileLayer {
fn create(width: usize, height: usize, tile_size: TileSize) -> Self {
Self {
bank_id: 0,
tile_size,
map: TileMap::create(width, height),
}
}
}
impl std::ops::Deref for TileLayer {
type Target = TileMap;
fn deref(&self) -> &Self::Target {
&self.map
}
}
impl std::ops::DerefMut for TileLayer {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.map
}
}
pub struct ScrollableTileLayer {
pub layer: TileLayer,
pub scroll_x: i32,
pub scroll_y: i32,
}
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,
}
}
}
impl std::ops::Deref for ScrollableTileLayer {
type Target = TileLayer;
fn deref(&self) -> &Self::Target {
&self.layer
}
}
impl std::ops::DerefMut for ScrollableTileLayer {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.layer
}
}
pub struct HudTileLayer {
pub layer: TileLayer,
}
impl HudTileLayer {
pub fn new(width: usize, height: usize) -> Self {
Self {
layer: TileLayer::create(width, height, Size8),
}
}
}
impl std::ops::Deref for HudTileLayer {
type Target = TileLayer;
fn deref(&self) -> &Self::Target {
&self.layer
}
}
impl std::ops::DerefMut for HudTileLayer {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.layer
}
}

View File

@ -0,0 +1,21 @@
use crate::model::Color;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rect {
pub x: i32,
pub y: i32,
pub w: i32,
pub h: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WindowId(pub u32);
#[derive(Debug, Clone)]
pub struct Window {
pub id: WindowId,
pub viewport: Rect,
pub has_focus: bool,
pub title: String,
pub color: Color,
}

View File

@ -0,0 +1,123 @@
use crate::log::{LogLevel, LogService, LogSource};
#[derive(Debug, Clone, Copy, Default)]
pub struct TelemetryFrame {
pub frame_index: u64,
pub vm_steps: u32,
pub cycles_used: u64,
pub cycles_budget: u64,
pub syscalls: u32,
pub host_cpu_time_us: u64,
pub violations: u32,
// GFX Banks
pub gfx_used_bytes: usize,
pub gfx_inflight_bytes: usize,
pub gfx_slots_occupied: u32,
// Audio Banks
pub audio_used_bytes: usize,
pub audio_inflight_bytes: usize,
pub audio_slots_occupied: u32,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CertificationConfig {
pub enabled: bool,
pub cycles_budget_per_frame: Option<u64>,
pub max_syscalls_per_frame: Option<u32>,
pub max_host_cpu_us_per_frame: Option<u64>,
}
pub struct Certifier {
pub config: CertificationConfig,
}
impl Certifier {
pub fn new(config: CertificationConfig) -> Self {
Self { config }
}
pub fn evaluate(&self, telemetry: &TelemetryFrame, log_service: &mut LogService, ts_ms: u64) -> usize {
if !self.config.enabled {
return 0;
}
let mut violations = 0;
if let Some(budget) = self.config.cycles_budget_per_frame {
if telemetry.cycles_used > budget {
log_service.log(
ts_ms,
telemetry.frame_index,
LogLevel::Warn,
LogSource::Pos,
0xCA01,
format!("Cert: cycles_used exceeded budget ({} > {})", telemetry.cycles_used, budget),
);
violations += 1;
}
}
if let Some(limit) = self.config.max_syscalls_per_frame {
if telemetry.syscalls > limit {
log_service.log(
ts_ms,
telemetry.frame_index,
LogLevel::Warn,
LogSource::Pos,
0xCA02,
format!("Cert: syscalls per frame exceeded limit ({} > {})", telemetry.syscalls, limit),
);
violations += 1;
}
}
if let Some(limit) = self.config.max_host_cpu_us_per_frame {
if telemetry.host_cpu_time_us > limit {
log_service.log(
ts_ms,
telemetry.frame_index,
LogLevel::Warn,
LogSource::Pos,
0xCA03,
format!("Cert: host_cpu_time_us exceeded limit ({} > {})", telemetry.host_cpu_time_us, limit),
);
violations += 1;
}
}
violations
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::log::LogService;
#[test]
fn test_certifier_violations() {
let config = CertificationConfig {
enabled: true,
cycles_budget_per_frame: Some(100),
max_syscalls_per_frame: Some(5),
max_host_cpu_us_per_frame: Some(1000),
};
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"));
assert!(logs[1].msg.contains("syscalls"));
}
}

View File

@ -0,0 +1,151 @@
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
/// in a Float64) to ensure mathematical correctness.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Value {
/// 32-bit signed integer. Used for most loop counters and indices.
Int32(i32),
/// 64-bit signed integer. Used for large numbers and timestamps.
Int64(i64),
/// 64-bit double precision float. Used for physics and complex math.
Float(f64),
/// Boolean value (true/false).
Boolean(bool),
/// UTF-8 string. Strings are immutable and usually come from the Constant Pool.
String(String),
/// Bounded 16-bit-ish integer.
Bounded(u32),
/// A pointer to an object on the heap.
Gate(usize),
/// Represents the absence of a value (equivalent to `null` or `undefined`).
Null,
}
impl PartialEq for Value {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Value::Int32(a), Value::Int32(b)) => a == b,
(Value::Int64(a), Value::Int64(b)) => a == b,
(Value::Int32(a), Value::Int64(b)) => *a as i64 == *b,
(Value::Int64(a), Value::Int32(b)) => *a == *b as i64,
(Value::Float(a), Value::Float(b)) => a == b,
(Value::Int32(a), Value::Float(b)) => *a as f64 == *b,
(Value::Float(a), Value::Int32(b)) => *a == *b as f64,
(Value::Int64(a), Value::Float(b)) => *a as f64 == *b,
(Value::Float(a), Value::Int64(b)) => *a == *b as f64,
(Value::Boolean(a), Value::Boolean(b)) => a == b,
(Value::String(a), Value::String(b)) => a == b,
(Value::Bounded(a), Value::Bounded(b)) => a == b,
(Value::Gate(a), Value::Gate(b)) => a == b,
(Value::Null, Value::Null) => true,
_ => false,
}
}
}
impl PartialOrd for Value {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match (self, other) {
(Value::Int32(a), Value::Int32(b)) => a.partial_cmp(b),
(Value::Int64(a), Value::Int64(b)) => a.partial_cmp(b),
(Value::Int32(a), Value::Int64(b)) => (*a as i64).partial_cmp(b),
(Value::Int64(a), Value::Int32(b)) => a.partial_cmp(&(*b as i64)),
(Value::Float(a), Value::Float(b)) => a.partial_cmp(b),
(Value::Bounded(a), Value::Bounded(b)) => a.partial_cmp(b),
(Value::Int32(a), Value::Float(b)) => (*a as f64).partial_cmp(b),
(Value::Float(a), Value::Int32(b)) => a.partial_cmp(&(*b as f64)),
(Value::Int64(a), Value::Float(b)) => (*a as f64).partial_cmp(b),
(Value::Float(a), Value::Int64(b)) => a.partial_cmp(&(*b as f64)),
(Value::Boolean(a), Value::Boolean(b)) => a.partial_cmp(b),
(Value::String(a), Value::String(b)) => a.partial_cmp(b),
_ => None,
}
}
}
impl Value {
pub fn as_float(&self) -> Option<f64> {
match self {
Value::Int32(i) => Some(*i as f64),
Value::Int64(i) => Some(*i as f64),
Value::Float(f) => Some(*f),
Value::Bounded(b) => Some(*b as f64),
_ => None,
}
}
pub fn as_integer(&self) -> Option<i64> {
match self {
Value::Int32(i) => Some(*i as i64),
Value::Int64(i) => Some(*i),
Value::Float(f) => Some(*f as i64),
Value::Bounded(b) => Some(*b as i64),
_ => None,
}
}
pub fn to_string(&self) -> String {
match self {
Value::Int32(i) => i.to_string(),
Value::Int64(i) => i.to_string(),
Value::Float(f) => f.to_string(),
Value::Bounded(b) => format!("{}b", b),
Value::Boolean(b) => b.to_string(),
Value::String(s) => s.clone(),
Value::Gate(r) => format!("[Gate {}]", r),
Value::Null => "null".to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_value_equality() {
assert_eq!(Value::Int32(10), Value::Int32(10));
assert_eq!(Value::Int64(10), Value::Int64(10));
assert_eq!(Value::Int32(10), Value::Int64(10));
assert_eq!(Value::Int64(10), Value::Int32(10));
assert_eq!(Value::Float(10.5), Value::Float(10.5));
assert_eq!(Value::Int32(10), Value::Float(10.0));
assert_eq!(Value::Float(10.0), Value::Int32(10));
assert_eq!(Value::Int64(10), Value::Float(10.0));
assert_eq!(Value::Float(10.0), Value::Int64(10));
assert_ne!(Value::Int32(10), Value::Int32(11));
assert_ne!(Value::Int64(10), Value::Int64(11));
assert_ne!(Value::Int32(10), Value::Int64(11));
assert_ne!(Value::Int32(10), Value::Float(10.1));
assert_eq!(Value::Boolean(true), Value::Boolean(true));
assert_ne!(Value::Boolean(true), Value::Boolean(false));
assert_eq!(Value::String("oi".into()), Value::String("oi".into()));
assert_eq!(Value::Null, Value::Null);
}
#[test]
fn test_value_conversions() {
let v_int32 = Value::Int32(42);
assert_eq!(v_int32.as_float(), Some(42.0));
assert_eq!(v_int32.as_integer(), Some(42));
let v_int64 = Value::Int64(42);
assert_eq!(v_int64.as_float(), Some(42.0));
assert_eq!(v_int64.as_integer(), Some(42));
let v_float = Value::Float(42.7);
assert_eq!(v_float.as_float(), Some(42.7));
assert_eq!(v_float.as_integer(), Some(42));
let v_bool = Value::Boolean(true);
assert_eq!(v_bool.as_float(), None);
assert_eq!(v_bool.as_integer(), None);
}
}

View File

@ -7,4 +7,5 @@ license.workspace = true
[dependencies] [dependencies]
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149" serde_json = "1.0.149"
prometeu-bytecode = { path = "../prometeu-bytecode" } prometeu-bytecode = { path = "../prometeu-bytecode" }
prometeu-abi = { path = "../prometeu-abi" }

View File

@ -16,14 +16,17 @@
//! The "Source of Truth" for the console's behavior lives here. //! The "Source of Truth" for the console's behavior lives here.
pub mod hardware; pub mod hardware;
pub mod log;
pub mod virtual_machine; pub mod virtual_machine;
pub mod model;
pub mod firmware; pub mod firmware;
pub mod fs; pub mod fs;
pub mod telemetry;
pub mod debugger_protocol;
pub mod prometeu_os; pub mod prometeu_os;
mod prometeu_hub; mod prometeu_hub;
// Facade/reexports for ABI modules (temporary during PR-00.x)
pub use prometeu_abi as abi;
pub use prometeu_abi::model;
pub use prometeu_abi::log;
pub use prometeu_abi::telemetry;
pub use prometeu_abi::debugger_protocol;
pub use hardware::hardware::Hardware; pub use hardware::hardware::Hardware;

View File

@ -1,5 +1,4 @@
mod virtual_machine; mod virtual_machine;
mod value;
mod call_frame; mod call_frame;
mod scope_frame; mod scope_frame;
mod program; mod program;
@ -12,7 +11,7 @@ use crate::hardware::HardwareBridge;
pub use program::ProgramImage; pub use program::ProgramImage;
pub use prometeu_bytecode::abi::TrapInfo; pub use prometeu_bytecode::abi::TrapInfo;
pub use prometeu_bytecode::opcode::OpCode; pub use prometeu_bytecode::opcode::OpCode;
pub use value::Value; pub use prometeu_abi::virtual_machine::Value;
pub use verifier::VerifierError; pub use verifier::VerifierError;
pub use virtual_machine::{BudgetReport, LogicalFrameEndingReason, VirtualMachine}; pub use virtual_machine::{BudgetReport, LogicalFrameEndingReason, VirtualMachine};

View File

@ -1,7 +1,7 @@
use crate::hardware::HardwareBridge; use crate::hardware::HardwareBridge;
use crate::virtual_machine::call_frame::CallFrame; use crate::virtual_machine::call_frame::CallFrame;
use crate::virtual_machine::scope_frame::ScopeFrame; use crate::virtual_machine::scope_frame::ScopeFrame;
use crate::virtual_machine::value::Value; use crate::virtual_machine::Value;
use crate::virtual_machine::{NativeInterface, ProgramImage, VmInitError}; use crate::virtual_machine::{NativeInterface, ProgramImage, VmInitError};
use prometeu_bytecode::abi::{TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_OOB, TRAP_TYPE}; use prometeu_bytecode::abi::{TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_OOB, TRAP_TYPE};
use prometeu_bytecode::opcode::OpCode; use prometeu_bytecode::opcode::OpCode;

View File

@ -405,38 +405,6 @@ pub struct AnalysisDb {
--- ---
## PR-00.1 — Introduzir `prometeu-abi` (renomear/diluir `prometeu-core`)
**Branch:** `pr-00-1-prometeu-abi`
### Objetivo
Criar crate `prometeu-abi` contendo **somente tipos e contratos** (types+model+protocols), e manter `prometeu-core` temporariamente como facade/reexport para não quebrar consumidores.
### Passos prescritivos
1. Criar novo crate `crates/prometeu-abi/` com `src/lib.rs`.
2. Copiar para `prometeu-abi` (sem execução):
* `model/*`
* `debugger_protocol.rs`
* `telemetry.rs`
* `log.rs` (somente tipos/config; se tiver runtime logger, deixar no core por enquanto)
* **traits/contratos** usados por outros crates (ex.: IDs, enums de eventos)
3. Atualizar dependências dos crates consumidores para apontar para `prometeu-abi` onde aplicável.
4. Manter `prometeu-core` compilando com:
* `pub use prometeu_abi as abi;`
* reexports temporários de tipos que mudaram de path.
### Critérios de aceite
* Workspace compila.
* Nenhum comportamento muda.
* `prometeu-core` ainda existe (temporário).
---
## PR-00.2 — Extrair `prometeu-vm` do `prometeu-core` ## PR-00.2 — Extrair `prometeu-vm` do `prometeu-core`
**Branch:** `pr-00-2-prometeu-vm` **Branch:** `pr-00-2-prometeu-vm`