feat: implement cartridge v1 loader and contract

This commit is contained in:
bQUARKz 2026-01-17 10:27:04 +00:00 committed by Nilton Constantino
parent cbfc5a1ead
commit 7023f81389
No known key found for this signature in database
12 changed files with 226 additions and 65 deletions

30
Cargo.lock generated
View File

@ -746,6 +746,12 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]] [[package]]
name = "jni" name = "jni"
version = "0.21.1" version = "0.21.1"
@ -1467,6 +1473,10 @@ checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
[[package]] [[package]]
name = "prometeu-core" name = "prometeu-core"
version = "0.1.0" version = "0.1.0"
dependencies = [
"serde",
"serde_json",
]
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
@ -1671,6 +1681,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [ dependencies = [
"serde_core", "serde_core",
"serde_derive",
] ]
[[package]] [[package]]
@ -1693,6 +1704,19 @@ dependencies = [
"syn 2.0.114", "syn 2.0.114",
] ]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@ -2661,3 +2685,9 @@ dependencies = [
"quote", "quote",
"syn 2.0.114", "syn 2.0.114",
] ]
[[package]]
name = "zmij"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea"

9
cart-test/manifest.json Normal file
View File

@ -0,0 +1,9 @@
{
"magic": "PMTU",
"cartridge_version": 1,
"app_id": 1234,
"title": "Cart de Teste",
"app_version": "1.0.0",
"app_mode": "Invalid",
"entrypoint": "0"
}

BIN
cart-test/program.pbc Normal file

Binary file not shown.

View File

@ -37,15 +37,30 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect(); let args: Vec<String> = std::env::args().collect();
let mut fs_root = None; let mut fs_root = None;
let mut cap_config = None; let mut cap_config = None;
let mut cartridge_path = None;
let mut i = 0; let mut i = 1; // Pula o nome do executável
while i < args.len() { while i < args.len() {
if args[i] == "--fs-root" && i + 1 < args.len() { match args[i].as_str() {
fs_root = Some(args[i + 1].clone()); "run" => {
i += 1; if i + 1 < args.len() {
} else if args[i] == "--cap" && i + 1 < args.len() { cartridge_path = Some(args[i + 1].clone());
cap_config = load_cap_config(&args[i + 1]); i += 1;
i += 1; }
}
"--fs-root" => {
if i + 1 < args.len() {
fs_root = Some(args[i + 1].clone());
i += 1;
}
}
"--cap" => {
if i + 1 < args.len() {
cap_config = load_cap_config(&args[i + 1]);
i += 1;
}
}
_ => {}
} }
i += 1; i += 1;
} }
@ -53,6 +68,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let event_loop = EventLoop::new()?; let event_loop = EventLoop::new()?;
let mut runner = PrometeuRunner::new(fs_root, cap_config); let mut runner = PrometeuRunner::new(fs_root, cap_config);
if let Some(path) = cartridge_path {
match runner.load_cartridge(&path) {
Ok(_) => println!("Cartridge loaded: {}", path),
Err(e) => {
eprintln!("Failed to load cartridge: {:?}", e);
return Ok(());
}
}
}
event_loop.run_app(&mut runner)?; event_loop.run_app(&mut runner)?;
Ok(()) Ok(())

View File

