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",
]
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "jni"
version = "0.21.1"
@ -1467,6 +1473,10 @@ checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
[[package]]
name = "prometeu-core"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "quick-xml"
@ -1671,6 +1681,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
@ -1693,6 +1704,19 @@ dependencies = [
"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]]
name = "shlex"
version = "1.3.0"
@ -2661,3 +2685,9 @@ dependencies = [
"quote",
"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 mut fs_root = 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() {
if args[i] == "--fs-root" && i + 1 < args.len() {
fs_root = Some(args[i + 1].clone());
i += 1;
} else if args[i] == "--cap" && i + 1 < args.len() {
cap_config = load_cap_config(&args[i + 1]);
i += 1;
match args[i].as_str() {
"run" => {
if i + 1 < args.len() {
cartridge_path = Some(args[i + 1].clone());
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;
}
@ -53,6 +68,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let event_loop = EventLoop::new()?;
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)?;
Ok(())

View File

@ -17,6 +17,7 @@ use winit::event_loop::{ActiveEventLoop, ControlFlow};
use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::{Window, WindowAttributes, WindowId};
use prometeu_core::model::{CartridgeError, CartridgeLoader};
use prometeu_core::telemetry::CertificationConfig;
pub struct PrometeuRunner {
@ -50,6 +51,12 @@ pub struct 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 {
let target_fps = 60;

View File

@ -3,4 +3,6 @@ name = "prometeu-core"
version = "0.1.0"
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 {
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);
}
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(
self.cartridge.header.title.clone(),
self.cartridge.title.clone(),
Rect { x: 40, y: 20, w: 240, h: 140 },
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 {
Game,
System,
}
#[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 header: AppHeader,
pub program: Program,
pub app_id: u32,
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 {
pub fn new(header: AppHeader, program: Program) -> Self {
Self { header, program }
}
#[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,
}

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 sample;
mod cartridge;
mod cartridge_loader;
mod window;
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 sample::Sample;
pub use sprite::Sprite;

View File

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

View File

@ -45,9 +45,19 @@ impl VirtualMachine {
}
}
pub fn initialize(&mut self, program: Program) {
self.program = program;
self.pc = 0;
pub fn initialize(&mut self, program_bytes: Vec<u8>, entrypoint: &str) {
// Por enquanto, tratamos os bytes como a ROM diretamente.
// 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.call_stack.clear();
self.globals.clear();