Compare commits
13 Commits
105a6de11e
...
5ba47eb20a
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ba47eb20a | |||
| 91fab7de96 | |||
| 232fb7106d | |||
| d79c11faa8 | |||
| 295666b310 | |||
| 41266d3fd5 | |||
| 74df4b89ac | |||
| ae85ed4eff | |||
| 5e00f6bb0d | |||
| 57439846c5 | |||
| 5e3cb87b57 | |||
| 7c35edcb84 | |||
| c3396f5a61 |
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,8 +68,33 @@ impl HardwareBridge for Hardware {
|
||||
self.frame_composer.emit_sprite(sprite)
|
||||
}
|
||||
|
||||
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) {
|
||||
self.frame_composer.render_frame(&mut self.gfx);
|
||||
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 {
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 }));
|
||||
|
||||
@ -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
|
||||
|
||||
@ -286,6 +286,9 @@ pub enum Capability {
|
||||
None,
|
||||
System,
|
||||
Gfx,
|
||||
Composer,
|
||||
Gfx2d,
|
||||
Gfxui,
|
||||
Audio,
|
||||
Fs,
|
||||
Log,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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),
|
||||
];
|
||||
|
||||
@ -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),
|
||||
];
|
||||
|
||||
32
crates/console/prometeu-hal/src/syscalls/domains/gfxui.rs
Normal file
32
crates/console/prometeu-hal/src/syscalls/domains/gfxui.rs
Normal 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),
|
||||
];
|
||||
@ -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())
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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,21 +126,23 @@ 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;
|
||||
}
|
||||
|
||||
for window in os.windows().windows() {
|
||||
render_shell_window(
|
||||
hw.gfx_mut(),
|
||||
window.viewport,
|
||||
window.color,
|
||||
&window.title,
|
||||
pointer_x,
|
||||
pointer_y,
|
||||
);
|
||||
}
|
||||
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() {
|
||||
append_shell_window_commands(
|
||||
&mut commands,
|
||||
window.viewport,
|
||||
window.color,
|
||||
&window.title,
|
||||
pointer_x,
|
||||
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);
|
||||
}
|
||||
|
||||
ShellUiFramePacket::new(commands)
|
||||
}
|
||||
|
||||
fn render_shell_window(
|
||||
gfx: &mut dyn GfxBridge,
|
||||
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));
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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)]);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@ -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"}]}
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
```
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
]
|
||||
},
|
||||
"host-dependent": {
|
||||
"baseline_percent": 43,
|
||||
"baseline_percent": 35,
|
||||
"paths": [
|
||||
"crates/host/prometeu-host-desktop-winit/src/"
|
||||
]
|
||||
|
||||
@ -5,5 +5,5 @@
|
||||
"title": "Stress Console",
|
||||
"app_version": "0.1.0",
|
||||
"app_mode": "Game",
|
||||
"capabilities": ["gfx", "log", "asset"]
|
||||
"capabilities": ["composer", "gfx2d", "log", "asset"]
|
||||
}
|
||||
|
||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user