Compare commits
18 Commits
98d2d81882
...
76254928e6
| Author | SHA1 | Date | |
|---|---|---|---|
| 76254928e6 | |||
| 1c74631a5e | |||
| aaed1e95dd | |||
| 2865cb3803 | |||
| 4f52a65169 | |||
| b0b8bb9028 | |||
| 4a5210f347 | |||
| 3fef407efc | |||
| cc700c6cf8 | |||
| dd90ff812c | |||
| 240fe65da7 | |||
| f4260d0cf4 | |||
| a1bd60671b | |||
| 5ef43045bc | |||
| 3931e86b41 | |||
| 5a0476e8b0 | |||
| ed05f337ce | |||
| 94c80e61ba |
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1475,6 +1475,8 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prometeu-bytecode",
|
||||
"prometeu-hal",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@ -14,7 +14,7 @@ use prometeu_hal::glyph::Glyph;
|
||||
use prometeu_hal::glyph_bank::{GlyphBank, TileSize};
|
||||
use prometeu_hal::sample::Sample;
|
||||
use prometeu_hal::scene_bank::SceneBank;
|
||||
use prometeu_hal::scene_layer::{MotionFactor, SceneLayer};
|
||||
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
||||
use prometeu_hal::sound_bank::SoundBank;
|
||||
use prometeu_hal::tile::Tile;
|
||||
use prometeu_hal::tilemap::TileMap;
|
||||
@ -748,7 +748,7 @@ impl AssetManager {
|
||||
active: false,
|
||||
glyph_bank_id: 0,
|
||||
tile_size: TileSize::Size8,
|
||||
motion_factor: MotionFactor { x: 1.0, y: 1.0 },
|
||||
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
|
||||
tilemap: TileMap { width: 0, height: 0, tiles: Vec::new() },
|
||||
});
|
||||
let mut layers = layers;
|
||||
@ -770,20 +770,20 @@ impl AssetManager {
|
||||
32 => TileSize::Size32,
|
||||
other => return Err(format!("Invalid SCENE tile size: {}", other)),
|
||||
};
|
||||
let motion_factor_x = f32::from_le_bytes([
|
||||
let parallax_factor_x = f32::from_le_bytes([
|
||||
buffer[offset + 4],
|
||||
buffer[offset + 5],
|
||||
buffer[offset + 6],
|
||||
buffer[offset + 7],
|
||||
]);
|
||||
let motion_factor_y = f32::from_le_bytes([
|
||||
let parallax_factor_y = f32::from_le_bytes([
|
||||
buffer[offset + 8],
|
||||
buffer[offset + 9],
|
||||
buffer[offset + 10],
|
||||
buffer[offset + 11],
|
||||
]);
|
||||
if !motion_factor_x.is_finite() || !motion_factor_y.is_finite() {
|
||||
return Err("Invalid SCENE motion_factor".to_string());
|
||||
if !parallax_factor_x.is_finite() || !parallax_factor_y.is_finite() {
|
||||
return Err("Invalid SCENE parallax_factor".to_string());
|
||||
}
|
||||
|
||||
let width = u32::from_le_bytes([
|
||||
@ -847,7 +847,7 @@ impl AssetManager {
|
||||
active: (flags & 0b0000_0001) != 0,
|
||||
glyph_bank_id,
|
||||
tile_size,
|
||||
motion_factor: MotionFactor { x: motion_factor_x, y: motion_factor_y },
|
||||
parallax_factor: ParallaxFactor { x: parallax_factor_x, y: parallax_factor_y },
|
||||
tilemap: TileMap { width, height, tiles },
|
||||
};
|
||||
}
|
||||
@ -1105,7 +1105,7 @@ mod tests {
|
||||
SCENE_PAYLOAD_MAGIC_V1, SCENE_PAYLOAD_VERSION_V1,
|
||||
};
|
||||
use prometeu_hal::glyph::Glyph;
|
||||
use prometeu_hal::scene_layer::{MotionFactor, SceneLayer};
|
||||
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
||||
use prometeu_hal::tile::Tile;
|
||||
use prometeu_hal::tilemap::TileMap;
|
||||
|
||||
@ -1144,11 +1144,11 @@ mod tests {
|
||||
|
||||
fn test_scene() -> SceneBank {
|
||||
let make_layer =
|
||||
|glyph_bank_id: u8, motion_x: f32, motion_y: f32, tile_size: TileSize| SceneLayer {
|
||||
|glyph_bank_id: u8, parallax_x: f32, parallax_y: f32, tile_size: TileSize| SceneLayer {
|
||||
active: glyph_bank_id != 3,
|
||||
glyph_bank_id,
|
||||
tile_size,
|
||||
motion_factor: MotionFactor { x: motion_x, y: motion_y },
|
||||
parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_y },
|
||||
tilemap: TileMap {
|
||||
width: 2,
|
||||
height: 2,
|
||||
@ -1227,8 +1227,8 @@ mod tests {
|
||||
data.push(layer.glyph_bank_id);
|
||||
data.push(layer.tile_size as u8);
|
||||
data.push(0);
|
||||
data.extend_from_slice(&layer.motion_factor.x.to_le_bytes());
|
||||
data.extend_from_slice(&layer.motion_factor.y.to_le_bytes());
|
||||
data.extend_from_slice(&layer.parallax_factor.x.to_le_bytes());
|
||||
data.extend_from_slice(&layer.parallax_factor.y.to_le_bytes());
|
||||
data.extend_from_slice(&(layer.tilemap.width as u32).to_le_bytes());
|
||||
data.extend_from_slice(&(layer.tilemap.height as u32).to_le_bytes());
|
||||
data.extend_from_slice(&(layer.tilemap.tiles.len() as u32).to_le_bytes());
|
||||
@ -1359,7 +1359,7 @@ mod tests {
|
||||
let decoded = AssetManager::decode_scene_bank_from_buffer(&entry, &data).expect("scene");
|
||||
|
||||
assert_eq!(decoded.layers[1].glyph_bank_id, 1);
|
||||
assert_eq!(decoded.layers[1].motion_factor.x, 0.5);
|
||||
assert_eq!(decoded.layers[1].parallax_factor.x, 0.5);
|
||||
assert_eq!(decoded.layers[2].tile_size, TileSize::Size32);
|
||||
assert_eq!(decoded.layers[0].tilemap.tiles[1].flip_x, true);
|
||||
assert_eq!(decoded.layers[2].tilemap.tiles[2].flip_y, true);
|
||||
|
||||
719
crates/console/prometeu-drivers/src/frame_composer.rs
Normal file
719
crates/console/prometeu-drivers/src/frame_composer.rs
Normal file
@ -0,0 +1,719 @@
|
||||
use crate::memory_banks::SceneBankPoolAccess;
|
||||
use prometeu_hal::GfxBridge;
|
||||
use prometeu_hal::glyph::Glyph;
|
||||
use prometeu_hal::scene_bank::SceneBank;
|
||||
use prometeu_hal::scene_viewport_cache::SceneViewportCache;
|
||||
use prometeu_hal::scene_viewport_resolver::{CacheRefreshRequest, SceneViewportResolver};
|
||||
use prometeu_hal::sprite::Sprite;
|
||||
use std::sync::Arc;
|
||||
|
||||
const EMPTY_SPRITE: Sprite = Sprite {
|
||||
glyph: Glyph { glyph_id: 0, palette_id: 0 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 0,
|
||||
bank_id: 0,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 0,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum SceneStatus {
|
||||
#[default]
|
||||
Unbound,
|
||||
Available {
|
||||
scene_bank_id: usize,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SpriteController {
|
||||
sprites: [Sprite; 512],
|
||||
sprite_count: usize,
|
||||
frame_counter: u64,
|
||||
dropped_sprites: usize,
|
||||
layer_buckets: [Vec<usize>; 4],
|
||||
}
|
||||
|
||||
impl Default for SpriteController {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl SpriteController {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
sprites: [EMPTY_SPRITE; 512],
|
||||
sprite_count: 0,
|
||||
frame_counter: 0,
|
||||
dropped_sprites: 0,
|
||||
layer_buckets: std::array::from_fn(|_| Vec::with_capacity(128)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sprites(&self) -> &[Sprite; 512] {
|
||||
&self.sprites
|
||||
}
|
||||
|
||||
pub fn sprites_mut(&mut self) -> &mut [Sprite; 512] {
|
||||
&mut self.sprites
|
||||
}
|
||||
|
||||
pub fn begin_frame(&mut self) {
|
||||
self.frame_counter = self.frame_counter.wrapping_add(1);
|
||||
self.sprite_count = 0;
|
||||
self.dropped_sprites = 0;
|
||||
for bucket in &mut self.layer_buckets {
|
||||
bucket.clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emit_sprite(&mut self, mut sprite: Sprite) -> bool {
|
||||
let Some(bucket) = self.layer_buckets.get_mut(sprite.layer as usize) else {
|
||||
self.dropped_sprites += 1;
|
||||
return false;
|
||||
};
|
||||
if self.sprite_count >= self.sprites.len() {
|
||||
self.dropped_sprites += 1;
|
||||
return false;
|
||||
}
|
||||
|
||||
sprite.active = true;
|
||||
let index = self.sprite_count;
|
||||
self.sprites[index] = sprite;
|
||||
self.sprite_count += 1;
|
||||
bucket.push(index);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn sprite_count(&self) -> usize {
|
||||
self.sprite_count
|
||||
}
|
||||
|
||||
pub fn frame_counter(&self) -> u64 {
|
||||
self.frame_counter
|
||||
}
|
||||
|
||||
pub fn dropped_sprites(&self) -> usize {
|
||||
self.dropped_sprites
|
||||
}
|
||||
|
||||
pub fn ordered_sprites(&self) -> Vec<Sprite> {
|
||||
let mut ordered = Vec::with_capacity(self.sprite_count);
|
||||
for bucket in &self.layer_buckets {
|
||||
let mut indices = bucket.clone();
|
||||
indices.sort_by_key(|&index| self.sprites[index].priority);
|
||||
for index in indices {
|
||||
ordered.push(self.sprites[index]);
|
||||
}
|
||||
}
|
||||
ordered
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FrameComposer {
|
||||
scene_bank_pool: Arc<dyn SceneBankPoolAccess>,
|
||||
viewport_width_px: usize,
|
||||
viewport_height_px: usize,
|
||||
active_scene_id: Option<usize>,
|
||||
active_scene: Option<Arc<SceneBank>>,
|
||||
scene_status: SceneStatus,
|
||||
camera_x_px: i32,
|
||||
camera_y_px: i32,
|
||||
cache: Option<SceneViewportCache>,
|
||||
resolver: Option<SceneViewportResolver>,
|
||||
sprite_controller: SpriteController,
|
||||
}
|
||||
|
||||
impl FrameComposer {
|
||||
pub fn new(
|
||||
viewport_width_px: usize,
|
||||
viewport_height_px: usize,
|
||||
scene_bank_pool: Arc<dyn SceneBankPoolAccess>,
|
||||
) -> Self {
|
||||
Self {
|
||||
scene_bank_pool,
|
||||
viewport_width_px,
|
||||
viewport_height_px,
|
||||
active_scene_id: None,
|
||||
active_scene: None,
|
||||
scene_status: SceneStatus::Unbound,
|
||||
camera_x_px: 0,
|
||||
camera_y_px: 0,
|
||||
cache: None,
|
||||
resolver: None,
|
||||
sprite_controller: SpriteController::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn viewport_size(&self) -> (usize, usize) {
|
||||
(self.viewport_width_px, self.viewport_height_px)
|
||||
}
|
||||
|
||||
pub fn scene_bank_pool(&self) -> &Arc<dyn SceneBankPoolAccess> {
|
||||
&self.scene_bank_pool
|
||||
}
|
||||
|
||||
pub fn scene_bank_slot(&self, slot: usize) -> Option<Arc<SceneBank>> {
|
||||
self.scene_bank_pool.scene_bank_slot(slot)
|
||||
}
|
||||
|
||||
pub fn scene_bank_slot_count(&self) -> usize {
|
||||
self.scene_bank_pool.scene_bank_slot_count()
|
||||
}
|
||||
|
||||
pub fn active_scene_id(&self) -> Option<usize> {
|
||||
self.active_scene_id
|
||||
}
|
||||
|
||||
pub fn active_scene(&self) -> Option<&Arc<SceneBank>> {
|
||||
self.active_scene.as_ref()
|
||||
}
|
||||
|
||||
pub fn scene_status(&self) -> SceneStatus {
|
||||
self.scene_status
|
||||
}
|
||||
|
||||
pub fn camera(&self) -> (i32, i32) {
|
||||
(self.camera_x_px, self.camera_y_px)
|
||||
}
|
||||
|
||||
pub fn bind_scene(&mut self, scene_bank_id: usize) -> bool {
|
||||
let Some(scene) = self.scene_bank_pool.scene_bank_slot(scene_bank_id) else {
|
||||
self.unbind_scene();
|
||||
return false;
|
||||
};
|
||||
|
||||
let (cache, resolver) =
|
||||
Self::build_scene_runtime(self.viewport_width_px, self.viewport_height_px, &scene);
|
||||
self.active_scene_id = Some(scene_bank_id);
|
||||
self.active_scene = Some(scene);
|
||||
self.scene_status = SceneStatus::Available { scene_bank_id };
|
||||
self.cache = Some(cache);
|
||||
self.resolver = Some(resolver);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn unbind_scene(&mut self) {
|
||||
self.active_scene_id = None;
|
||||
self.active_scene = None;
|
||||
self.scene_status = SceneStatus::Unbound;
|
||||
self.cache = None;
|
||||
self.resolver = None;
|
||||
}
|
||||
|
||||
pub fn set_camera(&mut self, x: i32, y: i32) {
|
||||
self.camera_x_px = x;
|
||||
self.camera_y_px = y;
|
||||
}
|
||||
|
||||
pub fn cache(&self) -> Option<&SceneViewportCache> {
|
||||
self.cache.as_ref()
|
||||
}
|
||||
|
||||
pub fn resolver(&self) -> Option<&SceneViewportResolver> {
|
||||
self.resolver.as_ref()
|
||||
}
|
||||
|
||||
pub fn sprite_controller(&self) -> &SpriteController {
|
||||
&self.sprite_controller
|
||||
}
|
||||
|
||||
pub fn sprite_controller_mut(&mut self) -> &mut SpriteController {
|
||||
&mut self.sprite_controller
|
||||
}
|
||||
|
||||
pub fn begin_frame(&mut self) {
|
||||
self.sprite_controller.begin_frame();
|
||||
}
|
||||
|
||||
pub fn emit_sprite(&mut self, sprite: Sprite) -> bool {
|
||||
self.sprite_controller.emit_sprite(sprite)
|
||||
}
|
||||
|
||||
pub fn ordered_sprites(&self) -> Vec<Sprite> {
|
||||
self.sprite_controller.ordered_sprites()
|
||||
}
|
||||
|
||||
pub fn render_frame(&mut self, gfx: &mut dyn GfxBridge) {
|
||||
let ordered_sprites = self.ordered_sprites();
|
||||
gfx.load_frame_sprites(&ordered_sprites);
|
||||
|
||||
if let (Some(scene), Some(cache), Some(resolver)) =
|
||||
(self.active_scene.as_deref(), self.cache.as_mut(), self.resolver.as_mut())
|
||||
{
|
||||
let update = resolver.update(scene, self.camera_x_px, self.camera_y_px);
|
||||
Self::apply_refresh_requests(cache, scene, &update.refresh_requests);
|
||||
// `FrameComposer` owns only canonical game-frame composition.
|
||||
// Deferred `gfx.*` primitives are drained later by a separate
|
||||
// overlay/debug stage outside this service boundary.
|
||||
gfx.render_scene_from_cache(cache, &update);
|
||||
return;
|
||||
}
|
||||
|
||||
// No-scene frames still stop at canonical game composition. Final
|
||||
// overlay/debug work remains outside `FrameComposer`.
|
||||
gfx.render_no_scene_frame();
|
||||
}
|
||||
|
||||
fn build_scene_runtime(
|
||||
viewport_width_px: usize,
|
||||
viewport_height_px: usize,
|
||||
scene: &SceneBank,
|
||||
) -> (SceneViewportCache, SceneViewportResolver) {
|
||||
let min_tile_px =
|
||||
scene.layers.iter().map(|layer| layer.tile_size as usize).min().unwrap_or(8);
|
||||
let cache_width_tiles = viewport_width_px.div_ceil(min_tile_px) + 5;
|
||||
let cache_height_tiles = viewport_height_px.div_ceil(min_tile_px) + 4;
|
||||
let hysteresis_safe_px = min_tile_px.saturating_sub(4) as i32;
|
||||
let hysteresis_trigger_px = (min_tile_px + 4) as i32;
|
||||
|
||||
(
|
||||
SceneViewportCache::new(scene, cache_width_tiles, cache_height_tiles),
|
||||
SceneViewportResolver::new(
|
||||
viewport_width_px as i32,
|
||||
viewport_height_px as i32,
|
||||
cache_width_tiles,
|
||||
cache_height_tiles,
|
||||
hysteresis_safe_px,
|
||||
hysteresis_trigger_px,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn apply_refresh_requests(
|
||||
cache: &mut SceneViewportCache,
|
||||
scene: &SceneBank,
|
||||
refresh_requests: &[CacheRefreshRequest],
|
||||
) {
|
||||
for request in refresh_requests {
|
||||
match *request {
|
||||
CacheRefreshRequest::InvalidateLayer { layer_index } => {
|
||||
cache.layers[layer_index].invalidate_all();
|
||||
}
|
||||
CacheRefreshRequest::RefreshLine { layer_index, cache_y } => {
|
||||
cache.refresh_layer_line(scene, layer_index, cache_y);
|
||||
}
|
||||
CacheRefreshRequest::RefreshColumn { layer_index, cache_x } => {
|
||||
cache.refresh_layer_column(scene, layer_index, cache_x);
|
||||
}
|
||||
CacheRefreshRequest::RefreshRegion { layer_index, region } => {
|
||||
cache.refresh_layer_region(scene, layer_index, region);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::gfx::Gfx;
|
||||
use crate::memory_banks::{
|
||||
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller,
|
||||
};
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::glyph_bank::{GlyphBank, TileSize};
|
||||
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
||||
use prometeu_hal::tile::Tile;
|
||||
use prometeu_hal::tilemap::TileMap;
|
||||
|
||||
fn make_scene() -> SceneBank {
|
||||
make_scene_with_palette(1, 1, TileSize::Size8)
|
||||
}
|
||||
|
||||
fn make_scene_with_palette(
|
||||
glyph_bank_id: u8,
|
||||
palette_id: u8,
|
||||
tile_size: TileSize,
|
||||
) -> SceneBank {
|
||||
let layer = SceneLayer {
|
||||
active: true,
|
||||
glyph_bank_id,
|
||||
tile_size,
|
||||
parallax_factor: ParallaxFactor { x: 1.0, y: 0.5 },
|
||||
tilemap: TileMap {
|
||||
width: 2,
|
||||
height: 2,
|
||||
tiles: vec![
|
||||
Tile {
|
||||
active: true,
|
||||
glyph: Glyph { glyph_id: 0, palette_id },
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
};
|
||||
4
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
SceneBank { layers: std::array::from_fn(|_| layer.clone()) }
|
||||
}
|
||||
|
||||
fn make_glyph_bank(tile_size: TileSize, palette_id: u8, color: Color) -> GlyphBank {
|
||||
let size = tile_size as usize;
|
||||
let mut bank = GlyphBank::new(tile_size, size, size);
|
||||
bank.palettes[palette_id as usize][1] = color;
|
||||
for pixel in &mut bank.pixel_indices {
|
||||
*pixel = 1;
|
||||
}
|
||||
bank
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_composer_starts_unbound_with_empty_owned_state() {
|
||||
let frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()));
|
||||
|
||||
assert_eq!(frame_composer.viewport_size(), (320, 180));
|
||||
assert_eq!(frame_composer.active_scene_id(), None);
|
||||
assert!(frame_composer.active_scene().is_none());
|
||||
assert_eq!(frame_composer.scene_status(), SceneStatus::Unbound);
|
||||
assert_eq!(frame_composer.camera(), (0, 0));
|
||||
assert!(frame_composer.cache().is_none());
|
||||
assert!(frame_composer.resolver().is_none());
|
||||
assert_eq!(frame_composer.sprite_controller().sprites().len(), 512);
|
||||
assert_eq!(frame_composer.sprite_controller().sprite_count(), 0);
|
||||
assert_eq!(frame_composer.sprite_controller().dropped_sprites(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_composer_exposes_shared_scene_bank_access() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
banks.install_scene_bank(3, Arc::new(make_scene()));
|
||||
|
||||
let frame_composer = FrameComposer::new(320, 180, banks);
|
||||
let scene =
|
||||
frame_composer.scene_bank_slot(3).expect("scene bank slot 3 should be resident");
|
||||
|
||||
assert_eq!(frame_composer.scene_bank_slot_count(), 16);
|
||||
assert_eq!(scene.layers[0].tile_size, TileSize::Size8);
|
||||
assert_eq!(scene.layers[0].parallax_factor.y, 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_scene_stores_scene_identity_and_shared_reference() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
banks.install_scene_bank(3, Arc::new(make_scene()));
|
||||
|
||||
let expected_scene =
|
||||
banks.scene_bank_slot(3).expect("scene bank slot 3 should be resident");
|
||||
let mut frame_composer = FrameComposer::new(320, 180, banks);
|
||||
|
||||
assert!(frame_composer.bind_scene(3));
|
||||
|
||||
assert_eq!(frame_composer.active_scene_id(), Some(3));
|
||||
assert!(Arc::ptr_eq(
|
||||
frame_composer.active_scene().expect("active scene should exist"),
|
||||
&expected_scene,
|
||||
));
|
||||
assert_eq!(frame_composer.scene_status(), SceneStatus::Available { scene_bank_id: 3 });
|
||||
assert!(frame_composer.cache().is_some());
|
||||
assert!(frame_composer.resolver().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbind_scene_clears_scene_and_cache_state() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
banks.install_scene_bank(1, Arc::new(make_scene()));
|
||||
|
||||
let mut frame_composer = FrameComposer::new(320, 180, banks);
|
||||
assert!(frame_composer.bind_scene(1));
|
||||
|
||||
frame_composer.unbind_scene();
|
||||
|
||||
assert_eq!(frame_composer.active_scene_id(), None);
|
||||
assert!(frame_composer.active_scene().is_none());
|
||||
assert_eq!(frame_composer.scene_status(), SceneStatus::Unbound);
|
||||
assert!(frame_composer.cache().is_none());
|
||||
assert!(frame_composer.resolver().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_camera_stores_top_left_pixel_coordinates() {
|
||||
let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()));
|
||||
|
||||
frame_composer.set_camera(-12, 48);
|
||||
|
||||
assert_eq!(frame_composer.camera(), (-12, 48));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_scene_derives_cache_and_resolver_from_eight_pixel_layers() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
banks.install_scene_bank(0, Arc::new(make_scene()));
|
||||
|
||||
let mut frame_composer = FrameComposer::new(320, 180, banks);
|
||||
assert!(frame_composer.bind_scene(0));
|
||||
|
||||
let cache = frame_composer.cache().expect("cache should exist for bound scene");
|
||||
assert_eq!((cache.width(), cache.height()), (45, 27));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_scene_binding_falls_back_to_no_scene_state() {
|
||||
let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()));
|
||||
|
||||
assert!(!frame_composer.bind_scene(7));
|
||||
|
||||
assert_eq!(frame_composer.scene_status(), SceneStatus::Unbound);
|
||||
assert!(frame_composer.cache().is_none());
|
||||
assert!(frame_composer.resolver().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sprite_controller_begin_frame_resets_sprite_count_and_buckets() {
|
||||
let mut controller = SpriteController::new();
|
||||
let emitted = controller.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id: 1, palette_id: 2 },
|
||||
x: 4,
|
||||
y: 5,
|
||||
layer: 2,
|
||||
bank_id: 3,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 1,
|
||||
});
|
||||
assert!(emitted);
|
||||
|
||||
controller.begin_frame();
|
||||
|
||||
assert_eq!(controller.frame_counter(), 1);
|
||||
assert_eq!(controller.sprite_count(), 0);
|
||||
assert_eq!(controller.dropped_sprites(), 0);
|
||||
assert!(controller.ordered_sprites().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sprite_controller_orders_by_layer_then_priority_then_fifo() {
|
||||
let mut controller = SpriteController::new();
|
||||
controller.begin_frame();
|
||||
|
||||
assert!(controller.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id: 10, palette_id: 0 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 1,
|
||||
bank_id: 0,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 2,
|
||||
}));
|
||||
assert!(controller.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id: 11, palette_id: 0 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 0,
|
||||
bank_id: 0,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 3,
|
||||
}));
|
||||
assert!(controller.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id: 12, palette_id: 0 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 1,
|
||||
bank_id: 0,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 1,
|
||||
}));
|
||||
assert!(controller.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id: 13, palette_id: 0 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 1,
|
||||
bank_id: 0,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 2,
|
||||
}));
|
||||
|
||||
let ordered = controller.ordered_sprites();
|
||||
let ordered_ids: Vec<u16> = ordered.iter().map(|sprite| sprite.glyph.glyph_id).collect();
|
||||
|
||||
assert_eq!(ordered_ids, vec![11, 12, 10, 13]);
|
||||
assert!(ordered.iter().all(|sprite| sprite.active));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sprite_controller_drops_overflow_without_panicking() {
|
||||
let mut controller = SpriteController::new();
|
||||
controller.begin_frame();
|
||||
|
||||
for glyph_id in 0..512 {
|
||||
assert!(controller.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id, palette_id: 0 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 0,
|
||||
bank_id: 0,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
let overflowed = controller.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id: 999, palette_id: 0 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 0,
|
||||
bank_id: 0,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 0,
|
||||
});
|
||||
|
||||
assert!(!overflowed);
|
||||
assert_eq!(controller.sprite_count(), 512);
|
||||
assert_eq!(controller.dropped_sprites(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_composer_emits_ordered_sprites_for_rendering() {
|
||||
let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()));
|
||||
frame_composer.begin_frame();
|
||||
|
||||
assert!(frame_composer.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id: 21, palette_id: 0 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 2,
|
||||
bank_id: 1,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 1,
|
||||
}));
|
||||
assert!(frame_composer.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id: 20, palette_id: 0 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 1,
|
||||
bank_id: 1,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 0,
|
||||
}));
|
||||
|
||||
let ordered = frame_composer.ordered_sprites();
|
||||
|
||||
assert_eq!(ordered.len(), 2);
|
||||
assert_eq!(ordered[0].glyph.glyph_id, 20);
|
||||
assert_eq!(ordered[1].glyph.glyph_id, 21);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_frame_without_scene_uses_sprite_only_path() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
banks.install_glyph_bank(1, Arc::new(make_glyph_bank(TileSize::Size8, 3, Color::WHITE)));
|
||||
|
||||
let mut frame_composer =
|
||||
FrameComposer::new(16, 16, Arc::clone(&banks) as Arc<dyn SceneBankPoolAccess>);
|
||||
frame_composer.begin_frame();
|
||||
assert!(frame_composer.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id: 0, palette_id: 3 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 0,
|
||||
bank_id: 1,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 0,
|
||||
}));
|
||||
|
||||
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
|
||||
gfx.scene_fade_level = 31;
|
||||
gfx.hud_fade_level = 31;
|
||||
|
||||
frame_composer.render_frame(&mut gfx);
|
||||
gfx.present();
|
||||
|
||||
assert_eq!(gfx.front_buffer()[0], Color::WHITE.raw());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_frame_with_scene_applies_refreshes_before_composition() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
banks.install_glyph_bank(0, Arc::new(make_glyph_bank(TileSize::Size8, 2, Color::BLUE)));
|
||||
banks.install_scene_bank(0, Arc::new(make_scene_with_palette(0, 2, TileSize::Size8)));
|
||||
|
||||
let mut frame_composer =
|
||||
FrameComposer::new(16, 16, Arc::clone(&banks) as Arc<dyn SceneBankPoolAccess>);
|
||||
assert!(frame_composer.bind_scene(0));
|
||||
|
||||
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
|
||||
gfx.scene_fade_level = 31;
|
||||
gfx.hud_fade_level = 31;
|
||||
|
||||
frame_composer.render_frame(&mut gfx);
|
||||
gfx.present();
|
||||
|
||||
assert!(
|
||||
frame_composer
|
||||
.cache()
|
||||
.expect("cache should exist")
|
||||
.layers
|
||||
.iter()
|
||||
.all(|layer| layer.valid)
|
||||
);
|
||||
assert_eq!(gfx.front_buffer()[0], Color::BLUE.raw());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_frame_survives_scene_transition_through_unbind_and_rebind() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
banks.install_glyph_bank(0, Arc::new(make_glyph_bank(TileSize::Size8, 1, Color::RED)));
|
||||
banks.install_glyph_bank(1, Arc::new(make_glyph_bank(TileSize::Size8, 2, Color::BLUE)));
|
||||
banks.install_glyph_bank(2, Arc::new(make_glyph_bank(TileSize::Size8, 3, Color::WHITE)));
|
||||
banks.install_scene_bank(0, Arc::new(make_scene_with_palette(0, 1, TileSize::Size8)));
|
||||
banks.install_scene_bank(1, Arc::new(make_scene_with_palette(1, 2, TileSize::Size8)));
|
||||
|
||||
let mut frame_composer =
|
||||
FrameComposer::new(16, 16, Arc::clone(&banks) as Arc<dyn SceneBankPoolAccess>);
|
||||
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
|
||||
gfx.scene_fade_level = 31;
|
||||
gfx.hud_fade_level = 31;
|
||||
|
||||
assert!(frame_composer.bind_scene(0));
|
||||
frame_composer.render_frame(&mut gfx);
|
||||
gfx.present();
|
||||
assert_eq!(gfx.front_buffer()[0], Color::RED.raw());
|
||||
|
||||
frame_composer.unbind_scene();
|
||||
frame_composer.begin_frame();
|
||||
assert!(frame_composer.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id: 0, palette_id: 3 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 0,
|
||||
bank_id: 2,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 0,
|
||||
}));
|
||||
frame_composer.render_frame(&mut gfx);
|
||||
gfx.present();
|
||||
assert_eq!(gfx.front_buffer()[0], Color::WHITE.raw());
|
||||
|
||||
frame_composer.begin_frame();
|
||||
assert!(frame_composer.bind_scene(1));
|
||||
frame_composer.render_frame(&mut gfx);
|
||||
gfx.present();
|
||||
assert_eq!(gfx.front_buffer()[0], Color::BLUE.raw());
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
use crate::gfx_overlay::{DeferredGfxOverlay, OverlayCommand};
|
||||
use crate::memory_banks::GlyphBankPoolAccess;
|
||||
use prometeu_hal::GfxBridge;
|
||||
use prometeu_hal::color::Color;
|
||||
@ -31,22 +32,11 @@ pub enum BlendMode {
|
||||
|
||||
/// PROMETEU Graphics Subsystem (GFX).
|
||||
///
|
||||
/// Models a specialized graphics chip with a fixed resolution, double buffering,
|
||||
/// and a multi-layered tile/sprite architecture.
|
||||
///
|
||||
/// The GFX system works by composing several "layers" into a single 16-bit
|
||||
/// RGB565 framebuffer. It supports hardware-accelerated primitives (lines, rects)
|
||||
/// and specialized console features like background scrolling and sprite sorting.
|
||||
///
|
||||
/// ### Layer Composition Order (back to front):
|
||||
/// 1. **Priority 0 Sprites**: Objects behind everything else.
|
||||
/// 2. **Tile Layer 0 + Priority 1 Sprites**: Background 0.
|
||||
/// 3. **Tile Layer 1 + Priority 2 Sprites**: Background 1.
|
||||
/// 4. **Tile Layer 2 + Priority 3 Sprites**: Background 2.
|
||||
/// 5. **Tile Layer 3 + Priority 4 Sprites**: Foreground.
|
||||
/// 6. **Scene Fade**: Global brightness/color filter.
|
||||
/// 7. **HUD Layer**: Fixed UI elements (always on top).
|
||||
/// 8. **HUD Fade**: Independent fade for the UI.
|
||||
/// `Gfx` owns the framebuffer backend and the canonical game-frame raster path
|
||||
/// consumed by `FrameComposer`. That canonical path covers scene composition,
|
||||
/// sprite composition, and fades. Public `gfx.*` primitives remain valid, but
|
||||
/// they do not define the canonical game composition contract; they belong to a
|
||||
/// separate final overlay/debug stage.
|
||||
pub struct Gfx {
|
||||
/// Width of the internal framebuffer in pixels.
|
||||
w: usize,
|
||||
@ -54,11 +44,16 @@ pub struct Gfx {
|
||||
h: usize,
|
||||
/// Front buffer: the "VRAM" currently being displayed by the Host window.
|
||||
front: Vec<u16>,
|
||||
/// Back buffer: the "Work RAM" where new frames are composed.
|
||||
/// Back buffer: the working buffer where canonical game frames are composed
|
||||
/// before any final overlay/debug drain.
|
||||
back: Vec<u16>,
|
||||
|
||||
/// Shared access to graphical memory banks (tiles and palettes).
|
||||
pub glyph_banks: Arc<dyn GlyphBankPoolAccess>,
|
||||
/// Deferred overlay/debug capture kept separate from canonical game composition.
|
||||
overlay: DeferredGfxOverlay,
|
||||
/// Internal guard to replay deferred overlay commands without re-enqueueing them.
|
||||
is_draining_overlay: bool,
|
||||
/// Hardware sprite list (512 slots). Equivalent to OAM (Object Attribute Memory).
|
||||
pub sprites: [Sprite; 512],
|
||||
|
||||
@ -71,12 +66,29 @@ pub struct Gfx {
|
||||
/// Target color for the HUD fade effect.
|
||||
pub hud_fade_color: Color,
|
||||
|
||||
/// Internal cache used to sort sprites into priority groups to optimize rendering.
|
||||
priority_buckets: [Vec<usize>; 5],
|
||||
/// Internal sprite count for the current frame state.
|
||||
sprite_count: usize,
|
||||
/// Internal cache used to sort sprites by layer while keeping stable priority order.
|
||||
layer_buckets: [Vec<usize>; 4],
|
||||
}
|
||||
|
||||
const GLYPH_UNKNOWN: [u8; 5] = [0x7, 0x7, 0x7, 0x7, 0x7];
|
||||
|
||||
struct RenderTarget<'a> {
|
||||
back: &'a mut [u16],
|
||||
screen_w: usize,
|
||||
screen_h: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct CachedTileDraw<'a> {
|
||||
x: i32,
|
||||
y: i32,
|
||||
entry: CachedTileEntry,
|
||||
bank: &'a GlyphBank,
|
||||
tile_size: prometeu_hal::glyph_bank::TileSize,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn glyph_for_char(c: char) -> &'static [u8; 5] {
|
||||
match c.to_ascii_uppercase() {
|
||||
@ -207,12 +219,15 @@ impl GfxBridge for Gfx {
|
||||
fn present(&mut self) {
|
||||
self.present()
|
||||
}
|
||||
fn render_all(&mut self) {
|
||||
self.render_all()
|
||||
fn render_no_scene_frame(&mut self) {
|
||||
self.render_no_scene_frame()
|
||||
}
|
||||
fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate) {
|
||||
self.render_scene_from_cache(cache, update)
|
||||
}
|
||||
fn load_frame_sprites(&mut self, sprites: &[Sprite]) {
|
||||
self.load_frame_sprites(sprites)
|
||||
}
|
||||
fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) {
|
||||
self.draw_text(x, y, text, color)
|
||||
}
|
||||
@ -224,6 +239,7 @@ impl GfxBridge for Gfx {
|
||||
&self.sprites[index]
|
||||
}
|
||||
fn sprite_mut(&mut self, index: usize) -> &mut Sprite {
|
||||
self.sprite_count = self.sprite_count.max(index.saturating_add(1)).min(self.sprites.len());
|
||||
&mut self.sprites[index]
|
||||
}
|
||||
|
||||
@ -262,6 +278,7 @@ impl Gfx {
|
||||
glyph: EMPTY_GLYPH,
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 0,
|
||||
bank_id: 0,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
@ -276,13 +293,15 @@ impl Gfx {
|
||||
front: vec![0; len],
|
||||
back: vec![0; len],
|
||||
glyph_banks,
|
||||
overlay: DeferredGfxOverlay::default(),
|
||||
is_draining_overlay: false,
|
||||
sprites: [EMPTY_SPRITE; 512],
|
||||
sprite_count: 0,
|
||||
scene_fade_level: 31,
|
||||
scene_fade_color: Color::BLACK,
|
||||
hud_fade_level: 31,
|
||||
hud_fade_color: Color::BLACK,
|
||||
priority_buckets: [
|
||||
Vec::with_capacity(128),
|
||||
layer_buckets: [
|
||||
Vec::with_capacity(128),
|
||||
Vec::with_capacity(128),
|
||||
Vec::with_capacity(128),
|
||||
@ -295,6 +314,42 @@ impl Gfx {
|
||||
(self.w, self.h)
|
||||
}
|
||||
|
||||
pub fn begin_overlay_frame(&mut self) {
|
||||
self.overlay.begin_frame();
|
||||
}
|
||||
|
||||
pub fn overlay(&self) -> &DeferredGfxOverlay {
|
||||
&self.overlay
|
||||
}
|
||||
|
||||
pub fn drain_overlay_debug(&mut self) {
|
||||
let commands = self.overlay.take_commands();
|
||||
self.is_draining_overlay = true;
|
||||
|
||||
for command in commands {
|
||||
match command {
|
||||
OverlayCommand::FillRectBlend { x, y, w, h, color, mode } => {
|
||||
self.fill_rect_blend(x, y, w, h, color, mode)
|
||||
}
|
||||
OverlayCommand::DrawLine { x0, y0, x1, y1, color } => {
|
||||
self.draw_line(x0, y0, x1, y1, color)
|
||||
}
|
||||
OverlayCommand::DrawCircle { x, y, r, color } => self.draw_circle(x, y, r, color),
|
||||
OverlayCommand::DrawDisc { x, y, r, border_color, fill_color } => {
|
||||
self.draw_disc(x, y, r, border_color, fill_color)
|
||||
}
|
||||
OverlayCommand::DrawSquare { x, y, w, h, border_color, fill_color } => {
|
||||
self.draw_square(x, y, w, h, border_color, fill_color)
|
||||
}
|
||||
OverlayCommand::DrawText { x, y, text, color } => {
|
||||
self.draw_text(x, y, &text, color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.is_draining_overlay = false;
|
||||
}
|
||||
|
||||
/// The buffer that the host should display (RGB565).
|
||||
pub fn front_buffer(&self) -> &[u16] {
|
||||
&self.front
|
||||
@ -314,6 +369,10 @@ impl Gfx {
|
||||
color: Color,
|
||||
mode: BlendMode,
|
||||
) {
|
||||
if !self.is_draining_overlay {
|
||||
self.overlay.push(OverlayCommand::FillRectBlend { x, y, w, h, color, mode });
|
||||
return;
|
||||
}
|
||||
if color == Color::COLOR_KEY {
|
||||
return;
|
||||
}
|
||||
@ -355,6 +414,10 @@ impl Gfx {
|
||||
|
||||
/// Draws a line between two points using Bresenham's algorithm.
|
||||
pub fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) {
|
||||
if !self.is_draining_overlay {
|
||||
self.overlay.push(OverlayCommand::DrawLine { x0, y0, x1, y1, color });
|
||||
return;
|
||||
}
|
||||
if color == Color::COLOR_KEY {
|
||||
return;
|
||||
}
|
||||
@ -387,6 +450,10 @@ impl Gfx {
|
||||
|
||||
/// Draws a circle outline using Midpoint Circle Algorithm.
|
||||
pub fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
|
||||
if !self.is_draining_overlay {
|
||||
self.overlay.push(OverlayCommand::DrawCircle { x: xc, y: yc, r, color });
|
||||
return;
|
||||
}
|
||||
if color == Color::COLOR_KEY {
|
||||
return;
|
||||
}
|
||||
@ -455,6 +522,10 @@ impl Gfx {
|
||||
|
||||
/// Draws a disc (filled circle with border).
|
||||
pub fn draw_disc(&mut self, x: i32, y: i32, r: i32, border_color: Color, fill_color: Color) {
|
||||
if !self.is_draining_overlay {
|
||||
self.overlay.push(OverlayCommand::DrawDisc { x, y, r, border_color, fill_color });
|
||||
return;
|
||||
}
|
||||
self.fill_circle(x, y, r, fill_color);
|
||||
self.draw_circle(x, y, r, border_color);
|
||||
}
|
||||
@ -484,6 +555,10 @@ impl Gfx {
|
||||
border_color: Color,
|
||||
fill_color: Color,
|
||||
) {
|
||||
if !self.is_draining_overlay {
|
||||
self.overlay.push(OverlayCommand::DrawSquare { x, y, w, h, border_color, fill_color });
|
||||
return;
|
||||
}
|
||||
self.fill_rect(x, y, w, h, fill_color);
|
||||
self.draw_rect(x, y, w, h, border_color);
|
||||
}
|
||||
@ -530,23 +605,33 @@ impl Gfx {
|
||||
std::mem::swap(&mut self.front, &mut self.back);
|
||||
}
|
||||
|
||||
pub fn load_frame_sprites(&mut self, sprites: &[Sprite]) {
|
||||
self.sprite_count = sprites.len().min(self.sprites.len());
|
||||
for (index, sprite) in sprites.iter().copied().take(self.sprites.len()).enumerate() {
|
||||
self.sprites[index] = Sprite { active: true, ..sprite };
|
||||
}
|
||||
for sprite in self.sprites.iter_mut().skip(self.sprite_count) {
|
||||
sprite.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// The main rendering pipeline.
|
||||
///
|
||||
/// This method composes the final frame by rasterizing layers and sprites in the
|
||||
/// correct priority order into the back buffer.
|
||||
/// Follows the hardware model where layers and sprites are composed every frame.
|
||||
pub fn render_all(&mut self) {
|
||||
self.populate_priority_buckets();
|
||||
|
||||
// 1. Priority 0 sprites: drawn at the very back, behind everything else.
|
||||
Self::draw_bucket_on_buffer(
|
||||
&mut self.back,
|
||||
self.w,
|
||||
self.h,
|
||||
&self.priority_buckets[0],
|
||||
&self.sprites,
|
||||
&*self.glyph_banks,
|
||||
);
|
||||
pub fn render_no_scene_frame(&mut self) {
|
||||
self.populate_layer_buckets();
|
||||
for bucket in &self.layer_buckets {
|
||||
Self::draw_bucket_on_buffer(
|
||||
&mut self.back,
|
||||
self.w,
|
||||
self.h,
|
||||
bucket,
|
||||
&self.sprites,
|
||||
&*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.
|
||||
@ -563,18 +648,17 @@ impl Gfx {
|
||||
/// plus sprite state and fade controls.
|
||||
pub fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate) {
|
||||
self.back.fill(Color::BLACK.raw());
|
||||
self.populate_priority_buckets();
|
||||
|
||||
Self::draw_bucket_on_buffer(
|
||||
&mut self.back,
|
||||
self.w,
|
||||
self.h,
|
||||
&self.priority_buckets[0],
|
||||
&self.sprites,
|
||||
&*self.glyph_banks,
|
||||
);
|
||||
self.populate_layer_buckets();
|
||||
|
||||
for layer_index in 0..cache.layers.len() {
|
||||
Self::draw_bucket_on_buffer(
|
||||
&mut self.back,
|
||||
self.w,
|
||||
self.h,
|
||||
&self.layer_buckets[layer_index],
|
||||
&self.sprites,
|
||||
&*self.glyph_banks,
|
||||
);
|
||||
Self::draw_cache_layer_to_buffer(
|
||||
&mut self.back,
|
||||
self.w,
|
||||
@ -583,31 +667,26 @@ impl Gfx {
|
||||
&update.copy_requests[layer_index],
|
||||
&*self.glyph_banks,
|
||||
);
|
||||
|
||||
Self::draw_bucket_on_buffer(
|
||||
&mut self.back,
|
||||
self.w,
|
||||
self.h,
|
||||
&self.priority_buckets[layer_index + 1],
|
||||
&self.sprites,
|
||||
&*self.glyph_banks,
|
||||
);
|
||||
}
|
||||
|
||||
Self::apply_fade_to_buffer(&mut self.back, self.scene_fade_level, self.scene_fade_color);
|
||||
Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color);
|
||||
}
|
||||
|
||||
fn populate_priority_buckets(&mut self) {
|
||||
for bucket in self.priority_buckets.iter_mut() {
|
||||
fn populate_layer_buckets(&mut self) {
|
||||
for bucket in self.layer_buckets.iter_mut() {
|
||||
bucket.clear();
|
||||
}
|
||||
|
||||
for (idx, sprite) in self.sprites.iter().enumerate() {
|
||||
if sprite.active && sprite.priority < 5 {
|
||||
self.priority_buckets[sprite.priority as usize].push(idx);
|
||||
for (idx, sprite) in self.sprites.iter().take(self.sprite_count).enumerate() {
|
||||
if sprite.active && (sprite.layer as usize) < self.layer_buckets.len() {
|
||||
self.layer_buckets[sprite.layer as usize].push(idx);
|
||||
}
|
||||
}
|
||||
|
||||
for bucket in self.layer_buckets.iter_mut() {
|
||||
bucket.sort_by_key(|&idx| self.sprites[idx].priority);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_cache_layer_to_buffer(
|
||||
@ -618,6 +697,7 @@ impl Gfx {
|
||||
request: &LayerCopyRequest,
|
||||
glyph_banks: &dyn GlyphBankPoolAccess,
|
||||
) {
|
||||
let mut target = RenderTarget { back, screen_w, screen_h };
|
||||
let layer_cache = &cache.layers[request.layer_index];
|
||||
if !layer_cache.valid {
|
||||
return;
|
||||
@ -646,52 +726,43 @@ impl Gfx {
|
||||
}
|
||||
|
||||
Self::draw_cached_tile_pixels(
|
||||
back,
|
||||
screen_w,
|
||||
screen_h,
|
||||
screen_tile_x,
|
||||
screen_tile_y,
|
||||
entry,
|
||||
&bank,
|
||||
request.tile_size,
|
||||
&mut target,
|
||||
CachedTileDraw {
|
||||
x: screen_tile_x,
|
||||
y: screen_tile_y,
|
||||
entry,
|
||||
bank: &bank,
|
||||
tile_size: request.tile_size,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_cached_tile_pixels(
|
||||
back: &mut [u16],
|
||||
screen_w: usize,
|
||||
screen_h: usize,
|
||||
x: i32,
|
||||
y: i32,
|
||||
entry: CachedTileEntry,
|
||||
bank: &GlyphBank,
|
||||
tile_size: prometeu_hal::glyph_bank::TileSize,
|
||||
) {
|
||||
let size = tile_size as usize;
|
||||
fn draw_cached_tile_pixels(target: &mut RenderTarget<'_>, tile: CachedTileDraw<'_>) {
|
||||
let size = tile.tile_size as usize;
|
||||
|
||||
for local_y in 0..size {
|
||||
let world_y = y + local_y as i32;
|
||||
if world_y < 0 || world_y >= screen_h as i32 {
|
||||
let world_y = tile.y + local_y as i32;
|
||||
if world_y < 0 || world_y >= target.screen_h as i32 {
|
||||
continue;
|
||||
}
|
||||
|
||||
for local_x in 0..size {
|
||||
let world_x = x + local_x as i32;
|
||||
if world_x < 0 || world_x >= screen_w as i32 {
|
||||
let world_x = tile.x + local_x as i32;
|
||||
if world_x < 0 || world_x >= target.screen_w as i32 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let fetch_x = if entry.flip_x() { size - 1 - local_x } else { local_x };
|
||||
let fetch_y = if entry.flip_y() { size - 1 - local_y } else { local_y };
|
||||
let px_index = bank.get_pixel_index(entry.glyph_id, fetch_x, fetch_y);
|
||||
let fetch_x = if tile.entry.flip_x() { size - 1 - local_x } else { local_x };
|
||||
let fetch_y = if tile.entry.flip_y() { size - 1 - local_y } else { local_y };
|
||||
let px_index = tile.bank.get_pixel_index(tile.entry.glyph_id, fetch_x, fetch_y);
|
||||
if px_index == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let color = bank.resolve_color(entry.palette_id, px_index);
|
||||
back[world_y as usize * screen_w + world_x as usize] = color.raw();
|
||||
let color = tile.bank.resolve_color(tile.entry.palette_id, px_index);
|
||||
target.back[world_y as usize * target.screen_w + world_x as usize] = color.raw();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -775,6 +846,10 @@ impl Gfx {
|
||||
}
|
||||
|
||||
pub fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) {
|
||||
if !self.is_draining_overlay {
|
||||
self.overlay.push(OverlayCommand::DrawText { x, y, text: text.to_string(), color });
|
||||
return;
|
||||
}
|
||||
let mut cx = x;
|
||||
for c in text.chars() {
|
||||
self.draw_char(cx, y, c, color);
|
||||
@ -819,10 +894,11 @@ impl Gfx {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::memory_banks::{GlyphBankPoolInstaller, MemoryBanks};
|
||||
use crate::FrameComposer;
|
||||
use crate::memory_banks::{GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess};
|
||||
use prometeu_hal::glyph_bank::TileSize;
|
||||
use prometeu_hal::scene_bank::SceneBank;
|
||||
use prometeu_hal::scene_layer::{MotionFactor, SceneLayer};
|
||||
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
||||
use prometeu_hal::scene_viewport_cache::SceneViewportCache;
|
||||
use prometeu_hal::scene_viewport_resolver::SceneViewportResolver;
|
||||
use prometeu_hal::tile::Tile;
|
||||
@ -853,7 +929,7 @@ mod tests {
|
||||
active: true,
|
||||
glyph_bank_id,
|
||||
tile_size: TileSize::Size8,
|
||||
motion_factor: MotionFactor { x: 1.0, y: 1.0 },
|
||||
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
|
||||
tilemap: TileMap {
|
||||
width,
|
||||
height,
|
||||
@ -875,7 +951,7 @@ mod tests {
|
||||
active: false,
|
||||
glyph_bank_id,
|
||||
tile_size: TileSize::Size8,
|
||||
motion_factor: MotionFactor { x: 1.0, y: 1.0 },
|
||||
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
|
||||
tilemap: TileMap { width, height, tiles: vec![Tile::default(); width * height] },
|
||||
}
|
||||
}
|
||||
@ -918,9 +994,12 @@ mod tests {
|
||||
fn test_draw_line() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
let mut gfx = Gfx::new(10, 10, banks);
|
||||
gfx.begin_overlay_frame();
|
||||
gfx.draw_line(0, 0, 9, 9, Color::WHITE);
|
||||
gfx.drain_overlay_debug();
|
||||
assert_eq!(gfx.back[0], Color::WHITE.0);
|
||||
assert_eq!(gfx.back[9 * 10 + 9], Color::WHITE.0);
|
||||
assert_eq!(gfx.overlay().command_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -946,11 +1025,54 @@ mod tests {
|
||||
fn test_draw_square() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
let mut gfx = Gfx::new(10, 10, banks);
|
||||
gfx.begin_overlay_frame();
|
||||
gfx.draw_square(2, 2, 6, 6, Color::WHITE, Color::BLACK);
|
||||
gfx.drain_overlay_debug();
|
||||
// Border
|
||||
assert_eq!(gfx.back[2 * 10 + 2], Color::WHITE.0);
|
||||
// Fill
|
||||
assert_eq!(gfx.back[3 * 10 + 3], Color::BLACK.0);
|
||||
assert_eq!(gfx.overlay().command_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw_text_captures_overlay_command() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
let mut gfx = Gfx::new(32, 18, banks);
|
||||
gfx.begin_overlay_frame();
|
||||
|
||||
gfx.draw_text(4, 5, "HUD", Color::WHITE);
|
||||
|
||||
assert_eq!(
|
||||
gfx.overlay().commands(),
|
||||
&[OverlayCommand::DrawText { x: 4, y: 5, text: "HUD".into(), color: Color::WHITE }]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlay_state_is_separate_from_frame_composer_sprite_state() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
let mut gfx = Gfx::new(32, 18, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
|
||||
let mut frame_composer =
|
||||
FrameComposer::new(32, 18, Arc::clone(&banks) as Arc<dyn SceneBankPoolAccess>);
|
||||
|
||||
gfx.begin_overlay_frame();
|
||||
frame_composer.begin_frame();
|
||||
frame_composer.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id: 0, palette_id: 0 },
|
||||
x: 1,
|
||||
y: 2,
|
||||
layer: 0,
|
||||
bank_id: 0,
|
||||
active: true,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 0,
|
||||
});
|
||||
gfx.draw_text(1, 1, "X", Color::WHITE);
|
||||
|
||||
assert_eq!(frame_composer.sprite_controller().sprite_count(), 1);
|
||||
assert_eq!(gfx.overlay().command_count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1004,6 +1126,7 @@ mod tests {
|
||||
glyph: Glyph { glyph_id: 0, palette_id: 4 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 0,
|
||||
bank_id: 0,
|
||||
active: true,
|
||||
flip_x: false,
|
||||
@ -1014,17 +1137,57 @@ mod tests {
|
||||
glyph: Glyph { glyph_id: 0, palette_id: 4 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 2,
|
||||
bank_id: 0,
|
||||
active: true,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 2,
|
||||
};
|
||||
gfx.sprite_count = 2;
|
||||
|
||||
gfx.render_scene_from_cache(&cache, &update);
|
||||
|
||||
assert_eq!(gfx.back[0], Color::BLUE.raw());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_frame_sprites_replaces_slot_first_submission_for_render_state() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
let mut gfx = Gfx::new(16, 16, banks as Arc<dyn GlyphBankPoolAccess>);
|
||||
|
||||
gfx.load_frame_sprites(&[
|
||||
Sprite {
|
||||
glyph: Glyph { glyph_id: 1, palette_id: 2 },
|
||||
x: 2,
|
||||
y: 3,
|
||||
layer: 1,
|
||||
bank_id: 4,
|
||||
active: false,
|
||||
flip_x: true,
|
||||
flip_y: false,
|
||||
priority: 7,
|
||||
},
|
||||
Sprite {
|
||||
glyph: Glyph { glyph_id: 5, palette_id: 6 },
|
||||
x: 7,
|
||||
y: 8,
|
||||
layer: 3,
|
||||
bank_id: 9,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: true,
|
||||
priority: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(gfx.sprite_count, 2);
|
||||
assert!(gfx.sprites[0].active);
|
||||
assert!(gfx.sprites[1].active);
|
||||
assert!(!gfx.sprites[2].active);
|
||||
assert_eq!(gfx.sprites[0].layer, 1);
|
||||
assert_eq!(gfx.sprites[1].glyph.glyph_id, 5);
|
||||
}
|
||||
}
|
||||
|
||||
/// Blends in RGB565 per channel with saturation.
|
||||
|
||||
39
crates/console/prometeu-drivers/src/gfx_overlay.rs
Normal file
39
crates/console/prometeu-drivers/src/gfx_overlay.rs
Normal file
@ -0,0 +1,39 @@
|
||||
use crate::gfx::BlendMode;
|
||||
use prometeu_hal::color::Color;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum OverlayCommand {
|
||||
FillRectBlend { x: i32, y: i32, w: i32, h: i32, color: Color, mode: BlendMode },
|
||||
DrawLine { x0: i32, y0: i32, x1: i32, y1: i32, color: Color },
|
||||
DrawCircle { x: i32, y: i32, r: i32, color: Color },
|
||||
DrawDisc { x: i32, y: i32, r: i32, border_color: Color, fill_color: Color },
|
||||
DrawSquare { x: i32, y: i32, w: i32, h: i32, border_color: Color, fill_color: Color },
|
||||
DrawText { x: i32, y: i32, text: String, color: Color },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DeferredGfxOverlay {
|
||||
commands: Vec<OverlayCommand>,
|
||||
}
|
||||
|
||||
impl DeferredGfxOverlay {
|
||||
pub fn begin_frame(&mut self) {
|
||||
self.commands.clear();
|
||||
}
|
||||
|
||||
pub fn push(&mut self, command: OverlayCommand) {
|
||||
self.commands.push(command);
|
||||
}
|
||||
|
||||
pub fn commands(&self) -> &[OverlayCommand] {
|
||||
&self.commands
|
||||
}
|
||||
|
||||
pub fn command_count(&self) -> usize {
|
||||
self.commands.len()
|
||||
}
|
||||
|
||||
pub fn take_commands(&mut self) -> Vec<OverlayCommand> {
|
||||
std::mem::take(&mut self.commands)
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,15 @@
|
||||
use crate::asset::AssetManager;
|
||||
use crate::audio::Audio;
|
||||
use crate::frame_composer::FrameComposer;
|
||||
use crate::gfx::Gfx;
|
||||
use crate::memory_banks::{
|
||||
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller,
|
||||
SoundBankPoolAccess, SoundBankPoolInstaller,
|
||||
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess,
|
||||
SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller,
|
||||
};
|
||||
use crate::pad::Pad;
|
||||
use crate::touch::Touch;
|
||||
use prometeu_hal::cartridge::AssetsPayloadSource;
|
||||
use prometeu_hal::sprite::Sprite;
|
||||
use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge};
|
||||
use std::sync::Arc;
|
||||
|
||||
@ -26,6 +28,8 @@ use std::sync::Arc;
|
||||
pub struct Hardware {
|
||||
/// The Graphics Processing Unit (GPU). Handles drawing primitives, sprites, and tilemaps.
|
||||
pub gfx: Gfx,
|
||||
/// Canonical frame orchestration owner for scene/camera/cache/resolver/sprites.
|
||||
pub frame_composer: FrameComposer,
|
||||
/// The Sound Processing Unit (SPU). Manages sample playback and volume control.
|
||||
pub audio: Audio,
|
||||
/// The standard digital gamepad. Provides state for D-Pad, face buttons, and triggers.
|
||||
@ -43,6 +47,36 @@ impl Default for Hardware {
|
||||
}
|
||||
|
||||
impl HardwareBridge for Hardware {
|
||||
fn begin_frame(&mut self) {
|
||||
self.gfx.begin_overlay_frame();
|
||||
self.frame_composer.begin_frame();
|
||||
}
|
||||
|
||||
fn bind_scene(&mut self, scene_bank_id: usize) -> bool {
|
||||
self.frame_composer.bind_scene(scene_bank_id)
|
||||
}
|
||||
|
||||
fn unbind_scene(&mut self) {
|
||||
self.frame_composer.unbind_scene();
|
||||
}
|
||||
|
||||
fn set_camera(&mut self, x: i32, y: i32) {
|
||||
self.frame_composer.set_camera(x, y);
|
||||
}
|
||||
|
||||
fn emit_sprite(&mut self, sprite: Sprite) -> bool {
|
||||
self.frame_composer.emit_sprite(sprite)
|
||||
}
|
||||
|
||||
fn render_frame(&mut self) {
|
||||
self.frame_composer.render_frame(&mut self.gfx);
|
||||
self.gfx.drain_overlay_debug();
|
||||
}
|
||||
|
||||
fn has_glyph_bank(&self, bank_id: usize) -> bool {
|
||||
self.gfx.glyph_banks.glyph_bank_slot(bank_id).is_some()
|
||||
}
|
||||
|
||||
fn gfx(&self) -> &dyn GfxBridge {
|
||||
&self.gfx
|
||||
}
|
||||
@ -98,6 +132,11 @@ impl Hardware {
|
||||
Self::H,
|
||||
Arc::clone(&memory_banks) as Arc<dyn GlyphBankPoolAccess>,
|
||||
),
|
||||
frame_composer: FrameComposer::new(
|
||||
Self::W,
|
||||
Self::H,
|
||||
Arc::clone(&memory_banks) as Arc<dyn SceneBankPoolAccess>,
|
||||
),
|
||||
audio: Audio::new(Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolAccess>),
|
||||
pad: Pad::default(),
|
||||
touch: Touch::default(),
|
||||
@ -122,7 +161,7 @@ mod tests {
|
||||
use prometeu_hal::glyph::Glyph;
|
||||
use prometeu_hal::glyph_bank::{GlyphBank, TileSize};
|
||||
use prometeu_hal::scene_bank::SceneBank;
|
||||
use prometeu_hal::scene_layer::{MotionFactor, SceneLayer};
|
||||
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
||||
use prometeu_hal::scene_viewport_cache::SceneViewportCache;
|
||||
use prometeu_hal::scene_viewport_resolver::SceneViewportResolver;
|
||||
use prometeu_hal::tile::Tile;
|
||||
@ -142,7 +181,7 @@ mod tests {
|
||||
active: true,
|
||||
glyph_bank_id: 0,
|
||||
tile_size: TileSize::Size8,
|
||||
motion_factor: MotionFactor { x: 1.0, y: 1.0 },
|
||||
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
|
||||
tilemap: TileMap {
|
||||
width: 4,
|
||||
height: 4,
|
||||
@ -182,4 +221,20 @@ mod tests {
|
||||
|
||||
assert_eq!(hardware.gfx.front_buffer()[0], Color::RED.raw());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hardware_constructs_frame_composer_with_shared_scene_bank_access() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
banks.install_scene_bank(2, Arc::new(make_scene()));
|
||||
|
||||
let hardware = Hardware::new_with_memory_banks(banks);
|
||||
let scene = hardware
|
||||
.frame_composer
|
||||
.scene_bank_slot(2)
|
||||
.expect("scene bank slot 2 should be resident");
|
||||
|
||||
assert_eq!(hardware.frame_composer.viewport_size(), (Hardware::W, Hardware::H));
|
||||
assert_eq!(hardware.frame_composer.scene_bank_slot_count(), 16);
|
||||
assert_eq!(scene.layers[0].tile_size, TileSize::Size8);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
mod asset;
|
||||
mod audio;
|
||||
mod frame_composer;
|
||||
mod gfx;
|
||||
mod gfx_overlay;
|
||||
pub mod hardware;
|
||||
mod memory_banks;
|
||||
mod pad;
|
||||
@ -8,7 +10,9 @@ mod touch;
|
||||
|
||||
pub use crate::asset::AssetManager;
|
||||
pub use crate::audio::{Audio, AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};
|
||||
pub use crate::frame_composer::{FrameComposer, SceneStatus, SpriteController};
|
||||
pub use crate::gfx::Gfx;
|
||||
pub use crate::gfx_overlay::{DeferredGfxOverlay, OverlayCommand};
|
||||
pub use crate::memory_banks::{
|
||||
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess,
|
||||
SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller,
|
||||
|
||||
10
crates/console/prometeu-hal/src/composer_status.rs
Normal file
10
crates/console/prometeu-hal/src/composer_status.rs
Normal file
@ -0,0 +1,10 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum ComposerOpStatus {
|
||||
Ok = 0,
|
||||
SceneUnavailable = 1,
|
||||
ArgRangeInvalid = 2,
|
||||
BankInvalid = 3,
|
||||
LayerInvalid = 4,
|
||||
SpriteOverflow = 5,
|
||||
}
|
||||
@ -48,8 +48,21 @@ pub trait GfxBridge {
|
||||
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color);
|
||||
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color);
|
||||
fn present(&mut self);
|
||||
fn render_all(&mut self);
|
||||
/// Render the canonical game frame with no bound scene.
|
||||
///
|
||||
/// Deferred `gfx.*` overlay/debug primitives are intentionally outside this
|
||||
/// contract and are drained by a separate final overlay stage.
|
||||
fn render_no_scene_frame(&mut self);
|
||||
/// Render the canonical scene-backed game frame from cache/resolver state.
|
||||
///
|
||||
/// Deferred `gfx.*` overlay/debug primitives are intentionally outside this
|
||||
/// contract and are drained by a separate final overlay stage.
|
||||
fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate);
|
||||
fn load_frame_sprites(&mut self, sprites: &[Sprite]);
|
||||
/// Submit text into the `gfx.*` primitive path.
|
||||
///
|
||||
/// Under the accepted runtime contract this is not the canonical game
|
||||
/// composition path; it belongs to the deferred final overlay/debug family.
|
||||
fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color);
|
||||
fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color);
|
||||
|
||||
|
||||
@ -2,9 +2,18 @@ use crate::asset_bridge::AssetBridge;
|
||||
use crate::audio_bridge::AudioBridge;
|
||||
use crate::gfx_bridge::GfxBridge;
|
||||
use crate::pad_bridge::PadBridge;
|
||||
use crate::sprite::Sprite;
|
||||
use crate::touch_bridge::TouchBridge;
|
||||
|
||||
pub trait HardwareBridge {
|
||||
fn begin_frame(&mut self);
|
||||
fn bind_scene(&mut self, scene_bank_id: usize) -> bool;
|
||||
fn unbind_scene(&mut self);
|
||||
fn set_camera(&mut self, x: i32, y: i32);
|
||||
fn emit_sprite(&mut self, sprite: Sprite) -> bool;
|
||||
fn render_frame(&mut self);
|
||||
fn has_glyph_bank(&self, bank_id: usize) -> bool;
|
||||
|
||||
fn gfx(&self) -> &dyn GfxBridge;
|
||||
fn gfx_mut(&mut self) -> &mut dyn GfxBridge;
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ pub mod button;
|
||||
pub mod cartridge;
|
||||
pub mod cartridge_loader;
|
||||
pub mod color;
|
||||
pub mod composer_status;
|
||||
pub mod debugger_protocol;
|
||||
pub mod gfx_bridge;
|
||||
pub mod glyph;
|
||||
@ -34,6 +35,7 @@ pub mod window;
|
||||
|
||||
pub use asset_bridge::AssetBridge;
|
||||
pub use audio_bridge::{AudioBridge, AudioOpStatus, LoopMode};
|
||||
pub use composer_status::ComposerOpStatus;
|
||||
pub use gfx_bridge::{BlendMode, GfxBridge, GfxOpStatus};
|
||||
pub use hardware_bridge::HardwareBridge;
|
||||
pub use host_context::{HostContext, HostContextProvider};
|
||||
|
||||
@ -10,16 +10,16 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::glyph_bank::TileSize;
|
||||
use crate::scene_layer::MotionFactor;
|
||||
use crate::scene_layer::ParallaxFactor;
|
||||
use crate::tile::Tile;
|
||||
use crate::tilemap::TileMap;
|
||||
|
||||
fn layer(glyph_bank_id: u8, motion_x: f32, motion_y: f32, glyph_id: u16) -> SceneLayer {
|
||||
fn layer(glyph_bank_id: u8, parallax_x: f32, parallax_y: f32, glyph_id: u16) -> SceneLayer {
|
||||
SceneLayer {
|
||||
active: true,
|
||||
glyph_bank_id,
|
||||
tile_size: TileSize::Size16,
|
||||
motion_factor: MotionFactor { x: motion_x, y: motion_y },
|
||||
parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_y },
|
||||
tilemap: TileMap {
|
||||
width: 1,
|
||||
height: 1,
|
||||
|
||||
@ -2,7 +2,7 @@ use crate::glyph_bank::TileSize;
|
||||
use crate::tilemap::TileMap;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct MotionFactor {
|
||||
pub struct ParallaxFactor {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
@ -12,7 +12,7 @@ pub struct SceneLayer {
|
||||
pub active: bool,
|
||||
pub glyph_bank_id: u8,
|
||||
pub tile_size: TileSize,
|
||||
pub motion_factor: MotionFactor,
|
||||
pub parallax_factor: ParallaxFactor,
|
||||
pub tilemap: TileMap,
|
||||
}
|
||||
|
||||
@ -23,12 +23,12 @@ mod tests {
|
||||
use crate::tile::Tile;
|
||||
|
||||
#[test]
|
||||
fn scene_layer_preserves_motion_factor_and_tilemap_ownership() {
|
||||
fn scene_layer_preserves_parallax_factor_and_tilemap_ownership() {
|
||||
let layer = SceneLayer {
|
||||
active: true,
|
||||
glyph_bank_id: 7,
|
||||
tile_size: TileSize::Size16,
|
||||
motion_factor: MotionFactor { x: 0.5, y: 0.75 },
|
||||
parallax_factor: ParallaxFactor { x: 0.5, y: 0.75 },
|
||||
tilemap: TileMap {
|
||||
width: 2,
|
||||
height: 1,
|
||||
@ -50,8 +50,8 @@ mod tests {
|
||||
};
|
||||
|
||||
assert_eq!(layer.glyph_bank_id, 7);
|
||||
assert_eq!(layer.motion_factor.x, 0.5);
|
||||
assert_eq!(layer.motion_factor.y, 0.75);
|
||||
assert_eq!(layer.parallax_factor.x, 0.5);
|
||||
assert_eq!(layer.parallax_factor.y, 0.75);
|
||||
assert_eq!(layer.tilemap.width, 2);
|
||||
assert_eq!(layer.tilemap.tiles[1].glyph.glyph_id, 22);
|
||||
assert!(layer.tilemap.tiles[1].flip_x);
|
||||
|
||||
@ -270,7 +270,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::glyph_bank::TileSize;
|
||||
use crate::scene_layer::MotionFactor;
|
||||
use crate::scene_layer::ParallaxFactor;
|
||||
use crate::tile::Tile;
|
||||
use crate::tilemap::TileMap;
|
||||
|
||||
@ -295,7 +295,7 @@ mod tests {
|
||||
active: true,
|
||||
glyph_bank_id,
|
||||
tile_size: TileSize::Size16,
|
||||
motion_factor: MotionFactor { x: 1.0, y: 1.0 },
|
||||
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
|
||||
tilemap: TileMap { width: 4, height: 4, tiles },
|
||||
}
|
||||
}
|
||||
@ -325,6 +325,34 @@ mod tests {
|
||||
assert_eq!(cache.layers[0].ring_origin(), (1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layer_cache_wraps_ring_origin_for_negative_and_large_movements() {
|
||||
let scene = make_scene();
|
||||
let mut cache = SceneViewportCache::new(&scene, 3, 3);
|
||||
|
||||
cache.move_layer_window_by(0, -1, -4);
|
||||
assert_eq!(cache.layers[0].logical_origin(), (-1, -4));
|
||||
assert_eq!(cache.layers[0].ring_origin(), (2, 2));
|
||||
|
||||
cache.move_layer_window_by(0, 7, 8);
|
||||
assert_eq!(cache.layers[0].logical_origin(), (6, 4));
|
||||
assert_eq!(cache.layers[0].ring_origin(), (0, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_window_to_matches_incremental_ring_movement() {
|
||||
let scene = make_scene();
|
||||
let mut direct = SceneViewportCache::new(&scene, 4, 4);
|
||||
let mut incremental = SceneViewportCache::new(&scene, 4, 4);
|
||||
|
||||
direct.move_layer_window_to(0, 9, -6);
|
||||
incremental.move_layer_window_by(0, 5, -2);
|
||||
incremental.move_layer_window_by(0, 4, -4);
|
||||
|
||||
assert_eq!(direct.layers[0].logical_origin(), incremental.layers[0].logical_origin());
|
||||
assert_eq!(direct.layers[0].ring_origin(), incremental.layers[0].ring_origin());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_entry_fields_are_derived_from_scene_tiles() {
|
||||
let scene = make_scene();
|
||||
@ -415,6 +443,113 @@ mod tests {
|
||||
assert_eq!(cache.layers[3].entry(3, 3).glyph_id, 415);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refresh_after_wrapped_window_move_materializes_new_logical_tiles() {
|
||||
let scene = make_scene();
|
||||
let mut cache = SceneViewportCache::new(&scene, 3, 3);
|
||||
|
||||
cache.refresh_layer_all(&scene, 0);
|
||||
cache.move_layer_window_to(0, 1, 2);
|
||||
cache.refresh_layer_all(&scene, 0);
|
||||
|
||||
assert_eq!(cache.layers[0].logical_origin(), (1, 2));
|
||||
assert_eq!(cache.layers[0].ring_origin(), (1, 2));
|
||||
assert_eq!(cache.layers[0].entry(0, 0).glyph_id, 109);
|
||||
assert_eq!(cache.layers[0].entry(1, 0).glyph_id, 110);
|
||||
assert_eq!(cache.layers[0].entry(2, 0).glyph_id, 111);
|
||||
assert_eq!(cache.layers[0].entry(0, 1).glyph_id, 113);
|
||||
assert_eq!(cache.layers[0].entry(2, 1).glyph_id, 115);
|
||||
assert_eq!(cache.layers[0].entry(0, 2), CachedTileEntry::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_refresh_uses_wrapped_physical_slots_after_window_move() {
|
||||
let scene = make_scene();
|
||||
let mut cache = SceneViewportCache::new(&scene, 3, 3);
|
||||
|
||||
cache.move_layer_window_to(0, 1, 0);
|
||||
cache.refresh_layer_column(&scene, 0, 0);
|
||||
|
||||
assert_eq!(cache.layers[0].ring_origin(), (1, 0));
|
||||
assert_eq!(cache.layers[0].entry(0, 0).glyph_id, 101);
|
||||
assert_eq!(cache.layers[0].entry(0, 1).glyph_id, 105);
|
||||
assert_eq!(cache.layers[0].entry(0, 2).glyph_id, 109);
|
||||
assert_eq!(cache.layers[0].entry(1, 0), CachedTileEntry::default());
|
||||
assert_eq!(cache.layers[0].entry(2, 0), CachedTileEntry::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_bounds_logical_origins_materialize_default_entries_after_wrap() {
|
||||
let scene = make_scene();
|
||||
let mut cache = SceneViewportCache::new(&scene, 2, 2);
|
||||
|
||||
cache.move_layer_window_to(0, -2, 3);
|
||||
cache.refresh_layer_all(&scene, 0);
|
||||
|
||||
for y in 0..2 {
|
||||
for x in 0..2 {
|
||||
assert_eq!(cache.layers[0].entry(x, y), CachedTileEntry::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ringbuffer_preserves_logical_tile_mapping_across_long_mixed_movements() {
|
||||
let scene = make_scene();
|
||||
let mut cache = SceneViewportCache::new(&scene, 3, 3);
|
||||
let motions = [
|
||||
(1, 0),
|
||||
(0, 1),
|
||||
(2, 2),
|
||||
(-1, 0),
|
||||
(0, -2),
|
||||
(4, 1),
|
||||
(-3, 3),
|
||||
(5, -4),
|
||||
(-6, 2),
|
||||
(3, -3),
|
||||
(7, 7),
|
||||
(-8, -5),
|
||||
];
|
||||
|
||||
for &(dx, dy) in &motions {
|
||||
cache.move_layer_window_by(0, dx, dy);
|
||||
cache.refresh_layer_all(&scene, 0);
|
||||
|
||||
let (origin_x, origin_y) = cache.layers[0].logical_origin();
|
||||
for cache_y in 0..cache.height() {
|
||||
for cache_x in 0..cache.width() {
|
||||
let expected_scene_x = origin_x + cache_x as i32;
|
||||
let expected_scene_y = origin_y + cache_y as i32;
|
||||
|
||||
let expected = if expected_scene_x < 0
|
||||
|| expected_scene_y < 0
|
||||
|| expected_scene_x as usize >= scene.layers[0].tilemap.width
|
||||
|| expected_scene_y as usize >= scene.layers[0].tilemap.height
|
||||
{
|
||||
CachedTileEntry::default()
|
||||
} else {
|
||||
let tile_x = expected_scene_x as usize;
|
||||
let tile_y = expected_scene_y as usize;
|
||||
let tile = scene.layers[0].tilemap.tiles
|
||||
[tile_y * scene.layers[0].tilemap.width + tile_x];
|
||||
CachedTileEntry::from_tile(&scene.layers[0], tile)
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
cache.layers[0].entry(cache_x, cache_y),
|
||||
expected,
|
||||
"mismatch at logical origin ({}, {}), cache ({}, {})",
|
||||
origin_x,
|
||||
origin_y,
|
||||
cache_x,
|
||||
cache_y
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materialization_populates_all_four_layers() {
|
||||
let scene = make_scene();
|
||||
|
||||
@ -96,8 +96,8 @@ impl SceneViewportResolver {
|
||||
let layer_inputs: [(i32, i32, i32, i32, i32); 4] = std::array::from_fn(|i| {
|
||||
let layer = &scene.layers[i];
|
||||
let tile_size_px = layer.tile_size as i32;
|
||||
let layer_camera_x_px = ((camera_x_px as f32) * layer.motion_factor.x).floor() as i32;
|
||||
let layer_camera_y_px = ((camera_y_px as f32) * layer.motion_factor.y).floor() as i32;
|
||||
let layer_camera_x_px = ((camera_x_px as f32) * layer.parallax_factor.x).floor() as i32;
|
||||
let layer_camera_y_px = ((camera_y_px as f32) * layer.parallax_factor.y).floor() as i32;
|
||||
let layer_center_x_px = layer_camera_x_px + self.viewport_width_px / 2;
|
||||
let layer_center_y_px = layer_camera_y_px + self.viewport_height_px / 2;
|
||||
(
|
||||
@ -388,14 +388,14 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::glyph_bank::TileSize;
|
||||
use crate::scene_layer::{MotionFactor, SceneLayer};
|
||||
use crate::scene_layer::{ParallaxFactor, SceneLayer};
|
||||
use crate::tile::Tile;
|
||||
use crate::tilemap::TileMap;
|
||||
|
||||
fn make_layer(
|
||||
tile_size: TileSize,
|
||||
motion_x: f32,
|
||||
motion_y: f32,
|
||||
parallax_x: f32,
|
||||
parallax_y: f32,
|
||||
width: usize,
|
||||
height: usize,
|
||||
) -> SceneLayer {
|
||||
@ -413,7 +413,7 @@ mod tests {
|
||||
active: true,
|
||||
glyph_bank_id: 1,
|
||||
tile_size,
|
||||
motion_factor: MotionFactor { x: motion_x, y: motion_y },
|
||||
parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_y },
|
||||
tilemap: TileMap { width, height, tiles },
|
||||
}
|
||||
}
|
||||
@ -443,7 +443,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn per_layer_copy_requests_follow_motion_factor() {
|
||||
fn per_layer_copy_requests_follow_parallax_factor() {
|
||||
let scene = make_scene();
|
||||
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ pub struct Sprite {
|
||||
pub glyph: Glyph,
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub layer: u8,
|
||||
pub bank_id: u8,
|
||||
pub active: bool,
|
||||
pub flip_x: bool,
|
||||
|
||||
@ -19,6 +19,7 @@ pub use resolver::{
|
||||
/// Each Syscall has a unique 32-bit ID. The IDs are grouped by category:
|
||||
/// - **0x0xxx**: System & OS Control
|
||||
/// - **0x1xxx**: Graphics (GFX)
|
||||
/// - **0x11xx**: Frame Composer orchestration
|
||||
/// - **0x2xxx**: Reserved for legacy input syscalls (disabled for v1 VM-owned input)
|
||||
/// - **0x3xxx**: Audio (PCM & Mixing)
|
||||
/// - **0x4xxx**: Filesystem (Sandboxed I/O)
|
||||
@ -35,9 +36,12 @@ pub enum Syscall {
|
||||
GfxDrawCircle = 0x1004,
|
||||
GfxDrawDisc = 0x1005,
|
||||
GfxDrawSquare = 0x1006,
|
||||
GfxSetSprite = 0x1007,
|
||||
GfxDrawText = 0x1008,
|
||||
GfxClear565 = 0x1010,
|
||||
ComposerBindScene = 0x1101,
|
||||
ComposerUnbindScene = 0x1102,
|
||||
ComposerSetCamera = 0x1103,
|
||||
ComposerEmitSprite = 0x1104,
|
||||
AudioPlaySample = 0x3001,
|
||||
AudioPlay = 0x3002,
|
||||
FsOpen = 0x4001,
|
||||
|
||||
22
crates/console/prometeu-hal/src/syscalls/domains/composer.rs
Normal file
22
crates/console/prometeu-hal/src/syscalls/domains/composer.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
|
||||
|
||||
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
||||
SyscallRegistryEntry::builder(Syscall::ComposerBindScene, "composer", "bind_scene")
|
||||
.args(1)
|
||||
.rets(1)
|
||||
.caps(caps::GFX)
|
||||
.cost(5),
|
||||
SyscallRegistryEntry::builder(Syscall::ComposerUnbindScene, "composer", "unbind_scene")
|
||||
.rets(1)
|
||||
.caps(caps::GFX)
|
||||
.cost(2),
|
||||
SyscallRegistryEntry::builder(Syscall::ComposerSetCamera, "composer", "set_camera")
|
||||
.args(2)
|
||||
.caps(caps::GFX)
|
||||
.cost(2),
|
||||
SyscallRegistryEntry::builder(Syscall::ComposerEmitSprite, "composer", "emit_sprite")
|
||||
.args(9)
|
||||
.rets(1)
|
||||
.caps(caps::GFX)
|
||||
.cost(5),
|
||||
];
|
||||
@ -25,11 +25,6 @@ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
||||
.args(6)
|
||||
.caps(caps::GFX)
|
||||
.cost(5),
|
||||
SyscallRegistryEntry::builder(Syscall::GfxSetSprite, "gfx", "set_sprite")
|
||||
.args(10)
|
||||
.rets(1)
|
||||
.caps(caps::GFX)
|
||||
.cost(5),
|
||||
SyscallRegistryEntry::builder(Syscall::GfxDrawText, "gfx", "draw_text")
|
||||
.args(4)
|
||||
.caps(caps::GFX)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
mod asset;
|
||||
mod audio;
|
||||
mod bank;
|
||||
mod composer;
|
||||
mod fs;
|
||||
mod gfx;
|
||||
mod log;
|
||||
@ -12,6 +13,7 @@ pub(crate) fn all_entries() -> impl Iterator<Item = &'static SyscallRegistryEntr
|
||||
system::ENTRIES
|
||||
.iter()
|
||||
.chain(gfx::ENTRIES.iter())
|
||||
.chain(composer::ENTRIES.iter())
|
||||
.chain(audio::ENTRIES.iter())
|
||||
.chain(fs::ENTRIES.iter())
|
||||
.chain(log::ENTRIES.iter())
|
||||
|
||||
@ -20,9 +20,12 @@ impl Syscall {
|
||||
0x1004 => Some(Self::GfxDrawCircle),
|
||||
0x1005 => Some(Self::GfxDrawDisc),
|
||||
0x1006 => Some(Self::GfxDrawSquare),
|
||||
0x1007 => Some(Self::GfxSetSprite),
|
||||
0x1008 => Some(Self::GfxDrawText),
|
||||
0x1010 => Some(Self::GfxClear565),
|
||||
0x1101 => Some(Self::ComposerBindScene),
|
||||
0x1102 => Some(Self::ComposerUnbindScene),
|
||||
0x1103 => Some(Self::ComposerSetCamera),
|
||||
0x1104 => Some(Self::ComposerEmitSprite),
|
||||
0x3001 => Some(Self::AudioPlaySample),
|
||||
0x3002 => Some(Self::AudioPlay),
|
||||
0x4001 => Some(Self::FsOpen),
|
||||
@ -68,9 +71,12 @@ impl Syscall {
|
||||
Self::GfxDrawCircle => "GfxDrawCircle",
|
||||
Self::GfxDrawDisc => "GfxDrawDisc",
|
||||
Self::GfxDrawSquare => "GfxDrawSquare",
|
||||
Self::GfxSetSprite => "GfxSetSprite",
|
||||
Self::GfxDrawText => "GfxDrawText",
|
||||
Self::GfxClear565 => "GfxClear565",
|
||||
Self::ComposerBindScene => "ComposerBindScene",
|
||||
Self::ComposerUnbindScene => "ComposerUnbindScene",
|
||||
Self::ComposerSetCamera => "ComposerSetCamera",
|
||||
Self::ComposerEmitSprite => "ComposerEmitSprite",
|
||||
Self::AudioPlaySample => "AudioPlaySample",
|
||||
Self::AudioPlay => "AudioPlay",
|
||||
Self::FsOpen => "FsOpen",
|
||||
|
||||
@ -63,6 +63,18 @@ fn resolver_rejects_unknown_identity() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolver_rejects_removed_legacy_gfx_set_sprite_identity() {
|
||||
assert!(resolve_syscall("gfx", "set_sprite", 1).is_none());
|
||||
|
||||
let requested = [SyscallIdentity { module: "gfx", name: "set_sprite", version: 1 }];
|
||||
let err = resolve_program_syscalls(&requested, caps::ALL).unwrap_err();
|
||||
assert_eq!(
|
||||
err,
|
||||
LoadError::UnknownSyscall { module: "gfx".into(), name: "set_sprite".into(), version: 1 }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolver_enforces_capabilities() {
|
||||
let requested = [SyscallIdentity { module: "gfx", name: "clear", version: 1 }];
|
||||
@ -194,10 +206,6 @@ fn status_first_syscall_signatures_are_pinned() {
|
||||
assert_eq!(draw_square.arg_slots, 6);
|
||||
assert_eq!(draw_square.ret_slots, 0);
|
||||
|
||||
let set_sprite = meta_for(Syscall::GfxSetSprite);
|
||||
assert_eq!(set_sprite.arg_slots, 10);
|
||||
assert_eq!(set_sprite.ret_slots, 1);
|
||||
|
||||
let draw_text = meta_for(Syscall::GfxDrawText);
|
||||
assert_eq!(draw_text.arg_slots, 4);
|
||||
assert_eq!(draw_text.ret_slots, 0);
|
||||
@ -206,6 +214,22 @@ fn status_first_syscall_signatures_are_pinned() {
|
||||
assert_eq!(clear_565.arg_slots, 1);
|
||||
assert_eq!(clear_565.ret_slots, 0);
|
||||
|
||||
let bind_scene = meta_for(Syscall::ComposerBindScene);
|
||||
assert_eq!(bind_scene.arg_slots, 1);
|
||||
assert_eq!(bind_scene.ret_slots, 1);
|
||||
|
||||
let unbind_scene = meta_for(Syscall::ComposerUnbindScene);
|
||||
assert_eq!(unbind_scene.arg_slots, 0);
|
||||
assert_eq!(unbind_scene.ret_slots, 1);
|
||||
|
||||
let set_camera = meta_for(Syscall::ComposerSetCamera);
|
||||
assert_eq!(set_camera.arg_slots, 2);
|
||||
assert_eq!(set_camera.ret_slots, 0);
|
||||
|
||||
let emit_sprite = meta_for(Syscall::ComposerEmitSprite);
|
||||
assert_eq!(emit_sprite.arg_slots, 9);
|
||||
assert_eq!(emit_sprite.ret_slots, 1);
|
||||
|
||||
let audio_play_sample = meta_for(Syscall::AudioPlaySample);
|
||||
assert_eq!(audio_play_sample.arg_slots, 5);
|
||||
assert_eq!(audio_play_sample.ret_slots, 1);
|
||||
@ -231,10 +255,10 @@ fn status_first_syscall_signatures_are_pinned() {
|
||||
fn declared_resolver_rejects_legacy_status_first_signatures() {
|
||||
let declared = vec![
|
||||
prometeu_bytecode::SyscallDecl {
|
||||
module: "gfx".into(),
|
||||
name: "set_sprite".into(),
|
||||
module: "composer".into(),
|
||||
name: "bind_scene".into(),
|
||||
version: 1,
|
||||
arg_slots: 10,
|
||||
arg_slots: 1,
|
||||
ret_slots: 0,
|
||||
},
|
||||
prometeu_bytecode::SyscallDecl {
|
||||
@ -306,10 +330,24 @@ fn declared_resolver_rejects_legacy_status_first_signatures() {
|
||||
fn declared_resolver_accepts_mixed_status_first_surface_as_a_single_module() {
|
||||
let declared = vec![
|
||||
prometeu_bytecode::SyscallDecl {
|
||||
module: "gfx".into(),
|
||||
name: "set_sprite".into(),
|
||||
module: "composer".into(),
|
||||
name: "bind_scene".into(),
|
||||
version: 1,
|
||||
arg_slots: 10,
|
||||
arg_slots: 1,
|
||||
ret_slots: 1,
|
||||
},
|
||||
prometeu_bytecode::SyscallDecl {
|
||||
module: "composer".into(),
|
||||
name: "unbind_scene".into(),
|
||||
version: 1,
|
||||
arg_slots: 0,
|
||||
ret_slots: 1,
|
||||
},
|
||||
prometeu_bytecode::SyscallDecl {
|
||||
module: "composer".into(),
|
||||
name: "emit_sprite".into(),
|
||||
version: 1,
|
||||
arg_slots: 9,
|
||||
ret_slots: 1,
|
||||
},
|
||||
prometeu_bytecode::SyscallDecl {
|
||||
@ -342,8 +380,10 @@ fn declared_resolver_accepts_mixed_status_first_surface_as_a_single_module() {
|
||||
assert_eq!(resolved.len(), declared.len());
|
||||
assert_eq!(resolved[0].meta.ret_slots, 1);
|
||||
assert_eq!(resolved[1].meta.ret_slots, 1);
|
||||
assert_eq!(resolved[2].meta.ret_slots, 2);
|
||||
assert_eq!(resolved[2].meta.ret_slots, 1);
|
||||
assert_eq!(resolved[3].meta.ret_slots, 1);
|
||||
assert_eq!(resolved[4].meta.ret_slots, 2);
|
||||
assert_eq!(resolved[5].meta.ret_slots, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -10,8 +10,8 @@ use prometeu_hal::sprite::Sprite;
|
||||
use prometeu_hal::syscalls::Syscall;
|
||||
use prometeu_hal::vm_fault::VmFault;
|
||||
use prometeu_hal::{
|
||||
AudioOpStatus, GfxOpStatus, HostContext, HostReturn, NativeInterface, SyscallId, expect_bool,
|
||||
expect_int,
|
||||
AudioOpStatus, ComposerOpStatus, HostContext, HostReturn, NativeInterface, SyscallId,
|
||||
expect_bool, expect_int,
|
||||
};
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
@ -56,6 +56,23 @@ impl VirtualMachineRuntime {
|
||||
pub(crate) fn get_color(&self, value: i64) -> Color {
|
||||
Color::from_raw(value as u16)
|
||||
}
|
||||
|
||||
fn int_arg_to_usize_status(value: i64) -> Result<usize, ComposerOpStatus> {
|
||||
usize::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid)
|
||||
}
|
||||
|
||||
fn int_arg_to_i32_trap(value: i64, name: &str) -> Result<i32, VmFault> {
|
||||
i32::try_from(value)
|
||||
.map_err(|_| VmFault::Trap(TRAP_OOB, format!("{name} value out of bounds")))
|
||||
}
|
||||
|
||||
fn int_arg_to_u8_status(value: i64) -> Result<u8, ComposerOpStatus> {
|
||||
u8::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid)
|
||||
}
|
||||
|
||||
fn int_arg_to_u16_status(value: i64) -> Result<u16, ComposerOpStatus> {
|
||||
u16::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid)
|
||||
}
|
||||
}
|
||||
|
||||
impl NativeInterface for VirtualMachineRuntime {
|
||||
@ -135,46 +152,6 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color);
|
||||
Ok(())
|
||||
}
|
||||
Syscall::GfxSetSprite => {
|
||||
let bank_id = expect_int(args, 0)? as u8;
|
||||
let index = expect_int(args, 1)? as usize;
|
||||
let x = expect_int(args, 2)? as i32;
|
||||
let y = expect_int(args, 3)? as i32;
|
||||
let glyph_id = expect_int(args, 4)? as u16;
|
||||
let palette_id = expect_int(args, 5)? as u8;
|
||||
let active = expect_bool(args, 6)?;
|
||||
let flip_x = expect_bool(args, 7)?;
|
||||
let flip_y = expect_bool(args, 8)?;
|
||||
let priority = expect_int(args, 9)? as u8;
|
||||
|
||||
if index >= 512 {
|
||||
ret.push_int(GfxOpStatus::InvalidSpriteIndex as i64);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if hw.assets().slot_info(SlotRef::gfx(bank_id as usize)).asset_id.is_none() {
|
||||
ret.push_int(GfxOpStatus::BankInvalid as i64);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if palette_id >= 64 || priority >= 5 {
|
||||
ret.push_int(GfxOpStatus::ArgRangeInvalid as i64);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
*hw.gfx_mut().sprite_mut(index) = Sprite {
|
||||
glyph: Glyph { glyph_id, palette_id },
|
||||
x,
|
||||
y,
|
||||
bank_id,
|
||||
active,
|
||||
flip_x,
|
||||
flip_y,
|
||||
priority,
|
||||
};
|
||||
ret.push_int(GfxOpStatus::Ok as i64);
|
||||
Ok(())
|
||||
}
|
||||
Syscall::GfxDrawText => {
|
||||
let x = expect_int(args, 0)? as i32;
|
||||
let y = expect_int(args, 1)? as i32;
|
||||
@ -191,6 +168,100 @@ impl NativeInterface for VirtualMachineRuntime {
|
||||
hw.gfx_mut().clear(Color::from_raw(color_val as u16));
|
||||
Ok(())
|
||||
}
|
||||
Syscall::ComposerBindScene => {
|
||||
let scene_bank_id = match Self::int_arg_to_usize_status(expect_int(args, 0)?) {
|
||||
Ok(id) => id,
|
||||
Err(status) => {
|
||||
ret.push_int(status as i64);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let status = if hw.bind_scene(scene_bank_id) {
|
||||
ComposerOpStatus::Ok
|
||||
} else {
|
||||
ComposerOpStatus::SceneUnavailable
|
||||
};
|
||||
ret.push_int(status as i64);
|
||||
Ok(())
|
||||
}
|
||||
Syscall::ComposerUnbindScene => {
|
||||
hw.unbind_scene();
|
||||
ret.push_int(ComposerOpStatus::Ok as i64);
|
||||
Ok(())
|
||||
}
|
||||
Syscall::ComposerSetCamera => {
|
||||
let x = Self::int_arg_to_i32_trap(expect_int(args, 0)?, "camera x")?;
|
||||
let y = Self::int_arg_to_i32_trap(expect_int(args, 1)?, "camera y")?;
|
||||
hw.set_camera(x, y);
|
||||
Ok(())
|
||||
}
|
||||
Syscall::ComposerEmitSprite => {
|
||||
let glyph_id = match Self::int_arg_to_u16_status(expect_int(args, 0)?) {
|
||||
Ok(value) => value,
|
||||
Err(status) => {
|
||||
ret.push_int(status as i64);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let palette_id = match Self::int_arg_to_u8_status(expect_int(args, 1)?) {
|
||||
Ok(value) if value < 64 => value,
|
||||
_ => {
|
||||
ret.push_int(ComposerOpStatus::ArgRangeInvalid as i64);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let x = Self::int_arg_to_i32_trap(expect_int(args, 2)?, "sprite x")?;
|
||||
let y = Self::int_arg_to_i32_trap(expect_int(args, 3)?, "sprite y")?;
|
||||
let layer = match Self::int_arg_to_u8_status(expect_int(args, 4)?) {
|
||||
Ok(value) if value < 4 => value,
|
||||
Ok(_) => {
|
||||
ret.push_int(ComposerOpStatus::LayerInvalid as i64);
|
||||
return Ok(());
|
||||
}
|
||||
Err(status) => {
|
||||
ret.push_int(status as i64);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let bank_id = match Self::int_arg_to_u8_status(expect_int(args, 5)?) {
|
||||
Ok(value) => value,
|
||||
Err(status) => {
|
||||
ret.push_int(status as i64);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let flip_x = expect_bool(args, 6)?;
|
||||
let flip_y = expect_bool(args, 7)?;
|
||||
let priority = match Self::int_arg_to_u8_status(expect_int(args, 8)?) {
|
||||
Ok(value) => value,
|
||||
Err(status) => {
|
||||
ret.push_int(status as i64);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if !hw.has_glyph_bank(bank_id as usize) {
|
||||
ret.push_int(ComposerOpStatus::BankInvalid as i64);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let emitted = hw.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id, palette_id },
|
||||
x,
|
||||
y,
|
||||
layer,
|
||||
bank_id,
|
||||
active: false,
|
||||
flip_x,
|
||||
flip_y,
|
||||
priority,
|
||||
});
|
||||
let status =
|
||||
if emitted { ComposerOpStatus::Ok } else { ComposerOpStatus::SpriteOverflow };
|
||||
ret.push_int(status as i64);
|
||||
Ok(())
|
||||
}
|
||||
Syscall::AudioPlaySample => {
|
||||
let sample_id_raw = expect_int(args, 0)?;
|
||||
let voice_id_raw = expect_int(args, 1)?;
|
||||
|
||||
@ -5,17 +5,25 @@ use prometeu_bytecode::Value;
|
||||
use prometeu_bytecode::assembler::assemble;
|
||||
use prometeu_bytecode::model::{BytecodeModule, ConstantPoolEntry, FunctionMeta, SyscallDecl};
|
||||
use prometeu_drivers::hardware::Hardware;
|
||||
use prometeu_drivers::{GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller};
|
||||
use prometeu_hal::AudioOpStatus;
|
||||
use prometeu_hal::GfxOpStatus;
|
||||
use prometeu_hal::ComposerOpStatus;
|
||||
use prometeu_hal::InputSignals;
|
||||
use prometeu_hal::asset::{
|
||||
AssetCodec, AssetEntry, AssetLoadError, AssetOpStatus, BankType, LoadStatus,
|
||||
};
|
||||
use prometeu_hal::cartridge::{AssetsPayloadSource, Cartridge};
|
||||
use prometeu_hal::glyph_bank::GLYPH_BANK_PALETTE_COUNT_V1;
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::glyph::Glyph;
|
||||
use prometeu_hal::glyph_bank::{GLYPH_BANK_PALETTE_COUNT_V1, GlyphBank, TileSize};
|
||||
use prometeu_hal::scene_bank::SceneBank;
|
||||
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
||||
use prometeu_hal::syscalls::caps;
|
||||
use prometeu_hal::tile::Tile;
|
||||
use prometeu_hal::tilemap::TileMap;
|
||||
use prometeu_vm::VmInitError;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
#[derive(Default)]
|
||||
@ -129,6 +137,40 @@ fn test_glyph_asset_data() -> Vec<u8> {
|
||||
data
|
||||
}
|
||||
|
||||
fn runtime_test_glyph_bank(tile_size: TileSize, palette_id: u8, color: Color) -> GlyphBank {
|
||||
let size = tile_size as usize;
|
||||
let mut bank = GlyphBank::new(tile_size, size, size);
|
||||
bank.palettes[palette_id as usize][1] = color;
|
||||
for pixel in &mut bank.pixel_indices {
|
||||
*pixel = 1;
|
||||
}
|
||||
bank
|
||||
}
|
||||
|
||||
fn runtime_test_scene(glyph_bank_id: u8, palette_id: u8, tile_size: TileSize) -> SceneBank {
|
||||
let layer = SceneLayer {
|
||||
active: true,
|
||||
glyph_bank_id,
|
||||
tile_size,
|
||||
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
|
||||
tilemap: TileMap {
|
||||
width: 2,
|
||||
height: 2,
|
||||
tiles: vec![
|
||||
Tile {
|
||||
active: true,
|
||||
glyph: Glyph { glyph_id: 0, palette_id },
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
};
|
||||
4
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
SceneBank { layers: std::array::from_fn(|_| layer.clone()) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() {
|
||||
let mut runtime = VirtualMachineRuntime::new(None);
|
||||
@ -233,6 +275,174 @@ fn tick_returns_panic_report_distinct_from_trap() {
|
||||
assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmPanic { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_renders_bound_eight_pixel_scene_through_frame_composer_path() {
|
||||
let mut runtime = VirtualMachineRuntime::new(None);
|
||||
let mut vm = VirtualMachine::default();
|
||||
let signals = InputSignals::default();
|
||||
let program =
|
||||
serialized_single_function_module(assemble("FRAME_SYNC\nHALT").expect("assemble"), vec![]);
|
||||
let cartridge = cartridge_with_program(program, caps::NONE);
|
||||
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
banks.install_glyph_bank(0, Arc::new(runtime_test_glyph_bank(TileSize::Size8, 2, Color::BLUE)));
|
||||
banks.install_scene_bank(0, Arc::new(runtime_test_scene(0, 2, TileSize::Size8)));
|
||||
let mut hardware = Hardware::new_with_memory_banks(Arc::clone(&banks));
|
||||
assert!(hardware.frame_composer.bind_scene(0));
|
||||
|
||||
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
||||
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
||||
|
||||
assert!(report.is_none(), "frame render path must not crash");
|
||||
hardware.gfx.present();
|
||||
assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_renders_scene_through_public_composer_syscalls() {
|
||||
let mut runtime = VirtualMachineRuntime::new(None);
|
||||
let mut vm = VirtualMachine::default();
|
||||
let signals = InputSignals::default();
|
||||
let code = assemble(
|
||||
"PUSH_I32 0\nHOSTCALL 0\nPOP_N 1\n\
|
||||
PUSH_I32 0\nPUSH_I32 0\nHOSTCALL 1\n\
|
||||
PUSH_I32 0\nPUSH_I32 2\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 2\n\
|
||||
FRAME_SYNC\nHALT",
|
||||
)
|
||||
.expect("assemble");
|
||||
let program = serialized_single_function_module(
|
||||
code,
|
||||
vec![
|
||||
SyscallDecl {
|
||||
module: "composer".into(),
|
||||
name: "bind_scene".into(),
|
||||
version: 1,
|
||||
arg_slots: 1,
|
||||
ret_slots: 1,
|
||||
},
|
||||
SyscallDecl {
|
||||
module: "composer".into(),
|
||||
name: "set_camera".into(),
|
||||
version: 1,
|
||||
arg_slots: 2,
|
||||
ret_slots: 0,
|
||||
},
|
||||
SyscallDecl {
|
||||
module: "composer".into(),
|
||||
name: "emit_sprite".into(),
|
||||
version: 1,
|
||||
arg_slots: 9,
|
||||
ret_slots: 1,
|
||||
},
|
||||
],
|
||||
);
|
||||
let cartridge = cartridge_with_program(program, caps::GFX);
|
||||
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
banks.install_glyph_bank(0, Arc::new(runtime_test_glyph_bank(TileSize::Size8, 2, Color::BLUE)));
|
||||
banks.install_scene_bank(0, Arc::new(runtime_test_scene(0, 2, TileSize::Size8)));
|
||||
let mut hardware = Hardware::new_with_memory_banks(Arc::clone(&banks));
|
||||
hardware.gfx.scene_fade_level = 31;
|
||||
hardware.gfx.hud_fade_level = 31;
|
||||
|
||||
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
||||
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
||||
|
||||
assert!(report.is_none(), "public composer path must not crash");
|
||||
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::Ok as i64)]);
|
||||
hardware.gfx.present();
|
||||
assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_draw_text_survives_scene_backed_frame_composition() {
|
||||
let mut runtime = VirtualMachineRuntime::new(None);
|
||||
let mut vm = VirtualMachine::default();
|
||||
let signals = InputSignals::default();
|
||||
let code = assemble(
|
||||
"PUSH_I32 0\nHOSTCALL 0\nPOP_N 1\n\
|
||||
PUSH_I32 0\nPUSH_I32 0\nHOSTCALL 1\n\
|
||||
PUSH_I32 0\nPUSH_I32 0\nPUSH_CONST 0\nPUSH_I32 65535\nHOSTCALL 2\n\
|
||||
FRAME_SYNC\nHALT",
|
||||
)
|
||||
.expect("assemble");
|
||||
let program = serialized_single_function_module_with_consts(
|
||||
code,
|
||||
vec![ConstantPoolEntry::String("I".into())],
|
||||
vec![
|
||||
SyscallDecl {
|
||||
module: "composer".into(),
|
||||
name: "bind_scene".into(),
|
||||
version: 1,
|
||||
arg_slots: 1,
|
||||
ret_slots: 1,
|
||||
},
|
||||
SyscallDecl {
|
||||
module: "composer".into(),
|
||||
name: "set_camera".into(),
|
||||
version: 1,
|
||||
arg_slots: 2,
|
||||
ret_slots: 0,
|
||||
},
|
||||
SyscallDecl {
|
||||
module: "gfx".into(),
|
||||
name: "draw_text".into(),
|
||||
version: 1,
|
||||
arg_slots: 4,
|
||||
ret_slots: 0,
|
||||
},
|
||||
],
|
||||
);
|
||||
let cartridge = cartridge_with_program(program, caps::GFX);
|
||||
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
banks.install_glyph_bank(0, Arc::new(runtime_test_glyph_bank(TileSize::Size8, 2, Color::BLUE)));
|
||||
banks.install_scene_bank(0, Arc::new(runtime_test_scene(0, 2, TileSize::Size8)));
|
||||
let mut hardware = Hardware::new_with_memory_banks(Arc::clone(&banks));
|
||||
hardware.gfx.scene_fade_level = 31;
|
||||
hardware.gfx.hud_fade_level = 31;
|
||||
|
||||
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
||||
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
||||
|
||||
assert!(report.is_none(), "scene-backed overlay text must not crash");
|
||||
hardware.gfx.present();
|
||||
assert_eq!(hardware.gfx.front_buffer()[0], Color::WHITE.raw());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_draw_text_survives_no_scene_frame_path() {
|
||||
let mut runtime = VirtualMachineRuntime::new(None);
|
||||
let mut vm = VirtualMachine::default();
|
||||
let signals = InputSignals::default();
|
||||
let code = assemble(
|
||||
"PUSH_I32 0\nPUSH_I32 0\nPUSH_CONST 0\nPUSH_I32 65535\nHOSTCALL 0\nFRAME_SYNC\nHALT",
|
||||
)
|
||||
.expect("assemble");
|
||||
let program = serialized_single_function_module_with_consts(
|
||||
code,
|
||||
vec![ConstantPoolEntry::String("I".into())],
|
||||
vec![SyscallDecl {
|
||||
module: "gfx".into(),
|
||||
name: "draw_text".into(),
|
||||
version: 1,
|
||||
arg_slots: 4,
|
||||
ret_slots: 0,
|
||||
}],
|
||||
);
|
||||
let cartridge = cartridge_with_program(program, caps::GFX);
|
||||
let mut hardware = Hardware::new();
|
||||
hardware.gfx.scene_fade_level = 31;
|
||||
hardware.gfx.hud_fade_level = 31;
|
||||
|
||||
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
||||
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
||||
|
||||
assert!(report.is_none(), "no-scene overlay text must not crash");
|
||||
hardware.gfx.present();
|
||||
assert_eq!(hardware.gfx.front_buffer()[0], Color::WHITE.raw());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initialize_vm_success_clears_previous_crash_report() {
|
||||
let mut runtime = VirtualMachineRuntime::new(None);
|
||||
@ -364,22 +574,19 @@ fn initialize_vm_failure_clears_previous_identity_and_handles() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_gfx_set_sprite_operational_error_returns_status_not_crash() {
|
||||
fn tick_composer_bind_scene_operational_error_returns_status_not_crash() {
|
||||
let mut runtime = VirtualMachineRuntime::new(None);
|
||||
let mut vm = VirtualMachine::default();
|
||||
let mut hardware = Hardware::new();
|
||||
let signals = InputSignals::default();
|
||||
let code = assemble(
|
||||
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
||||
)
|
||||
.expect("assemble");
|
||||
let code = assemble("PUSH_I32 99\nHOSTCALL 0\nHALT").expect("assemble");
|
||||
let program = serialized_single_function_module(
|
||||
code,
|
||||
vec![SyscallDecl {
|
||||
module: "gfx".into(),
|
||||
name: "set_sprite".into(),
|
||||
module: "composer".into(),
|
||||
name: "bind_scene".into(),
|
||||
version: 1,
|
||||
arg_slots: 10,
|
||||
arg_slots: 1,
|
||||
ret_slots: 1,
|
||||
}],
|
||||
);
|
||||
@ -389,26 +596,29 @@ fn tick_gfx_set_sprite_operational_error_returns_status_not_crash() {
|
||||
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
||||
assert!(report.is_none(), "operational error must not crash");
|
||||
assert!(vm.is_halted());
|
||||
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::BankInvalid as i64)]);
|
||||
assert_eq!(
|
||||
vm.operand_stack_top(1),
|
||||
vec![Value::Int64(ComposerOpStatus::SceneUnavailable as i64)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_gfx_set_sprite_invalid_index_returns_status_not_crash() {
|
||||
fn tick_composer_emit_sprite_operational_error_returns_status_not_crash() {
|
||||
let mut runtime = VirtualMachineRuntime::new(None);
|
||||
let mut vm = VirtualMachine::default();
|
||||
let mut hardware = Hardware::new();
|
||||
let signals = InputSignals::default();
|
||||
let code = assemble(
|
||||
"PUSH_I32 0\nPUSH_I32 512\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
||||
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
||||
)
|
||||
.expect("assemble");
|
||||
let program = serialized_single_function_module(
|
||||
code,
|
||||
vec![SyscallDecl {
|
||||
module: "gfx".into(),
|
||||
name: "set_sprite".into(),
|
||||
module: "composer".into(),
|
||||
name: "emit_sprite".into(),
|
||||
version: 1,
|
||||
arg_slots: 10,
|
||||
arg_slots: 9,
|
||||
ret_slots: 1,
|
||||
}],
|
||||
);
|
||||
@ -416,28 +626,57 @@ fn tick_gfx_set_sprite_invalid_index_returns_status_not_crash() {
|
||||
|
||||
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
||||
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
||||
assert!(report.is_none(), "invalid sprite index must not crash");
|
||||
assert!(report.is_none(), "operational error must not crash");
|
||||
assert!(vm.is_halted());
|
||||
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::InvalidSpriteIndex as i64)]);
|
||||
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::BankInvalid as i64)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_gfx_set_sprite_invalid_range_returns_status_not_crash() {
|
||||
fn tick_composer_emit_sprite_invalid_layer_returns_status_not_crash() {
|
||||
let mut runtime = VirtualMachineRuntime::new(None);
|
||||
let mut vm = VirtualMachine::default();
|
||||
let mut hardware = Hardware::new();
|
||||
let signals = InputSignals::default();
|
||||
let code = assemble(
|
||||
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 64\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
||||
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 4\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
||||
)
|
||||
.expect("assemble");
|
||||
let program = serialized_single_function_module(
|
||||
code,
|
||||
vec![SyscallDecl {
|
||||
module: "gfx".into(),
|
||||
name: "set_sprite".into(),
|
||||
module: "composer".into(),
|
||||
name: "emit_sprite".into(),
|
||||
version: 1,
|
||||
arg_slots: 10,
|
||||
arg_slots: 9,
|
||||
ret_slots: 1,
|
||||
}],
|
||||
);
|
||||
let cartridge = cartridge_with_program(program, caps::GFX);
|
||||
|
||||
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
||||
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
||||
assert!(report.is_none(), "invalid layer must not crash");
|
||||
assert!(vm.is_halted());
|
||||
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::LayerInvalid as i64)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_composer_emit_sprite_invalid_range_returns_status_not_crash() {
|
||||
let mut runtime = VirtualMachineRuntime::new(None);
|
||||
let mut vm = VirtualMachine::default();
|
||||
let mut hardware = Hardware::new();
|
||||
let signals = InputSignals::default();
|
||||
let code = assemble(
|
||||
"PUSH_I32 0\nPUSH_I32 64\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
||||
)
|
||||
.expect("assemble");
|
||||
let program = serialized_single_function_module(
|
||||
code,
|
||||
vec![SyscallDecl {
|
||||
module: "composer".into(),
|
||||
name: "emit_sprite".into(),
|
||||
version: 1,
|
||||
arg_slots: 9,
|
||||
ret_slots: 1,
|
||||
}],
|
||||
);
|
||||
@ -452,9 +691,12 @@ fn tick_gfx_set_sprite_invalid_range_returns_status_not_crash() {
|
||||
|
||||
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
||||
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
||||
assert!(report.is_none(), "invalid gfx parameter range must not crash");
|
||||
assert!(report.is_none(), "invalid composer parameter range must not crash");
|
||||
assert!(vm.is_halted());
|
||||
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::ArgRangeInvalid as i64)]);
|
||||
assert_eq!(
|
||||
vm.operand_stack_top(1),
|
||||
vec![Value::Int64(ComposerOpStatus::ArgRangeInvalid as i64)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -816,13 +1058,13 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() {
|
||||
fn tick_status_first_surface_smoke_across_composer_audio_and_asset() {
|
||||
let mut runtime = VirtualMachineRuntime::new(None);
|
||||
let mut vm = VirtualMachine::default();
|
||||
let mut hardware = Hardware::new();
|
||||
let signals = InputSignals::default();
|
||||
let code = assemble(
|
||||
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\n\
|
||||
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\n\
|
||||
PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 1\n\
|
||||
PUSH_I32 999\nPUSH_I32 0\nHOSTCALL 2\n\
|
||||
HALT"
|
||||
@ -832,10 +1074,10 @@ fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() {
|
||||
code,
|
||||
vec![
|
||||
SyscallDecl {
|
||||
module: "gfx".into(),
|
||||
name: "set_sprite".into(),
|
||||
module: "composer".into(),
|
||||
name: "emit_sprite".into(),
|
||||
version: 1,
|
||||
arg_slots: 10,
|
||||
arg_slots: 9,
|
||||
ret_slots: 1,
|
||||
},
|
||||
SyscallDecl {
|
||||
@ -866,28 +1108,28 @@ fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() {
|
||||
Value::Int64(0),
|
||||
Value::Int64(AssetLoadError::AssetNotFound as i64),
|
||||
Value::Int64(AudioOpStatus::BankInvalid as i64),
|
||||
Value::Int64(GfxOpStatus::BankInvalid as i64),
|
||||
Value::Int64(ComposerOpStatus::BankInvalid as i64),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_gfx_set_sprite_type_mismatch_surfaces_trap_not_panic() {
|
||||
fn tick_composer_emit_sprite_type_mismatch_surfaces_trap_not_panic() {
|
||||
let mut runtime = VirtualMachineRuntime::new(None);
|
||||
let mut vm = VirtualMachine::default();
|
||||
let mut hardware = Hardware::new();
|
||||
let signals = InputSignals::default();
|
||||
let code = assemble(
|
||||
"PUSH_BOOL 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
||||
"PUSH_BOOL 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
||||
)
|
||||
.expect("assemble");
|
||||
let program = serialized_single_function_module(
|
||||
code,
|
||||
vec![SyscallDecl {
|
||||
module: "gfx".into(),
|
||||
name: "set_sprite".into(),
|
||||
module: "composer".into(),
|
||||
name: "emit_sprite".into(),
|
||||
version: 1,
|
||||
arg_slots: 10,
|
||||
arg_slots: 9,
|
||||
ret_slots: 1,
|
||||
}],
|
||||
);
|
||||
|
||||
@ -145,7 +145,7 @@ impl VirtualMachineRuntime {
|
||||
if run.reason == LogicalFrameEndingReason::FrameSync
|
||||
|| run.reason == LogicalFrameEndingReason::EndOfRom
|
||||
{
|
||||
hw.gfx_mut().render_all();
|
||||
hw.render_frame();
|
||||
|
||||
// 1. Snapshot full telemetry at logical frame end
|
||||
let (glyph_bank, sound_bank) = Self::bank_telemetry_summary(hw);
|
||||
@ -250,6 +250,7 @@ impl VirtualMachineRuntime {
|
||||
_signals: &InputSignals,
|
||||
hw: &mut dyn HardwareBridge,
|
||||
) {
|
||||
hw.begin_frame();
|
||||
hw.audio_mut().clear_commands();
|
||||
self.logs_written_this_frame.clear();
|
||||
}
|
||||
|
||||
@ -2567,11 +2567,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_status_first_syscall_results_count_mismatch_panic() {
|
||||
// GfxSetSprite (0x1007) expects 1 result.
|
||||
let code = assemble(
|
||||
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nSYSCALL 0x1007",
|
||||
)
|
||||
.expect("assemble");
|
||||
// ComposerBindScene (0x1101) expects 1 result.
|
||||
let code = assemble("PUSH_I32 0\nSYSCALL 0x1101").expect("assemble");
|
||||
|
||||
struct BadNativeNoReturn;
|
||||
impl NativeInterface for BadNativeNoReturn {
|
||||
@ -2921,10 +2918,24 @@ mod tests {
|
||||
fn test_loader_patching_accepts_status_first_signatures() {
|
||||
let cases = vec![
|
||||
SyscallDecl {
|
||||
module: "gfx".into(),
|
||||
name: "set_sprite".into(),
|
||||
module: "composer".into(),
|
||||
name: "bind_scene".into(),
|
||||
version: 1,
|
||||
arg_slots: 10,
|
||||
arg_slots: 1,
|
||||
ret_slots: 1,
|
||||
},
|
||||
SyscallDecl {
|
||||
module: "composer".into(),
|
||||
name: "unbind_scene".into(),
|
||||
version: 1,
|
||||
arg_slots: 0,
|
||||
ret_slots: 1,
|
||||
},
|
||||
SyscallDecl {
|
||||
module: "composer".into(),
|
||||
name: "emit_sprite".into(),
|
||||
version: 1,
|
||||
arg_slots: 9,
|
||||
ret_slots: 1,
|
||||
},
|
||||
SyscallDecl {
|
||||
@ -2977,10 +2988,10 @@ mod tests {
|
||||
fn test_loader_patching_rejects_legacy_status_first_ret_slots() {
|
||||
let cases = vec![
|
||||
SyscallDecl {
|
||||
module: "gfx".into(),
|
||||
name: "set_sprite".into(),
|
||||
module: "composer".into(),
|
||||
name: "bind_scene".into(),
|
||||
version: 1,
|
||||
arg_slots: 10,
|
||||
arg_slots: 1,
|
||||
ret_slots: 0,
|
||||
},
|
||||
SyscallDecl {
|
||||
|
||||
@ -5,4 +5,6 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
prometeu-bytecode = { path = "../../console/prometeu-bytecode" }
|
||||
prometeu-hal = { path = "../../console/prometeu-hal" }
|
||||
anyhow = "1"
|
||||
serde_json = "1"
|
||||
|
||||
@ -3,7 +3,25 @@ use prometeu_bytecode::assembler::assemble;
|
||||
use prometeu_bytecode::model::{
|
||||
BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta, SyscallDecl,
|
||||
};
|
||||
use prometeu_hal::asset::{
|
||||
AssetCodec, AssetEntry, BankType, PreloadEntry, SCENE_DECODED_LAYER_OVERHEAD_BYTES_V1,
|
||||
SCENE_LAYER_COUNT_V1, SCENE_PAYLOAD_MAGIC_V1, SCENE_PAYLOAD_VERSION_V1,
|
||||
};
|
||||
use prometeu_hal::cartridge::{
|
||||
AssetsPackHeader, AssetsPackPrelude, ASSETS_PA_MAGIC, ASSETS_PA_PRELUDE_SIZE,
|
||||
ASSETS_PA_SCHEMA_VERSION,
|
||||
};
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::glyph::Glyph;
|
||||
use prometeu_hal::glyph_bank::{
|
||||
TileSize, GLYPH_BANK_COLORS_PER_PALETTE, GLYPH_BANK_PALETTE_COUNT_V1,
|
||||
};
|
||||
use prometeu_hal::scene_bank::SceneBank;
|
||||
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
||||
use prometeu_hal::tile::Tile;
|
||||
use prometeu_hal::tilemap::TileMap;
|
||||
use std::fs;
|
||||
use std::mem::size_of;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn asm(s: &str) -> Vec<u8> {
|
||||
@ -20,13 +38,6 @@ pub fn generate() -> Result<()> {
|
||||
arg_slots: 1,
|
||||
ret_slots: 0,
|
||||
},
|
||||
SyscallDecl {
|
||||
module: "gfx".into(),
|
||||
name: "draw_disc".into(),
|
||||
version: 1,
|
||||
arg_slots: 5,
|
||||
ret_slots: 0,
|
||||
},
|
||||
SyscallDecl {
|
||||
module: "gfx".into(),
|
||||
name: "draw_text".into(),
|
||||
@ -42,10 +53,24 @@ pub fn generate() -> Result<()> {
|
||||
ret_slots: 0,
|
||||
},
|
||||
SyscallDecl {
|
||||
module: "gfx".into(),
|
||||
name: "set_sprite".into(),
|
||||
module: "composer".into(),
|
||||
name: "bind_scene".into(),
|
||||
version: 1,
|
||||
arg_slots: 10,
|
||||
arg_slots: 1,
|
||||
ret_slots: 1,
|
||||
},
|
||||
SyscallDecl {
|
||||
module: "composer".into(),
|
||||
name: "set_camera".into(),
|
||||
version: 1,
|
||||
arg_slots: 2,
|
||||
ret_slots: 0,
|
||||
},
|
||||
SyscallDecl {
|
||||
module: "composer".into(),
|
||||
name: "emit_sprite".into(),
|
||||
version: 1,
|
||||
arg_slots: 9,
|
||||
ret_slots: 1,
|
||||
},
|
||||
];
|
||||
@ -59,7 +84,7 @@ pub fn generate() -> Result<()> {
|
||||
param_slots: 0,
|
||||
local_slots: 2,
|
||||
return_slots: 0,
|
||||
max_stack_slots: 16,
|
||||
max_stack_slots: 32,
|
||||
}];
|
||||
|
||||
let module = BytecodeModule {
|
||||
@ -67,7 +92,8 @@ pub fn generate() -> Result<()> {
|
||||
const_pool: vec![
|
||||
ConstantPoolEntry::String("stress".into()),
|
||||
ConstantPoolEntry::String("frame".into()),
|
||||
ConstantPoolEntry::String("missing_glyph_bank".into()),
|
||||
ConstantPoolEntry::String("overlay".into()),
|
||||
ConstantPoolEntry::String("composer".into()),
|
||||
],
|
||||
functions,
|
||||
code: rom,
|
||||
@ -89,129 +115,332 @@ pub fn generate() -> Result<()> {
|
||||
out_dir.push("stress-console");
|
||||
fs::create_dir_all(&out_dir)?;
|
||||
fs::write(out_dir.join("program.pbx"), bytes)?;
|
||||
let assets_pa_path = out_dir.join("assets.pa");
|
||||
if assets_pa_path.exists() {
|
||||
fs::remove_file(&assets_pa_path)?;
|
||||
}
|
||||
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\"]\n}\n")?;
|
||||
fs::write(out_dir.join("assets.pa"), build_assets_pack()?)?;
|
||||
fs::write(out_dir.join("manifest.json"), b"{\n \"magic\": \"PMTU\",\n \"cartridge_version\": 1,\n \"app_id\": 1,\n \"title\": \"Stress Console\",\n \"app_version\": \"0.1.0\",\n \"app_mode\": \"Game\",\n \"capabilities\": [\"gfx\", \"log\", \"asset\"]\n}\n")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn heavy_load(rom: &mut Vec<u8>) {
|
||||
// Single function 0: main
|
||||
// Everything runs here — no coroutines, no SPAWN, no YIELD.
|
||||
//
|
||||
// Global 0 = t (frame counter)
|
||||
// Local 0 = scratch
|
||||
// Local 1 = loop counter for discs
|
||||
//
|
||||
// Loop:
|
||||
// t = (t + 1)
|
||||
// clear screen
|
||||
// draw 500 discs using t for animation
|
||||
// draw 20 texts using t for animation
|
||||
// RET (runtime handles the frame loop)
|
||||
// Global 0 = frame counter
|
||||
// Global 1 = scene bound flag
|
||||
// Local 0 = sprite row
|
||||
// Local 1 = sprite col
|
||||
|
||||
// --- init locals ---
|
||||
// local 0: scratch
|
||||
// local 1: loop counter for discs
|
||||
rom.extend(asm("PUSH_I32 0\nSET_LOCAL 0\nPUSH_I32 0\nSET_LOCAL 1"));
|
||||
|
||||
// --- t = (t + 1) ---
|
||||
// t is global 0 to persist across prepare_call resets
|
||||
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 1\nADD\nSET_GLOBAL 0"));
|
||||
|
||||
// --- clear screen ---
|
||||
rom.extend(asm("GET_GLOBAL 1\nPUSH_I32 0\nEQ"));
|
||||
let jif_bind_done_offset = rom.len() + 2;
|
||||
rom.extend(asm("JMP_IF_FALSE 0"));
|
||||
|
||||
rom.extend(asm("PUSH_I32 0\nHOSTCALL 3\nPOP_N 1\nPUSH_I32 1\nSET_GLOBAL 1"));
|
||||
let bind_done_target = rom.len() as u32;
|
||||
|
||||
rom.extend(asm("PUSH_I32 0\nHOSTCALL 0"));
|
||||
// --- call status-first syscall path once per frame and drop status ---
|
||||
|
||||
rom.extend(asm(
|
||||
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 4\nPOP_N 1",
|
||||
"GET_GLOBAL 0\nPUSH_I32 2\nMUL\nPUSH_I32 192\nMOD\nGET_GLOBAL 0\nPUSH_I32 76\nMOD\nHOSTCALL 4",
|
||||
));
|
||||
|
||||
// --- draw 500 discs ---
|
||||
rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1"));
|
||||
let disc_loop_start = rom.len() as u32;
|
||||
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 500\nLT"));
|
||||
let jif_disc_end_offset = rom.len() + 2;
|
||||
rom.extend(asm("PUSH_I32 0\nSET_LOCAL 0"));
|
||||
let row_loop_start = rom.len() as u32;
|
||||
rom.extend(asm("GET_LOCAL 0\nPUSH_I32 16\nLT"));
|
||||
let jif_row_end_offset = rom.len() + 2;
|
||||
rom.extend(asm("JMP_IF_FALSE 0"));
|
||||
|
||||
// x = (t * (i+7) + i * 13) % 320
|
||||
rom.extend(asm("GET_GLOBAL 0\nGET_LOCAL 1\nPUSH_I32 7\nADD\nMUL\nGET_LOCAL 1\nPUSH_I32 13\nMUL\nADD\nPUSH_I32 320\nMOD"));
|
||||
// y = (t * (i+11) + i * 17) % 180
|
||||
rom.extend(asm("GET_GLOBAL 0\nGET_LOCAL 1\nPUSH_I32 11\nADD\nMUL\nGET_LOCAL 1\nPUSH_I32 17\nMUL\nADD\nPUSH_I32 180\nMOD"));
|
||||
// r = ( (i*13) % 20 ) + 5
|
||||
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 13\nMUL\nPUSH_I32 20\nMOD\nPUSH_I32 5\nADD"));
|
||||
// border color = (i * 1234) & 0xFFFF
|
||||
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1234\nMUL\nPUSH_I32 65535\nBIT_AND"));
|
||||
// fill color = (i * 5678 + t) & 0xFFFF
|
||||
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 5678\nMUL\nGET_GLOBAL 0\nADD\nPUSH_I32 65535\nBIT_AND"));
|
||||
// HOSTCALL gfx.draw_disc (x, y, r, border, fill)
|
||||
rom.extend(asm("HOSTCALL 1"));
|
||||
|
||||
// i++
|
||||
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1"));
|
||||
let jmp_disc_loop_offset = rom.len() + 2;
|
||||
rom.extend(asm("JMP 0"));
|
||||
let disc_loop_end = rom.len() as u32;
|
||||
|
||||
// --- draw 20 texts ---
|
||||
rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1"));
|
||||
let text_loop_start = rom.len() as u32;
|
||||
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 20\nLT"));
|
||||
let jif_text_end_offset = rom.len() + 2;
|
||||
let col_loop_start = rom.len() as u32;
|
||||
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 32\nLT"));
|
||||
let jif_col_end_offset = rom.len() + 2;
|
||||
rom.extend(asm("JMP_IF_FALSE 0"));
|
||||
|
||||
// x = (t * 3 + i * 40) % 320
|
||||
rom.extend(asm(
|
||||
"GET_GLOBAL 0\nPUSH_I32 3\nMUL\nGET_LOCAL 1\nPUSH_I32 40\nMUL\nADD\nPUSH_I32 320\nMOD",
|
||||
"PUSH_I32 0\n\
|
||||
GET_LOCAL 0\nPUSH_I32 32\nMUL\nGET_LOCAL 1\nADD\nGET_GLOBAL 0\nADD\nPUSH_I32 15\nMOD\nPUSH_I32 1\nADD\n\
|
||||
GET_LOCAL 1\nPUSH_I32 10\nMUL\nGET_GLOBAL 0\nPUSH_I32 10\nMOD\nADD\nPUSH_I32 320\nMOD\n\
|
||||
GET_LOCAL 0\nPUSH_I32 10\nMUL\nGET_GLOBAL 0\nPUSH_I32 2\nMUL\nPUSH_I32 10\nMOD\nADD\nPUSH_I32 180\nMOD\n\
|
||||
GET_LOCAL 0\nGET_LOCAL 1\nADD\nGET_GLOBAL 0\nADD\nPUSH_I32 4\nMOD\n\
|
||||
PUSH_I32 0\n\
|
||||
GET_LOCAL 1\nGET_GLOBAL 0\nADD\nPUSH_I32 1\nBIT_AND\nPUSH_I32 0\nNEQ\n\
|
||||
GET_LOCAL 0\nGET_GLOBAL 0\nADD\nPUSH_I32 1\nBIT_AND\nPUSH_I32 0\nNEQ\n\
|
||||
GET_LOCAL 0\nGET_LOCAL 1\nADD\nPUSH_I32 4\nMOD\n\
|
||||
HOSTCALL 5\nPOP_N 1",
|
||||
));
|
||||
// y = (i * 30 + t) % 180
|
||||
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 30\nMUL\nGET_GLOBAL 0\nADD\nPUSH_I32 180\nMOD"));
|
||||
// string (toggle between "stress" and "frame")
|
||||
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nBIT_AND\nPUSH_I32 0\nNEQ"));
|
||||
let jif_text_alt_offset = rom.len() + 2;
|
||||
rom.extend(asm("JMP_IF_FALSE 0"));
|
||||
rom.extend(asm("PUSH_CONST 0")); // "stress"
|
||||
let jmp_text_join_offset = rom.len() + 2;
|
||||
rom.extend(asm("JMP 0"));
|
||||
let text_alt_target = rom.len() as u32;
|
||||
rom.extend(asm("PUSH_CONST 1")); // "frame"
|
||||
let text_join_target = rom.len() as u32;
|
||||
|
||||
// color = (t * 10 + i * 1000) & 0xFFFF
|
||||
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 10\nMUL\nGET_LOCAL 1\nPUSH_I32 1000\nMUL\nADD\nPUSH_I32 65535\nBIT_AND"));
|
||||
// HOSTCALL gfx.draw_text (x, y, str, color)
|
||||
rom.extend(asm("HOSTCALL 2"));
|
||||
|
||||
// i++
|
||||
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1"));
|
||||
let jmp_text_loop_offset = rom.len() + 2;
|
||||
let jmp_col_loop_offset = rom.len() + 2;
|
||||
rom.extend(asm("JMP 0"));
|
||||
let text_loop_end = rom.len() as u32;
|
||||
let col_loop_end = rom.len() as u32;
|
||||
|
||||
rom.extend(asm("GET_LOCAL 0\nPUSH_I32 1\nADD\nSET_LOCAL 0"));
|
||||
let jmp_row_loop_offset = rom.len() + 2;
|
||||
rom.extend(asm("JMP 0"));
|
||||
let row_loop_end = rom.len() as u32;
|
||||
|
||||
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 3\nMUL\nPUSH_I32 220\nMOD\n\
|
||||
PUSH_I32 8\n\
|
||||
PUSH_CONST 0\n\
|
||||
GET_GLOBAL 0\nPUSH_I32 2047\nMUL\nPUSH_I32 65535\nBIT_AND\n\
|
||||
HOSTCALL 1"));
|
||||
rom.extend(asm("PUSH_I32 12\n\
|
||||
GET_GLOBAL 0\nPUSH_I32 2\nMUL\nPUSH_I32 120\nMOD\nPUSH_I32 24\nADD\n\
|
||||
PUSH_CONST 1\n\
|
||||
GET_GLOBAL 0\nPUSH_I32 4093\nMUL\nPUSH_I32 65535\nBIT_AND\n\
|
||||
HOSTCALL 1"));
|
||||
rom.extend(asm("PUSH_I32 220\n\
|
||||
GET_GLOBAL 0\nPUSH_I32 5\nMUL\nPUSH_I32 140\nMOD\n\
|
||||
PUSH_CONST 2\n\
|
||||
GET_GLOBAL 0\nPUSH_I32 1237\nMUL\nPUSH_I32 65535\nBIT_AND\n\
|
||||
HOSTCALL 1"));
|
||||
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 4\nMUL\nPUSH_I32 180\nMOD\nPUSH_I32 80\nADD\n\
|
||||
GET_GLOBAL 0\nPUSH_I32 3\nMUL\nPUSH_I32 90\nMOD\nPUSH_I32 70\nADD\n\
|
||||
PUSH_CONST 3\n\
|
||||
GET_GLOBAL 0\nPUSH_I32 3001\nMUL\nPUSH_I32 65535\nBIT_AND\n\
|
||||
HOSTCALL 1"));
|
||||
|
||||
// --- log every 60 frames ---
|
||||
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 60\nMOD\nPUSH_I32 0\nEQ"));
|
||||
let jif_log_offset = rom.len() + 2;
|
||||
rom.extend(asm("JMP_IF_FALSE 0"));
|
||||
rom.extend(asm("PUSH_I32 2\nPUSH_CONST 0\nHOSTCALL 3"));
|
||||
rom.extend(asm("PUSH_I32 2\nPUSH_CONST 0\nHOSTCALL 2"));
|
||||
let after_log = rom.len() as u32;
|
||||
|
||||
// --- end of function ---
|
||||
rom.extend(asm("FRAME_SYNC\nRET"));
|
||||
|
||||
// --- Patch jump targets ---
|
||||
let patch = |buf: &mut Vec<u8>, imm_offset: usize, target: u32| {
|
||||
buf[imm_offset..imm_offset + 4].copy_from_slice(&target.to_le_bytes());
|
||||
};
|
||||
|
||||
patch(rom, jif_disc_end_offset, disc_loop_end);
|
||||
patch(rom, jmp_disc_loop_offset, disc_loop_start);
|
||||
|
||||
patch(rom, jif_text_end_offset, text_loop_end);
|
||||
patch(rom, jif_text_alt_offset, text_alt_target);
|
||||
patch(rom, jmp_text_join_offset, text_join_target);
|
||||
patch(rom, jmp_text_loop_offset, text_loop_start);
|
||||
|
||||
patch(rom, jif_bind_done_offset, bind_done_target);
|
||||
patch(rom, jif_row_end_offset, row_loop_end);
|
||||
patch(rom, jif_col_end_offset, col_loop_end);
|
||||
patch(rom, jmp_col_loop_offset, col_loop_start);
|
||||
patch(rom, jmp_row_loop_offset, row_loop_start);
|
||||
patch(rom, jif_log_offset, after_log);
|
||||
}
|
||||
|
||||
fn build_assets_pack() -> Result<Vec<u8>> {
|
||||
let (glyph_entry, glyph_payload) = build_glyph_asset();
|
||||
let scene = build_scene_bank();
|
||||
let scene_payload = encode_scene_payload(&scene);
|
||||
let scene_entry = AssetEntry {
|
||||
asset_id: 1,
|
||||
asset_name: "stress_scene".into(),
|
||||
bank_type: BankType::SCENE,
|
||||
offset: glyph_payload.len() as u64,
|
||||
size: scene_payload.len() as u64,
|
||||
decoded_size: expected_scene_decoded_size(&scene) as u64,
|
||||
codec: AssetCodec::None,
|
||||
metadata: serde_json::json!({}),
|
||||
};
|
||||
|
||||
let asset_table = vec![glyph_entry, scene_entry];
|
||||
let preload =
|
||||
vec![PreloadEntry { asset_id: 0, slot: 0 }, PreloadEntry { asset_id: 1, slot: 0 }];
|
||||
let payload_len = glyph_payload.len() + scene_payload.len();
|
||||
let header = serde_json::to_vec(&AssetsPackHeader { asset_table, preload })?;
|
||||
let payload_offset = (ASSETS_PA_PRELUDE_SIZE + header.len()) as u64;
|
||||
let prelude = AssetsPackPrelude {
|
||||
magic: ASSETS_PA_MAGIC,
|
||||
schema_version: ASSETS_PA_SCHEMA_VERSION,
|
||||
header_len: header.len() as u32,
|
||||
payload_offset,
|
||||
flags: 0,
|
||||
reserved: 0,
|
||||
header_checksum: 0,
|
||||
};
|
||||
|
||||
let mut bytes = prelude.to_bytes().to_vec();
|
||||
bytes.extend_from_slice(&header);
|
||||
bytes.extend_from_slice(&glyph_payload);
|
||||
bytes.extend_from_slice(&scene_payload);
|
||||
debug_assert_eq!(bytes.len(), payload_offset as usize + payload_len);
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn build_glyph_asset() -> (AssetEntry, Vec<u8>) {
|
||||
let pixel_indices = vec![1_u8; 8 * 8];
|
||||
let mut payload = pack_4bpp(&pixel_indices);
|
||||
payload.extend_from_slice(&build_palette_bytes());
|
||||
|
||||
let entry = AssetEntry {
|
||||
asset_id: 0,
|
||||
asset_name: "stress_square".into(),
|
||||
bank_type: BankType::GLYPH,
|
||||
offset: 0,
|
||||
size: payload.len() as u64,
|
||||
decoded_size: (8 * 8 + GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * 2)
|
||||
as u64,
|
||||
codec: AssetCodec::None,
|
||||
metadata: serde_json::json!({
|
||||
"tile_size": 8,
|
||||
"width": 8,
|
||||
"height": 8,
|
||||
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
||||
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
||||
}),
|
||||
};
|
||||
|
||||
(entry, payload)
|
||||
}
|
||||
|
||||
fn build_palette_bytes() -> Vec<u8> {
|
||||
let mut bytes =
|
||||
Vec::with_capacity(GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * 2);
|
||||
for palette_id in 0..GLYPH_BANK_PALETTE_COUNT_V1 {
|
||||
for color_index in 0..GLYPH_BANK_COLORS_PER_PALETTE {
|
||||
let color = if color_index == 1 { stress_color(palette_id) } else { Color::BLACK };
|
||||
bytes.extend_from_slice(&color.raw().to_le_bytes());
|
||||
}
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
fn stress_color(palette_id: usize) -> Color {
|
||||
let r = ((palette_id * 53) % 256) as u8;
|
||||
let g = ((palette_id * 97 + 64) % 256) as u8;
|
||||
let b = ((palette_id * 29 + 128) % 256) as u8;
|
||||
Color::rgb(r, g, b)
|
||||
}
|
||||
|
||||
fn pack_4bpp(indices: &[u8]) -> Vec<u8> {
|
||||
let mut packed = Vec::with_capacity(indices.len().div_ceil(2));
|
||||
for chunk in indices.chunks(2) {
|
||||
let hi = chunk[0] & 0x0f;
|
||||
let lo = chunk.get(1).copied().unwrap_or(0) & 0x0f;
|
||||
packed.push((hi << 4) | lo);
|
||||
}
|
||||
packed
|
||||
}
|
||||
|
||||
fn build_scene_bank() -> SceneBank {
|
||||
let mut layers = std::array::from_fn(|layer_index| {
|
||||
let mut tiles = vec![
|
||||
Tile {
|
||||
active: false,
|
||||
glyph: Glyph { glyph_id: 0, palette_id: layer_index as u8 + 1 },
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
};
|
||||
64 * 32
|
||||
];
|
||||
|
||||
for step in 0..8 {
|
||||
let x = 4 + step * 7 + layer_index * 2;
|
||||
let y = 2 + step * 3 + layer_index * 2;
|
||||
let index = y * 64 + x;
|
||||
tiles[index] = Tile {
|
||||
active: true,
|
||||
glyph: Glyph { glyph_id: 0, palette_id: layer_index as u8 + 1 },
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
};
|
||||
}
|
||||
|
||||
SceneLayer {
|
||||
active: true,
|
||||
glyph_bank_id: 0,
|
||||
tile_size: TileSize::Size8,
|
||||
parallax_factor: match layer_index {
|
||||
0 => ParallaxFactor { x: 1.0, y: 1.0 },
|
||||
1 => ParallaxFactor { x: 0.75, y: 0.75 },
|
||||
2 => ParallaxFactor { x: 0.5, y: 0.5 },
|
||||
_ => ParallaxFactor { x: 0.25, y: 0.25 },
|
||||
},
|
||||
tilemap: TileMap { width: 64, height: 32, tiles },
|
||||
}
|
||||
});
|
||||
|
||||
// Keep the farthest layer a bit sparser so the diagonal remains visually readable.
|
||||
for step in 0..4 {
|
||||
let x = 10 + step * 12;
|
||||
let y = 4 + step * 5;
|
||||
let index = y * 64 + x;
|
||||
layers[3].tilemap.tiles[index].active = false;
|
||||
}
|
||||
|
||||
SceneBank { layers }
|
||||
}
|
||||
|
||||
fn expected_scene_decoded_size(scene: &SceneBank) -> usize {
|
||||
scene
|
||||
.layers
|
||||
.iter()
|
||||
.map(|layer| {
|
||||
SCENE_DECODED_LAYER_OVERHEAD_BYTES_V1 + layer.tilemap.tiles.len() * size_of::<Tile>()
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn encode_scene_payload(scene: &SceneBank) -> Vec<u8> {
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&SCENE_PAYLOAD_MAGIC_V1);
|
||||
data.extend_from_slice(&SCENE_PAYLOAD_VERSION_V1.to_le_bytes());
|
||||
data.extend_from_slice(&(SCENE_LAYER_COUNT_V1 as u16).to_le_bytes());
|
||||
data.extend_from_slice(&0_u32.to_le_bytes());
|
||||
|
||||
for layer in &scene.layers {
|
||||
let layer_flags = if layer.active { 0b0000_0001 } else { 0 };
|
||||
data.push(layer_flags);
|
||||
data.push(layer.glyph_bank_id);
|
||||
data.push(layer.tile_size as u8);
|
||||
data.push(0);
|
||||
data.extend_from_slice(&layer.parallax_factor.x.to_le_bytes());
|
||||
data.extend_from_slice(&layer.parallax_factor.y.to_le_bytes());
|
||||
data.extend_from_slice(&(layer.tilemap.width as u32).to_le_bytes());
|
||||
data.extend_from_slice(&(layer.tilemap.height as u32).to_le_bytes());
|
||||
data.extend_from_slice(&(layer.tilemap.tiles.len() as u32).to_le_bytes());
|
||||
data.extend_from_slice(&0_u32.to_le_bytes());
|
||||
|
||||
for tile in &layer.tilemap.tiles {
|
||||
let mut tile_flags = 0_u8;
|
||||
if tile.active {
|
||||
tile_flags |= 0b0000_0001;
|
||||
}
|
||||
if tile.flip_x {
|
||||
tile_flags |= 0b0000_0010;
|
||||
}
|
||||
if tile.flip_y {
|
||||
tile_flags |= 0b0000_0100;
|
||||
}
|
||||
data.push(tile_flags);
|
||||
data.push(tile.glyph.palette_id);
|
||||
data.extend_from_slice(&tile.glyph.glyph_id.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
data
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn assets_pack_contains_preloaded_glyph_and_scene_assets() {
|
||||
let bytes = build_assets_pack().expect("assets pack");
|
||||
let prelude =
|
||||
AssetsPackPrelude::from_bytes(&bytes[..ASSETS_PA_PRELUDE_SIZE]).expect("prelude");
|
||||
assert_eq!(prelude.magic, ASSETS_PA_MAGIC);
|
||||
assert_eq!(prelude.schema_version, ASSETS_PA_SCHEMA_VERSION);
|
||||
|
||||
let header_start = ASSETS_PA_PRELUDE_SIZE;
|
||||
let header_end = header_start + prelude.header_len as usize;
|
||||
let header: AssetsPackHeader =
|
||||
serde_json::from_slice(&bytes[header_start..header_end]).expect("header");
|
||||
|
||||
assert_eq!(header.asset_table.len(), 2);
|
||||
assert_eq!(header.preload.len(), 2);
|
||||
assert_eq!(header.asset_table[0].bank_type, BankType::GLYPH);
|
||||
assert_eq!(header.asset_table[1].bank_type, BankType::SCENE);
|
||||
assert_eq!(header.preload[0].slot, 0);
|
||||
assert_eq!(header.preload[1].slot, 0);
|
||||
assert_eq!(header.asset_table[0].offset, 0);
|
||||
assert_eq!(header.asset_table[1].offset, header.asset_table[0].size);
|
||||
assert_eq!(
|
||||
bytes.len(),
|
||||
prelude.payload_offset as usize
|
||||
+ header.asset_table[0].size as usize
|
||||
+ header.asset_table[1].size as usize
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
{"type":"meta","next_id":{"DSC":27,"AGD":27,"DEC":15,"PLN":22,"LSN":31,"CLSN":1}}
|
||||
{"type":"meta","next_id":{"DSC":29,"AGD":29,"DEC":17,"PLN":30,"LSN":31,"CLSN":1}}
|
||||
{"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":"workflow/agendas/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":"workflow/decisions/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":"workflow/plans/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":"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-0020","status":"done","ticket":"jenkins-gitea-integration","title":"Jenkins Gitea Integration and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins","gitea"],"agendas":[{"id":"AGD-0018","file":"workflow/agendas/AGD-0018-jenkins-gitea-integration-and-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0003","file":"workflow/decisions/DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"workflow/plans/PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
||||
{"type":"discussion","id":"DSC-0021","status":"done","ticket":"asset-entry-codec-enum-with-metadata","title":"Asset Entry Codec Enum Contract","created_at":"2026-04-09","updated_at":"2026-04-09","tags":["asset","runtime","codec","metadata"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"lessons/DSC-0021-asset-entry-codec-enum-contract/LSN-0024-string-on-the-wire-enum-in-runtime.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
|
||||
@ -19,6 +19,8 @@
|
||||
{"type":"discussion","id":"DSC-0024","status":"done","ticket":"generic-memory-bank-slot-contract","title":"Agenda - Generic Memory Bank Slot Contract","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["runtime","asset","memory-bank","slots","host"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0029","file":"lessons/DSC-0024-generic-memory-bank-slot-contract/LSN-0029-slot-first-bank-telemetry-belongs-in-asset-manager.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
||||
{"type":"discussion","id":"DSC-0025","status":"done","ticket":"scene-bank-and-viewport-cache-refactor","title":"Scene Bank and Viewport Cache Refactor","created_at":"2026-04-11","updated_at":"2026-04-14","tags":["gfx","tilemap","runtime","render"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0030","file":"lessons/DSC-0025-scene-bank-and-viewport-cache-refactor/LSN-0030-canonical-scene-cache-and-resolver-split.md","status":"done","created_at":"2026-04-14","updated_at":"2026-04-14"}]}
|
||||
{"type":"discussion","id":"DSC-0026","status":"open","ticket":"render-all-scene-cache-and-camera-integration","title":"Integrate render_all with Scene Cache and Camera","created_at":"2026-04-14","updated_at":"2026-04-15","tags":["gfx","runtime","render","camera","scene"],"agendas":[{"id":"AGD-0026","file":"workflow/agendas/AGD-0026-render-all-scene-cache-and-camera-integration.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15"}],"decisions":[{"id":"DEC-0014","file":"workflow/decisions/DEC-0014-frame-composer-render-integration.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_agenda":"AGD-0026"}],"plans":[{"id":"PLN-0017","file":"workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0014"]},{"id":"PLN-0018","file":"workflow/plans/PLN-0018-sprite-controller-and-frame-emission-model.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-14","ref_decisions":["DEC-0014"]},{"id":"PLN-0019","file":"workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0014"]},{"id":"PLN-0020","file":"workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0014"]},{"id":"PLN-0021","file":"workflow/plans/PLN-0021-service-retirement-callsite-migration-and-regression.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0014"]}],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0027","status":"open","ticket":"frame-composer-public-syscall-surface","title":"Agenda - FrameComposer Public Syscall Surface","created_at":"2026-04-17","updated_at":"2026-04-17","tags":["gfx","runtime","syscall","abi","frame-composer","scene","camera","sprites"],"agendas":[{"id":"AGD-0027","file":"workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17"}],"decisions":[{"id":"DEC-0015","file":"workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_agenda":"AGD-0027"}],"plans":[{"id":"PLN-0022","file":"workflow/plans/PLN-0022-composer-syscall-domain-and-spec-propagation.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0023","file":"workflow/plans/PLN-0023-composer-runtime-dispatch-and-legacy-removal.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0024","file":"workflow/plans/PLN-0024-composer-cartridge-tooling-and-regression-migration.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0025","file":"workflow/plans/PLN-0025-final-ci-validation-and-polish.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]}],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0028","status":"open","ticket":"deferred-overlay-and-primitive-composition","title":"Deferred Overlay and Primitive Composition over FrameComposer","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["gfx","runtime","render","frame-composer","overlay","primitives","hud"],"agendas":[{"id":"AGD-0028","file":"workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18"}],"decisions":[{"id":"DEC-0016","file":"workflow/decisions/DEC-0016-deferred-gfx-overlay-outside-frame-composer.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_agenda":"AGD-0028"}],"plans":[{"id":"PLN-0026","file":"workflow/plans/PLN-0026-gfx-overlay-contract-and-spec-propagation.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0027","file":"workflow/plans/PLN-0027-deferred-gfx-overlay-subsystem.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0028","file":"workflow/plans/PLN-0028-runtime-frame-end-overlay-integration-and-parity.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0029","file":"workflow/plans/PLN-0029-final-overlay-ci-validation-and-polish.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]}],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0014","status":"open","ticket":"perf-vm-allocation-and-copy-pressure","title":"Agenda - [PERF] VM Allocation and Copy Pressure","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0013","file":"workflow/agendas/AGD-0013-perf-vm-allocation-and-copy-pressure.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0015","status":"open","ticket":"perf-cartridge-boot-and-program-ownership","title":"Agenda - [PERF] Cartridge Boot and Program Ownership","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0014","file":"workflow/agendas/AGD-0014-perf-cartridge-boot-and-program-ownership.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0016","status":"done","ticket":"tilemap-empty-cell-vs-tile-id-zero","title":"Tilemap Empty Cell vs Tile ID Zero","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0015","file":"workflow/agendas/AGD-0015-tilemap-empty-cell-vs-tile-id-zero.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0022","file":"lessons/DSC-0016-tilemap-empty-cell-semantics/LSN-0022-tilemap-empty-cell-convergence.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
|
||||
|
||||
@ -0,0 +1,214 @@
|
||||
---
|
||||
id: AGD-0027
|
||||
ticket: frame-composer-public-syscall-surface
|
||||
title: Agenda - FrameComposer Public Syscall Surface
|
||||
status: accepted
|
||||
created: 2026-04-17
|
||||
updated: 2026-04-17
|
||||
tags: [gfx, runtime, syscall, abi, frame-composer, scene, camera, sprites]
|
||||
---
|
||||
|
||||
## Contexto
|
||||
|
||||
`DEC-0014` e os planos `PLN-0017` a `PLN-0021` fecharam a migração interna do pipeline de frame para `FrameComposer`:
|
||||
|
||||
- `FrameComposer` virou o orquestrador canônico do frame;
|
||||
- `Hardware` passou a agregá-lo ao lado de `Gfx`;
|
||||
- scene, camera, cache, resolver e sprite emission migraram para ownership interno dele;
|
||||
- o frame loop do runtime passou a renderizar via `FrameComposer.render_frame()`.
|
||||
|
||||
Isso resolveu a base operacional interna, mas não fechou a superfície pública equivalente para a VM. A ABI pública ainda expõe apenas o contrato legado de `gfx.set_sprite(...)`, enquanto `bind_scene(...)` e `set_camera(...)` existem apenas como APIs internas do driver.
|
||||
|
||||
Na prática, hoje temos uma assimetria:
|
||||
|
||||
- a base canônica do frame está em `FrameComposer`;
|
||||
- mas a ABI pública ainda não trata `FrameComposer` como serviço canônico para scene, camera e sprites.
|
||||
|
||||
Essa lacuna impede a migração do restante da stack e também impede um stress cartridge que atravesse de verdade o pipeline novo por syscall pública.
|
||||
|
||||
## Problema
|
||||
|
||||
Precisamos definir a nova superfície pública de syscall para o pipeline canônico de `FrameComposer` sem reabrir a decisão já aceita sobre ownership interno do frame.
|
||||
|
||||
O problema concreto não é “adicionar 2 ou 3 syscalls”. Precisamos decidir:
|
||||
|
||||
- quais operações de `FrameComposer` viram ABI pública agora;
|
||||
- se `gfx.set_sprite(...)` continua como shim legado ou perde status canônico;
|
||||
- qual é o contrato mínimo de scene/camera que a VM pode observar/controlar;
|
||||
- como nomear e versionar essa superfície pública sem criar um segundo modelo canônico concorrente;
|
||||
- qual é a estratégia de transição para cartridge, runtime tests e stress tests;
|
||||
- como propagar essa mudança para a spec canônica e, se necessário, para contratos de ABI e `ISA_CORE`.
|
||||
|
||||
## Pontos Criticos
|
||||
|
||||
- `DEC-0014` já fechou `FrameComposer` como base canônica interna; esta agenda não deve reabrir isso.
|
||||
- A ABI pública atual ainda expõe `gfx.set_sprite(...)` com semântica herdada de índice/slot, mesmo que a implementação interna já use frame emission.
|
||||
- `bind_scene(scene_bank_id)` e `set_camera(x, y)` já existem no driver, mas ainda não existem como syscalls públicas.
|
||||
- Se a nova ABI expuser demais logo de início, vamos congelar cedo demais detalhes que ainda não provaram valor operacional.
|
||||
- Se a nova ABI expuser de menos, manteremos um modelo híbrido por tempo demais:
|
||||
- canônico internamente via `FrameComposer`;
|
||||
- legado externamente via `Gfx`/`set_sprite`.
|
||||
- Precisamos decidir se o namespace público continua em `gfx.*` por estabilidade do domínio, ou se devemos introduzir algo como `frame.*`.
|
||||
- A transição precisa preservar compatibilidade suficiente para não quebrar cartridges e testes existentes antes da migração do restante.
|
||||
- O contrato de sprite precisa deixar claro se o chamador ainda informa índice, se informa `layer`, e se `active` continua existindo na superfície pública.
|
||||
- A mudança não pode ficar só em código/runtime; a spec canônica precisa ser atualizada para refletir o novo serviço público.
|
||||
- Se o contrato público afetar superfícies documentadas de ABI ou o material de `ISA_CORE`, essa propagação precisa ser tratada como parte da mesma thread, não como follow-up solto.
|
||||
|
||||
## Opcoes
|
||||
|
||||
### Opcao 1 - Expor um núcleo mínimo canônico em `gfx.*`
|
||||
|
||||
**Como seria:**
|
||||
Adicionar apenas a superfície mínima para a VM controlar o pipeline novo:
|
||||
|
||||
- `gfx.bind_scene(bank_id)`
|
||||
- `gfx.unbind_scene()`
|
||||
- `gfx.set_camera(x, y)`
|
||||
- `gfx.emit_sprite(...)`
|
||||
|
||||
`gfx.set_sprite(...)` permaneceria por um período como shim legado de compatibilidade.
|
||||
|
||||
**Vantagens:**
|
||||
- fecha rapidamente a lacuna operacional;
|
||||
- habilita stress real do pipeline novo;
|
||||
- reduz o tempo de convivência entre modelo canônico e legado;
|
||||
- mantém o domínio público em `gfx`, evitando churn de namespace.
|
||||
|
||||
**Desvantagens:**
|
||||
- introduz ABI nova que precisará de migração coordenada;
|
||||
- exige definir `emit_sprite(...)` com cuidado para não herdar sem querer o modelo de slot.
|
||||
|
||||
### Opcao 2 - Expor scene/camera agora e adiar o contrato novo de sprite
|
||||
|
||||
**Como seria:**
|
||||
Publicar apenas:
|
||||
|
||||
- `gfx.bind_scene(bank_id)`
|
||||
- `gfx.unbind_scene()`
|
||||
- `gfx.set_camera(x, y)`
|
||||
|
||||
Sprites continuariam publicamente via `gfx.set_sprite(...)` até uma segunda fase.
|
||||
|
||||
**Vantagens:**
|
||||
- menor mudança imediata de ABI;
|
||||
- desbloqueia o stress do world path e da câmera;
|
||||
- reduz o volume inicial da migração pública.
|
||||
|
||||
**Desvantagens:**
|
||||
- mantém dois modelos públicos de sprite por mais tempo;
|
||||
- prolonga a semântica de compatibilidade do syscall legado;
|
||||
- adia exatamente uma das partes centrais da migração para `FrameComposer`.
|
||||
|
||||
### Opcao 3 - Criar um novo namespace público separado, como `composer.*`
|
||||
|
||||
**Como seria:**
|
||||
O pipeline novo ganha syscalls em um domínio separado, por exemplo:
|
||||
|
||||
- `composer.bind_scene`
|
||||
- `composer.unbind_scene`
|
||||
- `composer.set_camera`
|
||||
- `composer.emit_sprite`
|
||||
|
||||
`gfx.*` ficaria como superfície legacy/low-level.
|
||||
|
||||
**Vantagens:**
|
||||
- deixa explícita a mudança de serviço canônico;
|
||||
- evita sobrecarregar semanticamente `gfx`.
|
||||
|
||||
**Desvantagens:**
|
||||
- adiciona churn conceitual e de nomenclatura;
|
||||
- fragmenta demais a superfície pública neste momento;
|
||||
- cria um custo de transição maior sem benefício operacional evidente.
|
||||
|
||||
## Sugestao / Recomendacao
|
||||
|
||||
Seguir com a **Opcao 3**.
|
||||
|
||||
Direção recomendada:
|
||||
|
||||
- a superfície pública canônica deve migrar para o domínio `composer.*`;
|
||||
- `FrameComposer` vira a base canônica também na ABI pública, com namespace próprio em vez de continuar semanticamente preso a `gfx.*`;
|
||||
- o núcleo mínimo público deve ser:
|
||||
- `composer.bind_scene(bank_id) -> status`
|
||||
- `composer.unbind_scene()`
|
||||
- `composer.set_camera(x, y)`
|
||||
- `composer.emit_sprite(...) -> status`
|
||||
- `gfx.set_sprite(...)` deve morrer e ser removido completamente do contrato público.
|
||||
|
||||
Para sprites, a recomendação provisória é:
|
||||
|
||||
- a nova ABI pública não deve exigir índice explícito;
|
||||
- `composer.emit_sprite(...)` deve receber o payload completo necessário para o frame:
|
||||
- `glyph_id`
|
||||
- `palette_id`
|
||||
- `x`
|
||||
- `y`
|
||||
- `layer`
|
||||
- `bank_id`
|
||||
- `flip_x`
|
||||
- `flip_y`
|
||||
- `priority`
|
||||
- a ABI pode futuramente agrupar esse payload se isso melhorar ergonomia, mas o contrato mínimo deve nascer completo;
|
||||
- `active` não deve continuar no contrato canônico novo;
|
||||
- overflow continua sendo ignorado com status/telemetria adequada, sem trapar o runtime.
|
||||
|
||||
Para scene/camera, a recomendação provisória é:
|
||||
|
||||
- manter o contrato mínimo já aceito internamente;
|
||||
- `bind_scene` por bank id;
|
||||
- `unbind_scene` explícito;
|
||||
- `set_camera(x, y)` em pixel space com top-left viewport.
|
||||
- `bind_scene(...)`, `unbind_scene(...)` e `emit_sprite(...)` devem usar `ComposerOpStatus` como retorno operacional canônico.
|
||||
|
||||
## Perguntas em Aberto
|
||||
|
||||
- Resolvido:
|
||||
- o nome público canônico de sprite será `composer.emit_sprite(...)`;
|
||||
- o syscall novo de sprite nasce completo com `glyph_id`, `palette_id`, `x`, `y`, `layer`, `bank_id`, `flip_x`, `flip_y`, `priority`;
|
||||
- `gfx.set_sprite(...)` deve morrer e ser removido completamente;
|
||||
- não haverá leitura de estado nesta primeira fase;
|
||||
- `bind_scene(...)`, `unbind_scene(...)` e `emit_sprite(...)` usarão `ComposerOpStatus`;
|
||||
- A ABI nova precisa expor refresh explícito, ou isso deve continuar totalmente interno ao `FrameComposer`?
|
||||
- Resolvido:
|
||||
- a ABI nova não deve expor refresh explícito;
|
||||
- o domínio público canônico será `composer.*`, não `gfx.*`.
|
||||
|
||||
## Criterio para Encerrar
|
||||
|
||||
Esta agenda pode ser encerrada quando houver acordo explícito sobre:
|
||||
|
||||
- a lista mínima de syscalls públicas canônicas do `FrameComposer`;
|
||||
- o nome canônico da operação pública de sprite;
|
||||
- a remoção completa de `gfx.set_sprite(...)` do contrato público;
|
||||
- o formato de retorno/status das novas operações;
|
||||
- a estratégia de transição necessária para decisão, plano e migração do restante da stack.
|
||||
|
||||
## Resolucao em Andamento
|
||||
|
||||
Direção atualmente acordada nesta agenda:
|
||||
|
||||
- o namespace público canônico será `composer.*`;
|
||||
- o núcleo mínimo inicial será:
|
||||
- `composer.bind_scene(bank_id) -> ComposerOpStatus`
|
||||
- `composer.unbind_scene() -> ComposerOpStatus`
|
||||
- `composer.set_camera(x, y)`
|
||||
- `composer.emit_sprite(glyph_id, palette_id, x, y, layer, bank_id, flip_x, flip_y, priority) -> ComposerOpStatus`
|
||||
- não haverá introspecção pública nesta primeira fase;
|
||||
- refresh/cache policy continua interno ao `FrameComposer`;
|
||||
- `gfx.set_sprite(...)` não terá caminho de compatibilidade e deve ser removido.
|
||||
|
||||
## Resolucao
|
||||
|
||||
Esta agenda fica aceita com os seguintes pontos fechados:
|
||||
|
||||
- o namespace público canônico do serviço será `composer.*`;
|
||||
- a superfície mínima inicial será:
|
||||
- `composer.bind_scene(bank_id) -> ComposerOpStatus`
|
||||
- `composer.unbind_scene() -> ComposerOpStatus`
|
||||
- `composer.set_camera(x, y)`
|
||||
- `composer.emit_sprite(glyph_id, palette_id, x, y, layer, bank_id, flip_x, flip_y, priority) -> ComposerOpStatus`
|
||||
- não haverá introspecção pública nesta primeira fase;
|
||||
- não haverá refresh/cache policy público;
|
||||
- `gfx.set_sprite(...)` deve ser removido completamente, sem shim de compatibilidade;
|
||||
- a transição deve introduzir `composer.*` e remover `gfx.set_sprite(...)` na mesma thread de migração, com atualização coordenada de bytecode, cartridges, tests e runtime;
|
||||
- a mesma thread deve atualizar a spec canônica do assunto e propagar a mudança para contratos de ABI e `ISA_CORE` quando essas superfícies forem impactadas pelo novo serviço público.
|
||||
@ -0,0 +1,140 @@
|
||||
---
|
||||
id: AGD-0028
|
||||
ticket: deferred-overlay-and-primitive-composition
|
||||
title: Deferred Overlay and Primitive Composition over FrameComposer
|
||||
status: accepted
|
||||
created: 2026-04-18
|
||||
updated: 2026-04-18
|
||||
resolved: 2026-04-18
|
||||
decision: DEC-0016
|
||||
tags: [gfx, runtime, render, frame-composer, overlay, primitives, hud]
|
||||
---
|
||||
|
||||
## Contexto
|
||||
|
||||
`FrameComposer.render_frame()` hoje recompõe o `back` no fim da logical frame. Quando há scene bound, o caminho `render_scene_from_cache(...)` limpa o buffer e desenha scene + sprites, o que apaga qualquer primitive ou `draw_text(...)` emitido antes via `gfx`.
|
||||
|
||||
Isso expôs um conflito de modelo:
|
||||
|
||||
- `composer.*` já é o caminho canônico de orquestração de frame;
|
||||
- `gfx.draw_text(...)` e demais primitives ainda escrevem diretamente no `back`;
|
||||
- o runtime só chama `render_frame()` no final do frame, então a escrita imediata em `back` deixou de ser semanticamente estável.
|
||||
- As primitives de `gfx` não são o mecanismo desejado para composição de jogos com scene/tile/sprite; elas existem principalmente como debug, instrumentação visual e artefatos rápidos.
|
||||
|
||||
Conteúdo relevante migrado de [AGD-0010](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/agendas/AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md):
|
||||
|
||||
- a arquitetura aceita continua sendo de framebuffer destrutivo em memória, não scene graph ou renderer tipo GPU;
|
||||
- otimizações em primitives devem preservar a semântica observável, mesmo quando ganharem fast paths internos;
|
||||
- existe preocupação explícita com custo por classe de primitive e com orçamento de memória no alvo handheld;
|
||||
- caminhos de spans/linhas/clears são desejáveis como aceleração interna, mas sem reabrir o modelo operacional do pipeline do jogo.
|
||||
|
||||
## Problema
|
||||
|
||||
Precisamos decidir qual é o modelo canônico para primitives e texto no pipeline pós-`FrameComposer`.
|
||||
|
||||
Sem isso:
|
||||
|
||||
- texto e primitives continuam com comportamento dependente da ordem interna do renderer;
|
||||
- o stress test e qualquer cartridge que combine `composer.*` com `gfx.*` terão resultado inconsistente;
|
||||
- fica indefinido se primitives pertencem ao mundo, ao HUD, ou a um overlay final.
|
||||
|
||||
## Pontos Criticos
|
||||
|
||||
- `draw_text(...)` e primitives screen-space não podem depender de escrita imediata em `back`.
|
||||
- Para esta thread, primitives de `gfx` devem permanecer agnósticas ao pipeline canônico de render do jogo e não devem ser mescladas semanticamente com tiles/sprites.
|
||||
- A ordem de composição precisa ser explícita e estável: `scene -> sprites -> HUD -> primitives/debug overlay`, ou outra ordem formal equivalente.
|
||||
- Precisamos decidir se o contrato público de `gfx.*` muda semanticamente sem mudar ABI, ou se parte dessa superfície migra para `composer.*`.
|
||||
- A solução deve preservar o caminho sem scene bound.
|
||||
- A implementação deve evitar contaminar a infraestrutura de `gfx` responsável por scene, sprites e HUD com estado misto de overlay/debug; se necessário, o overlay deve ter fila/fase própria.
|
||||
- melhorias internas de primitive path devem continuar permitidas, desde que não mudem a semântica de overlay final e não exijam buffers extras incompatíveis com o orçamento de memória aceito.
|
||||
|
||||
## Opcoes
|
||||
|
||||
### Opcao 1 - Manter escrita direta em `back`
|
||||
|
||||
- **Abordagem:** manter `gfx.draw_text(...)` e primitives rasterizando imediatamente.
|
||||
- **Pro:** zero mudança estrutural agora.
|
||||
- **Contra:** o modelo continua quebrado sempre que `render_frame()` recompõe o buffer depois.
|
||||
- **Tradeoff:** só funciona de forma confiável fora do caminho canônico do `FrameComposer`.
|
||||
|
||||
### Opcao 2 - Fila única de draw commands pós-scene/pós-sprite
|
||||
|
||||
- **Abordagem:** transformar texto e primitives em comandos diferidos, drenados depois de `scene + sprites`.
|
||||
- **Pro:** resolve o problema imediato de overlay/HUD e estabiliza o stress test.
|
||||
- **Contra:** mistura HUD e primitives/debug sob o mesmo conceito, reduzindo clareza contratual mesmo quando a ordem prática for a mesma.
|
||||
- **Tradeoff:** simples para V1, mas semanticamente mais fraco do que separar overlay de jogo e overlay de debug.
|
||||
|
||||
### Opcao 3 - Separar HUD diferido de primitives/debug overlay final
|
||||
|
||||
- **Abordagem:** tratar `gfx.draw_text(...)` e demais primitives de `gfx` como overlay/debug final, separado da composição canônica de jogo (`scene + sprites + HUD`).
|
||||
- **Pro:** casa com a intenção declarada para `gfx.*`: debug, artefato rápido e instrumentação visual acima do frame do jogo.
|
||||
- **Contra:** exige modelar explicitamente uma fase extra no pipeline.
|
||||
- **Tradeoff:** aumenta a clareza contratual e evita mesclar primitives com o domínio de jogo.
|
||||
|
||||
### Opcao 4 - Manter HUD e primitives no mesmo estágio final, mas com categorias separadas
|
||||
|
||||
- **Abordagem:** drenar HUD e primitives ambos no fim do frame, porém com filas/categorias distintas e ordem formal `HUD -> primitives`.
|
||||
- **Pro:** preserva implementação próxima entre caminhos similares, mantendo contrato separado.
|
||||
- **Contra:** é mais custoso que a opção 3 sem entregar muito valor adicional imediato.
|
||||
- **Tradeoff:** bom se já houver expectativa de HUD canônico separado no curtíssimo prazo.
|
||||
|
||||
## Sugestao / Recomendacao
|
||||
|
||||
Seguir com a **Opcao 3**.
|
||||
|
||||
Minha recomendação é:
|
||||
|
||||
- retirar a escrita direta em `back` como contrato operacional para `gfx.draw_text(...)` e demais primitives de `gfx`;
|
||||
- introduzir uma fila diferida canônica de primitives/debug overlay drenada no fim do frame;
|
||||
- tratar `gfx.*` primitive/text como superfície agnóstica ao pipeline de jogo e explicitamente acima da composição canônica;
|
||||
- não misturar semanticamente primitives com scene/tile/sprite/HUD.
|
||||
- evitar compartilhar indevidamente o mesmo mecanismo operacional de composição entre overlay/debug e os caminhos de scene/sprite/HUD, mesmo quando o backend de rasterização reutilizado for o mesmo.
|
||||
|
||||
Ordem recomendada para o frame canônico:
|
||||
|
||||
1. limpar/compor scene;
|
||||
2. compor sprites;
|
||||
3. compor HUD canônico, se existir;
|
||||
4. aplicar `scene_fade`;
|
||||
5. aplicar `hud_fade`;
|
||||
6. drenar primitives/debug overlay de `gfx.*`.
|
||||
|
||||
## Perguntas em Aberto
|
||||
|
||||
- `draw_text(...)` e as demais primitives de `gfx` entram todas na mesma família de overlay final já na V1, ou começamos só com `draw_text(...)`?
|
||||
- `render_no_scene_frame()` deve usar a mesma fila diferida para manter semântica idêntica com e sem scene?
|
||||
- HUD canônico precisa existir explicitamente nesta mesma thread, ou pode continuar implícito/externo enquanto as primitives já migram para overlay final?
|
||||
- quais fast paths internos de primitives continuam desejáveis nessa nova fase, por exemplo spans horizontais/verticais, fills e clears, sem misturar isso com a composição do jogo?
|
||||
- o overlay/debug final precisa de dirtying próprio por classe de primitive ou isso pode ficar fora da primeira migração?
|
||||
|
||||
## Criterio para Encerrar
|
||||
|
||||
Esta agenda pode ser encerrada quando tivermos uma resposta explícita para:
|
||||
|
||||
- o destino semântico de `draw_text(...)`;
|
||||
- se haverá uma fila própria para primitives/debug overlay e qual a relação dela com HUD;
|
||||
- a ordem canônica de composição do frame;
|
||||
- o escopo exato da primeira migração implementável sem reabrir o restante do pipeline.
|
||||
|
||||
## Resolucao Parcial
|
||||
|
||||
Direção já aceita nesta agenda:
|
||||
|
||||
- primitives e `draw_text(...)` de `gfx.*` devem ser tratadas como overlay/debug final;
|
||||
- esse overlay deve ser drenado **depois** de `hud_fade`;
|
||||
- scene, sprites e HUD canônico não devem ser semanticamente misturados com o overlay/debug;
|
||||
- a implementação deve preservar separação operacional suficiente para que o `gfx` usado pelo pipeline do jogo não passe a depender do estado transitório de primitives/debug;
|
||||
- otimizações de primitive path discutidas na `AGD-0010` continuam válidas, mas passam a operar dentro do domínio de overlay/debug final, não como parte da composição canônica de scene/sprite/HUD.
|
||||
|
||||
## Resolucao
|
||||
|
||||
Esta agenda fica aceita com os seguintes pontos fechados:
|
||||
|
||||
- `gfx.draw_text(...)` e as demais primitives públicas de `gfx.*` pertencem à mesma família V1 de overlay/debug final;
|
||||
- esse overlay/debug fica **fora** do `FrameComposer`;
|
||||
- `FrameComposer` continua restrito à composição canônica do jogo (`scene`, `sprites` e HUD canônico quando existir);
|
||||
- o overlay/debug deve ser drenado depois de `hud_fade`;
|
||||
- o caminho sem scene bound deve observar a mesma semântica final de overlay/debug;
|
||||
- HUD canônico explícito não faz parte desta thread e pode permanecer implícito/externo por enquanto;
|
||||
- fast paths internos de primitives continuam permitidos, desde que preservem a semântica observável do overlay/debug final;
|
||||
- dirtying granular ou otimizações finas por classe de primitive não fazem parte da primeira migração normativa desta thread.
|
||||
@ -0,0 +1,166 @@
|
||||
---
|
||||
id: DEC-0015
|
||||
ticket: frame-composer-public-syscall-surface
|
||||
title: FrameComposer Public Syscall Surface
|
||||
status: accepted
|
||||
created: 2026-04-17
|
||||
accepted: 2026-04-17
|
||||
agenda: AGD-0027
|
||||
plans: [PLN-0022, PLN-0023, PLN-0024, PLN-0025]
|
||||
tags: [gfx, runtime, syscall, abi, frame-composer, scene, camera, sprites]
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
Accepted.
|
||||
|
||||
## Contexto
|
||||
|
||||
`DEC-0014` locked `FrameComposer` as the canonical internal frame orchestration service and `PLN-0017` through `PLN-0021` completed that internal migration path. `Hardware` now owns `FrameComposer`, the runtime renders through `FrameComposer.render_frame()`, and scene/camera/cache/resolver/sprite ownership no longer belongs canonically to `Gfx`.
|
||||
|
||||
That migration did not define the equivalent public syscall contract for VM code. The public ABI still exposed legacy `gfx`-domain sprite control while the canonical scene/camera operations existed only as internal driver APIs.
|
||||
|
||||
This decision closes that public ABI gap without reopening the already accepted internal ownership model.
|
||||
|
||||
## Decisao
|
||||
|
||||
The canonical public syscall surface for frame orchestration SHALL move to the `composer.*` namespace.
|
||||
|
||||
Normatively:
|
||||
|
||||
- The canonical public service domain for `FrameComposer` operations SHALL be `composer`.
|
||||
- The initial canonical syscall set SHALL be:
|
||||
- `composer.bind_scene(bank_id) -> ComposerOpStatus`
|
||||
- `composer.unbind_scene() -> ComposerOpStatus`
|
||||
- `composer.set_camera(x, y)`
|
||||
- `composer.emit_sprite(glyph_id, palette_id, x, y, layer, bank_id, flip_x, flip_y, priority) -> ComposerOpStatus`
|
||||
- `composer.emit_sprite(...)` SHALL be the canonical public sprite submission path.
|
||||
- `composer.emit_sprite(...)` MUST NOT require a caller-provided sprite index.
|
||||
- `composer.emit_sprite(...)` MUST carry `layer` and `priority`.
|
||||
- `composer.emit_sprite(...)` MUST NOT expose `active` as part of the canonical contract.
|
||||
- `composer.bind_scene(...)`, `composer.unbind_scene()`, and `composer.emit_sprite(...)` SHALL return `ComposerOpStatus`.
|
||||
- `composer.set_camera(x, y)` SHALL keep the minimal V1 camera contract already accepted by `DEC-0014`:
|
||||
- `x` and `y` are `i32` pixel coordinates;
|
||||
- they represent the top-left viewport origin in world space.
|
||||
- The public ABI MUST NOT expose cache refresh policy or explicit refresh controls.
|
||||
- The public ABI MUST NOT expose scene/camera introspection in this first phase.
|
||||
- `gfx.set_sprite(...)` MUST be removed completely from the public contract.
|
||||
- No compatibility shim for `gfx.set_sprite(...)` SHALL remain as part of the canonical migration target.
|
||||
- Introduction of `composer.*` and removal of `gfx.set_sprite(...)` SHALL be executed in the same migration thread.
|
||||
|
||||
## Rationale
|
||||
|
||||
The public ABI must reflect the accepted ownership model rather than preserve a misleading legacy shape.
|
||||
|
||||
Keeping the canonical public surface under `gfx.*` would continue to tie orchestration semantics to the wrong service boundary. The new namespace makes the ownership change explicit:
|
||||
|
||||
- `Gfx` is the visual backend;
|
||||
- `FrameComposer` is the frame orchestration service.
|
||||
|
||||
Removing `gfx.set_sprite(...)` completely avoids prolonging a dual public sprite model. A compatibility shim would preserve legacy slot/index semantics in the public contract after those semantics had already ceased to be canonical internally.
|
||||
|
||||
Returning `ComposerOpStatus` for operational mutating calls preserves status-first behavior while keeping the public contract aligned with the new service boundary. Reusing `GfxOpStatus` would leak backend-domain semantics into orchestration-domain syscalls after that separation had already been made explicit.
|
||||
|
||||
Deferring introspection and explicit refresh controls keeps the first public ABI focused on control, not diagnostics or internal policy leakage.
|
||||
|
||||
## Invariantes / Contrato
|
||||
|
||||
### 1. Namespace
|
||||
|
||||
- Public frame-orchestration syscalls MUST live under `composer.*`.
|
||||
- `composer.*` SHALL be treated as the canonical public orchestration surface.
|
||||
- `gfx.*` SHALL NOT remain the canonical public orchestration namespace for scene/camera/sprite submission.
|
||||
|
||||
### 2. Scene Control
|
||||
|
||||
- `composer.bind_scene(bank_id)` MUST bind by scene bank id.
|
||||
- Binding semantics MUST remain aligned with `DEC-0014`:
|
||||
- scene resolution through the scene bank pool;
|
||||
- explicit bind/unbind lifecycle;
|
||||
- no implicit per-frame rebinding.
|
||||
- `composer.unbind_scene()` MUST leave no-scene rendering valid.
|
||||
- `ComposerOpStatus` SHALL be the canonical operational status family for composer-domain mutating syscalls.
|
||||
|
||||
### 3. Camera
|
||||
|
||||
- `composer.set_camera(x, y)` MUST remain the minimal V1 camera API.
|
||||
- Camera follow, smoothing, shake, transitions, and readback are OUT OF SCOPE for this decision.
|
||||
|
||||
### 4. Sprite Submission
|
||||
|
||||
- `composer.emit_sprite(...)` MUST be frame-emission based.
|
||||
- The caller MUST NOT provide sprite slot/index information.
|
||||
- The public payload MUST include:
|
||||
- `glyph_id`
|
||||
- `palette_id`
|
||||
- `x`
|
||||
- `y`
|
||||
- `layer`
|
||||
- `bank_id`
|
||||
- `flip_x`
|
||||
- `flip_y`
|
||||
- `priority`
|
||||
- The canonical public sprite contract MUST NOT include `active`.
|
||||
- Overflow behavior SHALL remain aligned with `DEC-0014`:
|
||||
- excess sprites are ignored;
|
||||
- overflow is not a hard VM fault in V1.
|
||||
|
||||
### 5. Non-Goals for V1 Public ABI
|
||||
|
||||
- No public refresh/invalidate syscalls.
|
||||
- No public cache inspection syscalls.
|
||||
- No public `scene_status()` syscall.
|
||||
- No public `get_camera()` syscall.
|
||||
|
||||
### 6. Migration Contract
|
||||
|
||||
- Migration MUST update:
|
||||
- syscall registry and ABI resolution;
|
||||
- runtime dispatch;
|
||||
- bytecode/cartridge declarations;
|
||||
- tests;
|
||||
- stress cartridges and related tooling where applicable.
|
||||
- Migration MUST NOT leave `gfx.set_sprite(...)` as a supported public fallback after the new contract lands.
|
||||
|
||||
## Impactos
|
||||
|
||||
### HAL
|
||||
|
||||
- The syscall enum, registry, metadata, and resolver will need a new `composer` domain surface.
|
||||
- `gfx.set_sprite(...)` must be removed from the public ABI contract.
|
||||
- A new `ComposerOpStatus` contract will need to be introduced for composer-domain operational returns.
|
||||
|
||||
### Runtime / VM
|
||||
|
||||
- Runtime dispatch must route public scene/camera/sprite orchestration through `FrameComposer`.
|
||||
- Existing bytecode declarations and cartridges that rely on `gfx.set_sprite(...)` will need coordinated migration.
|
||||
|
||||
### Spec / ABI / ISA_CORE
|
||||
|
||||
- The canonical spec for the public VM-facing graphics/composition surface must be updated to reflect `composer.*`.
|
||||
- ABI-facing documentation and contracts must be updated wherever syscall domain, names, arguments, or return semantics are specified.
|
||||
- `ISA_CORE` must be updated if and where it normatively references the public syscall surface affected by this decision.
|
||||
|
||||
### Drivers / Hardware
|
||||
|
||||
- `FrameComposer` already has the required internal base; execution work will focus on public ABI exposure rather than internal ownership redesign.
|
||||
|
||||
### Tooling / Stress
|
||||
|
||||
- Stress cartridges and bytecode generators can only exercise the canonical frame path publicly after `composer.*` exists.
|
||||
|
||||
## Referencias
|
||||
|
||||
- [AGD-0027-frame-composer-public-syscall-surface.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md)
|
||||
- [DEC-0014-frame-composer-render-integration.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/decisions/DEC-0014-frame-composer-render-integration.md)
|
||||
|
||||
## Propagacao Necessaria
|
||||
|
||||
- A new implementation plan MUST be created from this decision before code changes.
|
||||
- The plan MUST cover ABI introduction, legacy syscall removal, cartridge/test migration, regression coverage, and canonical spec propagation.
|
||||
- The plan MUST explicitly assess and update ABI and `ISA_CORE` artifacts where this decision changes documented public behavior.
|
||||
- Stress tooling SHOULD be updated as part of the migration thread so the public ABI can exercise the canonical frame path end-to-end.
|
||||
|
||||
## Revision Log
|
||||
|
||||
- 2026-04-17: Initial accepted decision from `AGD-0027`.
|
||||
@ -0,0 +1,150 @@
|
||||
---
|
||||
id: DEC-0016
|
||||
ticket: deferred-overlay-and-primitive-composition
|
||||
title: Deferred GFX Overlay Outside FrameComposer
|
||||
status: accepted
|
||||
created: 2026-04-18
|
||||
accepted: 2026-04-18
|
||||
agenda: AGD-0028
|
||||
plans: [PLN-0026, PLN-0027, PLN-0028, PLN-0029]
|
||||
tags: [gfx, runtime, render, frame-composer, overlay, primitives, hud]
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
Accepted.
|
||||
|
||||
## Contexto
|
||||
|
||||
`DEC-0014` and `DEC-0015` established `FrameComposer` as the canonical orchestration path for game-frame composition and exposed that orchestration publicly through `composer.*`.
|
||||
|
||||
That migration left `gfx.draw_text(...)` and other `gfx` primitives with their historical immediate-write behavior against the working framebuffer. Once the runtime moved to end-of-frame composition through `FrameComposer.render_frame()`, those immediate writes became unstable: scene-backed frame composition can rebuild the backbuffer after primitive calls have already touched it.
|
||||
|
||||
The resulting conflict is not about whether primitives should remain available. It is about their semantic place in the pipeline. The accepted direction of this thread is that `gfx` primitives are not part of the canonical game composition model. They are primarily for debug, quick visual instrumentation, and rapid artifacts, and they must remain agnostic to scene/tile/sprite/HUD composition.
|
||||
|
||||
Relevant performance context migrated from `AGD-0010` also remains in force:
|
||||
|
||||
- the renderer continues to be a destructive software framebuffer model, not a retained scene graph or GPU-style renderer;
|
||||
- internal primitive fast paths remain desirable;
|
||||
- memory growth must remain constrained for the handheld target;
|
||||
- optimization of primitive execution must not alter observable semantics.
|
||||
|
||||
## Decisao
|
||||
|
||||
`gfx.*` primitives and text SHALL move to a deferred final overlay model that lives outside `FrameComposer`.
|
||||
|
||||
Normatively:
|
||||
|
||||
- `FrameComposer` SHALL remain responsible only for canonical game-frame composition:
|
||||
- scene composition;
|
||||
- sprite composition;
|
||||
- canonical HUD composition when such a HUD stage exists.
|
||||
- `FrameComposer` MUST NOT become the owner of debug/primitive overlay state.
|
||||
- Public `gfx.*` primitives, including `gfx.draw_text(...)`, SHALL belong to a V1 `gfx` overlay/debug family.
|
||||
- That overlay/debug family SHALL be deferred rather than written immediately as the stable operational contract.
|
||||
- The deferred overlay/debug stage SHALL be drained after `hud_fade`.
|
||||
- The deferred overlay/debug stage SHALL be above scene, sprites, and canonical HUD in final visual order.
|
||||
- The no-scene path MUST preserve the same final overlay/debug semantics.
|
||||
- `gfx.*` primitives MUST remain semantically separate from scene/tile/sprite/HUD composition.
|
||||
- The implementation MUST preserve operational separation sufficient to prevent the canonical game pipeline from depending on transient primitive/debug state.
|
||||
|
||||
## Rationale
|
||||
|
||||
This decision keeps the architectural boundary clean.
|
||||
|
||||
`FrameComposer` exists to own the canonical game frame. Debug primitives do not belong to that contract. Pulling them into `FrameComposer` would make the orchestration service responsible for a second semantic domain with different goals:
|
||||
|
||||
- game composition must be deterministic and canonical;
|
||||
- primitive/text overlay must be opportunistic, screen-space, and pipeline-agnostic.
|
||||
|
||||
Keeping overlay/debug outside `FrameComposer` also aligns with the stated product intent: these primitives are useful helpers, but they are not meant to become a second composition language for games.
|
||||
|
||||
Draining them after `hud_fade` preserves the user-visible requirement that debug/overlay content stay truly on top and legible. This is more faithful to the accepted intent than treating primitives as part of HUD or world composition.
|
||||
|
||||
Finally, separating semantic ownership still leaves room for implementation reuse. Raster backends, span paths, and buffer-writing helpers may still be shared internally, provided the public operational model remains separate.
|
||||
|
||||
## Invariantes / Contrato
|
||||
|
||||
### 1. Ownership Boundary
|
||||
|
||||
- `FrameComposer` MUST own only canonical game-frame composition.
|
||||
- Primitive/debug overlay state MUST live outside `FrameComposer`.
|
||||
- The canonical game pipeline MUST NOT depend on primitive/debug overlay state for correctness.
|
||||
|
||||
### 2. Overlay Semantics
|
||||
|
||||
- `gfx.draw_text(...)` and sibling `gfx` primitives SHALL be treated as deferred final overlay/debug operations.
|
||||
- Immediate direct writes to `back` MUST NOT remain the stable operational contract for these primitives.
|
||||
- Final overlay/debug output MUST appear after:
|
||||
- scene composition;
|
||||
- sprite composition;
|
||||
- canonical HUD composition, if present;
|
||||
- `scene_fade`;
|
||||
- `hud_fade`.
|
||||
|
||||
### 3. Separation from Game Composition
|
||||
|
||||
- Primitive/debug overlay MUST NOT be reinterpreted as scene content.
|
||||
- Primitive/debug overlay MUST NOT be reinterpreted as sprite content.
|
||||
- Primitive/debug overlay MUST NOT be the vehicle for canonical HUD composition.
|
||||
- The public `gfx.*` primitive surface SHALL remain pipeline-agnostic relative to `composer.*`.
|
||||
|
||||
### 4. Consistency Across Frame Paths
|
||||
|
||||
- The scene-bound path and no-scene path MUST expose the same final overlay/debug behavior.
|
||||
- Users MUST NOT need to know whether a scene is bound for `gfx.*` primitives to appear as final overlay/debug content.
|
||||
|
||||
### 5. Internal Optimization Contract
|
||||
|
||||
- Internal fast paths for lines, spans, fills, clears, or similar primitive operations MAY be introduced.
|
||||
- Such fast paths MUST preserve the observable deferred overlay/debug semantics.
|
||||
- This decision DOES NOT require fine-grained dirtying or per-primitive-class invalidation in the first migration.
|
||||
|
||||
## Impactos
|
||||
|
||||
### Runtime / Drivers
|
||||
|
||||
- The runtime frame-end sequence must gain a distinct overlay/debug drain stage outside `FrameComposer`.
|
||||
- `gfx.draw_text(...)` and peer primitives can no longer rely on stable immediate framebuffer writes once this migration lands.
|
||||
|
||||
### GFX Backend
|
||||
|
||||
- `Gfx` will need an explicit deferred overlay/debug command path or equivalent subsystem boundary.
|
||||
- Shared raster helpers remain allowed, but the overlay/debug phase must stay semantically distinct from scene/sprite/HUD composition.
|
||||
|
||||
### FrameComposer
|
||||
|
||||
- `FrameComposer` must remain free of primitive/debug overlay ownership.
|
||||
- Any future HUD integration must not collapse that boundary.
|
||||
|
||||
### Spec / Docs
|
||||
|
||||
- The canonical graphics/runtime spec must describe `gfx.*` primitives as deferred final overlay/debug operations rather than stable immediate backbuffer writes.
|
||||
- Documentation that describes frame ordering must show overlay/debug after `hud_fade`.
|
||||
|
||||
### Performance Follow-up
|
||||
|
||||
- `AGD-0010` remains the home for broader renderer performance work, dirtying strategy, and low-level primitive optimization policy.
|
||||
- Primitive optimization carried out under that thread must respect the normative separation established here.
|
||||
|
||||
## Referencias
|
||||
|
||||
- [AGD-0028-deferred-overlay-and-primitive-composition.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md)
|
||||
- [AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/agendas/AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md)
|
||||
- [DEC-0014-frame-composer-render-integration.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/decisions/DEC-0014-frame-composer-render-integration.md)
|
||||
- [DEC-0015-frame-composer-public-syscall-surface.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md)
|
||||
|
||||
## Propagacao Necessaria
|
||||
|
||||
- A new implementation plan MUST be created before code changes.
|
||||
- That plan MUST cover:
|
||||
- deferred overlay/debug ownership outside `FrameComposer`;
|
||||
- runtime frame-end ordering changes;
|
||||
- no-scene path parity;
|
||||
- spec/documentation updates for `gfx.*` primitive semantics.
|
||||
- The implementation plan MUST NOT reopen the ownership boundary accepted here.
|
||||
|
||||
## Revision Log
|
||||
|
||||
- 2026-04-18: Initial accepted decision from `AGD-0028`.
|
||||
- 2026-04-18: Linked implementation plan family `PLN-0026` through `PLN-0029`.
|
||||
@ -26,6 +26,7 @@ Introduce `FrameComposer` as a first-class hardware-side subsystem and move cano
|
||||
- optional cache;
|
||||
- optional resolver;
|
||||
- owned `SpriteController`.
|
||||
- Preserve scene-layer metadata naming aligned with the world path contract, including `parallax_factor` as the canonical per-layer camera multiplier field.
|
||||
- Aggregate `FrameComposer` inside `Hardware`.
|
||||
- Expose the minimum driver-facing surface required for subsequent plans.
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@ Implement the `FrameComposer` scene-binding contract, minimal camera state, and
|
||||
|
||||
`DEC-0014` locks scene activation around `bind_scene(scene_bank_id)` with `SceneBankPoolAccess`, pointer-based access only, and `scene_bank_id + Arc<SceneBank>` retained inside `FrameComposer`.
|
||||
The same decision also requires `FrameComposer` to remain tile-size agnostic and to preserve canonical per-layer `tile_size`, including `8x8`.
|
||||
For the scene-layer motion contract, this plan treats `parallax_factor` as the canonical field name for the per-layer camera multiplier.
|
||||
|
||||
## Scope
|
||||
|
||||
@ -92,6 +93,7 @@ Align cache/resolver lifetime with the active scene contract.
|
||||
- On unbind:
|
||||
- discard cache/resolver and invalidate the world path.
|
||||
- Any initialization must derive layer math from the bound scene tile sizes instead of assuming `16x16`.
|
||||
- Any layer-camera math or related contract references must use `parallax_factor` terminology rather than generic `motion` naming.
|
||||
|
||||
**File(s):**
|
||||
- `crates/console/prometeu-drivers/src/frame_composer.rs`
|
||||
@ -104,6 +106,7 @@ Align cache/resolver lifetime with the active scene contract.
|
||||
- scene status reflects no-scene and active-scene states.
|
||||
- camera coordinates are stored as top-left pixel-space values.
|
||||
- bind/unbind remains valid for scenes whose layers use `8x8` tiles.
|
||||
- scene binding and camera-facing contracts preserve `parallax_factor` as the canonical layer field name.
|
||||
|
||||
### Integration Tests
|
||||
- `FrameComposer` can resolve a scene from the pool and survive no-scene operation.
|
||||
@ -119,6 +122,7 @@ Align cache/resolver lifetime with the active scene contract.
|
||||
- [ ] Camera contract is implemented as `i32` top-left viewport coordinates.
|
||||
- [ ] Cache/resolver lifetime follows scene bind/unbind.
|
||||
- [ ] Scene bind/cache/resolver setup preserves canonical per-layer tile sizes, including `8x8`.
|
||||
- [ ] Scene-layer camera multiplier naming is aligned on `parallax_factor`.
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@ Connect `FrameComposer` to `SceneViewportResolver`, apply cache refreshes inside
|
||||
|
||||
`DEC-0014` requires that cache refresh policy remain inside `FrameComposer` and that `FrameComposer.render_frame()` become the canonical frame entry while `Gfx` remains only the low-level execution backend.
|
||||
`DEC-0014` also requires the world path to remain tile-size agnostic, with explicit support for `8x8`, `16x16`, and `32x32` scene-layer tile sizes.
|
||||
For per-layer camera scaling, this plan treats `parallax_factor` as the canonical scene-layer field name.
|
||||
|
||||
## Scope
|
||||
|
||||
@ -43,6 +44,7 @@ Move cache-refresh orchestration fully into `FrameComposer`.
|
||||
- apply them to `SceneViewportCache`
|
||||
- Keep `Gfx` unaware of refresh semantics.
|
||||
- Ensure resolver and refresh math follow the bound layer `tile_size` values rather than any fixed `16x16` default.
|
||||
- Ensure per-layer camera math is expressed through `parallax_factor` naming in the resolver/cache path.
|
||||
|
||||
**File(s):**
|
||||
- `crates/console/prometeu-drivers/src/frame_composer.rs`
|
||||
@ -103,6 +105,7 @@ Protect the two canonical frame modes.
|
||||
- `render_frame()` with a scene applies resolver refreshes before composition.
|
||||
- cache refresh requests are applied by `FrameComposer`, not `Gfx`.
|
||||
- `render_frame()` with an `8x8` scene uses resolver/cache math derived from layer tile size rather than a `16x16` assumption.
|
||||
- Resolver/cache-facing tests use `parallax_factor` terminology for per-layer camera scaling.
|
||||
|
||||
### Integration Tests
|
||||
- scene bind + camera set + sprite emission + `render_frame()` produces the expected composed frame.
|
||||
@ -119,6 +122,7 @@ Protect the two canonical frame modes.
|
||||
- [ ] No-scene `sprites + fades` behavior remains valid.
|
||||
- [ ] `Gfx` remains backend-only for this path.
|
||||
- [ ] The world path is explicitly covered for `8x8` scenes without `16x16`-specific assumptions.
|
||||
- [ ] Resolver/cache/frame-path terminology is aligned on `parallax_factor` for scene-layer camera scaling.
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
||||
@ -0,0 +1,122 @@
|
||||
---
|
||||
id: PLN-0022
|
||||
ticket: frame-composer-public-syscall-surface
|
||||
title: Plan - Composer Syscall Domain and Spec Propagation
|
||||
status: accepted
|
||||
created: 2026-04-17
|
||||
completed:
|
||||
tags: [gfx, runtime, syscall, abi, spec, isa-core, frame-composer]
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Introduce the canonical `composer.*` syscall domain, define `ComposerOpStatus`, and propagate the new public contract through the canonical spec, ABI documentation, and `ISA_CORE` artifacts where affected.
|
||||
|
||||
## Background
|
||||
|
||||
`DEC-0015` locks the public orchestration surface on `composer.*`, requires `ComposerOpStatus` for mutating composer-domain calls, and requires propagation beyond code into canonical spec, ABI-facing documentation, and `ISA_CORE` where the public syscall surface is described normatively.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- add the `composer` syscall domain and ids
|
||||
- define `ComposerOpStatus`
|
||||
- remove `gfx.set_sprite(...)` from the public ABI contract
|
||||
- update canonical spec documentation for the new public surface
|
||||
- update ABI-facing documentation and `ISA_CORE` wherever the public syscall contract is described
|
||||
|
||||
### Excluded
|
||||
- runtime dispatch implementation
|
||||
- cartridge and stress program migration
|
||||
- final repository-wide CI execution
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1 - Define the public `composer` syscall contract
|
||||
|
||||
**What:**
|
||||
Add the new canonical public syscall surface to the HAL syscall contract.
|
||||
|
||||
**How:**
|
||||
- Extend the syscall enum, registry, metadata, and resolver with a new `composer` domain.
|
||||
- Allocate explicit syscall ids for:
|
||||
- `composer.bind_scene`
|
||||
- `composer.unbind_scene`
|
||||
- `composer.set_camera`
|
||||
- `composer.emit_sprite`
|
||||
- Remove `gfx.set_sprite` from the public syscall contract and registry.
|
||||
- Keep syscall metadata explicit for arg/ret slots and capability requirements.
|
||||
|
||||
**File(s):**
|
||||
- `crates/console/prometeu-hal/src/syscalls.rs`
|
||||
- `crates/console/prometeu-hal/src/syscalls/domains/*`
|
||||
- `crates/console/prometeu-hal/src/syscalls/registry.rs`
|
||||
- `crates/console/prometeu-hal/src/syscalls/resolver.rs`
|
||||
|
||||
### Step 2 - Introduce `ComposerOpStatus`
|
||||
|
||||
**What:**
|
||||
Create the status family for composer-domain mutating operations.
|
||||
|
||||
**How:**
|
||||
- Define a `ComposerOpStatus` type in HAL with explicit operational states needed by:
|
||||
- scene binding
|
||||
- scene unbinding
|
||||
- sprite emission
|
||||
- Ensure the enum is semantically composer-domain specific rather than a rename wrapper around `GfxOpStatus`.
|
||||
- Update public API references so composer syscalls return `ComposerOpStatus` where required by `DEC-0015`.
|
||||
|
||||
**File(s):**
|
||||
- `crates/console/prometeu-hal/src/*`
|
||||
- any shared status exports used by runtime/VM code
|
||||
|
||||
### Step 3 - Propagate the contract into spec, ABI docs, and `ISA_CORE`
|
||||
|
||||
**What:**
|
||||
Update normative documentation so the public contract no longer describes legacy `gfx.set_sprite`.
|
||||
|
||||
**How:**
|
||||
- Identify canonical spec files that describe VM graphics/composition syscalls.
|
||||
- Replace public references to legacy sprite orchestration with `composer.*`.
|
||||
- Update ABI-facing docs to pin:
|
||||
- namespace
|
||||
- names
|
||||
- arg order
|
||||
- return semantics
|
||||
- Update `ISA_CORE` if and where it references the affected syscall surface.
|
||||
- Keep published spec content in English per repository policy.
|
||||
|
||||
**File(s):**
|
||||
- canonical spec location(s)
|
||||
- ABI contract documentation
|
||||
- `ISA_CORE` artifact(s) if affected
|
||||
|
||||
## Test Requirements
|
||||
|
||||
### Unit Tests
|
||||
- syscall registry tests pin the new `composer.*` entries and reject removed legacy identities
|
||||
- `ComposerOpStatus` values are pinned where public return semantics are asserted
|
||||
|
||||
### Integration Tests
|
||||
- declared syscall resolution accepts `composer.*` declarations and rejects removed `gfx.set_sprite`
|
||||
|
||||
### Manual Verification
|
||||
- inspect canonical spec, ABI docs, and `ISA_CORE` references to confirm the public contract matches `DEC-0015`
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] The public syscall registry exposes `composer.bind_scene`, `composer.unbind_scene`, `composer.set_camera`, and `composer.emit_sprite`.
|
||||
- [ ] `ComposerOpStatus` exists as the canonical status family for composer-domain mutating syscalls.
|
||||
- [ ] `gfx.set_sprite` is removed from the public ABI contract.
|
||||
- [ ] Canonical spec documentation is updated to describe `composer.*`.
|
||||
- [ ] ABI-facing docs and `ISA_CORE` are updated wherever the affected public surface is documented.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Source decision: `DEC-0015`
|
||||
|
||||
## Risks
|
||||
|
||||
- Missing a normative doc location would leave the code and published contract divergent.
|
||||
- Reusing `GfxOpStatus` semantics by accident would weaken the service-boundary separation required by `DEC-0015`.
|
||||
- Removing the legacy syscall contract incompletely could leave resolver or ABI ambiguity behind.
|
||||
@ -0,0 +1,112 @@
|
||||
---
|
||||
id: PLN-0023
|
||||
ticket: frame-composer-public-syscall-surface
|
||||
title: Plan - Composer Runtime Dispatch and Legacy Removal
|
||||
status: accepted
|
||||
created: 2026-04-17
|
||||
completed:
|
||||
tags: [runtime, syscall, frame-composer, dispatch, migration]
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Route the new public `composer.*` syscalls through `FrameComposer`, remove legacy `gfx.set_sprite` handling, and align runtime-side operational behavior with `DEC-0015`.
|
||||
|
||||
## Background
|
||||
|
||||
`DEC-0015` closes the public contract around `composer.*` and requires that `gfx.set_sprite` be removed completely rather than kept as a compatibility shim. The internal `FrameComposer` ownership model already exists from `DEC-0014` and plans `PLN-0017` through `PLN-0021`.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- runtime syscall dispatch for `composer.*`
|
||||
- operational mapping from syscall args to `FrameComposer`
|
||||
- removal of legacy `gfx.set_sprite` runtime handling
|
||||
- runtime-facing tests for composer-domain behavior
|
||||
|
||||
### Excluded
|
||||
- spec and ABI doc propagation
|
||||
- cartridge/tooling migration
|
||||
- final `make ci` closure
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1 - Add runtime dispatch for `composer.*`
|
||||
|
||||
**What:**
|
||||
Teach VM runtime dispatch to call `FrameComposer` through the new public contract.
|
||||
|
||||
**How:**
|
||||
- Add dispatch arms for:
|
||||
- `composer.bind_scene`
|
||||
- `composer.unbind_scene`
|
||||
- `composer.set_camera`
|
||||
- `composer.emit_sprite`
|
||||
- Parse arguments exactly as pinned by the HAL metadata.
|
||||
- Return `ComposerOpStatus` for mutating composer-domain syscalls.
|
||||
|
||||
**File(s):**
|
||||
- `crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs`
|
||||
- any adjacent runtime helpers
|
||||
|
||||
### Step 2 - Map operational outcomes cleanly onto `ComposerOpStatus`
|
||||
|
||||
**What:**
|
||||
Make runtime failures and normal outcomes reflect the new composer-domain status model.
|
||||
|
||||
**How:**
|
||||
- Bind runtime-side operational checks to status outcomes such as:
|
||||
- scene bank unavailable
|
||||
- bank invalid
|
||||
- argument range invalid
|
||||
- layer invalid
|
||||
- sprite overflow if surfaced operationally
|
||||
- Keep non-fatal overflow behavior aligned with `DEC-0015`.
|
||||
|
||||
**File(s):**
|
||||
- `crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs`
|
||||
- `crates/console/prometeu-hal/src/*` as needed for shared status meaning
|
||||
|
||||
### Step 3 - Remove legacy `gfx.set_sprite` runtime support
|
||||
|
||||
**What:**
|
||||
Delete the old public runtime path for slot-style sprite submission.
|
||||
|
||||
**How:**
|
||||
- Remove dispatch support for `gfx.set_sprite`.
|
||||
- Remove runtime assumptions about `active`, caller-provided indices, and legacy sprite ABI shape.
|
||||
- Keep no private compatibility hook behind the public API.
|
||||
|
||||
**File(s):**
|
||||
- `crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs`
|
||||
- adjacent tests and public syscall references
|
||||
|
||||
## Test Requirements
|
||||
|
||||
### Unit Tests
|
||||
- runtime dispatch returns `ComposerOpStatus` for bind, unbind, and emit operations
|
||||
- `composer.set_camera` stores the minimal V1 camera coordinates correctly
|
||||
|
||||
### Integration Tests
|
||||
- a VM/runtime test can bind a scene, set camera, emit a sprite, reach `FRAME_SYNC`, and render through the canonical frame path
|
||||
- public runtime behavior rejects removed `gfx.set_sprite` declarations/calls
|
||||
|
||||
### Manual Verification
|
||||
- inspect dispatch code to confirm all public orchestration now routes through `FrameComposer` rather than a legacy `gfx` sprite syscall path
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Runtime dispatch supports all canonical `composer.*` syscalls.
|
||||
- [ ] Mutating composer-domain calls return `ComposerOpStatus`.
|
||||
- [ ] `gfx.set_sprite` is removed from runtime public handling.
|
||||
- [ ] Runtime tests cover scene bind, camera set, sprite emit, and frame rendering through the public path.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Depends on `PLN-0022`
|
||||
- Source decision: `DEC-0015`
|
||||
|
||||
## Risks
|
||||
|
||||
- Removing legacy handling before all runtime references are migrated can strand tests or bytecode fixtures.
|
||||
- Poor `ComposerOpStatus` mapping could collapse useful operational distinctions into generic failures.
|
||||
@ -0,0 +1,107 @@
|
||||
---
|
||||
id: PLN-0024
|
||||
ticket: frame-composer-public-syscall-surface
|
||||
title: Plan - Composer Cartridge, Tooling, and Regression Migration
|
||||
status: accepted
|
||||
created: 2026-04-17
|
||||
completed:
|
||||
tags: [runtime, bytecode, tooling, stress, regression, frame-composer]
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Migrate bytecode declarations, cartridges, stress tooling, and regression coverage from legacy public sprite orchestration to the canonical `composer.*` surface.
|
||||
|
||||
## Background
|
||||
|
||||
`DEC-0015` requires the new public composer-domain ABI to land without leaving `gfx.set_sprite` as a fallback. That means the migration must cover the generated bytecode, test cartridges, and stress tooling that still assume the old public contract.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- bytecode declaration updates for `composer.*`
|
||||
- cartridge and stress generator migration
|
||||
- regression coverage for the public composer-domain path
|
||||
- removal of legacy syscall usage from test and tooling surfaces
|
||||
|
||||
### Excluded
|
||||
- canonical spec propagation
|
||||
- runtime dispatch implementation
|
||||
- final repository-wide CI closure
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1 - Migrate declared syscall users and fixtures
|
||||
|
||||
**What:**
|
||||
Update code and fixtures that declare public syscalls so they target `composer.*`.
|
||||
|
||||
**How:**
|
||||
- Replace legacy public sprite syscall declarations with composer-domain declarations.
|
||||
- Update ABI expectations in bytecode-related tests and fixtures.
|
||||
- Ensure removal of `gfx.set_sprite` is reflected in any declaration validation snapshots.
|
||||
|
||||
**File(s):**
|
||||
- bytecode tests and fixtures
|
||||
- syscall declaration users across runtime and tools
|
||||
|
||||
### Step 2 - Migrate stress and cartridge tooling
|
||||
|
||||
**What:**
|
||||
Make the stress cartridge and related generators exercise the canonical public frame path.
|
||||
|
||||
**How:**
|
||||
- Update `pbxgen-stress` and any cartridge generators to declare and call `composer.*`.
|
||||
- Replace legacy sprite-path usage with `composer.emit_sprite`.
|
||||
- Add scene bind and camera usage where needed so the stress path reaches the real canonical pipeline.
|
||||
|
||||
**File(s):**
|
||||
- `crates/tools/pbxgen-stress/src/*`
|
||||
- `test-cartridges/stress-console/*`
|
||||
- related scripts such as `scripts/run-stress.sh`
|
||||
|
||||
### Step 3 - Expand regression coverage around the public path
|
||||
|
||||
**What:**
|
||||
Lock the new public orchestration contract with regression tests.
|
||||
|
||||
**How:**
|
||||
- Add tests that cover:
|
||||
- composer-domain declaration resolution
|
||||
- public bind/unbind/camera/emit behavior
|
||||
- scene rendering through the public path
|
||||
- stress/tooling integration using `composer.*`
|
||||
- Ensure no regression fixture still relies on removed `gfx.set_sprite`.
|
||||
|
||||
**File(s):**
|
||||
- runtime tests
|
||||
- HAL syscall tests
|
||||
- tooling tests where available
|
||||
|
||||
## Test Requirements
|
||||
|
||||
### Unit Tests
|
||||
- bytecode and syscall declaration tests pin `composer.*` names and slot counts
|
||||
|
||||
### Integration Tests
|
||||
- stress or cartridge-facing tests exercise scene bind, camera set, and sprite emit through `composer.*`
|
||||
- regression fixtures fail if `gfx.set_sprite` is reintroduced
|
||||
|
||||
### Manual Verification
|
||||
- inspect generated stress cartridge declarations and program behavior to confirm the public path is truly composer-domain based
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Bytecode declarations and fixtures use `composer.*` instead of legacy public sprite orchestration.
|
||||
- [ ] Stress tooling and test cartridges exercise the canonical public `FrameComposer` path.
|
||||
- [ ] Regression coverage protects against fallback to `gfx.set_sprite`.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Depends on `PLN-0022` and `PLN-0023`
|
||||
- Source decision: `DEC-0015`
|
||||
|
||||
## Risks
|
||||
|
||||
- Partial cartridge/tooling migration could leave the repository with hidden legacy public ABI usage.
|
||||
- Stress tooling may appear to pass while still missing scene/camera coverage if it only migrates sprite calls.
|
||||
@ -0,0 +1,96 @@
|
||||
---
|
||||
id: PLN-0025
|
||||
ticket: frame-composer-public-syscall-surface
|
||||
title: Plan - Final CI Validation and Polish
|
||||
status: accepted
|
||||
created: 2026-04-17
|
||||
completed:
|
||||
tags: [ci, validation, regression, polish]
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Run the final repository validation path, including `make ci`, and perform the last compatibility, formatting, lint, and regression fixes required to close the composer-domain migration cleanly.
|
||||
|
||||
## Background
|
||||
|
||||
`DEC-0015` requires a coordinated migration across ABI, runtime, tooling, cartridges, spec, and documentation. After the implementation plans land, the repository still needs a final closure pass so no residual breakage survives in formatting, linting, tests, generated artifacts, or CI expectations.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- final repository validation with `make ci`
|
||||
- fixups required by formatting, lint, tests, snapshots, or generated artifacts
|
||||
- final consistency pass across migrated files
|
||||
|
||||
### Excluded
|
||||
- introducing new contract changes beyond `DEC-0015`
|
||||
- reopening ABI or service-boundary decisions
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1 - Run the final validation entrypoint
|
||||
|
||||
**What:**
|
||||
Execute the repository’s final CI validation path.
|
||||
|
||||
**How:**
|
||||
- Run `make ci` after `PLN-0022`, `PLN-0023`, and `PLN-0024` are complete.
|
||||
- Capture failures from formatting, lint, tests, coverage setup, generation steps, or artifact drift.
|
||||
|
||||
**File(s):**
|
||||
- repository-wide validation entrypoints
|
||||
|
||||
### Step 2 - Apply closure fixes without reopening scope
|
||||
|
||||
**What:**
|
||||
Resolve residual breakage surfaced by final validation.
|
||||
|
||||
**How:**
|
||||
- Fix formatting and lint issues.
|
||||
- Update snapshots or generated artifacts only where the migrated public contract requires it.
|
||||
- Repair any remaining tests or documentation references that fail under `make ci`.
|
||||
- Do not widen scope beyond the accepted composer-domain migration.
|
||||
|
||||
**File(s):**
|
||||
- any files directly implicated by final validation failures
|
||||
|
||||
### Step 3 - Confirm final repository consistency
|
||||
|
||||
**What:**
|
||||
Leave the migration in a stable publishable state.
|
||||
|
||||
**How:**
|
||||
- Re-run `make ci` until it passes cleanly.
|
||||
- Verify no legacy public `gfx.set_sprite` usage remains in code, tests, tooling, or docs.
|
||||
- Confirm the worktree reflects only intended migration changes.
|
||||
|
||||
**File(s):**
|
||||
- repository-wide
|
||||
|
||||
## Test Requirements
|
||||
|
||||
### Unit Tests
|
||||
- whatever unit coverage is exercised by `make ci` must remain green
|
||||
|
||||
### Integration Tests
|
||||
- repository integration coverage under `make ci` must pass after the migration
|
||||
|
||||
### Manual Verification
|
||||
- inspect the tree for residual `gfx.set_sprite` references and incomplete composer-domain propagation
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `make ci` passes after the composer-domain migration family lands.
|
||||
- [ ] Final fixups do not reopen contract scope beyond `DEC-0015`.
|
||||
- [ ] No residual public `gfx.set_sprite` usage remains in the repository.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Depends on `PLN-0022`, `PLN-0023`, and `PLN-0024`
|
||||
- Source decision: `DEC-0015`
|
||||
|
||||
## Risks
|
||||
|
||||
- If this final closure pass is skipped, small residual regressions can survive across formatting, lint, or generated artifacts even when the core implementation is correct.
|
||||
- Late fixes can accidentally widen scope unless kept strictly bounded to validation fallout.
|
||||
@ -0,0 +1,93 @@
|
||||
---
|
||||
id: PLN-0026
|
||||
ticket: deferred-overlay-and-primitive-composition
|
||||
title: Plan - GFX Overlay Contract and Spec Propagation
|
||||
status: accepted
|
||||
created: 2026-04-18
|
||||
completed:
|
||||
tags: [gfx, runtime, spec, overlay, primitives, hud]
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Propagate `DEC-0016` into the canonical specs and internal contracts so `gfx.*` primitives are defined as deferred final overlay/debug operations outside `FrameComposer`.
|
||||
|
||||
## Background
|
||||
|
||||
`DEC-0016` locks a new semantic boundary:
|
||||
|
||||
- `FrameComposer` remains the owner of canonical game-frame composition;
|
||||
- `gfx.*` primitives and `draw_text(...)` become deferred final overlay/debug operations;
|
||||
- that overlay lives outside `FrameComposer` and is drained after `hud_fade`.
|
||||
|
||||
Execution must start by updating the normative contract before implementation changes spread through runtime and drivers.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- update canonical runtime/gfx spec text to describe deferred overlay semantics
|
||||
- update any ABI-facing or developer-facing docs that still imply direct stable writes to `back`
|
||||
- align local contract comments and module docs where they currently imply immediate-write semantics as the stable model
|
||||
|
||||
### Excluded
|
||||
- implementation of the overlay subsystem
|
||||
- runtime frame-end integration
|
||||
- final repository-wide CI
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1 - Update canonical graphics/runtime documentation
|
||||
|
||||
**What:**
|
||||
Publish the new semantic contract for `gfx.*` primitives.
|
||||
|
||||
**How:**
|
||||
- Update the canonical runtime/gfx spec so `gfx.draw_text(...)` and peer primitives are described as deferred final overlay/debug operations.
|
||||
- State explicitly that primitives are not part of canonical scene/sprite/HUD composition.
|
||||
- State the ordering rule that overlay/debug is drained after `hud_fade`.
|
||||
- Ensure the no-scene and scene-bound paths are described consistently.
|
||||
|
||||
**File(s):**
|
||||
- canonical runtime/gfx spec files under `docs/specs/runtime/`
|
||||
|
||||
### Step 2 - Align implementation-facing contract text
|
||||
|
||||
**What:**
|
||||
Remove stale implementation comments that imply immediate stable writes to the framebuffer.
|
||||
|
||||
**How:**
|
||||
- Inspect module-level comments and trait docs in `hal`, `drivers`, and runtime code for language that now contradicts `DEC-0016`.
|
||||
- Update only the contract-bearing comments and docs that materially affect maintenance and implementation clarity.
|
||||
|
||||
**File(s):**
|
||||
- `crates/console/prometeu-hal/src/gfx_bridge.rs`
|
||||
- `crates/console/prometeu-drivers/src/gfx.rs`
|
||||
- `crates/console/prometeu-drivers/src/frame_composer.rs`
|
||||
- runtime-adjacent modules where frame ordering is described
|
||||
|
||||
## Test Requirements
|
||||
|
||||
### Unit Tests
|
||||
- none required for pure doc propagation
|
||||
|
||||
### Integration Tests
|
||||
- none required for pure doc propagation
|
||||
|
||||
### Manual Verification
|
||||
- inspect the updated spec and local contract comments to confirm they no longer describe primitives as stable direct writes to `back`
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Canonical spec text describes `gfx.*` primitives as deferred final overlay/debug operations.
|
||||
- [ ] The spec states that overlay/debug is outside `FrameComposer`.
|
||||
- [ ] The spec states that overlay/debug is drained after `hud_fade`.
|
||||
- [ ] Local implementation-facing contract comments no longer imply immediate-write semantics as the stable model.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Source decision: `DEC-0016`
|
||||
|
||||
## Risks
|
||||
|
||||
- Missing a normative doc location would leave code and published contract divergent.
|
||||
- Over-editing local comments could unintentionally restate design choices outside the scope of `DEC-0016`.
|
||||
@ -0,0 +1,104 @@
|
||||
---
|
||||
id: PLN-0027
|
||||
ticket: deferred-overlay-and-primitive-composition
|
||||
title: Plan - Deferred GFX Overlay Subsystem
|
||||
status: accepted
|
||||
created: 2026-04-18
|
||||
completed:
|
||||
tags: [gfx, runtime, overlay, primitives, text, drivers]
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Introduce a dedicated deferred overlay/debug subsystem for `gfx.*` primitives outside `FrameComposer`, with command capture for `draw_text(...)` and the primitive family selected for the first migration.
|
||||
|
||||
## Background
|
||||
|
||||
`DEC-0016` requires primitive/text overlay ownership to remain outside `FrameComposer` while still allowing shared raster helpers and low-level optimizations internally. The new subsystem must preserve semantic separation from scene/sprite/HUD composition.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- introduce an overlay/debug command queue or equivalent subsystem outside `FrameComposer`
|
||||
- route `gfx.draw_text(...)` into deferred command capture instead of stable direct framebuffer writes
|
||||
- route the chosen V1 primitive family into the same deferred overlay/debug path
|
||||
- keep raster helper reuse allowed without merging semantic ownership
|
||||
|
||||
### Excluded
|
||||
- runtime frame-end sequencing
|
||||
- no-scene/scene parity tests at the runtime level
|
||||
- final repository-wide CI
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1 - Define overlay/debug state ownership in drivers
|
||||
|
||||
**What:**
|
||||
Create the subsystem that owns deferred `gfx.*` overlay/debug commands.
|
||||
|
||||
**How:**
|
||||
- Add a dedicated owner adjacent to `Gfx`/`Hardware`, but not inside `FrameComposer`.
|
||||
- Define the minimal command model required for V1 operations.
|
||||
- Keep the subsystem screen-space and explicitly pipeline-agnostic relative to `composer.*`.
|
||||
|
||||
**File(s):**
|
||||
- `crates/console/prometeu-drivers/src/*`
|
||||
- `crates/console/prometeu-hal/src/*` if bridge traits need extension
|
||||
|
||||
### Step 2 - Route text and selected primitives into deferred capture
|
||||
|
||||
**What:**
|
||||
Stop treating text/primitives as stable direct writes.
|
||||
|
||||
**How:**
|
||||
- Change `gfx.draw_text(...)` to enqueue deferred overlay/debug work.
|
||||
- Migrate the selected V1 primitive set into the same deferred path.
|
||||
- Keep any remaining unmigrated primitives either explicitly out of scope or routed consistently if they are already part of the accepted V1 set.
|
||||
- Preserve internal raster helper reuse where useful.
|
||||
|
||||
**File(s):**
|
||||
- `crates/console/prometeu-drivers/src/gfx.rs`
|
||||
- runtime dispatch call sites that submit `gfx.*` primitives
|
||||
|
||||
### Step 3 - Add local driver-level tests for deferred capture semantics
|
||||
|
||||
**What:**
|
||||
Prove that overlay/debug commands are captured separately from game composition state.
|
||||
|
||||
**How:**
|
||||
- Add tests that assert text/primitives do not need direct stable writes to `back` to survive until overlay drain.
|
||||
- Add tests that assert the overlay owner is independent from `FrameComposer` state.
|
||||
|
||||
**File(s):**
|
||||
- `crates/console/prometeu-drivers/src/gfx.rs`
|
||||
- new or existing driver test modules
|
||||
|
||||
## Test Requirements
|
||||
|
||||
### Unit Tests
|
||||
- command capture tests for `draw_text(...)`
|
||||
- tests for each migrated V1 primitive class
|
||||
- tests proving overlay/debug state is owned outside `FrameComposer`
|
||||
|
||||
### Integration Tests
|
||||
- none in this plan; runtime-level ordering is covered by the next plan
|
||||
|
||||
### Manual Verification
|
||||
- inspect driver ownership boundaries to confirm `FrameComposer` does not gain overlay/debug state
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] A dedicated deferred overlay/debug subsystem exists outside `FrameComposer`.
|
||||
- [ ] `gfx.draw_text(...)` is captured as deferred overlay/debug work.
|
||||
- [ ] The selected V1 primitive family is captured through the same subsystem.
|
||||
- [ ] Driver-level tests prove overlay/debug state is operationally separate from canonical game composition state.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Source decision: `DEC-0016`
|
||||
- Prefer to execute after `PLN-0026`
|
||||
|
||||
## Risks
|
||||
|
||||
- Accidentally reusing `FrameComposer` storage or state would violate the accepted ownership boundary.
|
||||
- Migrating only part of the primitive family without explicit scoping could create inconsistent semantics across `gfx.*`.
|
||||
@ -0,0 +1,106 @@
|
||||
---
|
||||
id: PLN-0028
|
||||
ticket: deferred-overlay-and-primitive-composition
|
||||
title: Plan - Runtime Frame-End Overlay Integration and Parity
|
||||
status: accepted
|
||||
created: 2026-04-18
|
||||
completed:
|
||||
tags: [runtime, overlay, frame-composer, no-scene, regression, stress]
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Integrate deferred overlay/debug draining into the runtime frame-end sequence so scene-bound and no-scene frames both present the same final `gfx.*` primitive behavior after `hud_fade`.
|
||||
|
||||
## Background
|
||||
|
||||
After `PLN-0027`, the overlay/debug subsystem will exist but still needs to be drained in the correct place relative to `FrameComposer.render_frame()`, fades, and present/present-adjacent behavior. This plan closes the observable runtime semantics required by `DEC-0016`.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- runtime frame-end ordering changes
|
||||
- scene-bound and no-scene parity
|
||||
- regression coverage for overlay visibility above the canonical game frame
|
||||
- stress-cartridge adjustments if needed to prove text/primitives now survive frame composition
|
||||
|
||||
### Excluded
|
||||
- broad renderer optimization work
|
||||
- final repository-wide CI
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1 - Insert overlay/debug drain into the frame-end path
|
||||
|
||||
**What:**
|
||||
Drain deferred overlay/debug after canonical game composition is complete.
|
||||
|
||||
**How:**
|
||||
- Update the runtime frame-end path so overlay/debug drain occurs after:
|
||||
- `FrameComposer.render_frame()`
|
||||
- `scene_fade`
|
||||
- `hud_fade`
|
||||
- Ensure the same ordering is respected in the no-scene path.
|
||||
|
||||
**File(s):**
|
||||
- `crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs`
|
||||
- `crates/console/prometeu-drivers/src/hardware.rs`
|
||||
- `crates/console/prometeu-drivers/src/gfx.rs`
|
||||
- any bridge traits needed by the runtime/hardware path
|
||||
|
||||
### Step 2 - Add runtime and driver regressions for final visual ordering
|
||||
|
||||
**What:**
|
||||
Lock the new visible behavior.
|
||||
|
||||
**How:**
|
||||
- Add tests proving `gfx.draw_text(...)` remains visible after scene-backed frame composition.
|
||||
- Add tests proving the same behavior with no scene bound.
|
||||
- Add tests proving overlay/debug sits above the canonical game frame rather than being erased by it.
|
||||
|
||||
**File(s):**
|
||||
- `crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs`
|
||||
- driver-level render tests where helpful
|
||||
|
||||
### Step 3 - Update stress/integration fixtures if needed
|
||||
|
||||
**What:**
|
||||
Restore or improve stress scenarios that rely on visible text/primitives.
|
||||
|
||||
**How:**
|
||||
- Update `pbxgen-stress` or related stress fixtures so text/primitives are once again a valid visible overlay signal.
|
||||
- Keep the stress focused on the new model rather than reintroducing obsolete immediate-write assumptions.
|
||||
|
||||
**File(s):**
|
||||
- `crates/tools/pbxgen-stress/src/lib.rs`
|
||||
- `test-cartridges/stress-console/*`
|
||||
|
||||
## Test Requirements
|
||||
|
||||
### Unit Tests
|
||||
- local ordering tests where runtime integration depends on helper sequencing
|
||||
|
||||
### Integration Tests
|
||||
- runtime tests for scene-bound overlay/debug visibility
|
||||
- runtime tests for no-scene parity
|
||||
- stress/tooling validation that text or primitives are visible again as final overlay/debug
|
||||
|
||||
### Manual Verification
|
||||
- run the stress path and visually confirm overlay/debug survives on top of scene/sprites after frame composition
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] The runtime drains deferred overlay/debug after canonical game composition and after `hud_fade`.
|
||||
- [ ] Scene-bound and no-scene paths expose the same overlay/debug semantics.
|
||||
- [ ] Regression tests prove `draw_text(...)` is no longer erased by scene-backed frame composition.
|
||||
- [ ] Stress/integration fixtures reflect the new final-overlay semantics where applicable.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Source decision: `DEC-0016`
|
||||
- Depends on `PLN-0027`
|
||||
|
||||
## Risks
|
||||
|
||||
- If fades are still applied after overlay/debug drain, the visible contract will contradict `DEC-0016`.
|
||||
- Incomplete parity between scene-bound and no-scene paths would leave runtime behavior mode-dependent.
|
||||
@ -0,0 +1,82 @@
|
||||
---
|
||||
id: PLN-0029
|
||||
ticket: deferred-overlay-and-primitive-composition
|
||||
title: Plan - Final Overlay CI Validation and Polish
|
||||
status: accepted
|
||||
created: 2026-04-18
|
||||
completed:
|
||||
tags: [ci, overlay, runtime, gfx, validation]
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Run the final repository validation path for the deferred overlay/debug migration and perform the last compatibility, formatting, lint, and regression fixes required to close the thread cleanly.
|
||||
|
||||
## Background
|
||||
|
||||
`DEC-0016` changes visible runtime semantics and touches both specs and code paths around frame composition. A dedicated final-validation plan is needed so the implementation family can close on a clean CI signal rather than leaving integration fallout for later.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- full-tree formatting, lint, and test validation
|
||||
- stress-path smoke validation after overlay integration
|
||||
- final cleanup fixes required to satisfy CI
|
||||
|
||||
### Excluded
|
||||
- new feature work outside the accepted overlay/debug migration
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1 - Run focused validation before full CI
|
||||
|
||||
**What:**
|
||||
Catch local fallout in the touched areas before the full repository pass.
|
||||
|
||||
**How:**
|
||||
- Run targeted tests for drivers, runtime, and `pbxgen-stress`.
|
||||
- Inspect touched files for stale immediate-write assumptions or missed contract updates.
|
||||
|
||||
**File(s):**
|
||||
- touched files from `PLN-0026` through `PLN-0028`
|
||||
|
||||
### Step 2 - Run final repository CI
|
||||
|
||||
**What:**
|
||||
Validate the migration end to end.
|
||||
|
||||
**How:**
|
||||
- Run the repository validation path, including `make ci`.
|
||||
- Fix any final formatting, lint, test, or generated-fixture fallout caused by the overlay/debug migration.
|
||||
- Do not widen scope beyond the accepted thread.
|
||||
|
||||
**File(s):**
|
||||
- repository-wide
|
||||
|
||||
## Test Requirements
|
||||
|
||||
### Unit Tests
|
||||
- all relevant crate unit tests pass after the migration
|
||||
|
||||
### Integration Tests
|
||||
- runtime and stress/integration tests pass after the migration
|
||||
- `make ci` passes
|
||||
|
||||
### Manual Verification
|
||||
- inspect the tree for residual direct-write assumptions or incomplete overlay propagation
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Targeted validation passes for the touched drivers/runtime/stress areas.
|
||||
- [ ] `make ci` passes after the deferred overlay/debug migration family lands.
|
||||
- [ ] No residual contract mismatch remains between spec text and code behavior in the touched thread.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Source decision: `DEC-0016`
|
||||
- Depends on `PLN-0026`, `PLN-0027`, and `PLN-0028`
|
||||
|
||||
## Risks
|
||||
|
||||
- Final CI may surface unrelated renderer assumptions that still expect immediate-write semantics.
|
||||
- Generated cartridge fixtures may drift if regeneration is forgotten during earlier plans.
|
||||
@ -48,10 +48,12 @@ The GFX maintains two buffers:
|
||||
|
||||
Per-frame flow:
|
||||
|
||||
1. The system draws to the back buffer
|
||||
2. Calls `present()`
|
||||
3. Buffers are swapped
|
||||
4. The host displays the front buffer
|
||||
1. The system prepares the logical frame
|
||||
2. Canonical game composition is rendered into the back buffer
|
||||
3. Deferred final overlay/debug primitives are drained on top of the completed game frame
|
||||
4. Calls `present()`
|
||||
5. Buffers are swapped
|
||||
6. The host displays the front buffer
|
||||
|
||||
This guarantees:
|
||||
|
||||
@ -183,7 +185,7 @@ Access:
|
||||
|
||||
---
|
||||
|
||||
## 10. Projection to the Back Buffer
|
||||
## 10. Canonical Game Projection to the Back Buffer
|
||||
|
||||
For each frame:
|
||||
|
||||
@ -198,6 +200,10 @@ For each frame:
|
||||
|
||||
3. Draw HUD layer last
|
||||
|
||||
This section describes only the canonical game composition path.
|
||||
|
||||
`gfx.*` primitives such as `draw_text`, `draw_line`, and `draw_disc` are not part of this canonical game projection order. In v1 they belong to a deferred final overlay/debug stage that is drained after canonical game composition is complete.
|
||||
|
||||
---
|
||||
|
||||
## 11. Drawing Order and Priority
|
||||
@ -214,6 +220,15 @@ Base order:
|
||||
4. Tile Layer 3
|
||||
5. Sprites (by priority between layers)
|
||||
6. HUD Layer
|
||||
7. Scene Fade
|
||||
8. HUD Fade
|
||||
9. Deferred `gfx.*` overlay/debug primitives
|
||||
|
||||
Normative boundary:
|
||||
|
||||
- Items 1 through 8 belong to canonical game-frame composition.
|
||||
- Item 9 is a separate overlay/debug stage.
|
||||
- Deferred `gfx.*` primitives MUST NOT be interpreted as scene, sprite, or canonical HUD content.
|
||||
|
||||
---
|
||||
|
||||
@ -258,8 +273,9 @@ Everything is:
|
||||
## 14. Where Blend is Applied
|
||||
|
||||
- Blending occurs during drawing
|
||||
- The result goes directly to the back buffer
|
||||
- There is no automatic post-composition
|
||||
- For canonical game composition, the result goes to the back buffer during composition
|
||||
- For deferred `gfx.*` overlay/debug primitives, the result is applied during the final overlay/debug drain stage
|
||||
- There is no automatic GPU-style post-processing pipeline
|
||||
|
||||
---
|
||||
|
||||
@ -296,6 +312,8 @@ controls:
|
||||
- **Scene Fade**: affects the entire scene (Tile Layers 0–3 + Sprites)
|
||||
- **HUD Fade**: affects only the HUD Layer (always composed last)
|
||||
|
||||
In v1, deferred `gfx.*` overlay/debug primitives are drained after both fades and therefore are not themselves part of scene or HUD fade application.
|
||||
|
||||
The fade is implemented without continuous per-pixel alpha and without floats.
|
||||
It uses a **discrete integer level** (0..31), which in practice produces an
|
||||
"almost continuous" visual result in 320×180 pixel art.
|
||||
@ -536,7 +554,12 @@ The system can measure:
|
||||
|
||||
## 19. Syscall Return and Fault Policy
|
||||
|
||||
`gfx` follows status-first policy for operations with operational failure modes.
|
||||
Graphics-related public ABI in v1 is split between:
|
||||
|
||||
- `gfx.*` for direct drawing/backend-oriented operations;
|
||||
- `composer.*` for frame orchestration operations.
|
||||
|
||||
Only operations with real operational rejection paths return explicit status values.
|
||||
|
||||
Fault boundary:
|
||||
|
||||
@ -544,50 +567,62 @@ Fault boundary:
|
||||
- `status`: operational failure;
|
||||
- `Panic`: internal runtime invariant break only.
|
||||
|
||||
### 19.1 `gfx.set_sprite`
|
||||
### 19.1 Return-shape matrix in v1
|
||||
|
||||
Return-shape matrix in v1:
|
||||
| Syscall | Return | Policy basis |
|
||||
| ----------------------- | ------------- | --------------------------------------------------- |
|
||||
| `gfx.clear` | `void` | no real operational failure path in v1 |
|
||||
| `gfx.fill_rect` | `void` | no real operational failure path in v1 |
|
||||
| `gfx.draw_line` | `void` | no real operational failure path in v1 |
|
||||
| `gfx.draw_circle` | `void` | no real operational failure path in v1 |
|
||||
| `gfx.draw_disc` | `void` | no real operational failure path in v1 |
|
||||
| `gfx.draw_square` | `void` | no real operational failure path in v1 |
|
||||
| `gfx.draw_text` | `void` | no real operational failure path in v1 |
|
||||
| `gfx.clear_565` | `void` | no real operational failure path in v1 |
|
||||
| `composer.bind_scene` | `status:int` | explicit orchestration-domain operational result |
|
||||
| `composer.unbind_scene` | `status:int` | explicit orchestration-domain operational result |
|
||||
| `composer.set_camera` | `void` | no real operational failure path in v1 |
|
||||
| `composer.emit_sprite` | `status:int` | explicit orchestration-domain operational rejection |
|
||||
|
||||
| Syscall | Return | Policy basis |
|
||||
| ------------------ | ------------- | ---------------------------------------------------- |
|
||||
| `gfx.clear` | `void` | no real operational failure path in v1 |
|
||||
| `gfx.fill_rect` | `void` | no real operational failure path in v1 |
|
||||
| `gfx.draw_line` | `void` | no real operational failure path in v1 |
|
||||
| `gfx.draw_circle` | `void` | no real operational failure path in v1 |
|
||||
| `gfx.draw_disc` | `void` | no real operational failure path in v1 |
|
||||
| `gfx.draw_square` | `void` | no real operational failure path in v1 |
|
||||
| `gfx.set_sprite` | `status:int` | operational rejection must be explicit |
|
||||
| `gfx.draw_text` | `void` | no real operational failure path in v1 |
|
||||
| `gfx.clear_565` | `void` | no real operational failure path in v1 |
|
||||
### 19.1.a Deferred overlay/debug semantics for `gfx.*`
|
||||
|
||||
Only `gfx.set_sprite` is status-returning in v1.
|
||||
All other `gfx` syscalls remain `void` unless a future domain revision introduces a real operational failure path.
|
||||
The public `gfx.*` primitive family remains valid in v1, but its stable operational meaning is:
|
||||
|
||||
### 19.2 `gfx.set_sprite`
|
||||
- deferred final overlay/debug composition;
|
||||
- screen-space and pipeline-agnostic relative to `composer.*`;
|
||||
- outside `FrameComposer`;
|
||||
- above scene, sprites, and canonical HUD;
|
||||
- drained after `hud_fade`.
|
||||
|
||||
`gfx.set_sprite` returns `status:int`.
|
||||
This means callers MUST NOT rely on stable immediate writes to the working back buffer as the public contract for `gfx.draw_text(...)` or sibling primitives.
|
||||
|
||||
### 19.2 `composer.emit_sprite`
|
||||
|
||||
`composer.emit_sprite` returns `status:int`.
|
||||
|
||||
ABI:
|
||||
1. `bank_id: int` — index of the tile bank
|
||||
2. `index: int` — sprite index (0..511)
|
||||
1. `glyph_id: int` — glyph index within the bank
|
||||
2. `palette_id: int` — palette index
|
||||
3. `x: int` — x coordinate
|
||||
4. `y: int` — y coordinate
|
||||
5. `tile_id: int` — tile index within the bank
|
||||
6. `palette_id: int` — palette index (0..63)
|
||||
7. `active: bool` — visibility toggle
|
||||
8. `flip_x: bool` — horizontal flip
|
||||
9. `flip_y: bool` — vertical flip
|
||||
10. `priority: int` — layer priority (0..4)
|
||||
5. `layer: int` — composition layer reference
|
||||
6. `bank_id: int` — glyph bank index
|
||||
7. `flip_x: bool` — horizontal flip
|
||||
8. `flip_y: bool` — vertical flip
|
||||
9. `priority: int` — within-layer ordering priority
|
||||
|
||||
Minimum status table:
|
||||
|
||||
- `0` = `OK`
|
||||
- `2` = `INVALID_SPRITE_INDEX`
|
||||
- `3` = `INVALID_ARG_RANGE`
|
||||
- `4` = `BANK_INVALID`
|
||||
- `1` = `SCENE_UNAVAILABLE`
|
||||
- `2` = `INVALID_ARG_RANGE`
|
||||
- `3` = `BANK_INVALID`
|
||||
- `4` = `LAYER_INVALID`
|
||||
- `5` = `SPRITE_OVERFLOW`
|
||||
|
||||
Operational notes:
|
||||
|
||||
- no fallback to default bank when the sprite bank id cannot be resolved;
|
||||
- no silent no-op for invalid index/range;
|
||||
- `palette_id` and `priority` must be validated against runtime-supported ranges.
|
||||
- the canonical public sprite contract is frame-emission based;
|
||||
- no caller-provided sprite index exists in the v1 canonical ABI;
|
||||
- no `active` flag exists in the v1 canonical ABI;
|
||||
- overflow remains non-fatal and must not escalate to trap in v1.
|
||||
|
||||
@ -39,6 +39,7 @@ Example:
|
||||
```
|
||||
("gfx", "present", 1)
|
||||
("audio", "play", 2)
|
||||
("composer", "emit_sprite", 1)
|
||||
```
|
||||
|
||||
This identity is:
|
||||
@ -198,6 +199,24 @@ For `asset.load`:
|
||||
- `slot` is the target slot index;
|
||||
- bank kind is resolved from `asset_table` by `asset_id`, not supplied by the caller.
|
||||
|
||||
### Composition surface (`composer`, v1)
|
||||
|
||||
The canonical frame-orchestration public ABI uses module `composer`.
|
||||
|
||||
Canonical operations in v1 are:
|
||||
|
||||
- `composer.bind_scene(bank_id) -> (status)`
|
||||
- `composer.unbind_scene() -> (status)`
|
||||
- `composer.set_camera(x, y) -> void`
|
||||
- `composer.emit_sprite(glyph_id, palette_id, x, y, layer, bank_id, flip_x, flip_y, priority) -> (status)`
|
||||
|
||||
For mutating composer operations:
|
||||
|
||||
- `status` is a `ComposerOpStatus` value;
|
||||
- `bind_scene`, `unbind_scene`, and `emit_sprite` are status-returning;
|
||||
- `set_camera` remains `void` in v1;
|
||||
- no caller-provided sprite index or `active` flag is part of the canonical contract.
|
||||
|
||||
## 7 Syscalls as Callable Entities (Not First-Class)
|
||||
|
||||
Syscalls behave like call sites, not like first-class guest values.
|
||||
|
||||
@ -85,6 +85,9 @@ Example:
|
||||
- `asset.load` currently resolves with `arg_slots = 2` and `ret_slots = 2`.
|
||||
- The canonical stack contract is `asset_id, slot -> status, handle`.
|
||||
- Callers do not provide an explicit asset kind; the runtime derives it from `asset_table`.
|
||||
- `composer.bind_scene` resolves with `arg_slots = 1` and `ret_slots = 1`.
|
||||
- The canonical stack contract is `bank_id -> status`.
|
||||
- `composer.emit_sprite` resolves with `arg_slots = 9` and `ret_slots = 1`.
|
||||
|
||||
#### Canonical Intrinsic Registry Artifact
|
||||
|
||||
|
||||
BIN
test-cartridges/stress-console/assets.pa
Normal file
BIN
test-cartridges/stress-console/assets.pa
Normal file
Binary file not shown.
@ -5,5 +5,5 @@
|
||||
"title": "Stress Console",
|
||||
"app_version": "0.1.0",
|
||||
"app_mode": "Game",
|
||||
"capabilities": ["gfx", "log"]
|
||||
"capabilities": ["gfx", "log", "asset"]
|
||||
}
|
||||
|
||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user