diff --git a/Cargo.lock b/Cargo.lock index e3fbe577..5bcd2b9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1469,6 +1469,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbxgen-dummy-boy" +version = "0.1.0" +dependencies = [ + "anyhow", + "prometeu-bytecode", + "prometeu-hal", + "serde_json", +] + [[package]] name = "pbxgen-stress" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index cc8e5d61..278dd394 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "crates/console/prometeu-vm", "crates/host/prometeu-host-desktop-winit", "crates/tools/prometeu-cli", + "crates/tools/pbxgen-dummy-boy", "crates/tools/pbxgen-stress", "crates/dev/prometeu-test-support", "crates/dev/prometeu-layer-tests", diff --git a/crates/console/prometeu-system/src/services/game_library.rs b/crates/console/prometeu-system/src/services/game_library.rs index 9d357203..1989e29e 100644 --- a/crates/console/prometeu-system/src/services/game_library.rs +++ b/crates/console/prometeu-system/src/services/game_library.rs @@ -315,4 +315,22 @@ mod tests { assert_eq!(library.diagnostics().len(), 1); assert_eq!(library.diagnostics()[0].reason, GameLibraryDiagnosticReason::RootNotDirectory); } + + #[test] + fn repository_test_cartridges_include_stress_and_dummy_boy() { + let games_root = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../..").join("test-cartridges"); + + let library = discover_games_root(games_root); + let entries = library.entries(); + + assert!(entries.iter().any(|entry| { + entry.app_id == 1 + && entry.title == "Stress Console" + && entry.path.ends_with("stress-console") + })); + assert!(entries.iter().any(|entry| { + entry.app_id == 2 && entry.title == "Dummy Boy" && entry.path.ends_with("dummy-boy") + })); + } } diff --git a/crates/tools/pbxgen-dummy-boy/Cargo.toml b/crates/tools/pbxgen-dummy-boy/Cargo.toml new file mode 100644 index 00000000..7d64f7aa --- /dev/null +++ b/crates/tools/pbxgen-dummy-boy/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "pbxgen-dummy-boy" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +prometeu-bytecode = { path = "../../console/prometeu-bytecode" } +prometeu-hal = { path = "../../console/prometeu-hal" } + +[dev-dependencies] +serde_json = "1" diff --git a/crates/tools/pbxgen-dummy-boy/src/lib.rs b/crates/tools/pbxgen-dummy-boy/src/lib.rs new file mode 100644 index 00000000..4cccf568 --- /dev/null +++ b/crates/tools/pbxgen-dummy-boy/src/lib.rs @@ -0,0 +1,147 @@ +use anyhow::Result; +use prometeu_bytecode::assembler::assemble; +use prometeu_bytecode::model::{BytecodeModule, DebugInfo, Export, FunctionMeta, SyscallDecl}; +use prometeu_hal::color::Color; +use std::fs; +use std::path::PathBuf; + +const APP_ID: u32 = 2; +const TITLE: &str = "Dummy Boy"; +const APP_VERSION: &str = "0.1.0"; + +fn asm(s: &str) -> Vec { + assemble(s).expect("assemble") +} + +pub fn generate() -> Result<()> { + let out_dir = cartridge_dir(); + fs::create_dir_all(&out_dir)?; + fs::write(out_dir.join("program.pbx"), build_program())?; + fs::write(out_dir.join("manifest.json"), manifest_json())?; + Ok(()) +} + +pub fn build_program() -> Vec { + let mut rom = Vec::new(); + + rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 1\nADD\nSET_GLOBAL 0")); + rom.extend(asm(&format!("PUSH_I64 {}\nHOSTCALL 0", Color::BLACK.raw()))); + + rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 3\nMOD\nPUSH_I32 0\nEQ")); + let jump_to_green_case_offset = rom.len() + 2; + rom.extend(asm("JMP_IF_FALSE 0")); + rom.extend(asm(&format!("PUSH_I64 {}\nSET_LOCAL 0", Color::RED.raw()))); + let jump_to_draw_after_red_offset = rom.len() + 2; + rom.extend(asm("JMP 0")); + + let green_case = rom.len() as u32; + rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 3\nMOD\nPUSH_I32 1\nEQ")); + let jump_to_blue_case_offset = rom.len() + 2; + rom.extend(asm("JMP_IF_FALSE 0")); + rom.extend(asm(&format!("PUSH_I64 {}\nSET_LOCAL 0", Color::GREEN.raw()))); + let jump_to_draw_after_green_offset = rom.len() + 2; + rom.extend(asm("JMP 0")); + + let blue_case = rom.len() as u32; + rom.extend(asm(&format!("PUSH_I64 {}\nSET_LOCAL 0", Color::BLUE.raw()))); + + let draw = rom.len() as u32; + rom.extend(asm(&format!( + "PUSH_I32 180\n\ + PUSH_I32 75\n\ + PUSH_I32 120\n\ + PUSH_I32 120\n\ + PUSH_I64 {}\n\ + GET_LOCAL 0\n\ + HOSTCALL 1\n\ + FRAME_SYNC\n\ + RET", + Color::WHITE.raw() + ))); + + patch_jump(&mut rom, jump_to_green_case_offset, green_case); + patch_jump(&mut rom, jump_to_draw_after_red_offset, draw); + patch_jump(&mut rom, jump_to_blue_case_offset, blue_case); + patch_jump(&mut rom, jump_to_draw_after_green_offset, draw); + + BytecodeModule { + version: 0, + const_pool: vec![], + functions: vec![FunctionMeta { + code_offset: 0, + code_len: rom.len() as u32, + param_slots: 0, + local_slots: 1, + return_slots: 0, + max_stack_slots: 8, + }], + code: rom, + debug_info: Some(DebugInfo { + pc_to_span: vec![], + function_names: vec![(0, "main".into())], + }), + exports: vec![Export { symbol: "main".into(), func_idx: 0 }], + syscalls: vec![ + SyscallDecl { + module: "gfx2d".into(), + name: "clear".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }, + SyscallDecl { + module: "gfx2d".into(), + name: "draw_square".into(), + version: 1, + arg_slots: 6, + ret_slots: 0, + }, + ], + } + .serialize() +} + +pub fn manifest_json() -> Vec { + format!( + "{{\n \"magic\": \"PMTU\",\n \"cartridge_version\": 1,\n \"app_id\": {APP_ID},\n \"title\": \"{TITLE}\",\n \"app_version\": \"{APP_VERSION}\",\n \"app_mode\": \"Game\",\n \"capabilities\": [\"gfx2d\"]\n}}\n" + ) + .into_bytes() +} + +fn cartridge_dir() -> PathBuf { + let mut out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + out_dir.pop(); + out_dir.pop(); + out_dir.pop(); + out_dir.push("test-cartridges"); + out_dir.push("dummy-boy"); + out_dir +} + +fn patch_jump(buf: &mut [u8], imm_offset: usize, target: u32) { + buf[imm_offset..imm_offset + 4].copy_from_slice(&target.to_le_bytes()); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generated_program_is_pbx_module_with_expected_syscalls() { + let bytes = build_program(); + + assert!(bytes.starts_with(b"PBS\0")); + assert!(bytes.len() > 64); + } + + #[test] + fn manifest_declares_distinct_game_identity() { + let manifest: serde_json::Value = + serde_json::from_slice(&manifest_json()).expect("manifest should parse"); + + assert_eq!(manifest["app_id"], APP_ID); + assert_eq!(manifest["title"], TITLE); + assert_eq!(manifest["app_mode"], "Game"); + assert_eq!(manifest["capabilities"], serde_json::json!(["gfx2d"])); + } +} diff --git a/crates/tools/pbxgen-dummy-boy/src/main.rs b/crates/tools/pbxgen-dummy-boy/src/main.rs new file mode 100644 index 00000000..954d6946 --- /dev/null +++ b/crates/tools/pbxgen-dummy-boy/src/main.rs @@ -0,0 +1,5 @@ +use anyhow::Result; + +fn main() -> Result<()> { + pbxgen_dummy_boy::generate() +} diff --git a/discussion/index.ndjson b/discussion/index.ndjson index f3184ac2..200fd52b 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,5 +1,5 @@ {"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":40,"PLN":163,"LSN":53,"CLSN":1}} -{"type":"discussion","id":"DSC-0043","status":"in_progress","ticket":"system-os-cartridge-switch-orchestrator","title":"SystemOS Cartridge Switch Orchestrator","created_at":"2026-07-03","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","game","cartridge","architecture"],"agendas":[{"id":"AGD-0044","file":"AGD-0044-systemos-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-05"}],"decisions":[{"id":"DEC-0039","file":"DEC-0039-systemos-game-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05","ref_agenda":"AGD-0044"}],"plans":[{"id":"PLN-0157","file":"PLN-0157-introduce-systemos-game-switch-target-contract.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0158","file":"PLN-0158-terminate-resident-game-before-switching-cartridges.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0159","file":"PLN-0159-recover-failed-game-switches-back-to-home.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0160","file":"PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0161","file":"PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0162","file":"PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0043","status":"in_progress","ticket":"system-os-cartridge-switch-orchestrator","title":"SystemOS Cartridge Switch Orchestrator","created_at":"2026-07-03","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","game","cartridge","architecture"],"agendas":[{"id":"AGD-0044","file":"AGD-0044-systemos-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-05"}],"decisions":[{"id":"DEC-0039","file":"DEC-0039-systemos-game-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05","ref_agenda":"AGD-0044"}],"plans":[{"id":"PLN-0157","file":"PLN-0157-introduce-systemos-game-switch-target-contract.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0158","file":"PLN-0158-terminate-resident-game-before-switching-cartridges.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0159","file":"PLN-0159-recover-failed-game-switches-back-to-home.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0160","file":"PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0161","file":"PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0162","file":"PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]}],"lessons":[]} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} {"type":"discussion","id":"DSC-0041","status":"done","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0052","file":"discussion/lessons/DSC-0041-foreground-stack-game-pause-shell-vm-backed/LSN-0052-foreground-ownership-and-vm-session-ownership-are-separate.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05"}]} diff --git a/discussion/workflow/plans/PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md b/discussion/workflow/plans/PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md index 34ad7d49..80b2ce06 100644 --- a/discussion/workflow/plans/PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md +++ b/discussion/workflow/plans/PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md @@ -2,7 +2,7 @@ id: PLN-0160 ticket: system-os-cartridge-switch-orchestrator title: Add Dummy Boy Visual Switch Test Cartridge -status: open +status: done created: 2026-07-05 ref_decisions: [DEC-0039] tags: [runtime, os, lifecycle, game, cartridge, architecture] diff --git a/test-cartridges/dummy-boy/README.md b/test-cartridges/dummy-boy/README.md new file mode 100644 index 00000000..4739363a --- /dev/null +++ b/test-cartridges/dummy-boy/README.md @@ -0,0 +1,11 @@ +# Dummy Boy + +Regenerate this cartridge with: + +```sh +cargo run -p pbxgen-dummy-boy +``` + +Dummy Boy is a minimal visual Game cartridge used by runtime switch tests. It +clears the frame and draws a centered square whose fill color cycles +deterministically across frames. diff --git a/test-cartridges/dummy-boy/manifest.json b/test-cartridges/dummy-boy/manifest.json new file mode 100644 index 00000000..936959f4 --- /dev/null +++ b/test-cartridges/dummy-boy/manifest.json @@ -0,0 +1,9 @@ +{ + "magic": "PMTU", + "cartridge_version": 1, + "app_id": 2, + "title": "Dummy Boy", + "app_version": "0.1.0", + "app_mode": "Game", + "capabilities": ["gfx2d"] +} diff --git a/test-cartridges/dummy-boy/program.pbx b/test-cartridges/dummy-boy/program.pbx new file mode 100644 index 00000000..ee2844a8 Binary files /dev/null and b/test-cartridges/dummy-boy/program.pbx differ