Compare commits

...

13 Commits

53 changed files with 1167 additions and 800 deletions

View File

@ -882,7 +882,7 @@ mod tests {
fn test_valid_sysc_roundtrip() {
let mut module = minimal_module();
module.syscalls = vec![SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "draw_line".into(),
version: 1,
arg_slots: 4,

View File

@ -33,7 +33,7 @@ fn roundtrip_disasm_assemble_byte_equal_with_closures_and_coroutines() {
emit(CoreOpCode::Sleep, Some(&3u32.to_le_bytes()), &mut prog);
// HOSTCALL sysc[2]
emit(CoreOpCode::Hostcall, Some(&2u32.to_le_bytes()), &mut prog);
// SYSCALL gfx.draw_line (0x1003)
// SYSCALL gfx2d.draw_line (0x1003)
emit(CoreOpCode::Syscall, Some(&0x1003u32.to_le_bytes()), &mut prog);
// FRAME_SYNC
emit(CoreOpCode::FrameSync, None, &mut prog);

View File

@ -1,6 +1,5 @@
use crate::asset::GlyphAssetSlotIndex;
use crate::memory_banks::SceneBankPoolAccess;
use prometeu_hal::GfxBridge;
use prometeu_hal::glyph::Glyph;
use prometeu_hal::scene_bank::SceneBank;
use prometeu_hal::scene_viewport_cache::SceneViewportCache;
@ -8,6 +7,10 @@ use prometeu_hal::scene_viewport_resolver::{
CacheRefreshRequest, ResolverUpdate, SceneViewportResolver,
};
use prometeu_hal::sprite::Sprite;
use prometeu_hal::{
BoundScenePacket, Camera2D, ComposerFramePacket, Game2DFramePacket, GameSpritePacket,
GfxBridge, HudPacket,
};
use std::sync::Arc;
const EMPTY_SPRITE: Sprite = Sprite {
@ -132,6 +135,28 @@ pub struct FrameComposer {
sprite_controller: SpriteController,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ComposerBuffer {
pub bound_scene: Option<BoundScenePacket>,
pub camera: Camera2D,
pub sprites: Vec<GameSpritePacket>,
pub hud: HudPacket,
}
impl ComposerBuffer {
pub fn close(self) -> Game2DFramePacket {
Game2DFramePacket::new(
ComposerFramePacket {
bound_scene: self.bound_scene,
camera: self.camera,
sprites: self.sprites,
hud: self.hud,
},
Vec::new(),
)
}
}
impl FrameComposer {
pub fn new(
viewport_width_px: usize,
@ -246,6 +271,35 @@ impl FrameComposer {
self.sprite_controller.ordered_sprites()
}
pub fn composer_buffer_snapshot(&self) -> ComposerBuffer {
ComposerBuffer {
bound_scene: self
.active_scene_id
.map(|scene_bank_id| BoundScenePacket { bank_id: scene_bank_id as i32 }),
camera: Camera2D { x: self.camera_x_px, y: self.camera_y_px },
sprites: self
.ordered_sprites()
.into_iter()
.map(|sprite| GameSpritePacket {
glyph_id: sprite.glyph.glyph_id as i32,
palette_id: sprite.glyph.palette_id as i32,
x: sprite.x,
y: sprite.y,
layer: sprite.layer as i32,
bank_id: sprite.bank_id as i32,
flip_x: sprite.flip_x,
flip_y: sprite.flip_y,
priority: sprite.priority as i32,
})
.collect(),
hud: HudPacket::default(),
}
}
pub fn close_game2d_packet(&self) -> Game2DFramePacket {
self.composer_buffer_snapshot().close()
}
pub fn render_frame(&mut self, gfx: &mut dyn GfxBridge) {
let ordered_sprites = self.ordered_sprites();
gfx.load_frame_sprites(&ordered_sprites);
@ -750,8 +804,6 @@ mod tests {
}));
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
gfx.scene_fade_level = 31;
gfx.hud_fade_level = 31;
frame_composer.render_frame(&mut gfx);
gfx.present();
@ -759,6 +811,73 @@ mod tests {
assert_eq!(gfx.front_buffer()[0], Color::WHITE.raw());
}
#[test]
fn composer_buffer_snapshot_closes_into_game2d_packet_without_fade_state() {
let mut frame_composer = FrameComposer::new(
Hardware::W,
Hardware::H,
Arc::new(MemoryBanks::default()),
make_glyph_slot_index(&[]),
);
frame_composer.set_camera(10, 20);
frame_composer.begin_frame();
assert!(frame_composer.emit_sprite(Sprite {
glyph: Glyph { glyph_id: 7, palette_id: 3 },
x: 4,
y: 5,
layer: 2,
bank_id: 1,
active: false,
flip_x: true,
flip_y: false,
priority: 9,
}));
let packet = frame_composer.close_game2d_packet();
assert_eq!(packet.composer.bound_scene, None);
assert_eq!(packet.composer.camera, Camera2D { x: 10, y: 20 });
assert_eq!(packet.composer.sprites.len(), 1);
assert_eq!(packet.composer.sprites[0].glyph_id, 7);
assert_eq!(packet.composer.sprites[0].palette_id, 3);
assert_eq!(packet.composer.sprites[0].layer, 2);
assert!(packet.composer.sprites[0].flip_x);
assert!(packet.composer.hud.commands.is_empty());
assert!(packet.gfx2d.is_empty());
}
#[test]
fn composer_buffer_preserves_integer_layer_ordering() {
let mut frame_composer = FrameComposer::new(
Hardware::W,
Hardware::H,
Arc::new(MemoryBanks::default()),
make_glyph_slot_index(&[]),
);
frame_composer.begin_frame();
for (glyph_id, layer, priority) in [(1, 3, 1), (2, 1, 2), (3, 1, 1), (4, 0, 9)] {
assert!(frame_composer.emit_sprite(Sprite {
glyph: Glyph { glyph_id, palette_id: 0 },
x: 0,
y: 0,
layer,
bank_id: 0,
active: false,
flip_x: false,
flip_y: false,
priority,
}));
}
let packet = frame_composer.close_game2d_packet();
let glyph_order: Vec<i32> =
packet.composer.sprites.iter().map(|sprite| sprite.glyph_id).collect();
assert_eq!(glyph_order, vec![4, 3, 2, 1]);
}
#[test]
fn render_frame_with_scene_applies_refreshes_before_composition() {
let banks = Arc::new(MemoryBanks::new());
@ -774,8 +893,6 @@ mod tests {
assert!(frame_composer.bind_scene(0));
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
gfx.scene_fade_level = 31;
gfx.hud_fade_level = 31;
frame_composer.render_frame(&mut gfx);
gfx.present();
@ -811,8 +928,6 @@ mod tests {
let expected_update = expected_resolver.update(&scene, 40, 0);
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
gfx.scene_fade_level = 31;
gfx.hud_fade_level = 31;
frame_composer.render_frame(&mut gfx);
frame_composer.set_camera(40, 0);
@ -844,8 +959,6 @@ mod tests {
make_glyph_slot_index(&[(0, 0), (1, 1)]),
);
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
gfx.scene_fade_level = 31;
gfx.hud_fade_level = 31;
assert!(frame_composer.bind_scene(0));
frame_composer.render_frame(&mut gfx);
@ -923,8 +1036,6 @@ mod tests {
assert!(frame_composer.bind_scene(0));
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
gfx.scene_fade_level = 31;
gfx.hud_fade_level = 31;
frame_composer.render_frame(&mut gfx);
gfx.present();

View File

@ -1,11 +1,11 @@
use crate::memory_banks::GlyphBankPoolAccess;
use prometeu_hal::GfxBridge;
use prometeu_hal::color::Color;
use prometeu_hal::glyph::Glyph;
use prometeu_hal::glyph_bank::GlyphBank;
use prometeu_hal::scene_viewport_cache::{CachedTileEntry, SceneViewportCache};
use prometeu_hal::scene_viewport_resolver::{LayerCopyRequest, ResolverUpdate};
use prometeu_hal::sprite::Sprite;
use prometeu_hal::{Game2DFramePacket, Gfx2dCommand, GfxBridge, GfxUiCommand, ShellUiFramePacket};
use std::sync::Arc;
/// Blending modes inspired by classic 16-bit hardware.
@ -33,7 +33,7 @@ pub enum BlendMode {
///
/// `Gfx` owns the framebuffer backend and the canonical game-frame raster path
/// consumed by `FrameComposer`. That canonical path covers scene composition,
/// sprite composition, and fades. Public `gfx.*` primitives remain valid.
/// sprite composition, and primitive packet consumption.
pub struct Gfx {
/// Width of the internal framebuffer in pixels.
w: usize,
@ -49,15 +49,6 @@ pub struct Gfx {
/// Hardware sprite list (512 slots). Equivalent to OAM (Object Attribute Memory).
pub sprites: [Sprite; 512],
/// Scene brightness/fade level (0 = black/invisible, 31 = fully visible).
pub scene_fade_level: u8,
/// Target color for the scene fade effect (usually Black).
pub scene_fade_color: Color,
/// HUD brightness/fade level (independent from the scene).
pub hud_fade_level: u8,
/// Target color for the HUD fade effect.
pub hud_fade_color: Color,
/// Internal sprite count for the current frame state.
sprite_count: usize,
/// Internal cache used to sort sprites by layer while keeping stable priority order.
@ -305,31 +296,6 @@ impl GfxBridge for Gfx {
self.sprite_count = self.sprite_count.max(index.saturating_add(1)).min(self.sprites.len());
&mut self.sprites[index]
}
fn scene_fade_level(&self) -> u8 {
self.scene_fade_level
}
fn set_scene_fade_level(&mut self, level: u8) {
self.scene_fade_level = level;
}
fn scene_fade_color(&self) -> Color {
self.scene_fade_color
}
fn set_scene_fade_color(&mut self, color: Color) {
self.scene_fade_color = color;
}
fn hud_fade_level(&self) -> u8 {
self.hud_fade_level
}
fn set_hud_fade_level(&mut self, level: u8) {
self.hud_fade_level = level;
}
fn hud_fade_color(&self) -> Color {
self.hud_fade_color
}
fn set_hud_fade_color(&mut self, color: Color) {
self.hud_fade_color = color;
}
}
impl Gfx {
@ -358,10 +324,6 @@ impl Gfx {
glyph_banks,
sprites: [EMPTY_SPRITE; 512],
sprite_count: 0,
scene_fade_level: 31,
scene_fade_color: Color::BLACK,
hud_fade_level: 31,
hud_fade_color: Color::BLACK,
layer_buckets: [
Vec::with_capacity(128),
Vec::with_capacity(128),
@ -637,20 +599,13 @@ impl Gfx {
&*self.glyph_banks,
);
}
// 2. Scene-only fallback path: sprites and fades still work even before a
// cache-backed world composition request is issued for the frame.
Self::apply_fade_to_buffer(&mut self.back, self.scene_fade_level, self.scene_fade_color);
// 3. HUD Fade: independent from scene fade; HUD composition itself remains external.
Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color);
}
/// Composes the world from the viewport cache using resolver copy requests.
///
/// This is the cache-backed world path accepted by DEC-0013. The canonical scene
/// is not consulted here; the renderer only consumes prepared cache materialization
/// plus sprite state and fade controls.
/// plus sprite state.
pub fn render_scene_from_cache(
&mut self,
cache: &SceneViewportCache,
@ -681,9 +636,88 @@ impl Gfx {
&*self.glyph_banks,
);
}
}
Self::apply_fade_to_buffer(&mut self.back, self.scene_fade_level, self.scene_fade_color);
Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color);
pub fn render_game2d_frame_packet(&mut self, packet: &Game2DFramePacket) {
let sprites: Vec<Sprite> = packet
.composer
.sprites
.iter()
.map(|sprite| Sprite {
glyph: Glyph {
glyph_id: sprite.glyph_id as u16,
palette_id: sprite.palette_id as u8,
},
x: sprite.x,
y: sprite.y,
layer: sprite.layer as u8,
bank_id: sprite.bank_id as u8,
active: true,
flip_x: sprite.flip_x,
flip_y: sprite.flip_y,
priority: sprite.priority as u8,
})
.collect();
self.load_frame_sprites(&sprites);
self.render_no_scene_frame();
for command in &packet.gfx2d {
self.apply_gfx2d_command(command);
}
}
pub fn render_shell_ui_frame_packet(&mut self, packet: &ShellUiFramePacket) {
for command in &packet.commands {
self.apply_gfxui_command(command);
}
}
fn apply_gfx2d_command(&mut self, command: &Gfx2dCommand) {
match command {
Gfx2dCommand::Clear { color } => self.clear(*color),
Gfx2dCommand::FillRect { rect, color } => {
self.fill_rect(rect.x, rect.y, rect.w, rect.h, *color);
}
Gfx2dCommand::DrawLine { x0, y0, x1, y1, color } => {
self.draw_line(*x0, *y0, *x1, *y1, *color);
}
Gfx2dCommand::DrawCircle { x, y, radius, color } => {
self.draw_circle(*x, *y, *radius, *color);
}
Gfx2dCommand::DrawDisc { x, y, radius, border_color, fill_color } => {
self.draw_disc(*x, *y, *radius, *border_color, *fill_color);
}
Gfx2dCommand::DrawSquare { rect, border_color, fill_color } => {
self.draw_square(rect.x, rect.y, rect.w, rect.h, *border_color, *fill_color);
}
Gfx2dCommand::DrawText { x, y, text, color } => {
self.draw_text(*x, *y, text, *color);
}
}
}
fn apply_gfxui_command(&mut self, command: &GfxUiCommand) {
match command {
GfxUiCommand::Clear { color } => self.clear(*color),
GfxUiCommand::FillRect { rect, color } => {
self.fill_rect(rect.x, rect.y, rect.w, rect.h, *color);
}
GfxUiCommand::DrawLine { x0, y0, x1, y1, color } => {
self.draw_line(*x0, *y0, *x1, *y1, *color);
}
GfxUiCommand::DrawCircle { x, y, radius, color } => {
self.draw_circle(*x, *y, *radius, *color);
}
GfxUiCommand::DrawDisc { x, y, radius, border_color, fill_color } => {
self.draw_disc(*x, *y, *radius, *border_color, *fill_color);
}
GfxUiCommand::DrawSquare { rect, border_color, fill_color } => {
self.draw_square(rect.x, rect.y, rect.w, rect.h, *border_color, *fill_color);
}
GfxUiCommand::DrawText { x, y, text, color } => {
self.draw_text(*x, *y, text, *color);
}
}
}
fn populate_layer_buckets(&mut self) {
@ -834,30 +868,6 @@ impl Gfx {
}
}
/// Applies the fade effect to the entire back buffer.
/// level: 0 (full color) to 31 (visible)
fn apply_fade_to_buffer(back: &mut [u32], level: u8, fade_color: Color) {
if level >= 31 {
return;
} // Fully visible, skip processing
let weight = level as u16;
let inv_weight = 31 - weight;
let (fr, fg, fb, fa) = unpack_rgba8888(fade_color.raw());
for px in back.iter_mut() {
let (sr, sg, sb, sa) = unpack_rgba8888(*px);
// Formula: (src * weight + fade * inv_weight) / 31
let r = ((sr as u16 * weight + fr as u16 * inv_weight) / 31) as u8;
let g = ((sg as u16 * weight + fg as u16 * inv_weight) / 31) as u8;
let b = ((sb as u16 * weight + fb as u16 * inv_weight) / 31) as u8;
let a = ((sa as u16 * weight + fa as u16 * inv_weight) / 31) as u8;
*px = pack_rgba8888(r, g, b, a);
}
}
pub fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) {
let mut cx = x;
let mut cy = y;
@ -1230,8 +1240,6 @@ mod tests {
let update = resolver.update(&scene, 0, 0);
let mut gfx = Gfx::new(16, 16, banks);
gfx.scene_fade_level = 31;
gfx.hud_fade_level = 31;
gfx.render_scene_from_cache(&cache, &update, &[0, 0, 0, 0]);
assert_eq!(gfx.back[0], Color::RED.raw());
@ -1262,8 +1270,6 @@ mod tests {
let update = resolver.update(&scene, 0, 0);
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
gfx.scene_fade_level = 31;
gfx.hud_fade_level = 31;
gfx.sprites[0] = Sprite {
glyph: Glyph { glyph_id: 0, palette_id: 4 },
@ -1331,4 +1337,22 @@ mod tests {
assert_eq!(gfx.sprites[0].layer, 1);
assert_eq!(gfx.sprites[1].glyph.glyph_id, 5);
}
#[test]
fn render_game2d_frame_packet_applies_primitive_commands() {
let mut gfx = Gfx::new(8, 8, Arc::new(MemoryBanks::default()));
let packet = Game2DFramePacket::new(
Default::default(),
vec![Gfx2dCommand::FillRect {
rect: prometeu_hal::primitives::Rect { x: 1, y: 1, w: 2, h: 2 },
color: Color::RED,
}],
);
gfx.render_game2d_frame_packet(&packet);
gfx.present();
assert_eq!(gfx.front_buffer()[1 + 8], Color::RED.raw());
assert_eq!(gfx.front_buffer()[2 + 2 * 8], Color::RED.raw());
}
}

View File

@ -11,6 +11,7 @@ use crate::touch::Touch;
use prometeu_hal::cartridge::AssetsPayloadSource;
use prometeu_hal::sprite::Sprite;
use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge};
use prometeu_hal::{Game2DFramePacket, RenderSubmission, RenderSubmissionPacket};
use std::sync::Arc;
/// Aggregate structure for all virtual hardware peripherals.
@ -67,9 +68,34 @@ impl HardwareBridge for Hardware {
self.frame_composer.emit_sprite(sprite)
}
fn render_frame(&mut self) {
fn close_game2d_packet(&self) -> Game2DFramePacket {
self.frame_composer.close_game2d_packet()
}
fn publish_render_submission(&mut self, submission: &RenderSubmission) {
match &submission.packet {
RenderSubmissionPacket::Game2D(packet) => {
if self.frame_composer.active_scene_id().is_none() {
self.gfx.render_game2d_frame_packet(packet);
} else {
self.frame_composer.render_frame(&mut self.gfx);
}
}
RenderSubmissionPacket::ShellUi(packet) => {
self.gfx.render_shell_ui_frame_packet(packet);
}
}
self.gfx.present();
}
fn render_frame(&mut self) {
if self.frame_composer.active_scene_id().is_none() {
let packet = self.frame_composer.close_game2d_packet();
self.gfx.render_game2d_frame_packet(&packet);
} else {
self.frame_composer.render_frame(&mut self.gfx);
}
}
fn has_glyph_bank(&self, bank_id: usize) -> bool {
self.gfx.glyph_banks.glyph_bank_slot(bank_id).is_some()
@ -215,8 +241,6 @@ mod tests {
let mut resolver = SceneViewportResolver::new(16, 16, 4, 4, 12, 20);
let update = resolver.update(&scene, 0, 0);
hardware.gfx.scene_fade_level = 31;
hardware.gfx.hud_fade_level = 31;
hardware.gfx.render_scene_from_cache(&cache, &update, &[0, 0, 0, 0]);
hardware.gfx.present();

View File

@ -9,7 +9,7 @@ mod touch;
pub use crate::asset::AssetManager;
pub use crate::audio::{Audio, AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};
pub use crate::frame_composer::{FrameComposer, SceneStatus, SpriteController};
pub use crate::frame_composer::{ComposerBuffer, FrameComposer, SceneStatus, SpriteController};
pub use crate::gfx::Gfx;
pub use crate::memory_banks::{
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess,

View File

@ -231,7 +231,7 @@ mod tests {
debug_info: None,
exports: vec![],
syscalls: vec![SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,

View File

@ -2,6 +2,7 @@ use crate::firmware::firmware_state::{FirmwareState, LaunchHubStep};
use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::color::Color;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::{FrameId, GfxUiCommand, RenderSubmission, ShellUiFramePacket};
use prometeu_system::CrashReport;
#[derive(Debug, Clone)]
@ -22,11 +23,8 @@ impl AppCrashesStep {
// Update peripherals for input on the crash screen
ctx.hw.pad_mut().begin_frame(ctx.signals);
// Error screen: red background, white text
ctx.hw.gfx_mut().clear(Color::RED);
// For now we just log or show something simple
// In the future, use draw_text
ctx.hw.gfx_mut().present();
let packet = ShellUiFramePacket::new(vec![GfxUiCommand::Clear { color: Color::RED }]);
ctx.hw.publish_render_submission(&RenderSubmission::shell_ui(FrameId::ZERO, packet));
// If START is pressed, return to the Hub
if ctx.hw.pad().start().down {

View File

@ -38,10 +38,6 @@ impl GameRunningStep {
let result = ctx.os.vm().tick(ctx.vm, ctx.signals, ctx.hw);
if !ctx.os.vm().logical_frame_active() {
ctx.hw.gfx_mut().present();
}
if let Some(report) = result {
let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report));
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));

View File

@ -2,6 +2,8 @@ use crate::firmware::firmware_state::{FirmwareState, LaunchHubStep};
use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::color::Color;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::primitives::Rect;
use prometeu_hal::{FrameId, GfxUiCommand, RenderSubmission, ShellUiFramePacket};
#[derive(Debug, Clone)]
pub struct SplashScreenStep {
@ -22,9 +24,6 @@ impl SplashScreenStep {
// Update peripherals for input
ctx.hw.pad_mut().begin_frame(ctx.signals);
// Clear screen
ctx.hw.gfx_mut().clear(Color::ORANGE);
// Draw expanding square
let (sw, sh) = ctx.hw.gfx().size();
let max_size = (sw.min(sh) as i32 / 2).max(1);
@ -38,8 +37,14 @@ impl SplashScreenStep {
let x = (sw as i32 - current_size) / 2;
let y = (sh as i32 - current_size) / 2;
ctx.hw.gfx_mut().fill_rect(x, y, current_size, current_size, Color::WHITE);
ctx.hw.gfx_mut().present();
let packet = ShellUiFramePacket::new(vec![
GfxUiCommand::Clear { color: Color::ORANGE },
GfxUiCommand::FillRect {
rect: Rect { x, y, w: current_size, h: current_size },
color: Color::WHITE,
},
]);
ctx.hw.publish_render_submission(&RenderSubmission::shell_ui(FrameId::ZERO, packet));
// Transition logic
// If any button is pressed at any time after the animation ends

View File

@ -286,6 +286,9 @@ pub enum Capability {
None,
System,
Gfx,
Composer,
Gfx2d,
Gfxui,
Audio,
Fs,
Log,

View File

@ -117,6 +117,9 @@ fn normalize_capabilities(capabilities: &[Capability]) -> Result<CapFlags, Cartr
Capability::None => caps::NONE,
Capability::System => caps::SYSTEM,
Capability::Gfx => caps::GFX,
Capability::Composer => caps::COMPOSER,
Capability::Gfx2d => caps::GFX2D,
Capability::Gfxui => caps::GFXUI,
Capability::Audio => caps::AUDIO,
Capability::Fs => caps::FS,
Capability::Log => caps::LOG,

View File

@ -73,13 +73,4 @@ pub trait GfxBridge {
fn sprite(&self, index: usize) -> &Sprite;
fn sprite_mut(&mut self, index: usize) -> &mut Sprite;
fn scene_fade_level(&self) -> u8;
fn set_scene_fade_level(&mut self, level: u8);
fn scene_fade_color(&self) -> Color;
fn set_scene_fade_color(&mut self, color: Color);
fn hud_fade_level(&self) -> u8;
fn set_hud_fade_level(&mut self, level: u8);
fn hud_fade_color(&self) -> Color;
fn set_hud_fade_color(&mut self, color: Color);
}

View File

@ -2,6 +2,7 @@ use crate::asset_bridge::AssetBridge;
use crate::audio_bridge::AudioBridge;
use crate::gfx_bridge::GfxBridge;
use crate::pad_bridge::PadBridge;
use crate::render_submission::{Game2DFramePacket, RenderSubmission};
use crate::sprite::Sprite;
use crate::touch_bridge::TouchBridge;
@ -11,6 +12,8 @@ pub trait HardwareBridge {
fn unbind_scene(&mut self);
fn set_camera(&mut self, x: i32, y: i32);
fn emit_sprite(&mut self, sprite: Sprite) -> bool;
fn close_game2d_packet(&self) -> Game2DFramePacket;
fn publish_render_submission(&mut self, submission: &RenderSubmission);
fn render_frame(&mut self);
fn has_glyph_bank(&self, bank_id: usize) -> bool;

View File

@ -18,7 +18,7 @@ pub use resolver::{
///
/// Each Syscall has a unique 32-bit ID. The IDs are grouped by category:
/// - **0x0xxx**: System & OS Control
/// - **0x1xxx**: Graphics (GFX)
/// - **0x1xxx**: Game 2D primitives (`gfx2d`) and Shell UI primitives (`gfxui`)
/// - **0x11xx**: Frame Composer orchestration
/// - **0x2xxx**: Reserved for legacy input syscalls (disabled for v1 VM-owned input)
/// - **0x3xxx**: Audio (PCM & Mixing)
@ -37,6 +37,13 @@ pub enum Syscall {
GfxDrawDisc = 0x1005,
GfxDrawSquare = 0x1006,
GfxDrawText = 0x1008,
GfxUiClear = 0x1201,
GfxUiFillRect = 0x1202,
GfxUiDrawLine = 0x1203,
GfxUiDrawCircle = 0x1204,
GfxUiDrawDisc = 0x1205,
GfxUiDrawSquare = 0x1206,
GfxUiDrawText = 0x1208,
ComposerBindScene = 0x1101,
ComposerUnbindScene = 0x1102,
ComposerSetCamera = 0x1103,

View File

@ -2,10 +2,13 @@ use super::CapFlags;
pub const NONE: CapFlags = 0;
pub const SYSTEM: CapFlags = 1 << 0;
pub const GFX: CapFlags = 1 << 1;
pub const COMPOSER: CapFlags = 1 << 1;
pub const AUDIO: CapFlags = 1 << 2;
pub const FS: CapFlags = 1 << 3;
pub const LOG: CapFlags = 1 << 4;
pub const ASSET: CapFlags = 1 << 5;
pub const BANK: CapFlags = 1 << 6;
pub const ALL: CapFlags = SYSTEM | GFX | AUDIO | FS | LOG | ASSET | BANK;
pub const GFX2D: CapFlags = 1 << 7;
pub const GFXUI: CapFlags = 1 << 8;
pub const GFX: CapFlags = COMPOSER | GFX2D;
pub const ALL: CapFlags = SYSTEM | COMPOSER | AUDIO | FS | LOG | ASSET | BANK | GFX2D | GFXUI;

View File

@ -4,19 +4,19 @@ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
SyscallRegistryEntry::builder(Syscall::ComposerBindScene, "composer", "bind_scene")
.args(1)
.rets(1)
.caps(caps::GFX)
.caps(caps::COMPOSER)
.cost(5),
SyscallRegistryEntry::builder(Syscall::ComposerUnbindScene, "composer", "unbind_scene")
.rets(1)
.caps(caps::GFX)
.caps(caps::COMPOSER)
.cost(2),
SyscallRegistryEntry::builder(Syscall::ComposerSetCamera, "composer", "set_camera")
.args(2)
.caps(caps::GFX)
.caps(caps::COMPOSER)
.cost(2),
SyscallRegistryEntry::builder(Syscall::ComposerEmitSprite, "composer", "emit_sprite")
.args(9)
.rets(1)
.caps(caps::GFX)
.caps(caps::COMPOSER)
.cost(5),
];

View File

@ -1,32 +1,32 @@
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
SyscallRegistryEntry::builder(Syscall::GfxClear, "gfx", "clear")
SyscallRegistryEntry::builder(Syscall::GfxClear, "gfx2d", "clear")
.args(1)
.caps(caps::GFX)
.caps(caps::GFX2D)
.cost(20),
SyscallRegistryEntry::builder(Syscall::GfxFillRect, "gfx", "fill_rect")
SyscallRegistryEntry::builder(Syscall::GfxFillRect, "gfx2d", "fill_rect")
.args(5)
.caps(caps::GFX)
.caps(caps::GFX2D)
.cost(20),
SyscallRegistryEntry::builder(Syscall::GfxDrawLine, "gfx", "draw_line")
SyscallRegistryEntry::builder(Syscall::GfxDrawLine, "gfx2d", "draw_line")
.args(5)
.caps(caps::GFX)
.caps(caps::GFX2D)
.cost(5),
SyscallRegistryEntry::builder(Syscall::GfxDrawCircle, "gfx", "draw_circle")
SyscallRegistryEntry::builder(Syscall::GfxDrawCircle, "gfx2d", "draw_circle")
.args(4)
.caps(caps::GFX)
.caps(caps::GFX2D)
.cost(5),
SyscallRegistryEntry::builder(Syscall::GfxDrawDisc, "gfx", "draw_disc")
SyscallRegistryEntry::builder(Syscall::GfxDrawDisc, "gfx2d", "draw_disc")
.args(5)
.caps(caps::GFX)
.caps(caps::GFX2D)
.cost(5),
SyscallRegistryEntry::builder(Syscall::GfxDrawSquare, "gfx", "draw_square")
SyscallRegistryEntry::builder(Syscall::GfxDrawSquare, "gfx2d", "draw_square")
.args(6)
.caps(caps::GFX)
.caps(caps::GFX2D)
.cost(5),
SyscallRegistryEntry::builder(Syscall::GfxDrawText, "gfx", "draw_text")
SyscallRegistryEntry::builder(Syscall::GfxDrawText, "gfx2d", "draw_text")
.args(4)
.caps(caps::GFX)
.caps(caps::GFX2D)
.cost(20),
];

View File

@ -0,0 +1,32 @@
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
SyscallRegistryEntry::builder(Syscall::GfxUiClear, "gfxui", "clear")
.args(1)
.caps(caps::GFXUI)
.cost(20),
SyscallRegistryEntry::builder(Syscall::GfxUiFillRect, "gfxui", "fill_rect")
.args(5)
.caps(caps::GFXUI)
.cost(20),
SyscallRegistryEntry::builder(Syscall::GfxUiDrawLine, "gfxui", "draw_line")
.args(5)
.caps(caps::GFXUI)
.cost(5),
SyscallRegistryEntry::builder(Syscall::GfxUiDrawCircle, "gfxui", "draw_circle")
.args(4)
.caps(caps::GFXUI)
.cost(5),
SyscallRegistryEntry::builder(Syscall::GfxUiDrawDisc, "gfxui", "draw_disc")
.args(5)
.caps(caps::GFXUI)
.cost(5),
SyscallRegistryEntry::builder(Syscall::GfxUiDrawSquare, "gfxui", "draw_square")
.args(6)
.caps(caps::GFXUI)
.cost(5),
SyscallRegistryEntry::builder(Syscall::GfxUiDrawText, "gfxui", "draw_text")
.args(4)
.caps(caps::GFXUI)
.cost(20),
];

View File

@ -4,6 +4,7 @@ mod bank;
mod composer;
mod fs;
mod gfx;
mod gfxui;
mod log;
mod system;
@ -13,6 +14,7 @@ pub(crate) fn all_entries() -> impl Iterator<Item = &'static SyscallRegistryEntr
system::ENTRIES
.iter()
.chain(gfx::ENTRIES.iter())
.chain(gfxui::ENTRIES.iter())
.chain(composer::ENTRIES.iter())
.chain(audio::ENTRIES.iter())
.chain(fs::ENTRIES.iter())

View File

@ -21,6 +21,13 @@ impl Syscall {
0x1005 => Some(Self::GfxDrawDisc),
0x1006 => Some(Self::GfxDrawSquare),
0x1008 => Some(Self::GfxDrawText),
0x1201 => Some(Self::GfxUiClear),
0x1202 => Some(Self::GfxUiFillRect),
0x1203 => Some(Self::GfxUiDrawLine),
0x1204 => Some(Self::GfxUiDrawCircle),
0x1205 => Some(Self::GfxUiDrawDisc),
0x1206 => Some(Self::GfxUiDrawSquare),
0x1208 => Some(Self::GfxUiDrawText),
0x1101 => Some(Self::ComposerBindScene),
0x1102 => Some(Self::ComposerUnbindScene),
0x1103 => Some(Self::ComposerSetCamera),
@ -70,6 +77,13 @@ impl Syscall {
Self::GfxDrawDisc => "GfxDrawDisc",
Self::GfxDrawSquare => "GfxDrawSquare",
Self::GfxDrawText => "GfxDrawText",
Self::GfxUiClear => "GfxUiClear",
Self::GfxUiFillRect => "GfxUiFillRect",
Self::GfxUiDrawLine => "GfxUiDrawLine",
Self::GfxUiDrawCircle => "GfxUiDrawCircle",
Self::GfxUiDrawDisc => "GfxUiDrawDisc",
Self::GfxUiDrawSquare => "GfxUiDrawSquare",
Self::GfxUiDrawText => "GfxUiDrawText",
Self::ComposerBindScene => "ComposerBindScene",
Self::ComposerUnbindScene => "ComposerUnbindScene",
Self::ComposerSetCamera => "ComposerSetCamera",

View File

@ -39,23 +39,23 @@ fn every_syscall_has_metadata() {
#[test]
fn resolver_returns_expected_id_for_known_identity() {
let id = resolve_syscall("gfx", "clear", 1).expect("known identity must resolve");
let id = resolve_syscall("gfx2d", "clear", 1).expect("known identity must resolve");
assert_eq!(id.id, 0x1001);
assert_eq!(id.meta.module, "gfx");
assert_eq!(id.meta.module, "gfx2d");
assert_eq!(id.meta.name, "clear");
assert_eq!(id.meta.version, 1);
}
#[test]
fn resolver_rejects_unknown_identity() {
let res = resolve_syscall("gfx", "nonexistent", 1);
let res = resolve_syscall("gfx2d", "nonexistent", 1);
assert!(res.is_none());
let requested = [SyscallIdentity { module: "gfx", name: "nonexistent", version: 1 }];
let requested = [SyscallIdentity { module: "gfx2d", name: "nonexistent", version: 1 }];
let err = resolve_program_syscalls(&requested, 0).unwrap_err();
match err {
LoadError::UnknownSyscall { module, name, version } => {
assert_eq!(module, "gfx");
assert_eq!(module, "gfx2d");
assert_eq!(name, "nonexistent");
assert_eq!(version, 1);
}
@ -65,23 +65,23 @@ fn resolver_rejects_unknown_identity() {
#[test]
fn resolver_rejects_removed_legacy_gfx_set_sprite_identity() {
assert!(resolve_syscall("gfx", "set_sprite", 1).is_none());
assert!(resolve_syscall("gfx2d", "set_sprite", 1).is_none());
let requested = [SyscallIdentity { module: "gfx", name: "set_sprite", version: 1 }];
let requested = [SyscallIdentity { module: "gfx2d", name: "set_sprite", version: 1 }];
let err = resolve_program_syscalls(&requested, caps::ALL).unwrap_err();
assert_eq!(
err,
LoadError::UnknownSyscall { module: "gfx".into(), name: "set_sprite".into(), version: 1 }
LoadError::UnknownSyscall { module: "gfx2d".into(), name: "set_sprite".into(), version: 1 }
);
}
#[test]
fn resolver_enforces_capabilities() {
let requested = [SyscallIdentity { module: "gfx", name: "clear", version: 1 }];
let requested = [SyscallIdentity { module: "gfx2d", name: "clear", version: 1 }];
let err = resolve_program_syscalls(&requested, 0).unwrap_err();
match err {
LoadError::MissingCapability { required, provided, module, name, version } => {
assert_eq!(module, "gfx");
assert_eq!(module, "gfx2d");
assert_eq!(name, "clear");
assert_eq!(version, 1);
assert_ne!(required, 0);
@ -90,15 +90,30 @@ fn resolver_enforces_capabilities() {
_ => panic!("expected MissingCapability error"),
}
let ok = resolve_program_syscalls(&requested, caps::GFX).expect("must resolve with caps");
let ok = resolve_program_syscalls(&requested, caps::GFX2D).expect("must resolve with caps");
assert_eq!(ok.len(), 1);
assert_eq!(ok[0].id, 0x1001);
}
#[test]
fn resolver_exposes_split_render_capabilities() {
let composer = resolve_syscall("composer", "bind_scene", 1).expect("composer resolves");
assert_eq!(composer.meta.caps, caps::COMPOSER);
let gfx2d = resolve_syscall("gfx2d", "clear", 1).expect("gfx2d resolves");
assert_eq!(gfx2d.meta.caps, caps::GFX2D);
let gfxui = resolve_syscall("gfxui", "clear", 1).expect("gfxui resolves");
assert_eq!(gfxui.id, 0x1201);
assert_eq!(gfxui.meta.caps, caps::GFXUI);
assert!(resolve_syscall("gfx", "clear", 1).is_none());
}
#[test]
fn declared_resolver_returns_expected_id_for_known_identity() {
let declared = [prometeu_bytecode::SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
@ -106,7 +121,7 @@ fn declared_resolver_returns_expected_id_for_known_identity() {
}];
let ok =
resolve_declared_program_syscalls(&declared, caps::GFX).expect("must resolve with ABI");
resolve_declared_program_syscalls(&declared, caps::GFX2D).expect("must resolve with ABI");
assert_eq!(ok.len(), 1);
assert_eq!(ok[0].id, 0x1001);
}
@ -114,18 +129,18 @@ fn declared_resolver_returns_expected_id_for_known_identity() {
#[test]
fn declared_resolver_rejects_unknown_identity() {
let declared = [prometeu_bytecode::SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "nonexistent".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
}];
let err = resolve_declared_program_syscalls(&declared, caps::GFX).unwrap_err();
let err = resolve_declared_program_syscalls(&declared, caps::GFX2D).unwrap_err();
assert_eq!(
err,
DeclaredLoadError::UnknownSyscall {
module: "gfx".into(),
module: "gfx2d".into(),
name: "nonexistent".into(),
version: 1,
}
@ -135,7 +150,7 @@ fn declared_resolver_rejects_unknown_identity() {
#[test]
fn declared_resolver_rejects_missing_capability() {
let declared = [prometeu_bytecode::SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
@ -146,9 +161,9 @@ fn declared_resolver_rejects_missing_capability() {
assert_eq!(
err,
DeclaredLoadError::MissingCapability {
required: caps::GFX,
required: caps::GFX2D,
provided: caps::NONE,
module: "gfx".into(),
module: "gfx2d".into(),
name: "clear".into(),
version: 1,
}
@ -158,18 +173,18 @@ fn declared_resolver_rejects_missing_capability() {
#[test]
fn declared_resolver_rejects_abi_mismatch() {
let declared = [prometeu_bytecode::SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "draw_line".into(),
version: 1,
arg_slots: 4,
ret_slots: 0,
}];
let err = resolve_declared_program_syscalls(&declared, caps::GFX).unwrap_err();
let err = resolve_declared_program_syscalls(&declared, caps::GFX2D).unwrap_err();
assert_eq!(
err,
DeclaredLoadError::AbiMismatch {
module: "gfx".into(),
module: "gfx2d".into(),
name: "draw_line".into(),
version: 1,
declared_arg_slots: 4,
@ -386,7 +401,7 @@ fn declared_resolver_accepts_mixed_status_first_surface_as_a_single_module() {
];
let resolved =
resolve_declared_program_syscalls(&declared, caps::GFX | caps::AUDIO | caps::ASSET)
resolve_declared_program_syscalls(&declared, caps::COMPOSER | caps::AUDIO | caps::ASSET)
.expect("mixed status-first surface must resolve together");
assert_eq!(resolved.len(), declared.len());

View File

@ -1,8 +1,9 @@
use crate::{CrashReport, SystemOS};
use prometeu_hal::color::Color;
use prometeu_hal::gfx_bridge::GfxBridge;
use prometeu_hal::primitives::Rect;
use prometeu_hal::{HardwareBridge, InputSignals};
use prometeu_hal::{
FrameId, GfxUiCommand, HardwareBridge, InputSignals, RenderSubmission, ShellUiFramePacket,
};
use prometeu_vm::VirtualMachine;
const SHELL_A_BUTTON: Rect = Rect { x: 112, y: 108, w: 112, h: 48 };
@ -101,8 +102,6 @@ impl PrometeuHub {
os: &mut SystemOS,
hw: &mut dyn HardwareBridge,
) -> Option<SystemProfileAction> {
hw.gfx_mut().clear(Color::INDIGO);
let in_shell = os.windows().focused_window().is_some();
if !in_shell {
@ -127,14 +126,13 @@ impl PrometeuHub {
let pointer_x = hw.touch().x();
let pointer_y = hw.touch().y();
if os.windows().window_count() == 0 {
render_home(hw.gfx_mut(), pointer_x, pointer_y);
return;
}
let packet = if os.windows().window_count() == 0 {
render_home_packet(pointer_x, pointer_y)
} else {
let mut commands = Vec::new();
for window in os.windows().windows() {
render_shell_window(
hw.gfx_mut(),
append_shell_window_commands(
&mut commands,
window.viewport,
window.color,
&window.title,
@ -142,6 +140,9 @@ impl PrometeuHub {
pointer_y,
);
}
ShellUiFramePacket::new(commands)
};
hw.publish_render_submission(&RenderSubmission::shell_ui(FrameId::ZERO, packet));
}
pub fn update_shell_profile(
@ -163,7 +164,6 @@ impl PrometeuHub {
}
self.render(os, hw);
hw.gfx_mut().present();
SystemProfileUpdate { crash, action }
}
@ -192,15 +192,29 @@ fn activation_input_down(hw: &dyn HardwareBridge) -> bool {
hw.touch().f().down || hw.pad().a().down || hw.pad().b().down || hw.pad().start().down
}
fn render_home(gfx: &mut dyn GfxBridge, pointer_x: i32, pointer_y: i32) {
gfx.fill_rect(VIEWPORT.x, VIEWPORT.y, VIEWPORT.w, VIEWPORT.h, COLOR_BG);
draw_panel(gfx, TITLE_PANEL, COLOR_PANEL_DARK, COLOR_BORDER);
draw_buffer_text(gfx, TITLE_PANEL.x + 58, TITLE_PANEL.y + 9, "PROMETEU HUB", COLOR_TEXT);
draw_panel(gfx, HOME_PANEL, COLOR_PANEL, COLOR_BORDER);
draw_buffer_text(gfx, HOME_PANEL.x + 26, HOME_PANEL.y + 22, "Native Shell Apps", COLOR_TEXT);
fn render_home_packet(pointer_x: i32, pointer_y: i32) -> ShellUiFramePacket {
let mut commands = Vec::new();
commands.push(GfxUiCommand::Clear { color: COLOR_BG });
fill_rect(&mut commands, VIEWPORT, COLOR_BG);
draw_panel(&mut commands, TITLE_PANEL, COLOR_PANEL_DARK, COLOR_BORDER);
draw_buffer_text(
gfx,
&mut commands,
TITLE_PANEL.x + 58,
TITLE_PANEL.y + 9,
"PROMETEU HUB",
COLOR_TEXT,
);
draw_panel(&mut commands, HOME_PANEL, COLOR_PANEL, COLOR_BORDER);
draw_buffer_text(
&mut commands,
HOME_PANEL.x + 26,
HOME_PANEL.y + 22,
"Native Shell Apps",
COLOR_TEXT,
);
draw_buffer_text(
&mut commands,
HOME_PANEL.x + 26,
HOME_PANEL.y + 40,
"first retro ui slice",
@ -209,50 +223,105 @@ fn render_home(gfx: &mut dyn GfxBridge, pointer_x: i32, pointer_y: i32) {
for button in HOME_BUTTONS {
let hovered = rect_contains(button.rect, pointer_x, pointer_y);
draw_button(gfx, button.rect, button.label, hovered);
}
draw_button(&mut commands, button.rect, button.label, hovered);
}
fn render_shell_window(
gfx: &mut dyn GfxBridge,
ShellUiFramePacket::new(commands)
}
fn append_shell_window_commands(
commands: &mut Vec<GfxUiCommand>,
viewport: Rect,
color: Color,
title: &str,
pointer_x: i32,
pointer_y: i32,
) {
gfx.fill_rect(VIEWPORT.x, VIEWPORT.y, VIEWPORT.w, VIEWPORT.h, COLOR_BG);
draw_panel(gfx, viewport, COLOR_PANEL_DARK, COLOR_BORDER);
gfx.fill_rect(viewport.x + 4, viewport.y + 24, viewport.w - 8, viewport.h - 28, color);
draw_buffer_text(gfx, viewport.x + 10, viewport.y + 8, title, COLOR_TEXT);
draw_buffer_text(gfx, viewport.x + 18, viewport.y + 44, "native fake shell", COLOR_PANEL_DARK);
fill_rect(commands, VIEWPORT, COLOR_BG);
draw_panel(commands, viewport, COLOR_PANEL_DARK, COLOR_BORDER);
fill_rect(
commands,
Rect { x: viewport.x + 4, y: viewport.y + 24, w: viewport.w - 8, h: viewport.h - 28 },
color,
);
draw_buffer_text(commands, viewport.x + 10, viewport.y + 8, title, COLOR_TEXT);
draw_buffer_text(
commands,
viewport.x + 18,
viewport.y + 44,
"native fake shell",
COLOR_PANEL_DARK,
);
let close_hovered = rect_contains(CLOSE_BUTTON, pointer_x, pointer_y);
let close_fill = if close_hovered { COLOR_HILITE } else { COLOR_BUTTON };
gfx.fill_rect(CLOSE_BUTTON.x, CLOSE_BUTTON.y, CLOSE_BUTTON.w, CLOSE_BUTTON.h, close_fill);
gfx.draw_rect(CLOSE_BUTTON.x, CLOSE_BUTTON.y, CLOSE_BUTTON.w, CLOSE_BUTTON.h, COLOR_BORDER);
draw_buffer_text(gfx, CLOSE_BUTTON.x + 5, CLOSE_BUTTON.y + 4, "x", COLOR_TEXT);
fill_rect(commands, CLOSE_BUTTON, close_fill);
draw_rect(commands, CLOSE_BUTTON, COLOR_BORDER);
draw_buffer_text(commands, CLOSE_BUTTON.x + 5, CLOSE_BUTTON.y + 4, "x", COLOR_TEXT);
}
fn draw_panel(gfx: &mut dyn GfxBridge, rect: Rect, fill: Color, border: Color) {
gfx.fill_rect(rect.x, rect.y, rect.w, rect.h, fill);
gfx.draw_rect(rect.x, rect.y, rect.w, rect.h, border);
gfx.draw_rect(rect.x + 2, rect.y + 2, rect.w - 4, rect.h - 4, COLOR_MUTED);
fn fill_rect(commands: &mut Vec<GfxUiCommand>, rect: Rect, color: Color) {
commands.push(GfxUiCommand::FillRect { rect, color });
}
fn draw_button(gfx: &mut dyn GfxBridge, rect: Rect, label: &str, hovered: bool) {
fn draw_rect(commands: &mut Vec<GfxUiCommand>, rect: Rect, color: Color) {
commands.push(GfxUiCommand::DrawLine {
x0: rect.x,
y0: rect.y,
x1: rect.x + rect.w - 1,
y1: rect.y,
color,
});
commands.push(GfxUiCommand::DrawLine {
x0: rect.x,
y0: rect.y + rect.h - 1,
x1: rect.x + rect.w - 1,
y1: rect.y + rect.h - 1,
color,
});
commands.push(GfxUiCommand::DrawLine {
x0: rect.x,
y0: rect.y,
x1: rect.x,
y1: rect.y + rect.h - 1,
color,
});
commands.push(GfxUiCommand::DrawLine {
x0: rect.x + rect.w - 1,
y0: rect.y,
x1: rect.x + rect.w - 1,
y1: rect.y + rect.h - 1,
color,
});
}
fn draw_panel(commands: &mut Vec<GfxUiCommand>, rect: Rect, fill: Color, border: Color) {
fill_rect(commands, rect, fill);
draw_rect(commands, rect, border);
draw_rect(
commands,
Rect { x: rect.x + 2, y: rect.y + 2, w: rect.w - 4, h: rect.h - 4 },
COLOR_MUTED,
);
}
fn draw_button(commands: &mut Vec<GfxUiCommand>, rect: Rect, label: &str, hovered: bool) {
let fill = if hovered { COLOR_BUTTON_ACTIVE } else { COLOR_BUTTON };
let border = if hovered { COLOR_HILITE } else { COLOR_BORDER };
gfx.fill_rect(rect.x, rect.y, rect.w, rect.h, fill);
gfx.draw_rect(rect.x, rect.y, rect.w, rect.h, border);
gfx.draw_rect(rect.x + 3, rect.y + 3, rect.w - 6, rect.h - 6, COLOR_MUTED);
gfx.fill_rect(rect.x + 12, rect.y + 12, 16, 24, border);
gfx.draw_rect(rect.x + 34, rect.y + 12, 62, 24, COLOR_PANEL_DARK);
draw_buffer_text(gfx, rect.x + 40, rect.y + 21, label, COLOR_TEXT);
fill_rect(commands, rect, fill);
draw_rect(commands, rect, border);
draw_rect(
commands,
Rect { x: rect.x + 3, y: rect.y + 3, w: rect.w - 6, h: rect.h - 6 },
COLOR_MUTED,
);
fill_rect(commands, Rect { x: rect.x + 12, y: rect.y + 12, w: 16, h: 24 }, border);
draw_rect(commands, Rect { x: rect.x + 34, y: rect.y + 12, w: 62, h: 24 }, COLOR_PANEL_DARK);
draw_buffer_text(commands, rect.x + 40, rect.y + 21, label, COLOR_TEXT);
}
fn draw_buffer_text(gfx: &mut dyn GfxBridge, x: i32, y: i32, text: &str, color: Color) {
gfx.draw_text(x, y, text, color);
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 });
}
#[cfg(test)]
@ -321,6 +390,16 @@ mod tests {
assert_eq!(HOME_BUTTONS[1].app, NativeShellApp::ShellB);
}
#[test]
fn home_render_produces_shell_ui_packet_commands() {
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| {
matches!(command, GfxUiCommand::DrawText { text, .. } if text == "PROMETEU HUB")
}));
}
#[test]
fn shell_close_button_sits_inside_shell_frame() {
assert!(rect_contains(SHELL_FRAME, CLOSE_BUTTON.x, CLOSE_BUTTON.y));

View File

@ -7,12 +7,13 @@ use prometeu_hal::asset::{AssetId, AssetOpStatus, BankType, SlotRef};
use prometeu_hal::color::Color;
use prometeu_hal::glyph::Glyph;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::primitives::Rect;
use prometeu_hal::sprite::Sprite;
use prometeu_hal::syscalls::Syscall;
use prometeu_hal::vm_fault::VmFault;
use prometeu_hal::{
AudioOpStatus, ComposerOpStatus, HostContext, HostReturn, NativeInterface, SyscallId,
expect_bool, expect_int,
AudioOpStatus, ComposerOpStatus, Gfx2dCommand, GfxUiCommand, HostContext, HostReturn,
NativeInterface, SyscallId, expect_bool, expect_int,
};
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
@ -185,6 +186,19 @@ impl NativeInterface for VmRuntimeHost<'_> {
Syscall::SystemRunCart => unreachable!(),
Syscall::GfxClear => {
let color = self.get_color(expect_int(args, 0)?)?;
self.gfx2d_commands.push(Gfx2dCommand::Clear { color });
hw.gfx_mut().clear(color);
Ok(())
}
Syscall::GfxUiClear => {
if self.current_cartridge_app_mode != AppMode::Shell {
return Err(VmFault::Trap(
TRAP_INVALID_SYSCALL,
format!("{} is only available to the Shell profile", syscall.name()),
));
}
let color = self.get_color(expect_int(args, 0)?)?;
self.gfxui_commands.push(GfxUiCommand::Clear { color });
hw.gfx_mut().clear(color);
Ok(())
}
@ -194,6 +208,25 @@ impl NativeInterface for VmRuntimeHost<'_> {
let w = expect_int(args, 2)? as i32;
let h = expect_int(args, 3)? as i32;
let color = self.get_color(expect_int(args, 4)?)?;
self.gfx2d_commands
.push(Gfx2dCommand::FillRect { rect: Rect { x, y, w, h }, color });
hw.gfx_mut().fill_rect(x, y, w, h, color);
Ok(())
}
Syscall::GfxUiFillRect => {
if self.current_cartridge_app_mode != AppMode::Shell {
return Err(VmFault::Trap(
TRAP_INVALID_SYSCALL,
format!("{} is only available to the Shell profile", syscall.name()),
));
}
let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? as i32;
let w = expect_int(args, 2)? as i32;
let h = expect_int(args, 3)? as i32;
let color = self.get_color(expect_int(args, 4)?)?;
self.gfxui_commands
.push(GfxUiCommand::FillRect { rect: Rect { x, y, w, h }, color });
hw.gfx_mut().fill_rect(x, y, w, h, color);
Ok(())
}
@ -203,6 +236,35 @@ impl NativeInterface for VmRuntimeHost<'_> {
let x2 = expect_int(args, 2)? as i32;
let y2 = expect_int(args, 3)? as i32;
let color = self.get_color(expect_int(args, 4)?)?;
self.gfx2d_commands.push(Gfx2dCommand::DrawLine {
x0: x1,
y0: y1,
x1: x2,
y1: y2,
color,
});
hw.gfx_mut().draw_line(x1, y1, x2, y2, color);
Ok(())
}
Syscall::GfxUiDrawLine => {
if self.current_cartridge_app_mode != AppMode::Shell {
return Err(VmFault::Trap(
TRAP_INVALID_SYSCALL,
format!("{} is only available to the Shell profile", syscall.name()),
));
}
let x1 = expect_int(args, 0)? as i32;
let y1 = expect_int(args, 1)? as i32;
let x2 = expect_int(args, 2)? as i32;
let y2 = expect_int(args, 3)? as i32;
let color = self.get_color(expect_int(args, 4)?)?;
self.gfxui_commands.push(GfxUiCommand::DrawLine {
x0: x1,
y0: y1,
x1: x2,
y1: y2,
color,
});
hw.gfx_mut().draw_line(x1, y1, x2, y2, color);
Ok(())
}
@ -211,6 +273,22 @@ impl NativeInterface for VmRuntimeHost<'_> {
let y = expect_int(args, 1)? as i32;
let r = expect_int(args, 2)? as i32;
let color = self.get_color(expect_int(args, 3)?)?;
self.gfx2d_commands.push(Gfx2dCommand::DrawCircle { x, y, radius: r, color });
hw.gfx_mut().draw_circle(x, y, r, color);
Ok(())
}
Syscall::GfxUiDrawCircle => {
if self.current_cartridge_app_mode != AppMode::Shell {
return Err(VmFault::Trap(
TRAP_INVALID_SYSCALL,
format!("{} is only available to the Shell profile", syscall.name()),
));
}
let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? as i32;
let r = expect_int(args, 2)? as i32;
let color = self.get_color(expect_int(args, 3)?)?;
self.gfxui_commands.push(GfxUiCommand::DrawCircle { x, y, radius: r, color });
hw.gfx_mut().draw_circle(x, y, r, color);
Ok(())
}
@ -220,6 +298,35 @@ impl NativeInterface for VmRuntimeHost<'_> {
let r = expect_int(args, 2)? as i32;
let border_color = self.get_color(expect_int(args, 3)?)?;
let fill_color = self.get_color(expect_int(args, 4)?)?;
self.gfx2d_commands.push(Gfx2dCommand::DrawDisc {
x,
y,
radius: r,
border_color,
fill_color,
});
hw.gfx_mut().draw_disc(x, y, r, border_color, fill_color);
Ok(())
}
Syscall::GfxUiDrawDisc => {
if self.current_cartridge_app_mode != AppMode::Shell {
return Err(VmFault::Trap(
TRAP_INVALID_SYSCALL,
format!("{} is only available to the Shell profile", syscall.name()),
));
}
let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? as i32;
let r = expect_int(args, 2)? as i32;
let border_color = self.get_color(expect_int(args, 3)?)?;
let fill_color = self.get_color(expect_int(args, 4)?)?;
self.gfxui_commands.push(GfxUiCommand::DrawDisc {
x,
y,
radius: r,
border_color,
fill_color,
});
hw.gfx_mut().draw_disc(x, y, r, border_color, fill_color);
Ok(())
}
@ -230,6 +337,32 @@ impl NativeInterface for VmRuntimeHost<'_> {
let h = expect_int(args, 3)? as i32;
let border_color = self.get_color(expect_int(args, 4)?)?;
let fill_color = self.get_color(expect_int(args, 5)?)?;
self.gfx2d_commands.push(Gfx2dCommand::DrawSquare {
rect: Rect { x, y, w, h },
border_color,
fill_color,
});
hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color);
Ok(())
}
Syscall::GfxUiDrawSquare => {
if self.current_cartridge_app_mode != AppMode::Shell {
return Err(VmFault::Trap(
TRAP_INVALID_SYSCALL,
format!("{} is only available to the Shell profile", syscall.name()),
));
}
let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? as i32;
let w = expect_int(args, 2)? as i32;
let h = expect_int(args, 3)? as i32;
let border_color = self.get_color(expect_int(args, 4)?)?;
let fill_color = self.get_color(expect_int(args, 5)?)?;
self.gfxui_commands.push(GfxUiCommand::DrawSquare {
rect: Rect { x, y, w, h },
border_color,
fill_color,
});
hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color);
Ok(())
}
@ -238,6 +371,22 @@ impl NativeInterface for VmRuntimeHost<'_> {
let y = expect_int(args, 1)? as i32;
let msg = expect_string(args, 2, "message")?;
let color = self.get_color(expect_int(args, 3)?)?;
self.gfx2d_commands.push(Gfx2dCommand::DrawText { x, y, text: msg.clone(), color });
hw.gfx_mut().draw_text(x, y, &msg, color);
Ok(())
}
Syscall::GfxUiDrawText => {
if self.current_cartridge_app_mode != AppMode::Shell {
return Err(VmFault::Trap(
TRAP_INVALID_SYSCALL,
format!("{} is only available to the Shell profile", syscall.name()),
));
}
let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? as i32;
let msg = expect_string(args, 2, "message")?;
let color = self.get_color(expect_int(args, 3)?)?;
self.gfxui_commands.push(GfxUiCommand::DrawText { x, y, text: msg.clone(), color });
hw.gfx_mut().draw_text(x, y, &msg, color);
Ok(())
}

View File

@ -22,6 +22,9 @@ impl VirtualMachineRuntime {
current_cartridge_title: String::new(),
current_cartridge_app_version: String::new(),
current_cartridge_app_mode: AppMode::Game,
render_manager: RenderManager::new(AppMode::Game),
gfx2d_commands: Vec::new(),
gfxui_commands: Vec::new(),
logs_written_this_frame: HashMap::new(),
atomic_telemetry,
last_crash_report: None,
@ -116,6 +119,9 @@ impl VirtualMachineRuntime {
self.current_cartridge_title.clear();
self.current_cartridge_app_version.clear();
self.current_cartridge_app_mode = AppMode::Game;
self.render_manager = RenderManager::new(AppMode::Game);
self.gfx2d_commands.clear();
self.gfxui_commands.clear();
self.logs_written_this_frame.clear();
self.last_crash_report = None;
@ -148,6 +154,7 @@ impl VirtualMachineRuntime {
self.current_cartridge_title = cartridge.title.clone();
self.current_cartridge_app_version = cartridge.app_version.clone();
self.current_cartridge_app_mode = cartridge.app_mode;
self.render_manager.set_active_app_mode(cartridge.app_mode);
Ok(())
}
Err(e) => {

View File

@ -1,5 +1,6 @@
mod dispatch;
mod lifecycle;
pub mod render_manager;
#[cfg(test)]
mod tests;
mod tick;
@ -9,7 +10,9 @@ use prometeu_bytecode::string_materialization_count;
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::log::LogService;
use prometeu_hal::telemetry::{AtomicTelemetry, CertificationConfig, Certifier};
use prometeu_hal::{Gfx2dCommand, GfxUiCommand};
use prometeu_vm::VirtualMachine;
pub use render_manager::RenderManager;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::AtomicU32;
@ -25,6 +28,9 @@ pub struct VirtualMachineRuntime {
pub current_cartridge_title: String,
pub current_cartridge_app_version: String,
pub current_cartridge_app_mode: AppMode,
pub render_manager: RenderManager,
pub gfx2d_commands: Vec<Gfx2dCommand>,
pub gfxui_commands: Vec<GfxUiCommand>,
pub logs_written_this_frame: HashMap<u32, u32>,
pub atomic_telemetry: Arc<AtomicTelemetry>,
pub last_crash_report: Option<CrashReport>,

View File

@ -0,0 +1,188 @@
use prometeu_hal::app_mode::AppMode;
use prometeu_hal::{
FrameId, Game2DFramePacket, RenderSubmission, RenderSubmissionPacket, ShellUiFramePacket,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderTransitionState {
Idle,
Pending { from: AppMode, to: AppMode },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderSubmissionError {
PacketAppModeMismatch { active: AppMode },
}
pub trait RenderSurface {
fn consume_submission(&mut self, submission: &RenderSubmission);
}
#[derive(Debug, Clone)]
pub struct RenderManager {
active_app_mode: AppMode,
next_frame_id: FrameId,
latest_complete_submission: Option<RenderSubmission>,
transition_state: RenderTransitionState,
}
impl RenderManager {
pub fn new(active_app_mode: AppMode) -> Self {
Self {
active_app_mode,
next_frame_id: FrameId::ZERO,
latest_complete_submission: None,
transition_state: RenderTransitionState::Idle,
}
}
pub fn active_app_mode(&self) -> AppMode {
self.active_app_mode
}
pub fn transition_state(&self) -> RenderTransitionState {
self.transition_state
}
pub fn latest_complete_submission(&self) -> Option<&RenderSubmission> {
self.latest_complete_submission.as_ref()
}
pub fn set_active_app_mode(&mut self, app_mode: AppMode) {
if self.active_app_mode == app_mode {
return;
}
let previous = self.active_app_mode;
self.active_app_mode = app_mode;
self.transition_state = RenderTransitionState::Pending { from: previous, to: app_mode };
}
pub fn acknowledge_transition(&mut self) {
self.transition_state = RenderTransitionState::Idle;
}
pub fn close_frame_with_packet(
&mut self,
packet: RenderSubmissionPacket,
) -> Result<&RenderSubmission, RenderSubmissionError> {
let frame_id = self.next_frame_id;
let submission = match (self.active_app_mode, packet) {
(AppMode::Game, RenderSubmissionPacket::Game2D(packet)) => {
RenderSubmission::game2d(frame_id, packet)
}
(AppMode::Shell, RenderSubmissionPacket::ShellUi(packet)) => {
RenderSubmission::shell_ui(frame_id, packet)
}
(active, _) => {
return Err(RenderSubmissionError::PacketAppModeMismatch { active });
}
};
self.next_frame_id = self.next_frame_id.next();
self.latest_complete_submission = Some(submission);
Ok(self.latest_complete_submission.as_ref().expect("submission was just stored"))
}
pub fn close_compat_frame(&mut self) -> &RenderSubmission {
let packet = match self.active_app_mode {
AppMode::Game => RenderSubmissionPacket::Game2D(Game2DFramePacket::default()),
AppMode::Shell => RenderSubmissionPacket::ShellUi(ShellUiFramePacket::new(Vec::new())),
};
self.close_frame_with_packet(packet).expect("compat packet matches active app mode")
}
pub fn publish_latest<S: RenderSurface>(&self, surface: &mut S) -> bool {
let Some(submission) = self.latest_complete_submission.as_ref() else {
return false;
};
surface.consume_submission(submission);
true
}
}
impl Default for RenderManager {
fn default() -> Self {
Self::new(AppMode::Game)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct RecordingSurface {
seen: Vec<FrameId>,
}
impl RenderSurface for RecordingSurface {
fn consume_submission(&mut self, submission: &RenderSubmission) {
self.seen.push(submission.frame_id);
}
}
#[test]
fn frame_closure_assigns_monotonic_frame_ids() {
let mut manager = RenderManager::new(AppMode::Game);
let first = manager.close_compat_frame().frame_id;
let second = manager.close_compat_frame().frame_id;
assert_eq!(first, FrameId::new(0));
assert_eq!(second, FrameId::new(1));
}
#[test]
fn latest_complete_submission_wins_without_queue_growth() {
let mut manager = RenderManager::new(AppMode::Game);
manager.close_compat_frame();
manager.close_compat_frame();
let latest = manager.latest_complete_submission().expect("latest submission");
assert_eq!(latest.frame_id, FrameId::new(1));
}
#[test]
fn rejects_packet_that_does_not_match_active_app_mode() {
let mut manager = RenderManager::new(AppMode::Game);
let err = manager
.close_frame_with_packet(RenderSubmissionPacket::ShellUi(ShellUiFramePacket::new(
Vec::new(),
)))
.expect_err("shell packet is invalid in game mode");
assert_eq!(err, RenderSubmissionError::PacketAppModeMismatch { active: AppMode::Game });
}
#[test]
fn app_mode_switch_records_noop_transition_placeholder() {
let mut manager = RenderManager::new(AppMode::Game);
manager.set_active_app_mode(AppMode::Shell);
assert_eq!(manager.active_app_mode(), AppMode::Shell);
assert_eq!(
manager.transition_state(),
RenderTransitionState::Pending { from: AppMode::Game, to: AppMode::Shell }
);
manager.acknowledge_transition();
assert_eq!(manager.transition_state(), RenderTransitionState::Idle);
}
#[test]
fn publish_latest_hands_submission_to_surface() {
let mut manager = RenderManager::new(AppMode::Game);
let mut surface = RecordingSurface::default();
assert!(!manager.publish_latest(&mut surface));
manager.close_compat_frame();
assert!(manager.publish_latest(&mut surface));
assert_eq!(surface.seen, vec![FrameId::new(0)]);
}
}

View File

@ -195,7 +195,7 @@ fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() {
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
@ -221,7 +221,7 @@ fn initialize_vm_succeeds_when_cartridge_capabilities_cover_hostcalls() {
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
@ -253,7 +253,7 @@ fn tick_returns_error_when_vm_ends_slice_with_trap() {
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
@ -304,7 +304,7 @@ fn tick_system_profile_rejects_gfx_game_surface() {
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
@ -388,6 +388,104 @@ fn tick_system_profile_rejects_composer_game_surface() {
}
}
#[test]
fn tick_game_profile_rejects_gfxui_shell_surface() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut fs = VirtualFS::new();
let mut fs_state = FsState::Unmounted;
let mut memcard = MemcardService::new();
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfxui".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
}],
);
let cartridge = cartridge_with_program_and_mode(program, caps::GFXUI, AppMode::Game);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime
.tick(
&mut log_service,
&mut fs,
&mut fs_state,
&mut memcard,
&mut open_files,
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
)
.expect("game gfxui call must trap");
match report {
CrashReport::VmTrap { trap } => {
assert_eq!(trap.code, prometeu_bytecode::TRAP_INVALID_SYSCALL);
assert!(trap.message.contains("GfxUiClear is only available to the Shell profile"));
}
other => panic!("expected VmTrap crash report, got {:?}", other),
}
}
#[test]
fn tick_shell_profile_closes_gfxui_commands_into_shell_packet() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut fs = VirtualFS::new();
let mut fs_state = FsState::Unmounted;
let mut memcard = MemcardService::new();
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let mut hardware = Hardware::new();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 287454020\nHOSTCALL 0\nFRAME_SYNC\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfxui".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
}],
);
let cartridge = cartridge_with_program_and_mode(program, caps::GFXUI, AppMode::Shell);
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(
&mut log_service,
&mut fs,
&mut fs_state,
&mut memcard,
&mut open_files,
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
);
assert!(report.is_none());
let submission =
runtime.render_manager.latest_complete_submission().expect("closed submission");
let prometeu_hal::RenderSubmissionPacket::ShellUi(packet) = &submission.packet else {
panic!("expected ShellUi submission");
};
assert_eq!(packet.commands.len(), 1);
assert!(matches!(packet.commands[0], prometeu_hal::GfxUiCommand::Clear { .. }));
assert_eq!(hardware.gfx.front_buffer()[0], Color::from_raw(0x11223344).raw());
}
#[test]
fn tick_system_profile_rejects_bank_game_surface() {
let mut runtime = VirtualMachineRuntime::new(None);
@ -556,7 +654,6 @@ fn tick_renders_bound_eight_pixel_scene_through_frame_composer_path() {
);
assert!(report.is_none(), "frame render path must not crash");
hardware.gfx.present();
assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw());
}
@ -614,8 +711,6 @@ fn tick_renders_scene_through_public_composer_syscalls() {
let mut slots = [None; 16];
slots[0] = Some(0);
glyph_slot_index.rebuild_from_slots(&slots);
hardware.gfx.scene_fade_level = 31;
hardware.gfx.hud_fade_level = 31;
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(
@ -632,7 +727,6 @@ fn tick_renders_scene_through_public_composer_syscalls() {
assert!(report.is_none(), "public composer path must not crash");
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::Ok as i64)]);
hardware.gfx.present();
assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw());
}
@ -655,7 +749,7 @@ fn tick_draw_text_survives_no_scene_frame_path() {
code,
vec![ConstantPoolEntry::String("I".into())],
vec![SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "draw_text".into(),
version: 1,
arg_slots: 4,
@ -664,8 +758,6 @@ fn tick_draw_text_survives_no_scene_frame_path() {
);
let cartridge = cartridge_with_program(program, caps::GFX);
let mut hardware = Hardware::new();
hardware.gfx.scene_fade_level = 31;
hardware.gfx.hud_fade_level = 31;
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let report = runtime.tick(
@ -681,10 +773,92 @@ fn tick_draw_text_survives_no_scene_frame_path() {
);
assert!(report.is_none(), "no-scene overlay text must not crash");
hardware.gfx.present();
let submission =
runtime.render_manager.latest_complete_submission().expect("closed submission");
let prometeu_hal::RenderSubmissionPacket::Game2D(packet) = &submission.packet else {
panic!("expected Game2D submission");
};
assert_eq!(packet.gfx2d.len(), 1);
assert!(matches!(packet.gfx2d[0], prometeu_hal::Gfx2dCommand::DrawText { .. }));
assert_eq!(hardware.gfx.front_buffer()[0], Color::from_raw(0x11223344).raw());
}
#[test]
fn tick_game_submissions_are_immutable_and_latest_complete_wins() {
let mut runtime = VirtualMachineRuntime::new(None);
let mut log_service = LogService::new(4096);
let mut fs = VirtualFS::new();
let mut fs_state = FsState::Unmounted;
let mut memcard = MemcardService::new();
let mut open_files: HashMap<u32, String> = HashMap::new();
let mut next_handle = 1;
let mut vm = VirtualMachine::default();
let signals = InputSignals::default();
let code = assemble("PUSH_I32 287454020\nHOSTCALL 0\nFRAME_SYNC\nHALT").expect("assemble");
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfx2d".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
}],
);
let cartridge = cartridge_with_program(program, caps::GFX);
let mut hardware = Hardware::new();
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
let first_report = runtime.tick(
&mut log_service,
&mut fs,
&mut fs_state,
&mut memcard,
&mut open_files,
&mut next_handle,
&mut vm,
&signals,
&mut hardware,
);
assert!(first_report.is_none());
let first_submission =
runtime.render_manager.latest_complete_submission().expect("first submission").clone();
let prometeu_hal::RenderSubmissionPacket::Game2D(first_packet) = &first_submission.packet
else {
panic!("expected first Game2D submission");
};
assert_eq!(first_packet.gfx2d.len(), 1);
assert!(matches!(
first_packet.gfx2d[0],
prometeu_hal::Gfx2dCommand::Clear { color } if color == Color::from_raw(0x11223344)
));
assert_eq!(hardware.gfx.front_buffer()[0], Color::from_raw(0x11223344).raw());
let second_packet = prometeu_hal::Game2DFramePacket::new(
Default::default(),
vec![prometeu_hal::Gfx2dCommand::Clear { color: Color::from_raw(0x55667788) }],
);
runtime
.render_manager
.close_frame_with_packet(prometeu_hal::RenderSubmissionPacket::Game2D(second_packet))
.expect("manual Game2D packet matches active mode");
let latest = runtime.render_manager.latest_complete_submission().expect("latest submission");
assert_ne!(latest.frame_id, first_submission.frame_id);
let prometeu_hal::RenderSubmissionPacket::Game2D(latest_packet) = &latest.packet else {
panic!("expected latest Game2D submission");
};
let prometeu_hal::Gfx2dCommand::Clear { color: latest_color } = latest_packet.gfx2d[0] else {
panic!("expected latest clear command");
};
assert_eq!(latest_color, Color::from_raw(0x55667788));
assert!(matches!(
first_packet.gfx2d[0],
prometeu_hal::Gfx2dCommand::Clear { color } if color == Color::from_raw(0x11223344)
));
prometeu_hal::HardwareBridge::publish_render_submission(&mut hardware, latest);
assert_eq!(hardware.gfx.front_buffer()[0], Color::from_raw(0x55667788).raw());
}
#[test]
fn initialize_vm_success_clears_previous_crash_report() {
let mut runtime = VirtualMachineRuntime::new(None);
@ -696,7 +870,7 @@ fn initialize_vm_success_clears_previous_crash_report() {
let program = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
@ -740,6 +914,9 @@ fn reset_clears_cartridge_scoped_runtime_state() {
runtime.frame_start_heap_allocations = 11;
runtime.frame_start_string_materializations = 22;
runtime.needs_prepare_entry_call = true;
runtime
.gfxui_commands
.push(prometeu_hal::GfxUiCommand::Clear { color: Color::from_raw(0x11223344) });
runtime.reset(&mut vm);
@ -756,6 +933,7 @@ fn reset_clears_cartridge_scoped_runtime_state() {
assert!(runtime.current_cartridge_app_version.is_empty());
assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game);
assert!(runtime.logs_written_this_frame.is_empty());
assert!(runtime.gfxui_commands.is_empty());
assert_eq!(runtime.atomic_telemetry.logs_count.load(Ordering::Relaxed), 0);
assert_eq!(runtime.atomic_telemetry.current_logs_count.load(Ordering::Relaxed), 0);
assert_eq!(runtime.atomic_telemetry.frame_index.load(Ordering::Relaxed), 0);
@ -856,7 +1034,7 @@ fn initialize_vm_failure_clears_previous_identity_and_handles() {
let good_program = serialized_single_function_module(
assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"),
vec![SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
@ -878,7 +1056,7 @@ fn initialize_vm_failure_clears_previous_identity_and_handles() {
let bad_program = serialized_single_function_module(
assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"),
vec![SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,

View File

@ -1,16 +1,30 @@
use super::dispatch::VmRuntimeHost;
use super::render_manager::RenderSurface;
use super::*;
use crate::CrashReport;
use crate::fs::{FsState, VirtualFS};
use crate::services::memcard::MemcardService;
use prometeu_hal::asset::{BankTelemetry, BankType};
use prometeu_hal::log::{LogLevel, LogService, LogSource};
use prometeu_hal::{HardwareBridge, HostContext, InputSignals};
use prometeu_hal::{
HardwareBridge, HostContext, InputSignals, RenderSubmission, RenderSubmissionPacket,
ShellUiFramePacket,
};
use prometeu_vm::LogicalFrameEndingReason;
use std::collections::HashMap;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::atomic::Ordering;
struct HardwareRenderSurface<'a> {
hw: &'a mut dyn HardwareBridge,
}
impl RenderSurface for HardwareRenderSurface<'_> {
fn consume_submission(&mut self, submission: &RenderSubmission) {
self.hw.publish_render_submission(submission);
}
}
impl VirtualMachineRuntime {
fn host_panic_payload_to_string(payload: Box<dyn std::any::Any + Send>) -> String {
if let Some(message) = payload.downcast_ref::<String>() {
@ -220,7 +234,25 @@ impl VirtualMachineRuntime {
if run.reason == LogicalFrameEndingReason::FrameSync
|| run.reason == LogicalFrameEndingReason::EndOfRom
{
if let Err(payload) = catch_unwind(AssertUnwindSafe(|| hw.render_frame())) {
self.render_manager.set_active_app_mode(self.current_cartridge_app_mode);
if self.current_cartridge_app_mode == AppMode::Game {
let mut packet = hw.close_game2d_packet();
packet.gfx2d = std::mem::take(&mut self.gfx2d_commands);
self.render_manager
.close_frame_with_packet(RenderSubmissionPacket::Game2D(packet))
.expect("game packet must match Game app mode");
} else {
let packet =
ShellUiFramePacket::new(std::mem::take(&mut self.gfxui_commands));
self.render_manager
.close_frame_with_packet(RenderSubmissionPacket::ShellUi(packet))
.expect("shell packet must match Shell app mode");
}
if let Err(payload) = catch_unwind(AssertUnwindSafe(|| {
let mut surface = HardwareRenderSurface { hw };
self.render_manager.publish_latest(&mut surface);
})) {
let message = Self::host_panic_payload_to_string(payload);
let report = CrashReport::VmPanic { message, pc: Some(vm.pc() as u32) };
self.log(

View File

@ -2707,7 +2707,7 @@ mod tests {
let bytes = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
@ -2733,7 +2733,7 @@ mod tests {
let bytes = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
@ -2765,14 +2765,14 @@ mod tests {
code,
vec![
SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
},
SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "draw_text".into(),
version: 1,
arg_slots: 4,
@ -2788,7 +2788,7 @@ mod tests {
Err(VmInitError::LoaderPatchFailed(
crate::vm_init_error::LoaderPatchError::UnusedSyscallDecl {
sysc_index: 1,
module: "gfx".into(),
module: "gfx2d".into(),
name: "draw_text".into(),
version: 1,
},
@ -2805,7 +2805,7 @@ mod tests {
let bytes = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
@ -2835,7 +2835,7 @@ mod tests {
let bytes = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "missing".into(),
version: 1,
arg_slots: 0,
@ -2850,7 +2850,7 @@ mod tests {
Err(VmInitError::LoaderPatchFailed(
crate::vm_init_error::LoaderPatchError::ResolveFailed(
prometeu_hal::syscalls::DeclaredLoadError::UnknownSyscall {
module: "gfx".into(),
module: "gfx2d".into(),
name: "missing".into(),
version: 1,
},
@ -2868,7 +2868,7 @@ mod tests {
let bytes = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
@ -2883,9 +2883,9 @@ mod tests {
Err(VmInitError::LoaderPatchFailed(
crate::vm_init_error::LoaderPatchError::ResolveFailed(
prometeu_hal::syscalls::DeclaredLoadError::MissingCapability {
required: prometeu_hal::syscalls::caps::GFX,
required: prometeu_hal::syscalls::caps::GFX2D,
provided: prometeu_hal::syscalls::caps::NONE,
module: "gfx".into(),
module: "gfx2d".into(),
name: "clear".into(),
version: 1,
},
@ -2903,7 +2903,7 @@ mod tests {
let bytes = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "draw_line".into(),
version: 1,
arg_slots: 4,
@ -2918,7 +2918,7 @@ mod tests {
Err(VmInitError::LoaderPatchFailed(
crate::vm_init_error::LoaderPatchError::ResolveFailed(
prometeu_hal::syscalls::DeclaredLoadError::AbiMismatch {
module: "gfx".into(),
module: "gfx2d".into(),
name: "draw_line".into(),
version: 1,
declared_arg_slots: 4,

View File

@ -4,7 +4,6 @@ pub mod debugger;
pub mod fs_backend;
pub mod input;
pub mod log_sink;
pub mod overlay;
pub mod runner;
pub mod stats;
pub mod utilities;

View File

@ -1,487 +0,0 @@
use crate::stats::HostStats;
use prometeu_firmware::Firmware;
const PANEL_X: usize = 6;
const PANEL_Y: usize = 3;
const PANEL_WIDTH: usize = 170;
const PANEL_PADDING_X: usize = 8;
const PANEL_PADDING_Y: usize = 3;
const LINE_HEIGHT: usize = 12;
const CHAR_SCALE: usize = 1;
const BAR_WIDTH: usize = PANEL_WIDTH - (PANEL_PADDING_X * 2);
const BAR_HEIGHT: usize = 6;
const BG: [u8; 4] = [10, 18, 32, 208];
const BORDER: [u8; 4] = [90, 126, 170, 255];
const TEXT: [u8; 4] = [235, 240, 255, 255];
const DIM: [u8; 4] = [150, 168, 196, 255];
const WARN: [u8; 4] = [255, 104, 104, 255];
const BAR_BG: [u8; 4] = [30, 42, 61, 255];
const BAR_FILL: [u8; 4] = [91, 184, 255, 255];
const BAR_WARN: [u8; 4] = [255, 150, 102, 255];
const OVERLAY_HEAP_FALLBACK_BYTES: usize = 8 * 1024 * 1024;
#[derive(Debug, Clone)]
pub(crate) struct OverlayMetric {
label: &'static str,
value: String,
warn: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct OverlayBar {
label: &'static str,
value: String,
ratio: f32,
warn: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct OverlaySnapshot {
rows: Vec<(OverlayMetric, OverlayMetric)>,
bars: Vec<OverlayBar>,
footer: Vec<OverlayMetric>,
}
#[derive(Clone, Copy)]
struct Rect {
x: usize,
y: usize,
width: usize,
height: usize,
}
struct FrameCanvas<'a> {
frame: &'a mut [u8],
width: usize,
height: usize,
}
pub(crate) fn capture_snapshot(stats: &HostStats, firmware: &mut Firmware) -> OverlaySnapshot {
let tel = firmware.os.vm().telemetry_snapshot();
let recent_logs = firmware.os.recent_logs(10);
let violations_count =
recent_logs.iter().filter(|e| e.tag >= 0xCA01 && e.tag <= 0xCA07).count();
let mut footer = Vec::new();
if violations_count > 0
&& let Some(event) =
recent_logs.into_iter().rev().find(|e| e.tag >= 0xCA01 && e.tag <= 0xCA07)
{
footer.push(OverlayMetric {
label: "CERT",
value: truncate_value(&event.msg, 28),
warn: true,
});
}
if let Some(report) = firmware.os.vm().last_crash_report() {
footer.push(OverlayMetric {
label: "CRASH",
value: truncate_value(&report.summary(), 28),
warn: true,
});
}
let cycles_ratio = ratio(tel.cycles_used, tel.cycles_budget);
let heap_total_bytes = firmware
.os
.vm()
.cert_config()
.max_heap_bytes
.or(if tel.heap_max_bytes > 0 { Some(tel.heap_max_bytes) } else { None })
.unwrap_or(OVERLAY_HEAP_FALLBACK_BYTES);
let heap_ratio = ratio(tel.heap_used_bytes as u64, heap_total_bytes as u64);
let glyph_ratio = ratio(tel.glyph_slots_used as u64, tel.glyph_slots_total as u64);
let sound_ratio = ratio(tel.sound_slots_used as u64, tel.sound_slots_total as u64);
let scene_ratio = ratio(tel.scene_slots_used as u64, tel.scene_slots_total as u64);
OverlaySnapshot {
rows: vec![
(
OverlayMetric {
label: "FPS",
value: format!("{:.1}", stats.current_fps),
warn: false,
},
OverlayMetric {
label: "CERT",
value: violations_count.to_string(),
warn: violations_count > 0,
},
),
(
OverlayMetric {
label: "HOST",
value: format!("{:.2}ms", stats.average_host_cpu_ms()),
warn: false,
},
OverlayMetric { label: "STEPS", value: tel.vm_steps.to_string(), warn: false },
),
(
OverlayMetric { label: "SYSC", value: tel.syscalls.to_string(), warn: false },
OverlayMetric { label: "LOGS", value: tel.logs_count.to_string(), warn: false },
),
],
bars: vec![
OverlayBar {
label: "HEAP",
value: format!(
"{}K{}",
tel.heap_used_bytes.div_ceil(1024),
if heap_total_bytes > 0 {
format!(" / {}K", heap_total_bytes.div_ceil(1024))
} else {
String::new()
}
),
ratio: heap_ratio,
warn: tel.heap_used_bytes >= heap_total_bytes,
},
OverlayBar {
label: "BUDGET",
value: if tel.cycles_budget > 0 {
format!(
"{:.1}K/{:.1}K",
tel.cycles_used as f64 / 1000.0,
tel.cycles_budget as f64 / 1000.0
)
} else {
"0.0K/0.0K".to_string()
},
ratio: cycles_ratio,
warn: cycles_ratio >= 0.9,
},
OverlayBar {
label: "GLYPH",
value: format!("{} / {} slots", tel.glyph_slots_used, tel.glyph_slots_total),
ratio: glyph_ratio,
warn: tel.glyph_slots_total > 0 && tel.glyph_slots_used >= tel.glyph_slots_total,
},
OverlayBar {
label: "SOUNDS",
value: format!("{} / {} slots", tel.sound_slots_used, tel.sound_slots_total),
ratio: sound_ratio,
warn: tel.sound_slots_total > 0 && tel.sound_slots_used >= tel.sound_slots_total,
},
OverlayBar {
label: "SCENE",
value: format!("{} / {} slots", tel.scene_slots_used, tel.scene_slots_total),
ratio: scene_ratio,
warn: tel.scene_slots_total > 0 && tel.scene_slots_used >= tel.scene_slots_total,
},
],
footer,
}
}
pub(crate) fn draw_overlay(
frame: &mut [u8],
frame_width: usize,
frame_height: usize,
snapshot: &OverlaySnapshot,
) {
let mut canvas = FrameCanvas { frame, width: frame_width, height: frame_height };
let panel_height = frame_height.saturating_sub(PANEL_Y * 2);
let panel_rect = Rect { x: PANEL_X, y: PANEL_Y, width: PANEL_WIDTH, height: panel_height };
fill_rect_alpha(&mut canvas, panel_rect, BG);
stroke_rect(&mut canvas, panel_rect, BORDER);
let mut y = PANEL_Y + PANEL_PADDING_Y;
for (left, right) in &snapshot.rows {
draw_metric_pair(canvas.frame, canvas.width, canvas.height, y, left, right);
y += LINE_HEIGHT;
}
for bar in &snapshot.bars {
let color = if bar.warn { WARN } else { TEXT };
draw_text(
canvas.frame,
canvas.width,
canvas.height,
PANEL_X + PANEL_PADDING_X,
y,
bar.label,
DIM,
);
draw_text(canvas.frame, canvas.width, canvas.height, PANEL_X + 48, y, &bar.value, color);
y += LINE_HEIGHT - 2;
let bar_x = PANEL_X + PANEL_PADDING_X;
let bar_rect = Rect { x: bar_x, y, width: BAR_WIDTH, height: BAR_HEIGHT };
fill_rect(&mut canvas, bar_rect, BAR_BG);
let fill_width = ((BAR_WIDTH as f32) * bar.ratio.clamp(0.0, 1.0)).round() as usize;
fill_rect(
&mut canvas,
Rect { x: bar_x, y, width: fill_width, height: BAR_HEIGHT },
if bar.warn { BAR_WARN } else { BAR_FILL },
);
stroke_rect(&mut canvas, bar_rect, BORDER);
y += BAR_HEIGHT + 6;
}
for line in &snapshot.footer {
let color = if line.warn { WARN } else { TEXT };
draw_text(
canvas.frame,
canvas.width,
canvas.height,
PANEL_X + PANEL_PADDING_X,
y,
line.label,
DIM,
);
draw_text(canvas.frame, canvas.width, canvas.height, PANEL_X + 48, y, &line.value, color);
y += LINE_HEIGHT;
}
}
fn draw_metric_pair(
frame: &mut [u8],
frame_width: usize,
frame_height: usize,
y: usize,
left: &OverlayMetric,
right: &OverlayMetric,
) {
let left_x = PANEL_X + PANEL_PADDING_X;
let right_x = PANEL_X + 86;
draw_metric(frame, frame_width, frame_height, left_x, y, left);
draw_metric(frame, frame_width, frame_height, right_x, y, right);
}
fn draw_metric(
frame: &mut [u8],
frame_width: usize,
frame_height: usize,
x: usize,
y: usize,
metric: &OverlayMetric,
) {
let color = if metric.warn { WARN } else { TEXT };
draw_text(frame, frame_width, frame_height, x, y, metric.label, DIM);
draw_text(frame, frame_width, frame_height, x + 30, y, &metric.value, color);
}
fn ratio(value: u64, total: u64) -> f32 {
if total == 0 { 0.0 } else { (value as f32 / total as f32).clamp(0.0, 1.0) }
}
fn truncate_value(value: &str, max_len: usize) -> String {
let mut upper = value.to_ascii_uppercase();
if upper.len() > max_len {
upper.truncate(max_len);
}
upper
}
fn draw_text(
frame: &mut [u8],
frame_width: usize,
frame_height: usize,
x: usize,
y: usize,
text: &str,
color: [u8; 4],
) {
let mut cursor_x = x;
for ch in text.chars() {
if ch == '\n' {
continue;
}
draw_char(frame, frame_width, frame_height, cursor_x, y, ch.to_ascii_uppercase(), color);
cursor_x += 6 * CHAR_SCALE;
}
}
fn draw_char(
frame: &mut [u8],
frame_width: usize,
frame_height: usize,
x: usize,
y: usize,
ch: char,
color: [u8; 4],
) {
let glyph = glyph_bits(ch);
for (row, bits) in glyph.iter().enumerate() {
for col in 0..5 {
if bits & (1 << (4 - col)) != 0 {
let mut canvas = FrameCanvas { frame, width: frame_width, height: frame_height };
fill_rect(
&mut canvas,
Rect {
x: x + col * CHAR_SCALE,
y: y + row * CHAR_SCALE,
width: CHAR_SCALE,
height: CHAR_SCALE,
},
color,
);
}
}
}
}
fn fill_rect_alpha(canvas: &mut FrameCanvas<'_>, rect: Rect, color: [u8; 4]) {
let max_x = (rect.x + rect.width).min(canvas.width);
let max_y = (rect.y + rect.height).min(canvas.height);
for py in rect.y..max_y {
for px in rect.x..max_x {
blend_pixel(canvas.frame, canvas.width, px, py, color);
}
}
}
fn fill_rect(canvas: &mut FrameCanvas<'_>, rect: Rect, color: [u8; 4]) {
let max_x = (rect.x + rect.width).min(canvas.width);
let max_y = (rect.y + rect.height).min(canvas.height);
for py in rect.y..max_y {
for px in rect.x..max_x {
write_pixel(canvas.frame, canvas.width, px, py, color);
}
}
}
fn stroke_rect(canvas: &mut FrameCanvas<'_>, rect: Rect, color: [u8; 4]) {
if rect.width == 0 || rect.height == 0 {
return;
}
fill_rect(canvas, Rect { x: rect.x, y: rect.y, width: rect.width, height: 1 }, color);
fill_rect(
canvas,
Rect { x: rect.x, y: rect.y + rect.height.saturating_sub(1), width: rect.width, height: 1 },
color,
);
fill_rect(canvas, Rect { x: rect.x, y: rect.y, width: 1, height: rect.height }, color);
fill_rect(
canvas,
Rect { x: rect.x + rect.width.saturating_sub(1), y: rect.y, width: 1, height: rect.height },
color,
);
}
fn blend_pixel(frame: &mut [u8], frame_width: usize, x: usize, y: usize, color: [u8; 4]) {
let idx = (y * frame_width + x) * 4;
let alpha = color[3] as f32 / 255.0;
let inv = 1.0 - alpha;
frame[idx] = (frame[idx] as f32 * inv + color[0] as f32 * alpha).round() as u8;
frame[idx + 1] = (frame[idx + 1] as f32 * inv + color[1] as f32 * alpha).round() as u8;
frame[idx + 2] = (frame[idx + 2] as f32 * inv + color[2] as f32 * alpha).round() as u8;
frame[idx + 3] = 0xFF;
}
fn write_pixel(frame: &mut [u8], frame_width: usize, x: usize, y: usize, color: [u8; 4]) {
let idx = (y * frame_width + x) * 4;
frame[idx] = color[0];
frame[idx + 1] = color[1];
frame[idx + 2] = color[2];
frame[idx + 3] = color[3];
}
fn glyph_bits(ch: char) -> [u8; 7] {
match ch {
'A' => [0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11],
'B' => [0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E],
'C' => [0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E],
'D' => [0x1E, 0x12, 0x11, 0x11, 0x11, 0x12, 0x1E],
'E' => [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F],
'F' => [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10],
'G' => [0x0E, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0E],
'H' => [0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11],
'I' => [0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E],
'J' => [0x01, 0x01, 0x01, 0x01, 0x11, 0x11, 0x0E],
'K' => [0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11],
'L' => [0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F],
'M' => [0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11],
'N' => [0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11],
'O' => [0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E],
'P' => [0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10],
'Q' => [0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D],
'R' => [0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11],
'S' => [0x0F, 0x10, 0x10, 0x0E, 0x01, 0x01, 0x1E],
'T' => [0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04],
'U' => [0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E],
'V' => [0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04],
'W' => [0x11, 0x11, 0x11, 0x15, 0x15, 0x15, 0x0A],
'X' => [0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11],
'Y' => [0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04],
'Z' => [0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F],
'0' => [0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E],
'1' => [0x04, 0x0C, 0x14, 0x04, 0x04, 0x04, 0x1F],
'2' => [0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F],
'3' => [0x1E, 0x01, 0x01, 0x0E, 0x01, 0x01, 0x1E],
'4' => [0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02],
'5' => [0x1F, 0x10, 0x10, 0x1E, 0x01, 0x01, 0x1E],
'6' => [0x0E, 0x10, 0x10, 0x1E, 0x11, 0x11, 0x0E],
'7' => [0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08],
'8' => [0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E],
'9' => [0x0E, 0x11, 0x11, 0x0F, 0x01, 0x01, 0x0E],
':' => [0x00, 0x04, 0x04, 0x00, 0x04, 0x04, 0x00],
'.' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x06],
'/' => [0x01, 0x02, 0x02, 0x04, 0x08, 0x08, 0x10],
'%' => [0x19, 0x19, 0x02, 0x04, 0x08, 0x13, 0x13],
'(' => [0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02],
')' => [0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08],
'-' => [0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00],
'_' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F],
'+' => [0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00],
' ' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
'?' => [0x0E, 0x11, 0x01, 0x02, 0x04, 0x00, 0x04],
_ => [0x0E, 0x11, 0x01, 0x02, 0x04, 0x00, 0x04],
}
}
#[cfg(test)]
mod tests {
use super::*;
use prometeu_drivers::hardware::Hardware;
fn sample_snapshot() -> OverlaySnapshot {
OverlaySnapshot {
rows: vec![
(
OverlayMetric { label: "FPS", value: "60.0".to_string(), warn: false },
OverlayMetric { label: "CERT", value: "2".to_string(), warn: true },
),
(
OverlayMetric { label: "HOST", value: "1.23ms".to_string(), warn: false },
OverlayMetric { label: "STEPS", value: "420".to_string(), warn: false },
),
],
bars: vec![
OverlayBar {
label: "HEAP",
value: "1024K / 8192K".to_string(),
ratio: 0.125,
warn: false,
},
OverlayBar {
label: "BUDGET",
value: "9.0K/10.0K".to_string(),
ratio: 0.9,
warn: true,
},
],
footer: vec![OverlayMetric {
label: "CRASH",
value: "VM PANIC".to_string(),
warn: true,
}],
}
}
#[test]
fn draw_overlay_writes_to_host_rgba_frame() {
let mut frame = vec![0u8; Hardware::W * Hardware::H * 4];
draw_overlay(&mut frame, Hardware::W, Hardware::H, &sample_snapshot());
assert!(frame.iter().any(|&byte| byte != 0));
}
#[test]
fn truncate_value_normalizes_and_caps() {
assert_eq!(truncate_value("panic: lowercase", 12), "PANIC: LOWER");
}
}

View File

@ -3,7 +3,6 @@ use crate::debugger::HostDebugger;
use crate::fs_backend::HostDirBackend;
use crate::input::HostInputHandler;
use crate::log_sink::HostConsoleSink;
use crate::overlay::{capture_snapshot, draw_overlay};
use crate::stats::HostStats;
use crate::utilities::draw_rgba8888_to_rgba8;
use pixels::wgpu::PresentMode;
@ -128,9 +127,6 @@ pub struct HostRunner {
/// directly instead of depending on guest-visible inspection syscalls.
debugger: HostDebugger,
/// Flag to enable/disable the technical telemetry display.
overlay_enabled: bool,
/// The physical audio driver.
audio: HostAudio,
/// Last known pause state to sync with audio.
@ -170,7 +166,6 @@ impl HostRunner {
stats: HostStats::new(),
debugger: HostDebugger::new(),
overlay_enabled: false,
audio: HostAudio::new(),
last_paused_state: false,
presentation: PresentationState::default(),
@ -272,9 +267,6 @@ impl ApplicationHandler for HostRunner {
}
WindowEvent::RedrawRequested => {
let overlay_snapshot =
self.overlay_enabled.then(|| capture_snapshot(&self.stats, &mut self.firmware));
// Get Pixels directly from the field (not via helper that gets the entire &mut self)
let pixels = self.pixels.as_mut().expect("pixels not initialized");
@ -286,10 +278,6 @@ impl ApplicationHandler for HostRunner {
let src = self.hardware.gfx.front_buffer();
draw_rgba8888_to_rgba8(src, frame);
if let Some(snapshot) = overlay_snapshot.as_ref() {
draw_overlay(frame, Hardware::W, Hardware::H, snapshot);
}
} // <- frame borrow ends here
if pixels.render().is_err() {
@ -309,12 +297,6 @@ impl ApplicationHandler for HostRunner {
self.request_redraw_if_needed();
println!("[Debugger] Execution started!");
}
if is_down && code == KeyCode::F1 {
self.overlay_enabled = !self.overlay_enabled;
self.invalidate_host_surface();
self.request_redraw_if_needed();
}
}
}
@ -332,12 +314,8 @@ impl ApplicationHandler for HostRunner {
// 1. Process pending debug commands from the network.
self.debugger.check_commands(&mut self.firmware, &mut self.hardware);
// Sync inspection mode state. This is host-owned overlay/debugger control,
// not a guest-visible debug ABI switch.
self.firmware
.os
.vm()
.set_inspection_active(self.overlay_enabled || self.debugger.stream.is_some());
// Sync debugger inspection state; this is not a guest-visible debug ABI switch.
self.firmware.os.vm().set_inspection_active(self.debugger.stream.is_some());
// 2. Maintain a filesystem connection if it was lost (e.g., directory removed).
if let Some(root) = &self.fs_root

View File

@ -42,14 +42,14 @@ pub fn generate() -> Result<()> {
let mut rom: Vec<u8> = Vec::new();
let syscalls = vec![
SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
},
SyscallDecl {
module: "gfx".into(),
module: "gfx2d".into(),
name: "draw_text".into(),
version: 1,
arg_slots: 4,
@ -126,7 +126,7 @@ pub fn generate() -> Result<()> {
fs::create_dir_all(&out_dir)?;
fs::write(out_dir.join("program.pbx"), bytes)?;
fs::write(out_dir.join("assets.pa"), build_assets_pack()?)?;
fs::write(out_dir.join("manifest.json"), b"{\n \"magic\": \"PMTU\",\n \"cartridge_version\": 1,\n \"app_id\": 1,\n \"title\": \"Stress Console\",\n \"app_version\": \"0.1.0\",\n \"app_mode\": \"Game\",\n \"capabilities\": [\"gfx\", \"log\", \"asset\"]\n}\n")?;
fs::write(out_dir.join("manifest.json"), b"{\n \"magic\": \"PMTU\",\n \"cartridge_version\": 1,\n \"app_id\": 1,\n \"title\": \"Stress Console\",\n \"app_version\": \"0.1.0\",\n \"app_mode\": \"Game\",\n \"capabilities\": [\"composer\", \"gfx2d\", \"log\", \"asset\"]\n}\n")?;
Ok(())
}

View File

@ -1,5 +1,5 @@
{"type":"meta","next_id":{"DSC":39,"AGD":39,"DEC":31,"PLN":87,"LSN":47,"CLSN":1}}
{"type":"discussion","id":"DSC-0038","status":"in_progress","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-05-25","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[{"id":"AGD-0038","file":"AGD-0038-renderframepacket-boundary-for-classic-2d-renderer.md","status":"accepted","created_at":"2026-05-25","updated_at":"2026-05-25"}],"decisions":[{"id":"DEC-0030","file":"DEC-0030-logical-render-pipeline-command-boundary.md","status":"accepted","created_at":"2026-05-25","updated_at":"2026-05-25","ref_agenda":"AGD-0038"}],"plans":[{"id":"PLN-0073","file":"PLN-0073-render-contract-specs.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0074","file":"PLN-0074-hal-render-submission-types.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0075","file":"PLN-0075-rendermanager-core.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0076","file":"PLN-0076-capabilities-and-abi-domain-split.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0077","file":"PLN-0077-composer-buffer-and-game2d-packet.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0078","file":"PLN-0078-classic2d-game-renderer-consumer.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0079","file":"PLN-0079-gfx2d-primitive-domain.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0080","file":"PLN-0080-shell-ui-packet-and-gfxui-domain.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0081","file":"PLN-0081-shell-hub-migration.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0082","file":"PLN-0082-frame-publication-and-present-boundary.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0083","file":"PLN-0083-fade-removal.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0084","file":"PLN-0084-host-debug-overlay-removal.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0085","file":"PLN-0085-pbs-stdlib-and-syscall-declarations.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0086","file":"PLN-0086-end-to-end-render-boundary-validation.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0038","status":"in_progress","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-05-25","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[{"id":"AGD-0038","file":"AGD-0038-renderframepacket-boundary-for-classic-2d-renderer.md","status":"accepted","created_at":"2026-05-25","updated_at":"2026-05-25"}],"decisions":[{"id":"DEC-0030","file":"DEC-0030-logical-render-pipeline-command-boundary.md","status":"accepted","created_at":"2026-05-25","updated_at":"2026-05-25","ref_agenda":"AGD-0038"}],"plans":[{"id":"PLN-0073","file":"PLN-0073-render-contract-specs.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0074","file":"PLN-0074-hal-render-submission-types.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0075","file":"PLN-0075-rendermanager-core.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0076","file":"PLN-0076-capabilities-and-abi-domain-split.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0077","file":"PLN-0077-composer-buffer-and-game2d-packet.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0078","file":"PLN-0078-classic2d-game-renderer-consumer.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0079","file":"PLN-0079-gfx2d-primitive-domain.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0080","file":"PLN-0080-shell-ui-packet-and-gfxui-domain.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0081","file":"PLN-0081-shell-hub-migration.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0082","file":"PLN-0082-frame-publication-and-present-boundary.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0083","file":"PLN-0083-fade-removal.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0084","file":"PLN-0084-host-debug-overlay-removal.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0085","file":"PLN-0085-pbs-stdlib-and-syscall-declarations.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0086","file":"PLN-0086-end-to-end-render-boundary-validation.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0035","status":"done","ticket":"task-owned-shell-windows","title":"Agenda - Task-Owned Shell Windows","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","task","window-manager","shell","lifecycle"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0044","file":"discussion/lessons/DSC-0035-task-owned-shell-windows/LSN-0044-task-window-liveness-belongs-to-the-task.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]}
{"type":"discussion","id":"DSC-0034","status":"done","ticket":"system-os-domain-facades","title":"Agenda - SystemOS Domain Facades","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["runtime","os","services","api-surface","lifecycle","fs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0034-system-os-domain-facades/LSN-0043-systemos-domain-facades.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]}
{"type":"discussion","id":"DSC-0023","status":"done","ticket":"perf-full-migration-to-atomic-telemetry","title":"Agenda - [PERF] Full Migration to Atomic Telemetry","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["perf","runtime","telemetry"],"agendas":[{"id":"AGD-0021","file":"AGD-0021-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0008","file":"DEC-0008-full-migration-to-atomic-telemetry.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0007","file":"PLN-0007-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0028","file":"discussion/lessons/DSC-0023-perf-full-migration-to-atomic-telemetry/LSN-0028-converging-to-single-atomic-telemetry-source.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}

View File

@ -2,7 +2,7 @@
id: PLN-0075
ticket: render-frame-packet-boundary
title: RenderManager Core
status: open
status: done
created: 2026-05-25
ref_decisions: [DEC-0030]
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]

View File

@ -2,7 +2,7 @@
id: PLN-0076
ticket: render-frame-packet-boundary
title: Capabilities and ABI Domain Split
status: open
status: done
created: 2026-05-25
ref_decisions: [DEC-0030]
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]

View File

@ -2,7 +2,7 @@
id: PLN-0077
ticket: render-frame-packet-boundary
title: Composer Buffer and Game2D Packet
status: open
status: done
created: 2026-05-25
ref_decisions: [DEC-0030]
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]

View File

@ -2,7 +2,7 @@
id: PLN-0078
ticket: render-frame-packet-boundary
title: Classic2D Game Renderer Consumer
status: open
status: done
created: 2026-05-25
ref_decisions: [DEC-0030]
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]

View File

@ -2,7 +2,7 @@
id: PLN-0079
ticket: render-frame-packet-boundary
title: Gfx2D Primitive Domain
status: open
status: done
created: 2026-05-25
ref_decisions: [DEC-0030]
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]

View File

@ -2,7 +2,7 @@
id: PLN-0080
ticket: render-frame-packet-boundary
title: Shell UI Packet and GfxUI Domain
status: open
status: done
created: 2026-05-25
ref_decisions: [DEC-0030]
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]

View File

@ -2,7 +2,7 @@
id: PLN-0081
ticket: render-frame-packet-boundary
title: Shell Hub Migration
status: open
status: done
created: 2026-05-25
ref_decisions: [DEC-0030]
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]

View File

@ -2,7 +2,7 @@
id: PLN-0082
ticket: render-frame-packet-boundary
title: Frame Publication and Present Boundary
status: open
status: done
created: 2026-05-25
ref_decisions: [DEC-0030]
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]

View File

@ -2,7 +2,7 @@
id: PLN-0083
ticket: render-frame-packet-boundary
title: Fade Removal
status: open
status: done
created: 2026-05-25
ref_decisions: [DEC-0030]
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]

View File

@ -2,7 +2,7 @@
id: PLN-0084
ticket: render-frame-packet-boundary
title: Host Debug Overlay Removal
status: open
status: done
created: 2026-05-25
ref_decisions: [DEC-0030]
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]

View File

@ -2,7 +2,7 @@
id: PLN-0085
ticket: render-frame-packet-boundary
title: PBS Stdlib and Syscall Declarations
status: open
status: done
created: 2026-05-25
ref_decisions: [DEC-0030]
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]

View File

@ -2,7 +2,7 @@
id: PLN-0086
ticket: render-frame-packet-boundary
title: End-to-End Render Boundary Validation
status: open
status: done
created: 2026-05-25
ref_decisions: [DEC-0030]
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]

View File

@ -18,7 +18,7 @@ It covers:
- profiling;
- event and fault visibility;
- certification input data;
- host-side debug overlay (HUD) isolation.
- host-side debug UI isolation.
## 2 Execution Modes
@ -131,15 +131,15 @@ The host may:
- observe buffers separately
- identify excessive redraw
Host-owned overlay or inspection surfaces MUST observe the last published logical frame rather than force continuous redraw to probe for changes.
Host-owned inspection surfaces MUST observe the last published logical frame rather than force continuous redraw to probe for changes.
When overlay, debugger, or other host-only diagnostics become visually stable:
When debugger or other host-only diagnostics become stable:
- the host MUST preserve the last valid visible image;
- the host MUST NOT keep recomposing the surface by continuous polling alone;
- additional redraw is only justified by a newly published logical frame or a host-owned invalidation event such as overlay toggle, resize, expose, or explicit debugger-visible state transition.
- additional redraw is only justified by a newly published logical frame or a host-owned invalidation event such as resize, expose, or explicit debugger-visible state transition.
Paused or breakpointed execution does not grant permission to swap logical machine buffers just to sustain a host-only HUD.
Paused or breakpointed execution does not grant permission to swap logical machine buffers just to sustain host-only diagnostics.
## 6 Time Profiling (Cycles)
@ -285,28 +285,32 @@ Each event has:
- cost
- consequence
## 11 Host-Side Debug Overlay (HUD) Isolation
## 11 Host-Side Debug UI Isolation
The visual Debug Overlay (HUD) for technical inspection is not part of the emulated machine pipeline.
The desktop host MUST NOT inject a debug overlay into Game or Shell output. Any
future visual debug UI is a separate product/domain concern and requires its own
decision before it can become part of host presentation.
### 10.1 Responsibilities
1. **Runtime:** Only exposes telemetry data via the machine diagnostics surface. It does not perform HUD rendering or string formatting.
2. **Emulated graphics contract:** Machine graphics primitives such as `fill_rect` and `draw_text` remain valid parts of the emulated graphics/syscall contract. They are not host overlay APIs.
3. **Host overlay module:** The Desktop Host owns a dedicated overlay module that performs host-side text, panel, and simple bar composition.
4. **Composition boundary:** The overlay is composed on the Host presentation surface after the emulated frame is ready. Overlay pixels must not be written back into the emulated framebuffer.
5. **Host control:** Overlay visibility and presentation policy remain under Host control.
6. **Host (Desktop):** Responsible for collecting telemetry from the runtime and rendering the HUD as a native, transparent layer.
3. **Host diagnostics:** The Desktop Host may consume telemetry for logs,
debugger streams, and non-render diagnostics, but it does not compose debug
pixels into the presentation surface.
4. **Composition boundary:** Published Game and Shell output must be presented
without host-owned debug overlay injection.
### 10.2 Principles
- **Zero Pipeline Interference:** HUD rendering must not inject pixels into the emulated framebuffer. It is applied after upscaling or as a separate display surface.
- **Zero Cycle Impact:** HUD-related processing (like formatting technical text) must occur outside the emulated machine cycles.
- **Toggle Control:** The activation of the overlay (typically via **F1**) is managed by the Host layer.
- **Zero Pipeline Interference:** Host diagnostics must not inject pixels into
the emulated framebuffer or the host presentation surface.
- **Zero Cycle Impact:** Diagnostic processing must occur outside the emulated
machine cycles.
### 10.3 Atomic Telemetry Model
To ensure zero-impact synchronization between the VM and the Host Debug Overlay, PROMETEU uses a **push-based atomic model**:
To ensure zero-impact synchronization between the VM and host diagnostics, PROMETEU uses a **push-based atomic model**:
1. **Atomic Storage:** Metrics such as cycles, syscalls, and memory usage are stored in a dedicated `AtomicTelemetry` structure using thread-safe atomic types (`AtomicU64`, `AtomicU32`, etc.).
2. **Lockless Access:** The Host (Desktop) reads these metrics asynchronously and without locks by taking a `snapshot()` of the atomic state.
@ -320,4 +324,4 @@ All machine diagnostics and profiling data:
- feed the host-owned certification report;
- are collected deterministically;
- do not require a guest-visible debug ABI;
- are consistent regardless of whether the Host HUD is active or not.
- are consistent regardless of whether host diagnostics are active or not.

View File

@ -51,7 +51,8 @@ The contract is about logical behavior, not identical physical latency or throug
- audio output
- physical input collection
- access to the sandbox file system
- **technical inspection surfaces (Debug Overlay/HUD)**
- technical inspection and debugger integrations that do not modify presented
Game or Shell pixels
The host provides realization of machine surfaces. It does not redefine cartridge semantics.
@ -123,8 +124,8 @@ The graphics system:
The platform layer:
- consumes closed render submissions through a render-surface implementation
- **may overlay technical HUDs without modifying the logical framebuffer**
- may transport published RGBA8888 output into a host presentation surface where a host-only overlay layer is composed
- transports published RGBA8888 output into a host presentation surface without
injecting host-owned debug overlay pixels
The host presentation layer MUST treat RGBA8888 as the canonical logical
framebuffer format. RGB565 conversion is not part of the normal host
@ -136,17 +137,19 @@ host-owned invalidation, not by perpetual redraw polling.
In particular:
- a stable published submission MAY remain visible across multiple host wakeups without recomposition;
- a host MAY redraw the same logical frame again when its own surface is invalidated by resize, expose, or host-only overlay/debug changes;
- a host MAY redraw the same logical frame again when its own surface is invalidated by resize, expose, or debugger-visible host changes;
- a host MUST NOT invent intermediate logical frames or require continuous redraw merely to discover whether a new logical frame exists.
## 9 Debug and Inspection Isolation
To preserve portability and certification purity, technical inspection tools (like the Debug Overlay) are moved to the Host layer.
To preserve portability and certification purity, technical inspection tools are host-owned and must not inject pixels into Game or Shell output.
- **Host-exclusive:** These tools are only implemented where they are relevant (e.g., Desktop) and do not exist in the logical machine.
- **Non-intrusive:** They must not consume machine cycles or alter memory state.
- **Consistent Results:** A cartridge will produce the same logical results and certification metrics regardless of the Host's inspection capabilities.
- **Contract Boundary:** Emulated graphics primitives such as `fill_rect` and `draw_text` remain part of the machine contract, but Host overlays must not depend on them.
- **Contract Boundary:** Emulated graphics primitives such as `fill_rect` and
`draw_text` remain part of the machine contract; host diagnostics must not
depend on them for visual injection.
### 9.1 Atomic Telemetry Interface

View File

@ -39,7 +39,7 @@ Syscalls are identified by a canonical triple:
Example:
```
("gfx", "present", 1)
("gfx2d", "draw_line", 1)
("audio", "play", 2)
("composer", "emit_sprite", 1)
```

View File

@ -46,7 +46,7 @@
]
},
"host-dependent": {
"baseline_percent": 43,
"baseline_percent": 35,
"paths": [
"crates/host/prometeu-host-desktop-winit/src/"
]

View File

@ -5,5 +5,5 @@
"title": "Stress Console",
"app_version": "0.1.0",
"app_mode": "Game",
"capabilities": ["gfx", "log", "asset"]
"capabilities": ["composer", "gfx2d", "log", "asset"]
}