Compare commits
17 Commits
38544ed3f1
...
261d9561a4
| Author | SHA1 | Date | |
|---|---|---|---|
| 261d9561a4 | |||
| 5ba47eb20a | |||
| 91fab7de96 | |||
| 232fb7106d | |||
| d79c11faa8 | |||
| 295666b310 | |||
| 41266d3fd5 | |||
| 74df4b89ac | |||
| ae85ed4eff | |||
| 5e00f6bb0d | |||
| 57439846c5 | |||
| 5e3cb87b57 | |||
| 7c35edcb84 | |||
| c3396f5a61 | |||
| 105a6de11e | |||
| 96a23e5419 | |||
| 2b69be0f11 |
@ -882,7 +882,7 @@ mod tests {
|
|||||||
fn test_valid_sysc_roundtrip() {
|
fn test_valid_sysc_roundtrip() {
|
||||||
let mut module = minimal_module();
|
let mut module = minimal_module();
|
||||||
module.syscalls = vec![SyscallDecl {
|
module.syscalls = vec![SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "draw_line".into(),
|
name: "draw_line".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 4,
|
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);
|
emit(CoreOpCode::Sleep, Some(&3u32.to_le_bytes()), &mut prog);
|
||||||
// HOSTCALL sysc[2]
|
// HOSTCALL sysc[2]
|
||||||
emit(CoreOpCode::Hostcall, Some(&2u32.to_le_bytes()), &mut prog);
|
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);
|
emit(CoreOpCode::Syscall, Some(&0x1003u32.to_le_bytes()), &mut prog);
|
||||||
// FRAME_SYNC
|
// FRAME_SYNC
|
||||||
emit(CoreOpCode::FrameSync, None, &mut prog);
|
emit(CoreOpCode::FrameSync, None, &mut prog);
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
use crate::asset::GlyphAssetSlotIndex;
|
use crate::asset::GlyphAssetSlotIndex;
|
||||||
use crate::memory_banks::SceneBankPoolAccess;
|
use crate::memory_banks::SceneBankPoolAccess;
|
||||||
use prometeu_hal::GfxBridge;
|
|
||||||
use prometeu_hal::glyph::Glyph;
|
use prometeu_hal::glyph::Glyph;
|
||||||
use prometeu_hal::scene_bank::SceneBank;
|
use prometeu_hal::scene_bank::SceneBank;
|
||||||
use prometeu_hal::scene_viewport_cache::SceneViewportCache;
|
use prometeu_hal::scene_viewport_cache::SceneViewportCache;
|
||||||
@ -8,6 +7,10 @@ use prometeu_hal::scene_viewport_resolver::{
|
|||||||
CacheRefreshRequest, ResolverUpdate, SceneViewportResolver,
|
CacheRefreshRequest, ResolverUpdate, SceneViewportResolver,
|
||||||
};
|
};
|
||||||
use prometeu_hal::sprite::Sprite;
|
use prometeu_hal::sprite::Sprite;
|
||||||
|
use prometeu_hal::{
|
||||||
|
BoundScenePacket, Camera2D, ComposerFramePacket, Game2DFramePacket, GameSpritePacket,
|
||||||
|
GfxBridge, HudPacket,
|
||||||
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
const EMPTY_SPRITE: Sprite = Sprite {
|
const EMPTY_SPRITE: Sprite = Sprite {
|
||||||
@ -132,6 +135,28 @@ pub struct FrameComposer {
|
|||||||
sprite_controller: SpriteController,
|
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 {
|
impl FrameComposer {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
viewport_width_px: usize,
|
viewport_width_px: usize,
|
||||||
@ -246,6 +271,35 @@ impl FrameComposer {
|
|||||||
self.sprite_controller.ordered_sprites()
|
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) {
|
pub fn render_frame(&mut self, gfx: &mut dyn GfxBridge) {
|
||||||
let ordered_sprites = self.ordered_sprites();
|
let ordered_sprites = self.ordered_sprites();
|
||||||
gfx.load_frame_sprites(&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>);
|
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.render_frame(&mut gfx);
|
||||||
gfx.present();
|
gfx.present();
|
||||||
@ -759,6 +811,73 @@ mod tests {
|
|||||||
assert_eq!(gfx.front_buffer()[0], Color::WHITE.raw());
|
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]
|
#[test]
|
||||||
fn render_frame_with_scene_applies_refreshes_before_composition() {
|
fn render_frame_with_scene_applies_refreshes_before_composition() {
|
||||||
let banks = Arc::new(MemoryBanks::new());
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
@ -774,8 +893,6 @@ mod tests {
|
|||||||
assert!(frame_composer.bind_scene(0));
|
assert!(frame_composer.bind_scene(0));
|
||||||
|
|
||||||
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
|
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.render_frame(&mut gfx);
|
||||||
gfx.present();
|
gfx.present();
|
||||||
@ -811,8 +928,6 @@ mod tests {
|
|||||||
let expected_update = expected_resolver.update(&scene, 40, 0);
|
let expected_update = expected_resolver.update(&scene, 40, 0);
|
||||||
|
|
||||||
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
|
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.render_frame(&mut gfx);
|
||||||
frame_composer.set_camera(40, 0);
|
frame_composer.set_camera(40, 0);
|
||||||
@ -844,8 +959,6 @@ mod tests {
|
|||||||
make_glyph_slot_index(&[(0, 0), (1, 1)]),
|
make_glyph_slot_index(&[(0, 0), (1, 1)]),
|
||||||
);
|
);
|
||||||
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
|
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));
|
assert!(frame_composer.bind_scene(0));
|
||||||
frame_composer.render_frame(&mut gfx);
|
frame_composer.render_frame(&mut gfx);
|
||||||
@ -923,8 +1036,6 @@ mod tests {
|
|||||||
assert!(frame_composer.bind_scene(0));
|
assert!(frame_composer.bind_scene(0));
|
||||||
|
|
||||||
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
|
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.render_frame(&mut gfx);
|
||||||
gfx.present();
|
gfx.present();
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
use crate::memory_banks::GlyphBankPoolAccess;
|
use crate::memory_banks::GlyphBankPoolAccess;
|
||||||
use prometeu_hal::GfxBridge;
|
|
||||||
use prometeu_hal::color::Color;
|
use prometeu_hal::color::Color;
|
||||||
use prometeu_hal::glyph::Glyph;
|
use prometeu_hal::glyph::Glyph;
|
||||||
use prometeu_hal::glyph_bank::GlyphBank;
|
use prometeu_hal::glyph_bank::GlyphBank;
|
||||||
use prometeu_hal::scene_viewport_cache::{CachedTileEntry, SceneViewportCache};
|
use prometeu_hal::scene_viewport_cache::{CachedTileEntry, SceneViewportCache};
|
||||||
use prometeu_hal::scene_viewport_resolver::{LayerCopyRequest, ResolverUpdate};
|
use prometeu_hal::scene_viewport_resolver::{LayerCopyRequest, ResolverUpdate};
|
||||||
use prometeu_hal::sprite::Sprite;
|
use prometeu_hal::sprite::Sprite;
|
||||||
|
use prometeu_hal::{Game2DFramePacket, Gfx2dCommand, GfxBridge, GfxUiCommand, ShellUiFramePacket};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// Blending modes inspired by classic 16-bit hardware.
|
/// 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
|
/// `Gfx` owns the framebuffer backend and the canonical game-frame raster path
|
||||||
/// consumed by `FrameComposer`. That canonical path covers scene composition,
|
/// 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 {
|
pub struct Gfx {
|
||||||
/// Width of the internal framebuffer in pixels.
|
/// Width of the internal framebuffer in pixels.
|
||||||
w: usize,
|
w: usize,
|
||||||
@ -49,15 +49,6 @@ pub struct Gfx {
|
|||||||
/// Hardware sprite list (512 slots). Equivalent to OAM (Object Attribute Memory).
|
/// Hardware sprite list (512 slots). Equivalent to OAM (Object Attribute Memory).
|
||||||
pub sprites: [Sprite; 512],
|
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.
|
/// Internal sprite count for the current frame state.
|
||||||
sprite_count: usize,
|
sprite_count: usize,
|
||||||
/// Internal cache used to sort sprites by layer while keeping stable priority order.
|
/// 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());
|
self.sprite_count = self.sprite_count.max(index.saturating_add(1)).min(self.sprites.len());
|
||||||
&mut self.sprites[index]
|
&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 {
|
impl Gfx {
|
||||||
@ -358,10 +324,6 @@ impl Gfx {
|
|||||||
glyph_banks,
|
glyph_banks,
|
||||||
sprites: [EMPTY_SPRITE; 512],
|
sprites: [EMPTY_SPRITE; 512],
|
||||||
sprite_count: 0,
|
sprite_count: 0,
|
||||||
scene_fade_level: 31,
|
|
||||||
scene_fade_color: Color::BLACK,
|
|
||||||
hud_fade_level: 31,
|
|
||||||
hud_fade_color: Color::BLACK,
|
|
||||||
layer_buckets: [
|
layer_buckets: [
|
||||||
Vec::with_capacity(128),
|
Vec::with_capacity(128),
|
||||||
Vec::with_capacity(128),
|
Vec::with_capacity(128),
|
||||||
@ -637,20 +599,13 @@ impl Gfx {
|
|||||||
&*self.glyph_banks,
|
&*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.
|
/// 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
|
/// 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
|
/// 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(
|
pub fn render_scene_from_cache(
|
||||||
&mut self,
|
&mut self,
|
||||||
cache: &SceneViewportCache,
|
cache: &SceneViewportCache,
|
||||||
@ -681,9 +636,88 @@ impl Gfx {
|
|||||||
&*self.glyph_banks,
|
&*self.glyph_banks,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Self::apply_fade_to_buffer(&mut self.back, self.scene_fade_level, self.scene_fade_color);
|
pub fn render_game2d_frame_packet(&mut self, packet: &Game2DFramePacket) {
|
||||||
Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color);
|
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) {
|
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) {
|
pub fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) {
|
||||||
let mut cx = x;
|
let mut cx = x;
|
||||||
let mut cy = y;
|
let mut cy = y;
|
||||||
@ -1230,8 +1240,6 @@ mod tests {
|
|||||||
let update = resolver.update(&scene, 0, 0);
|
let update = resolver.update(&scene, 0, 0);
|
||||||
|
|
||||||
let mut gfx = Gfx::new(16, 16, banks);
|
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]);
|
gfx.render_scene_from_cache(&cache, &update, &[0, 0, 0, 0]);
|
||||||
|
|
||||||
assert_eq!(gfx.back[0], Color::RED.raw());
|
assert_eq!(gfx.back[0], Color::RED.raw());
|
||||||
@ -1262,8 +1270,6 @@ mod tests {
|
|||||||
let update = resolver.update(&scene, 0, 0);
|
let update = resolver.update(&scene, 0, 0);
|
||||||
|
|
||||||
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
|
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 {
|
gfx.sprites[0] = Sprite {
|
||||||
glyph: Glyph { glyph_id: 0, palette_id: 4 },
|
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[0].layer, 1);
|
||||||
assert_eq!(gfx.sprites[1].glyph.glyph_id, 5);
|
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::cartridge::AssetsPayloadSource;
|
||||||
use prometeu_hal::sprite::Sprite;
|
use prometeu_hal::sprite::Sprite;
|
||||||
use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge};
|
use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge};
|
||||||
|
use prometeu_hal::{Game2DFramePacket, RenderSubmission, RenderSubmissionPacket};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// Aggregate structure for all virtual hardware peripherals.
|
/// Aggregate structure for all virtual hardware peripherals.
|
||||||
@ -67,9 +68,34 @@ impl HardwareBridge for Hardware {
|
|||||||
self.frame_composer.emit_sprite(sprite)
|
self.frame_composer.emit_sprite(sprite)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_frame(&mut self) {
|
fn close_game2d_packet(&self) -> Game2DFramePacket {
|
||||||
|
self.frame_composer.close_game2d_packet()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn publish_render_submission(&mut self, submission: &RenderSubmission) {
|
||||||
|
match &submission.packet {
|
||||||
|
RenderSubmissionPacket::Game2D(packet) => {
|
||||||
|
if self.frame_composer.active_scene_id().is_none() {
|
||||||
|
self.gfx.render_game2d_frame_packet(packet);
|
||||||
|
} else {
|
||||||
self.frame_composer.render_frame(&mut self.gfx);
|
self.frame_composer.render_frame(&mut self.gfx);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
RenderSubmissionPacket::ShellUi(packet) => {
|
||||||
|
self.gfx.render_shell_ui_frame_packet(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.gfx.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_frame(&mut self) {
|
||||||
|
if self.frame_composer.active_scene_id().is_none() {
|
||||||
|
let packet = self.frame_composer.close_game2d_packet();
|
||||||
|
self.gfx.render_game2d_frame_packet(&packet);
|
||||||
|
} else {
|
||||||
|
self.frame_composer.render_frame(&mut self.gfx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn has_glyph_bank(&self, bank_id: usize) -> bool {
|
fn has_glyph_bank(&self, bank_id: usize) -> bool {
|
||||||
self.gfx.glyph_banks.glyph_bank_slot(bank_id).is_some()
|
self.gfx.glyph_banks.glyph_bank_slot(bank_id).is_some()
|
||||||
@ -215,8 +241,6 @@ mod tests {
|
|||||||
let mut resolver = SceneViewportResolver::new(16, 16, 4, 4, 12, 20);
|
let mut resolver = SceneViewportResolver::new(16, 16, 4, 4, 12, 20);
|
||||||
let update = resolver.update(&scene, 0, 0);
|
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.render_scene_from_cache(&cache, &update, &[0, 0, 0, 0]);
|
||||||
hardware.gfx.present();
|
hardware.gfx.present();
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ mod touch;
|
|||||||
|
|
||||||
pub use crate::asset::AssetManager;
|
pub use crate::asset::AssetManager;
|
||||||
pub use crate::audio::{Audio, AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};
|
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::gfx::Gfx;
|
||||||
pub use crate::memory_banks::{
|
pub use crate::memory_banks::{
|
||||||
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess,
|
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess,
|
||||||
|
|||||||
@ -231,7 +231,7 @@ mod tests {
|
|||||||
debug_info: None,
|
debug_info: None,
|
||||||
exports: vec![],
|
exports: vec![],
|
||||||
syscalls: vec![SyscallDecl {
|
syscalls: vec![SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
|
|||||||
@ -2,6 +2,7 @@ use crate::firmware::firmware_state::{FirmwareState, LaunchHubStep};
|
|||||||
use crate::firmware::prometeu_context::PrometeuContext;
|
use crate::firmware::prometeu_context::PrometeuContext;
|
||||||
use prometeu_hal::color::Color;
|
use prometeu_hal::color::Color;
|
||||||
use prometeu_hal::log::{LogLevel, LogSource};
|
use prometeu_hal::log::{LogLevel, LogSource};
|
||||||
|
use prometeu_hal::{FrameId, GfxUiCommand, RenderSubmission, ShellUiFramePacket};
|
||||||
use prometeu_system::CrashReport;
|
use prometeu_system::CrashReport;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -22,11 +23,8 @@ impl AppCrashesStep {
|
|||||||
// Update peripherals for input on the crash screen
|
// Update peripherals for input on the crash screen
|
||||||
ctx.hw.pad_mut().begin_frame(ctx.signals);
|
ctx.hw.pad_mut().begin_frame(ctx.signals);
|
||||||
|
|
||||||
// Error screen: red background, white text
|
let packet = ShellUiFramePacket::new(vec![GfxUiCommand::Clear { color: Color::RED }]);
|
||||||
ctx.hw.gfx_mut().clear(Color::RED);
|
ctx.hw.publish_render_submission(&RenderSubmission::shell_ui(FrameId::ZERO, packet));
|
||||||
// For now we just log or show something simple
|
|
||||||
// In the future, use draw_text
|
|
||||||
ctx.hw.gfx_mut().present();
|
|
||||||
|
|
||||||
// If START is pressed, return to the Hub
|
// If START is pressed, return to the Hub
|
||||||
if ctx.hw.pad().start().down {
|
if ctx.hw.pad().start().down {
|
||||||
|
|||||||
@ -38,10 +38,6 @@ impl GameRunningStep {
|
|||||||
|
|
||||||
let result = ctx.os.vm().tick(ctx.vm, ctx.signals, ctx.hw);
|
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 {
|
if let Some(report) = result {
|
||||||
let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report));
|
let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report));
|
||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { 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 crate::firmware::prometeu_context::PrometeuContext;
|
||||||
use prometeu_hal::color::Color;
|
use prometeu_hal::color::Color;
|
||||||
use prometeu_hal::log::{LogLevel, LogSource};
|
use prometeu_hal::log::{LogLevel, LogSource};
|
||||||
|
use prometeu_hal::primitives::Rect;
|
||||||
|
use prometeu_hal::{FrameId, GfxUiCommand, RenderSubmission, ShellUiFramePacket};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SplashScreenStep {
|
pub struct SplashScreenStep {
|
||||||
@ -22,9 +24,6 @@ impl SplashScreenStep {
|
|||||||
// Update peripherals for input
|
// Update peripherals for input
|
||||||
ctx.hw.pad_mut().begin_frame(ctx.signals);
|
ctx.hw.pad_mut().begin_frame(ctx.signals);
|
||||||
|
|
||||||
// Clear screen
|
|
||||||
ctx.hw.gfx_mut().clear(Color::ORANGE);
|
|
||||||
|
|
||||||
// Draw expanding square
|
// Draw expanding square
|
||||||
let (sw, sh) = ctx.hw.gfx().size();
|
let (sw, sh) = ctx.hw.gfx().size();
|
||||||
let max_size = (sw.min(sh) as i32 / 2).max(1);
|
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 x = (sw as i32 - current_size) / 2;
|
||||||
let y = (sh 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);
|
let packet = ShellUiFramePacket::new(vec![
|
||||||
ctx.hw.gfx_mut().present();
|
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
|
// Transition logic
|
||||||
// If any button is pressed at any time after the animation ends
|
// If any button is pressed at any time after the animation ends
|
||||||
|
|||||||
@ -286,6 +286,9 @@ pub enum Capability {
|
|||||||
None,
|
None,
|
||||||
System,
|
System,
|
||||||
Gfx,
|
Gfx,
|
||||||
|
Composer,
|
||||||
|
Gfx2d,
|
||||||
|
Gfxui,
|
||||||
Audio,
|
Audio,
|
||||||
Fs,
|
Fs,
|
||||||
Log,
|
Log,
|
||||||
|
|||||||
@ -117,6 +117,9 @@ fn normalize_capabilities(capabilities: &[Capability]) -> Result<CapFlags, Cartr
|
|||||||
Capability::None => caps::NONE,
|
Capability::None => caps::NONE,
|
||||||
Capability::System => caps::SYSTEM,
|
Capability::System => caps::SYSTEM,
|
||||||
Capability::Gfx => caps::GFX,
|
Capability::Gfx => caps::GFX,
|
||||||
|
Capability::Composer => caps::COMPOSER,
|
||||||
|
Capability::Gfx2d => caps::GFX2D,
|
||||||
|
Capability::Gfxui => caps::GFXUI,
|
||||||
Capability::Audio => caps::AUDIO,
|
Capability::Audio => caps::AUDIO,
|
||||||
Capability::Fs => caps::FS,
|
Capability::Fs => caps::FS,
|
||||||
Capability::Log => caps::LOG,
|
Capability::Log => caps::LOG,
|
||||||
|
|||||||
@ -73,13 +73,4 @@ pub trait GfxBridge {
|
|||||||
|
|
||||||
fn sprite(&self, index: usize) -> &Sprite;
|
fn sprite(&self, index: usize) -> &Sprite;
|
||||||
fn sprite_mut(&mut self, index: usize) -> &mut 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::audio_bridge::AudioBridge;
|
||||||
use crate::gfx_bridge::GfxBridge;
|
use crate::gfx_bridge::GfxBridge;
|
||||||
use crate::pad_bridge::PadBridge;
|
use crate::pad_bridge::PadBridge;
|
||||||
|
use crate::render_submission::{Game2DFramePacket, RenderSubmission};
|
||||||
use crate::sprite::Sprite;
|
use crate::sprite::Sprite;
|
||||||
use crate::touch_bridge::TouchBridge;
|
use crate::touch_bridge::TouchBridge;
|
||||||
|
|
||||||
@ -11,6 +12,8 @@ pub trait HardwareBridge {
|
|||||||
fn unbind_scene(&mut self);
|
fn unbind_scene(&mut self);
|
||||||
fn set_camera(&mut self, x: i32, y: i32);
|
fn set_camera(&mut self, x: i32, y: i32);
|
||||||
fn emit_sprite(&mut self, sprite: Sprite) -> bool;
|
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 render_frame(&mut self);
|
||||||
fn has_glyph_bank(&self, bank_id: usize) -> bool;
|
fn has_glyph_bank(&self, bank_id: usize) -> bool;
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@ pub mod native_helpers;
|
|||||||
pub mod native_interface;
|
pub mod native_interface;
|
||||||
pub mod pad_bridge;
|
pub mod pad_bridge;
|
||||||
pub mod primitives;
|
pub mod primitives;
|
||||||
|
pub mod render_submission;
|
||||||
pub mod sample;
|
pub mod sample;
|
||||||
pub mod scene_bank;
|
pub mod scene_bank;
|
||||||
pub mod scene_layer;
|
pub mod scene_layer;
|
||||||
@ -45,4 +46,9 @@ pub use input_signals::InputSignals;
|
|||||||
pub use native_helpers::{expect_bool, expect_int};
|
pub use native_helpers::{expect_bool, expect_int};
|
||||||
pub use native_interface::{NativeInterface, SyscallId};
|
pub use native_interface::{NativeInterface, SyscallId};
|
||||||
pub use pad_bridge::PadBridge;
|
pub use pad_bridge::PadBridge;
|
||||||
|
pub use render_submission::{
|
||||||
|
BoundScenePacket, Camera2D, ComposerFramePacket, FrameId, Game2DFramePacket, GameSpritePacket,
|
||||||
|
Gfx2dCommand, GfxUiCommand, HudCommand, HudPacket, RenderSubmission, RenderSubmissionPacket,
|
||||||
|
ShellUiFramePacket,
|
||||||
|
};
|
||||||
pub use touch_bridge::TouchBridge;
|
pub use touch_bridge::TouchBridge;
|
||||||
|
|||||||
203
crates/console/prometeu-hal/src/render_submission.rs
Normal file
203
crates/console/prometeu-hal/src/render_submission.rs
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
use crate::app_mode::AppMode;
|
||||||
|
use crate::color::Color;
|
||||||
|
use crate::primitives::Rect;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||||
|
pub struct FrameId(pub u64);
|
||||||
|
|
||||||
|
impl FrameId {
|
||||||
|
pub const ZERO: Self = Self(0);
|
||||||
|
|
||||||
|
pub const fn new(value: u64) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn get(self) -> u64 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn next(self) -> Self {
|
||||||
|
Self(self.0.wrapping_add(1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct RenderSubmission {
|
||||||
|
pub frame_id: FrameId,
|
||||||
|
pub app_mode: AppMode,
|
||||||
|
pub packet: RenderSubmissionPacket,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderSubmission {
|
||||||
|
pub const fn game2d(frame_id: FrameId, packet: Game2DFramePacket) -> Self {
|
||||||
|
Self { frame_id, app_mode: AppMode::Game, packet: RenderSubmissionPacket::Game2D(packet) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn shell_ui(frame_id: FrameId, packet: ShellUiFramePacket) -> Self {
|
||||||
|
Self { frame_id, app_mode: AppMode::Shell, packet: RenderSubmissionPacket::ShellUi(packet) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn is_coherent(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
(self.app_mode, &self.packet),
|
||||||
|
(AppMode::Game, RenderSubmissionPacket::Game2D(_))
|
||||||
|
| (AppMode::Shell, RenderSubmissionPacket::ShellUi(_))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum RenderSubmissionPacket {
|
||||||
|
Game2D(Game2DFramePacket),
|
||||||
|
ShellUi(ShellUiFramePacket),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub struct Game2DFramePacket {
|
||||||
|
pub composer: ComposerFramePacket,
|
||||||
|
pub gfx2d: Vec<Gfx2dCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Game2DFramePacket {
|
||||||
|
pub fn new(composer: ComposerFramePacket, gfx2d: Vec<Gfx2dCommand>) -> Self {
|
||||||
|
Self { composer, gfx2d }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ShellUiFramePacket {
|
||||||
|
pub commands: Vec<GfxUiCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShellUiFramePacket {
|
||||||
|
pub fn new(commands: Vec<GfxUiCommand>) -> Self {
|
||||||
|
Self { commands }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub struct ComposerFramePacket {
|
||||||
|
pub bound_scene: Option<BoundScenePacket>,
|
||||||
|
pub camera: Camera2D,
|
||||||
|
pub sprites: Vec<GameSpritePacket>,
|
||||||
|
pub hud: HudPacket,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct BoundScenePacket {
|
||||||
|
pub bank_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub struct Camera2D {
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct GameSpritePacket {
|
||||||
|
pub glyph_id: i32,
|
||||||
|
pub palette_id: i32,
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
pub layer: i32,
|
||||||
|
pub bank_id: i32,
|
||||||
|
pub flip_x: bool,
|
||||||
|
pub flip_y: bool,
|
||||||
|
pub priority: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub struct HudPacket {
|
||||||
|
pub commands: Vec<HudCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum HudCommand {
|
||||||
|
Clear { color: Color },
|
||||||
|
FillRect { rect: Rect, color: Color },
|
||||||
|
DrawText { x: i32, y: i32, text: String, color: Color },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Gfx2dCommand {
|
||||||
|
Clear { color: Color },
|
||||||
|
FillRect { rect: Rect, color: Color },
|
||||||
|
DrawLine { x0: i32, y0: i32, x1: i32, y1: i32, color: Color },
|
||||||
|
DrawCircle { x: i32, y: i32, radius: i32, color: Color },
|
||||||
|
DrawDisc { x: i32, y: i32, radius: i32, border_color: Color, fill_color: Color },
|
||||||
|
DrawSquare { rect: Rect, border_color: Color, fill_color: Color },
|
||||||
|
DrawText { x: i32, y: i32, text: String, color: Color },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum GfxUiCommand {
|
||||||
|
Clear { color: Color },
|
||||||
|
FillRect { rect: Rect, color: Color },
|
||||||
|
DrawLine { x0: i32, y0: i32, x1: i32, y1: i32, color: Color },
|
||||||
|
DrawCircle { x: i32, y: i32, radius: i32, color: Color },
|
||||||
|
DrawDisc { x: i32, y: i32, radius: i32, border_color: Color, fill_color: Color },
|
||||||
|
DrawSquare { rect: Rect, border_color: Color, fill_color: Color },
|
||||||
|
DrawText { x: i32, y: i32, text: String, color: Color },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn constructors_pin_app_mode_to_packet_kind() {
|
||||||
|
let game = RenderSubmission::game2d(FrameId::new(7), Game2DFramePacket::default());
|
||||||
|
assert_eq!(game.app_mode, AppMode::Game);
|
||||||
|
assert!(matches!(game.packet, RenderSubmissionPacket::Game2D(_)));
|
||||||
|
assert!(game.is_coherent());
|
||||||
|
|
||||||
|
let shell =
|
||||||
|
RenderSubmission::shell_ui(FrameId::new(8), ShellUiFramePacket::new(Vec::new()));
|
||||||
|
assert_eq!(shell.app_mode, AppMode::Shell);
|
||||||
|
assert!(matches!(shell.packet, RenderSubmissionPacket::ShellUi(_)));
|
||||||
|
assert!(shell.is_coherent());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn coherence_check_rejects_mismatched_envelope() {
|
||||||
|
let submission = RenderSubmission {
|
||||||
|
frame_id: FrameId::new(1),
|
||||||
|
app_mode: AppMode::Game,
|
||||||
|
packet: RenderSubmissionPacket::ShellUi(ShellUiFramePacket::new(Vec::new())),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(!submission.is_coherent());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn closed_submission_owns_command_payload() {
|
||||||
|
let mut commands = vec![Gfx2dCommand::DrawText {
|
||||||
|
x: 1,
|
||||||
|
y: 2,
|
||||||
|
text: "before".to_string(),
|
||||||
|
color: Color::WHITE,
|
||||||
|
}];
|
||||||
|
let submission = RenderSubmission::game2d(
|
||||||
|
FrameId::new(2),
|
||||||
|
Game2DFramePacket::new(Default::default(), commands.clone()),
|
||||||
|
);
|
||||||
|
|
||||||
|
commands.push(Gfx2dCommand::Clear { color: Color::BLACK });
|
||||||
|
|
||||||
|
let RenderSubmissionPacket::Game2D(packet) = submission.packet else {
|
||||||
|
panic!("expected game packet");
|
||||||
|
};
|
||||||
|
assert_eq!(packet.gfx2d.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
packet.gfx2d[0],
|
||||||
|
Gfx2dCommand::DrawText { x: 1, y: 2, text: "before".to_string(), color: Color::WHITE }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_id_advances_monotonically_with_wrapping_arithmetic() {
|
||||||
|
assert_eq!(FrameId::new(41).next(), FrameId::new(42));
|
||||||
|
assert_eq!(FrameId::new(u64::MAX).next(), FrameId::ZERO);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,7 +18,7 @@ pub use resolver::{
|
|||||||
///
|
///
|
||||||
/// Each Syscall has a unique 32-bit ID. The IDs are grouped by category:
|
/// Each Syscall has a unique 32-bit ID. The IDs are grouped by category:
|
||||||
/// - **0x0xxx**: System & OS Control
|
/// - **0x0xxx**: System & OS Control
|
||||||
/// - **0x1xxx**: Graphics (GFX)
|
/// - **0x1xxx**: Game 2D primitives (`gfx2d`) and Shell UI primitives (`gfxui`)
|
||||||
/// - **0x11xx**: Frame Composer orchestration
|
/// - **0x11xx**: Frame Composer orchestration
|
||||||
/// - **0x2xxx**: Reserved for legacy input syscalls (disabled for v1 VM-owned input)
|
/// - **0x2xxx**: Reserved for legacy input syscalls (disabled for v1 VM-owned input)
|
||||||
/// - **0x3xxx**: Audio (PCM & Mixing)
|
/// - **0x3xxx**: Audio (PCM & Mixing)
|
||||||
@ -37,6 +37,13 @@ pub enum Syscall {
|
|||||||
GfxDrawDisc = 0x1005,
|
GfxDrawDisc = 0x1005,
|
||||||
GfxDrawSquare = 0x1006,
|
GfxDrawSquare = 0x1006,
|
||||||
GfxDrawText = 0x1008,
|
GfxDrawText = 0x1008,
|
||||||
|
GfxUiClear = 0x1201,
|
||||||
|
GfxUiFillRect = 0x1202,
|
||||||
|
GfxUiDrawLine = 0x1203,
|
||||||
|
GfxUiDrawCircle = 0x1204,
|
||||||
|
GfxUiDrawDisc = 0x1205,
|
||||||
|
GfxUiDrawSquare = 0x1206,
|
||||||
|
GfxUiDrawText = 0x1208,
|
||||||
ComposerBindScene = 0x1101,
|
ComposerBindScene = 0x1101,
|
||||||
ComposerUnbindScene = 0x1102,
|
ComposerUnbindScene = 0x1102,
|
||||||
ComposerSetCamera = 0x1103,
|
ComposerSetCamera = 0x1103,
|
||||||
|
|||||||
@ -2,10 +2,13 @@ use super::CapFlags;
|
|||||||
|
|
||||||
pub const NONE: CapFlags = 0;
|
pub const NONE: CapFlags = 0;
|
||||||
pub const SYSTEM: CapFlags = 1 << 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 AUDIO: CapFlags = 1 << 2;
|
||||||
pub const FS: CapFlags = 1 << 3;
|
pub const FS: CapFlags = 1 << 3;
|
||||||
pub const LOG: CapFlags = 1 << 4;
|
pub const LOG: CapFlags = 1 << 4;
|
||||||
pub const ASSET: CapFlags = 1 << 5;
|
pub const ASSET: CapFlags = 1 << 5;
|
||||||
pub const BANK: CapFlags = 1 << 6;
|
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")
|
SyscallRegistryEntry::builder(Syscall::ComposerBindScene, "composer", "bind_scene")
|
||||||
.args(1)
|
.args(1)
|
||||||
.rets(1)
|
.rets(1)
|
||||||
.caps(caps::GFX)
|
.caps(caps::COMPOSER)
|
||||||
.cost(5),
|
.cost(5),
|
||||||
SyscallRegistryEntry::builder(Syscall::ComposerUnbindScene, "composer", "unbind_scene")
|
SyscallRegistryEntry::builder(Syscall::ComposerUnbindScene, "composer", "unbind_scene")
|
||||||
.rets(1)
|
.rets(1)
|
||||||
.caps(caps::GFX)
|
.caps(caps::COMPOSER)
|
||||||
.cost(2),
|
.cost(2),
|
||||||
SyscallRegistryEntry::builder(Syscall::ComposerSetCamera, "composer", "set_camera")
|
SyscallRegistryEntry::builder(Syscall::ComposerSetCamera, "composer", "set_camera")
|
||||||
.args(2)
|
.args(2)
|
||||||
.caps(caps::GFX)
|
.caps(caps::COMPOSER)
|
||||||
.cost(2),
|
.cost(2),
|
||||||
SyscallRegistryEntry::builder(Syscall::ComposerEmitSprite, "composer", "emit_sprite")
|
SyscallRegistryEntry::builder(Syscall::ComposerEmitSprite, "composer", "emit_sprite")
|
||||||
.args(9)
|
.args(9)
|
||||||
.rets(1)
|
.rets(1)
|
||||||
.caps(caps::GFX)
|
.caps(caps::COMPOSER)
|
||||||
.cost(5),
|
.cost(5),
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,32 +1,32 @@
|
|||||||
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
|
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
|
||||||
|
|
||||||
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxClear, "gfx", "clear")
|
SyscallRegistryEntry::builder(Syscall::GfxClear, "gfx2d", "clear")
|
||||||
.args(1)
|
.args(1)
|
||||||
.caps(caps::GFX)
|
.caps(caps::GFX2D)
|
||||||
.cost(20),
|
.cost(20),
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxFillRect, "gfx", "fill_rect")
|
SyscallRegistryEntry::builder(Syscall::GfxFillRect, "gfx2d", "fill_rect")
|
||||||
.args(5)
|
.args(5)
|
||||||
.caps(caps::GFX)
|
.caps(caps::GFX2D)
|
||||||
.cost(20),
|
.cost(20),
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxDrawLine, "gfx", "draw_line")
|
SyscallRegistryEntry::builder(Syscall::GfxDrawLine, "gfx2d", "draw_line")
|
||||||
.args(5)
|
.args(5)
|
||||||
.caps(caps::GFX)
|
.caps(caps::GFX2D)
|
||||||
.cost(5),
|
.cost(5),
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxDrawCircle, "gfx", "draw_circle")
|
SyscallRegistryEntry::builder(Syscall::GfxDrawCircle, "gfx2d", "draw_circle")
|
||||||
.args(4)
|
.args(4)
|
||||||
.caps(caps::GFX)
|
.caps(caps::GFX2D)
|
||||||
.cost(5),
|
.cost(5),
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxDrawDisc, "gfx", "draw_disc")
|
SyscallRegistryEntry::builder(Syscall::GfxDrawDisc, "gfx2d", "draw_disc")
|
||||||
.args(5)
|
.args(5)
|
||||||
.caps(caps::GFX)
|
.caps(caps::GFX2D)
|
||||||
.cost(5),
|
.cost(5),
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxDrawSquare, "gfx", "draw_square")
|
SyscallRegistryEntry::builder(Syscall::GfxDrawSquare, "gfx2d", "draw_square")
|
||||||
.args(6)
|
.args(6)
|
||||||
.caps(caps::GFX)
|
.caps(caps::GFX2D)
|
||||||
.cost(5),
|
.cost(5),
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxDrawText, "gfx", "draw_text")
|
SyscallRegistryEntry::builder(Syscall::GfxDrawText, "gfx2d", "draw_text")
|
||||||
.args(4)
|
.args(4)
|
||||||
.caps(caps::GFX)
|
.caps(caps::GFX2D)
|
||||||
.cost(20),
|
.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 composer;
|
||||||
mod fs;
|
mod fs;
|
||||||
mod gfx;
|
mod gfx;
|
||||||
|
mod gfxui;
|
||||||
mod log;
|
mod log;
|
||||||
mod system;
|
mod system;
|
||||||
|
|
||||||
@ -13,6 +14,7 @@ pub(crate) fn all_entries() -> impl Iterator<Item = &'static SyscallRegistryEntr
|
|||||||
system::ENTRIES
|
system::ENTRIES
|
||||||
.iter()
|
.iter()
|
||||||
.chain(gfx::ENTRIES.iter())
|
.chain(gfx::ENTRIES.iter())
|
||||||
|
.chain(gfxui::ENTRIES.iter())
|
||||||
.chain(composer::ENTRIES.iter())
|
.chain(composer::ENTRIES.iter())
|
||||||
.chain(audio::ENTRIES.iter())
|
.chain(audio::ENTRIES.iter())
|
||||||
.chain(fs::ENTRIES.iter())
|
.chain(fs::ENTRIES.iter())
|
||||||
|
|||||||
@ -21,6 +21,13 @@ impl Syscall {
|
|||||||
0x1005 => Some(Self::GfxDrawDisc),
|
0x1005 => Some(Self::GfxDrawDisc),
|
||||||
0x1006 => Some(Self::GfxDrawSquare),
|
0x1006 => Some(Self::GfxDrawSquare),
|
||||||
0x1008 => Some(Self::GfxDrawText),
|
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),
|
0x1101 => Some(Self::ComposerBindScene),
|
||||||
0x1102 => Some(Self::ComposerUnbindScene),
|
0x1102 => Some(Self::ComposerUnbindScene),
|
||||||
0x1103 => Some(Self::ComposerSetCamera),
|
0x1103 => Some(Self::ComposerSetCamera),
|
||||||
@ -70,6 +77,13 @@ impl Syscall {
|
|||||||
Self::GfxDrawDisc => "GfxDrawDisc",
|
Self::GfxDrawDisc => "GfxDrawDisc",
|
||||||
Self::GfxDrawSquare => "GfxDrawSquare",
|
Self::GfxDrawSquare => "GfxDrawSquare",
|
||||||
Self::GfxDrawText => "GfxDrawText",
|
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::ComposerBindScene => "ComposerBindScene",
|
||||||
Self::ComposerUnbindScene => "ComposerUnbindScene",
|
Self::ComposerUnbindScene => "ComposerUnbindScene",
|
||||||
Self::ComposerSetCamera => "ComposerSetCamera",
|
Self::ComposerSetCamera => "ComposerSetCamera",
|
||||||
|
|||||||
@ -39,23 +39,23 @@ fn every_syscall_has_metadata() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolver_returns_expected_id_for_known_identity() {
|
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.id, 0x1001);
|
||||||
assert_eq!(id.meta.module, "gfx");
|
assert_eq!(id.meta.module, "gfx2d");
|
||||||
assert_eq!(id.meta.name, "clear");
|
assert_eq!(id.meta.name, "clear");
|
||||||
assert_eq!(id.meta.version, 1);
|
assert_eq!(id.meta.version, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolver_rejects_unknown_identity() {
|
fn resolver_rejects_unknown_identity() {
|
||||||
let res = resolve_syscall("gfx", "nonexistent", 1);
|
let res = resolve_syscall("gfx2d", "nonexistent", 1);
|
||||||
assert!(res.is_none());
|
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();
|
let err = resolve_program_syscalls(&requested, 0).unwrap_err();
|
||||||
match err {
|
match err {
|
||||||
LoadError::UnknownSyscall { module, name, version } => {
|
LoadError::UnknownSyscall { module, name, version } => {
|
||||||
assert_eq!(module, "gfx");
|
assert_eq!(module, "gfx2d");
|
||||||
assert_eq!(name, "nonexistent");
|
assert_eq!(name, "nonexistent");
|
||||||
assert_eq!(version, 1);
|
assert_eq!(version, 1);
|
||||||
}
|
}
|
||||||
@ -65,23 +65,23 @@ fn resolver_rejects_unknown_identity() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolver_rejects_removed_legacy_gfx_set_sprite_identity() {
|
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();
|
let err = resolve_program_syscalls(&requested, caps::ALL).unwrap_err();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
err,
|
err,
|
||||||
LoadError::UnknownSyscall { module: "gfx".into(), name: "set_sprite".into(), version: 1 }
|
LoadError::UnknownSyscall { module: "gfx2d".into(), name: "set_sprite".into(), version: 1 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolver_enforces_capabilities() {
|
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();
|
let err = resolve_program_syscalls(&requested, 0).unwrap_err();
|
||||||
match err {
|
match err {
|
||||||
LoadError::MissingCapability { required, provided, module, name, version } => {
|
LoadError::MissingCapability { required, provided, module, name, version } => {
|
||||||
assert_eq!(module, "gfx");
|
assert_eq!(module, "gfx2d");
|
||||||
assert_eq!(name, "clear");
|
assert_eq!(name, "clear");
|
||||||
assert_eq!(version, 1);
|
assert_eq!(version, 1);
|
||||||
assert_ne!(required, 0);
|
assert_ne!(required, 0);
|
||||||
@ -90,15 +90,30 @@ fn resolver_enforces_capabilities() {
|
|||||||
_ => panic!("expected MissingCapability error"),
|
_ => 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.len(), 1);
|
||||||
assert_eq!(ok[0].id, 0x1001);
|
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]
|
#[test]
|
||||||
fn declared_resolver_returns_expected_id_for_known_identity() {
|
fn declared_resolver_returns_expected_id_for_known_identity() {
|
||||||
let declared = [prometeu_bytecode::SyscallDecl {
|
let declared = [prometeu_bytecode::SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
@ -106,7 +121,7 @@ fn declared_resolver_returns_expected_id_for_known_identity() {
|
|||||||
}];
|
}];
|
||||||
|
|
||||||
let ok =
|
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.len(), 1);
|
||||||
assert_eq!(ok[0].id, 0x1001);
|
assert_eq!(ok[0].id, 0x1001);
|
||||||
}
|
}
|
||||||
@ -114,18 +129,18 @@ fn declared_resolver_returns_expected_id_for_known_identity() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn declared_resolver_rejects_unknown_identity() {
|
fn declared_resolver_rejects_unknown_identity() {
|
||||||
let declared = [prometeu_bytecode::SyscallDecl {
|
let declared = [prometeu_bytecode::SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "nonexistent".into(),
|
name: "nonexistent".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
ret_slots: 0,
|
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!(
|
assert_eq!(
|
||||||
err,
|
err,
|
||||||
DeclaredLoadError::UnknownSyscall {
|
DeclaredLoadError::UnknownSyscall {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "nonexistent".into(),
|
name: "nonexistent".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
}
|
}
|
||||||
@ -135,7 +150,7 @@ fn declared_resolver_rejects_unknown_identity() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn declared_resolver_rejects_missing_capability() {
|
fn declared_resolver_rejects_missing_capability() {
|
||||||
let declared = [prometeu_bytecode::SyscallDecl {
|
let declared = [prometeu_bytecode::SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
@ -146,9 +161,9 @@ fn declared_resolver_rejects_missing_capability() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
err,
|
err,
|
||||||
DeclaredLoadError::MissingCapability {
|
DeclaredLoadError::MissingCapability {
|
||||||
required: caps::GFX,
|
required: caps::GFX2D,
|
||||||
provided: caps::NONE,
|
provided: caps::NONE,
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
}
|
}
|
||||||
@ -158,18 +173,18 @@ fn declared_resolver_rejects_missing_capability() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn declared_resolver_rejects_abi_mismatch() {
|
fn declared_resolver_rejects_abi_mismatch() {
|
||||||
let declared = [prometeu_bytecode::SyscallDecl {
|
let declared = [prometeu_bytecode::SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "draw_line".into(),
|
name: "draw_line".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 4,
|
arg_slots: 4,
|
||||||
ret_slots: 0,
|
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!(
|
assert_eq!(
|
||||||
err,
|
err,
|
||||||
DeclaredLoadError::AbiMismatch {
|
DeclaredLoadError::AbiMismatch {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "draw_line".into(),
|
name: "draw_line".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
declared_arg_slots: 4,
|
declared_arg_slots: 4,
|
||||||
@ -386,7 +401,7 @@ fn declared_resolver_accepts_mixed_status_first_surface_as_a_single_module() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let resolved =
|
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");
|
.expect("mixed status-first surface must resolve together");
|
||||||
|
|
||||||
assert_eq!(resolved.len(), declared.len());
|
assert_eq!(resolved.len(), declared.len());
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
use crate::{CrashReport, SystemOS};
|
use crate::{CrashReport, SystemOS};
|
||||||
use prometeu_hal::color::Color;
|
use prometeu_hal::color::Color;
|
||||||
use prometeu_hal::gfx_bridge::GfxBridge;
|
|
||||||
use prometeu_hal::primitives::Rect;
|
use prometeu_hal::primitives::Rect;
|
||||||
use prometeu_hal::{HardwareBridge, InputSignals};
|
use prometeu_hal::{
|
||||||
|
FrameId, GfxUiCommand, HardwareBridge, InputSignals, RenderSubmission, ShellUiFramePacket,
|
||||||
|
};
|
||||||
use prometeu_vm::VirtualMachine;
|
use prometeu_vm::VirtualMachine;
|
||||||
|
|
||||||
const SHELL_A_BUTTON: Rect = Rect { x: 112, y: 108, w: 112, h: 48 };
|
const SHELL_A_BUTTON: Rect = Rect { x: 112, y: 108, w: 112, h: 48 };
|
||||||
@ -101,8 +102,6 @@ impl PrometeuHub {
|
|||||||
os: &mut SystemOS,
|
os: &mut SystemOS,
|
||||||
hw: &mut dyn HardwareBridge,
|
hw: &mut dyn HardwareBridge,
|
||||||
) -> Option<SystemProfileAction> {
|
) -> Option<SystemProfileAction> {
|
||||||
hw.gfx_mut().clear(Color::INDIGO);
|
|
||||||
|
|
||||||
let in_shell = os.windows().focused_window().is_some();
|
let in_shell = os.windows().focused_window().is_some();
|
||||||
|
|
||||||
if !in_shell {
|
if !in_shell {
|
||||||
@ -127,14 +126,13 @@ impl PrometeuHub {
|
|||||||
let pointer_x = hw.touch().x();
|
let pointer_x = hw.touch().x();
|
||||||
let pointer_y = hw.touch().y();
|
let pointer_y = hw.touch().y();
|
||||||
|
|
||||||
if os.windows().window_count() == 0 {
|
let packet = if os.windows().window_count() == 0 {
|
||||||
render_home(hw.gfx_mut(), pointer_x, pointer_y);
|
render_home_packet(pointer_x, pointer_y)
|
||||||
return;
|
} else {
|
||||||
}
|
let mut commands = Vec::new();
|
||||||
|
|
||||||
for window in os.windows().windows() {
|
for window in os.windows().windows() {
|
||||||
render_shell_window(
|
append_shell_window_commands(
|
||||||
hw.gfx_mut(),
|
&mut commands,
|
||||||
window.viewport,
|
window.viewport,
|
||||||
window.color,
|
window.color,
|
||||||
&window.title,
|
&window.title,
|
||||||
@ -142,6 +140,9 @@ impl PrometeuHub {
|
|||||||
pointer_y,
|
pointer_y,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
ShellUiFramePacket::new(commands)
|
||||||
|
};
|
||||||
|
hw.publish_render_submission(&RenderSubmission::shell_ui(FrameId::ZERO, packet));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_shell_profile(
|
pub fn update_shell_profile(
|
||||||
@ -163,7 +164,6 @@ impl PrometeuHub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.render(os, hw);
|
self.render(os, hw);
|
||||||
hw.gfx_mut().present();
|
|
||||||
|
|
||||||
SystemProfileUpdate { crash, action }
|
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
|
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) {
|
fn render_home_packet(pointer_x: i32, pointer_y: i32) -> ShellUiFramePacket {
|
||||||
gfx.fill_rect(VIEWPORT.x, VIEWPORT.y, VIEWPORT.w, VIEWPORT.h, COLOR_BG);
|
let mut commands = Vec::new();
|
||||||
draw_panel(gfx, TITLE_PANEL, COLOR_PANEL_DARK, COLOR_BORDER);
|
commands.push(GfxUiCommand::Clear { color: COLOR_BG });
|
||||||
draw_buffer_text(gfx, TITLE_PANEL.x + 58, TITLE_PANEL.y + 9, "PROMETEU HUB", COLOR_TEXT);
|
fill_rect(&mut commands, VIEWPORT, COLOR_BG);
|
||||||
|
draw_panel(&mut commands, TITLE_PANEL, COLOR_PANEL_DARK, COLOR_BORDER);
|
||||||
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);
|
|
||||||
draw_buffer_text(
|
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.x + 26,
|
||||||
HOME_PANEL.y + 40,
|
HOME_PANEL.y + 40,
|
||||||
"first retro ui slice",
|
"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 {
|
for button in HOME_BUTTONS {
|
||||||
let hovered = rect_contains(button.rect, pointer_x, pointer_y);
|
let hovered = rect_contains(button.rect, pointer_x, pointer_y);
|
||||||
draw_button(gfx, button.rect, button.label, hovered);
|
draw_button(&mut commands, button.rect, button.label, hovered);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_shell_window(
|
ShellUiFramePacket::new(commands)
|
||||||
gfx: &mut dyn GfxBridge,
|
}
|
||||||
|
|
||||||
|
fn append_shell_window_commands(
|
||||||
|
commands: &mut Vec<GfxUiCommand>,
|
||||||
viewport: Rect,
|
viewport: Rect,
|
||||||
color: Color,
|
color: Color,
|
||||||
title: &str,
|
title: &str,
|
||||||
pointer_x: i32,
|
pointer_x: i32,
|
||||||
pointer_y: i32,
|
pointer_y: i32,
|
||||||
) {
|
) {
|
||||||
gfx.fill_rect(VIEWPORT.x, VIEWPORT.y, VIEWPORT.w, VIEWPORT.h, COLOR_BG);
|
fill_rect(commands, VIEWPORT, COLOR_BG);
|
||||||
draw_panel(gfx, viewport, COLOR_PANEL_DARK, COLOR_BORDER);
|
draw_panel(commands, viewport, COLOR_PANEL_DARK, COLOR_BORDER);
|
||||||
gfx.fill_rect(viewport.x + 4, viewport.y + 24, viewport.w - 8, viewport.h - 28, color);
|
fill_rect(
|
||||||
draw_buffer_text(gfx, viewport.x + 10, viewport.y + 8, title, COLOR_TEXT);
|
commands,
|
||||||
draw_buffer_text(gfx, viewport.x + 18, viewport.y + 44, "native fake shell", COLOR_PANEL_DARK);
|
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_hovered = rect_contains(CLOSE_BUTTON, pointer_x, pointer_y);
|
||||||
let close_fill = if close_hovered { COLOR_HILITE } else { COLOR_BUTTON };
|
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);
|
fill_rect(commands, CLOSE_BUTTON, close_fill);
|
||||||
gfx.draw_rect(CLOSE_BUTTON.x, CLOSE_BUTTON.y, CLOSE_BUTTON.w, CLOSE_BUTTON.h, COLOR_BORDER);
|
draw_rect(commands, CLOSE_BUTTON, COLOR_BORDER);
|
||||||
draw_buffer_text(gfx, CLOSE_BUTTON.x + 5, CLOSE_BUTTON.y + 4, "x", COLOR_TEXT);
|
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) {
|
fn fill_rect(commands: &mut Vec<GfxUiCommand>, rect: Rect, color: Color) {
|
||||||
gfx.fill_rect(rect.x, rect.y, rect.w, rect.h, fill);
|
commands.push(GfxUiCommand::FillRect { rect, color });
|
||||||
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 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 fill = if hovered { COLOR_BUTTON_ACTIVE } else { COLOR_BUTTON };
|
||||||
let border = if hovered { COLOR_HILITE } else { COLOR_BORDER };
|
let border = if hovered { COLOR_HILITE } else { COLOR_BORDER };
|
||||||
gfx.fill_rect(rect.x, rect.y, rect.w, rect.h, fill);
|
fill_rect(commands, rect, fill);
|
||||||
gfx.draw_rect(rect.x, rect.y, rect.w, rect.h, border);
|
draw_rect(commands, rect, border);
|
||||||
gfx.draw_rect(rect.x + 3, rect.y + 3, rect.w - 6, rect.h - 6, COLOR_MUTED);
|
draw_rect(
|
||||||
gfx.fill_rect(rect.x + 12, rect.y + 12, 16, 24, border);
|
commands,
|
||||||
gfx.draw_rect(rect.x + 34, rect.y + 12, 62, 24, COLOR_PANEL_DARK);
|
Rect { x: rect.x + 3, y: rect.y + 3, w: rect.w - 6, h: rect.h - 6 },
|
||||||
draw_buffer_text(gfx, rect.x + 40, rect.y + 21, label, COLOR_TEXT);
|
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) {
|
fn draw_buffer_text(commands: &mut Vec<GfxUiCommand>, x: i32, y: i32, text: &str, color: Color) {
|
||||||
gfx.draw_text(x, y, text, color);
|
commands.push(GfxUiCommand::DrawText { x, y, text: text.to_string(), color });
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -321,6 +390,16 @@ mod tests {
|
|||||||
assert_eq!(HOME_BUTTONS[1].app, NativeShellApp::ShellB);
|
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]
|
#[test]
|
||||||
fn shell_close_button_sits_inside_shell_frame() {
|
fn shell_close_button_sits_inside_shell_frame() {
|
||||||
assert!(rect_contains(SHELL_FRAME, CLOSE_BUTTON.x, CLOSE_BUTTON.y));
|
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::color::Color;
|
||||||
use prometeu_hal::glyph::Glyph;
|
use prometeu_hal::glyph::Glyph;
|
||||||
use prometeu_hal::log::{LogLevel, LogSource};
|
use prometeu_hal::log::{LogLevel, LogSource};
|
||||||
|
use prometeu_hal::primitives::Rect;
|
||||||
use prometeu_hal::sprite::Sprite;
|
use prometeu_hal::sprite::Sprite;
|
||||||
use prometeu_hal::syscalls::Syscall;
|
use prometeu_hal::syscalls::Syscall;
|
||||||
use prometeu_hal::vm_fault::VmFault;
|
use prometeu_hal::vm_fault::VmFault;
|
||||||
use prometeu_hal::{
|
use prometeu_hal::{
|
||||||
AudioOpStatus, ComposerOpStatus, HostContext, HostReturn, NativeInterface, SyscallId,
|
AudioOpStatus, ComposerOpStatus, Gfx2dCommand, GfxUiCommand, HostContext, HostReturn,
|
||||||
expect_bool, expect_int,
|
NativeInterface, SyscallId, expect_bool, expect_int,
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
@ -185,6 +186,19 @@ impl NativeInterface for VmRuntimeHost<'_> {
|
|||||||
Syscall::SystemRunCart => unreachable!(),
|
Syscall::SystemRunCart => unreachable!(),
|
||||||
Syscall::GfxClear => {
|
Syscall::GfxClear => {
|
||||||
let color = self.get_color(expect_int(args, 0)?)?;
|
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);
|
hw.gfx_mut().clear(color);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -194,6 +208,25 @@ impl NativeInterface for VmRuntimeHost<'_> {
|
|||||||
let w = expect_int(args, 2)? as i32;
|
let w = expect_int(args, 2)? as i32;
|
||||||
let h = expect_int(args, 3)? as i32;
|
let h = expect_int(args, 3)? as i32;
|
||||||
let color = self.get_color(expect_int(args, 4)?)?;
|
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);
|
hw.gfx_mut().fill_rect(x, y, w, h, color);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -203,6 +236,35 @@ impl NativeInterface for VmRuntimeHost<'_> {
|
|||||||
let x2 = expect_int(args, 2)? as i32;
|
let x2 = expect_int(args, 2)? as i32;
|
||||||
let y2 = expect_int(args, 3)? as i32;
|
let y2 = expect_int(args, 3)? as i32;
|
||||||
let color = self.get_color(expect_int(args, 4)?)?;
|
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);
|
hw.gfx_mut().draw_line(x1, y1, x2, y2, color);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -211,6 +273,22 @@ impl NativeInterface for VmRuntimeHost<'_> {
|
|||||||
let y = expect_int(args, 1)? as i32;
|
let y = expect_int(args, 1)? as i32;
|
||||||
let r = expect_int(args, 2)? as i32;
|
let r = expect_int(args, 2)? as i32;
|
||||||
let color = self.get_color(expect_int(args, 3)?)?;
|
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);
|
hw.gfx_mut().draw_circle(x, y, r, color);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -220,6 +298,35 @@ impl NativeInterface for VmRuntimeHost<'_> {
|
|||||||
let r = expect_int(args, 2)? as i32;
|
let r = expect_int(args, 2)? as i32;
|
||||||
let border_color = self.get_color(expect_int(args, 3)?)?;
|
let border_color = self.get_color(expect_int(args, 3)?)?;
|
||||||
let fill_color = self.get_color(expect_int(args, 4)?)?;
|
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);
|
hw.gfx_mut().draw_disc(x, y, r, border_color, fill_color);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -230,6 +337,32 @@ impl NativeInterface for VmRuntimeHost<'_> {
|
|||||||
let h = expect_int(args, 3)? as i32;
|
let h = expect_int(args, 3)? as i32;
|
||||||
let border_color = self.get_color(expect_int(args, 4)?)?;
|
let border_color = self.get_color(expect_int(args, 4)?)?;
|
||||||
let fill_color = self.get_color(expect_int(args, 5)?)?;
|
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);
|
hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -238,6 +371,22 @@ impl NativeInterface for VmRuntimeHost<'_> {
|
|||||||
let y = expect_int(args, 1)? as i32;
|
let y = expect_int(args, 1)? as i32;
|
||||||
let msg = expect_string(args, 2, "message")?;
|
let msg = expect_string(args, 2, "message")?;
|
||||||
let color = self.get_color(expect_int(args, 3)?)?;
|
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);
|
hw.gfx_mut().draw_text(x, y, &msg, color);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,9 @@ impl VirtualMachineRuntime {
|
|||||||
current_cartridge_title: String::new(),
|
current_cartridge_title: String::new(),
|
||||||
current_cartridge_app_version: String::new(),
|
current_cartridge_app_version: String::new(),
|
||||||
current_cartridge_app_mode: AppMode::Game,
|
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(),
|
logs_written_this_frame: HashMap::new(),
|
||||||
atomic_telemetry,
|
atomic_telemetry,
|
||||||
last_crash_report: None,
|
last_crash_report: None,
|
||||||
@ -116,6 +119,9 @@ impl VirtualMachineRuntime {
|
|||||||
self.current_cartridge_title.clear();
|
self.current_cartridge_title.clear();
|
||||||
self.current_cartridge_app_version.clear();
|
self.current_cartridge_app_version.clear();
|
||||||
self.current_cartridge_app_mode = AppMode::Game;
|
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.logs_written_this_frame.clear();
|
||||||
|
|
||||||
self.last_crash_report = None;
|
self.last_crash_report = None;
|
||||||
@ -148,6 +154,7 @@ impl VirtualMachineRuntime {
|
|||||||
self.current_cartridge_title = cartridge.title.clone();
|
self.current_cartridge_title = cartridge.title.clone();
|
||||||
self.current_cartridge_app_version = cartridge.app_version.clone();
|
self.current_cartridge_app_version = cartridge.app_version.clone();
|
||||||
self.current_cartridge_app_mode = cartridge.app_mode;
|
self.current_cartridge_app_mode = cartridge.app_mode;
|
||||||
|
self.render_manager.set_active_app_mode(cartridge.app_mode);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
mod dispatch;
|
mod dispatch;
|
||||||
mod lifecycle;
|
mod lifecycle;
|
||||||
|
pub mod render_manager;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
mod tick;
|
mod tick;
|
||||||
@ -9,7 +10,9 @@ use prometeu_bytecode::string_materialization_count;
|
|||||||
use prometeu_hal::app_mode::AppMode;
|
use prometeu_hal::app_mode::AppMode;
|
||||||
use prometeu_hal::log::LogService;
|
use prometeu_hal::log::LogService;
|
||||||
use prometeu_hal::telemetry::{AtomicTelemetry, CertificationConfig, Certifier};
|
use prometeu_hal::telemetry::{AtomicTelemetry, CertificationConfig, Certifier};
|
||||||
|
use prometeu_hal::{Gfx2dCommand, GfxUiCommand};
|
||||||
use prometeu_vm::VirtualMachine;
|
use prometeu_vm::VirtualMachine;
|
||||||
|
pub use render_manager::RenderManager;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::AtomicU32;
|
use std::sync::atomic::AtomicU32;
|
||||||
@ -25,6 +28,9 @@ pub struct VirtualMachineRuntime {
|
|||||||
pub current_cartridge_title: String,
|
pub current_cartridge_title: String,
|
||||||
pub current_cartridge_app_version: String,
|
pub current_cartridge_app_version: String,
|
||||||
pub current_cartridge_app_mode: AppMode,
|
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 logs_written_this_frame: HashMap<u32, u32>,
|
||||||
pub atomic_telemetry: Arc<AtomicTelemetry>,
|
pub atomic_telemetry: Arc<AtomicTelemetry>,
|
||||||
pub last_crash_report: Option<CrashReport>,
|
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(
|
let program = serialized_single_function_module(
|
||||||
code,
|
code,
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
@ -221,7 +221,7 @@ fn initialize_vm_succeeds_when_cartridge_capabilities_cover_hostcalls() {
|
|||||||
let program = serialized_single_function_module(
|
let program = serialized_single_function_module(
|
||||||
code,
|
code,
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
@ -253,7 +253,7 @@ fn tick_returns_error_when_vm_ends_slice_with_trap() {
|
|||||||
let program = serialized_single_function_module(
|
let program = serialized_single_function_module(
|
||||||
code,
|
code,
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
@ -304,7 +304,7 @@ fn tick_system_profile_rejects_gfx_game_surface() {
|
|||||||
let program = serialized_single_function_module(
|
let program = serialized_single_function_module(
|
||||||
code,
|
code,
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 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]
|
#[test]
|
||||||
fn tick_system_profile_rejects_bank_game_surface() {
|
fn tick_system_profile_rejects_bank_game_surface() {
|
||||||
let mut runtime = VirtualMachineRuntime::new(None);
|
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");
|
assert!(report.is_none(), "frame render path must not crash");
|
||||||
hardware.gfx.present();
|
|
||||||
assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw());
|
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];
|
let mut slots = [None; 16];
|
||||||
slots[0] = Some(0);
|
slots[0] = Some(0);
|
||||||
glyph_slot_index.rebuild_from_slots(&slots);
|
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");
|
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
|
||||||
let report = runtime.tick(
|
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!(report.is_none(), "public composer path must not crash");
|
||||||
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::Ok as i64)]);
|
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());
|
assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -655,7 +749,7 @@ fn tick_draw_text_survives_no_scene_frame_path() {
|
|||||||
code,
|
code,
|
||||||
vec![ConstantPoolEntry::String("I".into())],
|
vec![ConstantPoolEntry::String("I".into())],
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "draw_text".into(),
|
name: "draw_text".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 4,
|
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 cartridge = cartridge_with_program(program, caps::GFX);
|
||||||
let mut hardware = Hardware::new();
|
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");
|
runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize");
|
||||||
let report = runtime.tick(
|
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");
|
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());
|
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]
|
#[test]
|
||||||
fn initialize_vm_success_clears_previous_crash_report() {
|
fn initialize_vm_success_clears_previous_crash_report() {
|
||||||
let mut runtime = VirtualMachineRuntime::new(None);
|
let mut runtime = VirtualMachineRuntime::new(None);
|
||||||
@ -696,7 +870,7 @@ fn initialize_vm_success_clears_previous_crash_report() {
|
|||||||
let program = serialized_single_function_module(
|
let program = serialized_single_function_module(
|
||||||
code,
|
code,
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
@ -740,6 +914,9 @@ fn reset_clears_cartridge_scoped_runtime_state() {
|
|||||||
runtime.frame_start_heap_allocations = 11;
|
runtime.frame_start_heap_allocations = 11;
|
||||||
runtime.frame_start_string_materializations = 22;
|
runtime.frame_start_string_materializations = 22;
|
||||||
runtime.needs_prepare_entry_call = true;
|
runtime.needs_prepare_entry_call = true;
|
||||||
|
runtime
|
||||||
|
.gfxui_commands
|
||||||
|
.push(prometeu_hal::GfxUiCommand::Clear { color: Color::from_raw(0x11223344) });
|
||||||
|
|
||||||
runtime.reset(&mut vm);
|
runtime.reset(&mut vm);
|
||||||
|
|
||||||
@ -756,6 +933,7 @@ fn reset_clears_cartridge_scoped_runtime_state() {
|
|||||||
assert!(runtime.current_cartridge_app_version.is_empty());
|
assert!(runtime.current_cartridge_app_version.is_empty());
|
||||||
assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game);
|
assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game);
|
||||||
assert!(runtime.logs_written_this_frame.is_empty());
|
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.logs_count.load(Ordering::Relaxed), 0);
|
||||||
assert_eq!(runtime.atomic_telemetry.current_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);
|
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(
|
let good_program = serialized_single_function_module(
|
||||||
assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"),
|
assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"),
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
@ -878,7 +1056,7 @@ fn initialize_vm_failure_clears_previous_identity_and_handles() {
|
|||||||
let bad_program = serialized_single_function_module(
|
let bad_program = serialized_single_function_module(
|
||||||
assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"),
|
assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"),
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
|
|||||||
@ -1,16 +1,30 @@
|
|||||||
use super::dispatch::VmRuntimeHost;
|
use super::dispatch::VmRuntimeHost;
|
||||||
|
use super::render_manager::RenderSurface;
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::CrashReport;
|
use crate::CrashReport;
|
||||||
use crate::fs::{FsState, VirtualFS};
|
use crate::fs::{FsState, VirtualFS};
|
||||||
use crate::services::memcard::MemcardService;
|
use crate::services::memcard::MemcardService;
|
||||||
use prometeu_hal::asset::{BankTelemetry, BankType};
|
use prometeu_hal::asset::{BankTelemetry, BankType};
|
||||||
use prometeu_hal::log::{LogLevel, LogService, LogSource};
|
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 prometeu_vm::LogicalFrameEndingReason;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||||
use std::sync::atomic::Ordering;
|
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 {
|
impl VirtualMachineRuntime {
|
||||||
fn host_panic_payload_to_string(payload: Box<dyn std::any::Any + Send>) -> String {
|
fn host_panic_payload_to_string(payload: Box<dyn std::any::Any + Send>) -> String {
|
||||||
if let Some(message) = payload.downcast_ref::<String>() {
|
if let Some(message) = payload.downcast_ref::<String>() {
|
||||||
@ -220,7 +234,25 @@ impl VirtualMachineRuntime {
|
|||||||
if run.reason == LogicalFrameEndingReason::FrameSync
|
if run.reason == LogicalFrameEndingReason::FrameSync
|
||||||
|| run.reason == LogicalFrameEndingReason::EndOfRom
|
|| 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 message = Self::host_panic_payload_to_string(payload);
|
||||||
let report = CrashReport::VmPanic { message, pc: Some(vm.pc() as u32) };
|
let report = CrashReport::VmPanic { message, pc: Some(vm.pc() as u32) };
|
||||||
self.log(
|
self.log(
|
||||||
|
|||||||
@ -2707,7 +2707,7 @@ mod tests {
|
|||||||
let bytes = serialized_single_function_module(
|
let bytes = serialized_single_function_module(
|
||||||
code,
|
code,
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
@ -2733,7 +2733,7 @@ mod tests {
|
|||||||
let bytes = serialized_single_function_module(
|
let bytes = serialized_single_function_module(
|
||||||
code,
|
code,
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
@ -2765,14 +2765,14 @@ mod tests {
|
|||||||
code,
|
code,
|
||||||
vec![
|
vec![
|
||||||
SyscallDecl {
|
SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
ret_slots: 0,
|
ret_slots: 0,
|
||||||
},
|
},
|
||||||
SyscallDecl {
|
SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "draw_text".into(),
|
name: "draw_text".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 4,
|
arg_slots: 4,
|
||||||
@ -2788,7 +2788,7 @@ mod tests {
|
|||||||
Err(VmInitError::LoaderPatchFailed(
|
Err(VmInitError::LoaderPatchFailed(
|
||||||
crate::vm_init_error::LoaderPatchError::UnusedSyscallDecl {
|
crate::vm_init_error::LoaderPatchError::UnusedSyscallDecl {
|
||||||
sysc_index: 1,
|
sysc_index: 1,
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "draw_text".into(),
|
name: "draw_text".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
},
|
},
|
||||||
@ -2805,7 +2805,7 @@ mod tests {
|
|||||||
let bytes = serialized_single_function_module(
|
let bytes = serialized_single_function_module(
|
||||||
code,
|
code,
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
@ -2835,7 +2835,7 @@ mod tests {
|
|||||||
let bytes = serialized_single_function_module(
|
let bytes = serialized_single_function_module(
|
||||||
code,
|
code,
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "missing".into(),
|
name: "missing".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 0,
|
arg_slots: 0,
|
||||||
@ -2850,7 +2850,7 @@ mod tests {
|
|||||||
Err(VmInitError::LoaderPatchFailed(
|
Err(VmInitError::LoaderPatchFailed(
|
||||||
crate::vm_init_error::LoaderPatchError::ResolveFailed(
|
crate::vm_init_error::LoaderPatchError::ResolveFailed(
|
||||||
prometeu_hal::syscalls::DeclaredLoadError::UnknownSyscall {
|
prometeu_hal::syscalls::DeclaredLoadError::UnknownSyscall {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "missing".into(),
|
name: "missing".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
},
|
},
|
||||||
@ -2868,7 +2868,7 @@ mod tests {
|
|||||||
let bytes = serialized_single_function_module(
|
let bytes = serialized_single_function_module(
|
||||||
code,
|
code,
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
@ -2883,9 +2883,9 @@ mod tests {
|
|||||||
Err(VmInitError::LoaderPatchFailed(
|
Err(VmInitError::LoaderPatchFailed(
|
||||||
crate::vm_init_error::LoaderPatchError::ResolveFailed(
|
crate::vm_init_error::LoaderPatchError::ResolveFailed(
|
||||||
prometeu_hal::syscalls::DeclaredLoadError::MissingCapability {
|
prometeu_hal::syscalls::DeclaredLoadError::MissingCapability {
|
||||||
required: prometeu_hal::syscalls::caps::GFX,
|
required: prometeu_hal::syscalls::caps::GFX2D,
|
||||||
provided: prometeu_hal::syscalls::caps::NONE,
|
provided: prometeu_hal::syscalls::caps::NONE,
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
},
|
},
|
||||||
@ -2903,7 +2903,7 @@ mod tests {
|
|||||||
let bytes = serialized_single_function_module(
|
let bytes = serialized_single_function_module(
|
||||||
code,
|
code,
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "draw_line".into(),
|
name: "draw_line".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 4,
|
arg_slots: 4,
|
||||||
@ -2918,7 +2918,7 @@ mod tests {
|
|||||||
Err(VmInitError::LoaderPatchFailed(
|
Err(VmInitError::LoaderPatchFailed(
|
||||||
crate::vm_init_error::LoaderPatchError::ResolveFailed(
|
crate::vm_init_error::LoaderPatchError::ResolveFailed(
|
||||||
prometeu_hal::syscalls::DeclaredLoadError::AbiMismatch {
|
prometeu_hal::syscalls::DeclaredLoadError::AbiMismatch {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "draw_line".into(),
|
name: "draw_line".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
declared_arg_slots: 4,
|
declared_arg_slots: 4,
|
||||||
|
|||||||
@ -4,7 +4,6 @@ pub mod debugger;
|
|||||||
pub mod fs_backend;
|
pub mod fs_backend;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod log_sink;
|
pub mod log_sink;
|
||||||
pub mod overlay;
|
|
||||||
pub mod runner;
|
pub mod runner;
|
||||||
pub mod stats;
|
pub mod stats;
|
||||||
pub mod utilities;
|
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::fs_backend::HostDirBackend;
|
||||||
use crate::input::HostInputHandler;
|
use crate::input::HostInputHandler;
|
||||||
use crate::log_sink::HostConsoleSink;
|
use crate::log_sink::HostConsoleSink;
|
||||||
use crate::overlay::{capture_snapshot, draw_overlay};
|
|
||||||
use crate::stats::HostStats;
|
use crate::stats::HostStats;
|
||||||
use crate::utilities::draw_rgba8888_to_rgba8;
|
use crate::utilities::draw_rgba8888_to_rgba8;
|
||||||
use pixels::wgpu::PresentMode;
|
use pixels::wgpu::PresentMode;
|
||||||
@ -128,9 +127,6 @@ pub struct HostRunner {
|
|||||||
/// directly instead of depending on guest-visible inspection syscalls.
|
/// directly instead of depending on guest-visible inspection syscalls.
|
||||||
debugger: HostDebugger,
|
debugger: HostDebugger,
|
||||||
|
|
||||||
/// Flag to enable/disable the technical telemetry display.
|
|
||||||
overlay_enabled: bool,
|
|
||||||
|
|
||||||
/// The physical audio driver.
|
/// The physical audio driver.
|
||||||
audio: HostAudio,
|
audio: HostAudio,
|
||||||
/// Last known pause state to sync with audio.
|
/// Last known pause state to sync with audio.
|
||||||
@ -170,7 +166,6 @@ impl HostRunner {
|
|||||||
|
|
||||||
stats: HostStats::new(),
|
stats: HostStats::new(),
|
||||||
debugger: HostDebugger::new(),
|
debugger: HostDebugger::new(),
|
||||||
overlay_enabled: false,
|
|
||||||
audio: HostAudio::new(),
|
audio: HostAudio::new(),
|
||||||
last_paused_state: false,
|
last_paused_state: false,
|
||||||
presentation: PresentationState::default(),
|
presentation: PresentationState::default(),
|
||||||
@ -272,9 +267,6 @@ impl ApplicationHandler for HostRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WindowEvent::RedrawRequested => {
|
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)
|
// 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");
|
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();
|
let src = self.hardware.gfx.front_buffer();
|
||||||
|
|
||||||
draw_rgba8888_to_rgba8(src, frame);
|
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
|
} // <- frame borrow ends here
|
||||||
|
|
||||||
if pixels.render().is_err() {
|
if pixels.render().is_err() {
|
||||||
@ -309,12 +297,6 @@ impl ApplicationHandler for HostRunner {
|
|||||||
self.request_redraw_if_needed();
|
self.request_redraw_if_needed();
|
||||||
println!("[Debugger] Execution started!");
|
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.
|
// 1. Process pending debug commands from the network.
|
||||||
self.debugger.check_commands(&mut self.firmware, &mut self.hardware);
|
self.debugger.check_commands(&mut self.firmware, &mut self.hardware);
|
||||||
|
|
||||||
// Sync inspection mode state. This is host-owned overlay/debugger control,
|
// Sync debugger inspection state; this is not a guest-visible debug ABI switch.
|
||||||
// not a guest-visible debug ABI switch.
|
self.firmware.os.vm().set_inspection_active(self.debugger.stream.is_some());
|
||||||
self.firmware
|
|
||||||
.os
|
|
||||||
.vm()
|
|
||||||
.set_inspection_active(self.overlay_enabled || self.debugger.stream.is_some());
|
|
||||||
|
|
||||||
// 2. Maintain a filesystem connection if it was lost (e.g., directory removed).
|
// 2. Maintain a filesystem connection if it was lost (e.g., directory removed).
|
||||||
if let Some(root) = &self.fs_root
|
if let Some(root) = &self.fs_root
|
||||||
|
|||||||
@ -42,14 +42,14 @@ pub fn generate() -> Result<()> {
|
|||||||
let mut rom: Vec<u8> = Vec::new();
|
let mut rom: Vec<u8> = Vec::new();
|
||||||
let syscalls = vec![
|
let syscalls = vec![
|
||||||
SyscallDecl {
|
SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
ret_slots: 0,
|
ret_slots: 0,
|
||||||
},
|
},
|
||||||
SyscallDecl {
|
SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx2d".into(),
|
||||||
name: "draw_text".into(),
|
name: "draw_text".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 4,
|
arg_slots: 4,
|
||||||
@ -126,7 +126,7 @@ pub fn generate() -> Result<()> {
|
|||||||
fs::create_dir_all(&out_dir)?;
|
fs::create_dir_all(&out_dir)?;
|
||||||
fs::write(out_dir.join("program.pbx"), bytes)?;
|
fs::write(out_dir.join("program.pbx"), bytes)?;
|
||||||
fs::write(out_dir.join("assets.pa"), build_assets_pack()?)?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
{"type":"meta","next_id":{"DSC":38,"AGD":38,"DEC":30,"PLN":73,"LSN":47,"CLSN":1}}
|
{"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":"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-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-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"}]}
|
{"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"}]}
|
||||||
|
|||||||
@ -0,0 +1,446 @@
|
|||||||
|
---
|
||||||
|
id: AGD-0038
|
||||||
|
ticket: render-frame-packet-boundary
|
||||||
|
title: Logical Render Pipelines and Command Packets
|
||||||
|
status: accepted
|
||||||
|
created: 2026-05-25
|
||||||
|
resolved:
|
||||||
|
decision:
|
||||||
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agenda - Logical Render Pipelines and Command Packets
|
||||||
|
|
||||||
|
## Contexto
|
||||||
|
|
||||||
|
Depois da migracao para RGBA8888, o proximo passo arquitetural do renderer nao deve ser GPU, 3D, multiplos backends ou troca dinamica complexa de pipeline. O objetivo imediato e separar melhor tres responsabilidades:
|
||||||
|
|
||||||
|
1. pipelines logicos produzem comandos de alto nivel;
|
||||||
|
2. uma implementacao de hardware/render surface consome esses comandos;
|
||||||
|
3. o framebuffer RGBA8888 atual continua sendo a superficie final no desktop.
|
||||||
|
|
||||||
|
A direcao desejada agora e explicitar pelo menos dois pipelines logicos:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Game 2D Pipeline
|
||||||
|
scene + sprites + HUD
|
||||||
|
-> Game2DFramePacket / Game2DCommandSet
|
||||||
|
-> hardware/render implementation
|
||||||
|
-> framebuffer RGBA8888
|
||||||
|
|
||||||
|
Prometeu UI Pipeline
|
||||||
|
shell/home/system UI/windows
|
||||||
|
-> UiFramePacket / UiCommandSet
|
||||||
|
-> hardware/render implementation
|
||||||
|
-> framebuffer RGBA8888
|
||||||
|
```
|
||||||
|
|
||||||
|
Esses pipelines podem funcionar de formas totalmente diferentes. O ponto comum nao deve ser uma API unica pobre que force tudo a parecer igual. O ponto comum deve ser a fronteira operacional: cada pipeline publica seu proprio conjunto logico de comandos para uma implementacao de hardware/superficie.
|
||||||
|
|
||||||
|
Com isso, se depois surgir um pipeline 3D, outro pipeline 2D, ou um renderer especializado para hardware barato/DIY, o trabalho fica concentrado no novo pipeline e/ou no consumidor de comandos, sem reabrir a VM inteira.
|
||||||
|
|
||||||
|
## Problema
|
||||||
|
|
||||||
|
Hoje o runtime ainda mistura producao logica de frame com desenho imediato no `Gfx`/framebuffer.
|
||||||
|
|
||||||
|
O codigo ja tem separacoes parciais:
|
||||||
|
|
||||||
|
- `FrameComposer` coordena scene binding, camera, cache/resolver e sprites do jogo.
|
||||||
|
- `Gfx` rasteriza em `back`, faz `present()` para `front`, e expoe `front_buffer()` para o host.
|
||||||
|
- `VirtualMachineRuntime` chama `hw.render_frame()` quando o frame logico termina.
|
||||||
|
- Prometeu Hub, splash/crash e varias chamadas `gfx.*` ainda desenham diretamente via `gfx_mut()`.
|
||||||
|
- O host desktop copia o `front_buffer()` RGBA8888 para `pixels`.
|
||||||
|
|
||||||
|
O limite atual ainda e "codigo logico chama renderer", nao "pipeline publica comandos para uma implementacao".
|
||||||
|
|
||||||
|
Isso cria custo futuro:
|
||||||
|
|
||||||
|
- o pipeline 2D de jogo fica preso ao formato operacional atual de `Gfx`;
|
||||||
|
- a UI do Prometeu fica misturada ao mesmo caminho imperativo de primitives;
|
||||||
|
- um futuro pipeline 3D teria que disputar o mesmo contrato em vez de definir sua propria saida logica;
|
||||||
|
- hardware barato/DIY teria que emular detalhes de `Gfx` em vez de consumir um command set adequado;
|
||||||
|
- testes ficam presos a pixels finais mesmo quando o que interessa e o pacote logico produzido.
|
||||||
|
|
||||||
|
## Pontos Criticos
|
||||||
|
|
||||||
|
### 1. Pipeline nao e backend
|
||||||
|
|
||||||
|
Pipeline deve significar "modelo logico de producao de comandos". Backend/implementacao deve significar "como esses comandos viram pixels/superficie".
|
||||||
|
|
||||||
|
Nao queremos agora:
|
||||||
|
|
||||||
|
- registry sofisticado de backends;
|
||||||
|
- troca dinamica de pipeline;
|
||||||
|
- GPU backend;
|
||||||
|
- 3D;
|
||||||
|
- refatoracao agressiva de asset pipeline;
|
||||||
|
- mudanca visual.
|
||||||
|
|
||||||
|
Queremos uma fronteira para que esses itens possam existir depois sem contaminar o passo atual.
|
||||||
|
|
||||||
|
### 2. Game 2D e Prometeu UI nao precisam compartilhar o mesmo command set
|
||||||
|
|
||||||
|
O pipeline 2D de jogos naturalmente fala de scene/cache/camera/sprites/HUD.
|
||||||
|
|
||||||
|
O pipeline de UI do Prometeu naturalmente fala de janelas, paineis, texto, retangulos, estados de foco, home/shell/system UI, talvez layout e clipping.
|
||||||
|
|
||||||
|
Forcar ambos a um `RenderCommand` generico demais no v1 pode criar uma abstracao fraca. A agenda deve decidir se o contrato comum sera:
|
||||||
|
|
||||||
|
- um enum top-level de pacotes por pipeline; ou
|
||||||
|
- uma trait de submissao ao hardware; ou
|
||||||
|
- um `FrameSubmission` que contem command sets tipados por dominio.
|
||||||
|
|
||||||
|
### 3. `FrameComposer` continua sendo bom candidato para o Game 2D Pipeline
|
||||||
|
|
||||||
|
`FrameComposer` ja e o ponto natural para produzir o pacote de jogo:
|
||||||
|
|
||||||
|
- scene binding;
|
||||||
|
- camera;
|
||||||
|
- cache/resolver;
|
||||||
|
- sprites por frame;
|
||||||
|
- HUD/primitives de jogo quando aplicavel.
|
||||||
|
|
||||||
|
O menor corte para jogo ainda parece ser transformar `FrameComposer.render_frame(&mut dyn GfxBridge)` em uma producao de `Game2DFramePacket`, consumida em seguida pelo renderer Classic2D atual.
|
||||||
|
|
||||||
|
### 4. Prometeu UI precisa sair do desenho direto, mas talvez nao no mesmo PR
|
||||||
|
|
||||||
|
Prometeu Hub e system UI hoje usam `hw.gfx_mut()` diretamente. Isso e exatamente o tipo de acoplamento que a arquitetura quer remover, mas migrar UI junto com Game 2D pode deixar a primeira mudanca grande demais.
|
||||||
|
|
||||||
|
A decisao precisa escolher entre:
|
||||||
|
|
||||||
|
- definir os dois contratos agora e implementar em fases;
|
||||||
|
- implementar primeiro Game 2D e registrar UI como segundo plan;
|
||||||
|
- ou implementar um envelope comum e dois producers minimos no mesmo plano.
|
||||||
|
|
||||||
|
### 5. HUD pertence ao pipeline 2D de jogo, nao ao Prometeu UI por padrao
|
||||||
|
|
||||||
|
Para esta agenda, "HUD" significa HUD do jogo como parte do frame de jogo: overlays do cart/gameplay, texto/debug de jogo e primitives 2D associadas ao jogo.
|
||||||
|
|
||||||
|
Prometeu UI significa UI do sistema: hub, shell, janelas, home, crash/splash se forem tratados como telas do sistema.
|
||||||
|
|
||||||
|
Essa separacao evita confundir UI de jogo com UI do console.
|
||||||
|
|
||||||
|
### 6. A implementacao inicial ainda pode ser uma so
|
||||||
|
|
||||||
|
Mesmo com dois pipelines logicos, a implementacao inicial pode continuar sendo o `Gfx`/Classic2D atual gravando em RGBA8888. O ganho arquitetural nao exige multiplos backends. Exige que a entrada da implementacao deixe de ser acesso direto ao framebuffer e passe a ser submissao de comandos logicos.
|
||||||
|
|
||||||
|
### 7. A fronteira deve preparar VM e render pipeline paralelos
|
||||||
|
|
||||||
|
Render thread nao e objetivo do v1, mas a fronteira nao deve impedir que a VM e o renderer rodem em paralelo depois.
|
||||||
|
|
||||||
|
Isso implica que a submissao de frame deve ser tratada como um snapshot fechado do trabalho de render daquele frame. Depois de publicada, a VM nao deve continuar mutando estruturas que o renderer precisa ler. A implementacao inicial pode consumir o snapshot imediatamente e no mesmo thread, mas o contrato deve evitar dependencias como `&mut Hardware`, `&mut Gfx`, ou acesso mutavel ao estado vivo da VM durante a rasterizacao.
|
||||||
|
|
||||||
|
Para o v1, isso sugere:
|
||||||
|
|
||||||
|
- `RenderSubmission` deve carregar `frame_id`, `app_mode` e dados suficientes para identificar a ordem de publicacao;
|
||||||
|
- command sets pequenos devem preferir dados owned ou buffers owned pelo produtor ate a publicacao;
|
||||||
|
- referencias pesadas, como cache/assets, devem ser read-only e ter ownership compartilhado ou lifetime claramente limitado;
|
||||||
|
- a camada de hardware/render surface deve ser a unica dona de publicacao/present;
|
||||||
|
- a VM deve produzir/submeter comandos, nao desenhar nem esperar acesso ao framebuffer;
|
||||||
|
- testes devem conseguir validar o packet/submission sem depender apenas do framebuffer final.
|
||||||
|
|
||||||
|
### 8. Deve existir um `RenderManager` acima dos pipelines
|
||||||
|
|
||||||
|
Mesmo evitando registry sofisticado de backends no v1, a arquitetura precisa de uma camada acima dos pipelines individuais. Essa camada nao deve ser "mais um pipeline"; ela deve coordenar:
|
||||||
|
|
||||||
|
- qual `AppMode` esta ativo (`Game` ou `Shell`);
|
||||||
|
- quais command buffers/submissions estao validos para esse modo;
|
||||||
|
- transicoes entre modos;
|
||||||
|
- publicacao da superficie;
|
||||||
|
- politica de latest frame;
|
||||||
|
- capacidades disponiveis por modo.
|
||||||
|
|
||||||
|
Motivacao principal: transicoes entre Shell e Game nao pertencem completamente a nenhum dos dois pipelines. Exemplo: o Shell UI pode executar uma transicao visual de saida ao abrir um jogo, mas depois que o Game 2D assume, o Shell UI ja nao existe mais para executar a transicao de entrada do jogo. Isso sugere uma base comum de render/control plane acima dos pipelines, capaz de manter transicoes e estado de superficie durante a troca de modo.
|
||||||
|
|
||||||
|
Essa camada deve se chamar `RenderManager`. O nome indica coordenacao de render, nao implementacao de desenho. A decisao precisa separar com cuidado:
|
||||||
|
|
||||||
|
- `RenderManager` como abstracao/runtime contract de coordenacao;
|
||||||
|
- host/render-surface implementation como adaptador local que sabe publicar em uma superficie real.
|
||||||
|
|
||||||
|
O `RenderManager` coordena pipelines e superficie, mas nao produz comandos de jogo/UI e nao deve conter detalhes de host desktop.
|
||||||
|
|
||||||
|
### 9. Command buffers e submissions sao conceitos diferentes
|
||||||
|
|
||||||
|
Cada dominio/pipeline acumula estado e comandos em seu proprio buffer mutavel durante o frame logico. Exemplos:
|
||||||
|
|
||||||
|
- `ComposerBuffer` para scene, camera e sprites;
|
||||||
|
- `Gfx2dBuffer` para primitivas 2D de jogo;
|
||||||
|
- `GfxUiBuffer` para primitivas de Shell UI;
|
||||||
|
- `ComposerBuffer` tambem cobre HUD do Game 2D no v1.
|
||||||
|
|
||||||
|
No fechamento do frame, o `RenderManager` transforma esses buffers na `RenderSubmission` valida para o `AppMode` atual.
|
||||||
|
|
||||||
|
Depois do fechamento, a submission e um snapshot fechado. Produtores logicos nao podem continuar mutando a submission fechada. Essa regra deve ser cravada agora porque e a base para VM e render pipeline rodarem em paralelo no futuro.
|
||||||
|
|
||||||
|
Politica de backlog: nao existe fila infinita. A regra canonica e `latest complete submission wins`.
|
||||||
|
|
||||||
|
## Opcoes
|
||||||
|
|
||||||
|
### Opcao A - Dois command sets tipados, uma implementacao Classic2D inicial
|
||||||
|
|
||||||
|
**Abordagem:**
|
||||||
|
|
||||||
|
- Criar `Game2DFramePacket` para scene/sprites/HUD.
|
||||||
|
- Criar `UiFramePacket` para Prometeu UI/system UI.
|
||||||
|
- Criar um envelope simples, por exemplo `RenderSubmission`, que identifica qual pacote esta sendo submetido.
|
||||||
|
- Fazer a implementacao Classic2D atual consumir os pacotes e continuar escrevendo no framebuffer RGBA8888.
|
||||||
|
- Implementar em fases: primeiro Game 2D, depois Prometeu UI, ou ambos com producers minimos se o plano ficar pequeno o suficiente.
|
||||||
|
|
||||||
|
**Pro:**
|
||||||
|
|
||||||
|
- preserva modelos logicos diferentes para jogo e UI;
|
||||||
|
- evita uma command list generica demais;
|
||||||
|
- prepara 3D ou outros pipelines sem forcar eles a parecerem com 2D;
|
||||||
|
- deixa claro que pipeline e backend sao coisas diferentes.
|
||||||
|
|
||||||
|
**Con:**
|
||||||
|
|
||||||
|
- cria mais tipos no inicio;
|
||||||
|
- precisa definir como o hardware recebe pacotes heterogeneos;
|
||||||
|
- se so Game 2D for migrado no primeiro plan, UI continua acoplada por um tempo.
|
||||||
|
|
||||||
|
**Maintainability:**
|
||||||
|
|
||||||
|
Alta. Esta opcao combina com a direcao de longo prazo sem exigir varios backends agora.
|
||||||
|
|
||||||
|
### Opcao B - Um `RenderCommand` universal para tudo
|
||||||
|
|
||||||
|
**Abordagem:**
|
||||||
|
|
||||||
|
- Criar uma display list unica com comandos como `Clear`, `DrawSprite`, `DrawText`, `FillRect`, `SceneCache`, `Window`, `Transition`.
|
||||||
|
- Fazer Game 2D e Prometeu UI emitirem a mesma lista.
|
||||||
|
|
||||||
|
**Pro:**
|
||||||
|
|
||||||
|
- facil de passar por uma unica fila;
|
||||||
|
- simples para logging/replay inicial;
|
||||||
|
- reduz o numero de tipos.
|
||||||
|
|
||||||
|
**Con:**
|
||||||
|
|
||||||
|
- tende a virar um denominador comum fraco;
|
||||||
|
- mistura semantica de jogo, UI de sistema e futuros pipelines;
|
||||||
|
- pode forcar o pipeline 3D futuro a um contrato 2D inadequado;
|
||||||
|
- aumenta chance de reabrir a arquitetura quando outro pipeline surgir.
|
||||||
|
|
||||||
|
**Maintainability:**
|
||||||
|
|
||||||
|
Media a baixa como arquitetura principal. Pode ser util internamente dentro de um pipeline, mas nao como contrato unico do runtime.
|
||||||
|
|
||||||
|
### Opcao C - Trait comum de pipeline, sem pacote explicito
|
||||||
|
|
||||||
|
**Abordagem:**
|
||||||
|
|
||||||
|
- Definir algo como `RenderPipeline` com metodo `render(&mut HardwareSurface)`.
|
||||||
|
- Game 2D e UI implementam a trait, mas continuam desenhando por callbacks imediatos.
|
||||||
|
|
||||||
|
**Pro:**
|
||||||
|
|
||||||
|
- pequena mudanca inicial;
|
||||||
|
- organiza nomes e ownership;
|
||||||
|
- reduz um pouco o acoplamento por modulo.
|
||||||
|
|
||||||
|
**Con:**
|
||||||
|
|
||||||
|
- nao cria pacote logico observavel;
|
||||||
|
- continua misturando producao e execucao;
|
||||||
|
- nao ajuda tanto render thread, replay, testes de comandos ou hardware alternativo.
|
||||||
|
|
||||||
|
**Maintainability:**
|
||||||
|
|
||||||
|
Insuficiente se virar a decisao final. Pode ser uma API auxiliar, mas nao substitui command packets.
|
||||||
|
|
||||||
|
### Opcao D - Definir todos os pipelines e backend abstraction agora
|
||||||
|
|
||||||
|
**Abordagem:**
|
||||||
|
|
||||||
|
- Criar pipeline registry, backend interfaces, CPU/GPU abstractions, surfaces, scheduling, command queues e talvez render thread.
|
||||||
|
|
||||||
|
**Pro:**
|
||||||
|
|
||||||
|
- parece completo no papel;
|
||||||
|
- antecipa muitos cenarios futuros.
|
||||||
|
|
||||||
|
**Con:**
|
||||||
|
|
||||||
|
- grande demais para o objetivo atual;
|
||||||
|
- alto risco de abstrair sem uso real;
|
||||||
|
- pode atrasar a estabilizacao do 2D atual;
|
||||||
|
- tende a introduzir nomes e contratos que depois precisarao ser quebrados.
|
||||||
|
|
||||||
|
**Maintainability:**
|
||||||
|
|
||||||
|
Baixa para o momento atual. Melhor deixar essa camada emergir depois de pelo menos dois pipelines reais funcionando.
|
||||||
|
|
||||||
|
## Sugestao / Recomendacao
|
||||||
|
|
||||||
|
A recomendacao atual e a Opcao A: definir dois pipelines logicos com command sets proprios e uma implementacao Classic2D inicial.
|
||||||
|
|
||||||
|
Modelo recomendado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Game2DPipeline
|
||||||
|
produces Game2DFramePacket
|
||||||
|
owns/uses FrameComposer concepts:
|
||||||
|
scene/cache/camera
|
||||||
|
sprites
|
||||||
|
game HUD
|
||||||
|
HUD/primitives
|
||||||
|
|
||||||
|
PrometeuUiPipeline
|
||||||
|
produces UiFramePacket
|
||||||
|
owns/uses system UI concepts:
|
||||||
|
home/hub
|
||||||
|
shell windows
|
||||||
|
panels/rects/text
|
||||||
|
focus/input visual state
|
||||||
|
|
||||||
|
RenderHardware / RenderSurface implementation
|
||||||
|
consumes typed submissions:
|
||||||
|
Game2D(Game2DFramePacket)
|
||||||
|
PrometeuUi(UiFramePacket)
|
||||||
|
future: Game3D(...), Custom2D(...), etc.
|
||||||
|
writes to current RGBA8888 framebuffer for now
|
||||||
|
|
||||||
|
RenderManager
|
||||||
|
owns current AppMode render routing
|
||||||
|
closes per-domain command buffers into a submission
|
||||||
|
manages mode transitions
|
||||||
|
keeps only the latest complete submission
|
||||||
|
owns publish/present policy through the render surface
|
||||||
|
```
|
||||||
|
|
||||||
|
O primeiro plano de execucao pode ser incremental:
|
||||||
|
|
||||||
|
1. Definir nomes e contratos dos pacotes sem criar backend registry.
|
||||||
|
2. Migrar o Game 2D path para `Game2DFramePacket -> Classic2D`.
|
||||||
|
3. Manter comportamento pixel-level identico.
|
||||||
|
4. Em seguida migrar Prometeu UI para `UiFramePacket -> Classic2D`.
|
||||||
|
5. So depois discutir outros backends, render thread ou 3D.
|
||||||
|
|
||||||
|
Se quisermos reduzir ainda mais risco, a decisao pode determinar que o primeiro PR implementa apenas o Game 2D packet, mas ja reserva a arquitetura e nomes para `UiFramePacket`. O importante e nao fingir que Prometeu UI e apenas "mais primitives gfx" dentro do pipeline de jogo.
|
||||||
|
|
||||||
|
## Riscos
|
||||||
|
|
||||||
|
- **Abstracao comum errada:** um command set universal pode parecer simples agora, mas virar bloqueio para UI e 3D depois.
|
||||||
|
- **Escopo grande demais:** migrar Game 2D, UI, firmware screens e host presentation em um unico PR aumenta risco de regressao visual.
|
||||||
|
- **Dual path permanente:** se `gfx_mut()` continuar como escape hatch normal, a fronteira de comandos nao vira contrato real.
|
||||||
|
- **Ownership/lifetime dos pacotes:** pacotes borrowed reduzem copia no v1; pacotes owned ajudam render thread futura. A decisao precisa declarar o alvo do primeiro passo.
|
||||||
|
- **HUD ambiguo:** HUD de jogo e UI do Prometeu precisam ficar separados por contrato, mesmo que ambos sejam 2D.
|
||||||
|
- **Present/publication:** `present()` precisa ser tratado como publicacao de superficie, nao como parte aleatoria de cada produtor logico.
|
||||||
|
- **Asset/cache leakage:** command packets devem referenciar recursos logicos/resolvidos sem copiar asset pipeline nem expor detalhes demais do `Gfx`.
|
||||||
|
- **Paralelismo futuro:** se o packet v1 depender de estado mutavel vivo da VM/hardware, uma render thread futura exigira nova quebra arquitetural. A submissao precisa parecer um snapshot fechado mesmo quando consumida sincronicamente no v1.
|
||||||
|
- **Transicoes entre modos:** se Shell e Game forem totalmente independentes sem uma coordenacao mestre, efeitos de transicao e manutencao de superficie ficam sem dono claro.
|
||||||
|
- **Backpressure:** render futuro nao deve acumular fila infinita. A politica aceita e manter somente a submissao completa mais recente.
|
||||||
|
- **Abstracao vs host:** `RenderManager` deve ser contrato runtime/abstracao de coordenacao. Host desktop/render surface deve ser implementacao local separada, sem vazar detalhes para os pipelines.
|
||||||
|
- **HUD ownership:** HUD nao deve ficar escondido dentro de `Gfx2D`. `Gfx2D` deve conter apenas primitivas. No v1, HUD pertence explicitamente ao `composer`.
|
||||||
|
|
||||||
|
## Arquivos / Areas Provavelmente Afetados em uma Plan Futura
|
||||||
|
|
||||||
|
Nucleo Game 2D:
|
||||||
|
|
||||||
|
- `crates/console/prometeu-hal/src/` para tipos compartilhados de packet/submission, se virarem contrato de hardware.
|
||||||
|
- `crates/console/prometeu-hal/src/lib.rs` para exportar novos modulos.
|
||||||
|
- `crates/console/prometeu-drivers/src/frame_composer.rs` para produzir `Game2DFramePacket`.
|
||||||
|
- `crates/console/prometeu-drivers/src/gfx.rs` para consumir `Game2DFramePacket` no Classic2D atual.
|
||||||
|
- `crates/console/prometeu-drivers/src/hardware.rs` para conectar pipeline/submission/implementacao.
|
||||||
|
- `crates/console/prometeu-hal/src/hardware_bridge.rs` para reduzir acesso direto a `GfxBridge` no fluxo de frame.
|
||||||
|
|
||||||
|
Nucleo Prometeu UI:
|
||||||
|
|
||||||
|
- `crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs`
|
||||||
|
- `crates/console/prometeu-system/src/services/windows/*`
|
||||||
|
- `crates/console/prometeu-firmware/src/firmware/firmware_step_splash_screen.rs`
|
||||||
|
- `crates/console/prometeu-firmware/src/firmware/firmware_step_crash_screen.rs`
|
||||||
|
- possivelmente um novo modulo de UI commands em `prometeu-hal` ou `prometeu-system`.
|
||||||
|
|
||||||
|
Possivel, mas deve ser minimizado no primeiro passo:
|
||||||
|
|
||||||
|
- `crates/console/prometeu-system/src/services/vm_runtime/dispatch.rs` se `gfx.*` primitives forem convertidas em command submission.
|
||||||
|
- `crates/console/prometeu-hal/src/gfx_bridge.rs` se a ponte antiga for estreitada.
|
||||||
|
- `crates/host/prometeu-host-desktop-winit/src/runner.rs` se o acesso ao framebuffer for encapsulado, mas host presentation idealmente fica igual no v1.
|
||||||
|
|
||||||
|
## Perguntas em Aberto
|
||||||
|
|
||||||
|
- [x] O primeiro plano deve implementar Game 2D e Prometeu UI juntos, ou apenas Game 2D com contrato preparado para UI?
|
||||||
|
- Resposta: faseado. O primeiro plano deve priorizar Game 2D, mas o contrato deve nomear Prometeu UI como pipeline separado desde o inicio.
|
||||||
|
- [x] O contrato comum deve ser um enum `RenderSubmission`, uma trait de submissao, ou uma fila tipada por pipeline?
|
||||||
|
- Resposta: `AppMode` e `RenderSubmission` devem andar juntos. `AppMode::Game` e `AppMode::Shell` podem ter cadencias e pipelines diferentes; alguns renderers pertencem a um modo e nao ao outro. O envelope inicial pode ser um `RenderSubmission` tipado por modo/pipeline, sem registry dinamico.
|
||||||
|
- [x] `Game2DFramePacket` deve incluir HUD de jogo no v1 ou apenas reservar o campo?
|
||||||
|
- Resposta: deve incluir HUD de jogo no v1. O pipeline 2D de jogos abrange scene, sprites e HUD.
|
||||||
|
- [x] `UiFramePacket` deve representar UI em comandos baixos (`rect`, `text`, `clip`) ou em comandos mais semanticos (`window`, `panel`, `button`)?
|
||||||
|
- Resposta: comandos de UI podem ser diferentes dos comandos Game 2D, mas widget/layout deve ficar fora da implementacao do host. No host, `UiFramePacket` pode ser reduzido a diretivas semelhantes ou equivalentes as primitivas finais de desenho.
|
||||||
|
- [x] Pacotes v1 devem ser borrowed, owned, ou hibridos?
|
||||||
|
- Resposta: hibridos. O modelo precisa suportar transicoes de modo, como clicar no jogo, sair da UI, abrir o jogo e alternar entre `Game` e `Shell`, sem exigir copia pesada de todo recurso.
|
||||||
|
- [x] Onde fica `present()` no novo modelo: no hardware/render surface, no consumidor Classic2D, ou temporariamente no firmware?
|
||||||
|
- Resposta: `present()` pertence a implementacao de hardware/render surface quando ela precisa publicar/trocar buffers. Ele faz sentido em um blit/double-buffer path como Game 2D, mas nao deve ser parte obrigatoria do path canonico de todos os pipelines.
|
||||||
|
- [x] O acesso publico a `gfx.*` deve virar submissao de comandos, ser mantido como debug overlay temporario, ou ser separado em outra discussao?
|
||||||
|
- Resposta: separar apenas as primitivas de desenho por contrato de pipeline. O que ja e `composer.*` deve continuar no dominio `composer`, porque scene, camera, sprites, HUD e orquestracao de frame sao um contrato mais alto nivel do Game 2D, nao primitivas `gfx2d`. `Gfx2D` deve conter somente primitivas. O atual `gfx.*` deve ser dividido em primitivas apropriadas para Game e Shell, por exemplo `gfx2d.*` para primitivas do Game 2D e `gfxui.*` para Prometeu UI/Shell. As stdlibs podem continuar expondo nomes de alto nivel como `gfx`, desde que apontem para syscalls diferentes por `AppMode`.
|
||||||
|
- [x] A agenda aberta `DSC-0011` de dirty regions deve depender desta fronteira antes de continuar?
|
||||||
|
- Resposta: sim. `DSC-0011` vem depois desta fronteira, para nao otimizar o acoplamento errado.
|
||||||
|
- [x] O v1 precisa implementar render thread?
|
||||||
|
- Resposta: nao. Mas o v1 deve preparar a fronteira para isso: `RenderSubmission` deve ser um snapshot fechado por `frame_id`/`AppMode`, sem exigir `&mut Hardware` ou framebuffer mutavel no produtor logico. O consumo pode continuar sincrono na primeira implementacao.
|
||||||
|
- [x] Quem e dono do frame builder?
|
||||||
|
- Resposta: cada dominio/pipeline deve ter seus proprios buffers de comando/estado. O fechamento do frame deve ser centralizado no host/render coordinator, que transforma esses buffers na `RenderSubmission` valida para o `AppMode` atual.
|
||||||
|
- [x] Qual e a ordem de composicao Game 2D?
|
||||||
|
- Resposta: scene e sprites intercalam por `layer` inteiro usando os 4 layers disponiveis do Game 2D, normalmente como `sprite -> scene -> sprite -> scene -> ...` conforme ordenacao por layer. Depois vem um layer de HUD sobre tudo e entao publish. Transicoes visuais devem pertencer ao `RenderManager`, nao ao Game 2D frame path.
|
||||||
|
- [x] Como lidar com transicoes Game/Shell?
|
||||||
|
- Resposta: a arquitetura precisa de um `RenderManager` acima dos pipelines. Ele gerencia troca de modo, manutencao da superficie, capacidades ativas e transicoes que nao pertencem integralmente nem ao Shell UI nem ao Game 2D.
|
||||||
|
- [x] Qual politica de backpressure para render paralelo futuro?
|
||||||
|
- Resposta: sem fila infinita. Manter somente a submissao completa mais recente.
|
||||||
|
- [x] Capabilities devem ser separadas?
|
||||||
|
- Resposta: sim. O contrato deve prever capabilities separadas para `composer`, `gfx2d` e `gfxui`, reforcando o isolamento por `AppMode`.
|
||||||
|
- [x] O host debug overlay deve continuar?
|
||||||
|
- Resposta: nao como parte desta arquitetura. O overlay atual pode ser removido por completo em uma execucao futura; se um console/debug UI voltar a ser necessario, deve ser rediscutido como produto proprio, nao como overlay host-owned herdado.
|
||||||
|
- [x] Fade deve permanecer em algum contrato?
|
||||||
|
- Resposta: nao. Fade deve sair completamente. Ele era uma ideia de hardware inicial e nao deve permanecer como campo de packet, syscall ou habito de implementacao. Transicoes visuais futuras pertencem ao `RenderManager`, nao a `composer`, `gfx2d` ou `gfxui`.
|
||||||
|
- [x] HUD deve ser dominio proprio ou parte do `composer`?
|
||||||
|
- Resposta: HUD fica dentro de `composer` no v1, por ser parte alta do frame Game 2D junto de scene, camera e sprites. `Gfx2D` continua limitado a primitivas.
|
||||||
|
|
||||||
|
## Criterio para Encerrar
|
||||||
|
|
||||||
|
Esta agenda pode virar decisao quando houver consenso sobre:
|
||||||
|
|
||||||
|
- quais pipelines existem no v1;
|
||||||
|
- qual command set pertence ao Game 2D;
|
||||||
|
- qual command set pertence a Prometeu UI;
|
||||||
|
- qual e a fronteira comum entre pipeline e implementacao de hardware/superficie;
|
||||||
|
- o que fica explicitamente fora de escopo;
|
||||||
|
- estrategia de migracao incremental sem mudanca visual;
|
||||||
|
- testes de equivalencia esperados.
|
||||||
|
|
||||||
|
## Discussion
|
||||||
|
|
||||||
|
Atualizacao de 2026-05-25:
|
||||||
|
|
||||||
|
O foco nao deve ser apenas "introduzir `RenderFramePacket` para Classic2D". O foco deve ser separar pipelines logicos por dominio. Game 2D e Prometeu UI podem ter modelos de comando distintos, mas ambos devem submeter comandos a uma implementacao de hardware/render surface.
|
||||||
|
|
||||||
|
A menor refatoracao segura provavelmente continua partindo do Game 2D, porque `FrameComposer` ja concentra scene/cache/sprites. Mas a decisao deve nomear Prometeu UI como pipeline separado desde agora, para evitar que a UI do sistema vire uma extensao acidental do pipeline de jogo.
|
||||||
|
|
||||||
|
Respostas fechadas em discussao:
|
||||||
|
|
||||||
|
- `AppMode::Game` e `AppMode::Shell` continuam sendo os modos canonicos atuais; nao ha rename para `System`.
|
||||||
|
- `RenderSubmission` deve ser coerente com `AppMode`, porque a atualizacao de frame e os renderers disponiveis podem diferir entre Game e Shell.
|
||||||
|
- `composer.*` deve continuar representando scene, camera, sprites, HUD e orquestracao de frame do Game 2D.
|
||||||
|
- `Gfx2D` deve representar apenas primitivas/drawing commands do pipeline Game 2D.
|
||||||
|
- `GfxUI` deve representar primitivas/drawing commands do pipeline Prometeu UI/Shell.
|
||||||
|
- As stdlibs PBS de Game e Shell podem usar nomes parecidos em alto nivel, inclusive `gfx`, mas devem mapear para dominios ABI diferentes conforme `AppMode`.
|
||||||
|
- O host/render implementation pode reduzir command sets diferentes a diretivas locais parecidas, mas widget/layout nao deve morar no host.
|
||||||
|
- `present()` e publicacao de superficie, nao parte obrigatoria de todo pipeline logico.
|
||||||
|
- `DSC-0011` deve aguardar esta fronteira.
|
||||||
|
- Render thread futura nao deve ser implementada agora, mas a fronteira deve nascer compativel com VM e renderer rodando em paralelo: submissions fechadas, leitura imutavel de recursos compartilhados e publicacao isolada na render surface.
|
||||||
|
- Buffers de comando/estado devem pertencer aos dominios/pipelines; o fechamento em `RenderSubmission` deve ser centralizado no `RenderManager`.
|
||||||
|
- `RenderManager` e abstracao/runtime contract; host/render-surface implementation e adaptador local separado.
|
||||||
|
- Game 2D intercalara scene e sprites por layer. HUD fica acima de tudo e pertence ao `composer` no v1, ainda com contrato minimo e evolutivo.
|
||||||
|
- Fade sai completamente. Transicoes de entrada/saida entre Shell e Game pertencem ao `RenderManager`.
|
||||||
|
- Backpressure futuro: latest complete submission wins.
|
||||||
|
- Capabilities devem ser separadas para `composer`, `gfx2d` e `gfxui`.
|
||||||
|
- O debug overlay host-owned atual nao deve ser preservado como requisito; pode ser removido quando a execucao chegar nessa area.
|
||||||
|
|
||||||
|
## Resolution
|
||||||
|
|
||||||
|
As perguntas principais de escopo foram respondidas. A agenda ainda precisa ser aceita explicitamente antes de virar decisao normativa.
|
||||||
|
|
||||||
|
## Next Step
|
||||||
|
|
||||||
|
Fechar as perguntas sobre escopo do v1. Se a direcao de dois pipelines logicos for aceita, transformar esta agenda em uma decisao normativa antes de qualquer plan ou alteracao de codigo.
|
||||||
@ -0,0 +1,236 @@
|
|||||||
|
---
|
||||||
|
id: DEC-0030
|
||||||
|
ticket: render-frame-packet-boundary
|
||||||
|
title: Logical Render Pipeline Command Boundary
|
||||||
|
status: accepted
|
||||||
|
created: 2026-05-25
|
||||||
|
ref_agenda: AGD-0038
|
||||||
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Decision - Logical Render Pipeline Command Boundary
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted.
|
||||||
|
|
||||||
|
## Contexto
|
||||||
|
|
||||||
|
Prometeu ja concluiu a migracao do framebuffer/runtime para RGBA8888. O proximo passo do renderer nao e introduzir GPU, 3D, multiplos backends, troca dinamica complexa de pipeline ou render thread. O objetivo imediato e criar uma fronteira arquitetural limpa entre:
|
||||||
|
|
||||||
|
- producao logica de comandos de render;
|
||||||
|
- fechamento/publicacao de frame;
|
||||||
|
- coordenacao entre `AppMode::Game` e `AppMode::Shell`;
|
||||||
|
- rasterizacao/publicacao em uma superficie real.
|
||||||
|
|
||||||
|
O estado atual ainda acopla codigo logico ao `Gfx`/framebuffer. `FrameComposer` ja centraliza parte da orquestracao do frame de jogo, mas ainda desenha por `GfxBridge`. Prometeu Hub, shell/system UI, splash/crash e primitivas `gfx.*` tambem acessam desenho imperativo direto. Essa decisao normatiza a nova fronteira de comandos e o papel de cada dominio.
|
||||||
|
|
||||||
|
## Decisao
|
||||||
|
|
||||||
|
Prometeu MUST separar render pipeline logico de render implementation.
|
||||||
|
|
||||||
|
O runtime SHALL introduce a `RenderManager` as the runtime-level render coordination abstraction. `RenderManager` MUST NOT be a host-specific renderer and MUST NOT contain desktop presentation details. Host/render-surface implementations SHALL adapt `RenderManager` submissions to the local hardware or host surface.
|
||||||
|
|
||||||
|
The canonical render flow SHALL be:
|
||||||
|
|
||||||
|
```text
|
||||||
|
domain buffers during logical frame
|
||||||
|
-> RenderManager closes buffers
|
||||||
|
-> RenderSubmission snapshot
|
||||||
|
-> render surface / implementation consumes submission
|
||||||
|
-> RGBA8888 surface publication
|
||||||
|
```
|
||||||
|
|
||||||
|
The v1 logical render domains are:
|
||||||
|
|
||||||
|
- `composer.*` for Game 2D high-level frame composition: scene, camera, sprites, HUD, and Game 2D frame orchestration.
|
||||||
|
- `gfx2d.*` for Game 2D primitives only.
|
||||||
|
- `gfxui.*` for Shell UI primitives.
|
||||||
|
|
||||||
|
The v1 app modes are:
|
||||||
|
|
||||||
|
- `AppMode::Game`, which SHALL route to Game 2D submissions.
|
||||||
|
- `AppMode::Shell`, which SHALL route to Shell UI submissions.
|
||||||
|
|
||||||
|
No `AppMode::System` rename is accepted by this decision.
|
||||||
|
|
||||||
|
`RenderSubmission` MUST be coherent with `AppMode`. The initial submission model SHOULD be a typed envelope rather than a generic universal command list:
|
||||||
|
|
||||||
|
```text
|
||||||
|
RenderSubmission
|
||||||
|
- frame_id
|
||||||
|
- app_mode
|
||||||
|
- packet:
|
||||||
|
- Game2D(Game2DFramePacket)
|
||||||
|
- ShellUi(UiFramePacket)
|
||||||
|
```
|
||||||
|
|
||||||
|
The exact Rust layout may evolve during planning, but the contract MUST preserve typed per-pipeline command sets.
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
Typed command sets preserve domain meaning. Game 2D and Shell UI have different responsibilities and should not be forced into a weak universal command list. A future 3D or custom 2D pipeline should add its own packet rather than reinterpret a 2D/UI command model.
|
||||||
|
|
||||||
|
`RenderManager` is required because mode transitions do not belong completely to either Shell UI or Game 2D. Shell can render an exit transition before launching a game, but once Game mode owns the frame, Shell UI is no longer available to render the entry transition. Therefore transition coordination, surface maintenance, active render capabilities, and frame publication need a manager above individual pipelines.
|
||||||
|
|
||||||
|
Command buffers and submissions must be distinct. Domain buffers are mutable while the logical frame is being built. A `RenderSubmission` is a closed snapshot. This makes future VM/render parallelism possible without requiring a render thread in v1.
|
||||||
|
|
||||||
|
The current fade model is rejected. Fade was a hardware-direction idea that no longer belongs in the Prometeu render contract after RGBA8888. Fade MUST NOT remain as a packet field, syscall, or implementation habit. Future visual transitions belong to `RenderManager`.
|
||||||
|
|
||||||
|
## Invariantes / Contrato
|
||||||
|
|
||||||
|
### Pipeline and Implementation Boundary
|
||||||
|
|
||||||
|
- Pipelines MUST produce logical commands or state.
|
||||||
|
- Render implementations MUST consume submissions and write/publish to a surface.
|
||||||
|
- Pipeline code MUST NOT require direct mutable access to the framebuffer.
|
||||||
|
- Pipeline code MUST NOT depend on host-specific presentation details.
|
||||||
|
|
||||||
|
### RenderManager
|
||||||
|
|
||||||
|
- `RenderManager` MUST coordinate active `AppMode`, submission closure, mode transitions, capabilities, and publication policy.
|
||||||
|
- `RenderManager` MUST be a runtime abstraction, not a host renderer.
|
||||||
|
- Host/render-surface implementations MUST be separate adapters.
|
||||||
|
- `RenderManager` MUST own transition coordination between Game and Shell.
|
||||||
|
- `RenderManager` MUST own the policy for surface publication/present through the render surface.
|
||||||
|
|
||||||
|
### Command Buffers and Submissions
|
||||||
|
|
||||||
|
- Each render domain/pipeline MUST own its own mutable command/state buffer during a logical frame.
|
||||||
|
- `RenderManager` MUST close the active buffers into a `RenderSubmission` at frame boundary.
|
||||||
|
- Once closed, `RenderSubmission` MUST be treated as immutable by producers.
|
||||||
|
- `RenderSubmission` MUST include `frame_id` and `app_mode`.
|
||||||
|
- Future parallel render MUST be supported by the contract: consuming a submission MUST NOT require `&mut Hardware`, `&mut Gfx`, or live mutable VM state.
|
||||||
|
- Backpressure policy MUST be latest-complete-submission-wins. The system MUST NOT accumulate an unbounded frame queue.
|
||||||
|
|
||||||
|
### AppMode Routing
|
||||||
|
|
||||||
|
- `AppMode::Game` SHALL expose and route Game render domains.
|
||||||
|
- `AppMode::Shell` SHALL expose and route Shell UI render domains.
|
||||||
|
- Renderers/capabilities MAY differ by app mode.
|
||||||
|
- PBS stdlibs MAY expose similar high-level names, including `gfx`, but they MUST map to mode-appropriate ABI domains.
|
||||||
|
|
||||||
|
### ABI Domains
|
||||||
|
|
||||||
|
- `composer.*` MUST remain the high-level Game 2D composition domain.
|
||||||
|
- `composer.*` MUST own scene, camera, sprites, HUD, and Game 2D frame orchestration.
|
||||||
|
- `gfx2d.*` MUST contain Game 2D primitives only.
|
||||||
|
- `gfx2d.*` MUST NOT own scene, camera, sprites, HUD, or frame orchestration.
|
||||||
|
- `gfxui.*` MUST contain Shell UI primitives.
|
||||||
|
- `gfxui.*` MUST NOT contain widget/layout policy. Widget/layout belongs in Shell/UI code or stdlib, not the host implementation.
|
||||||
|
- Capabilities MUST be separated for `composer`, `gfx2d`, and `gfxui`.
|
||||||
|
|
||||||
|
### Game 2D Composition
|
||||||
|
|
||||||
|
- Game 2D scene and sprites MUST compose by integer `layer`.
|
||||||
|
- The v1 Game 2D model has four available layers.
|
||||||
|
- Scene and sprite composition MAY interleave by layer, normally as sprite/scene ordering across the four layers.
|
||||||
|
- HUD MUST render above scene/sprite composition.
|
||||||
|
- HUD belongs to `composer` in v1 and remains a minimal, evolvable contract.
|
||||||
|
- HUD in this decision means Game HUD only. Shell/system UI is always part of the Shell UI pipeline.
|
||||||
|
- Game 2D publish happens after scene/sprite/HUD composition.
|
||||||
|
|
||||||
|
### Shell UI Composition
|
||||||
|
|
||||||
|
- Shell UI MUST use its own `gfxui.*` primitive command set.
|
||||||
|
- Shell UI commands MAY reduce to the same local drawing directives as Game 2D primitives inside a host/render-surface implementation.
|
||||||
|
- That reduction MUST remain an implementation detail and MUST NOT collapse the ABI domains.
|
||||||
|
|
||||||
|
### Fade and Transitions
|
||||||
|
|
||||||
|
- Fade MUST be removed completely from canonical pipeline contracts.
|
||||||
|
- Fade MUST NOT appear as a `Game2DFramePacket`, `UiFramePacket`, `RenderSubmission`, `composer`, `gfx2d`, or `gfxui` field or syscall.
|
||||||
|
- Future visual transitions MUST be coordinated by `RenderManager`.
|
||||||
|
|
||||||
|
### Debug Overlay
|
||||||
|
|
||||||
|
- The current host-owned debug overlay is not part of the new render architecture.
|
||||||
|
- It MAY be removed during execution.
|
||||||
|
- A future console/debug UI MUST be discussed as a separate product/domain, not preserved as inherited host overlay behavior.
|
||||||
|
|
||||||
|
## Impactos
|
||||||
|
|
||||||
|
### Spec
|
||||||
|
|
||||||
|
Specs must define the logical render pipeline model, `RenderManager`, `RenderSubmission`, app-mode routing, domain buffers, and publication boundary in English.
|
||||||
|
|
||||||
|
The public syscall/ABI specs must replace the old primitive `gfx.*` surface with mode-appropriate `gfx2d.*` and `gfxui.*` primitive domains while preserving `composer.*` as the high-level Game 2D composition domain.
|
||||||
|
|
||||||
|
### Runtime
|
||||||
|
|
||||||
|
Runtime frame execution must stop treating `Gfx`/framebuffer as the direct target of logical render calls. It must accumulate domain state/commands and let `RenderManager` close submissions.
|
||||||
|
|
||||||
|
`VirtualMachineRuntime` and firmware flow must route frame closure through `RenderManager`.
|
||||||
|
|
||||||
|
### HAL
|
||||||
|
|
||||||
|
HAL must expose distinct contracts for:
|
||||||
|
|
||||||
|
- `composer`;
|
||||||
|
- `gfx2d`;
|
||||||
|
- `gfxui`;
|
||||||
|
- render submissions;
|
||||||
|
- render manager/surface boundary.
|
||||||
|
|
||||||
|
Capability flags must be split accordingly.
|
||||||
|
|
||||||
|
### Host
|
||||||
|
|
||||||
|
The desktop host remains a concrete render-surface implementation. It may continue publishing RGBA8888 to `pixels`, but that behavior must sit below the render-surface adapter, not inside logical pipeline APIs.
|
||||||
|
|
||||||
|
Host debug overlay may be removed.
|
||||||
|
|
||||||
|
### Firmware / Shell
|
||||||
|
|
||||||
|
Shell UI must migrate away from direct `gfx_mut()` drawing toward `gfxui.*`/Shell UI command buffers.
|
||||||
|
|
||||||
|
Game mode must migrate toward `composer.*` plus `gfx2d.*` buffers closed by `RenderManager`.
|
||||||
|
|
||||||
|
### PBS / Stdlibs
|
||||||
|
|
||||||
|
PBS Game and Shell stdlibs may present similar high-level APIs to authors. They must map to different ABI domains according to `AppMode`.
|
||||||
|
|
||||||
|
Game stdlib should map scene/camera/sprites/HUD to `composer.*` and primitive Game drawing to `gfx2d.*`.
|
||||||
|
|
||||||
|
Shell stdlib should map UI primitives to `gfxui.*`.
|
||||||
|
|
||||||
|
## Alternativas Descartadas
|
||||||
|
|
||||||
|
### Universal RenderCommand
|
||||||
|
|
||||||
|
A single `RenderCommand` display list for Game, Shell, and future pipelines is rejected. It collapses domain meaning and would likely force future 3D or custom pipelines into a 2D/UI-shaped contract.
|
||||||
|
|
||||||
|
### Trait-Only RenderPipeline
|
||||||
|
|
||||||
|
A trait that calls `render(&mut Surface)` without a closed packet is rejected as the primary boundary. It preserves immediate rendering and does not prepare VM/render parallelism.
|
||||||
|
|
||||||
|
### Backend Registry Now
|
||||||
|
|
||||||
|
A full backend registry, GPU/CPU backend abstraction, dynamic pipeline switching, render thread, or 3D implementation is out of scope. Those may come later after the command boundary is stable.
|
||||||
|
|
||||||
|
### Moving Composer into Gfx2D
|
||||||
|
|
||||||
|
Moving scene, camera, sprites, HUD, or frame orchestration into `gfx2d.*` is rejected. `gfx2d.*` is primitives only.
|
||||||
|
|
||||||
|
## Referencias
|
||||||
|
|
||||||
|
- Agenda: `AGD-0038`
|
||||||
|
- Related dependency: `DSC-0011` dirty regions must wait until this boundary exists.
|
||||||
|
- Related prior lessons: frame composition belongs above the render backend; public ABI must follow canonical service boundaries; RGBA8888 is the canonical framebuffer contract.
|
||||||
|
|
||||||
|
## Propagacao Necessaria
|
||||||
|
|
||||||
|
- Create a plan before any spec or code changes.
|
||||||
|
- Update canonical specs in English.
|
||||||
|
- Update HAL syscall registry, capability definitions, and resolver tests.
|
||||||
|
- Update PBS/stdlib syscall declarations for Game and Shell.
|
||||||
|
- Update runtime dispatch so render syscalls mutate domain buffers instead of drawing directly.
|
||||||
|
- Introduce `RenderManager`, domain buffers, `RenderSubmission`, and render-surface implementation boundary.
|
||||||
|
- Migrate Game 2D first, then Shell UI, unless the plan proves both can be safely migrated together.
|
||||||
|
- Remove fade surfaces from render contracts.
|
||||||
|
- Remove or explicitly retire the host debug overlay.
|
||||||
|
- Add packet/submission tests, app-mode capability tests, and pixel-equivalence tests for unchanged visible behavior.
|
||||||
|
|
||||||
|
## Revision Log
|
||||||
|
|
||||||
|
- 2026-05-25: Initial decision draft from `AGD-0038`.
|
||||||
67
discussion/workflow/plans/PLN-0073-render-contract-specs.md
Normal file
67
discussion/workflow/plans/PLN-0073-render-contract-specs.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0073
|
||||||
|
ticket: render-frame-packet-boundary
|
||||||
|
title: Render Contract Specs
|
||||||
|
status: done
|
||||||
|
created: 2026-05-25
|
||||||
|
ref_decisions: [DEC-0030]
|
||||||
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
Source decision: `DEC-0030`.
|
||||||
|
|
||||||
|
Publish canonical English spec language for the logical render pipeline boundary before code migration. The specs must make `RenderManager`, domain buffers, typed `RenderSubmission`, app-mode routing, render-surface publication, latest-complete-submission policy, and fade removal explicit.
|
||||||
|
|
||||||
|
## Target
|
||||||
|
|
||||||
|
Canonical specs describe the DEC-0030 render contract well enough that HAL, runtime, host, firmware, and PBS work can proceed without reopening architecture.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Define the logical flow from mutable domain buffers to closed `RenderSubmission` to render-surface publication.
|
||||||
|
- Define `RenderManager` as runtime coordination, not a host renderer.
|
||||||
|
- Define `RenderSubmission` with `frame_id`, `app_mode`, and typed Game 2D or Shell UI packet.
|
||||||
|
- Define `composer.*`, `gfx2d.*`, and `gfxui.*` as separate ABI domains.
|
||||||
|
- Specify latest-complete-submission-wins backpressure.
|
||||||
|
- Remove fade from canonical render contracts.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Rust implementation.
|
||||||
|
- PBS stdlib implementation.
|
||||||
|
- Pixel output changes.
|
||||||
|
- Backend registry, GPU backend, render thread, 3D pipeline, or debug console design.
|
||||||
|
|
||||||
|
## Execution Sequence
|
||||||
|
|
||||||
|
1. Inventory existing render, graphics, framebuffer, composer, and syscall specs.
|
||||||
|
2. Replace old primitive `gfx.*` ABI language with the DEC-0030 domain split.
|
||||||
|
3. Add sections for `RenderManager`, domain buffers, submission closure, immutability, and publication.
|
||||||
|
4. Add app-mode and capability routing language for Game and Shell.
|
||||||
|
5. Remove normative fade fields, syscalls, and packet references from render specs.
|
||||||
|
6. Cross-link impacted specs for HAL, runtime, host, firmware, and PBS propagation.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- Specs are in English and reference `DEC-0030`.
|
||||||
|
- No canonical render spec presents direct framebuffer mutation as the logical render API.
|
||||||
|
- No canonical render spec keeps fade as a packet field, syscall, or ABI habit.
|
||||||
|
- Specs distinguish logical pipeline production from render-surface implementation.
|
||||||
|
|
||||||
|
## Tests / Validation
|
||||||
|
|
||||||
|
- Run repository documentation/spec validation if available.
|
||||||
|
- Inspect `rg -n "fade|gfx\\.\\*|RenderSubmission|RenderManager|present\\(" specs discussion/workflow/decisions/DEC-0030-logical-render-pipeline-command-boundary.md`.
|
||||||
|
- Run `discussion validate` if discussion references change.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- Existing specs may use `gfx.*` as both author API and ABI name; preserve author clarity while changing ABI names.
|
||||||
|
- Removing fade text can accidentally delete transition requirements; move those requirements to `RenderManager` language.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- `specs/`
|
||||||
|
- `discussion/workflow/decisions/DEC-0030-logical-render-pipeline-command-boundary.md`
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0074
|
||||||
|
ticket: render-frame-packet-boundary
|
||||||
|
title: HAL Render Submission Types
|
||||||
|
status: done
|
||||||
|
created: 2026-05-25
|
||||||
|
ref_decisions: [DEC-0030]
|
||||||
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
Source decision: `DEC-0030`.
|
||||||
|
|
||||||
|
Introduce HAL-level submission types that encode the logical render boundary without binding consumers to live `Hardware`, `Gfx`, framebuffer, or VM state.
|
||||||
|
|
||||||
|
## Target
|
||||||
|
|
||||||
|
HAL exposes typed contracts for `RenderSubmission`, `Game2DFramePacket`, `ShellUiFramePacket`, frame ids, app-mode routing, and immutable closed submissions.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Add typed packet structures for Game 2D and Shell UI.
|
||||||
|
- Add a `RenderSubmission` envelope with `frame_id`, `app_mode`, and typed packet variant.
|
||||||
|
- Add or reuse a monotonic frame id type.
|
||||||
|
- Encode closed-submission immutability through ownership and API shape.
|
||||||
|
- Keep packet types free of host presentation details and live mutable runtime references.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- `RenderManager` behavior beyond type integration.
|
||||||
|
- Renderer/rasterizer implementation.
|
||||||
|
- Backend registry, render thread, GPU path, or 3D packet.
|
||||||
|
- PBS stdlib updates.
|
||||||
|
|
||||||
|
## Execution Sequence
|
||||||
|
|
||||||
|
1. Locate current HAL graphics, composer, capability, and app-mode modules.
|
||||||
|
2. Add packet and submission types where ABI-facing render data belongs.
|
||||||
|
3. Represent packets as typed variants: `Game2D(Game2DFramePacket)` and `ShellUi(ShellUiFramePacket)`.
|
||||||
|
4. Ensure constructors or closure APIs consume or clone domain buffer contents.
|
||||||
|
5. Add tests for app-mode and packet coherence.
|
||||||
|
6. Update internal references to use the new types without changing runtime rendering yet.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- HAL contains the DEC-0030 submission and packet vocabulary.
|
||||||
|
- A closed `RenderSubmission` can be handed to a consumer without borrowing live runtime rendering state.
|
||||||
|
- Game and Shell packets are distinguishable at the type level.
|
||||||
|
- The HAL API does not include fade fields.
|
||||||
|
|
||||||
|
## Tests / Validation
|
||||||
|
|
||||||
|
- Run HAL crate unit tests.
|
||||||
|
- Add tests for packet construction, app-mode coherence, and immutable ownership semantics.
|
||||||
|
- Inspect `rg -n "fade|UiFramePacket|ShellUiFramePacket|Game2DFramePacket|RenderSubmission" crates specs`.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- New types must live at the ABI/contract level, not inside a host renderer.
|
||||||
|
- Borrowing shortcuts could leak mutability and violate the future parallel render contract.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- HAL crate/module under `crates/`
|
||||||
|
- HAL tests
|
||||||
68
discussion/workflow/plans/PLN-0075-rendermanager-core.md
Normal file
68
discussion/workflow/plans/PLN-0075-rendermanager-core.md
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0075
|
||||||
|
ticket: render-frame-packet-boundary
|
||||||
|
title: RenderManager Core
|
||||||
|
status: done
|
||||||
|
created: 2026-05-25
|
||||||
|
ref_decisions: [DEC-0030]
|
||||||
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
Source decision: `DEC-0030`.
|
||||||
|
|
||||||
|
Add `RenderManager` as the runtime abstraction that closes domain buffers into typed submissions, tracks active app mode, owns transition coordination placeholders, and hands the latest complete submission to the render surface.
|
||||||
|
|
||||||
|
## Target
|
||||||
|
|
||||||
|
Runtime frame execution routes render closure through `RenderManager` without making it a host renderer or direct framebuffer writer.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Add the `RenderManager` module and public runtime API.
|
||||||
|
- Track active `AppMode` and frame id generation.
|
||||||
|
- Close the active domain buffer into a `RenderSubmission`.
|
||||||
|
- Enforce latest-complete-submission-wins rather than an unbounded queue.
|
||||||
|
- Add transition coordination placeholders owned by `RenderManager`.
|
||||||
|
- Provide a publication handoff to a render-surface abstraction.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Full visual transition implementation.
|
||||||
|
- Host-specific presentation details.
|
||||||
|
- Domain buffer implementation beyond integration points.
|
||||||
|
- Render thread, GPU backend, or backend registry.
|
||||||
|
|
||||||
|
## Execution Sequence
|
||||||
|
|
||||||
|
1. Add a runtime render module that owns `RenderManager` and related state.
|
||||||
|
2. Wire `VirtualMachineRuntime` frame closure through `RenderManager`.
|
||||||
|
3. Add active app-mode routing and rejection for incoherent packet closure.
|
||||||
|
4. Add latest-complete-submission storage and replacement semantics.
|
||||||
|
5. Add a render-surface handoff interface without calling host-specific `present()` from producers.
|
||||||
|
6. Add explicit no-op transition hooks for future work.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- Runtime has one frame closure authority for render submissions.
|
||||||
|
- Producers can mutate domain buffers before closure but cannot mutate the closed submission.
|
||||||
|
- `RenderManager` does not depend on desktop host types.
|
||||||
|
- Backpressure is latest-complete-submission-wins.
|
||||||
|
- App-mode routing is explicit and tested.
|
||||||
|
|
||||||
|
## Tests / Validation
|
||||||
|
|
||||||
|
- Add unit tests for frame id increment, latest submission replacement, app-mode routing, and no-op transition state.
|
||||||
|
- Add runtime integration coverage proving frame closure passes through `RenderManager`.
|
||||||
|
- Run affected crate tests.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- Existing frame lifecycle code may assume immediate framebuffer mutation; isolate compatibility shims and remove them in later plans.
|
||||||
|
- Transition placeholders must not become hidden fade behavior.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- Runtime crate/modules under `crates/`
|
||||||
|
- Runtime tests
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0076
|
||||||
|
ticket: render-frame-packet-boundary
|
||||||
|
title: Capabilities and ABI Domain Split
|
||||||
|
status: done
|
||||||
|
created: 2026-05-25
|
||||||
|
ref_decisions: [DEC-0030]
|
||||||
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
Source decision: `DEC-0030`.
|
||||||
|
|
||||||
|
Split render capabilities and syscall metadata so `composer`, `gfx2d`, and `gfxui` are independently addressable and enforceable according to `AppMode`.
|
||||||
|
|
||||||
|
## Target
|
||||||
|
|
||||||
|
Capability gates reject wrong-domain render syscalls, and metadata no longer treats the old primitive `gfx.*` ABI as the single render surface.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Add or rename capability flags for `COMPOSER`, `GFX2D`, and `GFXUI`.
|
||||||
|
- Update syscall metadata for all render-domain calls.
|
||||||
|
- Enforce domain availability by `AppMode::Game` and `AppMode::Shell`.
|
||||||
|
- Update resolver and capability tests.
|
||||||
|
- Preserve `composer.*` as Game 2D high-level composition.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Implementing command buffers.
|
||||||
|
- PBS stdlib author-facing wrappers.
|
||||||
|
- Host rendering.
|
||||||
|
- Backward compatibility for the old primitive `gfx.*` ABI.
|
||||||
|
|
||||||
|
## Execution Sequence
|
||||||
|
|
||||||
|
1. Locate capability definitions, syscall registry metadata, and resolver tests.
|
||||||
|
2. Introduce separate render capability identifiers for `composer`, `gfx2d`, and `gfxui`.
|
||||||
|
3. Update metadata for existing composer calls and new or renamed primitive domains.
|
||||||
|
4. Add app-mode checks so Game exposes `composer` and `gfx2d`, while Shell exposes `gfxui`.
|
||||||
|
5. Replace stale tests that assume one graphics capability.
|
||||||
|
6. Run resolver and runtime syscall tests.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- Wrong-domain calls fail before mutating render state.
|
||||||
|
- Game mode cannot use `gfxui.*`.
|
||||||
|
- Shell mode cannot use `composer.*` or `gfx2d.*`.
|
||||||
|
- Capability metadata uses the split domain names.
|
||||||
|
- No compatibility promise remains for the old primitive `gfx.*` ABI.
|
||||||
|
|
||||||
|
## Tests / Validation
|
||||||
|
|
||||||
|
- Add resolver tests for all three capabilities.
|
||||||
|
- Add app-mode enforcement tests for allowed and rejected calls.
|
||||||
|
- Run affected unit and integration tests.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- A large rename can hide semantic mistakes; verify every syscall maps to the correct domain responsibility.
|
||||||
|
- Author-facing stdlib names may still use `gfx`; keep this plan focused on ABI metadata.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- Syscall metadata and resolver modules under `crates/`
|
||||||
|
- Capability tests
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0077
|
||||||
|
ticket: render-frame-packet-boundary
|
||||||
|
title: Composer Buffer and Game2D Packet
|
||||||
|
status: done
|
||||||
|
created: 2026-05-25
|
||||||
|
ref_decisions: [DEC-0030]
|
||||||
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
Source decision: `DEC-0030`.
|
||||||
|
|
||||||
|
Convert current `composer.*` frame state into a mutable `ComposerBuffer` that closes into `Game2DFramePacket` at frame boundary.
|
||||||
|
|
||||||
|
## Target
|
||||||
|
|
||||||
|
Game 2D scene, camera, sprites, HUD, and frame orchestration are represented as domain-owned state until `RenderManager` closes them into a typed packet.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Add `ComposerBuffer`.
|
||||||
|
- Move scene, camera, sprite, HUD, layer ordering, and Game 2D orchestration state into the buffer.
|
||||||
|
- Close the buffer into `Game2DFramePacket`.
|
||||||
|
- Preserve the v1 four-layer scene/sprite composition model and HUD-above-game rule.
|
||||||
|
- Ensure fade is absent from Game 2D packet state.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Primitive `gfx2d.*` command buffering.
|
||||||
|
- Shell UI packet work.
|
||||||
|
- Renderer consumption refactor beyond packet shape.
|
||||||
|
- New Game 2D features.
|
||||||
|
|
||||||
|
## Execution Sequence
|
||||||
|
|
||||||
|
1. Locate current `FrameComposer`, composer syscalls, scene/camera/sprite/HUD state, and tests.
|
||||||
|
2. Introduce `ComposerBuffer` and migrate composer mutation paths into it.
|
||||||
|
3. Implement closure into `Game2DFramePacket` with immutable packet contents.
|
||||||
|
4. Preserve integer layer ordering across the four Game 2D layers and HUD final composition.
|
||||||
|
5. Route frame closure through `RenderManager`.
|
||||||
|
6. Update tests that currently inspect `FrameComposer` internals.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- `composer.*` owns Game 2D high-level frame state.
|
||||||
|
- Closed `Game2DFramePacket` contains all data required by the Game 2D renderer without live VM or `Gfx` access.
|
||||||
|
- Scene/sprite layer behavior and HUD ordering are preserved.
|
||||||
|
- Fade is not represented.
|
||||||
|
|
||||||
|
## Tests / Validation
|
||||||
|
|
||||||
|
- Add unit tests for buffer mutation and packet closure.
|
||||||
|
- Add composition ordering tests for scene, sprites, and HUD.
|
||||||
|
- Run current frame composer/render tests and compare existing expected output.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- Existing composer code may mix orchestration with drawing; split state capture from rendering before deeper migration.
|
||||||
|
- Layer behavior is user-visible and must not drift.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- Composer/frame-composer modules under `crates/`
|
||||||
|
- Game 2D tests
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0078
|
||||||
|
ticket: render-frame-packet-boundary
|
||||||
|
title: Classic2D Game Renderer Consumer
|
||||||
|
status: done
|
||||||
|
created: 2026-05-25
|
||||||
|
ref_decisions: [DEC-0030]
|
||||||
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
Source decision: `DEC-0030`.
|
||||||
|
|
||||||
|
Refactor the Classic2D renderer path so it consumes `Game2DFramePacket` instead of being called directly by `FrameComposer` or logical producers.
|
||||||
|
|
||||||
|
## Target
|
||||||
|
|
||||||
|
Classic2D remains the concrete Game 2D rasterizer, but its input is a closed packet and its visible pixel output remains equivalent.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Add a packet consumer entry point for the current Classic2D/Gfx renderer.
|
||||||
|
- Replace direct `FrameComposer` to `Gfx` calls with packet consumption.
|
||||||
|
- Keep RGBA8888 output unchanged for existing supported scenes.
|
||||||
|
- Keep renderer implementation below the logical pipeline boundary.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Redesigning scene, sprite, HUD, or primitive semantics.
|
||||||
|
- Shell UI rendering.
|
||||||
|
- Backend registry or GPU work.
|
||||||
|
- Dirty region optimization.
|
||||||
|
|
||||||
|
## Execution Sequence
|
||||||
|
|
||||||
|
1. Locate the current Classic2D/Gfx rendering entry points and `FrameComposer` call graph.
|
||||||
|
2. Add a `Game2DFramePacket` consumer API.
|
||||||
|
3. Move rendering reads from live composer state to packet data.
|
||||||
|
4. Update frame execution to pass the closed packet into the renderer through the render surface path.
|
||||||
|
5. Preserve existing framebuffer dimensions, RGBA8888 format, and pixel behavior.
|
||||||
|
6. Remove direct producer-to-renderer calls that bypass the packet boundary.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- Classic2D renders from `Game2DFramePacket`.
|
||||||
|
- Logical composer code no longer directly mutates `Gfx` during frame production.
|
||||||
|
- Existing Game 2D visual fixtures or tests remain pixel-equivalent except where explicitly updated for the new boundary.
|
||||||
|
|
||||||
|
## Tests / Validation
|
||||||
|
|
||||||
|
- Add packet-to-pixels tests for representative scene, sprite, primitive, and HUD cases.
|
||||||
|
- Run existing render tests.
|
||||||
|
- Add regression coverage comparing old expected pixel buffers where available.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- Packet conversion can accidentally alter draw order; keep tests focused on ordering and final pixels.
|
||||||
|
- Some current code may depend on side effects from direct `Gfx` mutation; identify and replace those effects explicitly.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- Classic2D/Gfx renderer modules under `crates/`
|
||||||
|
- Render tests and fixtures
|
||||||
66
discussion/workflow/plans/PLN-0079-gfx2d-primitive-domain.md
Normal file
66
discussion/workflow/plans/PLN-0079-gfx2d-primitive-domain.md
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0079
|
||||||
|
ticket: render-frame-packet-boundary
|
||||||
|
title: Gfx2D Primitive Domain
|
||||||
|
status: done
|
||||||
|
created: 2026-05-25
|
||||||
|
ref_decisions: [DEC-0030]
|
||||||
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
Source decision: `DEC-0030`.
|
||||||
|
|
||||||
|
Replace primitive Game drawing paths with `gfx2d.*` command buffering while keeping `gfx2d` limited to primitives only.
|
||||||
|
|
||||||
|
## Target
|
||||||
|
|
||||||
|
Game primitive drawing becomes a Game-mode ABI domain that records commands into a buffer consumed as part of Game 2D rendering.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Add `gfx2d.*` syscall domain and command buffer.
|
||||||
|
- Migrate primitive Game drawing calls from old `gfx.*` behavior to `gfx2d.*`.
|
||||||
|
- Ensure `gfx2d` contains primitives only.
|
||||||
|
- Integrate `gfx2d` commands into the Game 2D packet/render path without taking ownership of scene, camera, sprites, HUD, or frame orchestration.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Shell UI primitive domain.
|
||||||
|
- `composer.*` high-level Game state.
|
||||||
|
- Widget/layout policy.
|
||||||
|
- Compatibility with old primitive `gfx.*` ABI names.
|
||||||
|
|
||||||
|
## Execution Sequence
|
||||||
|
|
||||||
|
1. Inventory old primitive `gfx.*` syscalls and direct drawing callsites used by Game mode.
|
||||||
|
2. Define the `gfx2d.*` command enum and mutable command buffer.
|
||||||
|
3. Route Game primitive syscalls into the buffer.
|
||||||
|
4. Add app-mode and capability enforcement for Game-only access.
|
||||||
|
5. Integrate buffered primitives into Game 2D packet closure or renderer consumption at the DEC-0030-approved layer.
|
||||||
|
6. Remove or retire old primitive `gfx.*` ABI registrations.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- Game primitive commands are buffered as `gfx2d.*`.
|
||||||
|
- `gfx2d` does not own scene, camera, sprites, HUD, or frame orchestration.
|
||||||
|
- Shell mode cannot call `gfx2d.*`.
|
||||||
|
- No logical primitive path directly mutates framebuffer during production.
|
||||||
|
|
||||||
|
## Tests / Validation
|
||||||
|
|
||||||
|
- Add syscall tests for each migrated primitive.
|
||||||
|
- Add command buffer ordering tests.
|
||||||
|
- Add capability rejection tests for Shell mode.
|
||||||
|
- Run pixel-equivalence tests for unchanged primitive output.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- The old `gfx` naming may appear in PBS wrappers; distinguish author API names from ABI domain names.
|
||||||
|
- Primitive ordering relative to composer output must be explicit.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- Syscall registry and runtime render domain modules under `crates/`
|
||||||
|
- Game primitive tests
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0080
|
||||||
|
ticket: render-frame-packet-boundary
|
||||||
|
title: Shell UI Packet and GfxUI Domain
|
||||||
|
status: done
|
||||||
|
created: 2026-05-25
|
||||||
|
ref_decisions: [DEC-0030]
|
||||||
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
Source decision: `DEC-0030`.
|
||||||
|
|
||||||
|
Add `gfxui.*` command buffering and `ShellUiFramePacket` for Shell UI primitives while keeping widget/layout policy outside the host/render-surface implementation.
|
||||||
|
|
||||||
|
## Target
|
||||||
|
|
||||||
|
Shell UI emits typed UI primitive commands into a Shell-only packet that render-surface implementations can consume without collapsing UI and Game ABI domains.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Define `gfxui.*` primitive commands.
|
||||||
|
- Add a mutable Shell UI command buffer.
|
||||||
|
- Close Shell UI commands into `ShellUiFramePacket`.
|
||||||
|
- Enforce Shell-only capability and app-mode access.
|
||||||
|
- Keep widget/layout policy in Shell/UI code or stdlib layers.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Migrating all Shell callsites; that is handled by the Shell/Hub migration plan.
|
||||||
|
- Game primitive rendering.
|
||||||
|
- New widget framework.
|
||||||
|
- Host-owned debug overlay.
|
||||||
|
|
||||||
|
## Execution Sequence
|
||||||
|
|
||||||
|
1. Define the `gfxui.*` syscall and command vocabulary needed for current Shell UI primitives.
|
||||||
|
2. Add a Shell UI command buffer and closure into `ShellUiFramePacket`.
|
||||||
|
3. Add capability metadata and resolver enforcement for `GFXUI`.
|
||||||
|
4. Add a render-surface consumer path that can rasterize Shell UI commands as an implementation detail.
|
||||||
|
5. Keep layout/widget decisions above the packet boundary.
|
||||||
|
6. Add tests for closure, app-mode gating, and packet consumption.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- Shell UI commands close into `ShellUiFramePacket`.
|
||||||
|
- `gfxui.*` does not contain widget/layout policy.
|
||||||
|
- Game mode cannot call `gfxui.*`.
|
||||||
|
- Host/render-surface code consumes commands without redefining Shell UI product policy.
|
||||||
|
|
||||||
|
## Tests / Validation
|
||||||
|
|
||||||
|
- Add Shell UI command buffer unit tests.
|
||||||
|
- Add app-mode rejection tests for Game mode.
|
||||||
|
- Add render-surface consumer tests for representative primitives.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- It is easy to migrate widget policy into host rendering while adding UI primitives; keep commands low-level and policy-free.
|
||||||
|
- Shell UI and Game primitives may rasterize similarly, but ABI domains must remain distinct.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- Shell UI render domain modules under `crates/`
|
||||||
|
- Syscall metadata and tests
|
||||||
67
discussion/workflow/plans/PLN-0081-shell-hub-migration.md
Normal file
67
discussion/workflow/plans/PLN-0081-shell-hub-migration.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0081
|
||||||
|
ticket: render-frame-packet-boundary
|
||||||
|
title: Shell Hub Migration
|
||||||
|
status: done
|
||||||
|
created: 2026-05-25
|
||||||
|
ref_decisions: [DEC-0030]
|
||||||
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
Source decision: `DEC-0030`.
|
||||||
|
|
||||||
|
Move Prometeu Hub, shell windows, splash/crash UI, and Shell drawing away from `gfx_mut()` into `gfxui.*` and Shell UI command buffers.
|
||||||
|
|
||||||
|
## Target
|
||||||
|
|
||||||
|
Shell-facing UI no longer draws directly into `Gfx`; it produces Shell UI commands that close into `ShellUiFramePacket`.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Migrate Prometeu Hub UI drawing.
|
||||||
|
- Migrate shell windows and window-manager drawing.
|
||||||
|
- Migrate splash and crash UI drawing.
|
||||||
|
- Remove direct Shell access to `gfx_mut()` for render production.
|
||||||
|
- Preserve visible Shell output where behavior is not intentionally changed.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Designing new Hub visuals.
|
||||||
|
- New widget/layout framework.
|
||||||
|
- Game mode migration.
|
||||||
|
- Debug console replacement.
|
||||||
|
|
||||||
|
## Execution Sequence
|
||||||
|
|
||||||
|
1. Inventory Shell, Hub, splash, crash, and firmware drawing callsites that use `gfx_mut()` or direct framebuffer access.
|
||||||
|
2. Replace direct primitive drawing with `gfxui.*` buffer writes.
|
||||||
|
3. Route Shell frame closure through `RenderManager`.
|
||||||
|
4. Ensure Shell UI packets are consumed by the render-surface implementation.
|
||||||
|
5. Remove now-unused direct Shell rendering hooks.
|
||||||
|
6. Update tests and snapshots for unchanged visible behavior.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- Shell UI production no longer requires mutable `Gfx` access.
|
||||||
|
- Hub, shell windows, splash, and crash UI can render through `ShellUiFramePacket`.
|
||||||
|
- Shell mode does not use `composer.*` or `gfx2d.*`.
|
||||||
|
- Visible UI output is preserved except for approved fixture updates caused by boundary-only refactoring.
|
||||||
|
|
||||||
|
## Tests / Validation
|
||||||
|
|
||||||
|
- Add integration tests for Hub/shell frame production.
|
||||||
|
- Add tests for splash and crash UI packet output.
|
||||||
|
- Run host smoke tests where available.
|
||||||
|
- Use pixel or snapshot comparisons for representative Shell screens.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- Firmware and Shell code may mix lifecycle side effects with drawing; split command production without changing lifecycle semantics.
|
||||||
|
- Some host tests may assume immediate framebuffer writes; update them to observe submissions or final pixels.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- Shell, Hub, firmware, and host integration modules under `crates/`
|
||||||
|
- Shell UI tests and fixtures
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0082
|
||||||
|
ticket: render-frame-packet-boundary
|
||||||
|
title: Frame Publication and Present Boundary
|
||||||
|
status: done
|
||||||
|
created: 2026-05-25
|
||||||
|
ref_decisions: [DEC-0030]
|
||||||
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
Source decision: `DEC-0030`.
|
||||||
|
|
||||||
|
Move `present()` and surface publication behind the render-surface implementation boundary so runtime producers no longer publish buffers directly.
|
||||||
|
|
||||||
|
## Target
|
||||||
|
|
||||||
|
Only the render-surface implementation publishes RGBA8888 output; logical producers and `RenderManager` hand off closed submissions through a boundary.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Identify all direct `present()` and surface publication callsites.
|
||||||
|
- Define the render-surface adapter boundary.
|
||||||
|
- Route publication through the adapter from `RenderManager`.
|
||||||
|
- Keep desktop `pixels` publication as a host implementation detail.
|
||||||
|
- Preserve frame pacing and invalidation behavior.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- New backend registry.
|
||||||
|
- GPU implementation.
|
||||||
|
- Render thread.
|
||||||
|
- Dirty regions.
|
||||||
|
- Visual transition design.
|
||||||
|
|
||||||
|
## Execution Sequence
|
||||||
|
|
||||||
|
1. Inventory `present()`, framebuffer publication, and host invalidation callsites.
|
||||||
|
2. Add or refine a render-surface trait/adapter that consumes `RenderSubmission`.
|
||||||
|
3. Move host desktop publication below that adapter.
|
||||||
|
4. Update runtime frame loop to publish only through `RenderManager` and the adapter.
|
||||||
|
5. Remove direct publication from composer, `gfx2d`, `gfxui`, Shell, and Game producers.
|
||||||
|
6. Verify frame pacing and host invalidation still occur at the correct boundary.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- Logical render domains cannot call `present()` or publish buffers directly.
|
||||||
|
- Host desktop publication remains functional below the render-surface boundary.
|
||||||
|
- `RenderManager` owns the policy for selecting the latest complete submission.
|
||||||
|
- Existing frame pacing behavior is not regressed.
|
||||||
|
|
||||||
|
## Tests / Validation
|
||||||
|
|
||||||
|
- Add unit tests for render-surface handoff.
|
||||||
|
- Add integration tests proving producers cannot publish directly.
|
||||||
|
- Run host desktop render/pacing tests.
|
||||||
|
- Inspect `rg -n "present\\(|publish|pixels" crates` for boundary violations.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- Publication and invalidation may be coupled in current host code; preserve timing semantics while moving ownership.
|
||||||
|
- A too-broad adapter can become a backend registry prematurely.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- Runtime render manager modules
|
||||||
|
- Host desktop render surface modules
|
||||||
|
- Frame pacing tests
|
||||||
63
discussion/workflow/plans/PLN-0083-fade-removal.md
Normal file
63
discussion/workflow/plans/PLN-0083-fade-removal.md
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0083
|
||||||
|
ticket: render-frame-packet-boundary
|
||||||
|
title: Fade Removal
|
||||||
|
status: done
|
||||||
|
created: 2026-05-25
|
||||||
|
ref_decisions: [DEC-0030]
|
||||||
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
Source decision: `DEC-0030`.
|
||||||
|
|
||||||
|
Remove fade fields, syscalls, packet members, renderer state, tests, and docs from canonical render contracts.
|
||||||
|
|
||||||
|
## Target
|
||||||
|
|
||||||
|
Fade is no longer a render pipeline feature. Future visual transitions remain the responsibility of `RenderManager` and require separate product/domain work.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Remove fade from specs, HAL packet types, syscall metadata, runtime state, renderer state, tests, and PBS declarations.
|
||||||
|
- Remove or update tests that assert fade behavior.
|
||||||
|
- Add guard tests or searches that prevent reintroducing fade in canonical render contracts.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Implementing replacement visual transitions.
|
||||||
|
- Replacing fade with another transition API.
|
||||||
|
- Preserving backward compatibility for fade syscalls.
|
||||||
|
|
||||||
|
## Execution Sequence
|
||||||
|
|
||||||
|
1. Inventory all fade references in specs, crates, tests, examples, and PBS stdlibs.
|
||||||
|
2. Delete fade fields and syscalls from canonical ABI and packet definitions.
|
||||||
|
3. Remove renderer/runtime fade state and behavior.
|
||||||
|
4. Update callers and tests to stop setting or expecting fade.
|
||||||
|
5. Add documentation language that transitions belong to `RenderManager`.
|
||||||
|
6. Run targeted and full affected tests.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- No canonical packet, syscall, or render contract contains fade.
|
||||||
|
- Runtime and renderer code do not maintain fade state.
|
||||||
|
- Tests no longer depend on fade behavior.
|
||||||
|
- Specs clearly state future transitions are `RenderManager` responsibility.
|
||||||
|
|
||||||
|
## Tests / Validation
|
||||||
|
|
||||||
|
- Run `rg -n "\\bfade\\b|Fade" specs crates discussion/workflow` and verify remaining references are only historical decision/agenda text or explicit removal notes.
|
||||||
|
- Run affected unit and integration tests.
|
||||||
|
- Add regression tests preventing fade fields in current packet types.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- Removing tests can reduce coverage; replace fade-specific assertions with packet/transition boundary assertions where appropriate.
|
||||||
|
- Historical discussion files may keep fade references; do not rewrite history unless the framework requires it.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- Specs
|
||||||
|
- HAL, runtime, renderer, syscall, PBS, and tests under `crates/`
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0084
|
||||||
|
ticket: render-frame-packet-boundary
|
||||||
|
title: Host Debug Overlay Removal
|
||||||
|
status: done
|
||||||
|
created: 2026-05-25
|
||||||
|
ref_decisions: [DEC-0030]
|
||||||
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
Source decision: `DEC-0030`.
|
||||||
|
|
||||||
|
Remove the current host-owned debug overlay from the render path. Any future console/debug UI is a separate product/domain discussion.
|
||||||
|
|
||||||
|
## Target
|
||||||
|
|
||||||
|
Host rendering no longer injects an inherited debug overlay into Game or Shell output.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Identify host-owned debug overlay code and render hooks.
|
||||||
|
- Remove overlay rendering from the host render path.
|
||||||
|
- Remove overlay toggles/tests that assume inherited host drawing.
|
||||||
|
- Preserve non-render telemetry and logging where they are not tied to overlay drawing.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Designing a replacement debug console.
|
||||||
|
- Moving debug UI into `gfxui.*`.
|
||||||
|
- Changing telemetry collection.
|
||||||
|
- Removing unrelated developer diagnostics.
|
||||||
|
|
||||||
|
## Execution Sequence
|
||||||
|
|
||||||
|
1. Inventory debug overlay rendering callsites and tests.
|
||||||
|
2. Remove host overlay injection from Game and Shell render paths.
|
||||||
|
3. Delete overlay-specific drawing state, toggles, and fixture expectations.
|
||||||
|
4. Keep telemetry/logging APIs that do not render overlay pixels.
|
||||||
|
5. Update docs/specs to state that future debug UI requires a separate decision.
|
||||||
|
6. Run host and render tests.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- Host render path does not draw debug overlay pixels.
|
||||||
|
- Game and Shell output do not depend on host overlay state.
|
||||||
|
- Remaining diagnostics do not violate the render pipeline boundary.
|
||||||
|
- Docs do not preserve host overlay as an architectural requirement.
|
||||||
|
|
||||||
|
## Tests / Validation
|
||||||
|
|
||||||
|
- Run host render tests.
|
||||||
|
- Add or update tests proving overlay state does not affect final rendered output.
|
||||||
|
- Run `rg -n "debug overlay|overlay|DebugOverlay" crates specs discussion/workflow` and inspect remaining references.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- Removing overlay code can accidentally remove useful telemetry; keep non-render diagnostics intact.
|
||||||
|
- The word overlay may also refer to Game HUD/primitives; distinguish those from host debug overlay.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- Host desktop modules under `crates/`
|
||||||
|
- Host/render tests
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0085
|
||||||
|
ticket: render-frame-packet-boundary
|
||||||
|
title: PBS Stdlib and Syscall Declarations
|
||||||
|
status: done
|
||||||
|
created: 2026-05-25
|
||||||
|
ref_decisions: [DEC-0030]
|
||||||
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
Source decision: `DEC-0030`.
|
||||||
|
|
||||||
|
Update PBS Game and Shell stdlibs so high-level APIs map to app-mode-specific ABI domains: Game uses `composer.*` and `gfx2d.*`; Shell uses `gfxui.*`.
|
||||||
|
|
||||||
|
## Target
|
||||||
|
|
||||||
|
PBS authors can keep ergonomic high-level APIs where appropriate, but generated or declared syscalls use the correct DEC-0030 ABI domain for the active app mode.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Update Game stdlib declarations for `composer.*` and `gfx2d.*`.
|
||||||
|
- Update Shell stdlib declarations for `gfxui.*`.
|
||||||
|
- Remove old primitive `gfx.*` ABI declarations.
|
||||||
|
- Keep author-facing wrappers clear about mode-specific behavior.
|
||||||
|
- Update examples/tests that compile PBS code.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Runtime implementation of the syscall handlers.
|
||||||
|
- New visual APIs beyond the domain split.
|
||||||
|
- Backward compatibility for old primitive `gfx.*` ABI declarations.
|
||||||
|
- Widget/layout redesign.
|
||||||
|
|
||||||
|
## Execution Sequence
|
||||||
|
|
||||||
|
1. Inventory PBS stdlib files and syscall declaration generation.
|
||||||
|
2. Map Game scene/camera/sprites/HUD APIs to `composer.*`.
|
||||||
|
3. Map Game primitive APIs to `gfx2d.*`.
|
||||||
|
4. Map Shell primitive APIs to `gfxui.*`.
|
||||||
|
5. Remove or fail old primitive `gfx.*` ABI declarations.
|
||||||
|
6. Update PBS compile tests, examples, and syscall metadata expectations.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- PBS Game code emits only Game-allowed render ABI domains.
|
||||||
|
- PBS Shell code emits only Shell-allowed render ABI domains.
|
||||||
|
- Old primitive `gfx.*` ABI declarations are gone or explicitly rejected.
|
||||||
|
- Existing examples are updated to the new declarations.
|
||||||
|
|
||||||
|
## Tests / Validation
|
||||||
|
|
||||||
|
- Run PBS stdlib and compiler tests.
|
||||||
|
- Add Game and Shell declaration tests for emitted syscall names.
|
||||||
|
- Add negative tests for wrong-domain declarations.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- User-facing API names may remain similar while ABI names change; tests must assert emitted syscall domains, not just wrapper names.
|
||||||
|
- Examples can mask stale declarations if they do not compile in both Game and Shell contexts.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- PBS stdlib files
|
||||||
|
- Syscall declaration generation
|
||||||
|
- PBS tests and examples
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
id: PLN-0086
|
||||||
|
ticket: render-frame-packet-boundary
|
||||||
|
title: End-to-End Render Boundary Validation
|
||||||
|
status: done
|
||||||
|
created: 2026-05-25
|
||||||
|
ref_decisions: [DEC-0030]
|
||||||
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
Source decision: `DEC-0030`.
|
||||||
|
|
||||||
|
Add end-to-end validation proving the new boundary works across Game and Shell modes after the granular implementation plans land.
|
||||||
|
|
||||||
|
## Target
|
||||||
|
|
||||||
|
Integration coverage demonstrates typed submissions, app-mode capability gates, immutable closed submissions, render-surface publication, fade removal, and visible output preservation.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Add Game end-to-end tests from PBS/syscall production to `Game2DFramePacket` and final pixels.
|
||||||
|
- Add Shell end-to-end tests from UI production to `ShellUiFramePacket` and final pixels.
|
||||||
|
- Test wrong-domain capability failures.
|
||||||
|
- Test closed-submission immutability.
|
||||||
|
- Test latest-complete-submission-wins behavior.
|
||||||
|
- Test absence of fade and host debug overlay from canonical paths.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Implementing missing production code.
|
||||||
|
- New rendering features.
|
||||||
|
- Performance tuning.
|
||||||
|
- Dirty regions.
|
||||||
|
|
||||||
|
## Execution Sequence
|
||||||
|
|
||||||
|
1. Define representative Game and Shell scenarios that cover composer, `gfx2d`, and `gfxui`.
|
||||||
|
2. Add integration harness access to inspect closed submissions where appropriate.
|
||||||
|
3. Assert Game produces `RenderSubmission::Game2D` and Shell produces `RenderSubmission::ShellUi`.
|
||||||
|
4. Assert wrong-domain syscalls fail before mutating buffers.
|
||||||
|
5. Assert closed submissions remain unchanged after producers continue mutating the next frame.
|
||||||
|
6. Assert final pixels match existing expected output for unchanged scenarios.
|
||||||
|
7. Add searches or tests proving fade and host debug overlay are absent from the active render path.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- Game and Shell each have end-to-end typed submission tests.
|
||||||
|
- Capability gates reject `gfxui` in Game and `composer`/`gfx2d` in Shell.
|
||||||
|
- Closed submissions are immutable snapshots.
|
||||||
|
- Latest complete submission replaces older unpublished submissions without queue growth.
|
||||||
|
- Unchanged Game and Shell scenarios remain visually equivalent.
|
||||||
|
|
||||||
|
## Tests / Validation
|
||||||
|
|
||||||
|
- Run full affected Rust test suites.
|
||||||
|
- Run PBS compile/runtime integration tests.
|
||||||
|
- Run pixel-equivalence tests for representative Game and Shell output.
|
||||||
|
- Run `discussion validate` once execution discussion artifacts are updated.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- End-to-end tests may become brittle if they assert host details; assert the logical contract and final pixels, not internal host implementation names.
|
||||||
|
- Pixel-equivalence fixtures must account for intentional removals such as host debug overlay.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- Integration tests under `crates/`
|
||||||
|
- PBS tests
|
||||||
|
- Render fixtures
|
||||||
@ -9,7 +9,29 @@ Didactic companion: [`../learn/mental-model-gfx.md`](../runtime/learn/mental-mod
|
|||||||
|
|
||||||
The **GFX** peripheral is responsible for generating images in PROMETEU.
|
The **GFX** peripheral is responsible for generating images in PROMETEU.
|
||||||
|
|
||||||
It is an explicit 2D graphics device based on:
|
`DEC-0030` defines the current logical render boundary. PROMETEU render
|
||||||
|
production is split from render implementation:
|
||||||
|
|
||||||
|
```text
|
||||||
|
domain buffers during logical frame
|
||||||
|
-> RenderManager closes buffers
|
||||||
|
-> RenderSubmission snapshot
|
||||||
|
-> render surface / implementation consumes submission
|
||||||
|
-> RGBA8888 surface publication
|
||||||
|
```
|
||||||
|
|
||||||
|
The GFX peripheral remains the classic local raster implementation for
|
||||||
|
PROMETEU's 2D output. Logical render APIs do not target `Gfx` or the
|
||||||
|
framebuffer directly.
|
||||||
|
|
||||||
|
`RenderSubmission` is the closed snapshot passed across the render boundary. It
|
||||||
|
MUST contain a frame id, the active app mode, and one typed packet:
|
||||||
|
`Game2DFramePacket` for `AppMode::Game` or `ShellUiFramePacket` for
|
||||||
|
`AppMode::Shell`. Once closed, producers MUST treat the submission as
|
||||||
|
immutable. The runtime backpressure policy is latest-complete-submission-wins;
|
||||||
|
it MUST NOT accumulate an unbounded frame queue.
|
||||||
|
|
||||||
|
The current 2D graphics model is based on:
|
||||||
|
|
||||||
- framebuffer
|
- framebuffer
|
||||||
- tilemaps
|
- tilemaps
|
||||||
@ -48,19 +70,19 @@ palette, host presentation, or compatibility contract.
|
|||||||
|
|
||||||
## 3. Double Buffering
|
## 3. Double Buffering
|
||||||
|
|
||||||
The GFX maintains two buffers:
|
The render surface implementation maintains the concrete buffers needed to
|
||||||
|
publish a frame. The classic software implementation uses:
|
||||||
|
|
||||||
- **Back Buffer** — where the frame is built
|
- **Back Buffer** — where the frame is built
|
||||||
- **Front Buffer** — where the frame is displayed
|
- **Front Buffer** — where the frame is displayed
|
||||||
|
|
||||||
Per-frame flow:
|
Per-frame flow:
|
||||||
|
|
||||||
1. The system prepares the logical frame
|
1. Game or Shell code mutates mode-specific domain buffers during the logical frame.
|
||||||
2. Canonical game composition is rendered into the back buffer
|
2. `RenderManager` closes the active buffers into an immutable `RenderSubmission`.
|
||||||
3. Deferred final overlay/debug primitives are drained on top of the completed game frame
|
3. The render surface implementation consumes the submission and rasterizes to its back buffer.
|
||||||
4. Calls `present()`
|
4. The render surface publishes the completed RGBA8888 surface.
|
||||||
5. Buffers are swapped
|
5. The host displays the published surface.
|
||||||
6. The host displays the front buffer
|
|
||||||
|
|
||||||
This guarantees:
|
This guarantees:
|
||||||
|
|
||||||
@ -192,14 +214,20 @@ Access:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. Canonical Game Projection to the Back Buffer
|
## 10. Canonical Game Projection
|
||||||
|
|
||||||
For each frame:
|
Game mode uses a typed Game 2D submission. `composer.*` owns high-level Game 2D
|
||||||
|
frame composition: scene binding, camera, sprites, HUD, and frame orchestration.
|
||||||
|
`gfx2d.*` owns Game 2D primitives only. Both domains are mutable while the
|
||||||
|
logical frame is being produced and are closed by `RenderManager` into a
|
||||||
|
`Game2DFramePacket`.
|
||||||
|
|
||||||
|
For each Game 2D packet:
|
||||||
|
|
||||||
1. For each Tile Layer, in order:
|
1. For each Tile Layer, in order:
|
||||||
- Rasterize visible tiles from the cache
|
- Rasterize visible tiles from the cache
|
||||||
- Apply scroll, flip, and transparency
|
- Apply scroll, flip, and transparency
|
||||||
- Write to the back buffer
|
- Write to the render surface's working buffer
|
||||||
|
|
||||||
2. Draw sprites:
|
2. Draw sprites:
|
||||||
- With priority between layers
|
- With priority between layers
|
||||||
@ -207,9 +235,11 @@ For each frame:
|
|||||||
|
|
||||||
3. Draw HUD layer last
|
3. Draw HUD layer last
|
||||||
|
|
||||||
This section describes only the canonical game composition path.
|
4. Draw buffered `gfx2d.*` primitives according to the Game 2D packet contract.
|
||||||
|
|
||||||
`gfx.*` primitives such as `draw_text`, `draw_line`, and `draw_disc` are not part of this canonical game projection order. In v1 they belong to a deferred final overlay/debug stage that is drained after canonical game composition is complete.
|
This section describes only the Game 2D packet rendering path. Shell/system UI
|
||||||
|
uses `gfxui.*` and `ShellUiFramePacket`; it is never part of Game HUD or
|
||||||
|
`composer.*`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -227,15 +257,15 @@ Base order:
|
|||||||
4. Tile Layer 3
|
4. Tile Layer 3
|
||||||
5. Sprites (by priority between layers)
|
5. Sprites (by priority between layers)
|
||||||
6. HUD Layer
|
6. HUD Layer
|
||||||
7. Scene Fade
|
7. Buffered `gfx2d.*` Game primitives
|
||||||
8. HUD Fade
|
|
||||||
9. Deferred `gfx.*` overlay/debug primitives
|
|
||||||
|
|
||||||
Normative boundary:
|
Normative boundary:
|
||||||
|
|
||||||
- Items 1 through 8 belong to canonical game-frame composition.
|
- Items 1 through 6 belong to `composer.*` Game-frame composition.
|
||||||
- Item 9 is a separate overlay/debug stage.
|
- Item 7 belongs to `gfx2d.*`.
|
||||||
- Deferred `gfx.*` primitives MUST NOT be interpreted as scene, sprite, or canonical HUD content.
|
- `gfx2d.*` primitives MUST NOT be interpreted as scene, sprite, camera, HUD,
|
||||||
|
or frame orchestration.
|
||||||
|
- Shell/system UI belongs to `gfxui.*` and `ShellUiFramePacket`, not Game HUD.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -287,7 +317,8 @@ Everything is:
|
|||||||
|
|
||||||
- Blending occurs during drawing
|
- Blending occurs during drawing
|
||||||
- For canonical game composition, the result goes to the back buffer during composition
|
- For canonical game composition, the result goes to the back buffer during composition
|
||||||
- For deferred `gfx.*` overlay/debug primitives, the result is applied during the final overlay/debug drain stage
|
- For `gfx2d.*` and `gfxui.*` primitives, the result is applied when the render
|
||||||
|
surface consumes the closed packet for the active app mode
|
||||||
- There is no automatic GPU-style post-processing pipeline
|
- There is no automatic GPU-style post-processing pipeline
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -301,6 +332,7 @@ By design:
|
|||||||
- HDR
|
- HDR
|
||||||
- Gamma correction
|
- Gamma correction
|
||||||
- RGB565 compatibility framebuffers
|
- RGB565 compatibility framebuffers
|
||||||
|
- fade fields, fade syscalls, or fade packet members
|
||||||
- multi-format backend selection
|
- multi-format backend selection
|
||||||
- render-thread ownership as part of this contract
|
- render-thread ownership as part of this contract
|
||||||
|
|
||||||
@ -318,104 +350,24 @@ By design:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 17. Special PostFX — Fade (Scene and HUD)
|
## 17. Transitions and Removed Fade Contract
|
||||||
|
|
||||||
PROMETEU supports **gradual fade** as a special PostFX, with two independent
|
The previous special fade model is not part of the canonical render contract.
|
||||||
controls:
|
Current render packets, syscalls, and ABI domains MUST NOT expose scene fade,
|
||||||
|
HUD fade, fade levels, fade colors, or equivalent inherited fade state.
|
||||||
|
|
||||||
- **Scene Fade**: affects the entire scene (Tile Layers 0–3 + Sprites)
|
Future visual transitions between Shell and Game belong to `RenderManager`.
|
||||||
- **HUD Fade**: affects only the HUD Layer (always composed last)
|
They are not owned by `composer.*`, `gfx2d.*`, `gfxui.*`, `Game2DFramePacket`,
|
||||||
|
or `ShellUiFramePacket`.
|
||||||
In v1, deferred `gfx.*` overlay/debug primitives are drained after both fades and therefore are not themselves part of scene or HUD fade application.
|
|
||||||
|
|
||||||
The fade uses a **discrete integer level** (0..31), which in practice produces an
|
|
||||||
"almost continuous" visual result in Prometeu's `480x270` pixel-art framebuffer.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 17.1 Fade Representation
|
|
||||||
|
|
||||||
Each fade is represented by:
|
|
||||||
|
|
||||||
- `fade_level: u8` in the range **[0..31]**
|
|
||||||
- `0` → fully replaced by the fade color
|
|
||||||
- `31` → fully visible (no fade)
|
|
||||||
- `fade_color: RGBA8888`
|
|
||||||
- color the image will be blended into
|
|
||||||
|
|
||||||
Registers:
|
|
||||||
|
|
||||||
- `SCENE_FADE_LEVEL` (0..31)
|
|
||||||
- `SCENE_FADE_COLOR` (RGBA8888)
|
|
||||||
- `HUD_FADE_LEVEL` (0..31)
|
|
||||||
- `HUD_FADE_COLOR` (RGBA8888)
|
|
||||||
|
|
||||||
Common cases:
|
|
||||||
|
|
||||||
- Fade-out: `fade_color = BLACK`
|
|
||||||
- Flash/teleport: `fade_color = WHITE`
|
|
||||||
- Special effects: any RGBA8888 color
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 17.2 Fade Operation (Blending with Arbitrary Color)
|
|
||||||
|
|
||||||
For each RGBA8888 pixel `src` and fade color `fc`, the final pixel `dst` is calculated per channel.
|
|
||||||
|
|
||||||
1) Extract components:
|
|
||||||
|
|
||||||
- `src_r8`, `src_g8`, `src_b8`, `src_a8`
|
|
||||||
- `fc_r8`, `fc_g8`, `fc_b8`, `fc_a8`
|
|
||||||
|
|
||||||
2) Apply integer blending:
|
|
||||||
|
|
||||||
```
|
|
||||||
src_weight = fade_level // 0..31
|
|
||||||
fc_weight = 31 - fade_level
|
|
||||||
|
|
||||||
r8 = (src_r8 * src_weight + fc_r8 * fc_weight) / 31
|
|
||||||
g8 = (src_g8 * src_weight + fc_g8 * fc_weight) / 31
|
|
||||||
b8 = (src_b8 * src_weight + fc_b8 * fc_weight) / 31
|
|
||||||
a8 = (src_a8 * src_weight + fc_a8 * fc_weight) / 31
|
|
||||||
```
|
|
||||||
- `src_r8`, `src_g8`, `src_b8`, `src_a8`
|
|
||||||
- `fc_r8`, `fc_g8`, `fc_b8`, `fc_a8`
|
|
||||||
|
|
||||||
3) Repack:
|
|
||||||
|
|
||||||
```
|
|
||||||
dst = pack_rgba8888_rgba(r8, g8, b8, a8)
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
|
|
||||||
- Deterministic operation
|
|
||||||
- Integers only
|
|
||||||
- Can be optimized via LUT
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 17.3 Order of Application in the Frame
|
|
||||||
|
|
||||||
The frame composition follows this order:
|
|
||||||
|
|
||||||
1. Rasterize **Tile Layers 0–3** → Back Buffer
|
|
||||||
2. Rasterize **Sprites** according to priority
|
|
||||||
3. (Optional) Extra pipeline (Emission/Light/Glow etc.)
|
|
||||||
4. Apply **Scene Fade** using:
|
|
||||||
- `SCENE_FADE_LEVEL`
|
|
||||||
- `SCENE_FADE_COLOR`
|
|
||||||
5. Rasterize **HUD Layer**
|
|
||||||
6. Apply **HUD Fade** using:
|
|
||||||
- `HUD_FADE_LEVEL`
|
|
||||||
- `HUD_FADE_COLOR`
|
|
||||||
7. `present()`
|
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- Scene Fade never affects the HUD
|
- render domain packets MUST NOT contain fade fields;
|
||||||
- HUD Fade never affects the scene
|
- public render syscalls MUST NOT expose fade controls;
|
||||||
|
- render implementations MUST NOT treat fade as an inherited post-processing
|
||||||
|
habit;
|
||||||
|
- transition work requires an explicit `RenderManager` contract or a later
|
||||||
|
decision.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -570,8 +522,13 @@ The system can measure:
|
|||||||
|
|
||||||
Graphics-related public ABI in v1 is split between:
|
Graphics-related public ABI in v1 is split between:
|
||||||
|
|
||||||
- `gfx.*` for direct drawing/backend-oriented operations;
|
- `composer.*` for Game 2D high-level frame composition;
|
||||||
- `composer.*` for frame orchestration operations.
|
- `gfx2d.*` for Game 2D primitives only;
|
||||||
|
- `gfxui.*` for Shell UI primitives only.
|
||||||
|
|
||||||
|
`composer.*` and `gfx2d.*` are available to `AppMode::Game`.
|
||||||
|
`gfxui.*` is available to `AppMode::Shell`. Renderers and capabilities may
|
||||||
|
differ by app mode.
|
||||||
|
|
||||||
Only operations with real operational rejection paths return explicit status values.
|
Only operations with real operational rejection paths return explicit status values.
|
||||||
|
|
||||||
@ -585,29 +542,32 @@ Fault boundary:
|
|||||||
|
|
||||||
| Syscall | Return | Policy basis |
|
| Syscall | Return | Policy basis |
|
||||||
| ----------------------- | ------------- | --------------------------------------------------- |
|
| ----------------------- | ------------- | --------------------------------------------------- |
|
||||||
| `gfx.clear` | `void` | no real operational failure path in v1 |
|
| `gfx2d.clear` | `void` | no real operational failure path in v1 |
|
||||||
| `gfx.fill_rect` | `void` | no real operational failure path in v1 |
|
| `gfx2d.fill_rect` | `void` | no real operational failure path in v1 |
|
||||||
| `gfx.draw_line` | `void` | no real operational failure path in v1 |
|
| `gfx2d.draw_line` | `void` | no real operational failure path in v1 |
|
||||||
| `gfx.draw_circle` | `void` | no real operational failure path in v1 |
|
| `gfx2d.draw_circle` | `void` | no real operational failure path in v1 |
|
||||||
| `gfx.draw_disc` | `void` | no real operational failure path in v1 |
|
| `gfx2d.draw_disc` | `void` | no real operational failure path in v1 |
|
||||||
| `gfx.draw_square` | `void` | no real operational failure path in v1 |
|
| `gfx2d.draw_square` | `void` | no real operational failure path in v1 |
|
||||||
| `gfx.draw_text` | `void` | no real operational failure path in v1 |
|
| `gfx2d.draw_text` | `void` | no real operational failure path in v1 |
|
||||||
|
| `gfxui.*` primitives | `void` | no real operational failure path in v1 |
|
||||||
| `composer.bind_scene` | `status:int` | status-returning API, but missing scene glyph dependencies are fatal runtime errors |
|
| `composer.bind_scene` | `status:int` | status-returning API, but missing scene glyph dependencies are fatal runtime errors |
|
||||||
| `composer.unbind_scene` | `status:int` | explicit orchestration-domain operational result |
|
| `composer.unbind_scene` | `status:int` | explicit orchestration-domain operational result |
|
||||||
| `composer.set_camera` | `void` | no real operational failure path in v1 |
|
| `composer.set_camera` | `void` | no real operational failure path in v1 |
|
||||||
| `composer.emit_sprite` | `status:int` | explicit orchestration-domain operational rejection |
|
| `composer.emit_sprite` | `status:int` | explicit orchestration-domain operational rejection |
|
||||||
|
|
||||||
### 19.1.a Deferred overlay/debug semantics for `gfx.*`
|
### 19.1.a Primitive domain semantics
|
||||||
|
|
||||||
The public `gfx.*` primitive family remains valid in v1, but its stable operational meaning is:
|
The primitive domains have stable operational meaning:
|
||||||
|
|
||||||
- deferred final overlay/debug composition;
|
- `gfx2d.*` is Game 2D primitive command buffering;
|
||||||
- screen-space and pipeline-agnostic relative to `composer.*`;
|
- `gfx2d.*` is screen-space and primitive-only relative to `composer.*`;
|
||||||
- outside `FrameComposer`;
|
- `gfx2d.*` is outside scene, camera, sprites, HUD, and Game 2D frame orchestration;
|
||||||
- above scene, sprites, and canonical HUD;
|
- `gfxui.*` is Shell UI primitive command buffering;
|
||||||
- drained after `hud_fade`.
|
- `gfxui.*` does not contain widget or layout policy.
|
||||||
|
|
||||||
This means callers MUST NOT rely on stable immediate writes to the working back buffer as the public contract for `gfx.draw_text(...)` or sibling primitives.
|
Callers MUST NOT rely on stable immediate writes to the working back buffer as
|
||||||
|
the public contract for primitive drawing. Primitive calls mutate domain command
|
||||||
|
buffers that close into the active typed submission.
|
||||||
|
|
||||||
### 19.1.b Scene dependency fatal boundary
|
### 19.1.b Scene dependency fatal boundary
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@ It covers:
|
|||||||
- profiling;
|
- profiling;
|
||||||
- event and fault visibility;
|
- event and fault visibility;
|
||||||
- certification input data;
|
- certification input data;
|
||||||
- host-side debug overlay (HUD) isolation.
|
- host-side debug UI isolation.
|
||||||
|
|
||||||
## 2 Execution Modes
|
## 2 Execution Modes
|
||||||
|
|
||||||
@ -131,15 +131,15 @@ The host may:
|
|||||||
- observe buffers separately
|
- observe buffers separately
|
||||||
- identify excessive redraw
|
- 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 preserve the last valid visible image;
|
||||||
- the host MUST NOT keep recomposing the surface by continuous polling alone;
|
- 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)
|
## 6 Time Profiling (Cycles)
|
||||||
|
|
||||||
@ -285,28 +285,32 @@ Each event has:
|
|||||||
- cost
|
- cost
|
||||||
- consequence
|
- 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
|
### 10.1 Responsibilities
|
||||||
|
|
||||||
1. **Runtime:** Only exposes telemetry data via the machine diagnostics surface. It does not perform HUD rendering or string formatting.
|
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.
|
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.
|
3. **Host diagnostics:** The Desktop Host may consume telemetry for logs,
|
||||||
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.
|
debugger streams, and non-render diagnostics, but it does not compose debug
|
||||||
5. **Host control:** Overlay visibility and presentation policy remain under Host control.
|
pixels into the presentation surface.
|
||||||
6. **Host (Desktop):** Responsible for collecting telemetry from the runtime and rendering the HUD as a native, transparent layer.
|
4. **Composition boundary:** Published Game and Shell output must be presented
|
||||||
|
without host-owned debug overlay injection.
|
||||||
|
|
||||||
### 10.2 Principles
|
### 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 Pipeline Interference:** Host diagnostics must not inject pixels into
|
||||||
- **Zero Cycle Impact:** HUD-related processing (like formatting technical text) must occur outside the emulated machine cycles.
|
the emulated framebuffer or the host presentation surface.
|
||||||
- **Toggle Control:** The activation of the overlay (typically via **F1**) is managed by the Host layer.
|
- **Zero Cycle Impact:** Diagnostic processing must occur outside the emulated
|
||||||
|
machine cycles.
|
||||||
|
|
||||||
### 10.3 Atomic Telemetry Model
|
### 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.).
|
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.
|
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;
|
- feed the host-owned certification report;
|
||||||
- are collected deterministically;
|
- are collected deterministically;
|
||||||
- do not require a guest-visible debug ABI;
|
- 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
|
- audio output
|
||||||
- physical input collection
|
- physical input collection
|
||||||
- access to the sandbox file system
|
- 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.
|
The host provides realization of machine surfaces. It does not redefine cartridge semantics.
|
||||||
|
|
||||||
@ -116,37 +117,39 @@ Hardware differences:
|
|||||||
|
|
||||||
The graphics system:
|
The graphics system:
|
||||||
|
|
||||||
- operates on a logical RGBA8888 framebuffer
|
- produces typed logical render submissions that render to RGBA8888 output
|
||||||
- uses an indexed palette
|
- uses an indexed palette
|
||||||
- does not depend on a specific GPU
|
- does not depend on a specific GPU
|
||||||
|
|
||||||
The platform layer:
|
The platform layer:
|
||||||
|
|
||||||
- only displays the framebuffer
|
- consumes closed render submissions through a render-surface implementation
|
||||||
- does not reinterpret graphics commands
|
- transports published RGBA8888 output into a host presentation surface without
|
||||||
- **may overlay technical HUDs without modifying the logical framebuffer**
|
injecting host-owned debug overlay pixels
|
||||||
- may transport the logical framebuffer into a host presentation surface where a host-only overlay layer is composed
|
|
||||||
|
|
||||||
The host presentation layer MUST treat RGBA8888 as the canonical logical
|
The host presentation layer MUST treat RGBA8888 as the canonical logical
|
||||||
framebuffer format. RGB565 conversion is not part of the normal host
|
framebuffer format. RGB565 conversion is not part of the normal host
|
||||||
presentation contract.
|
presentation contract.
|
||||||
|
|
||||||
Host presentation SHOULD be driven by published logical frames and explicit host-owned invalidation, not by perpetual redraw polling.
|
Host presentation SHOULD be driven by published render submissions and explicit
|
||||||
|
host-owned invalidation, not by perpetual redraw polling.
|
||||||
|
|
||||||
In particular:
|
In particular:
|
||||||
|
|
||||||
- a stable logical frame MAY remain visible across multiple host wakeups without recomposition;
|
- 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.
|
- 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
|
## 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.
|
- **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.
|
- **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.
|
- **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
|
### 9.1 Atomic Telemetry Interface
|
||||||
|
|
||||||
|
|||||||
@ -39,7 +39,7 @@ Syscalls are identified by a canonical triple:
|
|||||||
Example:
|
Example:
|
||||||
|
|
||||||
```
|
```
|
||||||
("gfx", "present", 1)
|
("gfx2d", "draw_line", 1)
|
||||||
("audio", "play", 2)
|
("audio", "play", 2)
|
||||||
("composer", "emit_sprite", 1)
|
("composer", "emit_sprite", 1)
|
||||||
```
|
```
|
||||||
@ -220,29 +220,78 @@ For `asset.load`:
|
|||||||
- JSON-on-the-wire bank inspection payloads are not valid public ABI;
|
- JSON-on-the-wire bank inspection payloads are not valid public ABI;
|
||||||
- `bank.info` returns stack values, not textual structured payloads.
|
- `bank.info` returns stack values, not textual structured payloads.
|
||||||
|
|
||||||
### GFX surface (`gfx`, v1)
|
### Render surfaces (`composer`, `gfx2d`, `gfxui`, v1)
|
||||||
|
|
||||||
The public GFX ABI uses format-neutral operation names. API and syscall names
|
`DEC-0030` splits logical render production from render implementation. Public
|
||||||
MUST NOT encode RGB565 or any other pixel format suffix.
|
render syscalls mutate app-mode-specific domain buffers. They do not directly
|
||||||
|
publish a framebuffer and do not call host presentation APIs.
|
||||||
|
|
||||||
|
The canonical flow is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
domain buffers during logical frame
|
||||||
|
-> RenderManager closes buffers
|
||||||
|
-> RenderSubmission snapshot
|
||||||
|
-> render surface / implementation consumes submission
|
||||||
|
-> RGBA8888 surface publication
|
||||||
|
```
|
||||||
|
|
||||||
|
`RenderSubmission` is a typed closed snapshot containing `frame_id`, `app_mode`,
|
||||||
|
and exactly one packet: `Game2DFramePacket` for `AppMode::Game` or
|
||||||
|
`ShellUiFramePacket` for `AppMode::Shell`. Producers may mutate domain buffers
|
||||||
|
before closure only. Once `RenderManager` closes a submission, producers MUST
|
||||||
|
NOT mutate it. Backpressure is latest-complete-submission-wins; the runtime
|
||||||
|
MUST NOT expose an unbounded submission queue as part of the ABI.
|
||||||
|
|
||||||
|
Render API and syscall names MUST NOT encode RGB565 or any other pixel format
|
||||||
|
suffix.
|
||||||
|
|
||||||
|
#### Game 2D primitive surface (`gfx2d`, v1)
|
||||||
|
|
||||||
|
The `gfx2d` module is a Game profile primitive surface. It is limited to Game
|
||||||
|
2D primitive commands and does not own scene, camera, sprites, HUD, or frame
|
||||||
|
orchestration.
|
||||||
|
|
||||||
Canonical operations in v1 include:
|
Canonical operations in v1 include:
|
||||||
|
|
||||||
- `gfx.clear(color) -> void`
|
- `gfx2d.clear(color) -> void`
|
||||||
- `gfx.fill_rect(x, y, w, h, color) -> void`
|
- `gfx2d.fill_rect(x, y, w, h, color) -> void`
|
||||||
- `gfx.draw_line(x0, y0, x1, y1, color) -> void`
|
- `gfx2d.draw_line(x0, y0, x1, y1, color) -> void`
|
||||||
- `gfx.draw_circle(x, y, radius, color) -> void`
|
- `gfx2d.draw_circle(x, y, radius, color) -> void`
|
||||||
- `gfx.draw_disc(x, y, radius, color) -> void`
|
- `gfx2d.draw_disc(x, y, radius, color) -> void`
|
||||||
- `gfx.draw_square(x, y, size, color) -> void`
|
- `gfx2d.draw_square(x, y, size, color) -> void`
|
||||||
- `gfx.draw_text(x, y, text, color) -> void`
|
- `gfx2d.draw_text(x, y, text, color) -> void`
|
||||||
|
|
||||||
|
#### Shell UI primitive surface (`gfxui`, v1)
|
||||||
|
|
||||||
|
The `gfxui` module is a Shell profile primitive surface. It contains Shell UI
|
||||||
|
primitive commands only. Widget and layout policy belongs in Shell/UI code or
|
||||||
|
stdlib code, not in the host/render-surface implementation.
|
||||||
|
|
||||||
|
Canonical operations in v1 include primitive shapes equivalent to the subset
|
||||||
|
needed by Shell UI:
|
||||||
|
|
||||||
|
- `gfxui.clear(color) -> void`
|
||||||
|
- `gfxui.fill_rect(x, y, w, h, color) -> void`
|
||||||
|
- `gfxui.draw_line(x0, y0, x1, y1, color) -> void`
|
||||||
|
- `gfxui.draw_circle(x, y, radius, color) -> void`
|
||||||
|
- `gfxui.draw_disc(x, y, radius, color) -> void`
|
||||||
|
- `gfxui.draw_square(x, y, size, color) -> void`
|
||||||
|
- `gfxui.draw_text(x, y, text, color) -> void`
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- `color` arguments are packed RGBA8888 values using RGBA channel order.
|
- `color` arguments are packed RGBA8888 values using RGBA channel order.
|
||||||
- RGB565 raw values are not a public ABI contract.
|
- RGB565 raw values are not a public ABI contract.
|
||||||
- `Gfx*565` syscall names and `gfx.*_565` canonical identities are not valid
|
- `Gfx*565` syscall names and `gfx*.*_565` canonical identities are not valid
|
||||||
current ABI.
|
current ABI.
|
||||||
- The runtime framebuffer exposed across the host boundary is RGBA8888, not
|
- The runtime framebuffer exposed across the host boundary is RGBA8888, not
|
||||||
`u16` RGB565.
|
`u16` RGB565.
|
||||||
|
- The old primitive `gfx.*` ABI is not the canonical render domain. PBS stdlibs
|
||||||
|
may expose ergonomic high-level names, but generated host bindings must map
|
||||||
|
to `gfx2d.*` in Game mode or `gfxui.*` in Shell mode.
|
||||||
|
- Render packets and public render syscalls MUST NOT expose fade fields or fade
|
||||||
|
controls.
|
||||||
|
|
||||||
### Composition surface (`composer`, v1)
|
### Composition surface (`composer`, v1)
|
||||||
|
|
||||||
@ -251,6 +300,11 @@ The canonical frame-orchestration public ABI uses module `composer`.
|
|||||||
The `composer` module is a game profile surface. It is not inherited by
|
The `composer` module is a game profile surface. It is not inherited by
|
||||||
`System` profile apps as their public UI/composition API.
|
`System` profile apps as their public UI/composition API.
|
||||||
|
|
||||||
|
`composer` owns Game 2D high-level frame composition: scene binding, camera,
|
||||||
|
sprites, HUD, and Game 2D frame orchestration. It closes into a
|
||||||
|
`Game2DFramePacket` through `RenderManager`; it does not publish or present a
|
||||||
|
framebuffer directly.
|
||||||
|
|
||||||
Canonical operations in v1 are:
|
Canonical operations in v1 are:
|
||||||
|
|
||||||
- `composer.bind_scene(bank_id) -> (status)`
|
- `composer.bind_scene(bank_id) -> (status)`
|
||||||
@ -264,6 +318,8 @@ For mutating composer operations:
|
|||||||
- `bind_scene`, `unbind_scene`, and `emit_sprite` are status-returning;
|
- `bind_scene`, `unbind_scene`, and `emit_sprite` are status-returning;
|
||||||
- `set_camera` remains `void` in v1;
|
- `set_camera` remains `void` in v1;
|
||||||
- no caller-provided sprite index or `active` flag is part of the canonical contract.
|
- no caller-provided sprite index or `active` flag is part of the canonical contract.
|
||||||
|
- no fade level, fade color, or transition control is part of the canonical
|
||||||
|
composer ABI.
|
||||||
|
|
||||||
Fatal boundary clarification for `bind_scene`:
|
Fatal boundary clarification for `bind_scene`:
|
||||||
|
|
||||||
|
|||||||
@ -46,7 +46,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"host-dependent": {
|
"host-dependent": {
|
||||||
"baseline_percent": 43,
|
"baseline_percent": 35,
|
||||||
"paths": [
|
"paths": [
|
||||||
"crates/host/prometeu-host-desktop-winit/src/"
|
"crates/host/prometeu-host-desktop-winit/src/"
|
||||||
]
|
]
|
||||||
|
|||||||
@ -5,5 +5,5 @@
|
|||||||
"title": "Stress Console",
|
"title": "Stress Console",
|
||||||
"app_version": "0.1.0",
|
"app_version": "0.1.0",
|
||||||
"app_mode": "Game",
|
"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