implements PLN-0160

This commit is contained in:
bQUARKz 2026-07-05 01:18:33 +01:00
parent 3df8227ebf
commit d6afa6ae6b
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
11 changed files with 215 additions and 2 deletions

10
Cargo.lock generated
View File

@ -1469,6 +1469,16 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pbxgen-dummy-boy"
version = "0.1.0"
dependencies = [
"anyhow",
"prometeu-bytecode",
"prometeu-hal",
"serde_json",
]
[[package]] [[package]]
name = "pbxgen-stress" name = "pbxgen-stress"
version = "0.1.0" version = "0.1.0"

View File

@ -8,6 +8,7 @@ members = [
"crates/console/prometeu-vm", "crates/console/prometeu-vm",
"crates/host/prometeu-host-desktop-winit", "crates/host/prometeu-host-desktop-winit",
"crates/tools/prometeu-cli", "crates/tools/prometeu-cli",
"crates/tools/pbxgen-dummy-boy",
"crates/tools/pbxgen-stress", "crates/tools/pbxgen-stress",
"crates/dev/prometeu-test-support", "crates/dev/prometeu-test-support",
"crates/dev/prometeu-layer-tests", "crates/dev/prometeu-layer-tests",

View File

@ -315,4 +315,22 @@ mod tests {
assert_eq!(library.diagnostics().len(), 1); assert_eq!(library.diagnostics().len(), 1);
assert_eq!(library.diagnostics()[0].reason, GameLibraryDiagnosticReason::RootNotDirectory); 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")
}));
}
} }

View File

@ -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"

View File

@ -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<u8> {
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<u8> {
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<u8> {
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"]));
}
}

View File

@ -0,0 +1,5 @@
use anyhow::Result;
fn main() -> Result<()> {
pbxgen_dummy_boy::generate()
}

View File

@ -1,5 +1,5 @@
{"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":40,"PLN":163,"LSN":53,"CLSN":1}} {"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-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-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"}]} {"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"}]}

View File

@ -2,7 +2,7 @@
id: PLN-0160 id: PLN-0160
ticket: system-os-cartridge-switch-orchestrator ticket: system-os-cartridge-switch-orchestrator
title: Add Dummy Boy Visual Switch Test Cartridge title: Add Dummy Boy Visual Switch Test Cartridge
status: open status: done
created: 2026-07-05 created: 2026-07-05
ref_decisions: [DEC-0039] ref_decisions: [DEC-0039]
tags: [runtime, os, lifecycle, game, cartridge, architecture] tags: [runtime, os, lifecycle, game, cartridge, architecture]

View File

@ -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.

View File

@ -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"]
}

Binary file not shown.