Compare commits
No commits in common. "de53eb5ad72e7ff57db3d0e9b94cafb20d60a6be" and "b30ff73e3d33cfa600310f9d10f3a1289e214c4a" have entirely different histories.
de53eb5ad7
...
b30ff73e3d
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -1469,16 +1469,6 @@ 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"
|
||||
|
||||
@ -8,7 +8,6 @@ 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",
|
||||
|
||||
@ -1858,9 +1858,6 @@ impl AssetManager {
|
||||
self.glyph_slot_index.clear();
|
||||
self.sound_slots.write().unwrap().fill(None);
|
||||
self.scene_slots.write().unwrap().fill(None);
|
||||
self.gfx_installer.clear_glyph_banks();
|
||||
self.sound_installer.clear_sound_banks();
|
||||
self.scene_installer.clear_scene_banks();
|
||||
}
|
||||
|
||||
pub fn glyph_asset_slot_index(&self) -> GlyphAssetSlotIndex {
|
||||
@ -2671,51 +2668,6 @@ mod tests {
|
||||
assert_eq!(am.slot_info(SlotRef::scene(4)).asset_id, Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shutdown_clears_physical_memory_bank_slots() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
|
||||
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
||||
let scene_installer = Arc::clone(&banks) as Arc<dyn SceneBankPoolInstaller>;
|
||||
|
||||
let glyph_data = test_glyph_asset_data();
|
||||
let scene = test_scene();
|
||||
let scene_data = encode_scene_payload(&scene);
|
||||
let glyph_entry = test_glyph_asset_entry("preload_glyphs", 16, 16);
|
||||
let scene_entry = test_scene_asset_entry("preload_scene", &scene_data, &scene);
|
||||
|
||||
let mut payload = Vec::new();
|
||||
payload.extend_from_slice(&glyph_data);
|
||||
payload.extend_from_slice(&scene_data);
|
||||
let mut shifted_scene_entry = scene_entry;
|
||||
shifted_scene_entry.offset = glyph_data.len() as u64;
|
||||
|
||||
let am = AssetManager::new(
|
||||
vec![],
|
||||
AssetsPayloadSource::empty(),
|
||||
gfx_installer,
|
||||
sound_installer,
|
||||
scene_installer,
|
||||
);
|
||||
am.initialize_for_cartridge(
|
||||
vec![glyph_entry, shifted_scene_entry],
|
||||
vec![PreloadEntry { asset_id: 0, slot: 0 }, PreloadEntry { asset_id: 2, slot: 0 }],
|
||||
AssetsPayloadSource::from_bytes(payload),
|
||||
);
|
||||
|
||||
assert!(banks.glyph_bank_slot(0).is_some());
|
||||
assert!(banks.scene_bank_slot(0).is_some());
|
||||
assert_eq!(am.slot_info(SlotRef::gfx(0)).asset_id, Some(0));
|
||||
assert_eq!(am.slot_info(SlotRef::scene(0)).asset_id, Some(2));
|
||||
|
||||
am.shutdown();
|
||||
|
||||
assert!(banks.glyph_bank_slot(0).is_none());
|
||||
assert!(banks.scene_bank_slot(0).is_none());
|
||||
assert_eq!(am.slot_info(SlotRef::gfx(0)).asset_id, None);
|
||||
assert_eq!(am.slot_info(SlotRef::scene(0)).asset_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_returns_asset_not_found() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
|
||||
@ -24,8 +24,6 @@ pub trait GlyphBankPoolAccess: Send + Sync {
|
||||
pub trait GlyphBankPoolInstaller: Send + Sync {
|
||||
/// Atomically swaps the resident GlyphBank in the specified slot.
|
||||
fn install_glyph_bank(&self, slot: usize, bank: Arc<GlyphBank>);
|
||||
/// Clears all resident GlyphBank slots.
|
||||
fn clear_glyph_banks(&self);
|
||||
}
|
||||
|
||||
/// Non-generic interface for peripherals to access sound banks.
|
||||
@ -40,8 +38,6 @@ pub trait SoundBankPoolAccess: Send + Sync {
|
||||
pub trait SoundBankPoolInstaller: Send + Sync {
|
||||
/// Atomically swaps the resident SoundBank in the specified slot.
|
||||
fn install_sound_bank(&self, slot: usize, bank: Arc<SoundBank>);
|
||||
/// Clears all resident SoundBank slots.
|
||||
fn clear_sound_banks(&self);
|
||||
}
|
||||
|
||||
/// Non-generic interface for peripherals to access scene banks.
|
||||
@ -56,8 +52,6 @@ pub trait SceneBankPoolAccess: Send + Sync {
|
||||
pub trait SceneBankPoolInstaller: Send + Sync {
|
||||
/// Atomically swaps the resident SceneBank in the specified slot.
|
||||
fn install_scene_bank(&self, slot: usize, bank: Arc<SceneBank>);
|
||||
/// Clears all resident SceneBank slots.
|
||||
fn clear_scene_banks(&self);
|
||||
}
|
||||
|
||||
/// Centralized container for all hardware memory banks.
|
||||
@ -106,10 +100,6 @@ impl GlyphBankPoolInstaller for MemoryBanks {
|
||||
pool[slot] = Some(bank);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_glyph_banks(&self) {
|
||||
self.glyph_bank_pool.write().unwrap().fill(None);
|
||||
}
|
||||
}
|
||||
|
||||
impl SoundBankPoolAccess for MemoryBanks {
|
||||
@ -130,10 +120,6 @@ impl SoundBankPoolInstaller for MemoryBanks {
|
||||
pool[slot] = Some(bank);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_sound_banks(&self) {
|
||||
self.sound_bank_pool.write().unwrap().fill(None);
|
||||
}
|
||||
}
|
||||
|
||||
impl SceneBankPoolAccess for MemoryBanks {
|
||||
@ -154,10 +140,6 @@ impl SceneBankPoolInstaller for MemoryBanks {
|
||||
pool[slot] = Some(bank);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_scene_banks(&self) {
|
||||
self.scene_bank_pool.write().unwrap().fill(None);
|
||||
}
|
||||
}
|
||||
|
||||
impl HalRenderResourceAccess for MemoryBanks {
|
||||
|
||||
@ -250,24 +250,21 @@ impl Firmware {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::firmware::firmware_state::HubHomeStep;
|
||||
use crate::firmware::resident_game_termination::{self, ResidentGameTerminationOutcome};
|
||||
use prometeu_bytecode::assembler::assemble;
|
||||
use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl};
|
||||
use prometeu_drivers::{GlyphBankPoolAccess, SceneBankPoolAccess, TestPlatform};
|
||||
use prometeu_drivers::TestPlatform;
|
||||
use prometeu_hal::RuntimePlatform;
|
||||
use prometeu_hal::app_mode::AppMode;
|
||||
use prometeu_hal::asset::{BankType, SlotRef};
|
||||
use prometeu_hal::cartridge::AssetsPayloadSource;
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
use prometeu_hal::primitives::Rect;
|
||||
use prometeu_hal::syscalls::{Syscall, caps};
|
||||
use prometeu_system::process::ProcessState;
|
||||
use prometeu_system::task::{TaskId, TaskState};
|
||||
use prometeu_system::windows::WindowOwner;
|
||||
use prometeu_system::{
|
||||
CrashReport, ForegroundOwner, GameLifecycleEventKind, HUB_SUSPENDED_GAME_KILL_CONTROL,
|
||||
ResidentGameState, discover_games_root,
|
||||
CrashReport, ForegroundOwner, GameLifecycleEventKind, ResidentGameState,
|
||||
discover_games_root,
|
||||
};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
@ -302,18 +299,6 @@ mod tests {
|
||||
title: &str,
|
||||
app_version: &str,
|
||||
with_program: bool,
|
||||
) -> PathBuf {
|
||||
let program = with_program.then(halting_program);
|
||||
self.add_game_with_program(name, app_id, title, app_version, program)
|
||||
}
|
||||
|
||||
fn add_game_with_program(
|
||||
&self,
|
||||
name: &str,
|
||||
app_id: u32,
|
||||
title: &str,
|
||||
app_version: &str,
|
||||
program: Option<Vec<u8>>,
|
||||
) -> PathBuf {
|
||||
let cart = self.path.join(name);
|
||||
fs::create_dir_all(&cart).expect("test cartridge should be created");
|
||||
@ -329,8 +314,8 @@ mod tests {
|
||||
);
|
||||
fs::write(cart.join("manifest.json"), manifest)
|
||||
.expect("test manifest should be written");
|
||||
if let Some(program) = program {
|
||||
fs::write(cart.join("program.pbx"), program)
|
||||
if with_program {
|
||||
fs::write(cart.join("program.pbx"), halting_program())
|
||||
.expect("test program should be written");
|
||||
}
|
||||
cart
|
||||
@ -361,10 +346,6 @@ mod tests {
|
||||
.serialize()
|
||||
}
|
||||
|
||||
fn repository_test_cartridges_root() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../..").join("test-cartridges")
|
||||
}
|
||||
|
||||
fn invalid_game_cartridge() -> Cartridge {
|
||||
Cartridge {
|
||||
app_id: 7,
|
||||
@ -479,12 +460,6 @@ mod tests {
|
||||
windows.set_focus(id);
|
||||
}
|
||||
|
||||
fn front_buffer_pixel(platform: &TestPlatform, x: i32, y: i32) -> u32 {
|
||||
const FRAMEBUFFER_WIDTH: usize = 480;
|
||||
let index = x as usize + y as usize * FRAMEBUFFER_WIDTH;
|
||||
platform.local_hardware().gfx.front_buffer()[index]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_cartridge_transitions_to_app_crashes_when_vm_init_fails() {
|
||||
let mut firmware = Firmware::new(None);
|
||||
@ -832,433 +807,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repository_stress_to_dummy_boy_switch_is_end_to_end() {
|
||||
let library = discover_games_root(repository_test_cartridges_root());
|
||||
assert!(
|
||||
library
|
||||
.entries()
|
||||
.iter()
|
||||
.any(|entry| entry.app_id == 1 && entry.title == "Stress Console")
|
||||
);
|
||||
assert!(
|
||||
library.entries().iter().any(|entry| entry.app_id == 2 && entry.title == "Dummy Boy")
|
||||
);
|
||||
|
||||
let (mut firmware, mut platform) = hub_home_firmware();
|
||||
firmware.hub.set_game_library(library);
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
let stress_click =
|
||||
InputSignals { f_signal: true, x_pos: 72, y_pos: 118, ..Default::default() };
|
||||
firmware.tick(&stress_click, &mut platform);
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
let stress_task = match &firmware.state {
|
||||
FirmwareState::GameRunning(step) => step.task_id,
|
||||
other => panic!("expected Stress GameRunning state, got {:?}", other),
|
||||
};
|
||||
assert_eq!(firmware.os.vm().current_app_id(), 1);
|
||||
assert_eq!(firmware.os.vm().current_cartridge_title(), "Stress Console");
|
||||
assert_eq!(
|
||||
platform.assets().slot_info(SlotRef { asset_type: BankType::GLYPH, index: 0 }).asset_id,
|
||||
Some(100)
|
||||
);
|
||||
assert_eq!(
|
||||
platform.assets().slot_info(SlotRef { asset_type: BankType::SCENE, index: 0 }).asset_id,
|
||||
Some(1)
|
||||
);
|
||||
|
||||
firmware.request_home_from_host();
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
let hub_ownership = firmware.os.vm().active_render_ownership();
|
||||
assert_eq!(hub_ownership.app_mode, AppMode::Shell);
|
||||
assert_eq!(hub_ownership.app_id, 0);
|
||||
assert_eq!(
|
||||
platform.assets().slot_info(SlotRef { asset_type: BankType::GLYPH, index: 0 }).asset_id,
|
||||
Some(100)
|
||||
);
|
||||
assert_eq!(
|
||||
platform.assets().slot_info(SlotRef { asset_type: BankType::SCENE, index: 0 }).asset_id,
|
||||
Some(1)
|
||||
);
|
||||
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
let dummy_click =
|
||||
InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() };
|
||||
firmware.tick(&dummy_click, &mut platform);
|
||||
assert_eq!(
|
||||
platform.assets().slot_info(SlotRef { asset_type: BankType::GLYPH, index: 0 }).asset_id,
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
platform.assets().slot_info(SlotRef { asset_type: BankType::SCENE, index: 0 }).asset_id,
|
||||
None
|
||||
);
|
||||
assert!(platform.local_hardware().memory_banks.glyph_bank_slot(0).is_none());
|
||||
assert!(platform.local_hardware().memory_banks.scene_bank_slot(0).is_none());
|
||||
|
||||
assert_eq!(firmware.os.lifecycle().task_state(stress_task), Some(TaskState::Closed));
|
||||
assert_eq!(
|
||||
firmware.os.lifecycle().process_state_for_task(stress_task),
|
||||
Ok(ProcessState::Stopped)
|
||||
);
|
||||
assert_eq!(
|
||||
firmware.os.sessions().vm_session_for_task(stress_task).map(|session| session.task_id),
|
||||
None
|
||||
);
|
||||
|
||||
let dummy_task = firmware
|
||||
.os
|
||||
.lifecycle()
|
||||
.resident_game_task()
|
||||
.expect("Dummy Boy should be resident after switch load");
|
||||
assert_ne!(dummy_task, stress_task);
|
||||
assert_eq!(
|
||||
firmware.os.sessions().vm_session_for_task(dummy_task).map(|session| {
|
||||
(session.app_id, session.title.clone(), session.app_version.clone())
|
||||
}),
|
||||
Some((2, "Dummy Boy".to_string(), "0.1.0".to_string()))
|
||||
);
|
||||
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
match &firmware.state {
|
||||
FirmwareState::GameRunning(step) => assert_eq!(step.task_id, dummy_task),
|
||||
other => panic!("expected Dummy Boy GameRunning state, got {:?}", other),
|
||||
}
|
||||
assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Game(dummy_task));
|
||||
assert_eq!(firmware.os.vm().current_app_id(), 2);
|
||||
assert_eq!(firmware.os.vm().current_cartridge_title(), "Dummy Boy");
|
||||
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
let dummy_ownership = firmware.os.vm().active_render_ownership();
|
||||
assert_eq!(dummy_ownership.app_mode, AppMode::Game);
|
||||
assert_eq!(dummy_ownership.app_id, 2);
|
||||
|
||||
let front = platform.local_hardware().gfx.front_buffer();
|
||||
let center = 240 + 135 * 480;
|
||||
assert_eq!(front[center], Color::GREEN.raw());
|
||||
|
||||
firmware.request_home_from_host();
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
let hub_ownership = firmware.os.vm().active_render_ownership();
|
||||
assert_eq!(hub_ownership.app_mode, AppMode::Shell);
|
||||
assert_eq!(hub_ownership.app_id, 0);
|
||||
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
let stress_click =
|
||||
InputSignals { f_signal: true, x_pos: 72, y_pos: 118, ..Default::default() };
|
||||
firmware.tick(&stress_click, &mut platform);
|
||||
|
||||
assert_eq!(firmware.os.lifecycle().task_state(dummy_task), Some(TaskState::Closed));
|
||||
assert_eq!(
|
||||
firmware.os.lifecycle().process_state_for_task(dummy_task),
|
||||
Ok(ProcessState::Stopped)
|
||||
);
|
||||
assert_eq!(
|
||||
firmware.os.sessions().vm_session_for_task(dummy_task).map(|session| session.task_id),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
platform.assets().slot_info(SlotRef { asset_type: BankType::GLYPH, index: 0 }).asset_id,
|
||||
Some(100)
|
||||
);
|
||||
assert_eq!(
|
||||
platform.assets().slot_info(SlotRef { asset_type: BankType::SCENE, index: 0 }).asset_id,
|
||||
Some(1)
|
||||
);
|
||||
assert!(platform.local_hardware().memory_banks.glyph_bank_slot(0).is_some());
|
||||
assert!(platform.local_hardware().memory_banks.scene_bank_slot(0).is_some());
|
||||
|
||||
let second_stress_task = firmware
|
||||
.os
|
||||
.lifecycle()
|
||||
.resident_game_task()
|
||||
.expect("Stress should be resident after switching back");
|
||||
assert_ne!(second_stress_task, stress_task);
|
||||
assert_ne!(second_stress_task, dummy_task);
|
||||
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
match &firmware.state {
|
||||
FirmwareState::GameRunning(step) => assert_eq!(step.task_id, second_stress_task),
|
||||
other => panic!("expected second Stress GameRunning state, got {:?}", other),
|
||||
}
|
||||
assert_eq!(firmware.os.vm().current_app_id(), 1);
|
||||
assert_eq!(firmware.os.vm().current_cartridge_title(), "Stress Console");
|
||||
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
let stress_ownership = firmware.os.vm().active_render_ownership();
|
||||
assert_eq!(stress_ownership.app_mode, AppMode::Game);
|
||||
assert_eq!(stress_ownership.app_id, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resident_game_termination_noops_when_no_game_is_resident() {
|
||||
let (mut firmware, mut platform) = hub_home_firmware();
|
||||
let ownership_before = firmware.os.vm().active_render_ownership();
|
||||
|
||||
let outcome = resident_game_termination::terminate_resident_game(
|
||||
&mut firmware.os,
|
||||
&mut platform,
|
||||
true,
|
||||
)
|
||||
.expect("no resident game termination should not fail");
|
||||
|
||||
assert_eq!(outcome, ResidentGameTerminationOutcome::NoResidentGame);
|
||||
assert_eq!(firmware.os.vm().active_render_ownership(), ownership_before);
|
||||
assert_eq!(firmware.os.lifecycle().resident_game_task(), None);
|
||||
assert!(
|
||||
!firmware
|
||||
.os
|
||||
.recent_logs(16)
|
||||
.iter()
|
||||
.any(|event| event.msg.contains("Resident game killed from Hub"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resident_game_termination_closes_session_and_cleans_runtime_state() {
|
||||
let library = discover_games_root(repository_test_cartridges_root());
|
||||
let (mut firmware, mut platform) = hub_home_firmware();
|
||||
firmware.hub.set_game_library(library);
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
let stress_click =
|
||||
InputSignals { f_signal: true, x_pos: 72, y_pos: 118, ..Default::default() };
|
||||
firmware.tick(&stress_click, &mut platform);
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
let stress_task = match &firmware.state {
|
||||
FirmwareState::GameRunning(step) => step.task_id,
|
||||
other => panic!("expected Stress GameRunning state, got {:?}", other),
|
||||
};
|
||||
|
||||
firmware.request_home_from_host();
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
|
||||
assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(stress_task));
|
||||
assert!(firmware.game_lifecycle_audio_paused());
|
||||
assert_eq!(
|
||||
platform.assets().slot_info(SlotRef { asset_type: BankType::GLYPH, index: 0 }).asset_id,
|
||||
Some(100)
|
||||
);
|
||||
assert_eq!(
|
||||
platform.assets().slot_info(SlotRef { asset_type: BankType::SCENE, index: 0 }).asset_id,
|
||||
Some(1)
|
||||
);
|
||||
assert!(platform.local_hardware().memory_banks.glyph_bank_slot(0).is_some());
|
||||
assert!(platform.local_hardware().memory_banks.scene_bank_slot(0).is_some());
|
||||
|
||||
let outcome = resident_game_termination::terminate_resident_game(
|
||||
&mut firmware.os,
|
||||
&mut platform,
|
||||
true,
|
||||
)
|
||||
.expect("resident game termination should succeed");
|
||||
|
||||
assert_eq!(
|
||||
outcome,
|
||||
ResidentGameTerminationOutcome::Terminated {
|
||||
task_id: stress_task,
|
||||
title: "Stress Console".to_string()
|
||||
}
|
||||
);
|
||||
assert_eq!(firmware.os.lifecycle().resident_game_task(), None);
|
||||
assert_eq!(firmware.os.lifecycle().task_state(stress_task), Some(TaskState::Closed));
|
||||
assert_eq!(
|
||||
firmware.os.lifecycle().process_state_for_task(stress_task),
|
||||
Ok(ProcessState::Stopped)
|
||||
);
|
||||
assert_eq!(
|
||||
firmware.os.sessions().vm_session_for_task(stress_task).map(|session| session.task_id),
|
||||
None
|
||||
);
|
||||
assert!(!firmware.game_lifecycle_audio_paused());
|
||||
|
||||
let ownership = firmware.os.vm().active_render_ownership();
|
||||
assert_eq!(ownership.app_mode, AppMode::Shell);
|
||||
assert_eq!(ownership.app_id, 0);
|
||||
assert_eq!(platform.local_hardware().frame_composer.active_scene_id(), None);
|
||||
assert_eq!(
|
||||
platform.assets().slot_info(SlotRef { asset_type: BankType::GLYPH, index: 0 }).asset_id,
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
platform.assets().slot_info(SlotRef { asset_type: BankType::SCENE, index: 0 }).asset_id,
|
||||
None
|
||||
);
|
||||
assert!(platform.local_hardware().memory_banks.glyph_bank_slot(0).is_none());
|
||||
assert!(platform.local_hardware().memory_banks.scene_bank_slot(0).is_none());
|
||||
|
||||
assert!(
|
||||
firmware.os.recent_logs(32).iter().any(|event| {
|
||||
event.level == LogLevel::Info
|
||||
&& event.source == LogSource::Hub
|
||||
&& event.msg == "Resident game killed from Hub: Stress Console"
|
||||
}),
|
||||
"manual Hub kill should emit an INFO log"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hub_kill_control_click_terminates_resident_game_and_stays_home() {
|
||||
let library = discover_games_root(repository_test_cartridges_root());
|
||||
let (mut firmware, mut platform) = hub_home_firmware();
|
||||
firmware.hub.set_game_library(library);
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
let stress_click =
|
||||
InputSignals { f_signal: true, x_pos: 72, y_pos: 118, ..Default::default() };
|
||||
firmware.tick(&stress_click, &mut platform);
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
let stress_task = match &firmware.state {
|
||||
FirmwareState::GameRunning(step) => step.task_id,
|
||||
other => panic!("expected Stress GameRunning state, got {:?}", other),
|
||||
};
|
||||
|
||||
firmware.request_home_from_host();
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
|
||||
assert_eq!(firmware.os.lifecycle().resident_game_task(), Some(stress_task));
|
||||
|
||||
let kill_sample_x = HUB_SUSPENDED_GAME_KILL_CONTROL.x + 2;
|
||||
let kill_sample_y = HUB_SUSPENDED_GAME_KILL_CONTROL.y + 2;
|
||||
assert_eq!(
|
||||
front_buffer_pixel(&platform, kill_sample_x, kill_sample_y),
|
||||
Color::rgb(164, 32, 40).raw(),
|
||||
"Hub should render the suspended-game kill control before click"
|
||||
);
|
||||
|
||||
let kill_click = InputSignals {
|
||||
f_signal: true,
|
||||
x_pos: HUB_SUSPENDED_GAME_KILL_CONTROL.x,
|
||||
y_pos: HUB_SUSPENDED_GAME_KILL_CONTROL.y,
|
||||
..Default::default()
|
||||
};
|
||||
firmware.tick(&kill_click, &mut platform);
|
||||
|
||||
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
|
||||
assert_eq!(firmware.os.lifecycle().foreground_owner(), ForegroundOwner::Hub);
|
||||
assert_eq!(firmware.os.lifecycle().resident_game_task(), None);
|
||||
assert_eq!(firmware.os.lifecycle().resident_game(), None);
|
||||
assert_eq!(firmware.os.lifecycle().task_state(stress_task), Some(TaskState::Closed));
|
||||
assert_eq!(
|
||||
firmware.os.lifecycle().process_state_for_task(stress_task),
|
||||
Ok(ProcessState::Stopped)
|
||||
);
|
||||
assert_eq!(
|
||||
firmware.os.sessions().vm_session_for_task(stress_task).map(|session| session.task_id),
|
||||
None
|
||||
);
|
||||
assert!(!firmware.game_lifecycle_audio_paused());
|
||||
assert!(!firmware.resume_resident_game_from_home());
|
||||
|
||||
let ownership = firmware.os.vm().active_render_ownership();
|
||||
assert_eq!(ownership.app_mode, AppMode::Shell);
|
||||
assert_eq!(ownership.app_id, 0);
|
||||
assert_eq!(platform.local_hardware().frame_composer.active_scene_id(), None);
|
||||
assert_eq!(
|
||||
platform.assets().slot_info(SlotRef { asset_type: BankType::GLYPH, index: 0 }).asset_id,
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
platform.assets().slot_info(SlotRef { asset_type: BankType::SCENE, index: 0 }).asset_id,
|
||||
None
|
||||
);
|
||||
assert!(platform.local_hardware().memory_banks.glyph_bank_slot(0).is_none());
|
||||
assert!(platform.local_hardware().memory_banks.scene_bank_slot(0).is_none());
|
||||
assert!(
|
||||
firmware.os.recent_logs(32).iter().any(|event| {
|
||||
event.level == LogLevel::Info
|
||||
&& event.source == LogSource::Hub
|
||||
&& event.msg == "Resident game killed from Hub: Stress Console"
|
||||
}),
|
||||
"Hub kill control should use the canonical manual kill log"
|
||||
);
|
||||
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
assert_eq!(
|
||||
front_buffer_pixel(&platform, kill_sample_x, kill_sample_y),
|
||||
Color::rgb(12, 14, 22).raw(),
|
||||
"Hub should stop rendering the kill control after the game is gone"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_game_switch_returns_home_without_restoring_previous_game() {
|
||||
let root = TestGameRoot::new();
|
||||
root.add_game("stress", true);
|
||||
root.add_game_with_program("zzz-broken", 43, "Zzz Broken", "1.0.0", Some(vec![0, 0, 0, 0]));
|
||||
|
||||
let (mut firmware, mut platform) = hub_home_firmware();
|
||||
firmware.hub.set_game_library(discover_games_root(&root.path));
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
let stress_click =
|
||||
InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() };
|
||||
firmware.tick(&stress_click, &mut platform);
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
let first_game_task = match &firmware.state {
|
||||
FirmwareState::GameRunning(step) => step.task_id,
|
||||
other => panic!("expected first GameRunning state, got {:?}", other),
|
||||
};
|
||||
|
||||
firmware.request_home_from_host();
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
let broken_click =
|
||||
InputSignals { f_signal: true, x_pos: 72, y_pos: 118, ..Default::default() };
|
||||
firmware.tick(&broken_click, &mut platform);
|
||||
|
||||
assert_eq!(firmware.os.lifecycle().task_state(first_game_task), Some(TaskState::Closed));
|
||||
assert_eq!(
|
||||
firmware
|
||||
.os
|
||||
.sessions()
|
||||
.vm_session_for_task(first_game_task)
|
||||
.map(|session| session.task_id),
|
||||
None
|
||||
);
|
||||
let partial_task = firmware
|
||||
.os
|
||||
.lifecycle()
|
||||
.resident_game_task()
|
||||
.expect("failed target should be resident until recovery tick");
|
||||
assert_ne!(partial_task, first_game_task);
|
||||
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
|
||||
assert_eq!(firmware.os.lifecycle().resident_game_task(), None);
|
||||
assert_eq!(firmware.os.lifecycle().task_state(first_game_task), Some(TaskState::Closed));
|
||||
assert_eq!(firmware.os.lifecycle().task_state(partial_task), Some(TaskState::Closed));
|
||||
assert_eq!(
|
||||
firmware.os.lifecycle().process_state_for_task(partial_task),
|
||||
Ok(ProcessState::Stopped)
|
||||
);
|
||||
assert_eq!(
|
||||
firmware.os.sessions().vm_session_for_task(partial_task).map(|session| session.task_id),
|
||||
None
|
||||
);
|
||||
assert!(
|
||||
firmware
|
||||
.os
|
||||
.recent_logs(16)
|
||||
.iter()
|
||||
.any(|event| event.msg.contains("Failed to switch to game Zzz Broken app_id 43"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_games_root_candidate_is_omitted_from_home_launch_boundary() {
|
||||
let root = TestGameRoot::new();
|
||||
@ -1441,8 +989,6 @@ mod tests {
|
||||
};
|
||||
let tick_index_before_home = firmware.os.vm().tick_index();
|
||||
let render_ownership_before_home = firmware.os.vm().active_render_ownership();
|
||||
assert_eq!(render_ownership_before_home.app_mode, AppMode::Game);
|
||||
assert_eq!(render_ownership_before_home.app_id, 9);
|
||||
|
||||
firmware.request_home_from_host();
|
||||
|
||||
@ -1474,6 +1020,7 @@ mod tests {
|
||||
let render_ownership_after_home = firmware.os.vm().active_render_ownership();
|
||||
assert_eq!(render_ownership_after_home.app_mode, AppMode::Shell);
|
||||
assert_eq!(render_ownership_after_home.app_id, 0);
|
||||
assert!(render_ownership_after_home.epoch > render_ownership_before_home.epoch);
|
||||
assert!(firmware.os.lifecycle().pending_game_lifecycle_events().is_empty());
|
||||
assert_eq!(
|
||||
firmware.os.vm().delivered_game_lifecycle_events().last().map(|event| event.kind),
|
||||
@ -1502,8 +1049,6 @@ mod tests {
|
||||
firmware.request_home_from_host();
|
||||
firmware.tick(&signals, &mut platform);
|
||||
let hub_ownership = firmware.os.vm().active_render_ownership();
|
||||
assert_eq!(hub_ownership.app_mode, AppMode::Shell);
|
||||
assert_eq!(hub_ownership.app_id, 0);
|
||||
|
||||
assert!(firmware.resume_resident_game_from_home());
|
||||
assert!(matches!(firmware.state, FirmwareState::GameRunning(_)));
|
||||
@ -1516,6 +1061,7 @@ mod tests {
|
||||
let game_ownership = firmware.os.vm().active_render_ownership();
|
||||
assert_eq!(game_ownership.app_mode, AppMode::Game);
|
||||
assert_eq!(game_ownership.app_id, 9);
|
||||
assert!(game_ownership.epoch > hub_ownership.epoch);
|
||||
|
||||
let held_signals =
|
||||
InputSignals { up_signal: true, f_signal: true, x_pos: 72, y_pos: 90, ..signals };
|
||||
|
||||
@ -2,7 +2,6 @@ use crate::firmware::firmware_state::{
|
||||
AppCrashesStep, FirmwareState, LoadCartridgeStep, ShellRunningStep,
|
||||
};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use crate::firmware::resident_game_termination;
|
||||
use prometeu_hal::app_mode::AppMode;
|
||||
use prometeu_hal::cartridge_loader::CartridgeLoader;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
@ -70,11 +69,7 @@ impl HubHomeStep {
|
||||
return Some(FirmwareState::ResumeResidentGame);
|
||||
}
|
||||
|
||||
if let Err(error) = resident_game_termination::terminate_resident_game(
|
||||
ctx.os,
|
||||
ctx.platform,
|
||||
false,
|
||||
) {
|
||||
if let Err(error) = ctx.os.lifecycle().terminate_resident_game() {
|
||||
ctx.os.log(
|
||||
LogLevel::Error,
|
||||
LogSource::Hub,
|
||||
@ -88,9 +83,9 @@ impl HubHomeStep {
|
||||
}
|
||||
}
|
||||
|
||||
return Some(FirmwareState::LoadCartridge(
|
||||
LoadCartridgeStep::new_game_switch(cartridge),
|
||||
));
|
||||
return Some(FirmwareState::LoadCartridge(LoadCartridgeStep::new(
|
||||
cartridge,
|
||||
)));
|
||||
}
|
||||
Err(error) => {
|
||||
ctx.os.log(
|
||||
@ -106,18 +101,6 @@ impl HubHomeStep {
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(SystemProfileAction::KillResidentGame) => {
|
||||
if let Err(error) =
|
||||
resident_game_termination::terminate_resident_game(ctx.os, ctx.platform, true)
|
||||
{
|
||||
ctx.os.log(
|
||||
LogLevel::Error,
|
||||
LogSource::Hub,
|
||||
0,
|
||||
format!("Failed to kill resident game from Hub: {error:?}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
Some(SystemProfileAction::CloseShell) | None => {}
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
use crate::firmware::firmware_state::{
|
||||
AppCrashesStep, FirmwareState, GameRunningStep, HubHomeStep, ShellRunningStep,
|
||||
AppCrashesStep, FirmwareState, GameRunningStep, ShellRunningStep,
|
||||
};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use crate::firmware::resident_game_termination;
|
||||
use prometeu_hal::app_mode::AppMode;
|
||||
use prometeu_hal::cartridge::Cartridge;
|
||||
use prometeu_hal::color::Color;
|
||||
@ -17,28 +16,11 @@ pub struct LoadCartridgeStep {
|
||||
init_error: Option<CrashReport>,
|
||||
loaded_task_id: Option<prometeu_system::task::TaskId>,
|
||||
loaded_app_mode: Option<AppMode>,
|
||||
failure_recovery: LoadCartridgeFailureRecovery,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum LoadCartridgeFailureRecovery {
|
||||
AppCrash,
|
||||
ReturnToHub,
|
||||
}
|
||||
|
||||
impl LoadCartridgeStep {
|
||||
pub fn new(cartridge: Cartridge) -> Self {
|
||||
Self {
|
||||
cartridge,
|
||||
init_error: None,
|
||||
loaded_task_id: None,
|
||||
loaded_app_mode: None,
|
||||
failure_recovery: LoadCartridgeFailureRecovery::AppCrash,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_game_switch(cartridge: Cartridge) -> Self {
|
||||
Self { failure_recovery: LoadCartridgeFailureRecovery::ReturnToHub, ..Self::new(cartridge) }
|
||||
Self { cartridge, init_error: None, loaded_task_id: None, loaded_app_mode: None }
|
||||
}
|
||||
|
||||
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
|
||||
@ -49,8 +31,6 @@ impl LoadCartridgeStep {
|
||||
format!("Loading cartridge: {}", self.cartridge.title),
|
||||
);
|
||||
|
||||
ctx.platform.game2d_frame_composer().unbind_scene();
|
||||
|
||||
// Initialize Asset Manager
|
||||
ctx.platform.assets_mut().initialize_for_cartridge(
|
||||
self.cartridge.asset_table.clone(),
|
||||
@ -71,38 +51,6 @@ impl LoadCartridgeStep {
|
||||
|
||||
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
||||
if let Some(report) = self.init_error.take() {
|
||||
if self.failure_recovery == LoadCartridgeFailureRecovery::ReturnToHub {
|
||||
if self.cartridge.app_mode == AppMode::Game
|
||||
&& let Some(task_id) = ctx.os.lifecycle().resident_game_task()
|
||||
{
|
||||
let is_partial_target = ctx
|
||||
.os
|
||||
.sessions()
|
||||
.vm_session_for_task(task_id)
|
||||
.map(|session| session.app_id == self.cartridge.app_id)
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_partial_target {
|
||||
let _ = resident_game_termination::terminate_resident_game(
|
||||
ctx.os,
|
||||
ctx.platform,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.os.log(
|
||||
LogLevel::Error,
|
||||
LogSource::Hub,
|
||||
0,
|
||||
format!(
|
||||
"Failed to switch to game {} app_id {}; returning to Home: {:?}",
|
||||
self.cartridge.title, self.cartridge.app_id, report
|
||||
),
|
||||
);
|
||||
return Some(FirmwareState::HubHome(HubHomeStep));
|
||||
}
|
||||
|
||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
||||
}
|
||||
|
||||
|
||||
@ -12,7 +12,6 @@ pub(crate) mod firmware_step_reset;
|
||||
pub(crate) mod firmware_step_shell_running;
|
||||
pub(crate) mod firmware_step_splash_screen;
|
||||
mod prometeu_context;
|
||||
pub(crate) mod resident_game_termination;
|
||||
|
||||
pub use boot_target::BootTarget;
|
||||
pub use firmware::Firmware;
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
use prometeu_hal::RuntimePlatform;
|
||||
use prometeu_hal::app_mode::AppMode;
|
||||
use prometeu_hal::log::LogSource;
|
||||
use prometeu_system::task::TaskId;
|
||||
use prometeu_system::{LifecycleError, SystemOS};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum ResidentGameTerminationOutcome {
|
||||
NoResidentGame,
|
||||
Terminated { task_id: TaskId, title: String },
|
||||
}
|
||||
|
||||
pub(crate) fn terminate_resident_game(
|
||||
os: &mut SystemOS,
|
||||
platform: &mut dyn RuntimePlatform,
|
||||
log_manual_hub_kill: bool,
|
||||
) -> Result<ResidentGameTerminationOutcome, LifecycleError> {
|
||||
let Some(task_id) = os.lifecycle().resident_game_task() else {
|
||||
return Ok(ResidentGameTerminationOutcome::NoResidentGame);
|
||||
};
|
||||
|
||||
let title = os
|
||||
.sessions()
|
||||
.vm_session_for_task(task_id)
|
||||
.map(|session| session.title.clone())
|
||||
.unwrap_or_else(|| "<unknown>".to_string());
|
||||
|
||||
let terminated = os.lifecycle().terminate_resident_game()?;
|
||||
let Some(task_id) = terminated else {
|
||||
return Ok(ResidentGameTerminationOutcome::NoResidentGame);
|
||||
};
|
||||
|
||||
os.vm().transition_render_owner(AppMode::Shell, 0);
|
||||
platform.game2d_frame_composer().unbind_scene();
|
||||
platform.assets_mut().shutdown();
|
||||
|
||||
if log_manual_hub_kill {
|
||||
os.info(LogSource::Hub, format!("Resident game killed from Hub: {title}"));
|
||||
}
|
||||
|
||||
Ok(ResidentGameTerminationOutcome::Terminated { task_id, title })
|
||||
}
|
||||
@ -8,10 +8,7 @@ pub use os::{
|
||||
DEFAULT_GAME_PAUSE_BUDGET_TICKS, GameLifecycleEvent, GameLifecycleEventKind, LifecycleError,
|
||||
LifecycleOperation, SessionError, SystemOS,
|
||||
};
|
||||
pub use programs::{
|
||||
HUB_SUSPENDED_GAME_KILL_CONTROL, NativeShellApp, PrometeuHub, SystemProfileAction,
|
||||
SystemProfileUpdate,
|
||||
};
|
||||
pub use programs::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfileUpdate};
|
||||
pub use prometeu_hal::{RenderWorkerBackend, RenderWorkerFrameSink};
|
||||
pub use services::async_work::{
|
||||
AsyncWorkActiveJob, AsyncWorkCancelToken, AsyncWorkJobContext, AsyncWorkJobId,
|
||||
|
||||
@ -250,12 +250,6 @@ impl<'a> VmFacade<'a> {
|
||||
}
|
||||
|
||||
pub fn active_render_ownership(&self) -> RenderOwnership {
|
||||
if let Some(task_id) = self.foreground_session_task_id()
|
||||
&& let Some(session) = self.os.vm_sessions.for_task(task_id)
|
||||
{
|
||||
return session.runtime.render_manager.active_ownership();
|
||||
}
|
||||
|
||||
self.os.vm_runtime.render_manager.active_ownership()
|
||||
}
|
||||
|
||||
@ -323,8 +317,4 @@ impl<'a> VmFacade<'a> {
|
||||
.foreground_task()
|
||||
.or_else(|| self.os.foreground_stack.resident_game_task())
|
||||
}
|
||||
|
||||
fn foreground_session_task_id(&self) -> Option<TaskId> {
|
||||
self.os.task_manager.foreground_task()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
mod prometeu_hub;
|
||||
|
||||
pub use prometeu_hub::{
|
||||
HUB_SUSPENDED_GAME_KILL_CONTROL, NativeShellApp, PrometeuHub, SystemProfileAction,
|
||||
SystemProfileUpdate,
|
||||
};
|
||||
pub use prometeu_hub::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfileUpdate};
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
#[allow(clippy::module_inception)]
|
||||
mod prometeu_hub;
|
||||
|
||||
pub use prometeu_hub::{
|
||||
HUB_SUSPENDED_GAME_KILL_CONTROL, NativeShellApp, PrometeuHub, SystemProfileAction,
|
||||
SystemProfileUpdate,
|
||||
};
|
||||
pub use prometeu_hub::{NativeShellApp, PrometeuHub, SystemProfileAction, SystemProfileUpdate};
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
use crate::task::TaskId;
|
||||
use crate::{CrashReport, GameLibrary, GameSwitchTarget, SystemOS};
|
||||
use prometeu_hal::app_mode::AppMode;
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::primitives::Rect;
|
||||
use prometeu_hal::{
|
||||
@ -20,7 +19,6 @@ const GAME_ROW_W: i32 = 336;
|
||||
const GAME_ROW_H: i32 = 22;
|
||||
const GAME_ROW_GAP: i32 = 6;
|
||||
const MAX_HOME_GAME_ROWS: usize = 3;
|
||||
pub const HUB_SUSPENDED_GAME_KILL_CONTROL: Rect = Rect { x: 288, y: 230, w: 168, h: 24 };
|
||||
#[cfg(test)]
|
||||
const SHELL_FRAME: Rect = Rect { x: 40, y: 20, w: 400, h: 220 };
|
||||
|
||||
@ -33,8 +31,6 @@ const COLOR_TEXT: Color = Color::rgb(230, 236, 224);
|
||||
const COLOR_MUTED: Color = Color::rgb(91, 112, 128);
|
||||
const COLOR_BUTTON: Color = Color::rgb(45, 59, 82);
|
||||
const COLOR_BUTTON_ACTIVE: Color = Color::rgb(72, 91, 122);
|
||||
const COLOR_KILL: Color = Color::rgb(164, 32, 40);
|
||||
const COLOR_KILL_ACTIVE: Color = Color::rgb(206, 46, 54);
|
||||
|
||||
/// PrometeuHub: Launcher and system UI environment.
|
||||
pub struct PrometeuHub {
|
||||
@ -87,7 +83,6 @@ impl NativeShellApp {
|
||||
pub enum SystemProfileAction {
|
||||
LaunchNativeShell(NativeShellApp),
|
||||
LaunchGame { target: GameSwitchTarget },
|
||||
KillResidentGame,
|
||||
CloseShell,
|
||||
}
|
||||
|
||||
@ -136,14 +131,12 @@ impl PrometeuHub {
|
||||
}
|
||||
|
||||
let input = platform.input();
|
||||
let suspended_game = if in_shell { None } else { resident_game_indicator(os) };
|
||||
if let Some(action) = action_for_click(
|
||||
in_shell,
|
||||
input.touch().x(),
|
||||
input.touch().y(),
|
||||
input.touch().f().pressed,
|
||||
self.game_library.entries(),
|
||||
suspended_game.as_ref(),
|
||||
) {
|
||||
return Some(action);
|
||||
}
|
||||
@ -156,13 +149,7 @@ impl PrometeuHub {
|
||||
let pointer_y = platform.input().touch().y();
|
||||
|
||||
let packet = if os.windows().window_count() == 0 {
|
||||
let suspended_game = resident_game_indicator(os);
|
||||
render_home_packet(
|
||||
pointer_x,
|
||||
pointer_y,
|
||||
self.game_library.entries(),
|
||||
suspended_game.as_ref(),
|
||||
)
|
||||
render_home_packet(pointer_x, pointer_y, self.game_library.entries())
|
||||
} else {
|
||||
let mut commands = Vec::new();
|
||||
for window in os.windows().windows() {
|
||||
@ -245,7 +232,6 @@ fn action_for_click(
|
||||
y: i32,
|
||||
pressed: bool,
|
||||
game_entries: &[crate::GameLibraryEntry],
|
||||
suspended_game: Option<&SuspendedGameIndicator>,
|
||||
) -> Option<SystemProfileAction> {
|
||||
if !pressed {
|
||||
return None;
|
||||
@ -255,10 +241,6 @@ fn action_for_click(
|
||||
return rect_contains(CLOSE_BUTTON, x, y).then_some(SystemProfileAction::CloseShell);
|
||||
}
|
||||
|
||||
if suspended_game.is_some() && rect_contains(HUB_SUSPENDED_GAME_KILL_CONTROL, x, y) {
|
||||
return Some(SystemProfileAction::KillResidentGame);
|
||||
}
|
||||
|
||||
if let Some(entry) = game_entries
|
||||
.iter()
|
||||
.take(MAX_HOME_GAME_ROWS)
|
||||
@ -275,23 +257,6 @@ fn action_for_click(
|
||||
.map(|button| SystemProfileAction::LaunchNativeShell(button.app))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct SuspendedGameIndicator {
|
||||
title: String,
|
||||
}
|
||||
|
||||
fn resident_game_indicator(os: &mut SystemOS) -> Option<SuspendedGameIndicator> {
|
||||
let task_id = os.lifecycle().resident_game_task()?;
|
||||
let sessions = os.sessions();
|
||||
let session = sessions.vm_session_for_task(task_id)?;
|
||||
|
||||
if session.app_mode != AppMode::Game {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(SuspendedGameIndicator { title: session.title.clone() })
|
||||
}
|
||||
|
||||
fn game_row_rect(index: usize) -> Rect {
|
||||
Rect {
|
||||
x: GAME_ROW_X,
|
||||
@ -316,7 +281,6 @@ fn render_home_packet(
|
||||
pointer_x: i32,
|
||||
pointer_y: i32,
|
||||
game_entries: &[crate::GameLibraryEntry],
|
||||
suspended_game: Option<&SuspendedGameIndicator>,
|
||||
) -> ShellUiFramePacket {
|
||||
let mut commands = Vec::new();
|
||||
commands.push(GfxUiCommand::Clear { color: COLOR_BG });
|
||||
@ -356,11 +320,6 @@ fn render_home_packet(
|
||||
draw_button(&mut commands, button.rect, button.label, hovered);
|
||||
}
|
||||
|
||||
if let Some(suspended_game) = suspended_game {
|
||||
let hovered = rect_contains(HUB_SUSPENDED_GAME_KILL_CONTROL, pointer_x, pointer_y);
|
||||
draw_suspended_game_kill_control(&mut commands, suspended_game, hovered);
|
||||
}
|
||||
|
||||
ShellUiFramePacket::new(commands)
|
||||
}
|
||||
|
||||
@ -463,39 +422,6 @@ fn draw_game_row(commands: &mut Vec<GfxUiCommand>, rect: Rect, label: &str, hove
|
||||
draw_buffer_text(commands, rect.x + 8, rect.y + 7, label, COLOR_TEXT);
|
||||
}
|
||||
|
||||
fn draw_suspended_game_kill_control(
|
||||
commands: &mut Vec<GfxUiCommand>,
|
||||
suspended_game: &SuspendedGameIndicator,
|
||||
hovered: bool,
|
||||
) {
|
||||
let fill = if hovered { COLOR_KILL_ACTIVE } else { COLOR_KILL };
|
||||
let border = if hovered { COLOR_HILITE } else { COLOR_BORDER };
|
||||
fill_rect(commands, HUB_SUSPENDED_GAME_KILL_CONTROL, fill);
|
||||
draw_rect(commands, HUB_SUSPENDED_GAME_KILL_CONTROL, border);
|
||||
draw_buffer_text(
|
||||
commands,
|
||||
HUB_SUSPENDED_GAME_KILL_CONTROL.x + 8,
|
||||
HUB_SUSPENDED_GAME_KILL_CONTROL.y + 8,
|
||||
&fit_text_to_control(&suspended_game.title, HUB_SUSPENDED_GAME_KILL_CONTROL.w - 16),
|
||||
COLOR_TEXT,
|
||||
);
|
||||
}
|
||||
|
||||
fn fit_text_to_control(text: &str, max_width: i32) -> String {
|
||||
let max_chars = (max_width / 8).max(0) as usize;
|
||||
if text.chars().count() <= max_chars {
|
||||
return text.to_string();
|
||||
}
|
||||
|
||||
if max_chars <= 2 {
|
||||
return text.chars().take(max_chars).collect();
|
||||
}
|
||||
|
||||
let mut fitted: String = text.chars().take(max_chars - 2).collect();
|
||||
fitted.push_str("..");
|
||||
fitted
|
||||
}
|
||||
|
||||
fn draw_buffer_text(commands: &mut Vec<GfxUiCommand>, x: i32, y: i32, text: &str, color: Color) {
|
||||
commands.push(GfxUiCommand::DrawText { x, y, text: text.to_string(), color });
|
||||
}
|
||||
@ -536,14 +462,7 @@ mod tests {
|
||||
#[test]
|
||||
fn shell_a_button_click_emits_launch_action() {
|
||||
assert_eq!(
|
||||
action_for_click(
|
||||
false,
|
||||
HOME_BUTTONS[0].rect.x,
|
||||
HOME_BUTTONS[0].rect.y,
|
||||
true,
|
||||
&[],
|
||||
None
|
||||
),
|
||||
action_for_click(false, HOME_BUTTONS[0].rect.x, HOME_BUTTONS[0].rect.y, true, &[]),
|
||||
Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellA))
|
||||
);
|
||||
}
|
||||
@ -556,8 +475,7 @@ mod tests {
|
||||
SHELL_B_BUTTON.x + SHELL_B_BUTTON.w - 1,
|
||||
SHELL_B_BUTTON.y,
|
||||
true,
|
||||
&[],
|
||||
None
|
||||
&[]
|
||||
),
|
||||
Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellB))
|
||||
);
|
||||
@ -571,8 +489,7 @@ mod tests {
|
||||
SHELL_A_BUTTON.x + SHELL_A_BUTTON.w,
|
||||
SHELL_A_BUTTON.y,
|
||||
true,
|
||||
&[],
|
||||
None
|
||||
&[]
|
||||
),
|
||||
None
|
||||
);
|
||||
@ -582,8 +499,7 @@ mod tests {
|
||||
SHELL_A_BUTTON.x,
|
||||
SHELL_A_BUTTON.y + SHELL_A_BUTTON.h,
|
||||
true,
|
||||
&[],
|
||||
None
|
||||
&[]
|
||||
),
|
||||
None
|
||||
);
|
||||
@ -591,10 +507,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn held_pointer_does_not_emit_click_action() {
|
||||
assert_eq!(
|
||||
action_for_click(false, SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, false, &[], None),
|
||||
None
|
||||
);
|
||||
assert_eq!(action_for_click(false, SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, false, &[]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -622,14 +535,8 @@ mod tests {
|
||||
discovered_at: std::time::SystemTime::UNIX_EPOCH,
|
||||
};
|
||||
|
||||
let action = action_for_click(
|
||||
false,
|
||||
GAME_ROW_X,
|
||||
GAME_ROW_Y,
|
||||
true,
|
||||
std::slice::from_ref(&entry),
|
||||
None,
|
||||
);
|
||||
let action =
|
||||
action_for_click(false, GAME_ROW_X, GAME_ROW_Y, true, std::slice::from_ref(&entry));
|
||||
|
||||
assert_eq!(
|
||||
action,
|
||||
@ -647,10 +554,10 @@ mod tests {
|
||||
#[test]
|
||||
fn close_button_click_emits_close_only_in_shell() {
|
||||
assert_eq!(
|
||||
action_for_click(true, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true, &[], None),
|
||||
action_for_click(true, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true, &[]),
|
||||
Some(SystemProfileAction::CloseShell)
|
||||
);
|
||||
assert_eq!(action_for_click(false, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true, &[], None), None);
|
||||
assert_eq!(action_for_click(false, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true, &[]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -664,7 +571,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn home_render_produces_shell_ui_packet_commands() {
|
||||
let packet = render_home_packet(SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, &[], None);
|
||||
let packet = render_home_packet(SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, &[]);
|
||||
|
||||
assert!(matches!(packet.commands.first(), Some(GfxUiCommand::Clear { .. })));
|
||||
assert!(packet.commands.iter().any(|command| {
|
||||
@ -672,123 +579,6 @@ mod tests {
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resident_game_indicator_uses_only_resident_game_sessions() {
|
||||
let mut os = SystemOS::new(None);
|
||||
assert_eq!(resident_game_indicator(&mut os), None);
|
||||
|
||||
let shell = os.sessions().create_vm_shell_task(7, "VM Shell");
|
||||
assert_eq!(resident_game_indicator(&mut os), None);
|
||||
os.lifecycle().suspend_task(shell).expect("shell suspend should succeed");
|
||||
assert_eq!(resident_game_indicator(&mut os), None);
|
||||
|
||||
let game = os.sessions().create_vm_game_task(42, "Stress Console");
|
||||
os.lifecycle().suspend_task(game).expect("game suspend should succeed");
|
||||
|
||||
assert_eq!(
|
||||
resident_game_indicator(&mut os),
|
||||
Some(SuspendedGameIndicator { title: "Stress Console".to_string() })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn home_render_shows_suspended_game_kill_control_when_game_is_resident() {
|
||||
let suspended_game = SuspendedGameIndicator { title: "Stress Console".to_string() };
|
||||
let packet = render_home_packet(
|
||||
HUB_SUSPENDED_GAME_KILL_CONTROL.x,
|
||||
HUB_SUSPENDED_GAME_KILL_CONTROL.y,
|
||||
&[],
|
||||
Some(&suspended_game),
|
||||
);
|
||||
|
||||
assert!(packet.commands.iter().any(|command| {
|
||||
matches!(
|
||||
command,
|
||||
GfxUiCommand::FillRect { rect, color }
|
||||
if *rect == HUB_SUSPENDED_GAME_KILL_CONTROL && *color == COLOR_KILL_ACTIVE
|
||||
)
|
||||
}));
|
||||
assert!(packet.commands.iter().any(|command| {
|
||||
matches!(
|
||||
command,
|
||||
GfxUiCommand::DrawText { x, y, text, .. }
|
||||
if *x == HUB_SUSPENDED_GAME_KILL_CONTROL.x + 8
|
||||
&& *y == HUB_SUSPENDED_GAME_KILL_CONTROL.y + 8
|
||||
&& text == "Stress Console"
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn home_render_omits_suspended_game_kill_control_without_resident_game() {
|
||||
let packet = render_home_packet(
|
||||
HUB_SUSPENDED_GAME_KILL_CONTROL.x,
|
||||
HUB_SUSPENDED_GAME_KILL_CONTROL.y,
|
||||
&[],
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!packet.commands.iter().any(|command| {
|
||||
matches!(
|
||||
command,
|
||||
GfxUiCommand::FillRect { rect, color }
|
||||
if *rect == HUB_SUSPENDED_GAME_KILL_CONTROL
|
||||
&& (*color == COLOR_KILL || *color == COLOR_KILL_ACTIVE)
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suspended_game_kill_control_click_emits_kill_action() {
|
||||
let suspended_game = SuspendedGameIndicator { title: "Stress Console".to_string() };
|
||||
|
||||
assert_eq!(
|
||||
action_for_click(
|
||||
false,
|
||||
HUB_SUSPENDED_GAME_KILL_CONTROL.x,
|
||||
HUB_SUSPENDED_GAME_KILL_CONTROL.y,
|
||||
true,
|
||||
&[],
|
||||
Some(&suspended_game),
|
||||
),
|
||||
Some(SystemProfileAction::KillResidentGame)
|
||||
);
|
||||
assert_eq!(
|
||||
action_for_click(
|
||||
false,
|
||||
HUB_SUSPENDED_GAME_KILL_CONTROL.x,
|
||||
HUB_SUSPENDED_GAME_KILL_CONTROL.y,
|
||||
true,
|
||||
&[],
|
||||
None,
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suspended_game_kill_control_truncates_long_titles() {
|
||||
let long_title =
|
||||
SuspendedGameIndicator { title: "Extremely Long Resident Game Title".to_string() };
|
||||
let packet = render_home_packet(0, 0, &[], Some(&long_title));
|
||||
let label = packet
|
||||
.commands
|
||||
.iter()
|
||||
.find_map(|command| match command {
|
||||
GfxUiCommand::DrawText { x, y, text, .. }
|
||||
if *x == HUB_SUSPENDED_GAME_KILL_CONTROL.x + 8
|
||||
&& *y == HUB_SUSPENDED_GAME_KILL_CONTROL.y + 8 =>
|
||||
{
|
||||
Some(text)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.expect("kill control text should render");
|
||||
|
||||
assert!(label.ends_with(".."));
|
||||
assert!(label.chars().count() <= ((HUB_SUSPENDED_GAME_KILL_CONTROL.w - 16) / 8) as usize);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_close_button_sits_inside_shell_frame() {
|
||||
assert!(rect_contains(SHELL_FRAME, CLOSE_BUTTON.x, CLOSE_BUTTON.y));
|
||||
|
||||
@ -315,22 +315,4 @@ 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")
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,22 +26,16 @@ const IDLE_HOST_POLL_DT: Duration = Duration::from_millis(100);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PresentationState {
|
||||
latest_published_frame: Option<PresentationFrameMarker>,
|
||||
last_presented_frame: Option<PresentationFrameMarker>,
|
||||
latest_published_frame: u64,
|
||||
last_presented_frame: Option<u64>,
|
||||
host_invalidated: bool,
|
||||
redraw_requested: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct PresentationFrameMarker {
|
||||
ownership: RenderOwnership,
|
||||
frame_id: u64,
|
||||
}
|
||||
|
||||
impl Default for PresentationState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
latest_published_frame: None,
|
||||
latest_published_frame: 0,
|
||||
last_presented_frame: None,
|
||||
host_invalidated: true,
|
||||
redraw_requested: false,
|
||||
@ -50,10 +44,9 @@ impl Default for PresentationState {
|
||||
}
|
||||
|
||||
impl PresentationState {
|
||||
fn note_published_frame(&mut self, ownership: RenderOwnership, frame_id: u64) {
|
||||
let marker = PresentationFrameMarker { ownership, frame_id };
|
||||
if self.latest_published_frame != Some(marker) {
|
||||
self.latest_published_frame = Some(marker);
|
||||
fn note_published_frame(&mut self, frame_index: u64) {
|
||||
if frame_index > self.latest_published_frame {
|
||||
self.latest_published_frame = frame_index;
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,7 +55,7 @@ impl PresentationState {
|
||||
}
|
||||
|
||||
fn needs_redraw(&self) -> bool {
|
||||
self.host_invalidated || self.last_presented_frame != self.latest_published_frame
|
||||
self.host_invalidated || self.last_presented_frame != Some(self.latest_published_frame)
|
||||
}
|
||||
|
||||
fn should_request_redraw(&mut self) -> bool {
|
||||
@ -77,7 +70,7 @@ impl PresentationState {
|
||||
fn mark_presented(&mut self) {
|
||||
self.redraw_requested = false;
|
||||
self.host_invalidated = false;
|
||||
self.last_presented_frame = self.latest_published_frame;
|
||||
self.last_presented_frame = Some(self.latest_published_frame);
|
||||
}
|
||||
}
|
||||
|
||||
@ -444,11 +437,9 @@ impl ApplicationHandler for HostRunner {
|
||||
if let Some(worker_frame) = self.render_frame_store.latest_frame()
|
||||
&& should_present_worker_frame(&self.firmware.state, &worker_frame, active_ownership)
|
||||
{
|
||||
self.presentation
|
||||
.note_published_frame(worker_frame.ownership, worker_frame.frame_id.get());
|
||||
self.presentation.note_published_frame(worker_frame.frame_id.get());
|
||||
} else {
|
||||
self.presentation
|
||||
.note_published_frame(active_ownership, self.firmware.os.frame_index());
|
||||
self.presentation.note_published_frame(self.firmware.os.frame_index());
|
||||
}
|
||||
|
||||
if was_debugger_connected != self.debugger.stream.is_some()
|
||||
@ -514,37 +505,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn presentation_state_requests_redraw_for_new_frame_publication() {
|
||||
let ownership = RenderOwnership::new(1, AppMode::Game, 1);
|
||||
let mut state = PresentationState::default();
|
||||
state.mark_presented();
|
||||
|
||||
state.note_published_frame(ownership, 1);
|
||||
state.note_published_frame(1);
|
||||
|
||||
assert!(state.should_request_redraw());
|
||||
state.mark_presented();
|
||||
assert_eq!(
|
||||
state.last_presented_frame,
|
||||
Some(PresentationFrameMarker { ownership, frame_id: 1 })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presentation_state_requests_redraw_when_ownership_changes_with_lower_frame_id() {
|
||||
let first = RenderOwnership::new(3, AppMode::Game, 1);
|
||||
let next = RenderOwnership::new(4, AppMode::Game, 2);
|
||||
let mut state = PresentationState::default();
|
||||
|
||||
state.note_published_frame(first, 120);
|
||||
state.mark_presented();
|
||||
|
||||
state.note_published_frame(next, 0);
|
||||
|
||||
assert!(state.should_request_redraw());
|
||||
state.mark_presented();
|
||||
assert_eq!(
|
||||
state.last_presented_frame,
|
||||
Some(PresentationFrameMarker { ownership: next, frame_id: 0 })
|
||||
);
|
||||
assert_eq!(state.last_presented_frame, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -640,14 +608,11 @@ mod tests {
|
||||
let mut presentation = PresentationState::default();
|
||||
presentation.mark_presented();
|
||||
let published = store.latest_frame().expect("published frame");
|
||||
presentation.note_published_frame(published.ownership, published.frame_id.get());
|
||||
presentation.note_published_frame(published.frame_id.get());
|
||||
|
||||
assert!(presentation.should_request_redraw());
|
||||
presentation.mark_presented();
|
||||
assert_eq!(
|
||||
presentation.last_presented_frame,
|
||||
Some(PresentationFrameMarker { ownership: published.ownership, frame_id: 9 })
|
||||
);
|
||||
assert_eq!(presentation.last_presented_frame, Some(9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
[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"
|
||||
@ -1,147 +0,0 @@
|
||||
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"]));
|
||||
}
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
pbxgen_dummy_boy::generate()
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
{"type":"meta","next_id":{"DSC":45,"AGD":48,"DEC":41,"PLN":167,"LSN":55,"CLSN":1}}
|
||||
{"type":"discussion","id":"DSC-0044","status":"done","ticket":"hub-suspended-game-kill-affordance","title":"Hub Suspended Game Kill Affordance","created_at":"2026-07-05","updated_at":"2026-07-05","tags":["hub","lifecycle","game","ui"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0054","file":"discussion/lessons/DSC-0044-hub-suspended-game-kill-affordance/LSN-0054-manual-hub-kill-must-share-game-termination-cleanup.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05"}]}
|
||||
{"type":"discussion","id":"DSC-0043","status":"done","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":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0053","file":"discussion/lessons/DSC-0043-system-os-cartridge-switch-orchestrator/LSN-0053-game-switching-is-lifecycle-replacement-not-loader-work.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05"}]}
|
||||
{"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":"open","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-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"}]}
|
||||
|
||||
@ -1,67 +0,0 @@
|
||||
---
|
||||
id: LSN-0053
|
||||
ticket: system-os-cartridge-switch-orchestrator
|
||||
title: Game Switching Is Lifecycle Replacement, Not Loader Work
|
||||
created: 2026-07-05
|
||||
tags: [runtime, os, lifecycle, game, cartridge]
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
PROMETEU supports two superficially similar flows:
|
||||
|
||||
- `Game -> Home -> same Game`, which is pause/suspend/resume.
|
||||
- `Game A -> Home -> Game B`, which is destructive replacement.
|
||||
|
||||
The second flow is not a loader feature. It is a SystemOS lifecycle operation
|
||||
that happens to use the cartridge loader after the old resident Game has been
|
||||
removed from resumable state.
|
||||
|
||||
## Key Decisions
|
||||
|
||||
### SystemOS Game Cartridge Switch Orchestrator
|
||||
|
||||
**What:** A different-Game launch from Home terminates the previous resident
|
||||
Game before loading the new cartridge. The new Game receives a fresh VM session.
|
||||
|
||||
**Why:** Reusing or preserving the old Game while the new Game loads creates a
|
||||
rollback problem across VM state, handles, assets, input, audio, render
|
||||
ownership, debug state, and lifecycle delivery. V1 does not need that
|
||||
transactional model.
|
||||
|
||||
**Trade-offs:** Failed replacement returns to Home and does not restore the old
|
||||
Game. This is intentionally simpler than snapshot/rollback and makes state
|
||||
ownership visible in tests.
|
||||
|
||||
## Patterns and Algorithms
|
||||
|
||||
Use a resolved SystemOS switch target, not a guest syscall, for Home launches.
|
||||
The target carries cartridge path and identity, while SystemOS owns the policy:
|
||||
|
||||
1. detect whether the selected Game matches the resident `app_id`;
|
||||
2. resume if it is the same Game;
|
||||
3. terminate the old resident Game if it is different;
|
||||
4. load the new cartridge through the normal loader;
|
||||
5. create and initialize a fresh VM session;
|
||||
6. return to Home on replacement failure without restoring the previous Game.
|
||||
|
||||
The loader stays a materializer. It does not close tasks, remove sessions,
|
||||
repair foreground state, or own failure recovery.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Treating a different Game as a resume path leaks VM execution state.
|
||||
- Letting the loader own lifecycle cleanup hides policy in the wrong layer.
|
||||
- Keeping the old Game as rollback state requires correctness across assets,
|
||||
render ownership, input barriers, audio, open handles, and debugger state.
|
||||
- A switch test with only one cartridge cannot prove identity replacement.
|
||||
|
||||
## Takeaways
|
||||
|
||||
- Same-Game Home selection is resume; different-Game Home selection is
|
||||
destructive replacement.
|
||||
- SystemOS owns cartridge switching policy; the cartridge loader only loads.
|
||||
- Replacement failure should be a clean Home state, not a rollback to the old
|
||||
Game.
|
||||
- End-to-end switch tests need visually distinct cartridges to catch stale
|
||||
session or stale frame reuse.
|
||||
@ -1,72 +0,0 @@
|
||||
---
|
||||
id: LSN-0054
|
||||
ticket: hub-suspended-game-kill-affordance
|
||||
title: Manual Hub Kill Must Share Game Termination Cleanup
|
||||
created: 2026-07-05
|
||||
tags: [hub, lifecycle, game, ui, assets]
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
After a Game returns to Home, the Game can remain resident and suspended so it
|
||||
can be resumed later. That state needs a visible, direct way to terminate the
|
||||
resident Game without launching another cartridge just to force replacement.
|
||||
|
||||
The Hub kill affordance solved the UX gap, but the durable engineering lesson is
|
||||
the cleanup boundary: manual kill must not become a second, partial termination
|
||||
path.
|
||||
|
||||
## Key Decisions
|
||||
|
||||
### Hub Suspended Game Kill Affordance
|
||||
|
||||
**What:** Hub renders a red bottom-right control when a Game is resident and
|
||||
suspended. The control shows only the Game title. Clicking it immediately
|
||||
terminates the resident Game and keeps the user in Hub.
|
||||
|
||||
**Why:** The user can distinguish "resume this Game" from "discard this resident
|
||||
Game" without starting a different cartridge. Reusing the canonical termination
|
||||
path prevents a repeat of stale scene/glyph/asset state bugs.
|
||||
|
||||
**Trade-offs:** V1 has no confirmation step. The destructive nature is carried
|
||||
by placement, color, and the fact that the target Game is already out of
|
||||
foreground.
|
||||
|
||||
## Patterns and Algorithms
|
||||
|
||||
Manual Hub kill and destructive Game replacement should call the same resident
|
||||
Game termination helper. That helper is responsible for the whole runtime
|
||||
surface:
|
||||
|
||||
- close the Game task;
|
||||
- stop the process;
|
||||
- remove the VM session;
|
||||
- clear resident Game state and task-scoped lifecycle delivery;
|
||||
- restore Shell/Hub render ownership;
|
||||
- clear lifecycle audio pause state;
|
||||
- unbind the Game2D composer scene;
|
||||
- shut down cartridge-scoped asset manager state and physical banks;
|
||||
- emit explicit Hub/POS observability for manual kill.
|
||||
|
||||
The Hub itself should emit a high-level action such as `KillResidentGame`; it
|
||||
should not know how to clean runtime subsystems.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- A UI-only kill that removes a task but leaves assets or composer binding alive
|
||||
can still fail later when another Game renders.
|
||||
- A cleanup-only helper without an end-to-end Hub click test can miss the
|
||||
interaction between hitbox, foreground ownership, render owner, and no-resume.
|
||||
- Hard-coded click coordinates in tests make the UI contract harder to maintain;
|
||||
expose the layout hitbox when tests need to exercise it.
|
||||
|
||||
## Takeaways
|
||||
|
||||
- Visible system controls should produce system actions, not direct subsystem
|
||||
mutation from UI code.
|
||||
- Manual kill and Game replacement are variants of the same resident Game
|
||||
termination contract.
|
||||
- Cleanup tests should prove both logical state removal and visible-frame
|
||||
behavior: the control appears before kill and disappears after kill.
|
||||
- "No resume after kill" is as important as "task closed" because resumability
|
||||
is the user-facing state.
|
||||
@ -0,0 +1,178 @@
|
||||
---
|
||||
id: AGD-0044
|
||||
ticket: system-os-cartridge-switch-orchestrator
|
||||
title: SystemOS Cartridge Switch Orchestrator
|
||||
status: accepted
|
||||
created: 2026-07-03
|
||||
resolved:
|
||||
decision:
|
||||
tags: [runtime, os, lifecycle, game, cartridge, architecture]
|
||||
---
|
||||
|
||||
## Contexto
|
||||
|
||||
A `AGD-0041` cobre o caso `Game -> Home/Shell -> mesmo Game`: o usuario sai do
|
||||
foreground do jogo, o SO assume a tela, Shell/Hub pode rodar, e depois o mesmo
|
||||
Game e retomado.
|
||||
|
||||
Existe um segundo caso, diferente, que nao deve ser escondido dentro da
|
||||
`AGD-0041`: `Game A -> Home -> Game B`.
|
||||
|
||||
Nesse fluxo, o usuario esta com um Game rodando, aperta Home, volta para o SO e
|
||||
pede para iniciar outro jogo. O SystemOS nao deve simplesmente chamar um loader
|
||||
por cima do estado atual. Ele precisa orquestrar a substituicao do app corrente:
|
||||
fechar ou descartar o Game residente, limpar estado cartridge-scoped, resolver o
|
||||
novo alvo, carregar o proximo cartucho e entrar no novo estado de firmware.
|
||||
|
||||
Essa agenda tambem se relaciona com a `AGD-0003 / system-run-cart`: `run_cart`,
|
||||
se existir, nao e userland e nao e o orquestrador. O carregador de cartucho e
|
||||
apenas uma etapa dentro de uma transicao comandada pelo SystemOS.
|
||||
|
||||
## Problema
|
||||
|
||||
Sem um orquestrador explicito de troca de cartucho no SystemOS, a plataforma
|
||||
pode cair em estados incoerentes:
|
||||
|
||||
- carregar um novo Game sem fechar corretamente o Game anterior;
|
||||
- reaproveitar estado VM, assets, input, audio, render ou telemetria do cartucho
|
||||
anterior;
|
||||
- misturar boot direto por linha de comando com troca de app sob o SO;
|
||||
- deixar o loader decidir lifecycle, quando ele deveria apenas materializar o
|
||||
cartucho alvo;
|
||||
- tentar implementar `Home -> outro Game` antes de existir o contrato de
|
||||
foreground/pausa definido pela `AGD-0041`.
|
||||
|
||||
## Pontos Criticos
|
||||
|
||||
- Esta agenda deve ser executada depois da `AGD-0041`.
|
||||
- `Game -> Home -> mesmo Game` fica fora desta agenda.
|
||||
- O SystemOS deve ser a autoridade da troca entre cartuchos sob o SO.
|
||||
- O loader nao deve possuir politica de fechamento, pausa, foreground ou
|
||||
cleanup.
|
||||
- Boot direto por CLI/debug continua sendo caminho separado e mais simples.
|
||||
- A troca para outro Game deve tratar falha de load sem deixar o sistema em
|
||||
estado parcialmente substituido.
|
||||
- `.pmc`, catalogo completo e UX final de launcher continuam fora de escopo,
|
||||
salvo se forem necessarios para definir o contrato minimo.
|
||||
|
||||
## Opcoes
|
||||
|
||||
### Opcao A - Adiar tudo para depois da AGD-0041
|
||||
|
||||
- **Abordagem:** manter esta agenda como registro de escopo futuro; primeiro
|
||||
fechar e implementar o contrato `Game -> Home -> mesmo Game`.
|
||||
- **Pro:** evita misturar pausa/resume com substituicao de cartucho; reduz risco
|
||||
de reabrir arquitetura durante a `AGD-0041`.
|
||||
- **Contra:** a plataforma continua sem troca `Home -> outro Game` ate uma etapa
|
||||
posterior.
|
||||
- **Manutenibilidade:** alta, porque cada agenda fecha um eixo de lifecycle.
|
||||
|
||||
### Opcao B - Definir agora o contrato conceitual, implementar depois
|
||||
|
||||
- **Abordagem:** fechar uma decisao curta ja dizendo que o orquestrador pertence
|
||||
ao SystemOS, que `run_cart` nao e userland, e que a execucao depende da
|
||||
`AGD-0041`; o plano de codigo fica para depois.
|
||||
- **Pro:** reduz ambiguidade enquanto trabalhamos na `AGD-0041`.
|
||||
- **Contra:** pode produzir uma decisao com pouca evidencia operacional antes
|
||||
do contrato de foreground estar fechado.
|
||||
- **Manutenibilidade:** boa se a decisao for claramente marcada como dependente
|
||||
da `AGD-0041`.
|
||||
|
||||
### Opcao C - Implementar o orquestrador completo agora
|
||||
|
||||
- **Abordagem:** tentar resolver `Game A -> Home -> Game B` antes ou junto da
|
||||
`AGD-0041`.
|
||||
- **Pro:** ataca o fluxo final diretamente.
|
||||
- **Contra:** mistura pausa/resume, fechamento, cleanup, resolucao de alvo e
|
||||
falha de load em uma unica frente; alto risco de contrato instavel.
|
||||
- **Manutenibilidade:** baixa para v1, porque a troca completa depende do
|
||||
contrato de foreground ainda aberto.
|
||||
|
||||
## Sugestao / Recomendacao
|
||||
|
||||
`DSC-0041` foi fechada e o contrato de foreground/pausa/resume ja existe. A
|
||||
recomendacao agora deixa de ser adiar e passa a ser fechar uma decisao curta
|
||||
para o orquestrador de troca `Game A -> Home -> Game B`.
|
||||
|
||||
O contrato sugerido e:
|
||||
|
||||
- trocar para outro Game e uma substituicao controlada, nao uma pausa/resume;
|
||||
- o Game residente anterior e fechado/descartado;
|
||||
- a VM session do Game anterior e removida;
|
||||
- o novo Game sempre recebe uma VM session nova e zerada;
|
||||
- se o novo load falhar, o sistema volta ao Home/Hub com diagnostico e nao
|
||||
tenta preservar o Game anterior em v1;
|
||||
- o loader continua sendo etapa interna, sem possuir politica de lifecycle;
|
||||
- a evidencia minima deve usar dois cartuchos distintos, incluindo um cartucho
|
||||
simplista `dummy-boy` alem do stress test.
|
||||
|
||||
## Perguntas em Aberto
|
||||
|
||||
- [x] Depois da `AGD-0041`, qual estado representa um Game que sera descartado
|
||||
em vez de retomado?
|
||||
- Resposta: fechamento/encerramento controlado, nao `PausedSuspended`. O Game
|
||||
anterior deixa de ser resident/resumable e sua VM session e removida.
|
||||
- [x] O fechamento do Game atual e observavel para o firmware/game, ou e uma
|
||||
destruicao interna do SystemOS?
|
||||
- Resposta: deve ser observavel como uma transicao de lifecycle/firmware, mas
|
||||
nao cooperativo nem bloqueante para o Game. O SystemOS tem autoridade para
|
||||
encerrar e descartar a sessao. Pode ser emitido um evento de encerramento
|
||||
para permitir save/flush cooperativo quando existir esse mecanismo, mas a
|
||||
troca nao deve depender da resposta do Game.
|
||||
- [x] Quais estados cartridge-scoped devem ser limpos antes do novo load: VM,
|
||||
assets, FS handles, input, audio, render ownership, telemetry, logs?
|
||||
- Resposta: a troca nao reaproveita VM. O Game anterior e encerrado, sua VM
|
||||
session e descartada, e o novo Game recebe VM/session nova e zerada. Devem
|
||||
ser limpos ou reatribuídos os estados cartridge-scoped ligados a VM,
|
||||
handles abertos, input pendente/barreiras, audio do app, render ownership,
|
||||
telemetry/debug/crash state do app. Logs historicos podem permanecer como
|
||||
observabilidade.
|
||||
- [x] Se o novo cartucho falha ao carregar, o sistema volta ao Home, mostra uma
|
||||
tela de erro, ou tenta preservar o Game anterior?
|
||||
- Resposta: o Game anterior ja foi encerrado. Se o novo load falha, a sessao
|
||||
parcial do novo Game e descartada e o sistema volta ao Home/Hub com
|
||||
diagnostico. Preservar o Game anterior fica fora de escopo em v1.
|
||||
- [x] Como a resolucao do novo alvo e representada sem depender de catalogo
|
||||
completo ou `.pmc`?
|
||||
- Resposta: para o contrato minimo, o alvo pode ser um path/entrada ja
|
||||
resolvido pelo Hub/game library atual. O contrato nao depende de `.pmc` nem
|
||||
de catalogo final.
|
||||
- [x] Qual evidencia minima prova `Game A -> Home -> Game B` sem depender de UX
|
||||
final de launcher?
|
||||
- Resposta: criar um cartucho `dummy-boy` ao lado do stress test. O teste deve
|
||||
provar `Stress -> Home -> Dummy Boy`, com troca de VM session, identidade de
|
||||
cartridge distinta e render observavel do novo Game.
|
||||
|
||||
## Criterio para Encerrar
|
||||
|
||||
A agenda pode virar decisao quando:
|
||||
|
||||
- a `AGD-0041` tiver fechado o contrato de foreground/pausa/resume;
|
||||
- estiver claro que o orquestrador pertence ao SystemOS;
|
||||
- `run_cart`/loader estiver limitado a etapa interna, nao userland;
|
||||
- existir regra para fechar/descartar o Game atual;
|
||||
- existir contrato de limpeza de estado cartridge-scoped;
|
||||
- existir politica para falha de carregamento do novo Game;
|
||||
- houver criterio de teste minimo para `Game A -> Home -> Game B`.
|
||||
|
||||
## Discussao
|
||||
|
||||
- 2026-07-03: Agenda criada a partir da separacao entre `AGD-0041`
|
||||
(`Game -> Home -> mesmo Game`) e `AGD-0003` (`run_cart`/loader nao userland).
|
||||
O novo escopo cobre a troca posterior `Game A -> Home -> Game B`, com
|
||||
orquestracao pelo SystemOS depois que o contrato de foreground estiver
|
||||
fechado.
|
||||
- 2026-07-05: Apos fechamento de `DSC-0041`, a discussao convergiu para troca
|
||||
destrutiva simples em v1: ao iniciar outro Game, o Game residente anterior e
|
||||
encerrado, sua VM session e removida, e o novo Game recebe VM session nova.
|
||||
Falha de load volta ao Home/Hub com diagnostico. A evidencia minima deve
|
||||
incluir um novo cartucho `dummy-boy` que desenha um quadrado com cores
|
||||
oscilando, alem do stress test.
|
||||
- 2026-07-05: Refinamento: o encerramento do Game anterior pode emitir evento
|
||||
cooperativo para save/flush, mas esse evento nao e uma permissao nem uma
|
||||
barreira obrigatoria. O SystemOS continua com autoridade para concluir a troca
|
||||
e remover a VM session anterior.
|
||||
|
||||
## Resolution
|
||||
|
||||
Pronta para virar decisao, pendente confirmacao explicita do usuario.
|
||||
@ -0,0 +1,174 @@
|
||||
---
|
||||
id: DEC-0039
|
||||
ticket: system-os-cartridge-switch-orchestrator
|
||||
title: SystemOS Game Cartridge Switch Orchestrator
|
||||
status: accepted
|
||||
created: 2026-07-05
|
||||
ref_agenda: AGD-0044
|
||||
tags: [runtime, os, lifecycle, game, cartridge, architecture]
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
Open for review.
|
||||
|
||||
## Contexto
|
||||
|
||||
`DEC-0037` fechou o contrato `Game -> Home/Shell -> mesmo Game`: o Game pode
|
||||
ficar residente, ser pausado/suspenso pelo SystemOS, e depois ser retomado.
|
||||
|
||||
`DEC-0038` fechou o ownership de VM sessions: VM-backed mutable execution state
|
||||
pertence a sessoes de VM associadas a processos/tarefas, nao a uma VM global do
|
||||
firmware.
|
||||
|
||||
Esta decision cobre outro fluxo: `Game A -> Home -> Game B`. Nesse caso, o
|
||||
usuario nao esta retomando o mesmo Game. O usuario esta escolhendo substituir o
|
||||
Game residente por outro cartucho. Essa substituicao precisa ser orquestrada
|
||||
pelo SystemOS, nao pelo loader e nao por userland.
|
||||
|
||||
## Decisao
|
||||
|
||||
PROMETEU MUST tratar `Game A -> Home -> Game B` como uma substituicao
|
||||
controlada de Game, nao como pause/resume.
|
||||
|
||||
Quando o usuario escolher iniciar um Game diferente enquanto ja existe um Game
|
||||
residente, SystemOS MUST encerrar o Game residente anterior, remover sua
|
||||
resident state e descartar sua VM session antes de ativar o novo Game como
|
||||
resident/foreground.
|
||||
|
||||
O Game anterior MAY receber um evento cooperativo de encerramento para permitir
|
||||
save/flush quando esse mecanismo existir. Esse evento MUST NOT ser uma
|
||||
permissao, veto ou barreira obrigatoria para a troca. SystemOS MUST manter
|
||||
autoridade para concluir a troca e remover a VM session anterior mesmo se o
|
||||
Game nao reagir.
|
||||
|
||||
O novo Game MUST receber uma VM session nova e zerada. A troca MUST NOT
|
||||
reutilizar a VM session, PC, stack, heap, globals, open handles, cartridge
|
||||
identity, lifecycle state, debug state ou runtime state do Game anterior.
|
||||
|
||||
O loader/materializador de cartridge MUST permanecer uma etapa interna de
|
||||
carregamento. Ele MUST NOT possuir politica de lifecycle, foreground,
|
||||
close/discard, cleanup ou recuperacao de falha.
|
||||
|
||||
Se o novo cartucho falhar ao carregar, SystemOS MUST descartar qualquer session
|
||||
parcial ou estado cartridge-scoped parcial do novo Game e retornar ao Home/Hub
|
||||
com diagnostico. V1 MUST NOT tentar preservar ou restaurar o Game anterior
|
||||
depois que a troca tiver comecado.
|
||||
|
||||
Boot direto por CLI/debug permanece um caminho separado e mais simples. Esta
|
||||
decision governa troca de Game sob SystemOS depois que o usuario retorna ao
|
||||
Home/Hub.
|
||||
|
||||
A evidencia minima de implementacao MUST incluir dois test cartridges
|
||||
distintos: o stress cartridge existente e um novo cartridge `dummy-boy`.
|
||||
`dummy-boy` MUST renderizar um quadrado visualmente distinto com cor oscilante
|
||||
para que os testes provem que o segundo Game, e nao um frame/session anterior
|
||||
stale, esta ativo.
|
||||
|
||||
## Rationale
|
||||
|
||||
Trying to preserve Game A while loading Game B would turn the switch into a
|
||||
transactional snapshot/rollback problem. That is not needed for v1 and would
|
||||
pull in VM snapshot correctness, open handle restoration, assets, render
|
||||
ownership rollback, audio rollback, input state, debugger state, and telemetry
|
||||
rollback.
|
||||
|
||||
The simpler and safer v1 contract is destructive replacement:
|
||||
|
||||
1. the user requests another Game;
|
||||
2. SystemOS ends the previous resident Game;
|
||||
3. SystemOS removes its VM session and cartridge-scoped runtime state;
|
||||
4. SystemOS attempts to load the new Game into a fresh VM session;
|
||||
5. success enters the new Game;
|
||||
6. failure returns to Home/Hub with diagnostic.
|
||||
|
||||
This aligns with `DEC-0037` because pause/suspend remains the same-Game resume
|
||||
flow. It aligns with `DEC-0038` because VM identity is session-owned and each
|
||||
Game receives a distinct VM session.
|
||||
|
||||
## Invariantes / Contrato
|
||||
|
||||
- SystemOS is the authority for switching from one Game cartridge to another
|
||||
under Home/Hub.
|
||||
- `Game -> Home -> same Game` is pause/resume and remains governed by
|
||||
`DEC-0037`.
|
||||
- `Game A -> Home -> Game B` is destructive replacement and is governed by this
|
||||
decision.
|
||||
- A Game that is being replaced MUST stop being resident/resumable.
|
||||
- The previous Game VM session MUST be removed before the new Game becomes the
|
||||
active resident/foreground Game.
|
||||
- A cooperative close/terminate/save event MAY be delivered to the previous
|
||||
Game, but it MUST NOT block replacement.
|
||||
- The new Game MUST get a fresh VM session.
|
||||
- The new Game MUST NOT inherit VM execution state, open handles, lifecycle
|
||||
delivery state, cartridge identity, debug state, telemetry state, or crash
|
||||
state from the previous Game.
|
||||
- Render ownership MUST transition through SystemOS ownership policy and stale
|
||||
submissions from the previous Game MUST NOT be presented after the switch.
|
||||
- Input pending/barrier state MUST be cleared or made safe across the switch.
|
||||
- App audio from the previous Game MUST be stopped or detached from the active
|
||||
app before the new Game becomes foreground.
|
||||
- Logs MAY remain as historical observability; they are not cartridge execution
|
||||
state.
|
||||
- If new Game load fails, Home/Hub is the recovery state for v1.
|
||||
- V1 does not preserve, restore, or rollback to the previous Game after a
|
||||
switch begins.
|
||||
- The launcher target MAY be a path or already-resolved game-library entry for
|
||||
v1. `.pmc` and final catalog UX are out of scope.
|
||||
- `run_cart`, if retained, is not a userland command and not the switch
|
||||
orchestrator.
|
||||
|
||||
## Impactos
|
||||
|
||||
- **Runtime / SystemOS:** needs an explicit Game switch orchestration path that
|
||||
closes/removes the current resident Game session before loading the next Game.
|
||||
- **Lifecycle:** needs a controlled replacement transition distinct from
|
||||
pause/suspend/resume.
|
||||
- **VM sessions:** previous Game session removal and new Game session creation
|
||||
are mandatory switch steps.
|
||||
- **Firmware:** should observe macro states such as Home, loading, load failure,
|
||||
and GameRunning, but should not own switch policy.
|
||||
- **Hub / launcher:** should request a switch through SystemOS using a resolved
|
||||
target; it should not call a loader directly as lifecycle authority.
|
||||
- **Loader:** remains a materialization step and returns success/failure without
|
||||
owning cleanup policy.
|
||||
- **Renderer:** render ownership/stale submission invalidation must prove the
|
||||
new Game owns current presentation after success.
|
||||
- **Input/audio:** previous Game input/audio must not leak into the new Game.
|
||||
- **Tests:** need a second visually distinct test cartridge, `dummy-boy`, to
|
||||
prove Game identity and render transition.
|
||||
|
||||
## Alternativas Descartadas
|
||||
|
||||
- **Preserve Game A until Game B loads successfully:** rejected for v1 because it
|
||||
requires rollback semantics across VM, assets, handles, render ownership,
|
||||
input, audio, debugger, and telemetry.
|
||||
- **Treat Game A -> Game B as pause/resume:** rejected because a different Game
|
||||
should not inherit resumable state from the previous Game.
|
||||
- **Let the loader close the previous Game:** rejected because loader/materializer
|
||||
should not own lifecycle policy.
|
||||
- **Reuse the previous VM session for the new Game:** rejected because it
|
||||
violates VM session ownership and risks state leakage.
|
||||
- **Depend on `.pmc` or final catalog UX now:** rejected because the lifecycle
|
||||
contract can be validated using path/game-library targets first.
|
||||
|
||||
## Referencias
|
||||
|
||||
- Source agenda: `AGD-0044`
|
||||
- Foreground stack and Game pause contract: `DEC-0037`
|
||||
- VM session ownership for VM-backed processes: `DEC-0038`
|
||||
- SystemOS lifecycle/process/task authority: `DSC-0032`
|
||||
- Launch authority lesson: `LSN-0051`
|
||||
- Foreground/session ownership lesson: `LSN-0052`
|
||||
|
||||
## Propagacao Necessaria
|
||||
|
||||
- Add/update runtime spec for Game cartridge switching under SystemOS.
|
||||
- Add implementation plans for:
|
||||
- SystemOS switch orchestration;
|
||||
- resident Game termination/session removal;
|
||||
- fresh session load for the new Game;
|
||||
- failure recovery to Home/Hub;
|
||||
- `dummy-boy` test cartridge;
|
||||
- end-to-end `Stress -> Home -> Dummy Boy` validation.
|
||||
- Ensure host/debug direct boot remains separate from SystemOS Game switch.
|
||||
@ -0,0 +1,86 @@
|
||||
---
|
||||
id: PLN-0157
|
||||
ticket: system-os-cartridge-switch-orchestrator
|
||||
title: Introduce SystemOS Game Switch Target Contract
|
||||
status: done
|
||||
created: 2026-07-05
|
||||
ref_decisions: [DEC-0039]
|
||||
tags: [runtime, os, lifecycle, game, cartridge, architecture]
|
||||
---
|
||||
|
||||
## Briefing
|
||||
|
||||
DEC-0039 requires Game-to-Game switching under Home/Hub to be owned by SystemOS.
|
||||
Before destructive replacement can be implemented, Hub/Firmware/SystemOS need a
|
||||
small explicit contract for "launch this resolved Game target as a replacement"
|
||||
that is separate from direct boot and from loader internals.
|
||||
|
||||
## Objective
|
||||
|
||||
Introduce a SystemOS-owned Game switch request/target abstraction that can carry
|
||||
a resolved cartridge path or game-library entry from Hub/Home into the switch
|
||||
orchestrator without making the loader or userland the lifecycle authority.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Source decision: DEC-0039.
|
||||
- Builds on DEC-0037 and DEC-0038 as already implemented.
|
||||
- Must be implemented before PLN-0158, PLN-0159, and PLN-0161.
|
||||
|
||||
## Scope
|
||||
|
||||
- Define a minimal Game switch target type, such as `GameSwitchTarget` or
|
||||
`CartridgeLaunchTarget`, in the SystemOS/Firmware boundary.
|
||||
- Route Hub game-library launch actions through that target contract instead of
|
||||
treating a path as an implicit lifecycle operation.
|
||||
- Keep direct boot/debug boot paths separate from SystemOS Game switching.
|
||||
- Add tests proving the target preserves cartridge identity/path without
|
||||
starting lifecycle replacement by itself.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not implement destructive replacement in this plan.
|
||||
- Do not add `.pmc`, final catalog UX, or a new launcher UI.
|
||||
- Do not change direct CLI/debug boot behavior.
|
||||
- Do not add background execution.
|
||||
|
||||
## Execution Method
|
||||
|
||||
1. Inspect `crates/console/prometeu-system/src/services/game_library.rs`,
|
||||
`crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`,
|
||||
and `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs`.
|
||||
2. Add a minimal launch target model at the SystemOS/Firmware boundary:
|
||||
- resolved cartridge path;
|
||||
- optional app_id/title metadata if already available;
|
||||
- no loader-owned lifecycle policy.
|
||||
3. Update Hub action plumbing so `SystemProfileAction::LaunchGame` carries the
|
||||
resolved target type or maps immediately into it at the firmware boundary.
|
||||
4. Add a placeholder SystemOS/Firmware entry point for Game switching that
|
||||
accepts the target but still delegates to the existing load path until
|
||||
PLN-0158 changes replacement behavior.
|
||||
5. Add unit tests for target construction and path propagation.
|
||||
6. Keep direct `BootTarget::Cartridge` behavior unchanged.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- There is an explicit type or function contract representing a resolved
|
||||
SystemOS Game switch target.
|
||||
- Hub/Home no longer treats a raw path as lifecycle authority.
|
||||
- The loader remains a materialization step, not the switch orchestrator.
|
||||
- Direct boot/debug boot code paths remain separate and behaviorally unchanged.
|
||||
- Tests prove target identity is carried without implicitly closing or loading a
|
||||
Game.
|
||||
|
||||
## Tests
|
||||
|
||||
- `cargo test -p prometeu-system`
|
||||
- `cargo test -p prometeu-firmware`
|
||||
- `cargo test -p prometeu-host-desktop-winit`
|
||||
- `cargo clippy -p prometeu-system -p prometeu-firmware -p prometeu-host-desktop-winit --all-targets -- -D warnings`
|
||||
|
||||
## Affected Artifacts
|
||||
|
||||
- `crates/console/prometeu-system/src/services/game_library.rs`
|
||||
- `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`
|
||||
- `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs`
|
||||
- `crates/console/prometeu-firmware/src/firmware/firmware.rs`
|
||||
@ -0,0 +1,98 @@
|
||||
---
|
||||
id: PLN-0158
|
||||
ticket: system-os-cartridge-switch-orchestrator
|
||||
title: Terminate Resident Game Before Switching Cartridges
|
||||
status: done
|
||||
created: 2026-07-05
|
||||
ref_decisions: [DEC-0039]
|
||||
tags: [runtime, os, lifecycle, game, cartridge, architecture]
|
||||
---
|
||||
|
||||
## Briefing
|
||||
|
||||
DEC-0039 defines `Game A -> Home -> Game B` as destructive replacement. The
|
||||
current same-Game resume flow keeps the resident Game session alive; switching
|
||||
to a different Game must instead close/remove that resident session and create a
|
||||
fresh session for the new Game.
|
||||
|
||||
## Objective
|
||||
|
||||
Implement the SystemOS lifecycle/session operation that terminates the current
|
||||
resident Game before loading a different Game cartridge.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Source decision: DEC-0039.
|
||||
- Requires PLN-0157 target contract.
|
||||
- Should be implemented before PLN-0159 and PLN-0161.
|
||||
|
||||
## Scope
|
||||
|
||||
- Add a controlled resident Game termination/replacement operation to SystemOS
|
||||
lifecycle/session services.
|
||||
- Optionally emit a cooperative terminate/close/save lifecycle event when the
|
||||
current event model can carry it, but do not block replacement on it.
|
||||
- Remove the previous Game from resident/resumable state.
|
||||
- Remove the previous Game VM session before activating the new Game.
|
||||
- Clear or reassign app-scoped state that cannot leak into the new Game:
|
||||
session VM, open handles, pending lifecycle state, input barrier/pending
|
||||
input, audio active app state, render ownership, telemetry/debug/crash state.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not preserve Game A for rollback.
|
||||
- Do not implement failed-load recovery beyond surfacing errors; that is
|
||||
PLN-0159.
|
||||
- Do not add background execution or multiple resident Games.
|
||||
- Do not build final launcher UX.
|
||||
|
||||
## Execution Method
|
||||
|
||||
1. Inspect lifecycle/session state in:
|
||||
- `crates/console/prometeu-system/src/os/lifecycle.rs`;
|
||||
- `crates/console/prometeu-system/src/os/facades/sessions.rs`;
|
||||
- `crates/console/prometeu-system/src/services/foreground`;
|
||||
- `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs`;
|
||||
- `crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs`.
|
||||
2. Add a `replace_resident_game` or equivalent operation that:
|
||||
- finds the current resident Game;
|
||||
- marks its task/process closed or stopped according to existing lifecycle
|
||||
semantics;
|
||||
- removes the resident Game entry from foreground stack/lifecycle state;
|
||||
- removes its VM session.
|
||||
3. Ensure same-Game resume still bypasses replacement and remains governed by
|
||||
DEC-0037.
|
||||
4. Update game launch from Home so launching a different app_id triggers
|
||||
replacement before load.
|
||||
5. Ensure render ownership transitions away from the old Game before the new
|
||||
Game becomes foreground.
|
||||
6. Add focused tests for resident Game termination and session removal.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Launching a different Game from Home closes/removes the resident Game rather
|
||||
than resuming it.
|
||||
- The previous Game VM session is absent before the new Game is made active.
|
||||
- Same-Game selection still resumes the existing resident Game.
|
||||
- A cooperative close/terminate event, if added, is non-blocking.
|
||||
- No previous Game VM, handle, lifecycle, render, or debug state is reused by
|
||||
the new Game.
|
||||
|
||||
## Tests
|
||||
|
||||
- `cargo test -p prometeu-system`
|
||||
- `cargo test -p prometeu-firmware`
|
||||
- `cargo clippy -p prometeu-system -p prometeu-firmware --all-targets -- -D warnings`
|
||||
- Add tests for:
|
||||
- different Game launch removes previous resident session;
|
||||
- same Game launch resumes existing resident session;
|
||||
- old Game task/process state is closed/stopped;
|
||||
- old Game session cannot be resolved after replacement starts.
|
||||
|
||||
## Affected Artifacts
|
||||
|
||||
- `crates/console/prometeu-system/src/os/lifecycle.rs`
|
||||
- `crates/console/prometeu-system/src/os/facades/sessions.rs`
|
||||
- `crates/console/prometeu-system/src/services/foreground`
|
||||
- `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs`
|
||||
- `crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs`
|
||||
@ -0,0 +1,86 @@
|
||||
---
|
||||
id: PLN-0159
|
||||
ticket: system-os-cartridge-switch-orchestrator
|
||||
title: Recover Failed Game Switches Back To Home
|
||||
status: open
|
||||
created: 2026-07-05
|
||||
ref_decisions: [DEC-0039]
|
||||
tags: [runtime, os, lifecycle, game, cartridge, architecture]
|
||||
---
|
||||
|
||||
## Briefing
|
||||
|
||||
DEC-0039 rejects preserving Game A after a switch begins. If Game B fails to
|
||||
load, the system must discard partial Game B state and return to Home/Hub with a
|
||||
diagnostic, not attempt rollback to Game A.
|
||||
|
||||
## Objective
|
||||
|
||||
Implement controlled failed-switch recovery to Home/Hub after destructive Game
|
||||
replacement begins.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Source decision: DEC-0039.
|
||||
- Requires PLN-0157 target contract.
|
||||
- Requires PLN-0158 resident Game termination/session removal.
|
||||
|
||||
## Scope
|
||||
|
||||
- Detect load/init failure while switching to a different Game.
|
||||
- Discard any partial new Game task/session/window/render/input/audio state.
|
||||
- Return firmware/SystemOS to Home/Hub.
|
||||
- Log or expose a diagnostic that Home can surface or tests can assert.
|
||||
- Ensure previous Game is not restored after switch failure.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not implement transactional rollback.
|
||||
- Do not preserve the previous Game after switch begins.
|
||||
- Do not design final user-facing error UI.
|
||||
- Do not change direct boot failure behavior unless needed to keep paths
|
||||
separate.
|
||||
|
||||
## Execution Method
|
||||
|
||||
1. Inspect existing load failure flow in
|
||||
`crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs`.
|
||||
2. Add a switch-specific failure outcome or context flag that distinguishes
|
||||
SystemOS Game switching from direct boot load.
|
||||
3. On switch load failure:
|
||||
- remove partial new VM session/task/process if created;
|
||||
- clear partial cartridge-scoped state;
|
||||
- transition to `HubHome` instead of app crash when appropriate for switch
|
||||
recovery;
|
||||
- log a diagnostic containing target identity/path and failure summary.
|
||||
4. Preserve existing direct boot and explicit load failure tests unless a direct
|
||||
boot path intentionally still goes to crash.
|
||||
5. Add tests for bad Game B after Game A was already terminated.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Failed `Game A -> Home -> Broken Game B` returns to Home/Hub.
|
||||
- Previous Game A is not resident, resumable, or restored.
|
||||
- Partial Game B VM session/task/process state is removed or closed.
|
||||
- A diagnostic is logged or otherwise observable.
|
||||
- Existing direct boot/debug boot failure semantics remain separate.
|
||||
|
||||
## Tests
|
||||
|
||||
- `cargo test -p prometeu-firmware`
|
||||
- `cargo test -p prometeu-system`
|
||||
- `cargo test -p prometeu-host-desktop-winit`
|
||||
- `cargo clippy -p prometeu-system -p prometeu-firmware -p prometeu-host-desktop-winit --all-targets -- -D warnings`
|
||||
- Add tests for:
|
||||
- switch failure returns Home;
|
||||
- old Game not restored;
|
||||
- partial new session cleanup;
|
||||
- direct boot failure path remains unchanged.
|
||||
|
||||
## Affected Artifacts
|
||||
|
||||
- `crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs`
|
||||
- `crates/console/prometeu-firmware/src/firmware/firmware_step_hub_home.rs`
|
||||
- `crates/console/prometeu-firmware/src/firmware/firmware.rs`
|
||||
- `crates/console/prometeu-system/src/os/facades/sessions.rs`
|
||||
- `crates/console/prometeu-system/src/os/lifecycle.rs`
|
||||
@ -0,0 +1,90 @@
|
||||
---
|
||||
id: PLN-0160
|
||||
ticket: system-os-cartridge-switch-orchestrator
|
||||
title: Add Dummy Boy Visual Switch Test Cartridge
|
||||
status: open
|
||||
created: 2026-07-05
|
||||
ref_decisions: [DEC-0039]
|
||||
tags: [runtime, os, lifecycle, game, cartridge, architecture]
|
||||
---
|
||||
|
||||
## Briefing
|
||||
|
||||
DEC-0039 requires minimum evidence using two distinct test cartridges. The
|
||||
existing stress cartridge proves execution/logging but is not visually distinct
|
||||
enough for switch validation. A second cartridge, `dummy-boy`, must render an
|
||||
oscillating-color square.
|
||||
|
||||
## Objective
|
||||
|
||||
Add a simple `test-cartridges/dummy-boy` cartridge that renders a visible square
|
||||
with changing colors so Game switching can prove visual/session identity.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Source decision: DEC-0039.
|
||||
- Can be implemented independently of PLN-0157 through PLN-0159.
|
||||
- Required before PLN-0161 end-to-end validation.
|
||||
|
||||
## Scope
|
||||
|
||||
- Add `test-cartridges/dummy-boy/manifest.json`.
|
||||
- Generate or add `test-cartridges/dummy-boy/program.pbx`.
|
||||
- Add empty or minimal assets payload if the loader requires `assets.pa`.
|
||||
- Implement Dummy Boy behavior:
|
||||
- Game cartridge identity distinct from stress;
|
||||
- deterministic visible square;
|
||||
- color oscillates across frames;
|
||||
- no dependency on final launcher UX.
|
||||
- Add build/generation documentation or tests so the cartridge can be
|
||||
regenerated safely if needed.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not add final game content.
|
||||
- Do not add `.pmc`.
|
||||
- Do not depend on host screenshots for the first validation unless an existing
|
||||
harness already supports it.
|
||||
- Do not change stress cartridge behavior.
|
||||
|
||||
## Execution Method
|
||||
|
||||
1. Inspect the stress cartridge and any PBX generator tooling:
|
||||
- `test-cartridges/stress-console`;
|
||||
- `crates/tools/pbxgen-stress`;
|
||||
- bytecode assembler helpers in `crates/console/prometeu-bytecode`.
|
||||
2. Choose the smallest maintainable generator path:
|
||||
- extend existing PBX generator if appropriate; or
|
||||
- add a focused `pbxgen-dummy-boy` tool; or
|
||||
- add deterministic generated PBX with source instructions.
|
||||
3. Create `manifest.json` with unique `app_id`, title `Dummy Boy`, Game mode,
|
||||
and version.
|
||||
4. Generate `program.pbx` that renders the square and advances color over
|
||||
frames.
|
||||
5. Add tests or smoke checks that the cartridge is discoverable by games-root
|
||||
discovery.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- `test-cartridges/dummy-boy` exists and loads as a Game cartridge.
|
||||
- Dummy Boy has a unique `app_id` and title distinct from Stress.
|
||||
- Dummy Boy renders a visible square.
|
||||
- Square color changes deterministically over time.
|
||||
- Games-root discovery sees both Stress and Dummy Boy.
|
||||
- Existing stress cartridge remains unchanged.
|
||||
|
||||
## Tests
|
||||
|
||||
- `cargo test -p prometeu-hal`
|
||||
- `cargo test -p prometeu-system`
|
||||
- `cargo test -p prometeu-firmware`
|
||||
- Run any cartridge generator tests added by this plan.
|
||||
- Add or update tests for games-root discovery including Dummy Boy.
|
||||
|
||||
## Affected Artifacts
|
||||
|
||||
- `test-cartridges/dummy-boy/manifest.json`
|
||||
- `test-cartridges/dummy-boy/program.pbx`
|
||||
- `test-cartridges/dummy-boy/assets.pa` if required
|
||||
- `crates/tools/*` cartridge generator tooling if needed
|
||||
- `crates/console/prometeu-system/src/services/game_library.rs` tests if needed
|
||||
@ -0,0 +1,93 @@
|
||||
---
|
||||
id: PLN-0161
|
||||
ticket: system-os-cartridge-switch-orchestrator
|
||||
title: Validate Stress To Dummy Boy Game Switch End To End
|
||||
status: open
|
||||
created: 2026-07-05
|
||||
ref_decisions: [DEC-0039]
|
||||
tags: [runtime, os, lifecycle, game, cartridge, architecture]
|
||||
---
|
||||
|
||||
## Briefing
|
||||
|
||||
DEC-0039 requires proof that `Game A -> Home -> Game B` creates a fresh Game
|
||||
session and does not present stale state from Game A. The minimum evidence is
|
||||
switching from Stress to Dummy Boy.
|
||||
|
||||
## Objective
|
||||
|
||||
Add end-to-end validation for `Stress -> Home -> Dummy Boy` through the real
|
||||
Hub/Home flow, proving lifecycle, VM session, cartridge identity, and render
|
||||
ownership switch correctly.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Source decision: DEC-0039.
|
||||
- Requires PLN-0157 target contract.
|
||||
- Requires PLN-0158 destructive replacement.
|
||||
- Requires PLN-0159 failed-switch recovery for negative path assertions.
|
||||
- Requires PLN-0160 Dummy Boy cartridge.
|
||||
|
||||
## Scope
|
||||
|
||||
- Add firmware/system tests that drive:
|
||||
- launch Stress from games root;
|
||||
- request Home;
|
||||
- launch Dummy Boy;
|
||||
- assert Stress session is gone;
|
||||
- assert Dummy Boy session is active;
|
||||
- assert cartridge identity and render ownership belong to Dummy Boy.
|
||||
- Add a negative end-to-end test for broken Game B returning to Home if not
|
||||
already covered by PLN-0159.
|
||||
- Use existing test platform/render submission inspection where possible.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not require final launcher UX.
|
||||
- Do not add screenshot-based host automation unless existing test utilities
|
||||
make it cheap and deterministic.
|
||||
- Do not implement additional Game-to-Game variants beyond Stress to Dummy Boy.
|
||||
|
||||
## Execution Method
|
||||
|
||||
1. Inspect existing firmware tests in
|
||||
`crates/console/prometeu-firmware/src/firmware/firmware.rs`.
|
||||
2. Reuse existing `TestGameRoot` or games-root helpers to expose Stress and
|
||||
Dummy Boy.
|
||||
3. Drive input through Hub/Home selection rather than calling cartridge load
|
||||
directly when possible.
|
||||
4. Assert lifecycle/session invariants:
|
||||
- old task closed/stopped;
|
||||
- old VM session removed;
|
||||
- new VM session created;
|
||||
- foreground owner is new Game;
|
||||
- current cartridge identity is Dummy Boy.
|
||||
5. Assert render invariants:
|
||||
- render ownership epoch belongs to Dummy Boy app identity;
|
||||
- stale Stress submissions are not accepted after switch;
|
||||
- latest observable Game frame comes from Dummy Boy when render inspection is
|
||||
available.
|
||||
6. Add a focused test for failure path if missing after PLN-0159.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- End-to-end test proves Stress can be replaced by Dummy Boy from Home.
|
||||
- Stress is not resumable after switching to Dummy Boy.
|
||||
- Dummy Boy receives a fresh VM session.
|
||||
- Cartridge identity and active render ownership are Dummy Boy after switch.
|
||||
- Failure path returns Home and does not restore Stress.
|
||||
- Tests are deterministic and do not rely on manual host interaction.
|
||||
|
||||
## Tests
|
||||
|
||||
- `cargo test -p prometeu-firmware`
|
||||
- `cargo test -p prometeu-system`
|
||||
- `cargo test -p prometeu-host-desktop-winit`
|
||||
- `cargo clippy -p prometeu-system -p prometeu-firmware -p prometeu-host-desktop-winit --all-targets -- -D warnings`
|
||||
|
||||
## Affected Artifacts
|
||||
|
||||
- `crates/console/prometeu-firmware/src/firmware/firmware.rs`
|
||||
- `crates/console/prometeu-system/src/services/game_library.rs`
|
||||
- `test-cartridges/dummy-boy`
|
||||
- Host/render test utilities if needed
|
||||
@ -0,0 +1,93 @@
|
||||
---
|
||||
id: PLN-0162
|
||||
ticket: system-os-cartridge-switch-orchestrator
|
||||
title: Specify Game Switch Contract And Direct Boot Separation
|
||||
status: open
|
||||
created: 2026-07-05
|
||||
ref_decisions: [DEC-0039]
|
||||
tags: [runtime, os, lifecycle, game, cartridge, architecture]
|
||||
---
|
||||
|
||||
## Briefing
|
||||
|
||||
DEC-0039 must be reflected in the canonical runtime specs after the operational
|
||||
contract is implemented or alongside implementation. The spec must distinguish
|
||||
same-Game resume, Game replacement under SystemOS, and direct boot/debug boot.
|
||||
|
||||
## Objective
|
||||
|
||||
Update the canonical runtime documentation to describe SystemOS Game cartridge
|
||||
switching and to keep direct boot/debug boot separate from Home/Hub-driven Game
|
||||
replacement.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Source decision: DEC-0039.
|
||||
- Can be implemented before code if the spec is written as intended behavior.
|
||||
- Should be reconciled after PLN-0157 through PLN-0161 if implementation names
|
||||
differ from initial plan names.
|
||||
|
||||
## Scope
|
||||
|
||||
- Update relevant files under `docs/specs/runtime/`, especially:
|
||||
- `12-firmware-pos-and-prometeuhub.md`;
|
||||
- `13-cartridge.md`;
|
||||
- `14-boot-profiles.md`;
|
||||
- `09-events-and-concurrency.md` if lifecycle events need mention;
|
||||
- `10-debug-inspection-and-profiling.md` if debugger separation needs note.
|
||||
- Document:
|
||||
- SystemOS authority for Game switching;
|
||||
- destructive replacement semantics;
|
||||
- optional non-blocking terminate/save event;
|
||||
- fresh VM session for new Game;
|
||||
- failed switch recovery to Home/Hub;
|
||||
- direct boot/debug boot separation;
|
||||
- test cartridge evidence requirement.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not document `.pmc` or final catalog UX as required.
|
||||
- Do not specify background execution.
|
||||
- Do not revise DEC-0037 or DEC-0038 semantics.
|
||||
- Do not write lesson material; housekeeping will decide lessons after
|
||||
execution.
|
||||
|
||||
## Execution Method
|
||||
|
||||
1. Read the runtime spec files listed in scope.
|
||||
2. Add a Game switching section in the most appropriate canonical spec file.
|
||||
3. Cross-reference existing Home/Hub, cartridge, boot profile, and debug boot
|
||||
sections.
|
||||
4. Ensure language is English and normative enough for future implementers:
|
||||
`MUST`, `MUST NOT`, `MAY`, `SHOULD`.
|
||||
5. Verify no non-English agenda language leaks into specs.
|
||||
6. Run docs grep checks for the newly introduced terms.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Specs explicitly distinguish:
|
||||
- same-Game pause/resume;
|
||||
- Game A to Game B destructive replacement;
|
||||
- direct boot/debug boot.
|
||||
- Specs state that SystemOS owns Game switching policy.
|
||||
- Specs state that a new Game gets a fresh VM session.
|
||||
- Specs state that failed switch returns Home/Hub and does not restore the old
|
||||
Game in v1.
|
||||
- Specs mention Dummy Boy or equivalent two-cartridge evidence only as test
|
||||
evidence, not as runtime feature.
|
||||
- Documentation is English.
|
||||
|
||||
## Tests
|
||||
|
||||
- `rg -n "Game switch|destructive replacement|direct boot|Dummy Boy|VM session" docs/specs/runtime`
|
||||
- `discussion validate`
|
||||
- Run code tests only if this plan is implemented together with code changes:
|
||||
`cargo test -p prometeu-system -p prometeu-firmware`
|
||||
|
||||
## Affected Artifacts
|
||||
|
||||
- `docs/specs/runtime/12-firmware-pos-and-prometeuhub.md`
|
||||
- `docs/specs/runtime/13-cartridge.md`
|
||||
- `docs/specs/runtime/14-boot-profiles.md`
|
||||
- `docs/specs/runtime/09-events-and-concurrency.md`
|
||||
- `docs/specs/runtime/10-debug-inspection-and-profiling.md`
|
||||
@ -128,11 +128,6 @@ SystemOS resume request
|
||||
scheduler/runtime state. A Game may observe pause/resume, but it must not
|
||||
control whether SystemOS suspends or resumes the VM.
|
||||
|
||||
When SystemOS replaces one Game with another from Home, the old Game MAY receive
|
||||
a cooperative terminate/save lifecycle event if the runtime supports such an
|
||||
event. That event is non-blocking in v1. SystemOS remains authoritative and may
|
||||
close the old Game task/session without guest acknowledgement.
|
||||
|
||||
Pause budgets are measured in deterministic machine units, not host wall-clock
|
||||
time. A suspended Game receives no normal gameplay ticks, no normal Game input,
|
||||
and no frame pacing. Real background execution is outside the v1 foreground
|
||||
@ -168,20 +163,6 @@ requires it. Transient VM handles, open files, lifecycle delivery queues, and
|
||||
staging state are session-scoped unless another spec explicitly makes them
|
||||
durable app data.
|
||||
|
||||
During destructive Game replacement, the old Game VM session MUST be removed
|
||||
from execution eligibility and resumable state before the new Game becomes the
|
||||
foreground Game. The new Game MUST receive a fresh VM session. Failure while
|
||||
loading or initializing the replacement Game MUST return the machine to
|
||||
Hub/Home and MUST NOT restore the previous Game in v1.
|
||||
|
||||
Manual Hub termination of a suspended resident Game has the same session
|
||||
removal authority as destructive Game replacement, except that no replacement
|
||||
Game is started. SystemOS MUST remove the Game from execution eligibility and
|
||||
resumable state, close its task, stop its process, remove its VM session, clear
|
||||
task-scoped pending lifecycle delivery, and keep Hub/Home as the foreground
|
||||
owner. After manual Hub termination, resume queries MUST report no resident
|
||||
Game. A later launch of the same cartridge is a fresh Game session.
|
||||
|
||||
## 9 Render Worker Concurrency
|
||||
|
||||
The render worker is not a machine-visible event source and does not introduce
|
||||
|
||||
@ -44,11 +44,6 @@ PROMETEU operates in three main modes:
|
||||
|
||||
No mode alters the logical result of the program.
|
||||
|
||||
Debug boot is a host-selected direct cartridge boot profile. It follows the same
|
||||
cartridge/app-mode resolution path as direct boot and is separate from
|
||||
Home-driven Game switching. Debug boot MUST NOT be modeled as a guest-visible
|
||||
Game switch request and MUST NOT reuse a Home switch failure-recovery contract.
|
||||
|
||||
## 3 Execution Control
|
||||
|
||||
### 3.1 Pause and Resume
|
||||
|
||||
@ -43,9 +43,6 @@ The VM does not own the machine lifecycle. Firmware does.
|
||||
Game directory cartridges.
|
||||
- **Library entry**: an internal catalog record retained by Hub/SystemOS for a
|
||||
discovered cartridge.
|
||||
- **Game switch target**: a SystemOS-owned launch target derived from a library
|
||||
entry. It carries the cartridge path plus resolved identity such as `app_id`,
|
||||
`title`, and `app_version`.
|
||||
- **Foreground visual owner**: the single machine owner whose visual output may
|
||||
be presented at a given time.
|
||||
- **Paused**: a Game-visible lifecycle event/state. The Game may observe it and
|
||||
@ -55,8 +52,6 @@ The VM does not own the machine lifecycle. Firmware does.
|
||||
frame pacing.
|
||||
- **VM session**: the POS-owned mutable execution context for one VM-backed
|
||||
process. It contains the VM instance and per-session runtime state.
|
||||
- **Hub resident Game kill control**: a Hub/Home affordance that terminates the
|
||||
current resident Game instead of resuming it.
|
||||
|
||||
## 3 POS Responsibilities
|
||||
|
||||
@ -91,8 +86,6 @@ PrometeuHub is responsible for:
|
||||
- presenting the host-provided local games library when `--games-root <dir>` is
|
||||
configured;
|
||||
- emitting system-owned launch actions for selected Home entries.
|
||||
- presenting a resident Game kill control while Hub/Home owns foreground and a
|
||||
Game is resident/suspended.
|
||||
|
||||
The Hub does not execute bytecode directly. It always delegates execution setup to POS.
|
||||
|
||||
@ -223,17 +216,6 @@ It preserves Game VM state but is not execution-eligible except for the bounded
|
||||
pause handoff and later foreground resume. A VM-backed Shell has a separate VM
|
||||
session. A native Shell has no VM session.
|
||||
|
||||
When Hub/Home is foreground and a Game cartridge remains resident/suspended,
|
||||
PrometeuHub MUST render a red control in the bottom-right of the Hub viewport.
|
||||
The control MUST display only the resident Game title. It MUST NOT be rendered
|
||||
for no resident Game, Shell tasks, System tasks, or native Shell tasks.
|
||||
|
||||
Clicking the Hub resident Game kill control is a system-owned immediate
|
||||
termination request. Firmware MUST keep the user in Hub/Home, MUST NOT ask for
|
||||
confirmation in v1, and MUST NOT resume the killed Game on subsequent Home
|
||||
actions. The control MUST disappear on the next Hub frame after the resident
|
||||
Game is no longer present.
|
||||
|
||||
## 8 Cartridge Load Flow
|
||||
|
||||
Current high-level flow:
|
||||
@ -249,51 +231,11 @@ Current high-level flow:
|
||||
If VM initialization fails, firmware transitions to the crash path.
|
||||
|
||||
For v1, POS must enforce at most one resident Game session. Selecting the same
|
||||
resident Game from Home resumes that session. Selecting a different Game from
|
||||
Home is a SystemOS-owned Game switch and MUST be destructive replacement:
|
||||
|
||||
```text
|
||||
Game A resident
|
||||
-> Hub/Home selected Game B with different app_id
|
||||
-> SystemOS terminates Game A resident task/session
|
||||
-> SystemOS removes Game A from resumable state
|
||||
-> firmware loads Game B through LoadCartridge
|
||||
-> Game B receives a fresh VM session
|
||||
```
|
||||
|
||||
SystemOS MUST NOT create a second resident Game session. SystemOS MUST NOT
|
||||
reuse the old Game VM session, transient VM handles, pending lifecycle delivery
|
||||
state, render ownership, debug pause state, or crash/debug runtime state for the
|
||||
new Game. Durable app data remains governed by the relevant storage specs and
|
||||
may still be keyed by `app_id`.
|
||||
|
||||
SystemOS MAY emit a cooperative non-blocking terminate/save lifecycle event
|
||||
before closing the old Game when such an event exists. Replacement MUST NOT
|
||||
block on that event in v1. VM-backed Shell loading creates a distinct Shell
|
||||
resident Game from Home resumes that session. Selecting a different Game while a
|
||||
resident Game exists is outside the v1 foreground contract and must not create a
|
||||
second resident Game session. VM-backed Shell loading creates a distinct Shell
|
||||
session and must not overwrite resident Game VM state.
|
||||
|
||||
Manual Hub resident Game kill and destructive different-Game replacement MUST
|
||||
share the canonical resident-Game termination path. That path MUST:
|
||||
|
||||
- close the resident Game task;
|
||||
- mark its process stopped;
|
||||
- remove its VM session;
|
||||
- clear resident Game state and pending Game lifecycle delivery for that task;
|
||||
- keep or return foreground ownership to Hub/Home;
|
||||
- transition render ownership to Shell/Hub (`AppMode::Shell`, `app_id = 0`);
|
||||
- clear the Game lifecycle audio pause state;
|
||||
- unbind the Game2D frame composer scene;
|
||||
- shut down cartridge-scoped asset manager state, including slot identity and
|
||||
physical glyph/scene/sound banks.
|
||||
|
||||
After this operation, the killed Game MUST NOT be resumable. Launching that
|
||||
same Game again from Home is a fresh cartridge load and a fresh VM session.
|
||||
Manual Hub kill MUST emit an INFO Hub log with the message shape:
|
||||
|
||||
```text
|
||||
Resident game killed from Hub: <title>
|
||||
```
|
||||
|
||||
POS selects the cartridge and its execution context, but the cartridge's initial callable is not chosen by firmware metadata. Execution starts from the cartridge boot protocol defined in [`13-cartridge.md`](13-cartridge.md), currently `func_id = 0`.
|
||||
|
||||
When Home launches a discovered Game entry, the request is a system-owned action
|
||||
@ -306,16 +248,6 @@ If the selected cartridge cannot be loaded, firmware must leave or return the
|
||||
machine in the Hub/Home flow and record the failure through diagnostics or
|
||||
logging. A polished Home error surface is not required by v1.
|
||||
|
||||
If a destructive Game switch has already begun and the new Game fails to load or
|
||||
initialize, firmware MUST return to Hub/Home, MUST discard any partial new Game
|
||||
task/session/window/render/input/audio state, and MUST NOT restore the previous
|
||||
Game. Direct boot and debug boot failures remain governed by the boot profile
|
||||
that requested them and may still transition to the crash path.
|
||||
|
||||
Switch validation MUST include two distinct Game cartridges. The current
|
||||
repository evidence uses `Stress Console` and `Dummy Boy`; those cartridges are
|
||||
test evidence, not required runtime features.
|
||||
|
||||
## 9 Firmware States
|
||||
|
||||
The current firmware state model includes:
|
||||
@ -378,9 +310,6 @@ Expected behavior:
|
||||
resident Game session;
|
||||
- returning to the same Game resumes it through the foreground lifecycle and
|
||||
waits for current-epoch Game rendering before visual Game presentation.
|
||||
- clicking the red bottom-right resident Game kill control terminates the
|
||||
resident Game, keeps Home foreground, clears runtime/cartridge residue, and
|
||||
removes the resume path.
|
||||
|
||||
## 13 Relationship to Other Specs
|
||||
|
||||
|
||||
@ -186,9 +186,6 @@ For the v1 Home games library:
|
||||
- the manifest must declare `app_mode: "Game"`;
|
||||
- the library retains the loaded manifest data, `title`, `app_id`,
|
||||
`app_version`, the internal cartridge path, and discovery metadata;
|
||||
- the library entry identity is the input to the SystemOS Game switch target;
|
||||
- `app_id` is the v1 identity used to distinguish same-Game resume from
|
||||
different-Game destructive replacement;
|
||||
- invalid candidates are logged or reported as diagnostics and omitted from the
|
||||
Home list;
|
||||
- valid non-Game cartridges are omitted from the Game library.
|
||||
@ -196,11 +193,6 @@ For the v1 Home games library:
|
||||
Discovery reads metadata for cataloging. It does not replace the normal
|
||||
cartridge loader used when firmware starts the selected cartridge.
|
||||
|
||||
Selecting a discovered Game from Home is not cartridge self-execution and is not
|
||||
guest-controlled. The cartridge path is consumed by firmware/SystemOS, which
|
||||
owns same-Game resume, different-Game replacement, failure recovery, and VM
|
||||
session creation policy.
|
||||
|
||||
### Packaged `.pmc` form
|
||||
|
||||
This form is recognized conceptually by the loader boundary, but its actual load path is not implemented yet.
|
||||
|
||||
@ -97,11 +97,9 @@ The v1 games root contract is:
|
||||
and transitions through `LoadCartridge`;
|
||||
- launch failure leaves or returns the machine to Home and records an error.
|
||||
|
||||
Game-to-game switching after a game is already active is not a boot profile.
|
||||
That orchestration belongs to the foreground/home and cartridge switch
|
||||
contracts. In that path, SystemOS owns the switch policy: selecting the same
|
||||
resident Game resumes it, while selecting a different Game performs destructive
|
||||
replacement and creates a fresh VM session for the new Game.
|
||||
Game-to-game switching after a game is already active is not part of this boot
|
||||
profile. That orchestration belongs to the foreground/home and cartridge switch
|
||||
contracts.
|
||||
|
||||
The foreground/home contract defines `Game -> Home/Shell -> same Game` after a
|
||||
Game is already running. It does not change direct `--run`, debugger direct
|
||||
@ -110,10 +108,6 @@ are host/system controls, not guest cartridge boot APIs.
|
||||
|
||||
Direct boot creates the initial VM session for the selected cartridge. It does
|
||||
not create a reusable global VM shared by later Game or Shell cartridges.
|
||||
Direct boot and debug boot MUST NOT be treated as failed-switch recovery paths:
|
||||
load or initialization failures for direct boot may transition to the crash
|
||||
flow, while Home-driven switch failures return to Home and do not restore the
|
||||
previous Game.
|
||||
|
||||
## 5 Firmware State Relationship
|
||||
|
||||
@ -158,10 +152,6 @@ Direct single-cartridge boot remains a separate validation path:
|
||||
cargo run --release -p prometeu-host-desktop-winit -- --run test-cartridges/stress-console
|
||||
```
|
||||
|
||||
Game switch validation should cover the Home-driven two-cartridge path, for
|
||||
example `Stress Console -> Home -> Dummy Boy`, and assert that the first Game is
|
||||
not resumable after replacement.
|
||||
|
||||
## 8 Relationship to Other Specs
|
||||
|
||||
- [`12-firmware-pos-and-prometeuhub.md`](12-firmware-pos-and-prometeuhub.md) defines the firmware state machine that consumes boot targets.
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
# 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.
|
||||
@ -1,9 +0,0 @@
|
||||
{
|
||||
"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.
Loading…
x
Reference in New Issue
Block a user