Compare commits
No commits in common. "76254928e68f7bd6fc3a6dc4fc953a3984efca7b" and "98d2d81882bdfb3664e8cf6c65b931e472672f1a" have entirely different histories.
76254928e6
...
98d2d81882
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1475,8 +1475,6 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"prometeu-bytecode",
|
"prometeu-bytecode",
|
||||||
"prometeu-hal",
|
|
||||||
"serde_json",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@ -14,7 +14,7 @@ use prometeu_hal::glyph::Glyph;
|
|||||||
use prometeu_hal::glyph_bank::{GlyphBank, TileSize};
|
use prometeu_hal::glyph_bank::{GlyphBank, TileSize};
|
||||||
use prometeu_hal::sample::Sample;
|
use prometeu_hal::sample::Sample;
|
||||||
use prometeu_hal::scene_bank::SceneBank;
|
use prometeu_hal::scene_bank::SceneBank;
|
||||||
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
use prometeu_hal::scene_layer::{MotionFactor, SceneLayer};
|
||||||
use prometeu_hal::sound_bank::SoundBank;
|
use prometeu_hal::sound_bank::SoundBank;
|
||||||
use prometeu_hal::tile::Tile;
|
use prometeu_hal::tile::Tile;
|
||||||
use prometeu_hal::tilemap::TileMap;
|
use prometeu_hal::tilemap::TileMap;
|
||||||
@ -748,7 +748,7 @@ impl AssetManager {
|
|||||||
active: false,
|
active: false,
|
||||||
glyph_bank_id: 0,
|
glyph_bank_id: 0,
|
||||||
tile_size: TileSize::Size8,
|
tile_size: TileSize::Size8,
|
||||||
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
|
motion_factor: MotionFactor { x: 1.0, y: 1.0 },
|
||||||
tilemap: TileMap { width: 0, height: 0, tiles: Vec::new() },
|
tilemap: TileMap { width: 0, height: 0, tiles: Vec::new() },
|
||||||
});
|
});
|
||||||
let mut layers = layers;
|
let mut layers = layers;
|
||||||
@ -770,20 +770,20 @@ impl AssetManager {
|
|||||||
32 => TileSize::Size32,
|
32 => TileSize::Size32,
|
||||||
other => return Err(format!("Invalid SCENE tile size: {}", other)),
|
other => return Err(format!("Invalid SCENE tile size: {}", other)),
|
||||||
};
|
};
|
||||||
let parallax_factor_x = f32::from_le_bytes([
|
let motion_factor_x = f32::from_le_bytes([
|
||||||
buffer[offset + 4],
|
buffer[offset + 4],
|
||||||
buffer[offset + 5],
|
buffer[offset + 5],
|
||||||
buffer[offset + 6],
|
buffer[offset + 6],
|
||||||
buffer[offset + 7],
|
buffer[offset + 7],
|
||||||
]);
|
]);
|
||||||
let parallax_factor_y = f32::from_le_bytes([
|
let motion_factor_y = f32::from_le_bytes([
|
||||||
buffer[offset + 8],
|
buffer[offset + 8],
|
||||||
buffer[offset + 9],
|
buffer[offset + 9],
|
||||||
buffer[offset + 10],
|
buffer[offset + 10],
|
||||||
buffer[offset + 11],
|
buffer[offset + 11],
|
||||||
]);
|
]);
|
||||||
if !parallax_factor_x.is_finite() || !parallax_factor_y.is_finite() {
|
if !motion_factor_x.is_finite() || !motion_factor_y.is_finite() {
|
||||||
return Err("Invalid SCENE parallax_factor".to_string());
|
return Err("Invalid SCENE motion_factor".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let width = u32::from_le_bytes([
|
let width = u32::from_le_bytes([
|
||||||
@ -847,7 +847,7 @@ impl AssetManager {
|
|||||||
active: (flags & 0b0000_0001) != 0,
|
active: (flags & 0b0000_0001) != 0,
|
||||||
glyph_bank_id,
|
glyph_bank_id,
|
||||||
tile_size,
|
tile_size,
|
||||||
parallax_factor: ParallaxFactor { x: parallax_factor_x, y: parallax_factor_y },
|
motion_factor: MotionFactor { x: motion_factor_x, y: motion_factor_y },
|
||||||
tilemap: TileMap { width, height, tiles },
|
tilemap: TileMap { width, height, tiles },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1105,7 +1105,7 @@ mod tests {
|
|||||||
SCENE_PAYLOAD_MAGIC_V1, SCENE_PAYLOAD_VERSION_V1,
|
SCENE_PAYLOAD_MAGIC_V1, SCENE_PAYLOAD_VERSION_V1,
|
||||||
};
|
};
|
||||||
use prometeu_hal::glyph::Glyph;
|
use prometeu_hal::glyph::Glyph;
|
||||||
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
use prometeu_hal::scene_layer::{MotionFactor, SceneLayer};
|
||||||
use prometeu_hal::tile::Tile;
|
use prometeu_hal::tile::Tile;
|
||||||
use prometeu_hal::tilemap::TileMap;
|
use prometeu_hal::tilemap::TileMap;
|
||||||
|
|
||||||
@ -1144,11 +1144,11 @@ mod tests {
|
|||||||
|
|
||||||
fn test_scene() -> SceneBank {
|
fn test_scene() -> SceneBank {
|
||||||
let make_layer =
|
let make_layer =
|
||||||
|glyph_bank_id: u8, parallax_x: f32, parallax_y: f32, tile_size: TileSize| SceneLayer {
|
|glyph_bank_id: u8, motion_x: f32, motion_y: f32, tile_size: TileSize| SceneLayer {
|
||||||
active: glyph_bank_id != 3,
|
active: glyph_bank_id != 3,
|
||||||
glyph_bank_id,
|
glyph_bank_id,
|
||||||
tile_size,
|
tile_size,
|
||||||
parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_y },
|
motion_factor: MotionFactor { x: motion_x, y: motion_y },
|
||||||
tilemap: TileMap {
|
tilemap: TileMap {
|
||||||
width: 2,
|
width: 2,
|
||||||
height: 2,
|
height: 2,
|
||||||
@ -1227,8 +1227,8 @@ mod tests {
|
|||||||
data.push(layer.glyph_bank_id);
|
data.push(layer.glyph_bank_id);
|
||||||
data.push(layer.tile_size as u8);
|
data.push(layer.tile_size as u8);
|
||||||
data.push(0);
|
data.push(0);
|
||||||
data.extend_from_slice(&layer.parallax_factor.x.to_le_bytes());
|
data.extend_from_slice(&layer.motion_factor.x.to_le_bytes());
|
||||||
data.extend_from_slice(&layer.parallax_factor.y.to_le_bytes());
|
data.extend_from_slice(&layer.motion_factor.y.to_le_bytes());
|
||||||
data.extend_from_slice(&(layer.tilemap.width as u32).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.height as u32).to_le_bytes());
|
||||||
data.extend_from_slice(&(layer.tilemap.tiles.len() 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");
|
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].glyph_bank_id, 1);
|
||||||
assert_eq!(decoded.layers[1].parallax_factor.x, 0.5);
|
assert_eq!(decoded.layers[1].motion_factor.x, 0.5);
|
||||||
assert_eq!(decoded.layers[2].tile_size, TileSize::Size32);
|
assert_eq!(decoded.layers[2].tile_size, TileSize::Size32);
|
||||||
assert_eq!(decoded.layers[0].tilemap.tiles[1].flip_x, true);
|
assert_eq!(decoded.layers[0].tilemap.tiles[1].flip_x, true);
|
||||||
assert_eq!(decoded.layers[2].tilemap.tiles[2].flip_y, true);
|
assert_eq!(decoded.layers[2].tilemap.tiles[2].flip_y, true);
|
||||||
|
|||||||
@ -1,719 +0,0 @@
|
|||||||
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,4 +1,3 @@
|
|||||||
use crate::gfx_overlay::{DeferredGfxOverlay, OverlayCommand};
|
|
||||||
use crate::memory_banks::GlyphBankPoolAccess;
|
use crate::memory_banks::GlyphBankPoolAccess;
|
||||||
use prometeu_hal::GfxBridge;
|
use prometeu_hal::GfxBridge;
|
||||||
use prometeu_hal::color::Color;
|
use prometeu_hal::color::Color;
|
||||||
@ -32,11 +31,22 @@ pub enum BlendMode {
|
|||||||
|
|
||||||
/// PROMETEU Graphics Subsystem (GFX).
|
/// PROMETEU Graphics Subsystem (GFX).
|
||||||
///
|
///
|
||||||
/// `Gfx` owns the framebuffer backend and the canonical game-frame raster path
|
/// Models a specialized graphics chip with a fixed resolution, double buffering,
|
||||||
/// consumed by `FrameComposer`. That canonical path covers scene composition,
|
/// and a multi-layered tile/sprite architecture.
|
||||||
/// sprite composition, and fades. Public `gfx.*` primitives remain valid, but
|
///
|
||||||
/// they do not define the canonical game composition contract; they belong to a
|
/// The GFX system works by composing several "layers" into a single 16-bit
|
||||||
/// separate final overlay/debug stage.
|
/// 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.
|
||||||
pub struct Gfx {
|
pub struct Gfx {
|
||||||
/// Width of the internal framebuffer in pixels.
|
/// Width of the internal framebuffer in pixels.
|
||||||
w: usize,
|
w: usize,
|
||||||
@ -44,16 +54,11 @@ pub struct Gfx {
|
|||||||
h: usize,
|
h: usize,
|
||||||
/// Front buffer: the "VRAM" currently being displayed by the Host window.
|
/// Front buffer: the "VRAM" currently being displayed by the Host window.
|
||||||
front: Vec<u16>,
|
front: Vec<u16>,
|
||||||
/// Back buffer: the working buffer where canonical game frames are composed
|
/// Back buffer: the "Work RAM" where new frames are composed.
|
||||||
/// before any final overlay/debug drain.
|
|
||||||
back: Vec<u16>,
|
back: Vec<u16>,
|
||||||
|
|
||||||
/// Shared access to graphical memory banks (tiles and palettes).
|
/// Shared access to graphical memory banks (tiles and palettes).
|
||||||
pub glyph_banks: Arc<dyn GlyphBankPoolAccess>,
|
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).
|
/// Hardware sprite list (512 slots). Equivalent to OAM (Object Attribute Memory).
|
||||||
pub sprites: [Sprite; 512],
|
pub sprites: [Sprite; 512],
|
||||||
|
|
||||||
@ -66,29 +71,12 @@ pub struct Gfx {
|
|||||||
/// Target color for the HUD fade effect.
|
/// Target color for the HUD fade effect.
|
||||||
pub hud_fade_color: Color,
|
pub hud_fade_color: Color,
|
||||||
|
|
||||||
/// Internal sprite count for the current frame state.
|
/// Internal cache used to sort sprites into priority groups to optimize rendering.
|
||||||
sprite_count: usize,
|
priority_buckets: [Vec<usize>; 5],
|
||||||
/// 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];
|
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]
|
#[inline]
|
||||||
fn glyph_for_char(c: char) -> &'static [u8; 5] {
|
fn glyph_for_char(c: char) -> &'static [u8; 5] {
|
||||||
match c.to_ascii_uppercase() {
|
match c.to_ascii_uppercase() {
|
||||||
@ -219,15 +207,12 @@ impl GfxBridge for Gfx {
|
|||||||
fn present(&mut self) {
|
fn present(&mut self) {
|
||||||
self.present()
|
self.present()
|
||||||
}
|
}
|
||||||
fn render_no_scene_frame(&mut self) {
|
fn render_all(&mut self) {
|
||||||
self.render_no_scene_frame()
|
self.render_all()
|
||||||
}
|
}
|
||||||
fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate) {
|
fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate) {
|
||||||
self.render_scene_from_cache(cache, update)
|
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) {
|
fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) {
|
||||||
self.draw_text(x, y, text, color)
|
self.draw_text(x, y, text, color)
|
||||||
}
|
}
|
||||||
@ -239,7 +224,6 @@ impl GfxBridge for Gfx {
|
|||||||
&self.sprites[index]
|
&self.sprites[index]
|
||||||
}
|
}
|
||||||
fn sprite_mut(&mut self, index: usize) -> &mut Sprite {
|
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]
|
&mut self.sprites[index]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,7 +262,6 @@ impl Gfx {
|
|||||||
glyph: EMPTY_GLYPH,
|
glyph: EMPTY_GLYPH,
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
layer: 0,
|
|
||||||
bank_id: 0,
|
bank_id: 0,
|
||||||
active: false,
|
active: false,
|
||||||
flip_x: false,
|
flip_x: false,
|
||||||
@ -293,15 +276,13 @@ impl Gfx {
|
|||||||
front: vec![0; len],
|
front: vec![0; len],
|
||||||
back: vec![0; len],
|
back: vec![0; len],
|
||||||
glyph_banks,
|
glyph_banks,
|
||||||
overlay: DeferredGfxOverlay::default(),
|
|
||||||
is_draining_overlay: false,
|
|
||||||
sprites: [EMPTY_SPRITE; 512],
|
sprites: [EMPTY_SPRITE; 512],
|
||||||
sprite_count: 0,
|
|
||||||
scene_fade_level: 31,
|
scene_fade_level: 31,
|
||||||
scene_fade_color: Color::BLACK,
|
scene_fade_color: Color::BLACK,
|
||||||
hud_fade_level: 31,
|
hud_fade_level: 31,
|
||||||
hud_fade_color: Color::BLACK,
|
hud_fade_color: Color::BLACK,
|
||||||
layer_buckets: [
|
priority_buckets: [
|
||||||
|
Vec::with_capacity(128),
|
||||||
Vec::with_capacity(128),
|
Vec::with_capacity(128),
|
||||||
Vec::with_capacity(128),
|
Vec::with_capacity(128),
|
||||||
Vec::with_capacity(128),
|
Vec::with_capacity(128),
|
||||||
@ -314,42 +295,6 @@ impl Gfx {
|
|||||||
(self.w, self.h)
|
(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).
|
/// The buffer that the host should display (RGB565).
|
||||||
pub fn front_buffer(&self) -> &[u16] {
|
pub fn front_buffer(&self) -> &[u16] {
|
||||||
&self.front
|
&self.front
|
||||||
@ -369,10 +314,6 @@ impl Gfx {
|
|||||||
color: Color,
|
color: Color,
|
||||||
mode: BlendMode,
|
mode: BlendMode,
|
||||||
) {
|
) {
|
||||||
if !self.is_draining_overlay {
|
|
||||||
self.overlay.push(OverlayCommand::FillRectBlend { x, y, w, h, color, mode });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if color == Color::COLOR_KEY {
|
if color == Color::COLOR_KEY {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -414,10 +355,6 @@ impl Gfx {
|
|||||||
|
|
||||||
/// Draws a line between two points using Bresenham's algorithm.
|
/// 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) {
|
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 {
|
if color == Color::COLOR_KEY {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -450,10 +387,6 @@ impl Gfx {
|
|||||||
|
|
||||||
/// Draws a circle outline using Midpoint Circle Algorithm.
|
/// Draws a circle outline using Midpoint Circle Algorithm.
|
||||||
pub fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
|
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 {
|
if color == Color::COLOR_KEY {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -522,10 +455,6 @@ impl Gfx {
|
|||||||
|
|
||||||
/// Draws a disc (filled circle with border).
|
/// 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) {
|
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.fill_circle(x, y, r, fill_color);
|
||||||
self.draw_circle(x, y, r, border_color);
|
self.draw_circle(x, y, r, border_color);
|
||||||
}
|
}
|
||||||
@ -555,10 +484,6 @@ impl Gfx {
|
|||||||
border_color: Color,
|
border_color: Color,
|
||||||
fill_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.fill_rect(x, y, w, h, fill_color);
|
||||||
self.draw_rect(x, y, w, h, border_color);
|
self.draw_rect(x, y, w, h, border_color);
|
||||||
}
|
}
|
||||||
@ -605,33 +530,23 @@ impl Gfx {
|
|||||||
std::mem::swap(&mut self.front, &mut self.back);
|
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.
|
/// The main rendering pipeline.
|
||||||
///
|
///
|
||||||
/// This method composes the final frame by rasterizing layers and sprites in the
|
/// This method composes the final frame by rasterizing layers and sprites in the
|
||||||
/// correct priority order into the back buffer.
|
/// correct priority order into the back buffer.
|
||||||
/// Follows the hardware model where layers and sprites are composed every frame.
|
/// Follows the hardware model where layers and sprites are composed every frame.
|
||||||
pub fn render_no_scene_frame(&mut self) {
|
pub fn render_all(&mut self) {
|
||||||
self.populate_layer_buckets();
|
self.populate_priority_buckets();
|
||||||
for bucket in &self.layer_buckets {
|
|
||||||
|
// 1. Priority 0 sprites: drawn at the very back, behind everything else.
|
||||||
Self::draw_bucket_on_buffer(
|
Self::draw_bucket_on_buffer(
|
||||||
&mut self.back,
|
&mut self.back,
|
||||||
self.w,
|
self.w,
|
||||||
self.h,
|
self.h,
|
||||||
bucket,
|
&self.priority_buckets[0],
|
||||||
&self.sprites,
|
&self.sprites,
|
||||||
&*self.glyph_banks,
|
&*self.glyph_banks,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Scene-only fallback path: sprites and fades still work even before a
|
// 2. Scene-only fallback path: sprites and fades still work even before a
|
||||||
// cache-backed world composition request is issued for the frame.
|
// cache-backed world composition request is issued for the frame.
|
||||||
@ -648,17 +563,18 @@ impl Gfx {
|
|||||||
/// plus sprite state and fade controls.
|
/// plus sprite state and fade controls.
|
||||||
pub fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate) {
|
pub fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate) {
|
||||||
self.back.fill(Color::BLACK.raw());
|
self.back.fill(Color::BLACK.raw());
|
||||||
self.populate_layer_buckets();
|
self.populate_priority_buckets();
|
||||||
|
|
||||||
for layer_index in 0..cache.layers.len() {
|
|
||||||
Self::draw_bucket_on_buffer(
|
Self::draw_bucket_on_buffer(
|
||||||
&mut self.back,
|
&mut self.back,
|
||||||
self.w,
|
self.w,
|
||||||
self.h,
|
self.h,
|
||||||
&self.layer_buckets[layer_index],
|
&self.priority_buckets[0],
|
||||||
&self.sprites,
|
&self.sprites,
|
||||||
&*self.glyph_banks,
|
&*self.glyph_banks,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
for layer_index in 0..cache.layers.len() {
|
||||||
Self::draw_cache_layer_to_buffer(
|
Self::draw_cache_layer_to_buffer(
|
||||||
&mut self.back,
|
&mut self.back,
|
||||||
self.w,
|
self.w,
|
||||||
@ -667,26 +583,31 @@ impl Gfx {
|
|||||||
&update.copy_requests[layer_index],
|
&update.copy_requests[layer_index],
|
||||||
&*self.glyph_banks,
|
&*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.scene_fade_level, self.scene_fade_color);
|
||||||
Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color);
|
Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn populate_layer_buckets(&mut self) {
|
fn populate_priority_buckets(&mut self) {
|
||||||
for bucket in self.layer_buckets.iter_mut() {
|
for bucket in self.priority_buckets.iter_mut() {
|
||||||
bucket.clear();
|
bucket.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (idx, sprite) in self.sprites.iter().take(self.sprite_count).enumerate() {
|
for (idx, sprite) in self.sprites.iter().enumerate() {
|
||||||
if sprite.active && (sprite.layer as usize) < self.layer_buckets.len() {
|
if sprite.active && sprite.priority < 5 {
|
||||||
self.layer_buckets[sprite.layer as usize].push(idx);
|
self.priority_buckets[sprite.priority 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(
|
fn draw_cache_layer_to_buffer(
|
||||||
@ -697,7 +618,6 @@ impl Gfx {
|
|||||||
request: &LayerCopyRequest,
|
request: &LayerCopyRequest,
|
||||||
glyph_banks: &dyn GlyphBankPoolAccess,
|
glyph_banks: &dyn GlyphBankPoolAccess,
|
||||||
) {
|
) {
|
||||||
let mut target = RenderTarget { back, screen_w, screen_h };
|
|
||||||
let layer_cache = &cache.layers[request.layer_index];
|
let layer_cache = &cache.layers[request.layer_index];
|
||||||
if !layer_cache.valid {
|
if !layer_cache.valid {
|
||||||
return;
|
return;
|
||||||
@ -726,43 +646,52 @@ impl Gfx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Self::draw_cached_tile_pixels(
|
Self::draw_cached_tile_pixels(
|
||||||
&mut target,
|
back,
|
||||||
CachedTileDraw {
|
screen_w,
|
||||||
x: screen_tile_x,
|
screen_h,
|
||||||
y: screen_tile_y,
|
screen_tile_x,
|
||||||
|
screen_tile_y,
|
||||||
entry,
|
entry,
|
||||||
bank: &bank,
|
&bank,
|
||||||
tile_size: request.tile_size,
|
request.tile_size,
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_cached_tile_pixels(target: &mut RenderTarget<'_>, tile: CachedTileDraw<'_>) {
|
fn draw_cached_tile_pixels(
|
||||||
let size = tile.tile_size as usize;
|
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;
|
||||||
|
|
||||||
for local_y in 0..size {
|
for local_y in 0..size {
|
||||||
let world_y = tile.y + local_y as i32;
|
let world_y = y + local_y as i32;
|
||||||
if world_y < 0 || world_y >= target.screen_h as i32 {
|
if world_y < 0 || world_y >= screen_h as i32 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for local_x in 0..size {
|
for local_x in 0..size {
|
||||||
let world_x = tile.x + local_x as i32;
|
let world_x = x + local_x as i32;
|
||||||
if world_x < 0 || world_x >= target.screen_w as i32 {
|
if world_x < 0 || world_x >= screen_w as i32 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let fetch_x = if tile.entry.flip_x() { size - 1 - local_x } else { local_x };
|
let fetch_x = if 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 fetch_y = if 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);
|
let px_index = bank.get_pixel_index(entry.glyph_id, fetch_x, fetch_y);
|
||||||
if px_index == 0 {
|
if px_index == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let color = tile.bank.resolve_color(tile.entry.palette_id, px_index);
|
let color = bank.resolve_color(entry.palette_id, px_index);
|
||||||
target.back[world_y as usize * target.screen_w + world_x as usize] = color.raw();
|
back[world_y as usize * screen_w + world_x as usize] = color.raw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -846,10 +775,6 @@ impl Gfx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) {
|
pub fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) {
|
||||||
if !self.is_draining_overlay {
|
|
||||||
self.overlay.push(OverlayCommand::DrawText { x, y, text: text.to_string(), color });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let mut cx = x;
|
let mut cx = x;
|
||||||
for c in text.chars() {
|
for c in text.chars() {
|
||||||
self.draw_char(cx, y, c, color);
|
self.draw_char(cx, y, c, color);
|
||||||
@ -894,11 +819,10 @@ impl Gfx {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::FrameComposer;
|
use crate::memory_banks::{GlyphBankPoolInstaller, MemoryBanks};
|
||||||
use crate::memory_banks::{GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess};
|
|
||||||
use prometeu_hal::glyph_bank::TileSize;
|
use prometeu_hal::glyph_bank::TileSize;
|
||||||
use prometeu_hal::scene_bank::SceneBank;
|
use prometeu_hal::scene_bank::SceneBank;
|
||||||
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
use prometeu_hal::scene_layer::{MotionFactor, SceneLayer};
|
||||||
use prometeu_hal::scene_viewport_cache::SceneViewportCache;
|
use prometeu_hal::scene_viewport_cache::SceneViewportCache;
|
||||||
use prometeu_hal::scene_viewport_resolver::SceneViewportResolver;
|
use prometeu_hal::scene_viewport_resolver::SceneViewportResolver;
|
||||||
use prometeu_hal::tile::Tile;
|
use prometeu_hal::tile::Tile;
|
||||||
@ -929,7 +853,7 @@ mod tests {
|
|||||||
active: true,
|
active: true,
|
||||||
glyph_bank_id,
|
glyph_bank_id,
|
||||||
tile_size: TileSize::Size8,
|
tile_size: TileSize::Size8,
|
||||||
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
|
motion_factor: MotionFactor { x: 1.0, y: 1.0 },
|
||||||
tilemap: TileMap {
|
tilemap: TileMap {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@ -951,7 +875,7 @@ mod tests {
|
|||||||
active: false,
|
active: false,
|
||||||
glyph_bank_id,
|
glyph_bank_id,
|
||||||
tile_size: TileSize::Size8,
|
tile_size: TileSize::Size8,
|
||||||
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
|
motion_factor: MotionFactor { x: 1.0, y: 1.0 },
|
||||||
tilemap: TileMap { width, height, tiles: vec![Tile::default(); width * height] },
|
tilemap: TileMap { width, height, tiles: vec![Tile::default(); width * height] },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -994,12 +918,9 @@ mod tests {
|
|||||||
fn test_draw_line() {
|
fn test_draw_line() {
|
||||||
let banks = Arc::new(MemoryBanks::new());
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
let mut gfx = Gfx::new(10, 10, banks);
|
let mut gfx = Gfx::new(10, 10, banks);
|
||||||
gfx.begin_overlay_frame();
|
|
||||||
gfx.draw_line(0, 0, 9, 9, Color::WHITE);
|
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[0], Color::WHITE.0);
|
||||||
assert_eq!(gfx.back[9 * 10 + 9], Color::WHITE.0);
|
assert_eq!(gfx.back[9 * 10 + 9], Color::WHITE.0);
|
||||||
assert_eq!(gfx.overlay().command_count(), 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -1025,54 +946,11 @@ mod tests {
|
|||||||
fn test_draw_square() {
|
fn test_draw_square() {
|
||||||
let banks = Arc::new(MemoryBanks::new());
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
let mut gfx = Gfx::new(10, 10, banks);
|
let mut gfx = Gfx::new(10, 10, banks);
|
||||||
gfx.begin_overlay_frame();
|
|
||||||
gfx.draw_square(2, 2, 6, 6, Color::WHITE, Color::BLACK);
|
gfx.draw_square(2, 2, 6, 6, Color::WHITE, Color::BLACK);
|
||||||
gfx.drain_overlay_debug();
|
|
||||||
// Border
|
// Border
|
||||||
assert_eq!(gfx.back[2 * 10 + 2], Color::WHITE.0);
|
assert_eq!(gfx.back[2 * 10 + 2], Color::WHITE.0);
|
||||||
// Fill
|
// Fill
|
||||||
assert_eq!(gfx.back[3 * 10 + 3], Color::BLACK.0);
|
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]
|
#[test]
|
||||||
@ -1126,7 +1004,6 @@ mod tests {
|
|||||||
glyph: Glyph { glyph_id: 0, palette_id: 4 },
|
glyph: Glyph { glyph_id: 0, palette_id: 4 },
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
layer: 0,
|
|
||||||
bank_id: 0,
|
bank_id: 0,
|
||||||
active: true,
|
active: true,
|
||||||
flip_x: false,
|
flip_x: false,
|
||||||
@ -1137,57 +1014,17 @@ mod tests {
|
|||||||
glyph: Glyph { glyph_id: 0, palette_id: 4 },
|
glyph: Glyph { glyph_id: 0, palette_id: 4 },
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
layer: 2,
|
|
||||||
bank_id: 0,
|
bank_id: 0,
|
||||||
active: true,
|
active: true,
|
||||||
flip_x: false,
|
flip_x: false,
|
||||||
flip_y: false,
|
flip_y: false,
|
||||||
priority: 2,
|
priority: 2,
|
||||||
};
|
};
|
||||||
gfx.sprite_count = 2;
|
|
||||||
|
|
||||||
gfx.render_scene_from_cache(&cache, &update);
|
gfx.render_scene_from_cache(&cache, &update);
|
||||||
|
|
||||||
assert_eq!(gfx.back[0], Color::BLUE.raw());
|
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.
|
/// Blends in RGB565 per channel with saturation.
|
||||||
|
|||||||
@ -1,39 +0,0 @@
|
|||||||
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,15 +1,13 @@
|
|||||||
use crate::asset::AssetManager;
|
use crate::asset::AssetManager;
|
||||||
use crate::audio::Audio;
|
use crate::audio::Audio;
|
||||||
use crate::frame_composer::FrameComposer;
|
|
||||||
use crate::gfx::Gfx;
|
use crate::gfx::Gfx;
|
||||||
use crate::memory_banks::{
|
use crate::memory_banks::{
|
||||||
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess,
|
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller,
|
||||||
SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller,
|
SoundBankPoolAccess, SoundBankPoolInstaller,
|
||||||
};
|
};
|
||||||
use crate::pad::Pad;
|
use crate::pad::Pad;
|
||||||
use crate::touch::Touch;
|
use crate::touch::Touch;
|
||||||
use prometeu_hal::cartridge::AssetsPayloadSource;
|
use prometeu_hal::cartridge::AssetsPayloadSource;
|
||||||
use prometeu_hal::sprite::Sprite;
|
|
||||||
use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge};
|
use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@ -28,8 +26,6 @@ use std::sync::Arc;
|
|||||||
pub struct Hardware {
|
pub struct Hardware {
|
||||||
/// The Graphics Processing Unit (GPU). Handles drawing primitives, sprites, and tilemaps.
|
/// The Graphics Processing Unit (GPU). Handles drawing primitives, sprites, and tilemaps.
|
||||||
pub gfx: Gfx,
|
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.
|
/// The Sound Processing Unit (SPU). Manages sample playback and volume control.
|
||||||
pub audio: Audio,
|
pub audio: Audio,
|
||||||
/// The standard digital gamepad. Provides state for D-Pad, face buttons, and triggers.
|
/// The standard digital gamepad. Provides state for D-Pad, face buttons, and triggers.
|
||||||
@ -47,36 +43,6 @@ impl Default for Hardware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HardwareBridge 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 {
|
fn gfx(&self) -> &dyn GfxBridge {
|
||||||
&self.gfx
|
&self.gfx
|
||||||
}
|
}
|
||||||
@ -132,11 +98,6 @@ impl Hardware {
|
|||||||
Self::H,
|
Self::H,
|
||||||
Arc::clone(&memory_banks) as Arc<dyn GlyphBankPoolAccess>,
|
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>),
|
audio: Audio::new(Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolAccess>),
|
||||||
pad: Pad::default(),
|
pad: Pad::default(),
|
||||||
touch: Touch::default(),
|
touch: Touch::default(),
|
||||||
@ -161,7 +122,7 @@ mod tests {
|
|||||||
use prometeu_hal::glyph::Glyph;
|
use prometeu_hal::glyph::Glyph;
|
||||||
use prometeu_hal::glyph_bank::{GlyphBank, TileSize};
|
use prometeu_hal::glyph_bank::{GlyphBank, TileSize};
|
||||||
use prometeu_hal::scene_bank::SceneBank;
|
use prometeu_hal::scene_bank::SceneBank;
|
||||||
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
use prometeu_hal::scene_layer::{MotionFactor, SceneLayer};
|
||||||
use prometeu_hal::scene_viewport_cache::SceneViewportCache;
|
use prometeu_hal::scene_viewport_cache::SceneViewportCache;
|
||||||
use prometeu_hal::scene_viewport_resolver::SceneViewportResolver;
|
use prometeu_hal::scene_viewport_resolver::SceneViewportResolver;
|
||||||
use prometeu_hal::tile::Tile;
|
use prometeu_hal::tile::Tile;
|
||||||
@ -181,7 +142,7 @@ mod tests {
|
|||||||
active: true,
|
active: true,
|
||||||
glyph_bank_id: 0,
|
glyph_bank_id: 0,
|
||||||
tile_size: TileSize::Size8,
|
tile_size: TileSize::Size8,
|
||||||
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
|
motion_factor: MotionFactor { x: 1.0, y: 1.0 },
|
||||||
tilemap: TileMap {
|
tilemap: TileMap {
|
||||||
width: 4,
|
width: 4,
|
||||||
height: 4,
|
height: 4,
|
||||||
@ -221,20 +182,4 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(hardware.gfx.front_buffer()[0], Color::RED.raw());
|
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,8 +1,6 @@
|
|||||||
mod asset;
|
mod asset;
|
||||||
mod audio;
|
mod audio;
|
||||||
mod frame_composer;
|
|
||||||
mod gfx;
|
mod gfx;
|
||||||
mod gfx_overlay;
|
|
||||||
pub mod hardware;
|
pub mod hardware;
|
||||||
mod memory_banks;
|
mod memory_banks;
|
||||||
mod pad;
|
mod pad;
|
||||||
@ -10,9 +8,7 @@ mod touch;
|
|||||||
|
|
||||||
pub use crate::asset::AssetManager;
|
pub use crate::asset::AssetManager;
|
||||||
pub use crate::audio::{Audio, AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};
|
pub use crate::audio::{Audio, AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};
|
||||||
pub use crate::frame_composer::{FrameComposer, SceneStatus, SpriteController};
|
|
||||||
pub use crate::gfx::Gfx;
|
pub use crate::gfx::Gfx;
|
||||||
pub use crate::gfx_overlay::{DeferredGfxOverlay, OverlayCommand};
|
|
||||||
pub use crate::memory_banks::{
|
pub use crate::memory_banks::{
|
||||||
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess,
|
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess,
|
||||||
SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller,
|
SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller,
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
#[repr(i32)]
|
|
||||||
pub enum ComposerOpStatus {
|
|
||||||
Ok = 0,
|
|
||||||
SceneUnavailable = 1,
|
|
||||||
ArgRangeInvalid = 2,
|
|
||||||
BankInvalid = 3,
|
|
||||||
LayerInvalid = 4,
|
|
||||||
SpriteOverflow = 5,
|
|
||||||
}
|
|
||||||
@ -48,21 +48,8 @@ pub trait GfxBridge {
|
|||||||
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color);
|
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 draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color);
|
||||||
fn present(&mut self);
|
fn present(&mut self);
|
||||||
/// Render the canonical game frame with no bound scene.
|
fn render_all(&mut self);
|
||||||
///
|
|
||||||
/// 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 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_text(&mut self, x: i32, y: i32, text: &str, color: Color);
|
||||||
fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color);
|
fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color);
|
||||||
|
|
||||||
|
|||||||
@ -2,18 +2,9 @@ use crate::asset_bridge::AssetBridge;
|
|||||||
use crate::audio_bridge::AudioBridge;
|
use crate::audio_bridge::AudioBridge;
|
||||||
use crate::gfx_bridge::GfxBridge;
|
use crate::gfx_bridge::GfxBridge;
|
||||||
use crate::pad_bridge::PadBridge;
|
use crate::pad_bridge::PadBridge;
|
||||||
use crate::sprite::Sprite;
|
|
||||||
use crate::touch_bridge::TouchBridge;
|
use crate::touch_bridge::TouchBridge;
|
||||||
|
|
||||||
pub trait HardwareBridge {
|
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(&self) -> &dyn GfxBridge;
|
||||||
fn gfx_mut(&mut self) -> &mut dyn GfxBridge;
|
fn gfx_mut(&mut self) -> &mut dyn GfxBridge;
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,6 @@ pub mod button;
|
|||||||
pub mod cartridge;
|
pub mod cartridge;
|
||||||
pub mod cartridge_loader;
|
pub mod cartridge_loader;
|
||||||
pub mod color;
|
pub mod color;
|
||||||
pub mod composer_status;
|
|
||||||
pub mod debugger_protocol;
|
pub mod debugger_protocol;
|
||||||
pub mod gfx_bridge;
|
pub mod gfx_bridge;
|
||||||
pub mod glyph;
|
pub mod glyph;
|
||||||
@ -35,7 +34,6 @@ pub mod window;
|
|||||||
|
|
||||||
pub use asset_bridge::AssetBridge;
|
pub use asset_bridge::AssetBridge;
|
||||||
pub use audio_bridge::{AudioBridge, AudioOpStatus, LoopMode};
|
pub use audio_bridge::{AudioBridge, AudioOpStatus, LoopMode};
|
||||||
pub use composer_status::ComposerOpStatus;
|
|
||||||
pub use gfx_bridge::{BlendMode, GfxBridge, GfxOpStatus};
|
pub use gfx_bridge::{BlendMode, GfxBridge, GfxOpStatus};
|
||||||
pub use hardware_bridge::HardwareBridge;
|
pub use hardware_bridge::HardwareBridge;
|
||||||
pub use host_context::{HostContext, HostContextProvider};
|
pub use host_context::{HostContext, HostContextProvider};
|
||||||
|
|||||||
@ -10,16 +10,16 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::glyph::Glyph;
|
use crate::glyph::Glyph;
|
||||||
use crate::glyph_bank::TileSize;
|
use crate::glyph_bank::TileSize;
|
||||||
use crate::scene_layer::ParallaxFactor;
|
use crate::scene_layer::MotionFactor;
|
||||||
use crate::tile::Tile;
|
use crate::tile::Tile;
|
||||||
use crate::tilemap::TileMap;
|
use crate::tilemap::TileMap;
|
||||||
|
|
||||||
fn layer(glyph_bank_id: u8, parallax_x: f32, parallax_y: f32, glyph_id: u16) -> SceneLayer {
|
fn layer(glyph_bank_id: u8, motion_x: f32, motion_y: f32, glyph_id: u16) -> SceneLayer {
|
||||||
SceneLayer {
|
SceneLayer {
|
||||||
active: true,
|
active: true,
|
||||||
glyph_bank_id,
|
glyph_bank_id,
|
||||||
tile_size: TileSize::Size16,
|
tile_size: TileSize::Size16,
|
||||||
parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_y },
|
motion_factor: MotionFactor { x: motion_x, y: motion_y },
|
||||||
tilemap: TileMap {
|
tilemap: TileMap {
|
||||||
width: 1,
|
width: 1,
|
||||||
height: 1,
|
height: 1,
|
||||||
|
|||||||
@ -2,7 +2,7 @@ use crate::glyph_bank::TileSize;
|
|||||||
use crate::tilemap::TileMap;
|
use crate::tilemap::TileMap;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct ParallaxFactor {
|
pub struct MotionFactor {
|
||||||
pub x: f32,
|
pub x: f32,
|
||||||
pub y: f32,
|
pub y: f32,
|
||||||
}
|
}
|
||||||
@ -12,7 +12,7 @@ pub struct SceneLayer {
|
|||||||
pub active: bool,
|
pub active: bool,
|
||||||
pub glyph_bank_id: u8,
|
pub glyph_bank_id: u8,
|
||||||
pub tile_size: TileSize,
|
pub tile_size: TileSize,
|
||||||
pub parallax_factor: ParallaxFactor,
|
pub motion_factor: MotionFactor,
|
||||||
pub tilemap: TileMap,
|
pub tilemap: TileMap,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,12 +23,12 @@ mod tests {
|
|||||||
use crate::tile::Tile;
|
use crate::tile::Tile;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scene_layer_preserves_parallax_factor_and_tilemap_ownership() {
|
fn scene_layer_preserves_motion_factor_and_tilemap_ownership() {
|
||||||
let layer = SceneLayer {
|
let layer = SceneLayer {
|
||||||
active: true,
|
active: true,
|
||||||
glyph_bank_id: 7,
|
glyph_bank_id: 7,
|
||||||
tile_size: TileSize::Size16,
|
tile_size: TileSize::Size16,
|
||||||
parallax_factor: ParallaxFactor { x: 0.5, y: 0.75 },
|
motion_factor: MotionFactor { x: 0.5, y: 0.75 },
|
||||||
tilemap: TileMap {
|
tilemap: TileMap {
|
||||||
width: 2,
|
width: 2,
|
||||||
height: 1,
|
height: 1,
|
||||||
@ -50,8 +50,8 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(layer.glyph_bank_id, 7);
|
assert_eq!(layer.glyph_bank_id, 7);
|
||||||
assert_eq!(layer.parallax_factor.x, 0.5);
|
assert_eq!(layer.motion_factor.x, 0.5);
|
||||||
assert_eq!(layer.parallax_factor.y, 0.75);
|
assert_eq!(layer.motion_factor.y, 0.75);
|
||||||
assert_eq!(layer.tilemap.width, 2);
|
assert_eq!(layer.tilemap.width, 2);
|
||||||
assert_eq!(layer.tilemap.tiles[1].glyph.glyph_id, 22);
|
assert_eq!(layer.tilemap.tiles[1].glyph.glyph_id, 22);
|
||||||
assert!(layer.tilemap.tiles[1].flip_x);
|
assert!(layer.tilemap.tiles[1].flip_x);
|
||||||
|
|||||||
@ -270,7 +270,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::glyph::Glyph;
|
use crate::glyph::Glyph;
|
||||||
use crate::glyph_bank::TileSize;
|
use crate::glyph_bank::TileSize;
|
||||||
use crate::scene_layer::ParallaxFactor;
|
use crate::scene_layer::MotionFactor;
|
||||||
use crate::tile::Tile;
|
use crate::tile::Tile;
|
||||||
use crate::tilemap::TileMap;
|
use crate::tilemap::TileMap;
|
||||||
|
|
||||||
@ -295,7 +295,7 @@ mod tests {
|
|||||||
active: true,
|
active: true,
|
||||||
glyph_bank_id,
|
glyph_bank_id,
|
||||||
tile_size: TileSize::Size16,
|
tile_size: TileSize::Size16,
|
||||||
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
|
motion_factor: MotionFactor { x: 1.0, y: 1.0 },
|
||||||
tilemap: TileMap { width: 4, height: 4, tiles },
|
tilemap: TileMap { width: 4, height: 4, tiles },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -325,34 +325,6 @@ mod tests {
|
|||||||
assert_eq!(cache.layers[0].ring_origin(), (1, 1));
|
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]
|
#[test]
|
||||||
fn cache_entry_fields_are_derived_from_scene_tiles() {
|
fn cache_entry_fields_are_derived_from_scene_tiles() {
|
||||||
let scene = make_scene();
|
let scene = make_scene();
|
||||||
@ -443,113 +415,6 @@ mod tests {
|
|||||||
assert_eq!(cache.layers[3].entry(3, 3).glyph_id, 415);
|
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]
|
#[test]
|
||||||
fn materialization_populates_all_four_layers() {
|
fn materialization_populates_all_four_layers() {
|
||||||
let scene = make_scene();
|
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_inputs: [(i32, i32, i32, i32, i32); 4] = std::array::from_fn(|i| {
|
||||||
let layer = &scene.layers[i];
|
let layer = &scene.layers[i];
|
||||||
let tile_size_px = layer.tile_size as i32;
|
let tile_size_px = layer.tile_size as i32;
|
||||||
let layer_camera_x_px = ((camera_x_px as f32) * layer.parallax_factor.x).floor() 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.parallax_factor.y).floor() as i32;
|
let layer_camera_y_px = ((camera_y_px as f32) * layer.motion_factor.y).floor() as i32;
|
||||||
let layer_center_x_px = layer_camera_x_px + self.viewport_width_px / 2;
|
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;
|
let layer_center_y_px = layer_camera_y_px + self.viewport_height_px / 2;
|
||||||
(
|
(
|
||||||
@ -388,14 +388,14 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::glyph::Glyph;
|
use crate::glyph::Glyph;
|
||||||
use crate::glyph_bank::TileSize;
|
use crate::glyph_bank::TileSize;
|
||||||
use crate::scene_layer::{ParallaxFactor, SceneLayer};
|
use crate::scene_layer::{MotionFactor, SceneLayer};
|
||||||
use crate::tile::Tile;
|
use crate::tile::Tile;
|
||||||
use crate::tilemap::TileMap;
|
use crate::tilemap::TileMap;
|
||||||
|
|
||||||
fn make_layer(
|
fn make_layer(
|
||||||
tile_size: TileSize,
|
tile_size: TileSize,
|
||||||
parallax_x: f32,
|
motion_x: f32,
|
||||||
parallax_y: f32,
|
motion_y: f32,
|
||||||
width: usize,
|
width: usize,
|
||||||
height: usize,
|
height: usize,
|
||||||
) -> SceneLayer {
|
) -> SceneLayer {
|
||||||
@ -413,7 +413,7 @@ mod tests {
|
|||||||
active: true,
|
active: true,
|
||||||
glyph_bank_id: 1,
|
glyph_bank_id: 1,
|
||||||
tile_size,
|
tile_size,
|
||||||
parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_y },
|
motion_factor: MotionFactor { x: motion_x, y: motion_y },
|
||||||
tilemap: TileMap { width, height, tiles },
|
tilemap: TileMap { width, height, tiles },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -443,7 +443,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn per_layer_copy_requests_follow_parallax_factor() {
|
fn per_layer_copy_requests_follow_motion_factor() {
|
||||||
let scene = make_scene();
|
let scene = make_scene();
|
||||||
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
|
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,6 @@ pub struct Sprite {
|
|||||||
pub glyph: Glyph,
|
pub glyph: Glyph,
|
||||||
pub x: i32,
|
pub x: i32,
|
||||||
pub y: i32,
|
pub y: i32,
|
||||||
pub layer: u8,
|
|
||||||
pub bank_id: u8,
|
pub bank_id: u8,
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
pub flip_x: bool,
|
pub flip_x: bool,
|
||||||
|
|||||||
@ -19,7 +19,6 @@ pub use resolver::{
|
|||||||
/// Each Syscall has a unique 32-bit ID. The IDs are grouped by category:
|
/// Each Syscall has a unique 32-bit ID. The IDs are grouped by category:
|
||||||
/// - **0x0xxx**: System & OS Control
|
/// - **0x0xxx**: System & OS Control
|
||||||
/// - **0x1xxx**: Graphics (GFX)
|
/// - **0x1xxx**: Graphics (GFX)
|
||||||
/// - **0x11xx**: Frame Composer orchestration
|
|
||||||
/// - **0x2xxx**: Reserved for legacy input syscalls (disabled for v1 VM-owned input)
|
/// - **0x2xxx**: Reserved for legacy input syscalls (disabled for v1 VM-owned input)
|
||||||
/// - **0x3xxx**: Audio (PCM & Mixing)
|
/// - **0x3xxx**: Audio (PCM & Mixing)
|
||||||
/// - **0x4xxx**: Filesystem (Sandboxed I/O)
|
/// - **0x4xxx**: Filesystem (Sandboxed I/O)
|
||||||
@ -36,12 +35,9 @@ pub enum Syscall {
|
|||||||
GfxDrawCircle = 0x1004,
|
GfxDrawCircle = 0x1004,
|
||||||
GfxDrawDisc = 0x1005,
|
GfxDrawDisc = 0x1005,
|
||||||
GfxDrawSquare = 0x1006,
|
GfxDrawSquare = 0x1006,
|
||||||
|
GfxSetSprite = 0x1007,
|
||||||
GfxDrawText = 0x1008,
|
GfxDrawText = 0x1008,
|
||||||
GfxClear565 = 0x1010,
|
GfxClear565 = 0x1010,
|
||||||
ComposerBindScene = 0x1101,
|
|
||||||
ComposerUnbindScene = 0x1102,
|
|
||||||
ComposerSetCamera = 0x1103,
|
|
||||||
ComposerEmitSprite = 0x1104,
|
|
||||||
AudioPlaySample = 0x3001,
|
AudioPlaySample = 0x3001,
|
||||||
AudioPlay = 0x3002,
|
AudioPlay = 0x3002,
|
||||||
FsOpen = 0x4001,
|
FsOpen = 0x4001,
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
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,6 +25,11 @@ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
|||||||
.args(6)
|
.args(6)
|
||||||
.caps(caps::GFX)
|
.caps(caps::GFX)
|
||||||
.cost(5),
|
.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")
|
SyscallRegistryEntry::builder(Syscall::GfxDrawText, "gfx", "draw_text")
|
||||||
.args(4)
|
.args(4)
|
||||||
.caps(caps::GFX)
|
.caps(caps::GFX)
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
mod asset;
|
mod asset;
|
||||||
mod audio;
|
mod audio;
|
||||||
mod bank;
|
mod bank;
|
||||||
mod composer;
|
|
||||||
mod fs;
|
mod fs;
|
||||||
mod gfx;
|
mod gfx;
|
||||||
mod log;
|
mod log;
|
||||||
@ -13,7 +12,6 @@ pub(crate) fn all_entries() -> impl Iterator<Item = &'static SyscallRegistryEntr
|
|||||||
system::ENTRIES
|
system::ENTRIES
|
||||||
.iter()
|
.iter()
|
||||||
.chain(gfx::ENTRIES.iter())
|
.chain(gfx::ENTRIES.iter())
|
||||||
.chain(composer::ENTRIES.iter())
|
|
||||||
.chain(audio::ENTRIES.iter())
|
.chain(audio::ENTRIES.iter())
|
||||||
.chain(fs::ENTRIES.iter())
|
.chain(fs::ENTRIES.iter())
|
||||||
.chain(log::ENTRIES.iter())
|
.chain(log::ENTRIES.iter())
|
||||||
|
|||||||
@ -20,12 +20,9 @@ impl Syscall {
|
|||||||
0x1004 => Some(Self::GfxDrawCircle),
|
0x1004 => Some(Self::GfxDrawCircle),
|
||||||
0x1005 => Some(Self::GfxDrawDisc),
|
0x1005 => Some(Self::GfxDrawDisc),
|
||||||
0x1006 => Some(Self::GfxDrawSquare),
|
0x1006 => Some(Self::GfxDrawSquare),
|
||||||
|
0x1007 => Some(Self::GfxSetSprite),
|
||||||
0x1008 => Some(Self::GfxDrawText),
|
0x1008 => Some(Self::GfxDrawText),
|
||||||
0x1010 => Some(Self::GfxClear565),
|
0x1010 => Some(Self::GfxClear565),
|
||||||
0x1101 => Some(Self::ComposerBindScene),
|
|
||||||
0x1102 => Some(Self::ComposerUnbindScene),
|
|
||||||
0x1103 => Some(Self::ComposerSetCamera),
|
|
||||||
0x1104 => Some(Self::ComposerEmitSprite),
|
|
||||||
0x3001 => Some(Self::AudioPlaySample),
|
0x3001 => Some(Self::AudioPlaySample),
|
||||||
0x3002 => Some(Self::AudioPlay),
|
0x3002 => Some(Self::AudioPlay),
|
||||||
0x4001 => Some(Self::FsOpen),
|
0x4001 => Some(Self::FsOpen),
|
||||||
@ -71,12 +68,9 @@ impl Syscall {
|
|||||||
Self::GfxDrawCircle => "GfxDrawCircle",
|
Self::GfxDrawCircle => "GfxDrawCircle",
|
||||||
Self::GfxDrawDisc => "GfxDrawDisc",
|
Self::GfxDrawDisc => "GfxDrawDisc",
|
||||||
Self::GfxDrawSquare => "GfxDrawSquare",
|
Self::GfxDrawSquare => "GfxDrawSquare",
|
||||||
|
Self::GfxSetSprite => "GfxSetSprite",
|
||||||
Self::GfxDrawText => "GfxDrawText",
|
Self::GfxDrawText => "GfxDrawText",
|
||||||
Self::GfxClear565 => "GfxClear565",
|
Self::GfxClear565 => "GfxClear565",
|
||||||
Self::ComposerBindScene => "ComposerBindScene",
|
|
||||||
Self::ComposerUnbindScene => "ComposerUnbindScene",
|
|
||||||
Self::ComposerSetCamera => "ComposerSetCamera",
|
|
||||||
Self::ComposerEmitSprite => "ComposerEmitSprite",
|
|
||||||
Self::AudioPlaySample => "AudioPlaySample",
|
Self::AudioPlaySample => "AudioPlaySample",
|
||||||
Self::AudioPlay => "AudioPlay",
|
Self::AudioPlay => "AudioPlay",
|
||||||
Self::FsOpen => "FsOpen",
|
Self::FsOpen => "FsOpen",
|
||||||
|
|||||||
@ -63,18 +63,6 @@ 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]
|
#[test]
|
||||||
fn resolver_enforces_capabilities() {
|
fn resolver_enforces_capabilities() {
|
||||||
let requested = [SyscallIdentity { module: "gfx", name: "clear", version: 1 }];
|
let requested = [SyscallIdentity { module: "gfx", name: "clear", version: 1 }];
|
||||||
@ -206,6 +194,10 @@ fn status_first_syscall_signatures_are_pinned() {
|
|||||||
assert_eq!(draw_square.arg_slots, 6);
|
assert_eq!(draw_square.arg_slots, 6);
|
||||||
assert_eq!(draw_square.ret_slots, 0);
|
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);
|
let draw_text = meta_for(Syscall::GfxDrawText);
|
||||||
assert_eq!(draw_text.arg_slots, 4);
|
assert_eq!(draw_text.arg_slots, 4);
|
||||||
assert_eq!(draw_text.ret_slots, 0);
|
assert_eq!(draw_text.ret_slots, 0);
|
||||||
@ -214,22 +206,6 @@ fn status_first_syscall_signatures_are_pinned() {
|
|||||||
assert_eq!(clear_565.arg_slots, 1);
|
assert_eq!(clear_565.arg_slots, 1);
|
||||||
assert_eq!(clear_565.ret_slots, 0);
|
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);
|
let audio_play_sample = meta_for(Syscall::AudioPlaySample);
|
||||||
assert_eq!(audio_play_sample.arg_slots, 5);
|
assert_eq!(audio_play_sample.arg_slots, 5);
|
||||||
assert_eq!(audio_play_sample.ret_slots, 1);
|
assert_eq!(audio_play_sample.ret_slots, 1);
|
||||||
@ -255,10 +231,10 @@ fn status_first_syscall_signatures_are_pinned() {
|
|||||||
fn declared_resolver_rejects_legacy_status_first_signatures() {
|
fn declared_resolver_rejects_legacy_status_first_signatures() {
|
||||||
let declared = vec![
|
let declared = vec![
|
||||||
prometeu_bytecode::SyscallDecl {
|
prometeu_bytecode::SyscallDecl {
|
||||||
module: "composer".into(),
|
module: "gfx".into(),
|
||||||
name: "bind_scene".into(),
|
name: "set_sprite".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 10,
|
||||||
ret_slots: 0,
|
ret_slots: 0,
|
||||||
},
|
},
|
||||||
prometeu_bytecode::SyscallDecl {
|
prometeu_bytecode::SyscallDecl {
|
||||||
@ -330,24 +306,10 @@ fn declared_resolver_rejects_legacy_status_first_signatures() {
|
|||||||
fn declared_resolver_accepts_mixed_status_first_surface_as_a_single_module() {
|
fn declared_resolver_accepts_mixed_status_first_surface_as_a_single_module() {
|
||||||
let declared = vec![
|
let declared = vec![
|
||||||
prometeu_bytecode::SyscallDecl {
|
prometeu_bytecode::SyscallDecl {
|
||||||
module: "composer".into(),
|
module: "gfx".into(),
|
||||||
name: "bind_scene".into(),
|
name: "set_sprite".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 10,
|
||||||
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,
|
ret_slots: 1,
|
||||||
},
|
},
|
||||||
prometeu_bytecode::SyscallDecl {
|
prometeu_bytecode::SyscallDecl {
|
||||||
@ -380,10 +342,8 @@ fn declared_resolver_accepts_mixed_status_first_surface_as_a_single_module() {
|
|||||||
assert_eq!(resolved.len(), declared.len());
|
assert_eq!(resolved.len(), declared.len());
|
||||||
assert_eq!(resolved[0].meta.ret_slots, 1);
|
assert_eq!(resolved[0].meta.ret_slots, 1);
|
||||||
assert_eq!(resolved[1].meta.ret_slots, 1);
|
assert_eq!(resolved[1].meta.ret_slots, 1);
|
||||||
assert_eq!(resolved[2].meta.ret_slots, 1);
|
assert_eq!(resolved[2].meta.ret_slots, 2);
|
||||||
assert_eq!(resolved[3].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]
|
#[test]
|
||||||
|
|||||||
@ -10,8 +10,8 @@ use prometeu_hal::sprite::Sprite;
|
|||||||
use prometeu_hal::syscalls::Syscall;
|
use prometeu_hal::syscalls::Syscall;
|
||||||
use prometeu_hal::vm_fault::VmFault;
|
use prometeu_hal::vm_fault::VmFault;
|
||||||
use prometeu_hal::{
|
use prometeu_hal::{
|
||||||
AudioOpStatus, ComposerOpStatus, HostContext, HostReturn, NativeInterface, SyscallId,
|
AudioOpStatus, GfxOpStatus, HostContext, HostReturn, NativeInterface, SyscallId, expect_bool,
|
||||||
expect_bool, expect_int,
|
expect_int,
|
||||||
};
|
};
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
@ -56,23 +56,6 @@ impl VirtualMachineRuntime {
|
|||||||
pub(crate) fn get_color(&self, value: i64) -> Color {
|
pub(crate) fn get_color(&self, value: i64) -> Color {
|
||||||
Color::from_raw(value as u16)
|
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 {
|
impl NativeInterface for VirtualMachineRuntime {
|
||||||
@ -152,6 +135,46 @@ impl NativeInterface for VirtualMachineRuntime {
|
|||||||
hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color);
|
hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
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 => {
|
Syscall::GfxDrawText => {
|
||||||
let x = expect_int(args, 0)? as i32;
|
let x = expect_int(args, 0)? as i32;
|
||||||
let y = expect_int(args, 1)? as i32;
|
let y = expect_int(args, 1)? as i32;
|
||||||
@ -168,100 +191,6 @@ impl NativeInterface for VirtualMachineRuntime {
|
|||||||
hw.gfx_mut().clear(Color::from_raw(color_val as u16));
|
hw.gfx_mut().clear(Color::from_raw(color_val as u16));
|
||||||
Ok(())
|
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 => {
|
Syscall::AudioPlaySample => {
|
||||||
let sample_id_raw = expect_int(args, 0)?;
|
let sample_id_raw = expect_int(args, 0)?;
|
||||||
let voice_id_raw = expect_int(args, 1)?;
|
let voice_id_raw = expect_int(args, 1)?;
|
||||||
|
|||||||
@ -5,25 +5,17 @@ use prometeu_bytecode::Value;
|
|||||||
use prometeu_bytecode::assembler::assemble;
|
use prometeu_bytecode::assembler::assemble;
|
||||||
use prometeu_bytecode::model::{BytecodeModule, ConstantPoolEntry, FunctionMeta, SyscallDecl};
|
use prometeu_bytecode::model::{BytecodeModule, ConstantPoolEntry, FunctionMeta, SyscallDecl};
|
||||||
use prometeu_drivers::hardware::Hardware;
|
use prometeu_drivers::hardware::Hardware;
|
||||||
use prometeu_drivers::{GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller};
|
|
||||||
use prometeu_hal::AudioOpStatus;
|
use prometeu_hal::AudioOpStatus;
|
||||||
use prometeu_hal::ComposerOpStatus;
|
use prometeu_hal::GfxOpStatus;
|
||||||
use prometeu_hal::InputSignals;
|
use prometeu_hal::InputSignals;
|
||||||
use prometeu_hal::asset::{
|
use prometeu_hal::asset::{
|
||||||
AssetCodec, AssetEntry, AssetLoadError, AssetOpStatus, BankType, LoadStatus,
|
AssetCodec, AssetEntry, AssetLoadError, AssetOpStatus, BankType, LoadStatus,
|
||||||
};
|
};
|
||||||
use prometeu_hal::cartridge::{AssetsPayloadSource, Cartridge};
|
use prometeu_hal::cartridge::{AssetsPayloadSource, Cartridge};
|
||||||
use prometeu_hal::color::Color;
|
use prometeu_hal::glyph_bank::GLYPH_BANK_PALETTE_COUNT_V1;
|
||||||
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::syscalls::caps;
|
||||||
use prometeu_hal::tile::Tile;
|
|
||||||
use prometeu_hal::tilemap::TileMap;
|
|
||||||
use prometeu_vm::VmInitError;
|
use prometeu_vm::VmInitError;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@ -137,40 +129,6 @@ fn test_glyph_asset_data() -> Vec<u8> {
|
|||||||
data
|
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]
|
#[test]
|
||||||
fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() {
|
fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() {
|
||||||
let mut runtime = VirtualMachineRuntime::new(None);
|
let mut runtime = VirtualMachineRuntime::new(None);
|
||||||
@ -275,174 +233,6 @@ fn tick_returns_panic_report_distinct_from_trap() {
|
|||||||
assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmPanic { .. })));
|
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]
|
#[test]
|
||||||
fn initialize_vm_success_clears_previous_crash_report() {
|
fn initialize_vm_success_clears_previous_crash_report() {
|
||||||
let mut runtime = VirtualMachineRuntime::new(None);
|
let mut runtime = VirtualMachineRuntime::new(None);
|
||||||
@ -574,19 +364,22 @@ fn initialize_vm_failure_clears_previous_identity_and_handles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tick_composer_bind_scene_operational_error_returns_status_not_crash() {
|
fn tick_gfx_set_sprite_operational_error_returns_status_not_crash() {
|
||||||
let mut runtime = VirtualMachineRuntime::new(None);
|
let mut runtime = VirtualMachineRuntime::new(None);
|
||||||
let mut vm = VirtualMachine::default();
|
let mut vm = VirtualMachine::default();
|
||||||
let mut hardware = Hardware::new();
|
let mut hardware = Hardware::new();
|
||||||
let signals = InputSignals::default();
|
let signals = InputSignals::default();
|
||||||
let code = assemble("PUSH_I32 99\nHOSTCALL 0\nHALT").expect("assemble");
|
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 program = serialized_single_function_module(
|
let program = serialized_single_function_module(
|
||||||
code,
|
code,
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "composer".into(),
|
module: "gfx".into(),
|
||||||
name: "bind_scene".into(),
|
name: "set_sprite".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 10,
|
||||||
ret_slots: 1,
|
ret_slots: 1,
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
@ -596,29 +389,26 @@ fn tick_composer_bind_scene_operational_error_returns_status_not_crash() {
|
|||||||
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
||||||
assert!(report.is_none(), "operational error must not crash");
|
assert!(report.is_none(), "operational error must not crash");
|
||||||
assert!(vm.is_halted());
|
assert!(vm.is_halted());
|
||||||
assert_eq!(
|
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::BankInvalid as i64)]);
|
||||||
vm.operand_stack_top(1),
|
|
||||||
vec![Value::Int64(ComposerOpStatus::SceneUnavailable as i64)]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tick_composer_emit_sprite_operational_error_returns_status_not_crash() {
|
fn tick_gfx_set_sprite_invalid_index_returns_status_not_crash() {
|
||||||
let mut runtime = VirtualMachineRuntime::new(None);
|
let mut runtime = VirtualMachineRuntime::new(None);
|
||||||
let mut vm = VirtualMachine::default();
|
let mut vm = VirtualMachine::default();
|
||||||
let mut hardware = Hardware::new();
|
let mut hardware = Hardware::new();
|
||||||
let signals = InputSignals::default();
|
let signals = InputSignals::default();
|
||||||
let code = assemble(
|
let code = assemble(
|
||||||
"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",
|
"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",
|
||||||
)
|
)
|
||||||
.expect("assemble");
|
.expect("assemble");
|
||||||
let program = serialized_single_function_module(
|
let program = serialized_single_function_module(
|
||||||
code,
|
code,
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "composer".into(),
|
module: "gfx".into(),
|
||||||
name: "emit_sprite".into(),
|
name: "set_sprite".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 9,
|
arg_slots: 10,
|
||||||
ret_slots: 1,
|
ret_slots: 1,
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
@ -626,57 +416,28 @@ fn tick_composer_emit_sprite_operational_error_returns_status_not_crash() {
|
|||||||
|
|
||||||
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
||||||
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
||||||
assert!(report.is_none(), "operational error must not crash");
|
assert!(report.is_none(), "invalid sprite index must not crash");
|
||||||
assert!(vm.is_halted());
|
assert!(vm.is_halted());
|
||||||
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::BankInvalid as i64)]);
|
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::InvalidSpriteIndex as i64)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tick_composer_emit_sprite_invalid_layer_returns_status_not_crash() {
|
fn tick_gfx_set_sprite_invalid_range_returns_status_not_crash() {
|
||||||
let mut runtime = VirtualMachineRuntime::new(None);
|
let mut runtime = VirtualMachineRuntime::new(None);
|
||||||
let mut vm = VirtualMachine::default();
|
let mut vm = VirtualMachine::default();
|
||||||
let mut hardware = Hardware::new();
|
let mut hardware = Hardware::new();
|
||||||
let signals = InputSignals::default();
|
let signals = InputSignals::default();
|
||||||
let code = assemble(
|
let code = assemble(
|
||||||
"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",
|
"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",
|
||||||
)
|
)
|
||||||
.expect("assemble");
|
.expect("assemble");
|
||||||
let program = serialized_single_function_module(
|
let program = serialized_single_function_module(
|
||||||
code,
|
code,
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "composer".into(),
|
module: "gfx".into(),
|
||||||
name: "emit_sprite".into(),
|
name: "set_sprite".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 9,
|
arg_slots: 10,
|
||||||
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,
|
ret_slots: 1,
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
@ -691,12 +452,9 @@ fn tick_composer_emit_sprite_invalid_range_returns_status_not_crash() {
|
|||||||
|
|
||||||
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
||||||
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
||||||
assert!(report.is_none(), "invalid composer parameter range must not crash");
|
assert!(report.is_none(), "invalid gfx parameter range must not crash");
|
||||||
assert!(vm.is_halted());
|
assert!(vm.is_halted());
|
||||||
assert_eq!(
|
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::ArgRangeInvalid as i64)]);
|
||||||
vm.operand_stack_top(1),
|
|
||||||
vec![Value::Int64(ComposerOpStatus::ArgRangeInvalid as i64)]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -1058,13 +816,13 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tick_status_first_surface_smoke_across_composer_audio_and_asset() {
|
fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() {
|
||||||
let mut runtime = VirtualMachineRuntime::new(None);
|
let mut runtime = VirtualMachineRuntime::new(None);
|
||||||
let mut vm = VirtualMachine::default();
|
let mut vm = VirtualMachine::default();
|
||||||
let mut hardware = Hardware::new();
|
let mut hardware = Hardware::new();
|
||||||
let signals = InputSignals::default();
|
let signals = InputSignals::default();
|
||||||
let code = assemble(
|
let code = assemble(
|
||||||
"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 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 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 1\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\
|
PUSH_I32 999\nPUSH_I32 0\nHOSTCALL 2\n\
|
||||||
HALT"
|
HALT"
|
||||||
@ -1074,10 +832,10 @@ fn tick_status_first_surface_smoke_across_composer_audio_and_asset() {
|
|||||||
code,
|
code,
|
||||||
vec![
|
vec![
|
||||||
SyscallDecl {
|
SyscallDecl {
|
||||||
module: "composer".into(),
|
module: "gfx".into(),
|
||||||
name: "emit_sprite".into(),
|
name: "set_sprite".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 9,
|
arg_slots: 10,
|
||||||
ret_slots: 1,
|
ret_slots: 1,
|
||||||
},
|
},
|
||||||
SyscallDecl {
|
SyscallDecl {
|
||||||
@ -1108,28 +866,28 @@ fn tick_status_first_surface_smoke_across_composer_audio_and_asset() {
|
|||||||
Value::Int64(0),
|
Value::Int64(0),
|
||||||
Value::Int64(AssetLoadError::AssetNotFound as i64),
|
Value::Int64(AssetLoadError::AssetNotFound as i64),
|
||||||
Value::Int64(AudioOpStatus::BankInvalid as i64),
|
Value::Int64(AudioOpStatus::BankInvalid as i64),
|
||||||
Value::Int64(ComposerOpStatus::BankInvalid as i64),
|
Value::Int64(GfxOpStatus::BankInvalid as i64),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tick_composer_emit_sprite_type_mismatch_surfaces_trap_not_panic() {
|
fn tick_gfx_set_sprite_type_mismatch_surfaces_trap_not_panic() {
|
||||||
let mut runtime = VirtualMachineRuntime::new(None);
|
let mut runtime = VirtualMachineRuntime::new(None);
|
||||||
let mut vm = VirtualMachine::default();
|
let mut vm = VirtualMachine::default();
|
||||||
let mut hardware = Hardware::new();
|
let mut hardware = Hardware::new();
|
||||||
let signals = InputSignals::default();
|
let signals = InputSignals::default();
|
||||||
let code = assemble(
|
let code = assemble(
|
||||||
"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",
|
"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",
|
||||||
)
|
)
|
||||||
.expect("assemble");
|
.expect("assemble");
|
||||||
let program = serialized_single_function_module(
|
let program = serialized_single_function_module(
|
||||||
code,
|
code,
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "composer".into(),
|
module: "gfx".into(),
|
||||||
name: "emit_sprite".into(),
|
name: "set_sprite".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 9,
|
arg_slots: 10,
|
||||||
ret_slots: 1,
|
ret_slots: 1,
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -145,7 +145,7 @@ impl VirtualMachineRuntime {
|
|||||||
if run.reason == LogicalFrameEndingReason::FrameSync
|
if run.reason == LogicalFrameEndingReason::FrameSync
|
||||||
|| run.reason == LogicalFrameEndingReason::EndOfRom
|
|| run.reason == LogicalFrameEndingReason::EndOfRom
|
||||||
{
|
{
|
||||||
hw.render_frame();
|
hw.gfx_mut().render_all();
|
||||||
|
|
||||||
// 1. Snapshot full telemetry at logical frame end
|
// 1. Snapshot full telemetry at logical frame end
|
||||||
let (glyph_bank, sound_bank) = Self::bank_telemetry_summary(hw);
|
let (glyph_bank, sound_bank) = Self::bank_telemetry_summary(hw);
|
||||||
@ -250,7 +250,6 @@ impl VirtualMachineRuntime {
|
|||||||
_signals: &InputSignals,
|
_signals: &InputSignals,
|
||||||
hw: &mut dyn HardwareBridge,
|
hw: &mut dyn HardwareBridge,
|
||||||
) {
|
) {
|
||||||
hw.begin_frame();
|
|
||||||
hw.audio_mut().clear_commands();
|
hw.audio_mut().clear_commands();
|
||||||
self.logs_written_this_frame.clear();
|
self.logs_written_this_frame.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2567,8 +2567,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_status_first_syscall_results_count_mismatch_panic() {
|
fn test_status_first_syscall_results_count_mismatch_panic() {
|
||||||
// ComposerBindScene (0x1101) expects 1 result.
|
// GfxSetSprite (0x1007) expects 1 result.
|
||||||
let code = assemble("PUSH_I32 0\nSYSCALL 0x1101").expect("assemble");
|
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");
|
||||||
|
|
||||||
struct BadNativeNoReturn;
|
struct BadNativeNoReturn;
|
||||||
impl NativeInterface for BadNativeNoReturn {
|
impl NativeInterface for BadNativeNoReturn {
|
||||||
@ -2918,24 +2921,10 @@ mod tests {
|
|||||||
fn test_loader_patching_accepts_status_first_signatures() {
|
fn test_loader_patching_accepts_status_first_signatures() {
|
||||||
let cases = vec![
|
let cases = vec![
|
||||||
SyscallDecl {
|
SyscallDecl {
|
||||||
module: "composer".into(),
|
module: "gfx".into(),
|
||||||
name: "bind_scene".into(),
|
name: "set_sprite".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 10,
|
||||||
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,
|
ret_slots: 1,
|
||||||
},
|
},
|
||||||
SyscallDecl {
|
SyscallDecl {
|
||||||
@ -2988,10 +2977,10 @@ mod tests {
|
|||||||
fn test_loader_patching_rejects_legacy_status_first_ret_slots() {
|
fn test_loader_patching_rejects_legacy_status_first_ret_slots() {
|
||||||
let cases = vec![
|
let cases = vec![
|
||||||
SyscallDecl {
|
SyscallDecl {
|
||||||
module: "composer".into(),
|
module: "gfx".into(),
|
||||||
name: "bind_scene".into(),
|
name: "set_sprite".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 10,
|
||||||
ret_slots: 0,
|
ret_slots: 0,
|
||||||
},
|
},
|
||||||
SyscallDecl {
|
SyscallDecl {
|
||||||
|
|||||||
@ -5,6 +5,4 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
prometeu-bytecode = { path = "../../console/prometeu-bytecode" }
|
prometeu-bytecode = { path = "../../console/prometeu-bytecode" }
|
||||||
prometeu-hal = { path = "../../console/prometeu-hal" }
|
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
serde_json = "1"
|
|
||||||
|
|||||||
@ -3,25 +3,7 @@ use prometeu_bytecode::assembler::assemble;
|
|||||||
use prometeu_bytecode::model::{
|
use prometeu_bytecode::model::{
|
||||||
BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta, SyscallDecl,
|
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::fs;
|
||||||
use std::mem::size_of;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
fn asm(s: &str) -> Vec<u8> {
|
fn asm(s: &str) -> Vec<u8> {
|
||||||
@ -38,6 +20,13 @@ pub fn generate() -> Result<()> {
|
|||||||
arg_slots: 1,
|
arg_slots: 1,
|
||||||
ret_slots: 0,
|
ret_slots: 0,
|
||||||
},
|
},
|
||||||
|
SyscallDecl {
|
||||||
|
module: "gfx".into(),
|
||||||
|
name: "draw_disc".into(),
|
||||||
|
version: 1,
|
||||||
|
arg_slots: 5,
|
||||||
|
ret_slots: 0,
|
||||||
|
},
|
||||||
SyscallDecl {
|
SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "gfx".into(),
|
||||||
name: "draw_text".into(),
|
name: "draw_text".into(),
|
||||||
@ -53,24 +42,10 @@ pub fn generate() -> Result<()> {
|
|||||||
ret_slots: 0,
|
ret_slots: 0,
|
||||||
},
|
},
|
||||||
SyscallDecl {
|
SyscallDecl {
|
||||||
module: "composer".into(),
|
module: "gfx".into(),
|
||||||
name: "bind_scene".into(),
|
name: "set_sprite".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 1,
|
arg_slots: 10,
|
||||||
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,
|
ret_slots: 1,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -84,7 +59,7 @@ pub fn generate() -> Result<()> {
|
|||||||
param_slots: 0,
|
param_slots: 0,
|
||||||
local_slots: 2,
|
local_slots: 2,
|
||||||
return_slots: 0,
|
return_slots: 0,
|
||||||
max_stack_slots: 32,
|
max_stack_slots: 16,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
let module = BytecodeModule {
|
let module = BytecodeModule {
|
||||||
@ -92,8 +67,7 @@ pub fn generate() -> Result<()> {
|
|||||||
const_pool: vec![
|
const_pool: vec![
|
||||||
ConstantPoolEntry::String("stress".into()),
|
ConstantPoolEntry::String("stress".into()),
|
||||||
ConstantPoolEntry::String("frame".into()),
|
ConstantPoolEntry::String("frame".into()),
|
||||||
ConstantPoolEntry::String("overlay".into()),
|
ConstantPoolEntry::String("missing_glyph_bank".into()),
|
||||||
ConstantPoolEntry::String("composer".into()),
|
|
||||||
],
|
],
|
||||||
functions,
|
functions,
|
||||||
code: rom,
|
code: rom,
|
||||||
@ -115,332 +89,129 @@ pub fn generate() -> Result<()> {
|
|||||||
out_dir.push("stress-console");
|
out_dir.push("stress-console");
|
||||||
fs::create_dir_all(&out_dir)?;
|
fs::create_dir_all(&out_dir)?;
|
||||||
fs::write(out_dir.join("program.pbx"), bytes)?;
|
fs::write(out_dir.join("program.pbx"), bytes)?;
|
||||||
fs::write(out_dir.join("assets.pa"), build_assets_pack()?)?;
|
let assets_pa_path = out_dir.join("assets.pa");
|
||||||
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")?;
|
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")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
fn heavy_load(rom: &mut Vec<u8>) {
|
fn heavy_load(rom: &mut Vec<u8>) {
|
||||||
// Single function 0: main
|
// Single function 0: main
|
||||||
// Global 0 = frame counter
|
// Everything runs here — no coroutines, no SPAWN, no YIELD.
|
||||||
// Global 1 = scene bound flag
|
//
|
||||||
// Local 0 = sprite row
|
// Global 0 = t (frame counter)
|
||||||
// Local 1 = sprite col
|
// 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)
|
||||||
|
|
||||||
|
// --- 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"));
|
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 1\nADD\nSET_GLOBAL 0"));
|
||||||
|
|
||||||
rom.extend(asm("GET_GLOBAL 1\nPUSH_I32 0\nEQ"));
|
// --- clear screen ---
|
||||||
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"));
|
rom.extend(asm("PUSH_I32 0\nHOSTCALL 0"));
|
||||||
|
// --- call status-first syscall path once per frame and drop status ---
|
||||||
rom.extend(asm(
|
rom.extend(asm(
|
||||||
"GET_GLOBAL 0\nPUSH_I32 2\nMUL\nPUSH_I32 192\nMOD\nGET_GLOBAL 0\nPUSH_I32 76\nMOD\nHOSTCALL 4",
|
"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",
|
||||||
));
|
));
|
||||||
|
|
||||||
rom.extend(asm("PUSH_I32 0\nSET_LOCAL 0"));
|
// --- draw 500 discs ---
|
||||||
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"));
|
|
||||||
|
|
||||||
rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1"));
|
rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1"));
|
||||||
let col_loop_start = rom.len() as u32;
|
let disc_loop_start = rom.len() as u32;
|
||||||
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 32\nLT"));
|
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 500\nLT"));
|
||||||
let jif_col_end_offset = rom.len() + 2;
|
let jif_disc_end_offset = rom.len() + 2;
|
||||||
rom.extend(asm("JMP_IF_FALSE 0"));
|
rom.extend(asm("JMP_IF_FALSE 0"));
|
||||||
|
|
||||||
rom.extend(asm(
|
// x = (t * (i+7) + i * 13) % 320
|
||||||
"PUSH_I32 0\n\
|
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"));
|
||||||
GET_LOCAL 0\nPUSH_I32 32\nMUL\nGET_LOCAL 1\nADD\nGET_GLOBAL 0\nADD\nPUSH_I32 15\nMOD\nPUSH_I32 1\nADD\n\
|
// y = (t * (i+11) + i * 17) % 180
|
||||||
GET_LOCAL 1\nPUSH_I32 10\nMUL\nGET_GLOBAL 0\nPUSH_I32 10\nMOD\nADD\nPUSH_I32 320\nMOD\n\
|
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"));
|
||||||
GET_LOCAL 0\nPUSH_I32 10\nMUL\nGET_GLOBAL 0\nPUSH_I32 2\nMUL\nPUSH_I32 10\nMOD\nADD\nPUSH_I32 180\nMOD\n\
|
// r = ( (i*13) % 20 ) + 5
|
||||||
GET_LOCAL 0\nGET_LOCAL 1\nADD\nGET_GLOBAL 0\nADD\nPUSH_I32 4\nMOD\n\
|
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 13\nMUL\nPUSH_I32 20\nMOD\nPUSH_I32 5\nADD"));
|
||||||
PUSH_I32 0\n\
|
// border color = (i * 1234) & 0xFFFF
|
||||||
GET_LOCAL 1\nGET_GLOBAL 0\nADD\nPUSH_I32 1\nBIT_AND\nPUSH_I32 0\nNEQ\n\
|
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1234\nMUL\nPUSH_I32 65535\nBIT_AND"));
|
||||||
GET_LOCAL 0\nGET_GLOBAL 0\nADD\nPUSH_I32 1\nBIT_AND\nPUSH_I32 0\nNEQ\n\
|
// fill color = (i * 5678 + t) & 0xFFFF
|
||||||
GET_LOCAL 0\nGET_LOCAL 1\nADD\nPUSH_I32 4\nMOD\n\
|
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 5678\nMUL\nGET_GLOBAL 0\nADD\nPUSH_I32 65535\nBIT_AND"));
|
||||||
HOSTCALL 5\nPOP_N 1",
|
// 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"));
|
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1"));
|
||||||
let jmp_col_loop_offset = rom.len() + 2;
|
let jmp_disc_loop_offset = rom.len() + 2;
|
||||||
rom.extend(asm("JMP 0"));
|
rom.extend(asm("JMP 0"));
|
||||||
let col_loop_end = rom.len() as u32;
|
let disc_loop_end = rom.len() as u32;
|
||||||
|
|
||||||
rom.extend(asm("GET_LOCAL 0\nPUSH_I32 1\nADD\nSET_LOCAL 0"));
|
// --- draw 20 texts ---
|
||||||
let jmp_row_loop_offset = rom.len() + 2;
|
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;
|
||||||
|
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",
|
||||||
|
));
|
||||||
|
// 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"));
|
rom.extend(asm("JMP 0"));
|
||||||
let row_loop_end = rom.len() as u32;
|
let text_alt_target = rom.len() as u32;
|
||||||
|
rom.extend(asm("PUSH_CONST 1")); // "frame"
|
||||||
|
let text_join_target = rom.len() as u32;
|
||||||
|
|
||||||
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 3\nMUL\nPUSH_I32 220\nMOD\n\
|
// color = (t * 10 + i * 1000) & 0xFFFF
|
||||||
PUSH_I32 8\n\
|
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 10\nMUL\nGET_LOCAL 1\nPUSH_I32 1000\nMUL\nADD\nPUSH_I32 65535\nBIT_AND"));
|
||||||
PUSH_CONST 0\n\
|
// HOSTCALL gfx.draw_text (x, y, str, color)
|
||||||
GET_GLOBAL 0\nPUSH_I32 2047\nMUL\nPUSH_I32 65535\nBIT_AND\n\
|
rom.extend(asm("HOSTCALL 2"));
|
||||||
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"));
|
|
||||||
|
|
||||||
|
// i++
|
||||||
|
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1"));
|
||||||
|
let jmp_text_loop_offset = rom.len() + 2;
|
||||||
|
rom.extend(asm("JMP 0"));
|
||||||
|
let text_loop_end = rom.len() as u32;
|
||||||
|
|
||||||
|
// --- log every 60 frames ---
|
||||||
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 60\nMOD\nPUSH_I32 0\nEQ"));
|
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 60\nMOD\nPUSH_I32 0\nEQ"));
|
||||||
let jif_log_offset = rom.len() + 2;
|
let jif_log_offset = rom.len() + 2;
|
||||||
rom.extend(asm("JMP_IF_FALSE 0"));
|
rom.extend(asm("JMP_IF_FALSE 0"));
|
||||||
rom.extend(asm("PUSH_I32 2\nPUSH_CONST 0\nHOSTCALL 2"));
|
rom.extend(asm("PUSH_I32 2\nPUSH_CONST 0\nHOSTCALL 3"));
|
||||||
let after_log = rom.len() as u32;
|
let after_log = rom.len() as u32;
|
||||||
|
|
||||||
|
// --- end of function ---
|
||||||
rom.extend(asm("FRAME_SYNC\nRET"));
|
rom.extend(asm("FRAME_SYNC\nRET"));
|
||||||
|
|
||||||
|
// --- Patch jump targets ---
|
||||||
let patch = |buf: &mut Vec<u8>, imm_offset: usize, target: u32| {
|
let patch = |buf: &mut Vec<u8>, imm_offset: usize, target: u32| {
|
||||||
buf[imm_offset..imm_offset + 4].copy_from_slice(&target.to_le_bytes());
|
buf[imm_offset..imm_offset + 4].copy_from_slice(&target.to_le_bytes());
|
||||||
};
|
};
|
||||||
|
|
||||||
patch(rom, jif_bind_done_offset, bind_done_target);
|
patch(rom, jif_disc_end_offset, disc_loop_end);
|
||||||
patch(rom, jif_row_end_offset, row_loop_end);
|
patch(rom, jmp_disc_loop_offset, disc_loop_start);
|
||||||
patch(rom, jif_col_end_offset, col_loop_end);
|
|
||||||
patch(rom, jmp_col_loop_offset, col_loop_start);
|
patch(rom, jif_text_end_offset, text_loop_end);
|
||||||
patch(rom, jmp_row_loop_offset, row_loop_start);
|
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_log_offset, after_log);
|
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":29,"AGD":29,"DEC":17,"PLN":30,"LSN":31,"CLSN":1}}
|
{"type":"meta","next_id":{"DSC":27,"AGD":27,"DEC":15,"PLN":22,"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-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-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"}]}
|
{"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,8 +19,6 @@
|
|||||||
{"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-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-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-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-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-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"}]}
|
{"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"}]}
|
||||||
|
|||||||
@ -1,214 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
@ -1,140 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
@ -1,166 +0,0 @@
|
|||||||
---
|
|
||||||
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`.
|
|
||||||
@ -1,150 +0,0 @@
|
|||||||
---
|
|
||||||
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,7 +26,6 @@ Introduce `FrameComposer` as a first-class hardware-side subsystem and move cano
|
|||||||
- optional cache;
|
- optional cache;
|
||||||
- optional resolver;
|
- optional resolver;
|
||||||
- owned `SpriteController`.
|
- 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`.
|
- Aggregate `FrameComposer` inside `Hardware`.
|
||||||
- Expose the minimum driver-facing surface required for subsequent plans.
|
- Expose the minimum driver-facing surface required for subsequent plans.
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,6 @@ 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`.
|
`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`.
|
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
|
## Scope
|
||||||
|
|
||||||
@ -93,7 +92,6 @@ Align cache/resolver lifetime with the active scene contract.
|
|||||||
- On unbind:
|
- On unbind:
|
||||||
- discard cache/resolver and invalidate the world path.
|
- 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 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):**
|
**File(s):**
|
||||||
- `crates/console/prometeu-drivers/src/frame_composer.rs`
|
- `crates/console/prometeu-drivers/src/frame_composer.rs`
|
||||||
@ -106,7 +104,6 @@ Align cache/resolver lifetime with the active scene contract.
|
|||||||
- scene status reflects no-scene and active-scene states.
|
- scene status reflects no-scene and active-scene states.
|
||||||
- camera coordinates are stored as top-left pixel-space values.
|
- camera coordinates are stored as top-left pixel-space values.
|
||||||
- bind/unbind remains valid for scenes whose layers use `8x8` tiles.
|
- 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
|
### Integration Tests
|
||||||
- `FrameComposer` can resolve a scene from the pool and survive no-scene operation.
|
- `FrameComposer` can resolve a scene from the pool and survive no-scene operation.
|
||||||
@ -122,7 +119,6 @@ Align cache/resolver lifetime with the active scene contract.
|
|||||||
- [ ] Camera contract is implemented as `i32` top-left viewport coordinates.
|
- [ ] Camera contract is implemented as `i32` top-left viewport coordinates.
|
||||||
- [ ] Cache/resolver lifetime follows scene bind/unbind.
|
- [ ] Cache/resolver lifetime follows scene bind/unbind.
|
||||||
- [ ] Scene bind/cache/resolver setup preserves canonical per-layer tile sizes, including `8x8`.
|
- [ ] Scene bind/cache/resolver setup preserves canonical per-layer tile sizes, including `8x8`.
|
||||||
- [ ] Scene-layer camera multiplier naming is aligned on `parallax_factor`.
|
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,6 @@ 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` 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.
|
`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
|
## Scope
|
||||||
|
|
||||||
@ -44,7 +43,6 @@ Move cache-refresh orchestration fully into `FrameComposer`.
|
|||||||
- apply them to `SceneViewportCache`
|
- apply them to `SceneViewportCache`
|
||||||
- Keep `Gfx` unaware of refresh semantics.
|
- 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 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):**
|
**File(s):**
|
||||||
- `crates/console/prometeu-drivers/src/frame_composer.rs`
|
- `crates/console/prometeu-drivers/src/frame_composer.rs`
|
||||||
@ -105,7 +103,6 @@ Protect the two canonical frame modes.
|
|||||||
- `render_frame()` with a scene applies resolver refreshes before composition.
|
- `render_frame()` with a scene applies resolver refreshes before composition.
|
||||||
- cache refresh requests are applied by `FrameComposer`, not `Gfx`.
|
- 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.
|
- `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
|
### Integration Tests
|
||||||
- scene bind + camera set + sprite emission + `render_frame()` produces the expected composed frame.
|
- scene bind + camera set + sprite emission + `render_frame()` produces the expected composed frame.
|
||||||
@ -122,7 +119,6 @@ Protect the two canonical frame modes.
|
|||||||
- [ ] No-scene `sprites + fades` behavior remains valid.
|
- [ ] No-scene `sprites + fades` behavior remains valid.
|
||||||
- [ ] `Gfx` remains backend-only for this path.
|
- [ ] `Gfx` remains backend-only for this path.
|
||||||
- [ ] The world path is explicitly covered for `8x8` scenes without `16x16`-specific assumptions.
|
- [ ] 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
|
## Dependencies
|
||||||
|
|
||||||
|
|||||||
@ -1,122 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
@ -1,112 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
---
|
|
||||||
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`.
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
---
|
|
||||||
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.*`.
|
|
||||||
@ -1,106 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
---
|
|
||||||
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,12 +48,10 @@ The GFX maintains two buffers:
|
|||||||
|
|
||||||
Per-frame flow:
|
Per-frame flow:
|
||||||
|
|
||||||
1. The system prepares the logical frame
|
1. The system draws to the back buffer
|
||||||
2. Canonical game composition is rendered into the back buffer
|
2. Calls `present()`
|
||||||
3. Deferred final overlay/debug primitives are drained on top of the completed game frame
|
3. Buffers are swapped
|
||||||
4. Calls `present()`
|
4. The host displays the front buffer
|
||||||
5. Buffers are swapped
|
|
||||||
6. The host displays the front buffer
|
|
||||||
|
|
||||||
This guarantees:
|
This guarantees:
|
||||||
|
|
||||||
@ -185,7 +183,7 @@ Access:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. Canonical Game Projection to the Back Buffer
|
## 10. Projection to the Back Buffer
|
||||||
|
|
||||||
For each frame:
|
For each frame:
|
||||||
|
|
||||||
@ -200,10 +198,6 @@ For each frame:
|
|||||||
|
|
||||||
3. Draw HUD layer last
|
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
|
## 11. Drawing Order and Priority
|
||||||
@ -220,15 +214,6 @@ Base order:
|
|||||||
4. Tile Layer 3
|
4. Tile Layer 3
|
||||||
5. Sprites (by priority between layers)
|
5. Sprites (by priority between layers)
|
||||||
6. HUD Layer
|
6. HUD Layer
|
||||||
7. Scene Fade
|
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -273,9 +258,8 @@ Everything is:
|
|||||||
## 14. Where Blend is Applied
|
## 14. Where Blend is Applied
|
||||||
|
|
||||||
- Blending occurs during drawing
|
- Blending occurs during drawing
|
||||||
- For canonical game composition, the result goes to the back buffer during composition
|
- The result goes directly to the back buffer
|
||||||
- For deferred `gfx.*` overlay/debug primitives, the result is applied during the final overlay/debug drain stage
|
- There is no automatic post-composition
|
||||||
- There is no automatic GPU-style post-processing pipeline
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -312,8 +296,6 @@ controls:
|
|||||||
- **Scene Fade**: affects the entire scene (Tile Layers 0–3 + Sprites)
|
- **Scene Fade**: affects the entire scene (Tile Layers 0–3 + Sprites)
|
||||||
- **HUD Fade**: affects only the HUD Layer (always composed last)
|
- **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.
|
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
|
It uses a **discrete integer level** (0..31), which in practice produces an
|
||||||
"almost continuous" visual result in 320×180 pixel art.
|
"almost continuous" visual result in 320×180 pixel art.
|
||||||
@ -554,12 +536,7 @@ The system can measure:
|
|||||||
|
|
||||||
## 19. Syscall Return and Fault Policy
|
## 19. Syscall Return and Fault Policy
|
||||||
|
|
||||||
Graphics-related public ABI in v1 is split between:
|
`gfx` follows status-first policy for operations with operational failure modes.
|
||||||
|
|
||||||
- `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:
|
Fault boundary:
|
||||||
|
|
||||||
@ -567,62 +544,50 @@ Fault boundary:
|
|||||||
- `status`: operational failure;
|
- `status`: operational failure;
|
||||||
- `Panic`: internal runtime invariant break only.
|
- `Panic`: internal runtime invariant break only.
|
||||||
|
|
||||||
### 19.1 Return-shape matrix in v1
|
### 19.1 `gfx.set_sprite`
|
||||||
|
|
||||||
|
Return-shape matrix in v1:
|
||||||
|
|
||||||
| Syscall | Return | Policy basis |
|
| Syscall | Return | Policy basis |
|
||||||
| ----------------------- | ------------- | --------------------------------------------------- |
|
| ------------------ | ------------- | ---------------------------------------------------- |
|
||||||
| `gfx.clear` | `void` | no real operational failure path in v1 |
|
| `gfx.clear` | `void` | no real operational failure path in v1 |
|
||||||
| `gfx.fill_rect` | `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_line` | `void` | no real operational failure path in v1 |
|
||||||
| `gfx.draw_circle` | `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_disc` | `void` | no real operational failure path in v1 |
|
||||||
| `gfx.draw_square` | `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.draw_text` | `void` | no real operational failure path in v1 |
|
||||||
| `gfx.clear_565` | `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 |
|
|
||||||
|
|
||||||
### 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;
|
`gfx.set_sprite` returns `status:int`.
|
||||||
- screen-space and pipeline-agnostic relative to `composer.*`;
|
|
||||||
- outside `FrameComposer`;
|
|
||||||
- above scene, sprites, and canonical HUD;
|
|
||||||
- drained after `hud_fade`.
|
|
||||||
|
|
||||||
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:
|
ABI:
|
||||||
1. `glyph_id: int` — glyph index within the bank
|
1. `bank_id: int` — index of the tile bank
|
||||||
2. `palette_id: int` — palette index
|
2. `index: int` — sprite index (0..511)
|
||||||
3. `x: int` — x coordinate
|
3. `x: int` — x coordinate
|
||||||
4. `y: int` — y coordinate
|
4. `y: int` — y coordinate
|
||||||
5. `layer: int` — composition layer reference
|
5. `tile_id: int` — tile index within the bank
|
||||||
6. `bank_id: int` — glyph bank index
|
6. `palette_id: int` — palette index (0..63)
|
||||||
7. `flip_x: bool` — horizontal flip
|
7. `active: bool` — visibility toggle
|
||||||
8. `flip_y: bool` — vertical flip
|
8. `flip_x: bool` — horizontal flip
|
||||||
9. `priority: int` — within-layer ordering priority
|
9. `flip_y: bool` — vertical flip
|
||||||
|
10. `priority: int` — layer priority (0..4)
|
||||||
|
|
||||||
Minimum status table:
|
Minimum status table:
|
||||||
|
|
||||||
- `0` = `OK`
|
- `0` = `OK`
|
||||||
- `1` = `SCENE_UNAVAILABLE`
|
- `2` = `INVALID_SPRITE_INDEX`
|
||||||
- `2` = `INVALID_ARG_RANGE`
|
- `3` = `INVALID_ARG_RANGE`
|
||||||
- `3` = `BANK_INVALID`
|
- `4` = `BANK_INVALID`
|
||||||
- `4` = `LAYER_INVALID`
|
|
||||||
- `5` = `SPRITE_OVERFLOW`
|
|
||||||
|
|
||||||
Operational notes:
|
Operational notes:
|
||||||
|
|
||||||
- the canonical public sprite contract is frame-emission based;
|
- no fallback to default bank when the sprite bank id cannot be resolved;
|
||||||
- no caller-provided sprite index exists in the v1 canonical ABI;
|
- no silent no-op for invalid index/range;
|
||||||
- no `active` flag exists in the v1 canonical ABI;
|
- `palette_id` and `priority` must be validated against runtime-supported ranges.
|
||||||
- overflow remains non-fatal and must not escalate to trap in v1.
|
|
||||||
|
|||||||
@ -39,7 +39,6 @@ Example:
|
|||||||
```
|
```
|
||||||
("gfx", "present", 1)
|
("gfx", "present", 1)
|
||||||
("audio", "play", 2)
|
("audio", "play", 2)
|
||||||
("composer", "emit_sprite", 1)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This identity is:
|
This identity is:
|
||||||
@ -199,24 +198,6 @@ For `asset.load`:
|
|||||||
- `slot` is the target slot index;
|
- `slot` is the target slot index;
|
||||||
- bank kind is resolved from `asset_table` by `asset_id`, not supplied by the caller.
|
- 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)
|
## 7 Syscalls as Callable Entities (Not First-Class)
|
||||||
|
|
||||||
Syscalls behave like call sites, not like first-class guest values.
|
Syscalls behave like call sites, not like first-class guest values.
|
||||||
|
|||||||
@ -85,9 +85,6 @@ Example:
|
|||||||
- `asset.load` currently resolves with `arg_slots = 2` and `ret_slots = 2`.
|
- `asset.load` currently resolves with `arg_slots = 2` and `ret_slots = 2`.
|
||||||
- The canonical stack contract is `asset_id, slot -> status, handle`.
|
- 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`.
|
- 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
|
#### Canonical Intrinsic Registry Artifact
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@ -5,5 +5,5 @@
|
|||||||
"title": "Stress Console",
|
"title": "Stress Console",
|
||||||
"app_version": "0.1.0",
|
"app_version": "0.1.0",
|
||||||
"app_mode": "Game",
|
"app_mode": "Game",
|
||||||
"capabilities": ["gfx", "log", "asset"]
|
"capabilities": ["gfx", "log"]
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user