Compare commits
No commits in common. "5ba47eb20adaa9b88e1677f24ab524c469a1820d" and "105a6de11e661386604fc1a2873ef14bd55568e0" have entirely different histories.
5ba47eb20a
...
105a6de11e
@ -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: "gfx2d".into(),
|
module: "gfx".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 gfx2d.draw_line (0x1003)
|
// SYSCALL gfx.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,5 +1,6 @@
|
|||||||
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;
|
||||||
@ -7,10 +8,6 @@ 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 {
|
||||||
@ -135,28 +132,6 @@ 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,
|
||||||
@ -271,35 +246,6 @@ 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);
|
||||||
@ -804,6 +750,8 @@ 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();
|
||||||
@ -811,73 +759,6 @@ 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());
|
||||||
@ -893,6 +774,8 @@ 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();
|
||||||
@ -928,6 +811,8 @@ 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);
|
||||||
@ -959,6 +844,8 @@ 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);
|
||||||
@ -1036,6 +923,8 @@ 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 primitive packet consumption.
|
/// sprite composition, and fades. Public `gfx.*` primitives remain valid.
|
||||||
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,6 +49,15 @@ 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.
|
||||||
@ -296,6 +305,31 @@ 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 {
|
||||||
@ -324,6 +358,10 @@ 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),
|
||||||
@ -599,13 +637,20 @@ 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.
|
/// plus sprite state and fade controls.
|
||||||
pub fn render_scene_from_cache(
|
pub fn render_scene_from_cache(
|
||||||
&mut self,
|
&mut self,
|
||||||
cache: &SceneViewportCache,
|
cache: &SceneViewportCache,
|
||||||
@ -636,88 +681,9 @@ impl Gfx {
|
|||||||
&*self.glyph_banks,
|
&*self.glyph_banks,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_game2d_frame_packet(&mut self, packet: &Game2DFramePacket) {
|
Self::apply_fade_to_buffer(&mut self.back, self.scene_fade_level, self.scene_fade_color);
|
||||||
let sprites: Vec<Sprite> = packet
|
Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color);
|
||||||
.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) {
|
||||||
@ -868,6 +834,30 @@ 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;
|
||||||
@ -1240,6 +1230,8 @@ 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());
|
||||||
@ -1270,6 +1262,8 @@ 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 },
|
||||||
@ -1337,22 +1331,4 @@ 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,7 +11,6 @@ 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.
|
||||||
@ -68,34 +67,9 @@ impl HardwareBridge for Hardware {
|
|||||||
self.frame_composer.emit_sprite(sprite)
|
self.frame_composer.emit_sprite(sprite)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn close_game2d_packet(&self) -> Game2DFramePacket {
|
|
||||||
self.frame_composer.close_game2d_packet()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn publish_render_submission(&mut self, submission: &RenderSubmission) {
|
|
||||||
match &submission.packet {
|
|
||||||
RenderSubmissionPacket::Game2D(packet) => {
|
|
||||||
if self.frame_composer.active_scene_id().is_none() {
|
|
||||||
self.gfx.render_game2d_frame_packet(packet);
|
|
||||||
} else {
|
|
||||||
self.frame_composer.render_frame(&mut self.gfx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RenderSubmissionPacket::ShellUi(packet) => {
|
|
||||||
self.gfx.render_shell_ui_frame_packet(packet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.gfx.present();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_frame(&mut self) {
|
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);
|
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()
|
||||||
@ -241,6 +215,8 @@ 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::{ComposerBuffer, FrameComposer, SceneStatus, SpriteController};
|
pub use crate::frame_composer::{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: "gfx2d".into(),
|
module: "gfx".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
|
|||||||
@ -2,7 +2,6 @@ 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)]
|
||||||
@ -23,8 +22,11 @@ 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);
|
||||||
|
|
||||||
let packet = ShellUiFramePacket::new(vec![GfxUiCommand::Clear { color: Color::RED }]);
|
// Error screen: red background, white text
|
||||||
ctx.hw.publish_render_submission(&RenderSubmission::shell_ui(FrameId::ZERO, packet));
|
ctx.hw.gfx_mut().clear(Color::RED);
|
||||||
|
// For now we just log or show something simple
|
||||||
|
// In the future, use draw_text
|
||||||
|
ctx.hw.gfx_mut().present();
|
||||||
|
|
||||||
// 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,6 +38,10 @@ 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,8 +2,6 @@ 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 {
|
||||||
@ -24,6 +22,9 @@ 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);
|
||||||
@ -37,14 +38,8 @@ 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;
|
||||||
|
|
||||||
let packet = ShellUiFramePacket::new(vec![
|
ctx.hw.gfx_mut().fill_rect(x, y, current_size, current_size, Color::WHITE);
|
||||||
GfxUiCommand::Clear { color: Color::ORANGE },
|
ctx.hw.gfx_mut().present();
|
||||||
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,9 +286,6 @@ pub enum Capability {
|
|||||||
None,
|
None,
|
||||||
System,
|
System,
|
||||||
Gfx,
|
Gfx,
|
||||||
Composer,
|
|
||||||
Gfx2d,
|
|
||||||
Gfxui,
|
|
||||||
Audio,
|
Audio,
|
||||||
Fs,
|
Fs,
|
||||||
Log,
|
Log,
|
||||||
|
|||||||
@ -117,9 +117,6 @@ 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,4 +73,13 @@ 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,7 +2,6 @@ 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;
|
||||||
|
|
||||||
@ -12,8 +11,6 @@ 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;
|
||||||
|
|
||||||
|
|||||||
@ -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**: Game 2D primitives (`gfx2d`) and Shell UI primitives (`gfxui`)
|
/// - **0x1xxx**: Graphics (GFX)
|
||||||
/// - **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,13 +37,6 @@ 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,13 +2,10 @@ 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 COMPOSER: CapFlags = 1 << 1;
|
pub const GFX: 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 GFX2D: CapFlags = 1 << 7;
|
pub const ALL: CapFlags = SYSTEM | GFX | AUDIO | FS | LOG | ASSET | BANK;
|
||||||
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::COMPOSER)
|
.caps(caps::GFX)
|
||||||
.cost(5),
|
.cost(5),
|
||||||
SyscallRegistryEntry::builder(Syscall::ComposerUnbindScene, "composer", "unbind_scene")
|
SyscallRegistryEntry::builder(Syscall::ComposerUnbindScene, "composer", "unbind_scene")
|
||||||
.rets(1)
|
.rets(1)
|
||||||
.caps(caps::COMPOSER)
|
.caps(caps::GFX)
|
||||||
.cost(2),
|
.cost(2),
|
||||||
SyscallRegistryEntry::builder(Syscall::ComposerSetCamera, "composer", "set_camera")
|
SyscallRegistryEntry::builder(Syscall::ComposerSetCamera, "composer", "set_camera")
|
||||||
.args(2)
|
.args(2)
|
||||||
.caps(caps::COMPOSER)
|
.caps(caps::GFX)
|
||||||
.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::COMPOSER)
|
.caps(caps::GFX)
|
||||||
.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, "gfx2d", "clear")
|
SyscallRegistryEntry::builder(Syscall::GfxClear, "gfx", "clear")
|
||||||
.args(1)
|
.args(1)
|
||||||
.caps(caps::GFX2D)
|
.caps(caps::GFX)
|
||||||
.cost(20),
|
.cost(20),
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxFillRect, "gfx2d", "fill_rect")
|
SyscallRegistryEntry::builder(Syscall::GfxFillRect, "gfx", "fill_rect")
|
||||||
.args(5)
|
.args(5)
|
||||||
.caps(caps::GFX2D)
|
.caps(caps::GFX)
|
||||||
.cost(20),
|
.cost(20),
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxDrawLine, "gfx2d", "draw_line")
|
SyscallRegistryEntry::builder(Syscall::GfxDrawLine, "gfx", "draw_line")
|
||||||
.args(5)
|
.args(5)
|
||||||
.caps(caps::GFX2D)
|
.caps(caps::GFX)
|
||||||
.cost(5),
|
.cost(5),
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxDrawCircle, "gfx2d", "draw_circle")
|
SyscallRegistryEntry::builder(Syscall::GfxDrawCircle, "gfx", "draw_circle")
|
||||||
.args(4)
|
.args(4)
|
||||||
.caps(caps::GFX2D)
|
.caps(caps::GFX)
|
||||||
.cost(5),
|
.cost(5),
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxDrawDisc, "gfx2d", "draw_disc")
|
SyscallRegistryEntry::builder(Syscall::GfxDrawDisc, "gfx", "draw_disc")
|
||||||
.args(5)
|
.args(5)
|
||||||
.caps(caps::GFX2D)
|
.caps(caps::GFX)
|
||||||
.cost(5),
|
.cost(5),
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxDrawSquare, "gfx2d", "draw_square")
|
SyscallRegistryEntry::builder(Syscall::GfxDrawSquare, "gfx", "draw_square")
|
||||||
.args(6)
|
.args(6)
|
||||||
.caps(caps::GFX2D)
|
.caps(caps::GFX)
|
||||||
.cost(5),
|
.cost(5),
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxDrawText, "gfx2d", "draw_text")
|
SyscallRegistryEntry::builder(Syscall::GfxDrawText, "gfx", "draw_text")
|
||||||
.args(4)
|
.args(4)
|
||||||
.caps(caps::GFX2D)
|
.caps(caps::GFX)
|
||||||
.cost(20),
|
.cost(20),
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,32 +0,0 @@
|
|||||||
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,7 +4,6 @@ mod bank;
|
|||||||
mod composer;
|
mod composer;
|
||||||
mod fs;
|
mod fs;
|
||||||
mod gfx;
|
mod gfx;
|
||||||
mod gfxui;
|
|
||||||
mod log;
|
mod log;
|
||||||
mod system;
|
mod system;
|
||||||
|
|
||||||
@ -14,7 +13,6 @@ 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,13 +21,6 @@ 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),
|
||||||
@ -77,13 +70,6 @@ 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("gfx2d", "clear", 1).expect("known identity must resolve");
|
let id = resolve_syscall("gfx", "clear", 1).expect("known identity must resolve");
|
||||||
assert_eq!(id.id, 0x1001);
|
assert_eq!(id.id, 0x1001);
|
||||||
assert_eq!(id.meta.module, "gfx2d");
|
assert_eq!(id.meta.module, "gfx");
|
||||||
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("gfx2d", "nonexistent", 1);
|
let res = resolve_syscall("gfx", "nonexistent", 1);
|
||||||
assert!(res.is_none());
|
assert!(res.is_none());
|
||||||
|
|
||||||
let requested = [SyscallIdentity { module: "gfx2d", name: "nonexistent", version: 1 }];
|
let requested = [SyscallIdentity { module: "gfx", 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, "gfx2d");
|
assert_eq!(module, "gfx");
|
||||||
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("gfx2d", "set_sprite", 1).is_none());
|
assert!(resolve_syscall("gfx", "set_sprite", 1).is_none());
|
||||||
|
|
||||||
let requested = [SyscallIdentity { module: "gfx2d", name: "set_sprite", version: 1 }];
|
let requested = [SyscallIdentity { module: "gfx", 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: "gfx2d".into(), name: "set_sprite".into(), version: 1 }
|
LoadError::UnknownSyscall { module: "gfx".into(), name: "set_sprite".into(), version: 1 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolver_enforces_capabilities() {
|
fn resolver_enforces_capabilities() {
|
||||||
let requested = [SyscallIdentity { module: "gfx2d", name: "clear", version: 1 }];
|
let requested = [SyscallIdentity { module: "gfx", 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, "gfx2d");
|
assert_eq!(module, "gfx");
|
||||||
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,30 +90,15 @@ fn resolver_enforces_capabilities() {
|
|||||||
_ => panic!("expected MissingCapability error"),
|
_ => panic!("expected MissingCapability error"),
|
||||||
}
|
}
|
||||||
|
|
||||||
let ok = resolve_program_syscalls(&requested, caps::GFX2D).expect("must resolve with caps");
|
let ok = resolve_program_syscalls(&requested, caps::GFX).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: "gfx2d".into(),
|
module: "gfx".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
@ -121,7 +106,7 @@ fn declared_resolver_returns_expected_id_for_known_identity() {
|
|||||||
}];
|
}];
|
||||||
|
|
||||||
let ok =
|
let ok =
|
||||||
resolve_declared_program_syscalls(&declared, caps::GFX2D).expect("must resolve with ABI");
|
resolve_declared_program_syscalls(&declared, caps::GFX).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);
|
||||||
}
|
}
|
||||||
@ -129,18 +114,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: "gfx2d".into(),
|
module: "gfx".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::GFX2D).unwrap_err();
|
let err = resolve_declared_program_syscalls(&declared, caps::GFX).unwrap_err();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
err,
|
err,
|
||||||
DeclaredLoadError::UnknownSyscall {
|
DeclaredLoadError::UnknownSyscall {
|
||||||
module: "gfx2d".into(),
|
module: "gfx".into(),
|
||||||
name: "nonexistent".into(),
|
name: "nonexistent".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
}
|
}
|
||||||
@ -150,7 +135,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: "gfx2d".into(),
|
module: "gfx".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
@ -161,9 +146,9 @@ fn declared_resolver_rejects_missing_capability() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
err,
|
err,
|
||||||
DeclaredLoadError::MissingCapability {
|
DeclaredLoadError::MissingCapability {
|
||||||
required: caps::GFX2D,
|
required: caps::GFX,
|
||||||
provided: caps::NONE,
|
provided: caps::NONE,
|
||||||
module: "gfx2d".into(),
|
module: "gfx".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
}
|
}
|
||||||
@ -173,18 +158,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: "gfx2d".into(),
|
module: "gfx".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::GFX2D).unwrap_err();
|
let err = resolve_declared_program_syscalls(&declared, caps::GFX).unwrap_err();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
err,
|
err,
|
||||||
DeclaredLoadError::AbiMismatch {
|
DeclaredLoadError::AbiMismatch {
|
||||||
module: "gfx2d".into(),
|
module: "gfx".into(),
|
||||||
name: "draw_line".into(),
|
name: "draw_line".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
declared_arg_slots: 4,
|
declared_arg_slots: 4,
|
||||||
@ -401,7 +386,7 @@ fn declared_resolver_accepts_mixed_status_first_surface_as_a_single_module() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let resolved =
|
let resolved =
|
||||||
resolve_declared_program_syscalls(&declared, caps::COMPOSER | caps::AUDIO | caps::ASSET)
|
resolve_declared_program_syscalls(&declared, caps::GFX | 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,9 +1,8 @@
|
|||||||
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::{
|
use prometeu_hal::{HardwareBridge, InputSignals};
|
||||||
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 };
|
||||||
@ -102,6 +101,8 @@ 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 {
|
||||||
@ -126,13 +127,14 @@ 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();
|
||||||
|
|
||||||
let packet = if os.windows().window_count() == 0 {
|
if os.windows().window_count() == 0 {
|
||||||
render_home_packet(pointer_x, pointer_y)
|
render_home(hw.gfx_mut(), pointer_x, pointer_y);
|
||||||
} else {
|
return;
|
||||||
let mut commands = Vec::new();
|
}
|
||||||
|
|
||||||
for window in os.windows().windows() {
|
for window in os.windows().windows() {
|
||||||
append_shell_window_commands(
|
render_shell_window(
|
||||||
&mut commands,
|
hw.gfx_mut(),
|
||||||
window.viewport,
|
window.viewport,
|
||||||
window.color,
|
window.color,
|
||||||
&window.title,
|
&window.title,
|
||||||
@ -140,9 +142,6 @@ 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(
|
||||||
@ -164,6 +163,7 @@ impl PrometeuHub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.render(os, hw);
|
self.render(os, hw);
|
||||||
|
hw.gfx_mut().present();
|
||||||
|
|
||||||
SystemProfileUpdate { crash, action }
|
SystemProfileUpdate { crash, action }
|
||||||
}
|
}
|
||||||
@ -192,29 +192,15 @@ 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_packet(pointer_x: i32, pointer_y: i32) -> ShellUiFramePacket {
|
fn render_home(gfx: &mut dyn GfxBridge, pointer_x: i32, pointer_y: i32) {
|
||||||
let mut commands = Vec::new();
|
gfx.fill_rect(VIEWPORT.x, VIEWPORT.y, VIEWPORT.w, VIEWPORT.h, COLOR_BG);
|
||||||
commands.push(GfxUiCommand::Clear { color: COLOR_BG });
|
draw_panel(gfx, TITLE_PANEL, COLOR_PANEL_DARK, COLOR_BORDER);
|
||||||
fill_rect(&mut commands, VIEWPORT, COLOR_BG);
|
draw_buffer_text(gfx, TITLE_PANEL.x + 58, TITLE_PANEL.y + 9, "PROMETEU HUB", COLOR_TEXT);
|
||||||
draw_panel(&mut commands, TITLE_PANEL, COLOR_PANEL_DARK, COLOR_BORDER);
|
|
||||||
draw_buffer_text(
|
|
||||||
&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_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(
|
||||||
&mut commands,
|
gfx,
|
||||||
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",
|
||||||
@ -223,105 +209,50 @@ fn render_home_packet(pointer_x: i32, pointer_y: i32) -> ShellUiFramePacket {
|
|||||||
|
|
||||||
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(&mut commands, button.rect, button.label, hovered);
|
draw_button(gfx, button.rect, button.label, hovered);
|
||||||
}
|
}
|
||||||
|
|
||||||
ShellUiFramePacket::new(commands)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn append_shell_window_commands(
|
fn render_shell_window(
|
||||||
commands: &mut Vec<GfxUiCommand>,
|
gfx: &mut dyn GfxBridge,
|
||||||
viewport: Rect,
|
viewport: Rect,
|
||||||
color: Color,
|
color: Color,
|
||||||
title: &str,
|
title: &str,
|
||||||
pointer_x: i32,
|
pointer_x: i32,
|
||||||
pointer_y: i32,
|
pointer_y: i32,
|
||||||
) {
|
) {
|
||||||
fill_rect(commands, VIEWPORT, COLOR_BG);
|
gfx.fill_rect(VIEWPORT.x, VIEWPORT.y, VIEWPORT.w, VIEWPORT.h, COLOR_BG);
|
||||||
draw_panel(commands, viewport, COLOR_PANEL_DARK, COLOR_BORDER);
|
draw_panel(gfx, viewport, COLOR_PANEL_DARK, COLOR_BORDER);
|
||||||
fill_rect(
|
gfx.fill_rect(viewport.x + 4, viewport.y + 24, viewport.w - 8, viewport.h - 28, color);
|
||||||
commands,
|
draw_buffer_text(gfx, viewport.x + 10, viewport.y + 8, title, COLOR_TEXT);
|
||||||
Rect { x: viewport.x + 4, y: viewport.y + 24, w: viewport.w - 8, h: viewport.h - 28 },
|
draw_buffer_text(gfx, viewport.x + 18, viewport.y + 44, "native fake shell", COLOR_PANEL_DARK);
|
||||||
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 };
|
||||||
fill_rect(commands, CLOSE_BUTTON, close_fill);
|
gfx.fill_rect(CLOSE_BUTTON.x, CLOSE_BUTTON.y, CLOSE_BUTTON.w, CLOSE_BUTTON.h, close_fill);
|
||||||
draw_rect(commands, CLOSE_BUTTON, COLOR_BORDER);
|
gfx.draw_rect(CLOSE_BUTTON.x, CLOSE_BUTTON.y, CLOSE_BUTTON.w, CLOSE_BUTTON.h, COLOR_BORDER);
|
||||||
draw_buffer_text(commands, CLOSE_BUTTON.x + 5, CLOSE_BUTTON.y + 4, "x", COLOR_TEXT);
|
draw_buffer_text(gfx, CLOSE_BUTTON.x + 5, CLOSE_BUTTON.y + 4, "x", COLOR_TEXT);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fill_rect(commands: &mut Vec<GfxUiCommand>, rect: Rect, color: Color) {
|
fn draw_panel(gfx: &mut dyn GfxBridge, rect: Rect, fill: Color, border: Color) {
|
||||||
commands.push(GfxUiCommand::FillRect { rect, color });
|
gfx.fill_rect(rect.x, rect.y, rect.w, rect.h, fill);
|
||||||
|
gfx.draw_rect(rect.x, rect.y, rect.w, rect.h, border);
|
||||||
|
gfx.draw_rect(rect.x + 2, rect.y + 2, rect.w - 4, rect.h - 4, COLOR_MUTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_rect(commands: &mut Vec<GfxUiCommand>, rect: Rect, color: Color) {
|
fn draw_button(gfx: &mut dyn GfxBridge, rect: Rect, label: &str, hovered: bool) {
|
||||||
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 };
|
||||||
fill_rect(commands, rect, fill);
|
gfx.fill_rect(rect.x, rect.y, rect.w, rect.h, fill);
|
||||||
draw_rect(commands, rect, border);
|
gfx.draw_rect(rect.x, rect.y, rect.w, rect.h, border);
|
||||||
draw_rect(
|
gfx.draw_rect(rect.x + 3, rect.y + 3, rect.w - 6, rect.h - 6, COLOR_MUTED);
|
||||||
commands,
|
gfx.fill_rect(rect.x + 12, rect.y + 12, 16, 24, border);
|
||||||
Rect { x: rect.x + 3, y: rect.y + 3, w: rect.w - 6, h: rect.h - 6 },
|
gfx.draw_rect(rect.x + 34, rect.y + 12, 62, 24, COLOR_PANEL_DARK);
|
||||||
COLOR_MUTED,
|
draw_buffer_text(gfx, rect.x + 40, rect.y + 21, label, COLOR_TEXT);
|
||||||
);
|
|
||||||
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(commands: &mut Vec<GfxUiCommand>, x: i32, y: i32, text: &str, color: Color) {
|
fn draw_buffer_text(gfx: &mut dyn GfxBridge, x: i32, y: i32, text: &str, color: Color) {
|
||||||
commands.push(GfxUiCommand::DrawText { x, y, text: text.to_string(), color });
|
gfx.draw_text(x, y, text, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -390,16 +321,6 @@ 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,13 +7,12 @@ 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, Gfx2dCommand, GfxUiCommand, HostContext, HostReturn,
|
AudioOpStatus, ComposerOpStatus, HostContext, HostReturn, NativeInterface, SyscallId,
|
||||||
NativeInterface, SyscallId, expect_bool, expect_int,
|
expect_bool, expect_int,
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
@ -186,19 +185,6 @@ 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(())
|
||||||
}
|
}
|
||||||
@ -208,25 +194,6 @@ 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(())
|
||||||
}
|
}
|
||||||
@ -236,35 +203,6 @@ 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(())
|
||||||
}
|
}
|
||||||
@ -273,22 +211,6 @@ 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(())
|
||||||
}
|
}
|
||||||
@ -298,35 +220,6 @@ 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(())
|
||||||
}
|
}
|
||||||
@ -337,32 +230,6 @@ 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(())
|
||||||
}
|
}
|
||||||
@ -371,22 +238,6 @@ 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,9 +22,6 @@ 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,
|
||||||
@ -119,9 +116,6 @@ 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;
|
||||||
@ -154,7 +148,6 @@ 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,6 +1,5 @@
|
|||||||
mod dispatch;
|
mod dispatch;
|
||||||
mod lifecycle;
|
mod lifecycle;
|
||||||
pub mod render_manager;
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
mod tick;
|
mod tick;
|
||||||
@ -10,9 +9,7 @@ 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;
|
||||||
@ -28,9 +25,6 @@ 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>,
|
||||||
|
|||||||
@ -1,188 +0,0 @@
|
|||||||
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: "gfx2d".into(),
|
module: "gfx".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: "gfx2d".into(),
|
module: "gfx".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: "gfx2d".into(),
|
module: "gfx".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: "gfx2d".into(),
|
module: "gfx".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
@ -388,104 +388,6 @@ 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);
|
||||||
@ -654,6 +556,7 @@ 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());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -711,6 +614,8 @@ 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(
|
||||||
@ -727,6 +632,7 @@ 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());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -749,7 +655,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: "gfx2d".into(),
|
module: "gfx".into(),
|
||||||
name: "draw_text".into(),
|
name: "draw_text".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 4,
|
arg_slots: 4,
|
||||||
@ -758,6 +664,8 @@ 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(
|
||||||
@ -773,92 +681,10 @@ 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");
|
||||||
let submission =
|
hardware.gfx.present();
|
||||||
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);
|
||||||
@ -870,7 +696,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: "gfx2d".into(),
|
module: "gfx".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
@ -914,9 +740,6 @@ 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);
|
||||||
|
|
||||||
@ -933,7 +756,6 @@ 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);
|
||||||
@ -1034,7 +856,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: "gfx2d".into(),
|
module: "gfx".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
@ -1056,7 +878,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: "gfx2d".into(),
|
module: "gfx".into(),
|
||||||
name: "clear".into(),
|
name: "clear".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
|
|||||||
@ -1,30 +1,16 @@
|
|||||||
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::{
|
use prometeu_hal::{HardwareBridge, HostContext, InputSignals};
|
||||||
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>() {
|
||||||
@ -234,25 +220,7 @@ impl VirtualMachineRuntime {
|
|||||||
if run.reason == LogicalFrameEndingReason::FrameSync
|
if run.reason == LogicalFrameEndingReason::FrameSync
|
||||||
|| run.reason == LogicalFrameEndingReason::EndOfRom
|
|| run.reason == LogicalFrameEndingReason::EndOfRom
|
||||||
{
|
{
|
||||||
self.render_manager.set_active_app_mode(self.current_cartridge_app_mode);
|
if let Err(payload) = catch_unwind(AssertUnwindSafe(|| hw.render_frame())) {
|
||||||
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: "gfx2d".into(),
|
module: "gfx".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: "gfx2d".into(),
|
module: "gfx".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: "gfx2d".into(),
|
module: "gfx".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: "gfx2d".into(),
|
module: "gfx".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: "gfx2d".into(),
|
module: "gfx".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: "gfx2d".into(),
|
module: "gfx".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: "gfx2d".into(),
|
module: "gfx".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: "gfx2d".into(),
|
module: "gfx".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: "gfx2d".into(),
|
module: "gfx".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::GFX2D,
|
required: prometeu_hal::syscalls::caps::GFX,
|
||||||
provided: prometeu_hal::syscalls::caps::NONE,
|
provided: prometeu_hal::syscalls::caps::NONE,
|
||||||
module: "gfx2d".into(),
|
module: "gfx".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: "gfx2d".into(),
|
module: "gfx".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: "gfx2d".into(),
|
module: "gfx".into(),
|
||||||
name: "draw_line".into(),
|
name: "draw_line".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
declared_arg_slots: 4,
|
declared_arg_slots: 4,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ 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;
|
||||||
|
|||||||
487
crates/host/prometeu-host-desktop-winit/src/overlay.rs
Normal file
487
crates/host/prometeu-host-desktop-winit/src/overlay.rs
Normal file
@ -0,0 +1,487 @@
|
|||||||
|
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,6 +3,7 @@ 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;
|
||||||
@ -127,6 +128,9 @@ 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.
|
||||||
@ -166,6 +170,7 @@ 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(),
|
||||||
@ -267,6 +272,9 @@ 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");
|
||||||
|
|
||||||
@ -278,6 +286,10 @@ 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() {
|
||||||
@ -297,6 +309,12 @@ 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,8 +332,12 @@ 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 debugger inspection state; this is not a guest-visible debug ABI switch.
|
// Sync inspection mode state. This is host-owned overlay/debugger control,
|
||||||
self.firmware.os.vm().set_inspection_active(self.debugger.stream.is_some());
|
// not a guest-visible debug ABI switch.
|
||||||
|
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: "gfx2d".into(),
|
module: "gfx".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: "gfx2d".into(),
|
module: "gfx".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\": [\"composer\", \"gfx2d\", \"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\": [\"gfx\", \"log\", \"asset\"]\n}\n")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{"type":"meta","next_id":{"DSC":39,"AGD":39,"DEC":31,"PLN":87,"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-0038","status":"in_progress","ticket":"render-frame-packet-boundary","title":"Logical Render Pipelines and Command Packets","created_at":"2026-05-25","updated_at":"2026-05-25","tags":["gfx","renderer","runtime","frame-composer","architecture","ui","pipeline"],"agendas":[{"id":"AGD-0038","file":"AGD-0038-renderframepacket-boundary-for-classic-2d-renderer.md","status":"accepted","created_at":"2026-05-25","updated_at":"2026-05-25"}],"decisions":[{"id":"DEC-0030","file":"DEC-0030-logical-render-pipeline-command-boundary.md","status":"accepted","created_at":"2026-05-25","updated_at":"2026-05-25","ref_agenda":"AGD-0038"}],"plans":[{"id":"PLN-0073","file":"PLN-0073-render-contract-specs.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0074","file":"PLN-0074-hal-render-submission-types.md","status":"done","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0075","file":"PLN-0075-rendermanager-core.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0076","file":"PLN-0076-capabilities-and-abi-domain-split.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0077","file":"PLN-0077-composer-buffer-and-game2d-packet.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0078","file":"PLN-0078-classic2d-game-renderer-consumer.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0079","file":"PLN-0079-gfx2d-primitive-domain.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0080","file":"PLN-0080-shell-ui-packet-and-gfxui-domain.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0081","file":"PLN-0081-shell-hub-migration.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0082","file":"PLN-0082-frame-publication-and-present-boundary.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0083","file":"PLN-0083-fade-removal.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0084","file":"PLN-0084-host-debug-overlay-removal.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0085","file":"PLN-0085-pbs-stdlib-and-syscall-declarations.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]},{"id":"PLN-0086","file":"PLN-0086-end-to-end-render-boundary-validation.md","status":"open","created_at":"2026-05-25","updated_at":"2026-05-25","ref_decisions":["DEC-0030"]}],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-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"}]}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: PLN-0075
|
id: PLN-0075
|
||||||
ticket: render-frame-packet-boundary
|
ticket: render-frame-packet-boundary
|
||||||
title: RenderManager Core
|
title: RenderManager Core
|
||||||
status: done
|
status: open
|
||||||
created: 2026-05-25
|
created: 2026-05-25
|
||||||
ref_decisions: [DEC-0030]
|
ref_decisions: [DEC-0030]
|
||||||
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: PLN-0076
|
id: PLN-0076
|
||||||
ticket: render-frame-packet-boundary
|
ticket: render-frame-packet-boundary
|
||||||
title: Capabilities and ABI Domain Split
|
title: Capabilities and ABI Domain Split
|
||||||
status: done
|
status: open
|
||||||
created: 2026-05-25
|
created: 2026-05-25
|
||||||
ref_decisions: [DEC-0030]
|
ref_decisions: [DEC-0030]
|
||||||
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: PLN-0077
|
id: PLN-0077
|
||||||
ticket: render-frame-packet-boundary
|
ticket: render-frame-packet-boundary
|
||||||
title: Composer Buffer and Game2D Packet
|
title: Composer Buffer and Game2D Packet
|
||||||
status: done
|
status: open
|
||||||
created: 2026-05-25
|
created: 2026-05-25
|
||||||
ref_decisions: [DEC-0030]
|
ref_decisions: [DEC-0030]
|
||||||
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: PLN-0078
|
id: PLN-0078
|
||||||
ticket: render-frame-packet-boundary
|
ticket: render-frame-packet-boundary
|
||||||
title: Classic2D Game Renderer Consumer
|
title: Classic2D Game Renderer Consumer
|
||||||
status: done
|
status: open
|
||||||
created: 2026-05-25
|
created: 2026-05-25
|
||||||
ref_decisions: [DEC-0030]
|
ref_decisions: [DEC-0030]
|
||||||
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: PLN-0079
|
id: PLN-0079
|
||||||
ticket: render-frame-packet-boundary
|
ticket: render-frame-packet-boundary
|
||||||
title: Gfx2D Primitive Domain
|
title: Gfx2D Primitive Domain
|
||||||
status: done
|
status: open
|
||||||
created: 2026-05-25
|
created: 2026-05-25
|
||||||
ref_decisions: [DEC-0030]
|
ref_decisions: [DEC-0030]
|
||||||
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: PLN-0080
|
id: PLN-0080
|
||||||
ticket: render-frame-packet-boundary
|
ticket: render-frame-packet-boundary
|
||||||
title: Shell UI Packet and GfxUI Domain
|
title: Shell UI Packet and GfxUI Domain
|
||||||
status: done
|
status: open
|
||||||
created: 2026-05-25
|
created: 2026-05-25
|
||||||
ref_decisions: [DEC-0030]
|
ref_decisions: [DEC-0030]
|
||||||
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: PLN-0081
|
id: PLN-0081
|
||||||
ticket: render-frame-packet-boundary
|
ticket: render-frame-packet-boundary
|
||||||
title: Shell Hub Migration
|
title: Shell Hub Migration
|
||||||
status: done
|
status: open
|
||||||
created: 2026-05-25
|
created: 2026-05-25
|
||||||
ref_decisions: [DEC-0030]
|
ref_decisions: [DEC-0030]
|
||||||
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: PLN-0082
|
id: PLN-0082
|
||||||
ticket: render-frame-packet-boundary
|
ticket: render-frame-packet-boundary
|
||||||
title: Frame Publication and Present Boundary
|
title: Frame Publication and Present Boundary
|
||||||
status: done
|
status: open
|
||||||
created: 2026-05-25
|
created: 2026-05-25
|
||||||
ref_decisions: [DEC-0030]
|
ref_decisions: [DEC-0030]
|
||||||
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: PLN-0083
|
id: PLN-0083
|
||||||
ticket: render-frame-packet-boundary
|
ticket: render-frame-packet-boundary
|
||||||
title: Fade Removal
|
title: Fade Removal
|
||||||
status: done
|
status: open
|
||||||
created: 2026-05-25
|
created: 2026-05-25
|
||||||
ref_decisions: [DEC-0030]
|
ref_decisions: [DEC-0030]
|
||||||
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: PLN-0084
|
id: PLN-0084
|
||||||
ticket: render-frame-packet-boundary
|
ticket: render-frame-packet-boundary
|
||||||
title: Host Debug Overlay Removal
|
title: Host Debug Overlay Removal
|
||||||
status: done
|
status: open
|
||||||
created: 2026-05-25
|
created: 2026-05-25
|
||||||
ref_decisions: [DEC-0030]
|
ref_decisions: [DEC-0030]
|
||||||
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: PLN-0085
|
id: PLN-0085
|
||||||
ticket: render-frame-packet-boundary
|
ticket: render-frame-packet-boundary
|
||||||
title: PBS Stdlib and Syscall Declarations
|
title: PBS Stdlib and Syscall Declarations
|
||||||
status: done
|
status: open
|
||||||
created: 2026-05-25
|
created: 2026-05-25
|
||||||
ref_decisions: [DEC-0030]
|
ref_decisions: [DEC-0030]
|
||||||
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: PLN-0086
|
id: PLN-0086
|
||||||
ticket: render-frame-packet-boundary
|
ticket: render-frame-packet-boundary
|
||||||
title: End-to-End Render Boundary Validation
|
title: End-to-End Render Boundary Validation
|
||||||
status: done
|
status: open
|
||||||
created: 2026-05-25
|
created: 2026-05-25
|
||||||
ref_decisions: [DEC-0030]
|
ref_decisions: [DEC-0030]
|
||||||
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
tags: [gfx, renderer, runtime, frame-composer, architecture, ui, pipeline]
|
||||||
|
|||||||
@ -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 UI isolation.
|
- host-side debug overlay (HUD) 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 inspection surfaces MUST observe the last published logical frame rather than force continuous redraw to probe for changes.
|
Host-owned overlay or inspection surfaces MUST observe the last published logical frame rather than force continuous redraw to probe for changes.
|
||||||
|
|
||||||
When debugger or other host-only diagnostics become stable:
|
When overlay, debugger, or other host-only diagnostics become visually 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 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 overlay toggle, resize, expose, or explicit debugger-visible state transition.
|
||||||
|
|
||||||
Paused or breakpointed execution does not grant permission to swap logical machine buffers just to sustain host-only diagnostics.
|
Paused or breakpointed execution does not grant permission to swap logical machine buffers just to sustain a host-only HUD.
|
||||||
|
|
||||||
## 6 Time Profiling (Cycles)
|
## 6 Time Profiling (Cycles)
|
||||||
|
|
||||||
@ -285,32 +285,28 @@ Each event has:
|
|||||||
- cost
|
- cost
|
||||||
- consequence
|
- consequence
|
||||||
|
|
||||||
## 11 Host-Side Debug UI Isolation
|
## 11 Host-Side Debug Overlay (HUD) Isolation
|
||||||
|
|
||||||
The desktop host MUST NOT inject a debug overlay into Game or Shell output. Any
|
The visual Debug Overlay (HUD) for technical inspection is not part of the emulated machine pipeline.
|
||||||
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 diagnostics:** The Desktop Host may consume telemetry for logs,
|
3. **Host overlay module:** The Desktop Host owns a dedicated overlay module that performs host-side text, panel, and simple bar composition.
|
||||||
debugger streams, and non-render diagnostics, but it does not compose debug
|
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.
|
||||||
pixels into the presentation surface.
|
5. **Host control:** Overlay visibility and presentation policy remain under Host control.
|
||||||
4. **Composition boundary:** Published Game and Shell output must be presented
|
6. **Host (Desktop):** Responsible for collecting telemetry from the runtime and rendering the HUD as a native, transparent layer.
|
||||||
without host-owned debug overlay injection.
|
|
||||||
|
|
||||||
### 10.2 Principles
|
### 10.2 Principles
|
||||||
|
|
||||||
- **Zero Pipeline Interference:** Host diagnostics must not inject pixels into
|
- **Zero Pipeline Interference:** HUD rendering must not inject pixels into the emulated framebuffer. It is applied after upscaling or as a separate display surface.
|
||||||
the emulated framebuffer or the host presentation surface.
|
- **Zero Cycle Impact:** HUD-related processing (like formatting technical text) must occur outside the emulated machine cycles.
|
||||||
- **Zero Cycle Impact:** Diagnostic processing must occur outside the emulated
|
- **Toggle Control:** The activation of the overlay (typically via **F1**) is managed by the Host layer.
|
||||||
machine cycles.
|
|
||||||
|
|
||||||
### 10.3 Atomic Telemetry Model
|
### 10.3 Atomic Telemetry Model
|
||||||
|
|
||||||
To ensure zero-impact synchronization between the VM and host diagnostics, PROMETEU uses a **push-based atomic model**:
|
To ensure zero-impact synchronization between the VM and the Host Debug Overlay, 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.
|
||||||
@ -324,4 +320,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 host diagnostics are active or not.
|
- are consistent regardless of whether the Host HUD is active or not.
|
||||||
|
|||||||
@ -51,8 +51,7 @@ 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 and debugger integrations that do not modify presented
|
- **technical inspection surfaces (Debug Overlay/HUD)**
|
||||||
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.
|
||||||
|
|
||||||
@ -124,8 +123,8 @@ The graphics system:
|
|||||||
The platform layer:
|
The platform layer:
|
||||||
|
|
||||||
- consumes closed render submissions through a render-surface implementation
|
- consumes closed render submissions through a render-surface implementation
|
||||||
- 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 published RGBA8888 output 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
|
||||||
@ -137,19 +136,17 @@ host-owned invalidation, not by perpetual redraw polling.
|
|||||||
In particular:
|
In particular:
|
||||||
|
|
||||||
- a stable published submission 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 debugger-visible host changes;
|
- 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 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 are host-owned and must not inject pixels into Game or Shell output.
|
To preserve portability and certification purity, technical inspection tools (like the Debug Overlay) are moved to the Host layer.
|
||||||
|
|
||||||
- **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
|
- **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.
|
||||||
`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:
|
||||||
|
|
||||||
```
|
```
|
||||||
("gfx2d", "draw_line", 1)
|
("gfx", "present", 1)
|
||||||
("audio", "play", 2)
|
("audio", "play", 2)
|
||||||
("composer", "emit_sprite", 1)
|
("composer", "emit_sprite", 1)
|
||||||
```
|
```
|
||||||
|
|||||||
@ -46,7 +46,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"host-dependent": {
|
"host-dependent": {
|
||||||
"baseline_percent": 35,
|
"baseline_percent": 43,
|
||||||
"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": ["composer", "gfx2d", "log", "asset"]
|
"capabilities": ["gfx", "log", "asset"]
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user