@ -17,6 +17,7 @@ use winit::event_loop::{ActiveEventLoop, ControlFlow};
use winit::keyboard::{KeyCode, PhysicalKey}; use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::{Window, WindowAttributes, WindowId}; use winit::window::{Window, WindowAttributes, WindowId};
use prometeu_core::model::{CartridgeError, CartridgeLoader};
use prometeu_core::telemetry::CertificationConfig; use prometeu_core::telemetry::CertificationConfig;
pub struct PrometeuRunner { pub struct PrometeuRunner {
@ -50,6 +51,12 @@ pub struct PrometeuRunner {
} }
impl PrometeuRunner { impl PrometeuRunner {
pub(crate) fn load_cartridge(&mut self, path: &str) -> Result<(), CartridgeError> {
let cartridge = CartridgeLoader::load(path)?;
self.firmware.load_cartridge(cartridge);
Ok(())
}
pub(crate) fn new(fs_root: Option<String>, cap_config: Option<CertificationConfig>) -> Self { pub(crate) fn new(fs_root: Option<String>, cap_config: Option<CertificationConfig>) -> Self {
let target_fps = 60; let target_fps = 60;

View File

@ -3,4 +3,6 @@ name = "prometeu-core"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

View File

@ -10,14 +10,14 @@ pub struct LoadCartridgeStep {
impl LoadCartridgeStep { impl LoadCartridgeStep {
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
ctx.os.log(LogLevel::Info, LogSource::Pos, 0, format!("Loading cartridge: {}", self.cartridge.header.title)); ctx.os.log(LogLevel::Info, LogSource::Pos, 0, format!("Loading cartridge: {}", self.cartridge.title));
ctx.os.initialize_vm(ctx.vm, &self.cartridge); ctx.os.initialize_vm(ctx.vm, &self.cartridge);
} }
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> { pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
if self.cartridge.header.mode == AppMode::System { if self.cartridge.app_mode == AppMode::System {
let id = ctx.hub.window_manager.add_window( let id = ctx.hub.window_manager.add_window(
self.cartridge.header.title.clone(), self.cartridge.title.clone(),
Rect { x: 40, y: 20, w: 240, h: 140 }, Rect { x: 40, y: 20, w: 240, h: 140 },
Color::WHITE Color::WHITE
); );

View File

@ -1,29 +1,40 @@
use crate::virtual_machine::Program; use std::path::PathBuf;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum AppMode { pub enum AppMode {
Game, Game,
System, System,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AppHeader {
pub mode: AppMode,
pub app_id: String,
pub magic: u32,
pub version: u16,
pub title: String,
pub entrypoint: u32,
}
#[derive(Clone, Debug)]
pub struct Cartridge { pub struct Cartridge {
pub header: AppHeader, pub app_id: u32,
pub program: Program, pub title: String,
pub app_version: String,
pub app_mode: AppMode,
pub entrypoint: String,
pub program: Vec<u8>,
pub assets_path: Option<PathBuf>,
} }
impl Cartridge { #[derive(Debug)]
pub fn new(header: AppHeader, program: Program) -> Self { pub enum CartridgeError {
Self { header, program } 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,
} }

View File

@ -0,0 +1,77 @@
use std::fs;
use std::path::Path;
use crate::model::cartridge::{Cartridge, CartridgeError, CartridgeManifest};
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)?;
// Validação adicional conforme requisitos
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_path = path.join("assets");
let assets_path = if assets_path.exists() && assets_path.is_dir() {
Some(assets_path)
} else {
None
};
Ok(Cartridge {
app_id: manifest.app_id,
title: manifest.title,
app_version: manifest.app_version,
app_mode: manifest.app_mode,
entrypoint: manifest.entrypoint,
program,
assets_path,
})
}
}
pub struct PackedCartridgeLoader;
impl PackedCartridgeLoader {
pub fn load(_path: &Path) -> Result<Cartridge, CartridgeError> {
// Stub inicialmente, como solicitado
Err(CartridgeError::InvalidFormat)
}
}

View File

@ -6,10 +6,12 @@ mod tile_bank;
mod sprite; mod sprite;
mod sample; mod sample;
mod cartridge; mod cartridge;
mod cartridge_loader;
mod window; mod window;
pub use button::Button; pub use button::Button;
pub use cartridge::{AppHeader, AppMode, Cartridge}; pub use cartridge::{AppMode, Cartridge, CartridgeError};
pub use cartridge_loader::{CartridgeLoader, DirectoryCartridgeLoader, PackedCartridgeLoader};
pub use color::Color; pub use color::Color;
pub use sample::Sample; pub use sample::Sample;
pub use sprite::Sprite; pub use sprite::Sprite;

View File

@ -127,14 +127,10 @@ impl PrometeuOS {
/// Carrega um cartucho na PVM e reseta o estado de execução. /// Carrega um cartucho na PVM e reseta o estado de execução.
pub fn initialize_vm(&mut self, vm: &mut VirtualMachine, cartridge: &Cartridge) { pub fn initialize_vm(&mut self, vm: &mut VirtualMachine, cartridge: &Cartridge) {
vm.initialize(cartridge.program.clone()); vm.initialize(cartridge.program.clone(), &cartridge.entrypoint);
// Determina o app_id numérico (hash da string app_id) // Determina o app_id numérico
use std::collections::hash_map::DefaultHasher; self.current_app_id = cartridge.app_id;
use std::hash::{Hash, Hasher};
let mut s = DefaultHasher::new();
cartridge.header.app_id.hash(&mut s);
self.current_app_id = s.finish() as u32;
} }
/// Executa um tick do host (60Hz). /// Executa um tick do host (60Hz).
@ -308,8 +304,8 @@ impl PrometeuOS {
mod tests { mod tests {
use super::*; use super::*;
use crate::hardware::InputSignals; use crate::hardware::InputSignals;
use crate::model::{AppHeader, AppMode, Cartridge}; use crate::model::{AppMode, Cartridge};
use crate::virtual_machine::{Program, VirtualMachine}; use crate::virtual_machine::{Value, VirtualMachine};
use crate::Hardware; use crate::Hardware;
#[test] #[test]
@ -319,20 +315,15 @@ mod tests {
let mut hw = Hardware::new(); let mut hw = Hardware::new();
let signals = InputSignals::default(); let signals = InputSignals::default();
// JMP 0 (Loop infinito)
// OpCode::Jmp = 0x02, seguido por u32 0 (0x00, 0x00, 0x00, 0x00)
let rom = vec![0x02, 0x00, 0x00, 0x00, 0x00, 0x00]; let rom = vec![0x02, 0x00, 0x00, 0x00, 0x00, 0x00];
let program = Program::new(rom, vec![]);
let cartridge = Cartridge { let cartridge = Cartridge {
header: AppHeader { app_id: 1234,
mode: AppMode::Game, title: "test".to_string(),
app_id: "test".to_string(), app_version: "1.0.0".to_string(),
magic: 0, app_mode: AppMode::Game,
version: 1, entrypoint: "0".to_string(),
title: "test".to_string(), program: rom,
entrypoint: 0, assets_path: None,
},
program,
}; };
os.initialize_vm(&mut vm, &cartridge); os.initialize_vm(&mut vm, &cartridge);
@ -362,21 +353,17 @@ mod tests {
// FrameSync (0x80) // FrameSync (0x80)
// JMP 0 // JMP 0
let rom = vec![ let rom = vec![
0x10, 0x00, 0x00, 0x00, 0x00, 0x00, // PUSH const 0 (2 bytes opcode + 4 bytes u32)
0x80, 0x00, // FrameSync (2 bytes opcode) 0x80, 0x00, // FrameSync (2 bytes opcode)
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, // JMP 0 (2 bytes opcode + 4 bytes u32) 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, // JMP 0 (2 bytes opcode + 4 bytes u32)
]; ];
let program = Program::new(rom, vec![Value::Integer(0)]);
let cartridge = Cartridge { let cartridge = Cartridge {
header: AppHeader { app_id: 1234,
mode: AppMode::Game, title: "test".to_string(),
app_id: "test".to_string(), app_version: "1.0.0".to_string(),
magic: 0, app_mode: AppMode::Game,
version: 1, entrypoint: "0".to_string(),
title: "test".to_string(), program: rom,
entrypoint: 0, assets_path: None,
},
program,
}; };
os.initialize_vm(&mut vm, &cartridge); os.initialize_vm(&mut vm, &cartridge);

View File

@ -45,9 +45,19 @@ impl VirtualMachine {
} }
} }
pub fn initialize(&mut self, program: Program) { pub fn initialize(&mut self, program_bytes: Vec<u8>, entrypoint: &str) {
self.program = program; // Por enquanto, tratamos os bytes como a ROM diretamente.
self.pc = 0; // TODO: Implementar parser de .pbc para extrair constant_pool e rom reais.
self.program = Program::new(program_bytes, vec![]);
// Se o entrypoint for numérico, podemos tentar usá-lo como PC inicial.
// Se não, por enquanto ignoramos ou começamos do 0.
if let Ok(addr) = entrypoint.parse::<usize>() {
self.pc = addr;
} else {
self.pc = 0;
}
self.operand_stack.clear(); self.operand_stack.clear();
self.call_stack.clear(); self.call_stack.clear();
self.globals.clear(); self.globals.clear();